summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp1
-rw-r--r--BROADCASTS_OWNERS4
-rw-r--r--android-sdk-flags/Android.bp30
-rw-r--r--android-sdk-flags/flags.aconfig12
-rw-r--r--apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java2
-rw-r--r--apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java2
-rw-r--r--apex/blobstore/OWNERS5
-rw-r--r--apex/jobscheduler/framework/aconfig/job.aconfig7
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl8
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobParameters.java123
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java9
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java36
-rw-r--r--core/java/android/app/ActivityManager.java36
-rw-r--r--core/java/android/app/activity_manager.aconfig11
-rw-r--r--core/java/android/app/admin/flags/flags.aconfig10
-rw-r--r--core/java/android/content/Intent.java15
-rw-r--r--core/java/android/hardware/biometrics/BiometricConstants.java6
-rw-r--r--core/java/android/hardware/biometrics/BiometricManager.java7
-rw-r--r--core/java/android/os/AppZygote.java5
-rw-r--r--core/java/android/view/View.java10
-rw-r--r--core/java/android/webkit/WebViewDelegate.java10
-rw-r--r--core/java/android/widget/ScrollView.java19
-rw-r--r--core/java/android/widget/flags/flags.aconfig11
-rw-r--r--core/java/android/window/TaskFragmentOrganizer.java17
-rw-r--r--core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java1
-rw-r--r--core/java/com/android/internal/widget/LockPatternView.java27
-rw-r--r--core/java/com/android/internal/widget/ViewGroupFader.java23
-rw-r--r--core/res/res/values-watch/config.xml4
-rw-r--r--core/res/res/values/config.xml4
-rw-r--r--core/res/res/values/symbols.xml2
-rw-r--r--core/tests/coretests/src/com/android/internal/widget/ViewGroupFaderTest.java107
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java121
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java4
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java9
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java19
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java26
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java14
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java18
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt8
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt10
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java31
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt2
-rw-r--r--libs/androidfw/AssetManager.cpp32
-rw-r--r--libs/androidfw/include/androidfw/AssetManager.h22
-rw-r--r--libs/androidfw/include/androidfw/Idmap.h5
-rw-r--r--libs/androidfw/include/androidfw/misc.h35
-rw-r--r--libs/androidfw/misc.cpp55
-rw-r--r--libs/androidfw/tests/Idmap_test.cpp11
-rw-r--r--packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java4
-rw-r--r--packages/SettingsLib/Metadata/Android.bp23
-rw-r--r--packages/SettingsLib/Metadata/AndroidManifest.xml6
-rw-r--r--packages/SettingsLib/Metadata/processor/Android.bp11
-rw-r--r--packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor1
-rw-r--r--packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt226
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt49
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt174
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt127
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt204
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt29
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt157
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt95
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt40
-rw-r--r--packages/SettingsLib/Preference/Android.bp23
-rw-r--r--packages/SettingsLib/Preference/AndroidManifest.xml6
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt118
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt49
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt86
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt60
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt109
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt55
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt200
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt106
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt39
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt22
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt17
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt9
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt21
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt179
-rw-r--r--packages/SystemUI/Android.bp1
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig20
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt6
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt16
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt12
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt52
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt32
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt54
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt274
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt128
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt109
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt53
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java3
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt126
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java34
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt116
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java78
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java399
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java78
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt113
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt4
-rw-r--r--packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json831
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt348
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java155
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt278
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java19
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt26
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt33
-rw-r--r--packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java14
-rw-r--r--ravenwood/Android.bp3
-rw-r--r--ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java70
-rw-r--r--ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java66
-rw-r--r--ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java71
-rw-r--r--ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java78
-rw-r--r--ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java79
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java21
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java6
-rw-r--r--ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java288
-rw-r--r--ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java21
-rw-r--r--ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java10
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt8
-rw-r--r--services/appfunctions/TEST_MAPPING7
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java121
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java94
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java149
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java1
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java1
-rw-r--r--services/core/java/com/android/server/am/BatteryStatsService.java6
-rw-r--r--services/core/java/com/android/server/am/CachedAppOptimizer.java2
-rw-r--r--services/core/java/com/android/server/am/SettingsToPropertiesMapper.java2
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java5
-rw-r--r--services/core/java/com/android/server/biometrics/PreAuthInfo.java8
-rw-r--r--services/core/java/com/android/server/biometrics/Utils.java8
-rw-r--r--services/core/java/com/android/server/camera/CameraServiceProxy.java6
-rw-r--r--services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java6
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java4
-rw-r--r--services/core/java/com/android/server/input/debug/TouchpadDebugView.java56
-rw-r--r--services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java7
-rw-r--r--services/core/java/com/android/server/locksettings/LockSettingsService.java18
-rw-r--r--services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java9
-rw-r--r--services/core/java/com/android/server/notification/ManagedServices.java8
-rw-r--r--services/core/java/com/android/server/pm/InstallPackageHelper.java101
-rw-r--r--services/core/java/com/android/server/pm/PackageArchiver.java6
-rw-r--r--services/core/java/com/android/server/pm/PackageSetting.java12
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java29
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java17
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java3
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java29
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerService.java16
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java7
-rw-r--r--services/core/java/com/android/server/wm/DisplayPolicy.java15
-rw-r--r--services/core/java/com/android/server/wm/KeyguardController.java8
-rw-r--r--services/core/java/com/android/server/wm/RootWindowContainer.java4
-rw-r--r--services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java33
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerConstants.java2
-rw-r--r--services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt35
-rw-r--r--services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt125
-rw-r--r--services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt296
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java153
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java27
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java24
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java19
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java23
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java6
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java2
-rw-r--r--telecomm/java/com/android/internal/telecom/ITelecomService.aidl3
-rw-r--r--telephony/java/android/telephony/satellite/SatelliteManager.java11
-rw-r--r--tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java36
-rw-r--r--tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java25
-rw-r--r--tools/aapt/Package.cpp11
-rw-r--r--tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt27
-rw-r--r--tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt774
-rw-r--r--tools/lint/utils/README.md11
-rw-r--r--tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt4
-rw-r--r--tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt96
-rw-r--r--tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt191
-rwxr-xr-xtools/lint/utils/generate-exempt-aidl-interfaces.sh59
253 files changed, 9131 insertions, 1864 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index df4a3e5c3b35..bd17d6d2ece5 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -20,6 +20,7 @@ aconfig_declarations_group {
java_aconfig_libraries: [
// !!! KEEP THIS LIST ALPHABETICAL !!!
"aconfig_mediacodec_flags_java_lib",
+ "android-sdk-flags-java",
"android.adaptiveauth.flags-aconfig-java",
"android.app.appfunctions.flags-aconfig-java",
"android.app.contextualsearch.flags-aconfig-java",
diff --git a/BROADCASTS_OWNERS b/BROADCASTS_OWNERS
index 01f1f8a6ba57..f0cbe46ea402 100644
--- a/BROADCASTS_OWNERS
+++ b/BROADCASTS_OWNERS
@@ -1,5 +1,5 @@
# Bug component: 316181
-ctate@android.com
-jsharkey@google.com
+set noparent
+
sudheersai@google.com
yamasani@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/android-sdk-flags/Android.bp b/android-sdk-flags/Android.bp
new file mode 100644
index 000000000000..79a0b9a4f273
--- /dev/null
+++ b/android-sdk-flags/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+aconfig_declarations {
+ name: "android-sdk-flags",
+ package: "android.sdk",
+ container: "system",
+ srcs: ["flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "android-sdk-flags-java",
+ aconfig_declarations: "android-sdk-flags",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/android-sdk-flags/flags.aconfig b/android-sdk-flags/flags.aconfig
new file mode 100644
index 000000000000..cfe298e187d1
--- /dev/null
+++ b/android-sdk-flags/flags.aconfig
@@ -0,0 +1,12 @@
+package: "android.sdk"
+container: "system"
+
+flag {
+ name: "major_minor_versioning_scheme"
+ namespace: "android_sdk"
+ description: "Use the new SDK major.minor versioning scheme (e.g. Android 40.1) which replaces the old single-integer scheme (e.g. Android 15)."
+ bug: "350458259"
+
+ # Use is_fixed_read_only because DeviceConfig may not be available when Build.VERSION_CODES is first accessed
+ is_fixed_read_only: true
+}
diff --git a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java
index f20b1706129b..3577fcdf04d6 100644
--- a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java
+++ b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java
@@ -194,7 +194,7 @@ public final class ClientSocketPerfTest {
/**
* Simple benchmark for the amount of time to send a given number of messages
*/
- @Test
+ // @Test Temporarily disabled
@Parameters(method = "getParams")
public void time(Config config) throws Exception {
reset();
diff --git a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java
index af3c405eab82..ac5710047db9 100644
--- a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java
+++ b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java
@@ -198,7 +198,7 @@ public final class ServerSocketPerfTest {
executor.awaitTermination(5, TimeUnit.SECONDS);
}
- @Test
+ // @Test Temporarily disabled
@Parameters(method = "getParams")
public void throughput(Config config) throws Exception {
setup(config);
diff --git a/apex/blobstore/OWNERS b/apex/blobstore/OWNERS
index a53bbeaa8601..676cbc7eb2a3 100644
--- a/apex/blobstore/OWNERS
+++ b/apex/blobstore/OWNERS
@@ -1,2 +1,5 @@
+# Bug component: 25692
+set noparent
+
sudheersai@google.com
-yamasani@google.com
+yamasani@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig
index 80db264d0f44..5f5507587f72 100644
--- a/apex/jobscheduler/framework/aconfig/job.aconfig
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -23,3 +23,10 @@ flag {
description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content."
bug: "318731461"
}
+
+flag {
+ name: "cleanup_empty_jobs"
+ namespace: "backstage_power"
+ description: "Enables automatic cancellation of jobs due to leaked JobParameters, reducing unnecessary battery drain and improving system efficiency. This includes logging and traces for better issue diagnosis."
+ bug: "349688611"
+}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
index 96494ec28204..11d17ca749b7 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
@@ -85,6 +85,14 @@ interface IJobCallback {
*/
@UnsupportedAppUsage
void jobFinished(int jobId, boolean reschedule);
+
+ /*
+ * Inform JobScheduler to force finish this job because the client has lost
+ * the job handle. jobFinished can no longer be called from the client.
+ * @param jobId Unique integer used to identify this job
+ */
+ void forceJobFinished(int jobId);
+
/*
* Inform JobScheduler of a change in the estimated transfer payload.
*
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index e833bb95a302..52a761f8d486 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -34,15 +34,21 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.RemoteException;
+import android.system.SystemCleaner;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.Cleaner;
/**
* Contains the parameters used to configure/identify your job. You do not create this object
* yourself, instead it is handed in to your application by the System.
*/
public class JobParameters implements Parcelable {
+ private static final String TAG = "JobParameters";
/** @hide */
public static final int INTERNAL_STOP_REASON_UNKNOWN = -1;
@@ -306,6 +312,10 @@ public class JobParameters implements Parcelable {
private int mStopReason = STOP_REASON_UNDEFINED;
private int mInternalStopReason = INTERNAL_STOP_REASON_UNKNOWN;
private String debugStopReason; // Human readable stop reason for debugging.
+ @Nullable
+ private JobCleanupCallback mJobCleanupCallback;
+ @Nullable
+ private Cleaner.Cleanable mCleanable;
/** @hide */
public JobParameters(IBinder callback, String namespace, int jobId, PersistableBundle extras,
@@ -326,6 +336,8 @@ public class JobParameters implements Parcelable {
this.mTriggeredContentAuthorities = triggeredContentAuthorities;
this.mNetwork = network;
this.mJobNamespace = namespace;
+ this.mJobCleanupCallback = null;
+ this.mCleanable = null;
}
/**
@@ -597,6 +609,8 @@ public class JobParameters implements Parcelable {
mStopReason = in.readInt();
mInternalStopReason = in.readInt();
debugStopReason = in.readString();
+ mJobCleanupCallback = null;
+ mCleanable = null;
}
/** @hide */
@@ -612,6 +626,54 @@ public class JobParameters implements Parcelable {
this.debugStopReason = debugStopReason;
}
+ /** @hide */
+ public void initCleaner(JobCleanupCallback jobCleanupCallback) {
+ mJobCleanupCallback = jobCleanupCallback;
+ mCleanable = SystemCleaner.cleaner().register(this, mJobCleanupCallback);
+ }
+
+ /**
+ * Lazy initialize the cleaner and enable it
+ *
+ * @hide
+ */
+ public void enableCleaner() {
+ if (mJobCleanupCallback == null) {
+ initCleaner(new JobCleanupCallback(IJobCallback.Stub.asInterface(callback), jobId));
+ }
+ mJobCleanupCallback.enableCleaner();
+ }
+
+ /**
+ * Disable the cleaner from running and unregister it
+ *
+ * @hide
+ */
+ public void disableCleaner() {
+ if (mJobCleanupCallback != null) {
+ mJobCleanupCallback.disableCleaner();
+ if (mCleanable != null) {
+ mCleanable.clean();
+ mCleanable = null;
+ }
+ mJobCleanupCallback = null;
+ }
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ @Nullable
+ public Cleaner.Cleanable getCleanable() {
+ return mCleanable;
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ @Nullable
+ public JobCleanupCallback getJobCleanupCallback() {
+ return mJobCleanupCallback;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -647,6 +709,67 @@ public class JobParameters implements Parcelable {
dest.writeString(debugStopReason);
}
+ /**
+ * JobCleanupCallback is used track JobParameters leak. If the job is started
+ * and jobFinish is not called at the time of garbage collection of JobParameters
+ * instance, it is considered a job leak. Force finish the job.
+ *
+ * @hide
+ */
+ public static class JobCleanupCallback implements Runnable {
+ private final IJobCallback mCallback;
+ private final int mJobId;
+ private boolean mIsCleanerEnabled;
+
+ public JobCleanupCallback(
+ IJobCallback callback,
+ int jobId) {
+ mCallback = callback;
+ mJobId = jobId;
+ mIsCleanerEnabled = false;
+ }
+
+ /**
+ * Check if the cleaner is enabled
+ *
+ * @hide
+ */
+ public boolean isCleanerEnabled() {
+ return mIsCleanerEnabled;
+ }
+
+ /**
+ * Enable the cleaner to detect JobParameter leak
+ *
+ * @hide
+ */
+ public void enableCleaner() {
+ mIsCleanerEnabled = true;
+ }
+
+ /**
+ * Disable the cleaner from running.
+ *
+ * @hide
+ */
+ public void disableCleaner() {
+ mIsCleanerEnabled = false;
+ }
+
+ /** @hide */
+ @Override
+ public void run() {
+ if (!isCleanerEnabled()) {
+ return;
+ }
+ try {
+ mCallback.forceJobFinished(mJobId);
+ } catch (Exception e) {
+ Log.wtf(TAG, "Could not destroy running job", e);
+ }
+ }
+ }
+
public static final @android.annotation.NonNull Creator<JobParameters> CREATOR = new Creator<JobParameters>() {
@Override
public JobParameters createFromParcel(Parcel in) {
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
index 79d87edff9b2..5f80c52388b4 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
@@ -165,7 +165,13 @@ public abstract class JobServiceEngine {
case MSG_EXECUTE_JOB: {
final JobParameters params = (JobParameters) msg.obj;
try {
+ if (Flags.cleanupEmptyJobs()) {
+ params.enableCleaner();
+ }
boolean workOngoing = JobServiceEngine.this.onStartJob(params);
+ if (Flags.cleanupEmptyJobs() && !workOngoing) {
+ params.disableCleaner();
+ }
ackStartMessage(params, workOngoing);
} catch (Exception e) {
Log.e(TAG, "Error while executing job: " + params.getJobId());
@@ -190,6 +196,9 @@ public abstract class JobServiceEngine {
IJobCallback callback = params.getCallback();
if (callback != null) {
try {
+ if (Flags.cleanupEmptyJobs()) {
+ params.disableCleaner();
+ }
callback.jobFinished(params.getJobId(), needsReschedule);
} catch (RemoteException e) {
Log.e(TAG, "Error reporting job finish to system: binder has gone" +
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index be8e304a8101..ee246d84997f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -129,6 +129,8 @@ public final class JobServiceContext implements ServiceConnection {
private static final String[] VERB_STRINGS = {
"VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED"
};
+ private static final String TRACE_JOB_FORCE_FINISHED_PREFIX = "forceJobFinished:";
+ private static final String TRACE_JOB_FORCE_FINISHED_DELIMITER = "#";
// States that a job occupies while interacting with the client.
static final int VERB_BINDING = 0;
@@ -292,6 +294,11 @@ public final class JobServiceContext implements ServiceConnection {
}
@Override
+ public void forceJobFinished(int jobId) {
+ doForceJobFinished(this, jobId);
+ }
+
+ @Override
public void updateEstimatedNetworkBytes(int jobId, JobWorkItem item,
long downloadBytes, long uploadBytes) {
doUpdateEstimatedNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
@@ -762,6 +769,35 @@ public final class JobServiceContext implements ServiceConnection {
}
}
+ /**
+ * This method just adds traces to evaluate jobs that leak jobparameters at the client.
+ * It does not stop the job.
+ */
+ void doForceJobFinished(JobCallback cb, int jobId) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ final JobStatus executing;
+ synchronized (mLock) {
+ // not the current job, presumably it has finished in some way already
+ if (!verifyCallerLocked(cb)) {
+ return;
+ }
+
+ executing = getRunningJobLocked();
+ }
+ if (executing != null && jobId == executing.getJobId()) {
+ final StringBuilder stateSuffix = new StringBuilder();
+ stateSuffix.append(TRACE_JOB_FORCE_FINISHED_PREFIX);
+ stateSuffix.append(executing.getBatteryName());
+ stateSuffix.append(TRACE_JOB_FORCE_FINISHED_DELIMITER);
+ stateSuffix.append(executing.getJobId());
+ Trace.instant(Trace.TRACE_TAG_POWER, stateSuffix.toString());
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback cb, int jobId,
int workId, @BytesLong long transferredBytes) {
// TODO(255393346): Make sure apps call this appropriately and monitor for abuse
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index b83be6b86d04..b4fb4803a2b9 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -88,6 +88,7 @@ import android.util.Size;
import android.view.WindowInsetsController.Appearance;
import android.window.TaskSnapshot;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.LocalePicker;
import com.android.internal.app.procstats.ProcessStats;
import com.android.internal.os.RoSystemProperties;
@@ -238,6 +239,14 @@ public class ActivityManager {
private static final RateLimitingCache<List<ProcessErrorStateInfo>> mErrorProcessesCache =
new RateLimitingCache<>(10, 2);
+ /** Rate-Limiting cache that allows no more than 100 calls to the service per second. */
+ @GuardedBy("mMemoryInfoCache")
+ private static final RateLimitingCache<MemoryInfo> mMemoryInfoCache =
+ new RateLimitingCache<>(10);
+ /** Used to store cached results for rate-limited calls to getMemoryInfo(). */
+ @GuardedBy("mMemoryInfoCache")
+ private static final MemoryInfo mRateLimitedMemInfo = new MemoryInfo();
+
/**
* Query handler for mGetCurrentUserIdCache - returns a cached value of the current foreground
* user id if the backstage_power/android.app.cache_get_current_user_id flag is enabled.
@@ -3510,6 +3519,19 @@ public class ActivityManager {
foregroundAppThreshold = source.readLong();
}
+ /** @hide */
+ public void copyTo(MemoryInfo other) {
+ other.advertisedMem = advertisedMem;
+ other.availMem = availMem;
+ other.totalMem = totalMem;
+ other.threshold = threshold;
+ other.lowMemory = lowMemory;
+ other.hiddenAppThreshold = hiddenAppThreshold;
+ other.secondaryServerThreshold = secondaryServerThreshold;
+ other.visibleAppThreshold = visibleAppThreshold;
+ other.foregroundAppThreshold = foregroundAppThreshold;
+ }
+
public static final @android.annotation.NonNull Creator<MemoryInfo> CREATOR
= new Creator<MemoryInfo>() {
public MemoryInfo createFromParcel(Parcel source) {
@@ -3536,6 +3558,20 @@ public class ActivityManager {
* manage its memory.
*/
public void getMemoryInfo(MemoryInfo outInfo) {
+ if (Flags.rateLimitGetMemoryInfo()) {
+ synchronized (mMemoryInfoCache) {
+ mMemoryInfoCache.get(() -> {
+ getMemoryInfoInternal(mRateLimitedMemInfo);
+ return mRateLimitedMemInfo;
+ });
+ mRateLimitedMemInfo.copyTo(outInfo);
+ }
+ } else {
+ getMemoryInfoInternal(outInfo);
+ }
+ }
+
+ private void getMemoryInfoInternal(MemoryInfo outInfo) {
try {
getService().getMemoryInfo(outInfo);
} catch (RemoteException e) {
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 4d61f418af10..c0c81df465e2 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -125,3 +125,14 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "backstage_power"
+ name: "rate_limit_get_memory_info"
+ description: "Rate limit calls to getMemoryInfo using a cache"
+ is_fixed_read_only: true
+ bug: "364312431"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 8e08a95dad70..081dfe60d28c 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -152,6 +152,16 @@ flag {
}
flag {
+ name: "fix_race_condition_in_tie_profile_lock"
+ namespace: "enterprise"
+ description: "Fix race condition in tieProfileLockIfNecessary()"
+ bug: "355905501"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "quiet_mode_credential_bug_fix"
namespace: "enterprise"
description: "Guards a bugfix that ends the credential input flow if the managed user has not stopped."
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index da3cc1bda3be..031380dc1962 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -86,6 +86,7 @@ import android.util.Log;
import android.util.proto.ProtoOutputStream;
import com.android.internal.util.XmlUtils;
+import com.android.modules.expresslog.Counter;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -12805,6 +12806,8 @@ public class Intent implements Parcelable, Cloneable {
new ClipData.Item(text, htmlText, null, stream));
setClipData(clipData);
if (stream != null) {
+ logCounterIfFlagsMissing(FLAG_GRANT_READ_URI_PERMISSION,
+ "intents.value_explicit_uri_grant_for_send_action");
addFlags(FLAG_GRANT_READ_URI_PERMISSION);
}
return true;
@@ -12846,6 +12849,8 @@ public class Intent implements Parcelable, Cloneable {
setClipData(clipData);
if (streams != null) {
+ logCounterIfFlagsMissing(FLAG_GRANT_READ_URI_PERMISSION,
+ "intents.value_explicit_uri_grant_for_send_multiple_action");
addFlags(FLAG_GRANT_READ_URI_PERMISSION);
}
return true;
@@ -12865,6 +12870,10 @@ public class Intent implements Parcelable, Cloneable {
putExtra(MediaStore.EXTRA_OUTPUT, output);
setClipData(ClipData.newRawUri("", output));
+
+ logCounterIfFlagsMissing(
+ FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_READ_URI_PERMISSION,
+ "intents.value_explicit_uri_grant_for_image_capture_action");
addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION);
return true;
}
@@ -12873,6 +12882,12 @@ public class Intent implements Parcelable, Cloneable {
return false;
}
+ private void logCounterIfFlagsMissing(int requiredFlags, String metricId) {
+ if ((getFlags() & requiredFlags) != requiredFlags) {
+ Counter.logIncrement(metricId);
+ }
+ }
+
@android.ravenwood.annotation.RavenwoodThrow
private Uri maybeConvertFileToContentUri(Context context, Uri uri) {
if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())
diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java
index 8975191b54c1..9355937b0963 100644
--- a/core/java/android/hardware/biometrics/BiometricConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricConstants.java
@@ -170,6 +170,12 @@ public interface BiometricConstants {
int BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE = 20;
/**
+ * Biometrics is not allowed to verify in apps.
+ * @hide
+ */
+ int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21;
+
+ /**
* This constant is only used by SystemUI. It notifies SystemUI that authentication was paused
* because the authentication attempt was unsuccessful.
* @hide
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index 9bc46b9f382a..a4f7485fcaa5 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -94,6 +94,13 @@ public class BiometricManager {
BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE;
/**
+ * Biometrics is not allowed to verify in apps.
+ * @hide
+ */
+ public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS =
+ BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS;
+
+ /**
* A security vulnerability has been discovered and the sensor is unavailable until a
* security update has addressed this issue. This error can be received if for example,
* authentication was requested with {@link Authenticators#BIOMETRIC_STRONG}, but the
diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java
index 07fbe4a04ff1..0541a96e990e 100644
--- a/core/java/android/os/AppZygote.java
+++ b/core/java/android/os/AppZygote.java
@@ -111,12 +111,15 @@ public class AppZygote {
try {
int runtimeFlags = Zygote.getMemorySafetyRuntimeFlagsForSecondaryZygote(
mAppInfo, mProcessInfo);
+
+ final int[] sharedAppGid = {
+ UserHandle.getSharedAppGid(UserHandle.getAppId(mAppInfo.uid)) };
mZygote = Process.ZYGOTE_PROCESS.startChildZygote(
"com.android.internal.os.AppZygoteInit",
mAppInfo.processName + "_zygote",
mZygoteUid,
mZygoteUid,
- null, // gids
+ sharedAppGid, // Zygote gets access to shared app GID for profiles
runtimeFlags,
"app_zygote", // seInfo
abi, // abi
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 7b4ea41554f1..0ed0e60f6b4d 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -371,7 +371,7 @@ import java.util.function.Predicate;
* </tr>
* <tr>
* <td><code>{@link #onTouchEvent(MotionEvent)}</code></td>
- * <td>Called when a touch screen motion event occurs.
+ * <td>Called when a motion event occurs with pointers down on the view.
* </td>
* </tr>
* <tr>
@@ -17873,7 +17873,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
- * Implement this method to handle touch screen motion events.
+ * Implement this method to handle pointer events.
+ * <p>
+ * This method is called to handle motion events where pointers are down on
+ * the view. For example, this could include touchscreen touches, stylus
+ * touches, or click-and-drag events from a mouse. However, it is not called
+ * for motion events that do not involve pointers being down, such as hover
+ * events or mouse scroll wheel movements.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java
index 8501474b70a6..4c5802ccfcf5 100644
--- a/core/java/android/webkit/WebViewDelegate.java
+++ b/core/java/android/webkit/WebViewDelegate.java
@@ -137,9 +137,13 @@ public final class WebViewDelegate {
*/
@Deprecated
public void detachDrawGlFunctor(View containerView, long nativeDrawGLFunctor) {
- ViewRootImpl viewRootImpl = containerView.getViewRootImpl();
- if (nativeDrawGLFunctor != 0 && viewRootImpl != null) {
- viewRootImpl.detachFunctor(nativeDrawGLFunctor);
+ if (Flags.mainlineApis()) {
+ throw new UnsupportedOperationException();
+ } else {
+ ViewRootImpl viewRootImpl = containerView.getViewRootImpl();
+ if (nativeDrawGLFunctor != 0 && viewRootImpl != null) {
+ viewRootImpl.detachFunctor(nativeDrawGLFunctor);
+ }
}
}
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index b5bf529fadbd..511c832a4876 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -16,6 +16,7 @@
package android.widget;
+import static android.view.flags.Flags.enableTouchScrollFeedback;
import static android.view.flags.Flags.viewVelocityApi;
import android.annotation.ColorInt;
@@ -846,6 +847,8 @@ public class ScrollView extends FrameLayout {
deltaY += mTouchSlop;
}
}
+ boolean hitTopLimit = false;
+ boolean hitBottomLimit = false;
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
@@ -889,12 +892,14 @@ public class ScrollView extends FrameLayout {
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
+ hitTopLimit = true;
} else if (pulledToY > range) {
mEdgeGlowBottom.onPullDistance((float) deltaY / getHeight(),
1.f - displacement);
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
+ hitBottomLimit = true;
}
if (shouldDisplayEdgeEffects()
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
@@ -902,6 +907,20 @@ public class ScrollView extends FrameLayout {
}
}
}
+
+ // TODO: b/360198915 - Add unit tests.
+ if (enableTouchScrollFeedback()) {
+ if (hitTopLimit || hitBottomLimit) {
+ initHapticScrollFeedbackProviderIfNotExists();
+ mHapticScrollFeedbackProvider.onScrollLimit(vtev.getDeviceId(),
+ vtev.getSource(), MotionEvent.AXIS_Y,
+ /* isStart= */ hitTopLimit);
+ } else if (Math.abs(deltaY) != 0) {
+ initHapticScrollFeedbackProviderIfNotExists();
+ mHapticScrollFeedbackProvider.onScrollProgress(vtev.getDeviceId(),
+ vtev.getSource(), MotionEvent.AXIS_Y, deltaY);
+ }
+ }
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
diff --git a/core/java/android/widget/flags/flags.aconfig b/core/java/android/widget/flags/flags.aconfig
new file mode 100644
index 000000000000..f0ed83be8f1e
--- /dev/null
+++ b/core/java/android/widget/flags/flags.aconfig
@@ -0,0 +1,11 @@
+package: "android.widget.flags"
+container: "system"
+flag {
+ name: "enable_fading_view_group"
+ namespace: "system_performance"
+ description: "FRP screen during OOBE must have fading and scaling animation in Wear Watches"
+ bug: "348515581"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+} \ No newline at end of file
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index 4cc0d8a77a2b..c316800108bd 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -69,6 +69,23 @@ public class TaskFragmentOrganizer extends WindowOrganizer {
public static final String KEY_ERROR_CALLBACK_OP_TYPE = "operation_type";
/**
+ * Key to bundle {@link TaskFragmentInfo}s from the system in
+ * {@link #registerOrganizer(boolean, Bundle)}
+ *
+ * @hide
+ */
+ public static final String KEY_RESTORE_TASK_FRAGMENTS_INFO = "key_restore_task_fragments_info";
+
+ /**
+ * Key to bundle {@link TaskFragmentParentInfo} from the system in
+ * {@link #registerOrganizer(boolean, Bundle)}
+ *
+ * @hide
+ */
+ public static final String KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO =
+ "key_restore_task_fragment_parent_info";
+
+ /**
* No change set.
*/
@WindowManager.TransitionType
diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
index 12d326486e77..032ac4283712 100644
--- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
+++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
@@ -3025,6 +3025,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal,
@Override
public PackageImpl setSplitCodePaths(@Nullable String[] splitCodePaths) {
this.splitCodePaths = splitCodePaths;
+ this.mSplits = null; // reset for paths changed
if (splitCodePaths != null) {
int size = splitCodePaths.length;
for (int index = 0; index < size; index++) {
diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java
index 11c220b14bcc..0ec55f958f38 100644
--- a/core/java/com/android/internal/widget/LockPatternView.java
+++ b/core/java/com/android/internal/widget/LockPatternView.java
@@ -120,6 +120,7 @@ public class LockPatternView extends View {
private static final String TAG = "LockPatternView";
private OnPatternListener mOnPatternListener;
+ private ExternalHapticsPlayer mExternalHapticsPlayer;
@UnsupportedAppUsage
private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
@@ -317,6 +318,13 @@ public class LockPatternView extends View {
void onPatternDetected(List<Cell> pattern);
}
+ /** An external haptics player for pattern updates. */
+ public interface ExternalHapticsPlayer{
+
+ /** Perform haptic feedback when a cell is added to the pattern. */
+ void performCellAddedFeedback();
+ }
+
public LockPatternView(Context context) {
this(context, null);
}
@@ -461,6 +469,15 @@ public class LockPatternView extends View {
}
/**
+ * Set the external haptics player for feedback on pattern detection.
+ * @param player The external player.
+ */
+ @UnsupportedAppUsage
+ public void setExternalHapticsPlayer(ExternalHapticsPlayer player) {
+ mExternalHapticsPlayer = player;
+ }
+
+ /**
* Set the pattern explicitely (rather than waiting for the user to input
* a pattern).
* @param displayMode How to display the pattern.
@@ -847,6 +864,16 @@ public class LockPatternView extends View {
return null;
}
+ @Override
+ public boolean performHapticFeedback(int feedbackConstant, int flags) {
+ if (mExternalHapticsPlayer != null) {
+ mExternalHapticsPlayer.performCellAddedFeedback();
+ return true;
+ } else {
+ return super.performHapticFeedback(feedbackConstant, flags);
+ }
+ }
+
private void addCellToPattern(Cell newCell) {
mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
mPattern.add(newCell);
diff --git a/core/java/com/android/internal/widget/ViewGroupFader.java b/core/java/com/android/internal/widget/ViewGroupFader.java
index b54023a3382e..21206c244c8f 100644
--- a/core/java/com/android/internal/widget/ViewGroupFader.java
+++ b/core/java/com/android/internal/widget/ViewGroupFader.java
@@ -16,12 +16,14 @@
package com.android.internal.widget;
+import android.content.res.Resources;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.BaseInterpolator;
import android.view.animation.PathInterpolator;
+import android.widget.flags.Flags;
/**
* This class is ported from
@@ -36,7 +38,7 @@ import android.view.animation.PathInterpolator;
* height of the child. When not in the top or bottom regions, children have their default alpha and
* scale.
*/
-class ViewGroupFader {
+public class ViewGroupFader {
private static final float SCALE_LOWER_BOUND = 0.7f;
private float mScaleLowerBound = SCALE_LOWER_BOUND;
@@ -68,7 +70,7 @@ class ViewGroupFader {
private BaseInterpolator mBottomInterpolator = new PathInterpolator(0.3f, 0f, 0.7f, 1f);
/** Callback which is called when attempting to fade a view. */
- interface AnimationCallback {
+ public interface AnimationCallback {
boolean shouldFadeFromTop(View view);
boolean shouldFadeFromBottom(View view);
@@ -82,7 +84,7 @@ class ViewGroupFader {
* of the current position.
*/
// TODO(b/182846214): Clean up the interface design to avoid exposing too much details to users.
- interface ChildViewBoundsProvider {
+ public interface ChildViewBoundsProvider {
/**
* Provide the bounds of the child view.
*
@@ -168,7 +170,7 @@ class ViewGroupFader {
}
}
- ViewGroupFader(
+ public ViewGroupFader(
ViewGroup parent,
AnimationCallback callback,
ChildViewBoundsProvider childViewBoundsProvider) {
@@ -212,7 +214,7 @@ class ViewGroupFader {
this.mContainerBoundsProvider = boundsProvider;
}
- void updateFade() {
+ public void updateFade() {
mContainerBoundsProvider.provideBounds(mParent, mContainerBounds);
mTopBoundPixels = mContainerBounds.height() * mChainedBoundsTop;
mBottomBoundPixels = mContainerBounds.height() * mChainedBoundsBottom;
@@ -221,13 +223,20 @@ class ViewGroupFader {
}
/** For each list element, calculate and adjust the scale and alpha based on its position */
- private void updateListElementFades(ViewGroup parent, boolean shouldFade) {
+ public void updateListElementFades(ViewGroup parent, boolean shouldFade) {
for (int i = 0; i < parent.getChildCount(); i++) {
View child = parent.getChildAt(i);
if (child.getVisibility() != View.VISIBLE) {
continue;
}
+ if (Flags.enableFadingViewGroup() && Resources.getSystem().getBoolean(
+ com.android.internal.R.bool.config_enableViewGroupScalingFading)) {
+ if (child instanceof ViewGroup) {
+ updateListElementFades((ViewGroup) child, true);
+ }
+ }
+
if (shouldFade) {
fadeElement(parent, child);
}
@@ -312,4 +321,4 @@ class ViewGroupFader {
private static float lerp(float min, float max, float fraction) {
return min + (max - min) * fraction;
}
-}
+} \ No newline at end of file
diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml
index 52662149b23a..e28b6462bad7 100644
--- a/core/res/res/values-watch/config.xml
+++ b/core/res/res/values-watch/config.xml
@@ -96,4 +96,8 @@
<!-- True if the device supports system decorations on secondary displays. -->
<bool name="config_supportsSystemDecorsOnSecondaryDisplays">false</bool>
+
+ <!-- Whether to enable scaling and fading animation to scrollviews while scrolling.
+ P.S this is a change only intended for wear devices. -->
+ <bool name="config_enableViewGroupScalingFading">true</bool>
</resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 38aff7590a42..f6267f6174b6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -7121,4 +7121,8 @@
<!-- The maximum number of call log entries for each sim card that can be stored in the call log
provider on the device. -->
<integer name="config_maximumCallLogEntriesPerSim">500</integer>
+
+ <!-- Whether to enable scaling and fading animation to scrollviews while scrolling.
+ P.S this is a change only intended for wear devices. -->
+ <bool name="config_enableViewGroupScalingFading">false</bool>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 46938948b133..3c8c04e23087 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5601,4 +5601,6 @@
<!-- Fingerprint loe notification string -->
<java-symbol type="string" name="fingerprint_loe_notification_msg" />
+
+ <java-symbol type="bool" name="config_enableViewGroupScalingFading"/>
</resources>
diff --git a/core/tests/coretests/src/com/android/internal/widget/ViewGroupFaderTest.java b/core/tests/coretests/src/com/android/internal/widget/ViewGroupFaderTest.java
new file mode 100644
index 000000000000..eeabc2f4e0ed
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/ViewGroupFaderTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.test.AndroidTestCase;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.flags.Flags;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link ViewGroupFader}.
+ */
+public class ViewGroupFaderTest extends AndroidTestCase {
+
+ private Context mContext;
+ private ViewGroupFader mViewGroupFader;
+ private Resources mResources;
+
+ @Mock
+ private ViewGroup mViewGroup,mViewGroup1;
+
+ @Mock
+ private ViewGroupFader mockViewGroupFader;
+
+ @Mock
+ private ViewGroupFader.AnimationCallback mAnimationCallback;
+
+ @Mock
+ private ViewGroupFader.ChildViewBoundsProvider mChildViewBoundsProvider;
+
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ final Context mContext = getInstrumentation().getContext();
+ mResources = spy(mContext.getResources());
+ when(mResources.getBoolean(com.android.internal.R.bool.config_enableViewGroupScalingFading))
+ .thenReturn(true);
+ when(mViewGroup.getResources()).thenReturn(mResources);
+
+ mViewGroupFader = new ViewGroupFader(
+ mViewGroup,
+ mAnimationCallback,
+ mChildViewBoundsProvider);
+ }
+
+ /** This test checks that for each child of the parent viewgroup,
+ * updateListElementFades is called for each of its child, when the Flag is set to true
+ */
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FADING_VIEW_GROUP)
+ public void testFadingAndScrollingAnimationWorking_FlagOn() {
+ mViewGroup.addView(mViewGroup1);
+ mViewGroupFader.updateFade();
+
+ for (int i = 0; i < mViewGroup.getChildCount(); i++) {
+ View child = mViewGroup.getChildAt(i);
+ verify(mockViewGroupFader).updateListElementFades((ViewGroup)child,true);
+ }
+ }
+
+ /** This test checks that for each child of the parent viewgroup,
+ * updateListElementFades is never called for each of its child, when the Flag is set to false
+ */
+ @Test
+ public void testFadingAndScrollingAnimationNotWorking_FlagOff() {
+ mViewGroup.addView(mViewGroup1);
+ mViewGroupFader.updateFade();
+
+ for (int i = 0; i < mViewGroup.getChildCount(); i++) {
+ View child = mViewGroup.getChildAt(i);
+ verify(mockViewGroupFader,never()).updateListElementFades((ViewGroup)child,true);
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
index 4ce294213526..bfccb29bc952 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
@@ -16,16 +16,26 @@
package androidx.window.extensions.embedding;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENTS_INFO;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO;
+
import android.os.Build;
import android.os.Bundle;
+import android.os.IBinder;
import android.os.Looper;
import android.os.MessageQueue;
+import android.util.ArrayMap;
import android.util.Log;
+import android.util.SparseArray;
+import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentParentInfo;
+import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
/**
* Helper class to back up and restore the TaskFragmentOrganizer state, in order to resume
@@ -40,11 +50,21 @@ class BackupHelper {
@NonNull
private final SplitController mController;
@NonNull
+ private final SplitPresenter mPresenter;
+ @NonNull
private final BackupIdler mBackupIdler = new BackupIdler();
private boolean mBackupIdlerScheduled;
- BackupHelper(@NonNull SplitController splitController, @NonNull Bundle savedState) {
+ private final List<ParcelableTaskContainerData> mParcelableTaskContainerDataList =
+ new ArrayList<>();
+ private final ArrayMap<IBinder, TaskFragmentInfo> mTaskFragmentInfos = new ArrayMap<>();
+ private final SparseArray<TaskFragmentParentInfo> mTaskFragmentParentInfos =
+ new SparseArray<>();
+
+ BackupHelper(@NonNull SplitController splitController, @NonNull SplitPresenter splitPresenter,
+ @NonNull Bundle savedState) {
mController = splitController;
+ mPresenter = splitPresenter;
if (!savedState.isEmpty()) {
restoreState(savedState);
@@ -67,13 +87,13 @@ class BackupHelper {
public boolean queueIdle() {
synchronized (mController.mLock) {
mBackupIdlerScheduled = false;
- startBackup();
+ saveState();
}
return false;
}
}
- private void startBackup() {
+ private void saveState() {
final List<TaskContainer> taskContainers = mController.getTaskContainers();
if (taskContainers.isEmpty()) {
Log.w(TAG, "No task-container to back up");
@@ -97,13 +117,92 @@ class BackupHelper {
return;
}
- final List<ParcelableTaskContainerData> parcelableTaskContainerDataList =
- savedState.getParcelableArrayList(KEY_TASK_CONTAINERS,
- ParcelableTaskContainerData.class);
- for (ParcelableTaskContainerData data : parcelableTaskContainerDataList) {
- final TaskContainer taskContainer = new TaskContainer(data, mController);
- if (DEBUG) Log.d(TAG, "Restoring task " + taskContainer.getTaskId());
- // TODO(b/289875940): implement the TaskContainer restoration.
+ if (DEBUG) Log.d(TAG, "Start restoring saved-state");
+ mParcelableTaskContainerDataList.addAll(savedState.getParcelableArrayList(
+ KEY_TASK_CONTAINERS, ParcelableTaskContainerData.class));
+ if (DEBUG) Log.d(TAG, "Retrieved tasks : " + mParcelableTaskContainerDataList.size());
+ if (mParcelableTaskContainerDataList.isEmpty()) {
+ return;
+ }
+
+ final List<TaskFragmentInfo> infos = savedState.getParcelableArrayList(
+ KEY_RESTORE_TASK_FRAGMENTS_INFO, TaskFragmentInfo.class);
+ for (TaskFragmentInfo info : infos) {
+ if (DEBUG) Log.d(TAG, "Retrieved: " + info);
+ mTaskFragmentInfos.put(info.getFragmentToken(), info);
+ mPresenter.updateTaskFragmentInfo(info);
+ }
+
+ final List<TaskFragmentParentInfo> parentInfos = savedState.getParcelableArrayList(
+ KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO,
+ TaskFragmentParentInfo.class);
+ for (TaskFragmentParentInfo info : parentInfos) {
+ if (DEBUG) Log.d(TAG, "Retrieved: " + info);
+ mTaskFragmentParentInfos.put(info.getTaskId(), info);
+ }
+ }
+
+ boolean hasPendingStateToRestore() {
+ return !mParcelableTaskContainerDataList.isEmpty();
+ }
+
+ /**
+ * Returns {@code true} if any of the {@link TaskContainer} is restored.
+ * Otherwise, returns {@code false}.
+ */
+ boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct,
+ @NonNull Set<EmbeddingRule> rules) {
+ if (mParcelableTaskContainerDataList.isEmpty()) {
+ return false;
+ }
+
+ if (DEBUG) Log.d(TAG, "Rebuilding TaskContainers.");
+ final ArrayMap<String, EmbeddingRule> embeddingRuleMap = new ArrayMap<>();
+ for (EmbeddingRule rule : rules) {
+ embeddingRuleMap.put(rule.getTag(), rule);
+ }
+
+ boolean restoredAny = false;
+ for (int i = mParcelableTaskContainerDataList.size() - 1; i >= 0; i--) {
+ final ParcelableTaskContainerData parcelableTaskContainerData =
+ mParcelableTaskContainerDataList.get(i);
+ final List<String> tags = parcelableTaskContainerData.getSplitRuleTags();
+ if (!embeddingRuleMap.containsAll(tags)) {
+ // has unknown tag, unable to restore.
+ if (DEBUG) {
+ Log.d(TAG, "Rebuilding TaskContainer abort! Unknown Tag. Task#"
+ + parcelableTaskContainerData.mTaskId);
+ }
+ continue;
+ }
+
+ mParcelableTaskContainerDataList.remove(parcelableTaskContainerData);
+ final TaskContainer taskContainer = new TaskContainer(parcelableTaskContainerData,
+ mController, mTaskFragmentInfos);
+ if (DEBUG) Log.d(TAG, "Created TaskContainer " + taskContainer);
+ mController.addTaskContainer(taskContainer.getTaskId(), taskContainer);
+
+ for (ParcelableSplitContainerData splitData :
+ parcelableTaskContainerData.getParcelableSplitContainerDataList()) {
+ final SplitRule rule = (SplitRule) embeddingRuleMap.get(splitData.mSplitRuleTag);
+ assert rule != null;
+ if (mController.getContainer(splitData.getPrimaryContainerToken()) != null
+ && mController.getContainer(splitData.getSecondaryContainerToken())
+ != null) {
+ taskContainer.addSplitContainer(
+ new SplitContainer(splitData, mController, rule));
+ }
+ }
+
+ mController.onTaskFragmentParentRestored(wct, taskContainer.getTaskId(),
+ mTaskFragmentParentInfos.get(taskContainer.getTaskId()));
+ restoredAny = true;
+ }
+
+ if (mParcelableTaskContainerDataList.isEmpty()) {
+ mTaskFragmentParentInfos.clear();
+ mTaskFragmentInfos.clear();
}
+ return restoredAny;
}
-}
+} \ No newline at end of file
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java
index 817cfce69b2e..cb280c530c1b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java
@@ -89,13 +89,13 @@ class ParcelableSplitContainerData implements Parcelable {
};
@NonNull
- private IBinder getPrimaryContainerToken() {
+ IBinder getPrimaryContainerToken() {
return mSplitContainer != null ? mSplitContainer.getPrimaryContainer().getToken()
: mPrimaryContainerToken;
}
@NonNull
- private IBinder getSecondaryContainerToken() {
+ IBinder getSecondaryContainerToken() {
return mSplitContainer != null ? mSplitContainer.getSecondaryContainer().getToken()
: mSecondaryContainerToken;
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java
index 7377d005cda4..97aa69985907 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java
@@ -108,6 +108,15 @@ class ParcelableTaskContainerData implements Parcelable {
: mParcelableSplitContainerDataList;
}
+ @NonNull
+ List<String> getSplitRuleTags() {
+ final List<String> tags = new ArrayList<>();
+ for (ParcelableSplitContainerData data : getParcelableSplitContainerDataList()) {
+ tags.add(data.mSplitRuleTag);
+ }
+ return tags;
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 6d436ec01d98..faf73c24073f 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -86,6 +86,25 @@ class SplitContainer {
}
}
+ /** This is only used when restoring it from a {@link ParcelableSplitContainerData}. */
+ SplitContainer(@NonNull ParcelableSplitContainerData parcelableData,
+ @NonNull SplitController splitController, @NonNull SplitRule splitRule) {
+ mParcelableData = parcelableData;
+ mPrimaryContainer = splitController.getContainer(parcelableData.getPrimaryContainerToken());
+ mSecondaryContainer = splitController.getContainer(
+ parcelableData.getSecondaryContainerToken());
+ mSplitRule = splitRule;
+ mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes();
+ mCurrentSplitAttributes = mDefaultSplitAttributes;
+
+ if (shouldFinishPrimaryWithSecondary(splitRule)) {
+ mSecondaryContainer.addContainerToFinishOnExit(mPrimaryContainer);
+ }
+ if (shouldFinishSecondaryWithPrimary(splitRule)) {
+ mPrimaryContainer.addContainerToFinishOnExit(mSecondaryContainer);
+ }
+ }
+
void setPrimaryContainer(@NonNull TaskFragmentContainer primaryContainer) {
if (!mParcelableData.mIsPrimaryContainerMutable) {
throw new IllegalStateException("Cannot update primary TaskFragmentContainer");
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 f2f2b7ea7174..db4bb0e5e75e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -279,6 +279,26 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
Log.i(TAG, "Setting embedding rules. Size: " + rules.size());
mSplitRules.clear();
mSplitRules.addAll(rules);
+
+ if (!Flags.aeBackStackRestore() || !mPresenter.isRebuildTaskContainersNeeded()) {
+ return;
+ }
+
+ try {
+ final TransactionRecord transactionRecord =
+ mTransactionManager.startNewTransaction();
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
+ if (mPresenter.rebuildTaskContainers(wct, rules)) {
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ updateCallbackIfNecessary();
+ } else {
+ transactionRecord.abort();
+ }
+ } catch (IllegalStateException ex) {
+ Log.e(TAG, "Having an existing transaction while running restoration with"
+ + "new rules!! It is likely too late to perform the restoration "
+ + "already!?", ex);
+ }
}
}
@@ -903,6 +923,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
@GuardedBy("mLock")
+ void onTaskFragmentParentRestored(@NonNull WindowContainerTransaction wct, int taskId,
+ @NonNull TaskFragmentParentInfo parentInfo) {
+ onTaskFragmentParentInfoChanged(wct, taskId, parentInfo);
+ }
+
+ @GuardedBy("mLock")
void updateContainersInTaskIfVisible(@NonNull WindowContainerTransaction wct, int taskId) {
final TaskContainer taskContainer = getTaskContainer(taskId);
if (taskContainer == null) {
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 abc7b291fc32..0c0ded9bad74 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -24,6 +24,7 @@ import static androidx.window.extensions.embedding.SplitController.TAG;
import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
import android.annotation.AnimRes;
+import android.annotation.NonNull;
import android.app.Activity;
import android.app.ActivityThread;
import android.app.WindowConfiguration;
@@ -47,7 +48,6 @@ import android.window.TaskFragmentCreationParams;
import android.window.WindowContainerTransaction;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.window.extensions.core.util.function.Function;
import androidx.window.extensions.embedding.SplitAttributes.SplitType;
@@ -67,6 +67,7 @@ import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
/**
@@ -174,7 +175,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
} else {
registerOrganizer();
}
- mBackupHelper = new BackupHelper(controller, outSavedState);
+ mBackupHelper = new BackupHelper(controller, this, outSavedState);
if (!SplitController.ENABLE_SHELL_TRANSITIONS) {
// TODO(b/207070762): cleanup with legacy app transition
// Animation will be handled by WM Shell when Shell transition is enabled.
@@ -186,6 +187,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
mBackupHelper.scheduleBackup();
}
+ boolean isRebuildTaskContainersNeeded() {
+ return mBackupHelper.hasPendingStateToRestore();
+ }
+
+ boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct,
+ @NonNull Set<EmbeddingRule> rules) {
+ return mBackupHelper.rebuildTaskContainers(wct, rules);
+ }
+
/**
* Deletes the specified container and all other associated and dependent containers in the same
* transaction.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 608a3bee7509..74cce68f270b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -31,6 +31,7 @@ import android.app.WindowConfiguration.WindowingMode;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.IBinder;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -147,14 +148,23 @@ class TaskContainer {
/** This is only used when restoring it from a {@link ParcelableTaskContainerData}. */
TaskContainer(@NonNull ParcelableTaskContainerData data,
- @NonNull SplitController splitController) {
+ @NonNull SplitController splitController,
+ @NonNull ArrayMap<IBinder, TaskFragmentInfo> taskFragmentInfoMap) {
mParcelableTaskContainerData = new ParcelableTaskContainerData(data, this);
+ mInfo = new TaskFragmentParentInfo(new Configuration(), 0 /* displayId */, -1 /* taskId */,
+ false /* visible */, false /* hasDirectActivity */, null /* decorSurface */);
mSplitController = splitController;
for (ParcelableTaskFragmentContainerData tfData :
data.getParcelableTaskFragmentContainerDataList()) {
- final TaskFragmentContainer container =
- new TaskFragmentContainer(tfData, splitController, this);
- mContainers.add(container);
+ final TaskFragmentInfo info = taskFragmentInfoMap.get(tfData.mToken);
+ if (info != null && !info.isEmpty()) {
+ final TaskFragmentContainer container =
+ new TaskFragmentContainer(tfData, splitController, this);
+ container.setInfo(new WindowContainerTransaction(), info);
+ mContainers.add(container);
+ } else {
+ Log.d(TAG, "Drop " + tfData + " while restoring Task " + data.mTaskId);
+ }
}
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
index cf39415b3fe6..6c83d88032df 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
@@ -29,7 +29,6 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.TaskSnapshot;
/**
@@ -75,7 +74,7 @@ public abstract class PipContentOverlay {
public PipColorOverlay(Context context) {
mContext = context;
- mLeash = new SurfaceControl.Builder(new SurfaceSession())
+ mLeash = new SurfaceControl.Builder()
.setCallsite(TAG)
.setName(LAYER_NAME)
.setColorLayer()
@@ -123,7 +122,7 @@ public abstract class PipContentOverlay {
public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
mSnapshot = snapshot;
mSourceRectHint = new Rect(sourceRectHint);
- mLeash = new SurfaceControl.Builder(new SurfaceSession())
+ mLeash = new SurfaceControl.Builder()
.setCallsite(TAG)
.setName(LAYER_NAME)
.build();
@@ -183,7 +182,7 @@ public abstract class PipContentOverlay {
mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888);
prepareAppIconOverlay(appIcon);
- mLeash = new SurfaceControl.Builder(new SurfaceSession())
+ mLeash = new SurfaceControl.Builder()
.setCallsite(TAG)
.setName(LAYER_NAME)
.build();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 7b3b2071ef02..156399499c5b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -982,7 +982,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mShellBackAnimationRegistry.resetDefaultCrossActivity();
cancelLatencyTracking();
mReceivedNullNavigationInfo = false;
- mBackTransitionHandler.mLastTrigger = triggerBack;
if (mBackNavigationInfo != null) {
mPreviousNavigationType = mBackNavigationInfo.getType();
mBackNavigationInfo.onBackNavigationFinished(triggerBack);
@@ -1103,7 +1102,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
endLatencyTracking();
if (!validateAnimationTargets(apps)) {
Log.e(TAG, "Invalid animation targets!");
- mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
return;
}
mBackAnimationFinishedCallback = finishedCallback;
@@ -1113,7 +1111,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
return;
}
kickStartAnimation();
- mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
});
}
@@ -1121,7 +1118,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
public void onAnimationCancelled() {
mShellExecutor.execute(
() -> {
- mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
if (!mShellBackAnimationRegistry.cancel(
mBackNavigationInfo != null
? mBackNavigationInfo.getType()
@@ -1160,8 +1156,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
boolean mCloseTransitionRequested;
SurfaceControl.Transaction mFinishOpenTransaction;
Transitions.TransitionFinishCallback mFinishOpenTransitionCallback;
- QueuedTransition mQueuedTransition = null;
- boolean mLastTrigger;
// The Transition to make behindActivity become visible
IBinder mPrepareOpenTransition;
// The Transition to make behindActivity become invisible, if prepare open exist and
@@ -1178,13 +1172,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
}
- void consumeQueuedTransitionIfNeeded() {
- if (mQueuedTransition != null) {
- mQueuedTransition.consume();
- mQueuedTransition = null;
- }
- }
-
private void applyFinishOpenTransition() {
mOpenTransitionInfo = null;
mPrepareOpenTransition = null;
@@ -1215,7 +1202,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
@NonNull SurfaceControl.Transaction st,
@NonNull SurfaceControl.Transaction ft,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) {
+ final boolean isPrepareTransition =
+ info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
+ if (isPrepareTransition) {
kickStartAnimation();
}
// Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't
@@ -1240,21 +1229,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
if (mApps == null || mApps.length == 0) {
- if (mBackNavigationInfo != null && mShellBackAnimationRegistry
- .isWaitingAnimation(mBackNavigationInfo.getType())) {
- // Waiting for animation? Queue update to wait for animation start.
- consumeQueuedTransitionIfNeeded();
- mQueuedTransition = new QueuedTransition(info, st, ft, finishCallback);
- return true;
- } else if (mLastTrigger) {
- // animation was done, consume directly
+ if (mCloseTransitionRequested) {
+ // animation never start, consume directly
applyAndFinish(st, ft, finishCallback);
return true;
- } else {
- // animation was cancelled but transition haven't happen, we must handle it
- if (mClosePrepareTransition == null && mCurrentTracker.isFinished()) {
- createClosePrepareTransition();
- }
+ } else if (mClosePrepareTransition == null && isPrepareTransition) {
+ // Gesture animation was cancelled before prepare transition ready, create the
+ // the close prepare transition
+ createClosePrepareTransition();
}
}
@@ -1413,9 +1395,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
if (mPrepareOpenTransition != null) {
applyFinishOpenTransition();
}
- if (mQueuedTransition != null) {
- consumeQueuedTransitionIfNeeded();
- }
return;
}
// Handle the commit transition if this handler is running the open transition.
@@ -1423,11 +1402,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
t.apply();
if (mCloseTransitionRequested) {
if (mApps == null || mApps.length == 0) {
- if (mQueuedTransition == null) {
- // animation was done
- applyFinishOpenTransition();
- mCloseTransitionRequested = false;
- } // let queued transition finish.
+ // animation was done
+ applyFinishOpenTransition();
+ mCloseTransitionRequested = false;
} else {
// we are animating, wait until animation finish
mOnAnimationFinishCallback = () -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java
index 4b138e43bc3f..dd17e2980e58 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java
@@ -17,7 +17,6 @@
package com.android.wm.shell.common;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
/**
* Helpers for handling surface.
@@ -25,16 +24,15 @@ import android.view.SurfaceSession;
public class SurfaceUtils {
/** Creates a dim layer above host surface. */
public static SurfaceControl makeDimLayer(SurfaceControl.Transaction t, SurfaceControl host,
- String name, SurfaceSession surfaceSession) {
- final SurfaceControl dimLayer = makeColorLayer(host, name, surfaceSession);
+ String name) {
+ final SurfaceControl dimLayer = makeColorLayer(host, name);
t.setLayer(dimLayer, Integer.MAX_VALUE).setColor(dimLayer, new float[]{0f, 0f, 0f});
return dimLayer;
}
/** Creates a color layer for host surface. */
- public static SurfaceControl makeColorLayer(SurfaceControl host, String name,
- SurfaceSession surfaceSession) {
- return new SurfaceControl.Builder(surfaceSession)
+ public static SurfaceControl makeColorLayer(SurfaceControl host, String name) {
+ return new SurfaceControl.Builder()
.setParent(host)
.setColorLayer()
.setName(name)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index ef33b3830e45..3dc86decdb2e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -42,7 +42,6 @@ import android.view.InsetsState;
import android.view.ScrollCaptureResponse;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -311,7 +310,7 @@ public class SystemWindows {
@Override
protected SurfaceControl getParentSurface(IWindow window,
WindowManager.LayoutParams attrs) {
- SurfaceControl leash = new SurfaceControl.Builder(new SurfaceSession())
+ SurfaceControl leash = new SurfaceControl.Builder()
.setContainerLayer()
.setName("SystemWindowLeash")
.setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index 7175e361f91a..de3152ad7687 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -43,7 +43,6 @@ import android.view.IWindow;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
@@ -74,7 +73,6 @@ public class SplitDecorManager extends WindowlessWindowManager {
private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground";
private final IconProvider mIconProvider;
- private final SurfaceSession mSurfaceSession;
private Drawable mIcon;
private ImageView mVeilIconView;
@@ -103,17 +101,15 @@ public class SplitDecorManager extends WindowlessWindowManager {
private int mOffsetY;
private int mRunningAnimationCount = 0;
- public SplitDecorManager(Configuration configuration, IconProvider iconProvider,
- SurfaceSession surfaceSession) {
+ public SplitDecorManager(Configuration configuration, IconProvider iconProvider) {
super(configuration, null /* rootSurface */, null /* hostInputToken */);
mIconProvider = iconProvider;
- mSurfaceSession = surfaceSession;
}
@Override
protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
// Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setContainerLayer()
.setName(TAG)
.setHidden(true)
@@ -238,7 +234,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
if (mBackgroundLeash == null) {
mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
- RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession);
+ RESIZING_BACKGROUND_SURFACE_NAME);
t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask))
.setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
}
@@ -248,7 +244,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
final int left = isLandscape ? mOldMainBounds.width() : 0;
final int top = isLandscape ? 0 : mOldMainBounds.height();
mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
- GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession);
+ GAP_BACKGROUND_SURFACE_NAME);
// Fill up another side bounds area.
t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask))
.setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2)
@@ -405,7 +401,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
if (mBackgroundLeash == null) {
// Initialize background
mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
- RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession);
+ RESIZING_BACKGROUND_SURFACE_NAME);
t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask))
.setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
index 46c1a43f9efe..c5f19742c803 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
@@ -36,7 +36,6 @@ import android.view.InsetsState;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
@@ -98,7 +97,7 @@ public final class SplitWindowManager extends WindowlessWindowManager {
@Override
protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
// Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setContainerLayer()
.setName(TAG)
.setHidden(true)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
index 0564c95aef5c..d2b4f1ab6b0d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
@@ -38,7 +38,6 @@ import android.util.Log;
import android.view.IWindow;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
@@ -173,7 +172,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana
@Override
protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
String className = getClass().getSimpleName();
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setContainerLayer()
.setName(className + "Leash")
.setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt
index 831b331a11e9..abc26cfb3e13 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt
@@ -24,7 +24,6 @@ import android.os.Binder
import android.view.IWindow
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
-import android.view.SurfaceSession
import android.view.View
import android.view.WindowManager
import android.view.WindowlessWindowManager
@@ -106,7 +105,7 @@ class CompatUIComponent(
attrs: WindowManager.LayoutParams
): SurfaceControl? {
val className = javaClass.simpleName
- val builder = SurfaceControl.Builder(SurfaceSession())
+ val builder = SurfaceControl.Builder()
.setContainerLayer()
.setName(className + "Leash")
.setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
index 71cc8df80cad..422656c6d387 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
@@ -38,7 +38,6 @@ import android.view.IWindow;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
@@ -105,7 +104,7 @@ public final class BackgroundWindowManager extends WindowlessWindowManager {
@Override
protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setColorLayer()
.setBufferSize(mDisplayBounds.width(), mDisplayBounds.height())
.setFormat(PixelFormat.RGB_888)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 4df649ca8c93..f060158766fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -497,13 +497,8 @@ public class PipAnimationController {
mCurrentValue = value;
}
- boolean shouldApplyCornerRadius() {
- return !isOutPipDirection(mTransitionDirection);
- }
-
boolean shouldApplyShadowRadius() {
- return !isOutPipDirection(mTransitionDirection)
- && !isRemovePipDirection(mTransitionDirection);
+ return !isRemovePipDirection(mTransitionDirection);
}
boolean inScaleTransition() {
@@ -556,7 +551,7 @@ public class PipAnimationController {
final float alpha = getStartValue() * (1 - fraction) + getEndValue() * fraction;
setCurrentValue(alpha);
getSurfaceTransactionHelper().alpha(tx, leash, alpha)
- .round(tx, leash, shouldApplyCornerRadius())
+ .round(tx, leash, true /* applyCornerRadius */)
.shadow(tx, leash, shouldApplyShadowRadius());
if (!handlePipTransaction(leash, tx, destinationBounds, alpha)) {
tx.apply();
@@ -572,7 +567,7 @@ public class PipAnimationController {
getSurfaceTransactionHelper()
.resetScale(tx, leash, getDestinationBounds())
.crop(tx, leash, getDestinationBounds())
- .round(tx, leash, shouldApplyCornerRadius())
+ .round(tx, leash, true /* applyCornerRadius */)
.shadow(tx, leash, shouldApplyShadowRadius());
tx.show(leash);
tx.apply();
@@ -686,13 +681,11 @@ public class PipAnimationController {
getSurfaceTransactionHelper().scaleAndCrop(tx, leash,
adjustedSourceRectHint, initialSourceValue, bounds, insets,
isInPipDirection, fraction);
- if (shouldApplyCornerRadius()) {
- final Rect sourceBounds = new Rect(initialContainerRect);
- sourceBounds.inset(insets);
- getSurfaceTransactionHelper()
- .round(tx, leash, sourceBounds, bounds)
- .shadow(tx, leash, shouldApplyShadowRadius());
- }
+ final Rect sourceBounds = new Rect(initialContainerRect);
+ sourceBounds.inset(insets);
+ getSurfaceTransactionHelper()
+ .round(tx, leash, sourceBounds, bounds)
+ .shadow(tx, leash, shouldApplyShadowRadius());
}
if (!handlePipTransaction(leash, tx, bounds, /* alpha= */ 1f)) {
tx.apply();
@@ -741,11 +734,9 @@ public class PipAnimationController {
.rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds,
insets, degree, x, y, isOutPipDirection,
rotationDelta == ROTATION_270 /* clockwise */);
- if (shouldApplyCornerRadius()) {
- getSurfaceTransactionHelper()
- .round(tx, leash, sourceBounds, bounds)
- .shadow(tx, leash, shouldApplyShadowRadius());
- }
+ getSurfaceTransactionHelper()
+ .round(tx, leash, sourceBounds, bounds)
+ .shadow(tx, leash, shouldApplyShadowRadius());
if (!handlePipTransaction(leash, tx, bounds, 1f /* alpha */)) {
tx.apply();
}
@@ -761,7 +752,7 @@ public class PipAnimationController {
void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
getSurfaceTransactionHelper()
.alpha(tx, leash, 1f)
- .round(tx, leash, shouldApplyCornerRadius())
+ .round(tx, leash, true /* applyCornerRadius */)
.shadow(tx, leash, shouldApplyShadowRadius());
tx.show(leash);
tx.apply();
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 7e165afce7d4..793e2aa757a3 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
@@ -61,7 +61,6 @@ import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.view.WindowManager;
import android.widget.Toast;
import android.window.RemoteTransition;
@@ -897,7 +896,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
private SurfaceControl reparentSplitTasksForAnimation(RemoteAnimationTarget[] apps,
SurfaceControl.Transaction t, String callsite) {
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setContainerLayer()
.setName("RecentsAnimationSplitTasks")
.setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index f3113dce94a4..dad0d4eb4d8d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -104,7 +104,6 @@ import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.view.WindowManager;
import android.widget.Toast;
import android.window.DisplayAreaInfo;
@@ -171,8 +170,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private static final String TAG = StageCoordinator.class.getSimpleName();
- private final SurfaceSession mSurfaceSession = new SurfaceSession();
-
private final StageTaskListener mMainStage;
private final StageListenerImpl mMainStageListener = new StageListenerImpl();
private final StageTaskListener mSideStage;
@@ -334,7 +331,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDisplayId,
mMainStageListener,
mSyncQueue,
- mSurfaceSession,
iconProvider,
mWindowDecorViewModel);
mSideStage = new StageTaskListener(
@@ -343,7 +339,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mDisplayId,
mSideStageListener,
mSyncQueue,
- mSurfaceSession,
iconProvider,
mWindowDecorViewModel);
mDisplayController = displayController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 29b5114d87e6..d64c0a24be68 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -39,7 +39,6 @@ import android.util.Slog;
import android.util.SparseArray;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -93,7 +92,6 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener {
private final Context mContext;
private final StageListenerCallbacks mCallbacks;
- private final SurfaceSession mSurfaceSession;
private final SyncTransactionQueue mSyncQueue;
private final IconProvider mIconProvider;
private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
@@ -108,12 +106,11 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener {
StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
- SurfaceSession surfaceSession, IconProvider iconProvider,
+ IconProvider iconProvider,
Optional<WindowDecorViewModel> windowDecorViewModel) {
mContext = context;
mCallbacks = callbacks;
mSyncQueue = syncQueue;
- mSurfaceSession = surfaceSession;
mIconProvider = iconProvider;
mWindowDecorViewModel = windowDecorViewModel;
taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
@@ -203,12 +200,11 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener {
mRootTaskInfo = taskInfo;
mSplitDecorManager = new SplitDecorManager(
mRootTaskInfo.configuration,
- mIconProvider,
- mSurfaceSession);
+ mIconProvider);
mCallbacks.onRootTaskAppeared();
sendStatusChanged();
mSyncQueue.runInSync(t -> mDimLayer =
- SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession));
+ SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer"));
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
final int taskId = taskInfo.taskId;
mChildrenLeashes.put(taskId, leash);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index fac3592896ea..2e9b53eee13f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -33,7 +33,6 @@ import android.hardware.display.DisplayManager;
import android.util.SparseArray;
import android.view.IWindow;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.window.SplashScreenView;
@@ -204,7 +203,7 @@ public class StartingSurfaceDrawer {
@Override
protected SurfaceControl getParentSurface(IWindow window,
WindowManager.LayoutParams attrs) {
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setContainerLayer()
.setName("Windowless window")
.setHidden(false)
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 4fc6c4489f2b..ff4b981f5e8e 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
@@ -92,7 +92,6 @@ import android.os.UserHandle;
import android.util.ArrayMap;
import android.view.Choreographer;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
@@ -134,8 +133,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private final TransitionAnimation mTransitionAnimation;
private final DevicePolicyManager mDevicePolicyManager;
- private final SurfaceSession mSurfaceSession = new SurfaceSession();
-
/** Keeps track of the currently-running animations associated with each transition. */
private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>();
@@ -705,7 +702,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
TransitionInfo.Change change, TransitionInfo info, int animHint,
ArrayList<Animator> animations, Runnable onAnimFinish) {
final int rootIdx = TransitionUtil.rootIndexFor(change, info);
- final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession,
+ final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext,
mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(),
animHint);
// The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real
@@ -918,7 +915,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession,
+ final WindowThumbnail wt = WindowThumbnail.createAndAttach(
change.getLeash(), thumbnail, transaction);
final Animation a =
mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(bounds);
@@ -943,7 +940,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
@NonNull Runnable finishCallback, TransitionInfo.Change change,
TransitionInfo.AnimationOptions options, float cornerRadius) {
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession,
+ final WindowThumbnail wt = WindowThumbnail.createAndAttach(
change.getLeash(), options.getThumbnail(), transaction);
final Rect bounds = change.getEndAbsBounds();
final int orientation = mContext.getResources().getConfiguration().orientation;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index 3d79a1c8cebe..c385f9afcf3a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -74,8 +74,12 @@ public class HomeTransitionObserver implements TransitionObserver,
final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED);
if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
if (Flags.migratePredictiveBackTransition()) {
- if (!isBackGesture && TransitionUtil.isOpenOrCloseMode(mode)) {
- notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode));
+ final boolean gestureToHomeTransition = isBackGesture
+ && TransitionUtil.isClosingType(info.getType());
+ if (gestureToHomeTransition
+ || (!isBackGesture && TransitionUtil.isOpenOrCloseMode(mode))) {
+ notifyHomeVisibilityChanged(gestureToHomeTransition
+ || TransitionUtil.isOpeningType(mode));
}
} else {
if (TransitionUtil.isOpenOrCloseMode(mode) || isBackGesture) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 0bf9d368ab74..5802e2ca8133 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -38,7 +38,6 @@ import android.util.Slog;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
-import android.view.SurfaceSession;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.window.ScreenCapture;
@@ -112,7 +111,7 @@ class ScreenRotationAnimation {
/** Intensity of light/whiteness of the layout after rotation occurs. */
private float mEndLuma;
- ScreenRotationAnimation(Context context, SurfaceSession session, TransactionPool pool,
+ ScreenRotationAnimation(Context context, TransactionPool pool,
Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) {
mContext = context;
mTransactionPool = pool;
@@ -126,7 +125,7 @@ class ScreenRotationAnimation {
mStartRotation = change.getStartRotation();
mEndRotation = change.getEndRotation();
- mAnimLeash = new SurfaceControl.Builder(session)
+ mAnimLeash = new SurfaceControl.Builder()
.setParent(rootLeash)
.setEffectLayer()
.setCallsite("ShellRotationAnimation")
@@ -153,7 +152,7 @@ class ScreenRotationAnimation {
return;
}
- mScreenshotLayer = new SurfaceControl.Builder(session)
+ mScreenshotLayer = new SurfaceControl.Builder()
.setParent(mAnimLeash)
.setBLASTLayer()
.setSecure(screenshotBuffer.containsSecureLayers())
@@ -178,7 +177,7 @@ class ScreenRotationAnimation {
t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight));
if (!isCustomRotate()) {
- mBackColorSurface = new SurfaceControl.Builder(session)
+ mBackColorSurface = new SurfaceControl.Builder()
.setParent(rootLeash)
.setColorLayer()
.setOpaque(true)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
index 2c668ed3d84d..341f2bc66716 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
@@ -21,7 +21,6 @@ import android.graphics.GraphicBuffer;
import android.graphics.PixelFormat;
import android.hardware.HardwareBuffer;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
/**
* Represents a surface that is displayed over a transition surface.
@@ -33,10 +32,10 @@ class WindowThumbnail {
private WindowThumbnail() {}
/** Create a thumbnail surface and attach it over a parent surface. */
- static WindowThumbnail createAndAttach(SurfaceSession surfaceSession, SurfaceControl parent,
+ static WindowThumbnail createAndAttach(SurfaceControl parent,
HardwareBuffer thumbnailHeader, SurfaceControl.Transaction t) {
WindowThumbnail windowThumbnail = new WindowThumbnail();
- windowThumbnail.mSurfaceControl = new SurfaceControl.Builder(surfaceSession)
+ windowThumbnail.mSurfaceControl = new SurfaceControl.Builder()
.setParent(parent)
.setName("WindowThumanil : " + parent.toString())
.setCallsite("WindowThumanil")
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index fd6c4d8e604d..fb81ed4169ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -30,7 +30,6 @@ import android.view.Display
import android.view.LayoutInflater
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
-import android.view.SurfaceSession
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL
import android.view.WindowlessWindowManager
@@ -66,7 +65,6 @@ class ResizeVeil @JvmOverloads constructor(
private val lightColors = dynamicLightColorScheme(context)
private val darkColors = dynamicDarkColorScheme(context)
- private val surfaceSession = SurfaceSession()
private lateinit var iconView: ImageView
private var iconSize = 0
@@ -126,7 +124,7 @@ class ResizeVeil @JvmOverloads constructor(
.setCallsite("ResizeVeil#setupResizeVeil")
.build()
backgroundSurface = surfaceControlBuilderFactory
- .create("Resize veil background of Task=" + taskInfo.taskId, surfaceSession)
+ .create("Resize veil background of Task=" + taskInfo.taskId)
.setColorLayer()
.setHidden(true)
.setParent(veilSurface)
@@ -399,10 +397,6 @@ class ResizeVeil @JvmOverloads constructor(
fun create(name: String): SurfaceControl.Builder {
return SurfaceControl.Builder().setName(name)
}
-
- fun create(name: String, surfaceSession: SurfaceSession): SurfaceControl.Builder {
- return SurfaceControl.Builder(surfaceSession).setName(name)
- }
}
companion object {
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
index 426f40b5e81b..a54d497bf511 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.scenarios
import android.app.Instrumentation
-import android.platform.test.annotations.Postsubmit
import android.tools.NavBar
import android.tools.Rotation
import android.tools.flicker.rules.ChangeDisplayOrientationRule
@@ -33,15 +32,12 @@ import com.android.wm.shell.Utils
import org.junit.After
import org.junit.Assume
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.BlockJUnit4ClassRunner
-@RunWith(BlockJUnit4ClassRunner::class)
-@Postsubmit
-open class MaximizeAppWindow
-@JvmOverloads
+@Ignore("Test Base Class")
+abstract class MaximizeAppWindow
constructor(private val rotation: Rotation = Rotation.ROTATION_0, isResizable: Boolean = true) {
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index 413e49562435..e514dc38208e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -49,7 +49,6 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.util.SparseArray;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.ITaskOrganizer;
import android.window.ITaskOrganizerController;
import android.window.TaskAppearedInfo;
@@ -169,7 +168,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
public void testTaskLeashReleasedAfterVanished() throws RemoteException {
assumeFalse(ENABLE_SHELL_TRANSITIONS);
RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
- SurfaceControl taskLeash = new SurfaceControl.Builder(new SurfaceSession())
+ SurfaceControl taskLeash = new SurfaceControl.Builder()
.setName("task").build();
mOrganizer.registerOrganizer();
mOrganizer.onTaskAppeared(taskInfo, taskLeash);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index 751275b9e167..66dcef6f14cc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -24,7 +24,6 @@ import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -89,7 +88,7 @@ public class SplitTestUtils {
// Prepare root task for testing.
mRootTask = new TestRunningTaskInfoBuilder().build();
- mRootLeash = new SurfaceControl.Builder(new SurfaceSession()).setName("test").build();
+ mRootLeash = new SurfaceControl.Builder().setName("test").build();
onTaskAppeared(mRootTask, mRootLeash);
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index af288c81616d..ce3944a5855e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -53,7 +53,6 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.IRemoteTransition;
import android.window.RemoteTransition;
import android.window.TransitionInfo;
@@ -106,7 +105,6 @@ public class SplitTransitionTests extends ShellTestCase {
@Mock private DisplayInsetsController mDisplayInsetsController;
@Mock private TransactionPool mTransactionPool;
@Mock private Transitions mTransitions;
- @Mock private SurfaceSession mSurfaceSession;
@Mock private IconProvider mIconProvider;
@Mock private WindowDecorViewModel mWindowDecorViewModel;
@Mock private ShellExecutor mMainExecutor;
@@ -134,11 +132,11 @@ public class SplitTransitionTests extends ShellTestCase {
doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire();
mSplitLayout = SplitTestUtils.createMockSplitLayout();
mMainStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
- StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
+ StageTaskListener.StageListenerCallbacks.class), mSyncQueue,
mIconProvider, Optional.of(mWindowDecorViewModel)));
mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mSideStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
- StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
+ StageTaskListener.StageListenerCallbacks.class), mSyncQueue,
mIconProvider, Optional.of(mWindowDecorViewModel)));
mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index ce343b8a7fa9..a6c16c43c8cb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -50,7 +50,6 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.RemoteTransition;
import android.window.WindowContainerTransaction;
@@ -119,7 +118,6 @@ public class StageCoordinatorTests extends ShellTestCase {
private final Rect mBounds2 = new Rect(5, 10, 15, 20);
private final Rect mRootBounds = new Rect(0, 0, 45, 60);
- private SurfaceSession mSurfaceSession = new SurfaceSession();
private SurfaceControl mRootLeash;
private SurfaceControl mDividerLeash;
private ActivityManager.RunningTaskInfo mRootTask;
@@ -139,7 +137,7 @@ public class StageCoordinatorTests extends ShellTestCase {
mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController,
Optional.empty()));
- mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build();
+ mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build();
when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
@@ -149,7 +147,7 @@ public class StageCoordinatorTests extends ShellTestCase {
when(mSplitLayout.getDividerLeash()).thenReturn(mDividerLeash);
mRootTask = new TestRunningTaskInfoBuilder().build();
- mRootLeash = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
+ mRootLeash = new SurfaceControl.Builder().setName("test").build();
mStageCoordinator.onTaskAppeared(mRootTask, mRootLeash);
mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 8b5cb97505c0..b7b7d0d35bcf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -32,7 +32,6 @@ import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
import android.os.SystemProperties;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.WindowContainerTransaction;
import androidx.test.annotation.UiThreadTest;
@@ -82,7 +81,6 @@ public final class StageTaskListenerTests extends ShellTestCase {
private WindowContainerTransaction mWct;
@Captor
private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor;
- private SurfaceSession mSurfaceSession = new SurfaceSession();
private SurfaceControl mSurfaceControl;
private ActivityManager.RunningTaskInfo mRootTask;
private StageTaskListener mStageTaskListener;
@@ -97,12 +95,11 @@ public final class StageTaskListenerTests extends ShellTestCase {
DEFAULT_DISPLAY,
mCallbacks,
mSyncQueue,
- mSurfaceSession,
mIconProvider,
Optional.of(mWindowDecorViewModel));
mRootTask = new TestRunningTaskInfoBuilder().build();
mRootTask.parentTaskId = INVALID_TASK_ID;
- mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
+ mSurfaceControl = new SurfaceControl.Builder().setName("test").build();
mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
index 198488582700..17fd95b69dba 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
@@ -49,7 +49,6 @@ import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
-import android.view.SurfaceSession;
import android.view.ViewTreeObserver;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -95,7 +94,6 @@ public class TaskViewTest extends ShellTestCase {
Looper mViewLooper;
TestHandler mViewHandler;
- SurfaceSession mSession;
SurfaceControl mLeash;
Context mContext;
@@ -106,7 +104,7 @@ public class TaskViewTest extends ShellTestCase {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mLeash = new SurfaceControl.Builder(mSession)
+ mLeash = new SurfaceControl.Builder()
.setName("test")
.build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index f51a9608d442..8f49de0a98fb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
@@ -39,6 +40,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.view.SurfaceControl;
@@ -213,6 +215,35 @@ public class HomeTransitionObserverTest extends ShellTestCase {
verify(mListener, times(1)).onHomeVisibilityChanged(true);
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_MIGRATE_PREDICTIVE_BACK_TRANSITION)
+ public void testHomeActivityWithBackGestureNotifiesHomeIsVisibleAfterClose()
+ throws RemoteException {
+ TransitionInfo info = mock(TransitionInfo.class);
+ TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+ ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class);
+ when(change.getTaskInfo()).thenReturn(taskInfo);
+ when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change)));
+ when(info.getType()).thenReturn(TRANSIT_PREPARE_BACK_NAVIGATION);
+
+ when(change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)).thenReturn(true);
+ setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN, true);
+
+ mHomeTransitionObserver.onTransitionReady(mock(IBinder.class),
+ info,
+ mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class));
+ verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean());
+
+ when(info.getType()).thenReturn(TRANSIT_TO_BACK);
+ setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_CHANGE, true);
+ mHomeTransitionObserver.onTransitionReady(mock(IBinder.class),
+ info,
+ mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class));
+ verify(mListener, times(1)).onHomeVisibilityChanged(true);
+ }
+
/**
* Helper class to initialize variables for the rest.
*/
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
index a07be79579eb..e0d16aab1e07 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
@@ -97,7 +97,7 @@ class ResizeVeilTest : ShellTestCase() {
.thenReturn(spyResizeVeilSurfaceBuilder)
doReturn(mockResizeVeilSurface).whenever(spyResizeVeilSurfaceBuilder).build()
whenever(mockSurfaceControlBuilderFactory
- .create(eq("Resize veil background of Task=" + taskInfo.taskId), any()))
+ .create(eq("Resize veil background of Task=" + taskInfo.taskId)))
.thenReturn(spyBackgroundSurfaceBuilder)
doReturn(mockBackgroundSurface).whenever(spyBackgroundSurfaceBuilder).build()
whenever(mockSurfaceControlBuilderFactory
diff --git a/libs/androidfw/AssetManager.cpp b/libs/androidfw/AssetManager.cpp
index e6182454ad8a..5955915c9fcd 100644
--- a/libs/androidfw/AssetManager.cpp
+++ b/libs/androidfw/AssetManager.cpp
@@ -1420,18 +1420,20 @@ void AssetManager::mergeInfoLocked(SortedVector<AssetDir::FileInfo>* pMergedInfo
Mutex AssetManager::SharedZip::gLock;
DefaultKeyedVector<String8, wp<AssetManager::SharedZip> > AssetManager::SharedZip::gOpen;
-AssetManager::SharedZip::SharedZip(const String8& path, time_t modWhen)
- : mPath(path), mZipFile(NULL), mModWhen(modWhen),
- mResourceTableAsset(NULL), mResourceTable(NULL)
-{
- if (kIsDebug) {
- ALOGI("Creating SharedZip %p %s\n", this, mPath.c_str());
- }
- ALOGV("+++ opening zip '%s'\n", mPath.c_str());
- mZipFile = ZipFileRO::open(mPath.c_str());
- if (mZipFile == NULL) {
- ALOGD("failed to open Zip archive '%s'\n", mPath.c_str());
- }
+AssetManager::SharedZip::SharedZip(const String8& path, ModDate modWhen)
+ : mPath(path),
+ mZipFile(NULL),
+ mModWhen(modWhen),
+ mResourceTableAsset(NULL),
+ mResourceTable(NULL) {
+ if (kIsDebug) {
+ ALOGI("Creating SharedZip %p %s\n", this, mPath.c_str());
+ }
+ ALOGV("+++ opening zip '%s'\n", mPath.c_str());
+ mZipFile = ZipFileRO::open(mPath.c_str());
+ if (mZipFile == NULL) {
+ ALOGD("failed to open Zip archive '%s'\n", mPath.c_str());
+ }
}
AssetManager::SharedZip::SharedZip(int fd, const String8& path)
@@ -1453,7 +1455,7 @@ sp<AssetManager::SharedZip> AssetManager::SharedZip::get(const String8& path,
bool createIfNotPresent)
{
AutoMutex _l(gLock);
- time_t modWhen = getFileModDate(path.c_str());
+ auto modWhen = getFileModDate(path.c_str());
sp<SharedZip> zip = gOpen.valueFor(path).promote();
if (zip != NULL && zip->mModWhen == modWhen) {
return zip;
@@ -1520,8 +1522,8 @@ ResTable* AssetManager::SharedZip::setResourceTable(ResTable* res)
bool AssetManager::SharedZip::isUpToDate()
{
- time_t modWhen = getFileModDate(mPath.c_str());
- return mModWhen == modWhen;
+ auto modWhen = getFileModDate(mPath.c_str());
+ return mModWhen == modWhen;
}
void AssetManager::SharedZip::addOverlay(const asset_path& ap)
diff --git a/libs/androidfw/include/androidfw/AssetManager.h b/libs/androidfw/include/androidfw/AssetManager.h
index ce0985b38986..376c881ea376 100644
--- a/libs/androidfw/include/androidfw/AssetManager.h
+++ b/libs/androidfw/include/androidfw/AssetManager.h
@@ -280,21 +280,21 @@ private:
~SharedZip();
private:
- SharedZip(const String8& path, time_t modWhen);
- SharedZip(int fd, const String8& path);
- SharedZip(); // <-- not implemented
+ SharedZip(const String8& path, ModDate modWhen);
+ SharedZip(int fd, const String8& path);
+ SharedZip(); // <-- not implemented
- String8 mPath;
- ZipFileRO* mZipFile;
- time_t mModWhen;
+ String8 mPath;
+ ZipFileRO* mZipFile;
+ ModDate mModWhen;
- Asset* mResourceTableAsset;
- ResTable* mResourceTable;
+ Asset* mResourceTableAsset;
+ ResTable* mResourceTable;
- Vector<asset_path> mOverlays;
+ Vector<asset_path> mOverlays;
- static Mutex gLock;
- static DefaultKeyedVector<String8, wp<SharedZip> > gOpen;
+ static Mutex gLock;
+ static DefaultKeyedVector<String8, wp<SharedZip> > gOpen;
};
/*
diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h
index 64b1f0c6ed03..98f1aa86f2db 100644
--- a/libs/androidfw/include/androidfw/Idmap.h
+++ b/libs/androidfw/include/androidfw/Idmap.h
@@ -25,8 +25,9 @@
#include "android-base/macros.h"
#include "android-base/unique_fd.h"
#include "androidfw/ConfigDescription.h"
-#include "androidfw/StringPiece.h"
#include "androidfw/ResourceTypes.h"
+#include "androidfw/StringPiece.h"
+#include "androidfw/misc.h"
#include "utils/ByteOrder.h"
namespace android {
@@ -202,7 +203,7 @@ class LoadedIdmap {
android::base::unique_fd idmap_fd_;
std::string_view overlay_apk_path_;
std::string_view target_apk_path_;
- time_t idmap_last_mod_time_;
+ ModDate idmap_last_mod_time_;
private:
DISALLOW_COPY_AND_ASSIGN(LoadedIdmap);
diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h
index 077609d20d55..09ae40c35369 100644
--- a/libs/androidfw/include/androidfw/misc.h
+++ b/libs/androidfw/include/androidfw/misc.h
@@ -13,14 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+#pragma once
-#include <sys/types.h>
+#include <time.h>
//
// Handy utility functions and portability code.
//
-#ifndef _LIBS_ANDROID_FW_MISC_H
-#define _LIBS_ANDROID_FW_MISC_H
namespace android {
@@ -41,15 +40,35 @@ typedef enum FileType {
} FileType;
/* get the file's type; follows symlinks */
FileType getFileType(const char* fileName);
-/* get the file's modification date; returns -1 w/errno set on failure */
-time_t getFileModDate(const char* fileName);
+
+// MinGW doesn't support nanosecond resolution in stat() modification time, and given
+// that it only matters on the device it's ok to keep it at the second level there.
+#ifdef _WIN32
+using ModDate = time_t;
+inline constexpr ModDate kInvalidModDate = ModDate(-1);
+inline constexpr unsigned long long kModDateResolutionNs = 1ull * 1000 * 1000 * 1000;
+inline time_t toTimeT(ModDate m) {
+ return m;
+}
+#else
+using ModDate = timespec;
+inline constexpr ModDate kInvalidModDate = {-1, -1};
+inline constexpr unsigned long long kModDateResolutionNs = 1;
+inline time_t toTimeT(ModDate m) {
+ return m.tv_sec;
+}
+#endif
+
+/* get the file's modification date; returns kInvalidModDate w/errno set on failure */
+ModDate getFileModDate(const char* fileName);
/* same, but also returns -1 if the file has already been deleted */
-time_t getFileModDate(int fd);
+ModDate getFileModDate(int fd);
// Check if |path| or |fd| resides on a readonly filesystem.
bool isReadonlyFilesystem(const char* path);
bool isReadonlyFilesystem(int fd);
-}; // namespace android
+} // namespace android
-#endif // _LIBS_ANDROID_FW_MISC_H
+// Whoever uses getFileModDate() will need this as well
+bool operator==(const timespec& l, const timespec& r);
diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp
index 93dcaf549a90..9bdaf18a116a 100644
--- a/libs/androidfw/misc.cpp
+++ b/libs/androidfw/misc.cpp
@@ -28,11 +28,13 @@
#include <sys/vfs.h>
#endif // __linux__
-#include <cstring>
-#include <cstdio>
#include <errno.h>
#include <sys/stat.h>
+#include <cstdio>
+#include <cstring>
+#include <tuple>
+
namespace android {
/*
@@ -73,27 +75,32 @@ FileType getFileType(const char* fileName)
}
}
-/*
- * Get a file's modification date.
- */
-time_t getFileModDate(const char* fileName) {
- struct stat sb;
- if (stat(fileName, &sb) < 0) {
- return (time_t)-1;
- }
- return sb.st_mtime;
+static ModDate getModDate(const struct stat& st) {
+#ifdef _WIN32
+ return st.st_mtime;
+#else
+ return st.st_mtim;
+#endif
}
-time_t getFileModDate(int fd) {
- struct stat sb;
- if (fstat(fd, &sb) < 0) {
- return (time_t)-1;
- }
- if (sb.st_nlink <= 0) {
- errno = ENOENT;
- return (time_t)-1;
- }
- return sb.st_mtime;
+ModDate getFileModDate(const char* fileName) {
+ struct stat sb;
+ if (stat(fileName, &sb) < 0) {
+ return kInvalidModDate;
+ }
+ return getModDate(sb);
+}
+
+ModDate getFileModDate(int fd) {
+ struct stat sb;
+ if (fstat(fd, &sb) < 0) {
+ return kInvalidModDate;
+ }
+ if (sb.st_nlink <= 0) {
+ errno = ENOENT;
+ return kInvalidModDate;
+ }
+ return getModDate(sb);
}
#ifndef __linux__
@@ -124,4 +131,8 @@ bool isReadonlyFilesystem(int fd) {
}
#endif // __linux__
-}; // namespace android
+} // namespace android
+
+bool operator==(const timespec& l, const timespec& r) {
+ return std::tie(l.tv_sec, l.tv_nsec) == std::tie(r.tv_sec, l.tv_nsec);
+}
diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp
index 60aa7d88925d..cb2e56f5f5e4 100644
--- a/libs/androidfw/tests/Idmap_test.cpp
+++ b/libs/androidfw/tests/Idmap_test.cpp
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+#include <chrono>
+#include <thread>
+
#include "android-base/file.h"
#include "androidfw/ApkAssets.h"
#include "androidfw/AssetManager2.h"
@@ -27,6 +30,7 @@
#include "data/overlayable/R.h"
#include "data/system/R.h"
+using namespace std::chrono_literals;
using ::testing::NotNull;
namespace overlay = com::android::overlay;
@@ -218,10 +222,13 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) {
unlink(temp_file.path);
ASSERT_FALSE(apk_assets->IsUpToDate());
- sleep(2);
+
+ const auto sleep_duration =
+ std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull));
+ std::this_thread::sleep_for(sleep_duration);
base::WriteStringToFile("hello", temp_file.path);
- sleep(2);
+ std::this_thread::sleep_for(sleep_duration);
ASSERT_FALSE(apk_assets->IsUpToDate());
}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
index ff09084e24cd..c4173ed999f3 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
@@ -460,7 +460,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
event.startTracking();
return true;
}
@@ -479,7 +479,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat
return true;
}
- if (keyCode == KeyEvent.KEYCODE_BACK
+ if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE)
&& event.isTracking() && !event.isCanceled()) {
if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened()
&& !hasErrors()) {
diff --git a/packages/SettingsLib/Metadata/Android.bp b/packages/SettingsLib/Metadata/Android.bp
new file mode 100644
index 000000000000..207637f86372
--- /dev/null
+++ b/packages/SettingsLib/Metadata/Android.bp
@@ -0,0 +1,23 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "SettingsLibMetadata-srcs",
+ srcs: ["src/**/*.kt"],
+}
+
+android_library {
+ name: "SettingsLibMetadata",
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+ srcs: [":SettingsLibMetadata-srcs"],
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.fragment_fragment",
+ "guava",
+ "SettingsLibDataStore",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Metadata/AndroidManifest.xml b/packages/SettingsLib/Metadata/AndroidManifest.xml
new file mode 100644
index 000000000000..1c801e640f82
--- /dev/null
+++ b/packages/SettingsLib/Metadata/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.metadata">
+
+ <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Metadata/processor/Android.bp b/packages/SettingsLib/Metadata/processor/Android.bp
new file mode 100644
index 000000000000..d8acc7633d81
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/Android.bp
@@ -0,0 +1,11 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_plugin {
+ name: "SettingsLibMetadata-processor",
+ srcs: ["src/**/*.kt"],
+ processor_class: "com.android.settingslib.metadata.PreferenceScreenAnnotationProcessor",
+ java_resource_dirs: ["resources"],
+ visibility: ["//visibility:public"],
+}
diff --git a/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor b/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 000000000000..762a01a92f42
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+com.android.settingslib.metadata.PreferenceScreenAnnotationProcessor \ No newline at end of file
diff --git a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt
new file mode 100644
index 000000000000..620d717faf69
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import java.util.TreeMap
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeMirror
+import javax.tools.Diagnostic
+
+/** Processor to gather preference screens annotated with `@ProvidePreferenceScreen`. */
+class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
+ private val screens = TreeMap<String, ConstructorType>()
+ private val overlays = mutableMapOf<String, String>()
+ private val contextType: TypeMirror by lazy {
+ processingEnv.elementUtils.getTypeElement("android.content.Context").asType()
+ }
+
+ private var options: Map<String, Any?>? = null
+ private lateinit var annotationElement: TypeElement
+ private lateinit var optionsElement: TypeElement
+ private lateinit var screenType: TypeMirror
+
+ override fun getSupportedAnnotationTypes() = setOf(ANNOTATION, OPTIONS)
+
+ override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
+
+ override fun init(processingEnv: ProcessingEnvironment) {
+ super.init(processingEnv)
+ val elementUtils = processingEnv.elementUtils
+ annotationElement = elementUtils.getTypeElement(ANNOTATION)
+ optionsElement = elementUtils.getTypeElement(OPTIONS)
+ screenType = elementUtils.getTypeElement("$PACKAGE.$PREFERENCE_SCREEN_METADATA").asType()
+ }
+
+ override fun process(
+ annotations: MutableSet<out TypeElement>,
+ roundEnv: RoundEnvironment,
+ ): Boolean {
+ roundEnv.getElementsAnnotatedWith(optionsElement).singleOrNull()?.run {
+ if (options != null) error("@$OPTIONS_NAME is already specified: $options", this)
+ options =
+ annotationMirrors
+ .single { it.isElement(optionsElement) }
+ .elementValues
+ .entries
+ .associate { it.key.simpleName.toString() to it.value.value }
+ }
+ for (element in roundEnv.getElementsAnnotatedWith(annotationElement)) {
+ (element as? TypeElement)?.process()
+ }
+ if (roundEnv.processingOver()) codegen()
+ return false
+ }
+
+ private fun TypeElement.process() {
+ if (kind != ElementKind.CLASS || modifiers.contains(Modifier.ABSTRACT)) {
+ error("@$ANNOTATION_NAME must be added to non abstract class", this)
+ return
+ }
+ if (!processingEnv.typeUtils.isAssignable(asType(), screenType)) {
+ error("@$ANNOTATION_NAME must be added to $PREFERENCE_SCREEN_METADATA subclass", this)
+ return
+ }
+ val constructorType = getConstructorType()
+ if (constructorType == null) {
+ error(
+ "Class must be an object, or has single public constructor that " +
+ "accepts no parameter or a Context parameter",
+ this,
+ )
+ return
+ }
+ val screenQualifiedName = qualifiedName.toString()
+ screens[screenQualifiedName] = constructorType
+ val annotation = annotationMirrors.single { it.isElement(annotationElement) }
+ val overlay = annotation.getOverlay()
+ if (overlay != null) {
+ overlays.put(overlay, screenQualifiedName)?.let {
+ error("$overlay has been overlaid by $it", this)
+ }
+ }
+ }
+
+ private fun codegen() {
+ val collector = (options?.get("codegenCollector") as? String) ?: DEFAULT_COLLECTOR
+ if (collector.isEmpty()) return
+ val parts = collector.split('/')
+ if (parts.size == 3) {
+ generateCode(parts[0], parts[1], parts[2])
+ } else {
+ throw IllegalArgumentException(
+ "Collector option '$collector' does not follow 'PKG/CLASS/METHOD' format"
+ )
+ }
+ }
+
+ private fun generateCode(outputPkg: String, outputClass: String, outputFun: String) {
+ for ((overlay, screen) in overlays) {
+ if (screens.remove(overlay) == null) {
+ warn("$overlay is overlaid by $screen but not annotated with @$ANNOTATION_NAME")
+ } else {
+ processingEnv.messager.printMessage(
+ Diagnostic.Kind.NOTE,
+ "$overlay is overlaid by $screen",
+ )
+ }
+ }
+ processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use {
+ it.write("package $outputPkg;\n\n")
+ it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n\n")
+ it.write("// Generated by annotation processor for @$ANNOTATION_NAME\n")
+ it.write("public final class $outputClass {\n")
+ it.write(" private $outputClass() {}\n\n")
+ it.write(
+ " public static java.util.List<$PREFERENCE_SCREEN_METADATA> " +
+ "$outputFun(android.content.Context context) {\n"
+ )
+ it.write(
+ " java.util.ArrayList<$PREFERENCE_SCREEN_METADATA> screens = " +
+ "new java.util.ArrayList<>(${screens.size});\n"
+ )
+ for ((screen, constructorType) in screens) {
+ when (constructorType) {
+ ConstructorType.DEFAULT -> it.write(" screens.add(new $screen());\n")
+ ConstructorType.CONTEXT -> it.write(" screens.add(new $screen(context));\n")
+ ConstructorType.SINGLETON -> it.write(" screens.add($screen.INSTANCE);\n")
+ }
+ }
+ for ((overlay, screen) in overlays) {
+ it.write(" // $overlay is overlaid by $screen\n")
+ }
+ it.write(" return screens;\n")
+ it.write(" }\n")
+ it.write("}")
+ }
+ }
+
+ private fun AnnotationMirror.isElement(element: TypeElement) =
+ processingEnv.typeUtils.isSameType(annotationType.asElement().asType(), element.asType())
+
+ private fun AnnotationMirror.getOverlay(): String? {
+ for ((key, value) in elementValues) {
+ if (key.simpleName.contentEquals("overlay")) {
+ return if (value.isDefaultClassValue(key)) null else value.value.toString()
+ }
+ }
+ return null
+ }
+
+ private fun AnnotationValue.isDefaultClassValue(key: ExecutableElement) =
+ processingEnv.typeUtils.isSameType(
+ value as TypeMirror,
+ key.defaultValue.value as TypeMirror,
+ )
+
+ private fun TypeElement.getConstructorType(): ConstructorType? {
+ var constructor: ExecutableElement? = null
+ for (element in enclosedElements) {
+ if (element.isKotlinObject()) return ConstructorType.SINGLETON
+ if (element.kind != ElementKind.CONSTRUCTOR) continue
+ if (!element.modifiers.contains(Modifier.PUBLIC)) continue
+ if (constructor != null) return null
+ constructor = element as ExecutableElement
+ }
+ return constructor?.parameters?.run {
+ when {
+ isEmpty() -> ConstructorType.DEFAULT
+ size == 1 && processingEnv.typeUtils.isSameType(this[0].asType(), contextType) ->
+ ConstructorType.CONTEXT
+ else -> null
+ }
+ }
+ }
+
+ private fun Element.isKotlinObject() =
+ kind == ElementKind.FIELD &&
+ modifiers.run { contains(Modifier.PUBLIC) && contains(Modifier.STATIC) } &&
+ simpleName.toString() == "INSTANCE"
+
+ private fun warn(msg: CharSequence) =
+ processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, msg)
+
+ private fun error(msg: CharSequence, element: Element) =
+ processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg, element)
+
+ private enum class ConstructorType {
+ DEFAULT, // default constructor with no parameter
+ CONTEXT, // constructor with a Context parameter
+ SINGLETON, // Kotlin object class
+ }
+
+ companion object {
+ private const val PACKAGE = "com.android.settingslib.metadata"
+ private const val ANNOTATION_NAME = "ProvidePreferenceScreen"
+ private const val ANNOTATION = "$PACKAGE.$ANNOTATION_NAME"
+ private const val PREFERENCE_SCREEN_METADATA = "PreferenceScreenMetadata"
+
+ private const val OPTIONS_NAME = "ProvidePreferenceScreenOptions"
+ private const val OPTIONS = "$PACKAGE.$OPTIONS_NAME"
+ private const val DEFAULT_COLLECTOR = "$PACKAGE/PreferenceScreenCollector/get"
+ }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
new file mode 100644
index 000000000000..ea20a74de3cf
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import kotlin.reflect.KClass
+
+/**
+ * Annotation to provide preference screen.
+ *
+ * The annotated class must satisfy either condition:
+ * - the primary constructor has no parameter
+ * - the primary constructor has a single [android.content.Context] parameter
+ * - it is a Kotlin object class
+ *
+ * @param overlay if specified, current annotated screen will overlay the given screen
+ */
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.CLASS)
+@MustBeDocumented
+annotation class ProvidePreferenceScreen(
+ val overlay: KClass<out PreferenceScreenMetadata> = PreferenceScreenMetadata::class,
+)
+
+/**
+ * Provides options for [ProvidePreferenceScreen] annotation processor.
+ *
+ * @param codegenCollector generated collector class (format: "pkg/class/method"), an empty string
+ * means do not generate code
+ */
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.CLASS)
+@MustBeDocumented
+annotation class ProvidePreferenceScreenOptions(
+ val codegenCollector: String = "com.android.settingslib.metadata/PreferenceScreenCollector/get",
+)
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
new file mode 100644
index 000000000000..51a85803c6ed
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+import androidx.annotation.ArrayRes
+import androidx.annotation.IntDef
+import com.android.settingslib.datastore.KeyValueStore
+
+/** Permit of read and write request. */
+@IntDef(
+ ReadWritePermit.ALLOW,
+ ReadWritePermit.DISALLOW,
+ ReadWritePermit.REQUIRE_APP_PERMISSION,
+ ReadWritePermit.REQUIRE_USER_AGREEMENT,
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class ReadWritePermit {
+ companion object {
+ /** Allow to read/write value. */
+ const val ALLOW = 0
+ /** Disallow to read/write value (e.g. uid not allowed). */
+ const val DISALLOW = 1
+ /** Require (runtime/special) app permission from user explicitly. */
+ const val REQUIRE_APP_PERMISSION = 2
+ /** Require explicit user agreement (e.g. terms of service). */
+ const val REQUIRE_USER_AGREEMENT = 3
+ }
+}
+
+/** Preference interface that has a value persisted in datastore. */
+interface PersistentPreference<T> {
+
+ /**
+ * Returns the key-value storage of the preference.
+ *
+ * The default implementation returns the storage provided by
+ * [PreferenceScreenRegistry.getKeyValueStore].
+ */
+ fun storage(context: Context): KeyValueStore =
+ PreferenceScreenRegistry.getKeyValueStore(context, this as PreferenceMetadata)!!
+
+ /**
+ * Returns if the external application (identified by [callingUid]) has permission to read
+ * preference value.
+ *
+ * The underlying implementation does NOT need to check common states like isEnabled,
+ * isRestricted or isAvailable.
+ */
+ @ReadWritePermit
+ fun getReadPermit(context: Context, myUid: Int, callingUid: Int): Int =
+ PreferenceScreenRegistry.getReadPermit(
+ context,
+ myUid,
+ callingUid,
+ this as PreferenceMetadata,
+ )
+
+ /**
+ * Returns if the external application (identified by [callingUid]) has permission to write
+ * preference with given [value].
+ *
+ * The underlying implementation does NOT need to check common states like isEnabled,
+ * isRestricted or isAvailable.
+ */
+ @ReadWritePermit
+ fun getWritePermit(context: Context, value: T?, myUid: Int, callingUid: Int): Int =
+ PreferenceScreenRegistry.getWritePermit(
+ context,
+ value,
+ myUid,
+ callingUid,
+ this as PreferenceMetadata,
+ )
+}
+
+/** Descriptor of values. */
+sealed interface ValueDescriptor {
+
+ /** Returns if given value (represented by index) is valid. */
+ fun isValidValue(context: Context, index: Int): Boolean
+}
+
+/**
+ * A boolean type value.
+ *
+ * A zero value means `False`, otherwise it is `True`.
+ */
+interface BooleanValue : ValueDescriptor {
+ override fun isValidValue(context: Context, index: Int) = true
+}
+
+/** Value falls into a given array. */
+interface DiscreteValue<T> : ValueDescriptor {
+ @get:ArrayRes val values: Int
+
+ @get:ArrayRes val valuesDescription: Int
+
+ fun getValue(context: Context, index: Int): T
+}
+
+/**
+ * Value falls into a text array, whose element is [CharSequence] type.
+ *
+ * [values] resource is `<string-array>`.
+ */
+interface DiscreteTextValue : DiscreteValue<CharSequence> {
+ override fun isValidValue(context: Context, index: Int): Boolean {
+ if (index < 0) return false
+ return index < context.resources.getTextArray(values).size
+ }
+
+ override fun getValue(context: Context, index: Int): CharSequence =
+ context.resources.getTextArray(values)[index]
+}
+
+/**
+ * Value falls into a string array, whose element is [String] type.
+ *
+ * [values] resource is `<string-array>`.
+ */
+interface DiscreteStringValue : DiscreteValue<String> {
+ override fun isValidValue(context: Context, index: Int): Boolean {
+ if (index < 0) return false
+ return index < context.resources.getStringArray(values).size
+ }
+
+ override fun getValue(context: Context, index: Int): String =
+ context.resources.getStringArray(values)[index]
+}
+
+/**
+ * Value falls into an integer array.
+ *
+ * [values] resource is `<integer-array>`.
+ */
+interface DiscreteIntValue : DiscreteValue<Int> {
+ override fun isValidValue(context: Context, index: Int): Boolean {
+ if (index < 0) return false
+ return index < context.resources.getIntArray(values).size
+ }
+
+ override fun getValue(context: Context, index: Int): Int =
+ context.resources.getIntArray(values)[index]
+}
+
+/** Value is between a range. */
+interface RangeValue : ValueDescriptor {
+ /** The lower bound (inclusive) of the range. */
+ val minValue: Int
+
+ /** The upper bound (inclusive) of the range. */
+ val maxValue: Int
+
+ /** The increment step within the range. 0 means unset, which implies step size is 1. */
+ val incrementStep: Int
+ get() = 0
+
+ override fun isValidValue(context: Context, index: Int) = index in minValue..maxValue
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
new file mode 100644
index 000000000000..450373804b28
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+/** A node in preference hierarchy that is associated with [PreferenceMetadata]. */
+open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata)
+
+/**
+ * Preference hierarchy describes the structure of preferences recursively.
+ *
+ * A root hierarchy represents a preference screen. A sub-hierarchy represents a preference group.
+ */
+class PreferenceHierarchy internal constructor(metadata: PreferenceMetadata) :
+ PreferenceHierarchyNode(metadata) {
+
+ private val children = mutableListOf<PreferenceHierarchyNode>()
+
+ /** Adds a preference to the hierarchy. */
+ operator fun PreferenceMetadata.unaryPlus() {
+ children.add(PreferenceHierarchyNode(this))
+ }
+
+ /**
+ * Adds preference screen with given key (as a placeholder) to the hierarchy.
+ *
+ * This is mainly to support Android Settings overlays. OEMs might want to custom some of the
+ * screens. In resource-based hierarchy, it leverages the resource overlay. In terms of DSL or
+ * programmatic hierarchy, it will be a problem to specify concrete screen metadata objects.
+ * Instead, use preference screen key as a placeholder in the hierarchy and screen metadata will
+ * be looked up from [PreferenceScreenRegistry] lazily at runtime.
+ *
+ * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
+ */
+ operator fun String.unaryPlus() {
+ children.add(PreferenceHierarchyNode(PreferenceScreenRegistry[this]!!))
+ }
+
+ /** Adds a preference to the hierarchy. */
+ fun add(metadata: PreferenceMetadata) {
+ children.add(PreferenceHierarchyNode(metadata))
+ }
+
+ /** Adds a preference group to the hierarchy. */
+ operator fun PreferenceGroup.unaryPlus() = PreferenceHierarchy(this).also { children.add(it) }
+
+ /** Adds a preference group and returns its preference hierarchy. */
+ fun addGroup(metadata: PreferenceGroup): PreferenceHierarchy =
+ PreferenceHierarchy(metadata).also { children.add(it) }
+
+ /**
+ * Adds preference screen with given key (as a placeholder) to the hierarchy.
+ *
+ * This is mainly to support Android Settings overlays. OEMs might want to custom some of the
+ * screens. In resource-based hierarchy, it leverages the resource overlay. In terms of DSL or
+ * programmatic hierarchy, it will be a problem to specify concrete screen metadata objects.
+ * Instead, use preference screen key as a placeholder in the hierarchy and screen metadata will
+ * be looked up from [PreferenceScreenRegistry] lazily at runtime.
+ *
+ * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
+ */
+ fun addPreferenceScreen(screenKey: String) {
+ children.add(PreferenceHierarchy(PreferenceScreenRegistry[screenKey]!!))
+ }
+
+ /** Extensions to add more preferences to the hierarchy. */
+ operator fun plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this)
+
+ /** Traversals preference hierarchy and applies given action. */
+ fun forEach(action: (PreferenceHierarchyNode) -> Unit) {
+ for (it in children) action(it)
+ }
+
+ /** Traversals preference hierarchy and applies given action. */
+ suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) {
+ for (it in children) action(it)
+ }
+
+ /** Finds the [PreferenceMetadata] object associated with given key. */
+ fun find(key: String): PreferenceMetadata? {
+ if (metadata.key == key) return metadata
+ for (child in children) {
+ if (child is PreferenceHierarchy) {
+ val result = child.find(key)
+ if (result != null) return result
+ } else {
+ if (child.metadata.key == key) return child.metadata
+ }
+ }
+ return null
+ }
+
+ /** Returns all the [PreferenceMetadata]s appear in the hierarchy. */
+ fun getAllPreferences(): List<PreferenceMetadata> =
+ mutableListOf<PreferenceMetadata>().also { getAllPreferences(it) }
+
+ private fun getAllPreferences(result: MutableList<PreferenceMetadata>) {
+ result.add(metadata)
+ for (child in children) {
+ if (child is PreferenceHierarchy) {
+ child.getAllPreferences(result)
+ } else {
+ result.add(child.metadata)
+ }
+ }
+ }
+}
+
+/**
+ * Builder function to create [PreferenceHierarchy] in
+ * [DSL](https://kotlinlang.org/docs/type-safe-builders.html) manner.
+ */
+fun preferenceHierarchy(metadata: PreferenceMetadata, init: PreferenceHierarchy.() -> Unit) =
+ PreferenceHierarchy(metadata).also(init)
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
new file mode 100644
index 000000000000..f39f3a065e79
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.annotation.AnyThread
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+
+/**
+ * Interface provides preference metadata (title, summary, icon, etc.).
+ *
+ * Besides the existing APIs, subclass could integrate with following interface to provide more
+ * information:
+ * - [PreferenceTitleProvider]: provide dynamic title content
+ * - [PreferenceSummaryProvider]: provide dynamic summary content (e.g. based on preference value)
+ * - [PreferenceAvailabilityProvider]: provide preference availability (e.g. based on flag)
+ * - [PreferenceLifecycleProvider]: provide the lifecycle callbacks and notify state change
+ *
+ * Notes:
+ * - UI framework support:
+ * - This class does not involve any UI logic, it is the data layer.
+ * - Subclass could integrate with datastore and UI widget to provide UI layer. For instance,
+ * `PreferenceBinding` supports Jetpack Preference binding.
+ * - Datastore:
+ * - Subclass should implement the [PersistentPreference] to note that current preference is
+ * persistent in datastore.
+ * - It is always recommended to support back up preference value changed by user. Typically,
+ * the back up and restore happen within datastore, the [allowBackup] API is to mark if
+ * current preference value should be backed up (backup allowed by default).
+ * - Preference indexing for search:
+ * - Override [isIndexable] API to mark if preference is indexable (enabled by default).
+ * - If [isIndexable] returns true, preference title and summary will be indexed with cache.
+ * More indexing data could be provided through [keywords].
+ * - Settings search will cache the preference title/summary/keywords for indexing. The cache is
+ * invalidated when system locale changed, app upgraded, etc.
+ * - Dynamic content is not suitable to be cached for indexing. Subclass that implements
+ * [PreferenceTitleProvider] / [PreferenceSummaryProvider] will not have its title / summary
+ * indexed.
+ */
+@AnyThread
+interface PreferenceMetadata {
+
+ /** Preference key. */
+ val key: String
+
+ /**
+ * Preference title resource id.
+ *
+ * Implement [PreferenceTitleProvider] if title is generated dynamically.
+ */
+ val title: Int
+ @StringRes get() = 0
+
+ /**
+ * Preference summary resource id.
+ *
+ * Implement [PreferenceSummaryProvider] if summary is generated dynamically (e.g. summary is
+ * provided per preference value)
+ */
+ val summary: Int
+ @StringRes get() = 0
+
+ /** Icon of the preference. */
+ val icon: Int
+ @DrawableRes get() = 0
+
+ /** Additional keywords for indexing. */
+ val keywords: Int
+ @StringRes get() = 0
+
+ /**
+ * Return the extras Bundle object associated with this preference.
+ *
+ * It is used to provide more information for metadata.
+ */
+ fun extras(context: Context): Bundle? = null
+
+ /**
+ * Returns if preference is indexable, default value is `true`.
+ *
+ * Return `false` only when the preference is always unavailable on current device. If it is
+ * conditional available, override [PreferenceAvailabilityProvider].
+ */
+ fun isIndexable(context: Context): Boolean = true
+
+ /**
+ * Returns if preference is enabled.
+ *
+ * UI framework normally does not allow user to interact with the preference widget when it is
+ * disabled.
+ *
+ * [dependencyOfEnabledState] is provided to support dependency, the [shouldDisableDependents]
+ * value of dependent preference is used to decide enabled state.
+ */
+ fun isEnabled(context: Context): Boolean {
+ val dependency = dependencyOfEnabledState(context) ?: return true
+ return !dependency.shouldDisableDependents(context)
+ }
+
+ /** Returns the key of depended preference to decide the enabled state. */
+ fun dependencyOfEnabledState(context: Context): PreferenceMetadata? = null
+
+ /** Returns whether this preference's dependents should be disabled. */
+ fun shouldDisableDependents(context: Context): Boolean = !isEnabled(context)
+
+ /** Returns if the preference is persistent in datastore. */
+ fun isPersistent(context: Context): Boolean = this is PersistentPreference<*>
+
+ /**
+ * Returns if preference value backup is allowed (by default returns `true` if preference is
+ * persistent).
+ */
+ fun allowBackup(context: Context): Boolean = isPersistent(context)
+
+ /** Returns preference intent. */
+ fun intent(context: Context): Intent? = null
+
+ /** Returns preference order. */
+ fun order(context: Context): Int? = null
+
+ /**
+ * Returns the preference title.
+ *
+ * Implement [PreferenceTitleProvider] interface if title content is generated dynamically.
+ */
+ fun getPreferenceTitle(context: Context): CharSequence? =
+ when {
+ title != 0 -> context.getText(title)
+ this is PreferenceTitleProvider -> getTitle(context)
+ else -> null
+ }
+
+ /**
+ * Returns the preference summary.
+ *
+ * Implement [PreferenceSummaryProvider] interface if summary content is generated dynamically
+ * (e.g. summary is provided per preference value).
+ */
+ fun getPreferenceSummary(context: Context): CharSequence? =
+ when {
+ summary != 0 -> context.getText(summary)
+ this is PreferenceSummaryProvider -> getSummary(context)
+ else -> null
+ }
+}
+
+/** Metadata of preference group. */
+@AnyThread
+open class PreferenceGroup(override val key: String, override val title: Int) : PreferenceMetadata
+
+/** Metadata of preference screen. */
+@AnyThread
+interface PreferenceScreenMetadata : PreferenceMetadata {
+
+ /**
+ * The screen title resource, which precedes [getScreenTitle] if provided.
+ *
+ * By default, screen title is same with [title].
+ */
+ val screenTitle: Int
+ get() = title
+
+ /** Returns dynamic screen title, use [screenTitle] whenever possible. */
+ fun getScreenTitle(context: Context): CharSequence? = null
+
+ /** Returns the fragment class to show the preference screen. */
+ fun fragmentClass(): Class<out Fragment>?
+
+ /**
+ * Indicates if [getPreferenceHierarchy] returns a complete hierarchy of the preference screen.
+ *
+ * If `true`, the result of [getPreferenceHierarchy] will be used to inflate preference screen.
+ * Otherwise, it is an intermediate state called hybrid mode, preference hierarchy is
+ * represented by other ways (e.g. XML resource) and [PreferenceMetadata]s in
+ * [getPreferenceHierarchy] will only be used to bind UI widgets.
+ */
+ fun hasCompleteHierarchy(): Boolean = true
+
+ /**
+ * Returns the hierarchy of preference screen.
+ *
+ * The implementation MUST include all preferences into the hierarchy regardless of the runtime
+ * conditions. DO NOT check any condition (except compile time flag) before adding a preference.
+ */
+ fun getPreferenceHierarchy(context: Context): PreferenceHierarchy
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
new file mode 100644
index 000000000000..84014f191f68
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+
+/** Provides the associated preference screen key for binding. */
+interface PreferenceScreenBindingKeyProvider {
+
+ /** Returns the associated preference screen key. */
+ fun getPreferenceScreenBindingKey(context: Context): String?
+}
+
+/** Extra key to provide the preference screen key for binding. */
+const val EXTRA_BINDING_SCREEN_KEY = "settingslib:binding_screen_key"
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
new file mode 100644
index 000000000000..48798da57dae
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+import com.android.settingslib.datastore.KeyValueStore
+import com.google.common.base.Supplier
+import com.google.common.base.Suppliers
+import com.google.common.collect.ImmutableMap
+
+private typealias PreferenceScreenMap = ImmutableMap<String, PreferenceScreenMetadata>
+
+/** Registry of all available preference screens in the app. */
+object PreferenceScreenRegistry : ReadWritePermitProvider {
+
+ /** Provider of key-value store. */
+ private lateinit var keyValueStoreProvider: KeyValueStoreProvider
+
+ private var preferenceScreensSupplier: Supplier<PreferenceScreenMap> = Supplier {
+ ImmutableMap.of()
+ }
+
+ private val preferenceScreens: PreferenceScreenMap
+ get() = preferenceScreensSupplier.get()
+
+ private var readWritePermitProvider: ReadWritePermitProvider? = null
+
+ /** Sets the [KeyValueStoreProvider]. */
+ fun setKeyValueStoreProvider(keyValueStoreProvider: KeyValueStoreProvider) {
+ this.keyValueStoreProvider = keyValueStoreProvider
+ }
+
+ /**
+ * Returns the key-value store for given preference.
+ *
+ * Must call [setKeyValueStoreProvider] before invoking this method, otherwise
+ * [NullPointerException] is raised.
+ */
+ fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? =
+ keyValueStoreProvider.getKeyValueStore(context, preference)
+
+ /** Sets supplier to provide available preference screens. */
+ fun setPreferenceScreensSupplier(supplier: Supplier<List<PreferenceScreenMetadata>>) {
+ preferenceScreensSupplier =
+ Suppliers.memoize {
+ val screensBuilder = ImmutableMap.builder<String, PreferenceScreenMetadata>()
+ for (screen in supplier.get()) screensBuilder.put(screen.key, screen)
+ screensBuilder.buildOrThrow()
+ }
+ }
+
+ /** Sets available preference screens. */
+ fun setPreferenceScreens(vararg screens: PreferenceScreenMetadata) {
+ val screensBuilder = ImmutableMap.builder<String, PreferenceScreenMetadata>()
+ for (screen in screens) screensBuilder.put(screen.key, screen)
+ preferenceScreensSupplier = Suppliers.ofInstance(screensBuilder.buildOrThrow())
+ }
+
+ /** Returns [PreferenceScreenMetadata] of particular key. */
+ operator fun get(key: String?): PreferenceScreenMetadata? =
+ if (key != null) preferenceScreens[key] else null
+
+ /**
+ * Sets the provider to check read write permit. Read and write requests are denied by default.
+ */
+ fun setReadWritePermitProvider(readWritePermitProvider: ReadWritePermitProvider?) {
+ this.readWritePermitProvider = readWritePermitProvider
+ }
+
+ override fun getReadPermit(
+ context: Context,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) =
+ readWritePermitProvider?.getReadPermit(context, myUid, callingUid, preference)
+ ?: ReadWritePermit.DISALLOW
+
+ override fun getWritePermit(
+ context: Context,
+ value: Any?,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) =
+ readWritePermitProvider?.getWritePermit(context, value, myUid, callingUid, preference)
+ ?: ReadWritePermit.DISALLOW
+}
+
+/** Provider of [KeyValueStore]. */
+fun interface KeyValueStoreProvider {
+
+ /**
+ * Returns the key-value store for given preference.
+ *
+ * Here are some use cases:
+ * - provide the default storage for all preferences
+ * - determine the storage per preference keys or the interfaces implemented by the preference
+ */
+ fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore?
+}
+
+/** Provider of read and write permit. */
+interface ReadWritePermitProvider {
+
+ @ReadWritePermit
+ fun getReadPermit(
+ context: Context,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ): Int
+
+ @ReadWritePermit
+ fun getWritePermit(
+ context: Context,
+ value: Any?,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ): Int
+
+ companion object {
+ @JvmField
+ val ALLOW_ALL_READ_WRITE =
+ object : ReadWritePermitProvider {
+ override fun getReadPermit(
+ context: Context,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) = ReadWritePermit.ALLOW
+
+ override fun getWritePermit(
+ context: Context,
+ value: Any?,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) = ReadWritePermit.ALLOW
+ }
+ }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
new file mode 100644
index 000000000000..a3aa85df5325
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+
+/**
+ * Interface to provide dynamic preference title.
+ *
+ * Implement this interface implies that the preference title should not be cached for indexing.
+ */
+interface PreferenceTitleProvider {
+
+ /** Provides preference title. */
+ fun getTitle(context: Context): CharSequence?
+}
+
+/**
+ * Interface to provide dynamic preference summary.
+ *
+ * Implement this interface implies that the preference summary should not be cached for indexing.
+ */
+interface PreferenceSummaryProvider {
+
+ /** Provides preference summary. */
+ fun getSummary(context: Context): CharSequence?
+}
+
+/**
+ * Interface to provide the state of preference availability.
+ *
+ * UI framework normally does not show the preference widget if it is unavailable.
+ */
+interface PreferenceAvailabilityProvider {
+
+ /** Returns if the preference is available. */
+ fun isAvailable(context: Context): Boolean
+}
+
+/**
+ * Interface to provide the managed configuration state of the preference.
+ *
+ * See [Managed configurations](https://developer.android.com/work/managed-configurations) for the
+ * Android Enterprise support.
+ */
+interface PreferenceRestrictionProvider {
+
+ /** Returns if preference is restricted by managed configs. */
+ fun isRestricted(context: Context): Boolean
+}
+
+/**
+ * Preference lifecycle to deal with preference state.
+ *
+ * Implement this interface when preference depends on runtime conditions.
+ */
+interface PreferenceLifecycleProvider {
+
+ /**
+ * Called when preference is attached to UI.
+ *
+ * Subclass could override this API to register runtime condition listeners, and invoke
+ * `onPreferenceStateChanged(this)` on the given [preferenceStateObserver] to update UI when
+ * internal state (e.g. availability, enabled state, title, summary) is changed.
+ */
+ fun onAttach(context: Context, preferenceStateObserver: PreferenceStateObserver)
+
+ /**
+ * Called when preference is detached from UI.
+ *
+ * Clean up and release resource.
+ */
+ fun onDetach(context: Context)
+
+ /** Observer of preference state. */
+ interface PreferenceStateObserver {
+
+ /** Callbacks when preference state is changed. */
+ fun onPreferenceStateChanged(preference: PreferenceMetadata)
+ }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt
new file mode 100644
index 000000000000..ad996c7c8f86
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+import androidx.annotation.StringRes
+
+/**
+ * Common base class for preferences that have two selectable states, save a boolean value, and may
+ * have dependent preferences that are enabled/disabled based on the current state.
+ */
+interface TwoStatePreference : PreferenceMetadata, PersistentPreference<Boolean>, BooleanValue {
+
+ override fun shouldDisableDependents(context: Context) =
+ storage(context).getValue(key, Boolean::class.javaObjectType) != true ||
+ super.shouldDisableDependents(context)
+}
+
+/** A preference that provides a two-state toggleable option. */
+open class SwitchPreference
+@JvmOverloads
+constructor(
+ override val key: String,
+ @StringRes override val title: Int = 0,
+ @StringRes override val summary: Int = 0,
+) : TwoStatePreference
diff --git a/packages/SettingsLib/Preference/Android.bp b/packages/SettingsLib/Preference/Android.bp
new file mode 100644
index 000000000000..9665dbd17e2d
--- /dev/null
+++ b/packages/SettingsLib/Preference/Android.bp
@@ -0,0 +1,23 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "SettingsLibPreference-srcs",
+ srcs: ["src/**/*.kt"],
+}
+
+android_library {
+ name: "SettingsLibPreference",
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+ srcs: [":SettingsLibPreference-srcs"],
+ static_libs: [
+ "SettingsLibDataStore",
+ "SettingsLibMetadata",
+ "androidx.annotation_annotation",
+ "androidx.preference_preference",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Preference/AndroidManifest.xml b/packages/SettingsLib/Preference/AndroidManifest.xml
new file mode 100644
index 000000000000..2d7f7ba5ec40
--- /dev/null
+++ b/packages/SettingsLib/Preference/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.preference">
+
+ <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
new file mode 100644
index 000000000000..9be0e7194859
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import androidx.preference.DialogPreference
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import androidx.preference.SeekBarPreference
+import com.android.settingslib.metadata.DiscreteIntValue
+import com.android.settingslib.metadata.DiscreteValue
+import com.android.settingslib.metadata.PreferenceAvailabilityProvider
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.RangeValue
+
+/** Binding of preference widget and preference metadata. */
+interface PreferenceBinding {
+
+ /**
+ * Provides a new [Preference] widget instance.
+ *
+ * By default, it returns a new [Preference] object. Subclass could override this method to
+ * provide customized widget and do **one-off** initialization (e.g.
+ * [Preference.setOnPreferenceClickListener]). To update widget everytime when state is changed,
+ * override the [bind] method.
+ *
+ * Notes:
+ * - DO NOT set any properties defined in [PreferenceMetadata]. For example,
+ * title/summary/icon/extras/isEnabled/isVisible/isPersistent/dependency. These properties
+ * will be reset by [bind].
+ * - Override [bind] if needed to provide more information for customized widget.
+ */
+ fun createWidget(context: Context): Preference = Preference(context)
+
+ /**
+ * Binds preference widget with given metadata.
+ *
+ * Whenever metadata state is changed, this callback is invoked to update widget. By default,
+ * the common states like title, summary, enabled, etc. are already applied. Subclass should
+ * override this method to bind more data (e.g. read preference value from storage and apply it
+ * to widget).
+ *
+ * @param preference preference widget created by [createWidget]
+ * @param metadata metadata to apply
+ */
+ fun bind(preference: Preference, metadata: PreferenceMetadata) {
+ metadata.apply {
+ preference.key = key
+ if (icon != 0) {
+ preference.setIcon(icon)
+ } else {
+ preference.icon = null
+ }
+ val context = preference.context
+ preference.peekExtras()?.clear()
+ extras(context)?.let { preference.extras.putAll(it) }
+ preference.title = getPreferenceTitle(context)
+ preference.summary = getPreferenceSummary(context)
+ preference.isEnabled = isEnabled(context)
+ preference.isVisible =
+ (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false
+ preference.isPersistent = isPersistent(context)
+ metadata.order(context)?.let { preference.order = it }
+ // PreferenceRegistry will notify dependency change, so we do not need to set
+ // dependency here. This simplifies dependency management and avoid the
+ // IllegalStateException when call Preference.setDependency
+ preference.dependency = null
+ if (preference !is PreferenceScreen) { // avoid recursive loop when build graph
+ preference.fragment = (this as? PreferenceScreenCreator)?.fragmentClass()?.name
+ preference.intent = intent(context)
+ }
+ if (preference is DialogPreference) {
+ preference.dialogTitle = preference.title
+ }
+ if (preference is ListPreference && this is DiscreteValue<*>) {
+ preference.setEntries(valuesDescription)
+ if (this is DiscreteIntValue) {
+ val intValues = context.resources.getIntArray(values)
+ preference.entryValues = Array(intValues.size) { intValues[it].toString() }
+ } else {
+ preference.setEntryValues(values)
+ }
+ } else if (preference is SeekBarPreference && this is RangeValue) {
+ preference.min = minValue
+ preference.max = maxValue
+ preference.seekBarIncrement = incrementStep
+ }
+ }
+ }
+}
+
+/** Abstract preference screen to provide preference hierarchy and binding factory. */
+interface PreferenceScreenCreator : PreferenceScreenMetadata, PreferenceScreenProvider {
+
+ val preferenceBindingFactory: PreferenceBindingFactory
+ get() = DefaultPreferenceBindingFactory
+
+ override fun createPreferenceScreen(factory: PreferenceScreenFactory) =
+ factory.getOrCreatePreferenceScreen().apply {
+ inflatePreferenceHierarchy(preferenceBindingFactory, getPreferenceHierarchy(context))
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
new file mode 100644
index 000000000000..4c2e1ba683f6
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import com.android.settingslib.metadata.PreferenceGroup
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.SwitchPreference
+
+/** Factory to map [PreferenceMetadata] to [PreferenceBinding]. */
+interface PreferenceBindingFactory {
+
+ /** Returns the [PreferenceBinding] associated with the [PreferenceMetadata]. */
+ fun getPreferenceBinding(metadata: PreferenceMetadata): PreferenceBinding?
+}
+
+/** Default [PreferenceBindingFactory]. */
+object DefaultPreferenceBindingFactory : PreferenceBindingFactory {
+
+ override fun getPreferenceBinding(metadata: PreferenceMetadata) =
+ metadata as? PreferenceBinding
+ ?: when (metadata) {
+ is SwitchPreference -> SwitchPreferenceBinding.INSTANCE
+ is PreferenceGroup -> PreferenceGroupBinding.INSTANCE
+ is PreferenceScreenCreator -> PreferenceScreenBinding.INSTANCE
+ else -> DefaultPreferenceBinding
+ }
+}
+
+/** A preference key based binding factory. */
+class KeyedPreferenceBindingFactory(private val bindings: Map<String, PreferenceBinding>) :
+ PreferenceBindingFactory {
+
+ override fun getPreferenceBinding(metadata: PreferenceMetadata) =
+ bindings[metadata.key] ?: DefaultPreferenceBindingFactory.getPreferenceBinding(metadata)
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
new file mode 100644
index 000000000000..ede970e42e72
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreferenceCompat
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.PreferenceTitleProvider
+
+/** Binding of preference group associated with [PreferenceCategory]. */
+interface PreferenceScreenBinding : PreferenceBinding {
+
+ override fun bind(preference: Preference, metadata: PreferenceMetadata) {
+ super.bind(preference, metadata)
+ val context = preference.context
+ val screenMetadata = metadata as PreferenceScreenMetadata
+ // Pass the preference key to fragment, so that the fragment could find associated
+ // preference screen registered in PreferenceScreenRegistry
+ preference.extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key)
+ if (preference is PreferenceScreen) {
+ val screenTitle = screenMetadata.screenTitle
+ preference.title =
+ if (screenTitle != 0) {
+ context.getString(screenTitle)
+ } else {
+ screenMetadata.getScreenTitle(context)
+ ?: (this as? PreferenceTitleProvider)?.getTitle(context)
+ }
+ }
+ }
+
+ companion object {
+ @JvmStatic val INSTANCE = object : PreferenceScreenBinding {}
+ }
+}
+
+/** Binding of preference group associated with [PreferenceCategory]. */
+interface PreferenceGroupBinding : PreferenceBinding {
+
+ override fun createWidget(context: Context) = PreferenceCategory(context)
+
+ companion object {
+ @JvmStatic val INSTANCE = object : PreferenceGroupBinding {}
+ }
+}
+
+/** A boolean value type preference associated with [SwitchPreferenceCompat]. */
+interface SwitchPreferenceBinding : PreferenceBinding {
+
+ override fun createWidget(context: Context): Preference = SwitchPreferenceCompat(context)
+
+ override fun bind(preference: Preference, metadata: PreferenceMetadata) {
+ super.bind(preference, metadata)
+ (metadata as? PersistentPreference<*>)
+ ?.storage(preference.context)
+ ?.getValue(metadata.key, Boolean::class.javaObjectType)
+ ?.let { (preference as SwitchPreferenceCompat).isChecked = it }
+ }
+
+ companion object {
+ @JvmStatic val INSTANCE = object : SwitchPreferenceBinding {}
+ }
+}
+
+/** Default [PreferenceBinding] for [Preference]. */
+object DefaultPreferenceBinding : PreferenceBinding
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
new file mode 100644
index 000000000000..02acfca6f149
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import androidx.preference.PreferenceDataStore
+import com.android.settingslib.datastore.KeyValueStore
+
+/** Adapter to translate [KeyValueStore] into [PreferenceDataStore]. */
+class PreferenceDataStoreAdapter(private val keyValueStore: KeyValueStore) : PreferenceDataStore() {
+
+ override fun getBoolean(key: String, defValue: Boolean): Boolean =
+ keyValueStore.getValue(key, Boolean::class.javaObjectType) ?: defValue
+
+ override fun getFloat(key: String, defValue: Float): Float =
+ keyValueStore.getValue(key, Float::class.javaObjectType) ?: defValue
+
+ override fun getInt(key: String, defValue: Int): Int =
+ keyValueStore.getValue(key, Int::class.javaObjectType) ?: defValue
+
+ override fun getLong(key: String, defValue: Long): Long =
+ keyValueStore.getValue(key, Long::class.javaObjectType) ?: defValue
+
+ override fun getString(key: String, defValue: String?): String? =
+ keyValueStore.getValue(key, String::class.javaObjectType) ?: defValue
+
+ override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
+ (keyValueStore.getValue(key, Set::class.javaObjectType) as Set<String>?) ?: defValues
+
+ override fun putBoolean(key: String, value: Boolean) =
+ keyValueStore.setValue(key, Boolean::class.javaObjectType, value)
+
+ override fun putFloat(key: String, value: Float) =
+ keyValueStore.setValue(key, Float::class.javaObjectType, value)
+
+ override fun putInt(key: String, value: Int) =
+ keyValueStore.setValue(key, Int::class.javaObjectType, value)
+
+ override fun putLong(key: String, value: Long) =
+ keyValueStore.setValue(key, Long::class.javaObjectType, value)
+
+ override fun putString(key: String, value: String?) =
+ keyValueStore.setValue(key, String::class.javaObjectType, value)
+
+ override fun putStringSet(key: String, values: Set<String>?) =
+ keyValueStore.setValue(key, Set::class.javaObjectType, values)
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
new file mode 100644
index 000000000000..207200998b05
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.XmlRes
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
+import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.android.settingslib.preference.PreferenceScreenBindingHelper.Companion.bindRecursively
+
+/** Fragment to display a preference screen. */
+open class PreferenceFragment :
+ PreferenceFragmentCompat(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider {
+
+ private var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ preferenceScreen = createPreferenceScreen()
+ }
+
+ fun createPreferenceScreen(): PreferenceScreen? =
+ createPreferenceScreen(PreferenceScreenFactory(this))
+
+ override fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen? {
+ val context = factory.context
+ fun createPreferenceScreenFromResource() =
+ factory.inflate(getPreferenceScreenResId(context))
+
+ if (!usePreferenceScreenMetadata()) return createPreferenceScreenFromResource()
+
+ val screenKey = getPreferenceScreenBindingKey(context)
+ val screenCreator =
+ (PreferenceScreenRegistry[screenKey] as? PreferenceScreenCreator)
+ ?: return createPreferenceScreenFromResource()
+
+ val preferenceBindingFactory = screenCreator.preferenceBindingFactory
+ val preferenceHierarchy = screenCreator.getPreferenceHierarchy(context)
+ val preferenceScreen =
+ if (screenCreator.hasCompleteHierarchy()) {
+ factory.getOrCreatePreferenceScreen().apply {
+ inflatePreferenceHierarchy(preferenceBindingFactory, preferenceHierarchy)
+ }
+ } else {
+ createPreferenceScreenFromResource()?.also {
+ bindRecursively(it, preferenceBindingFactory, preferenceHierarchy)
+ } ?: return null
+ }
+ preferenceScreenBindingHelper =
+ PreferenceScreenBindingHelper(
+ context,
+ preferenceBindingFactory,
+ preferenceScreen,
+ preferenceHierarchy,
+ )
+ return preferenceScreen
+ }
+
+ /**
+ * Returns if preference screen metadata can be used to set up preference screen.
+ *
+ * This is for flagging purpose. If false (e.g. flag is disabled), xml resource is used to build
+ * preference screen.
+ */
+ protected open fun usePreferenceScreenMetadata(): Boolean = true
+
+ /** Returns the xml resource to create preference screen. */
+ @XmlRes protected open fun getPreferenceScreenResId(context: Context): Int = 0
+
+ override fun getPreferenceScreenBindingKey(context: Context): String? =
+ arguments?.getString(EXTRA_BINDING_SCREEN_KEY)
+
+ override fun onDestroy() {
+ preferenceScreenBindingHelper?.close()
+ super.onDestroy()
+ }
+
+ companion object {
+ /** Returns [PreferenceFragment] instance to display the preference screen of given key. */
+ fun of(screenKey: String): PreferenceFragment? {
+ val screenMetadata = PreferenceScreenRegistry[screenKey] ?: return null
+ if (
+ screenMetadata is PreferenceScreenCreator && screenMetadata.hasCompleteHierarchy()
+ ) {
+ return PreferenceFragment().apply {
+ arguments = Bundle().apply { putString(EXTRA_BINDING_SCREEN_KEY, screenKey) }
+ }
+ }
+ return null
+ }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
new file mode 100644
index 000000000000..5ef7823a4745
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import androidx.preference.PreferenceDataStore
+import androidx.preference.PreferenceGroup
+import com.android.settingslib.datastore.KeyValueStore
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceMetadata
+
+/** Inflates [PreferenceHierarchy] into given [PreferenceGroup] recursively. */
+fun PreferenceGroup.inflatePreferenceHierarchy(
+ preferenceBindingFactory: PreferenceBindingFactory,
+ hierarchy: PreferenceHierarchy,
+ storages: MutableMap<KeyValueStore, PreferenceDataStore> = mutableMapOf(),
+) {
+ fun PreferenceMetadata.preferenceBinding() = preferenceBindingFactory.getPreferenceBinding(this)
+
+ hierarchy.metadata.let { it.preferenceBinding()?.bind(this, it) }
+ hierarchy.forEach {
+ val metadata = it.metadata
+ val preferenceBinding = metadata.preferenceBinding() ?: return@forEach
+ val widget = preferenceBinding.createWidget(context)
+ if (it is PreferenceHierarchy) {
+ val preferenceGroup = widget as PreferenceGroup
+ // MUST add preference before binding, otherwise exception is raised when add child
+ addPreference(preferenceGroup)
+ preferenceGroup.inflatePreferenceHierarchy(preferenceBindingFactory, it)
+ } else {
+ preferenceBinding.bind(widget, metadata)
+ (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage ->
+ widget.preferenceDataStore =
+ storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) }
+ }
+ // MUST add preference after binding for persistent preference to get initial value
+ // (preference key is set within bind method)
+ addPreference(widget)
+ }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
new file mode 100644
index 000000000000..3610894c3fc0
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import androidx.preference.Preference
+import androidx.preference.PreferenceGroup
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.datastore.KeyedDataObservable
+import com.android.settingslib.datastore.KeyedObservable
+import com.android.settingslib.datastore.KeyedObserver
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceLifecycleProvider
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.google.common.collect.ImmutableMap
+import com.google.common.collect.ImmutableMultimap
+import java.util.concurrent.Executor
+
+/**
+ * Helper to bind preferences on given [preferenceScreen].
+ *
+ * When there is any preference change event detected (e.g. preference value changed, runtime
+ * states, dependency is updated), this helper class will re-bind [PreferenceMetadata] to update
+ * widget UI.
+ */
+class PreferenceScreenBindingHelper(
+ context: Context,
+ private val preferenceBindingFactory: PreferenceBindingFactory,
+ private val preferenceScreen: PreferenceScreen,
+ preferenceHierarchy: PreferenceHierarchy,
+) : KeyedDataObservable<String>(), AutoCloseable {
+
+ private val handler = Handler(Looper.getMainLooper())
+ private val executor =
+ object : Executor {
+ override fun execute(command: Runnable) {
+ handler.post(command)
+ }
+ }
+
+ private val preferences: ImmutableMap<String, PreferenceMetadata>
+ private val dependencies: ImmutableMultimap<String, String>
+ private val storages = mutableSetOf<KeyedObservable<String>>()
+
+ private val preferenceObserver: KeyedObserver<String?>
+
+ private val storageObserver =
+ KeyedObserver<String?> { key, _ ->
+ if (key != null) {
+ notifyChange(key, CHANGE_REASON_VALUE)
+ }
+ }
+
+ private val stateObserver =
+ object : PreferenceLifecycleProvider.PreferenceStateObserver {
+ override fun onPreferenceStateChanged(preference: PreferenceMetadata) {
+ notifyChange(preference.key, CHANGE_REASON_STATE)
+ }
+ }
+
+ init {
+ val preferencesBuilder = ImmutableMap.builder<String, PreferenceMetadata>()
+ val dependenciesBuilder = ImmutableMultimap.builder<String, String>()
+ fun PreferenceMetadata.addDependency(dependency: PreferenceMetadata) {
+ dependenciesBuilder.put(key, dependency.key)
+ }
+
+ fun PreferenceMetadata.add() {
+ preferencesBuilder.put(key, this)
+ dependencyOfEnabledState(context)?.addDependency(this)
+ if (this is PreferenceLifecycleProvider) onAttach(context, stateObserver)
+ if (this is PersistentPreference<*>) storages.add(storage(context))
+ }
+
+ fun PreferenceHierarchy.addPreferences() {
+ metadata.add()
+ forEach {
+ if (it is PreferenceHierarchy) {
+ it.addPreferences()
+ } else {
+ it.metadata.add()
+ }
+ }
+ }
+
+ preferenceHierarchy.addPreferences()
+ this.preferences = preferencesBuilder.buildOrThrow()
+ this.dependencies = dependenciesBuilder.build()
+
+ preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) }
+ addObserver(preferenceObserver, executor)
+ for (storage in storages) storage.addObserver(storageObserver, executor)
+ }
+
+ private fun onPreferenceChange(key: String?, reason: Int) {
+ if (key == null) return
+
+ // bind preference to update UI
+ preferenceScreen.findPreference<Preference>(key)?.let {
+ preferenceBindingFactory.bind(it, preferences[key])
+ }
+
+ // check reason to avoid potential infinite loop
+ if (reason != CHANGE_REASON_DEPENDENT) {
+ notifyDependents(key, mutableSetOf())
+ }
+ }
+
+ /** Notifies dependents recursively. */
+ private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) {
+ if (!notifiedKeys.add(key)) return
+ for (dependency in dependencies[key]) {
+ notifyChange(dependency, CHANGE_REASON_DEPENDENT)
+ notifyDependents(dependency, notifiedKeys)
+ }
+ }
+
+ override fun close() {
+ removeObserver(preferenceObserver)
+ val context = preferenceScreen.context
+ for (preference in preferences.values) {
+ if (preference is PreferenceLifecycleProvider) preference.onDetach(context)
+ }
+ for (storage in storages) storage.removeObserver(storageObserver)
+ }
+
+ companion object {
+ /** Preference value is changed. */
+ private const val CHANGE_REASON_VALUE = 0
+ /** Preference state (title/summary, enable state, etc.) is changed. */
+ private const val CHANGE_REASON_STATE = 1
+ /** Dependent preference state is changed. */
+ private const val CHANGE_REASON_DEPENDENT = 2
+
+ /** Updates preference screen that has incomplete hierarchy. */
+ @JvmStatic
+ fun bind(preferenceScreen: PreferenceScreen) {
+ PreferenceScreenRegistry[preferenceScreen.key]?.run {
+ if (!hasCompleteHierarchy()) {
+ val preferenceBindingFactory =
+ (this as? PreferenceScreenCreator)?.preferenceBindingFactory ?: return
+ bindRecursively(
+ preferenceScreen,
+ preferenceBindingFactory,
+ getPreferenceHierarchy(preferenceScreen.context),
+ )
+ }
+ }
+ }
+
+ internal fun bindRecursively(
+ preferenceScreen: PreferenceScreen,
+ preferenceBindingFactory: PreferenceBindingFactory,
+ preferenceHierarchy: PreferenceHierarchy,
+ ) =
+ preferenceScreen.bindRecursively(
+ preferenceBindingFactory,
+ preferenceHierarchy.getAllPreferences().associateBy { it.key },
+ )
+
+ private fun PreferenceGroup.bindRecursively(
+ preferenceBindingFactory: PreferenceBindingFactory,
+ preferences: Map<String, PreferenceMetadata>,
+ ) {
+ preferenceBindingFactory.bind(this, preferences[key])
+ val count = preferenceCount
+ for (index in 0 until count) {
+ val preference = getPreference(index)
+ if (preference is PreferenceGroup) {
+ preference.bindRecursively(preferenceBindingFactory, preferences)
+ } else {
+ preferenceBindingFactory.bind(preference, preferences[preference.key])
+ }
+ }
+ }
+
+ private fun PreferenceBindingFactory.bind(
+ preference: Preference,
+ metadata: PreferenceMetadata?,
+ ) = metadata?.let { getPreferenceBinding(it)?.bind(preference, it) }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
new file mode 100644
index 000000000000..7f99d7a9bbdd
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+
+/** Factory to create preference screen. */
+class PreferenceScreenFactory {
+ /** Preference manager to create/inflate preference screen. */
+ val preferenceManager: PreferenceManager
+
+ /**
+ * Optional existing hierarchy to merge the new hierarchies into.
+ *
+ * Provide existing hierarchy will preserve the internal state (e.g. scrollbar position) for
+ * [PreferenceFragmentCompat].
+ */
+ private val rootScreen: PreferenceScreen?
+
+ /**
+ * Factory constructor from preference fragment.
+ *
+ * The fragment must be within a valid lifecycle.
+ */
+ constructor(preferenceFragment: PreferenceFragmentCompat) {
+ preferenceManager = preferenceFragment.preferenceManager
+ rootScreen = preferenceFragment.preferenceScreen
+ }
+
+ /** Factory constructor from [Context]. */
+ constructor(context: Context) : this(PreferenceManager(context))
+
+ /** Factory constructor from [PreferenceManager]. */
+ constructor(preferenceManager: PreferenceManager) {
+ this.preferenceManager = preferenceManager
+ rootScreen = null
+ }
+
+ /** Context of the factory to create preference screen. */
+ val context: Context
+ get() = preferenceManager.context
+
+ /** Returns the existing hierarchy or create a new empty preference screen. */
+ fun getOrCreatePreferenceScreen(): PreferenceScreen =
+ rootScreen ?: preferenceManager.createPreferenceScreen(context)
+
+ /**
+ * Inflates [PreferenceScreen] from xml resource.
+ *
+ * @param xmlRes The resource ID of the XML to inflate
+ * @return The root hierarchy (if one was not provided, the new hierarchy's root)
+ */
+ fun inflate(xmlRes: Int): PreferenceScreen? =
+ if (xmlRes != 0) {
+ preferenceManager.inflateFromResource(preferenceManager.context, xmlRes, rootScreen)
+ } else {
+ rootScreen
+ }
+
+ /**
+ * Creates [PreferenceScreen] of given key.
+ *
+ * The screen must be registered in [PreferenceScreenFactory] and provide a complete hierarchy.
+ */
+ fun createBindingScreen(screenKey: String?): PreferenceScreen? {
+ val metadata = PreferenceScreenRegistry[screenKey] ?: return null
+ if (metadata is PreferenceScreenCreator && metadata.hasCompleteHierarchy()) {
+ return metadata.createPreferenceScreen(this)
+ }
+ return null
+ }
+
+ companion object {
+ /** Creates [PreferenceScreen] from [PreferenceScreenRegistry]. */
+ @JvmStatic
+ fun createBindingScreen(preference: Preference): PreferenceScreen? {
+ val preferenceScreenCreator =
+ (PreferenceScreenRegistry[preference.key] as? PreferenceScreenCreator)
+ ?: return null
+ if (!preferenceScreenCreator.hasCompleteHierarchy()) return null
+ val factory = PreferenceScreenFactory(preference.context)
+ val preferenceScreen = preferenceScreenCreator.createPreferenceScreen(factory)
+ factory.preferenceManager.setPreferences(preferenceScreen)
+ return preferenceScreen
+ }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt
new file mode 100644
index 000000000000..057329293796
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import androidx.preference.PreferenceScreen
+
+/**
+ * Interface to provide [PreferenceScreen].
+ *
+ * When implemented by Activity/Fragment, the Activity/Fragment [Context] APIs (e.g. `getContext()`,
+ * `getActivity()`) MUST not be used: preference screen creation could happen in background service,
+ * where the Activity/Fragment lifecycle callbacks (`onCreate`, `onDestroy`, etc.) are not invoked
+ * and context APIs return null.
+ */
+interface PreferenceScreenProvider {
+
+ /**
+ * Creates [PreferenceScreen].
+ *
+ * Preference screen creation could happen in background service. The implementation MUST use
+ * [PreferenceScreenFactory.context] to obtain context.
+ */
+ fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen?
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt
new file mode 100644
index 000000000000..65adec4a71a8
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings
+
+/** The contract between the device settings provider services and Settings. */
+object DeviceSettingContract {
+ const val INVISIBLE_PROFILES = "INVISIBLE_PROFILES"
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
index 457d6a3a714d..769b6e6796f9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
@@ -22,6 +22,7 @@ import android.text.TextUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference
import com.android.settingslib.bluetooth.devicesettings.DeviceSetting
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingContract
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig
@@ -30,6 +31,9 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingHelpPrefere
import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference
import com.android.settingslib.bluetooth.devicesettings.ToggleInfo
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.AppProvidedItem
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
@@ -103,9 +107,18 @@ class DeviceSettingRepositoryImpl(
private fun DeviceSettingItem.toModel(): DeviceSettingConfigItemModel {
return if (!TextUtils.isEmpty(preferenceKey)) {
- DeviceSettingConfigItemModel.BuiltinItem(settingId, preferenceKey!!)
+ if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) {
+ BluetoothProfilesItem(
+ settingId,
+ preferenceKey!!,
+ extras.getStringArrayList(DeviceSettingContract.INVISIBLE_PROFILES)
+ ?: emptyList()
+ )
+ } else {
+ CommonBuiltinItem(settingId, preferenceKey!!)
+ }
} else {
- DeviceSettingConfigItemModel.AppProvidedItem(settingId)
+ AppProvidedItem(settingId)
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index 33beb06e2ed5..7eae5b2a1f5f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -23,6 +23,7 @@ import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.IInterface
+import android.text.TextUtils
import android.util.Log
import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
@@ -84,6 +85,10 @@ class DeviceSettingServiceConnection(
}
setAction(intentAction)
}
+
+ fun isValid(): Boolean {
+ return !TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(intentAction)
+ }
}
private var isServiceEnabled =
@@ -96,7 +101,8 @@ class DeviceSettingServiceConnection(
} else if (allStatus.all { it is ServiceConnectionStatus.Connected }) {
allStatus
.filterIsInstance<
- ServiceConnectionStatus.Connected<IDeviceSettingsProviderService>
+ ServiceConnectionStatus.Connected<
+ IDeviceSettingsProviderService>
>()
.all { it.service.serviceStatus?.enabled == true }
} else {
@@ -215,6 +221,7 @@ class DeviceSettingServiceConnection(
)
}
}
+ ?.filter { it.isValid() }
?.distinct()
?.associateBy(
{ it },
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
index c1ac763929cd..08fb3fb8fb22 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
@@ -36,10 +36,23 @@ sealed interface DeviceSettingConfigItemModel {
@DeviceSettingId val settingId: Int
/** A built-in item in Settings. */
- data class BuiltinItem(
- @DeviceSettingId override val settingId: Int,
- val preferenceKey: String?
- ) : DeviceSettingConfigItemModel
+ sealed interface BuiltinItem : DeviceSettingConfigItemModel {
+ @DeviceSettingId override val settingId: Int
+ val preferenceKey: String
+
+ /** A general built-in item in Settings. */
+ data class CommonBuiltinItem(
+ @DeviceSettingId override val settingId: Int,
+ override val preferenceKey: String,
+ ) : BuiltinItem
+
+ /** A bluetooth profiles in Settings. */
+ data class BluetoothProfilesItem(
+ @DeviceSettingId override val settingId: Int,
+ override val preferenceKey: String,
+ val invisibleProfiles: List<String>,
+ ) : BuiltinItem
+ }
/** A remote item provided by other apps. */
data class AppProvidedItem(@DeviceSettingId override val settingId: Int) :
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
index ce155b5c0fa4..81b56343ceed 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
@@ -91,7 +91,9 @@ class DeviceSettingRepositoryTest {
`when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS)
`when`(
bluetoothDevice.getMetadata(
- DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS
+ )
+ )
.thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray())
`when`(configService.queryLocalInterface(anyString())).thenReturn(configService)
@@ -114,7 +116,8 @@ class DeviceSettingRepositoryTest {
connection.onServiceConnected(
ComponentName(
SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1,
- SETTING_PROVIDER_SERVICE_CLASS_NAME_1),
+ SETTING_PROVIDER_SERVICE_CLASS_NAME_1,
+ ),
settingProviderService1,
)
SETTING_PROVIDER_SERVICE_INTENT_ACTION_2 ->
@@ -146,16 +149,24 @@ class DeviceSettingRepositoryTest {
fun getDeviceSettingsConfig_withMetadata_success() {
testScope.runTest {
`when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
val config = underTest.getDeviceSettingsConfig(cachedDevice)
assertConfig(config!!, DEVICE_SETTING_CONFIG)
+ assertThat(config.mainItems[0])
+ .isInstanceOf(DeviceSettingConfigItemModel.AppProvidedItem::class.java)
+ assertThat(config.mainItems[1])
+ .isInstanceOf(
+ DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem::class.java
+ )
+ assertThat(config.mainItems[2])
+ .isInstanceOf(
+ DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem::class.java
+ )
}
}
@@ -163,16 +174,16 @@ class DeviceSettingRepositoryTest {
fun getDeviceSettingsConfig_noMetadata_returnNull() {
testScope.runTest {
`when`(
- bluetoothDevice.getMetadata(
- DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ bluetoothDevice.getMetadata(
+ DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS
+ )
+ )
.thenReturn("".toByteArray())
`when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
val config = underTest.getDeviceSettingsConfig(cachedDevice)
@@ -184,12 +195,10 @@ class DeviceSettingRepositoryTest {
fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() {
testScope.runTest {
`when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(false)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(false))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
val config = underTest.getDeviceSettingsConfig(cachedDevice)
@@ -219,12 +228,10 @@ class DeviceSettingRepositoryTest {
.getArgument<IDeviceSettingsListener>(1)
.onDeviceSettingsChanged(listOf(DEVICE_SETTING_1))
}
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
var setting: DeviceSettingModel? = null
underTest
@@ -247,12 +254,10 @@ class DeviceSettingRepositoryTest {
.getArgument<IDeviceSettingsListener>(1)
.onDeviceSettingsChanged(listOf(DEVICE_SETTING_2))
}
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
var setting: DeviceSettingModel? = null
underTest
@@ -270,17 +275,15 @@ class DeviceSettingRepositoryTest {
testScope.runTest {
`when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
`when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
- input ->
+ input ->
input
.getArgument<IDeviceSettingsListener>(1)
.onDeviceSettingsChanged(listOf(DEVICE_SETTING_HELP))
}
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
var setting: DeviceSettingModel? = null
underTest
@@ -324,12 +327,10 @@ class DeviceSettingRepositoryTest {
.getArgument<IDeviceSettingsListener>(1)
.onDeviceSettingsChanged(listOf(DEVICE_SETTING_1))
}
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
var setting: DeviceSettingModel? = null
underTest
@@ -347,8 +348,10 @@ class DeviceSettingRepositoryTest {
DeviceSettingState.Builder()
.setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER)
.setPreferenceState(
- ActionSwitchPreferenceState.Builder().setChecked(false).build())
- .build())
+ ActionSwitchPreferenceState.Builder().setChecked(false).build()
+ )
+ .build(),
+ )
}
}
@@ -362,12 +365,10 @@ class DeviceSettingRepositoryTest {
.getArgument<IDeviceSettingsListener>(1)
.onDeviceSettingsChanged(listOf(DEVICE_SETTING_2))
}
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
var setting: DeviceSettingModel? = null
underTest
@@ -385,8 +386,10 @@ class DeviceSettingRepositoryTest {
DeviceSettingState.Builder()
.setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC)
.setPreferenceState(
- MultiTogglePreferenceState.Builder().setState(2).build())
- .build())
+ MultiTogglePreferenceState.Builder().setState(2).build()
+ )
+ .build(),
+ )
}
}
@@ -437,7 +440,7 @@ class DeviceSettingRepositoryTest {
private fun assertConfig(
actual: DeviceSettingConfigModel,
- serviceResponse: DeviceSettingsConfig
+ serviceResponse: DeviceSettingsConfig,
) {
assertThat(actual.mainItems.size).isEqualTo(serviceResponse.mainContentItems.size)
for (i in 0..<actual.mainItems.size) {
@@ -451,7 +454,7 @@ class DeviceSettingRepositoryTest {
private fun assertConfigItem(
actual: DeviceSettingConfigItemModel,
- serviceResponse: DeviceSettingItem
+ serviceResponse: DeviceSettingItem,
) {
assertThat(actual.settingId).isEqualTo(serviceResponse.settingId)
}
@@ -485,24 +488,43 @@ class DeviceSettingRepositoryTest {
"</DEVICE_SETTINGS_CONFIG_ACTION>"
val DEVICE_INFO = DeviceInfo.Builder().setBluetoothAddress(BLUETOOTH_ADDRESS).build()
const val DEVICE_SETTING_ID_HELP = 12345
- val DEVICE_SETTING_ITEM_1 =
+ val DEVICE_SETTING_APP_PROVIDED_ITEM_1 =
DeviceSettingItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1,
SETTING_PROVIDER_SERVICE_CLASS_NAME_1,
- SETTING_PROVIDER_SERVICE_INTENT_ACTION_1)
- val DEVICE_SETTING_ITEM_2 =
+ SETTING_PROVIDER_SERVICE_INTENT_ACTION_1,
+ )
+ val DEVICE_SETTING_APP_PROVIDED_ITEM_2 =
DeviceSettingItem(
DeviceSettingId.DEVICE_SETTING_ID_ANC,
SETTING_PROVIDER_SERVICE_PACKAGE_NAME_2,
SETTING_PROVIDER_SERVICE_CLASS_NAME_2,
- SETTING_PROVIDER_SERVICE_INTENT_ACTION_2)
+ SETTING_PROVIDER_SERVICE_INTENT_ACTION_2,
+ )
+ val DEVICE_SETTING_BUILT_IN_ITEM =
+ DeviceSettingItem(
+ DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_AUDIO_DEVICE_TYPE_GROUP,
+ "",
+ "",
+ "",
+ "device_type",
+ )
+ val DEVICE_SETTING_BUILT_IN_BT_PROFILES_ITEM =
+ DeviceSettingItem(
+ DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
+ "",
+ "",
+ "",
+ "bluetooth_profiles",
+ )
val DEVICE_SETTING_HELP_ITEM =
DeviceSettingItem(
DEVICE_SETTING_ID_HELP,
SETTING_PROVIDER_SERVICE_PACKAGE_NAME_2,
SETTING_PROVIDER_SERVICE_CLASS_NAME_2,
- SETTING_PROVIDER_SERVICE_INTENT_ACTION_2)
+ SETTING_PROVIDER_SERVICE_INTENT_ACTION_2,
+ )
val DEVICE_SETTING_1 =
DeviceSetting.Builder()
.setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER)
@@ -511,7 +533,8 @@ class DeviceSettingRepositoryTest {
.setTitle("title1")
.setHasSwitch(true)
.setAllowedChangingState(true)
- .build())
+ .build()
+ )
.build()
val DEVICE_SETTING_2 =
DeviceSetting.Builder()
@@ -524,22 +547,30 @@ class DeviceSettingRepositoryTest {
ToggleInfo.Builder()
.setLabel("label1")
.setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
- .build())
+ .build()
+ )
.addToggleInfo(
ToggleInfo.Builder()
.setLabel("label2")
.setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
- .build())
- .build())
+ .build()
+ )
+ .build()
+ )
+ .build()
+ val DEVICE_SETTING_HELP =
+ DeviceSetting.Builder()
+ .setSettingId(DEVICE_SETTING_ID_HELP)
+ .setPreference(DeviceSettingHelpPreference.Builder().setIntent(Intent()).build())
.build()
- val DEVICE_SETTING_HELP = DeviceSetting.Builder()
- .setSettingId(DEVICE_SETTING_ID_HELP)
- .setPreference(DeviceSettingHelpPreference.Builder().setIntent(Intent()).build())
- .build()
val DEVICE_SETTING_CONFIG =
DeviceSettingsConfig(
- listOf(DEVICE_SETTING_ITEM_1),
- listOf(DEVICE_SETTING_ITEM_2),
+ listOf(
+ DEVICE_SETTING_APP_PROVIDED_ITEM_1,
+ DEVICE_SETTING_BUILT_IN_ITEM,
+ DEVICE_SETTING_BUILT_IN_BT_PROFILES_ITEM,
+ ),
+ listOf(DEVICE_SETTING_APP_PROVIDED_ITEM_2),
DEVICE_SETTING_HELP_ITEM,
)
}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d26a9066e075..a9e81c77acad 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -756,6 +756,7 @@ android_library {
"notification_flags_lib",
"PlatformComposeCore",
"PlatformComposeSceneTransitionLayout",
+ "PlatformComposeSceneTransitionLayoutTestsUtils",
"androidx.compose.runtime_runtime",
"androidx.compose.material3_material3",
"androidx.compose.material_material-icons-extended",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 892f778ff8c0..ad14035d9d0a 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -288,6 +288,16 @@ flag {
}
flag {
+ name: "qs_quick_rebind_active_tiles"
+ namespace: "systemui"
+ description: "Rebind active custom tiles quickly."
+ bug: "362526228"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "coroutine_tracing"
namespace: "systemui"
description: "Adds thread-local data to System UI's global coroutine scopes to "
@@ -606,16 +616,6 @@ flag {
}
flag {
- name: "screenshot_save_image_exporter"
- namespace: "systemui"
- description: "Save all screenshots using ImageExporter"
- bug: "352308052"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "screenshot_ui_controller_refactor"
namespace: "systemui"
description: "Simplify and refactor ScreenshotController"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 7fb88e8d1fcc..ae92d259d62b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -99,8 +99,8 @@ private fun SceneScope.BouncerScene(
BouncerContent(
viewModel,
dialogFactory,
- Modifier.sysuiResTag(Bouncer.TestTags.Root)
- .element(Bouncer.Elements.Content)
+ Modifier.element(Bouncer.Elements.Content)
+ .sysuiResTag(Bouncer.TestTags.Root)
.fillMaxSize()
)
}
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 3cb0d8af1ba4..df101c558dff 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
@@ -128,7 +128,11 @@ fun SceneContainer(
}
},
) {
- SceneTransitionLayout(state = state, modifier = modifier.fillMaxSize()) {
+ SceneTransitionLayout(
+ state = state,
+ modifier = modifier.fillMaxSize(),
+ swipeSourceDetector = viewModel.edgeDetector,
+ ) {
sceneByKey.forEach { (sceneKey, scene) ->
scene(
key = sceneKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index f3577fab8686..007b84a2954a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -395,14 +395,8 @@ private class DragControllerImpl(
return 0f
}
- fun animateTo(targetContent: T) {
- swipeAnimation.animateOffset(
- initialVelocity = velocity,
- targetContent = targetContent,
- )
- }
-
val fromContent = swipeAnimation.fromContent
+ val consumedVelocity: Float
if (canChangeContent) {
// If we are halfway between two contents, we check what the target will be based on the
// velocity and offset of the transition, then we launch the animation.
@@ -427,18 +421,16 @@ private class DragControllerImpl(
} else {
fromContent
}
-
- animateTo(targetContent = targetContent)
+ consumedVelocity = swipeAnimation.animateOffset(velocity, targetContent = targetContent)
} else {
// We are doing an overscroll preview animation between scenes.
check(fromContent == swipeAnimation.currentContent) {
"canChangeContent is false but currentContent != fromContent"
}
- animateTo(targetContent = fromContent)
+ consumedVelocity = swipeAnimation.animateOffset(velocity, targetContent = fromContent)
}
- // The onStop animation consumes any remaining velocity.
- return velocity
+ return consumedVelocity
}
/**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
index 2a09a77788e7..966bda410231 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
@@ -312,11 +312,16 @@ internal class SwipeAnimation<T : ContentKey>(
fun isAnimatingOffset(): Boolean = offsetAnimation != null
+ /**
+ * Animate the offset to a [targetContent], using the [initialVelocity] and an optional [spec]
+ *
+ * @return the velocity consumed
+ */
fun animateOffset(
initialVelocity: Float,
targetContent: T,
spec: AnimationSpec<Float>? = null,
- ) {
+ ): Float {
check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" }
val initialProgress = progress
@@ -374,7 +379,7 @@ internal class SwipeAnimation<T : ContentKey>(
if (skipAnimation) {
// Unblock the job.
offsetAnimationRunnable.complete(null)
- return
+ return 0f
}
val isTargetGreater = targetOffset > animatable.value
@@ -424,6 +429,9 @@ internal class SwipeAnimation<T : ContentKey>(
/* Ignore. */
}
}
+
+ // This animation always consumes the whole available velocity
+ return initialVelocity
}
/** An exception thrown during the animation to stop it immediately. */
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 79f82c948541..5b5935633166 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -1111,7 +1111,7 @@ class DraggableHandlerTest {
assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
// Release the finger.
- dragController.onDragStopped(velocity = -velocityThreshold)
+ dragController.onDragStopped(velocity = -velocityThreshold, expectedConsumed = false)
// Exhaust all coroutines *without advancing the clock*. Given that we are at progress >=
// 100% and that the overscroll on scene B is doing nothing, we are already idle.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index e2bdc49d590c..bb152086cdab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -30,10 +30,12 @@ import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.haptics.msdl.msdlPlayer
import com.android.systemui.res.R
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.testKosmos
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
@@ -89,6 +91,9 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() {
@Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
+ private val kosmos = testKosmos()
+ private val msdlPlayer = kosmos.msdlPlayer
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@@ -112,7 +117,8 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() {
mKeyguardMessageAreaControllerFactory,
mPostureController,
fakeFeatureFlags,
- mSelectedUserInteractor
+ mSelectedUserInteractor,
+ msdlPlayer,
)
mKeyguardPatternView.onAttachedToWindow()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java
index 8f9b7c8cbc45..12c866f0adb2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java
@@ -30,11 +30,11 @@ import android.graphics.Point;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.WindowManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
@@ -48,7 +48,7 @@ import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class)
public class MirrorWindowControlTest extends SysuiTestCase {
- @Mock WindowManager mWindowManager;
+ @Mock ViewCaptureAwareWindowManager mWindowManager;
View mView;
int mViewWidth;
int mViewHeight;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt
index dd85d9bd2d7c..fc57757c9a8c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt
@@ -20,11 +20,15 @@ import android.view.accessibility.CaptioningManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.userRepository
+import com.android.systemui.user.utils.FakeUserScopedService
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -39,10 +43,11 @@ import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
-@Suppress("UnspecifiedRegisterReceiverFlag")
@RunWith(AndroidJUnit4::class)
class CaptioningRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
@Captor
private lateinit var listenerCaptor: ArgumentCaptor<CaptioningManager.CaptioningChangeListener>
@@ -50,34 +55,33 @@ class CaptioningRepositoryTest : SysuiTestCase() {
private lateinit var underTest: CaptioningRepository
- private val testScope = TestScope()
-
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
underTest =
- CaptioningRepositoryImpl(
- captioningManager,
- testScope.testScheduler,
- testScope.backgroundScope
- )
+ with(kosmos) {
+ CaptioningRepositoryImpl(
+ FakeUserScopedService(captioningManager),
+ userRepository,
+ testScope.testScheduler,
+ applicationCoroutineScope,
+ )
+ }
}
@Test
fun isSystemAudioCaptioningEnabled_change_repositoryEmits() {
- testScope.runTest {
- `when`(captioningManager.isEnabled).thenReturn(false)
- val isSystemAudioCaptioningEnabled = mutableListOf<Boolean>()
- underTest.isSystemAudioCaptioningEnabled
- .onEach { isSystemAudioCaptioningEnabled.add(it) }
- .launchIn(backgroundScope)
+ kosmos.testScope.runTest {
+ `when`(captioningManager.isSystemAudioCaptioningEnabled).thenReturn(false)
+ val models by collectValues(underTest.captioningModel.filterNotNull())
runCurrent()
+ `when`(captioningManager.isSystemAudioCaptioningEnabled).thenReturn(true)
triggerOnSystemAudioCaptioningChange()
runCurrent()
- assertThat(isSystemAudioCaptioningEnabled)
+ assertThat(models.map { it.isSystemAudioCaptioningEnabled })
.containsExactlyElementsIn(listOf(false, true))
.inOrder()
}
@@ -85,18 +89,16 @@ class CaptioningRepositoryTest : SysuiTestCase() {
@Test
fun isSystemAudioCaptioningUiEnabled_change_repositoryEmits() {
- testScope.runTest {
- `when`(captioningManager.isSystemAudioCaptioningUiEnabled).thenReturn(false)
- val isSystemAudioCaptioningUiEnabled = mutableListOf<Boolean>()
- underTest.isSystemAudioCaptioningUiEnabled
- .onEach { isSystemAudioCaptioningUiEnabled.add(it) }
- .launchIn(backgroundScope)
+ kosmos.testScope.runTest {
+ `when`(captioningManager.isEnabled).thenReturn(false)
+ val models by collectValues(underTest.captioningModel.filterNotNull())
runCurrent()
+ `when`(captioningManager.isSystemAudioCaptioningUiEnabled).thenReturn(true)
triggerSystemAudioCaptioningUiChange()
runCurrent()
- assertThat(isSystemAudioCaptioningUiEnabled)
+ assertThat(models.map { it.isSystemAudioCaptioningUiEnabled })
.containsExactlyElementsIn(listOf(false, true))
.inOrder()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
index 3b0057d87048..e531e654cd34 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
@@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -73,6 +74,7 @@ class CommunalDreamStartableTest : SysuiTestCase() {
keyguardInteractor = kosmos.keyguardInteractor,
keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
dreamManager = dreamManager,
+ communalSceneInteractor = kosmos.communalSceneInteractor,
bgScope = kosmos.applicationCoroutineScope,
)
.apply { start() }
@@ -158,6 +160,36 @@ class CommunalDreamStartableTest : SysuiTestCase() {
}
}
+ @Test
+ fun shouldNotStartDreamWhenLaunchingWidget() =
+ testScope.runTest {
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setDreaming(false)
+ powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
+ kosmos.communalSceneInteractor.setIsLaunchingWidget(true)
+ whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
+ runCurrent()
+
+ transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB)
+
+ verify(dreamManager, never()).startDream()
+ }
+
+ @Test
+ fun shouldNotStartDreamWhenOccluded() =
+ testScope.runTest {
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setDreaming(false)
+ powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
+ keyguardRepository.setKeyguardOccluded(true)
+ whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
+ runCurrent()
+
+ transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB)
+
+ verify(dreamManager, never()).startDream()
+ }
+
private suspend fun TestScope.transition(from: KeyguardState, to: KeyguardState) {
kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
from = from,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
index af76b088787e..af76b088787e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 4b132c4276ea..a0bb01797f2c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -18,9 +18,12 @@
package com.android.systemui.scene.ui.viewmodel
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.classifier.fakeFalsingManager
@@ -37,6 +40,10 @@ import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -60,6 +67,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {
private val testScope by lazy { kosmos.testScope }
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
+ private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository }
private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig }
private val falsingManager by lazy { kosmos.fakeFalsingManager }
@@ -75,6 +83,8 @@ class SceneContainerViewModelTest : SysuiTestCase() {
sceneInteractor = sceneInteractor,
falsingInteractor = kosmos.falsingInteractor,
powerInteractor = kosmos.powerInteractor,
+ shadeInteractor = kosmos.shadeInteractor,
+ splitEdgeDetector = kosmos.splitEdgeDetector,
logger = kosmos.sceneLogger,
motionEventHandlerReceiver = { motionEventHandler ->
this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
@@ -287,4 +297,48 @@ class SceneContainerViewModelTest : SysuiTestCase() {
assertThat(actionableContentKey).isEqualTo(Overlays.QuickSettingsShade)
}
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_singleShade_usesDefaultEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(false)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Single)
+
+ assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_splitShade_usesDefaultEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(true)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Split)
+
+ assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(false)
+
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+ assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(true)
+
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+ assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
new file mode 100644
index 000000000000..3d76d280b2cc
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SplitEdgeDetectorTest : SysuiTestCase() {
+
+ private val edgeSize = 40
+ private val screenWidth = 800
+ private val screenHeight = 600
+
+ private var edgeSplitFraction = 0.7f
+
+ private val underTest =
+ SplitEdgeDetector(
+ topEdgeSplitFraction = { edgeSplitFraction },
+ edgeSize = edgeSize.dp,
+ )
+
+ @Test
+ fun source_noEdge_detectsNothing() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth / 2,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun source_swipeVerticallyOnTopLeft_detectsTopLeft() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopLeft)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnTopLeft_detectsLeft() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnTopRight_detectsTopRight() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth - 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnTopRight_detectsRight() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = screenWidth - 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = (screenWidth * edgeSplitFraction).toInt() - 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopLeft)
+ }
+
+ @Test
+ fun source_swipeVerticallyToRightOfSplit_detectsTopRight() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = (screenWidth * edgeSplitFraction).toInt() + 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun source_edgeSplitFractionUpdatesDynamically() {
+ val middleX = (screenWidth * 0.5f).toInt()
+ val topY = 0
+
+ // Split closer to the right; middle of screen is considered "left".
+ edgeSplitFraction = 0.6f
+ assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft)
+
+ // Split closer to the left; middle of screen is considered "right".
+ edgeSplitFraction = 0.4f
+ assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight)
+
+ // Illegal fraction.
+ edgeSplitFraction = 1.2f
+ assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
+
+ // Illegal fraction.
+ edgeSplitFraction = -0.3f
+ assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
+ }
+
+ @Test
+ fun source_swipeVerticallyOnBottom_detectsBottom() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth / 3,
+ y = screenHeight - (edgeSize / 2),
+ )
+ assertThat(detectedEdge).isEqualTo(Bottom)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnBottom_detectsNothing() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = screenWidth / 3,
+ y = screenHeight - (edgeSize - 1),
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnLeft_detectsLeft() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = edgeSize - 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnLeft_detectsNothing() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = edgeSize - 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnRight_detectsRight() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = screenWidth - edgeSize + 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnRight_detectsNothing() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth - edgeSize + 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun resolve_startInLtr_resolvesLeft() {
+ val resolvedEdge = Start.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun resolve_startInRtl_resolvesRight() {
+ val resolvedEdge = Start.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun resolve_endInLtr_resolvesRight() {
+ val resolvedEdge = End.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun resolve_endInRtl_resolvesLeft() {
+ val resolvedEdge = End.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun resolve_topStartInLtr_resolvesTopLeft() {
+ val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(TopLeft)
+ }
+
+ @Test
+ fun resolve_topStartInRtl_resolvesTopRight() {
+ val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun resolve_topEndInLtr_resolvesTopRight() {
+ val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun resolve_topEndInRtl_resolvesTopLeft() {
+ val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(TopLeft)
+ }
+
+ private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
+ return swipeFrom(x, y, Orientation.Vertical)
+ }
+
+ private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
+ return swipeFrom(x, y, Orientation.Horizontal)
+ }
+
+ private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? {
+ return underTest.source(
+ layoutSize = IntSize(width = screenWidth, height = screenHeight),
+ position = IntOffset(x, y),
+ density = Density(1f),
+ orientation = orientation,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index 3283ea154b3f..d163abf66b05 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -19,12 +19,9 @@ package com.android.systemui.shade.domain.interactor
import android.app.StatusBarManager.DISABLE2_NONE
import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE
import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -39,10 +36,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.power.shared.model.WakefulnessState
-import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.shadeTestUtil
-import com.android.systemui.shade.shared.flag.DualShade
-import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
import com.android.systemui.statusbar.phone.dozeParameters
@@ -66,18 +60,17 @@ import platform.test.runner.parameterized.Parameters
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
- val kosmos = testKosmos()
- val testScope = kosmos.testScope
- val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
- val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
- val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
- val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
- val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
- val powerRepository by lazy { kosmos.fakePowerRepository }
- val shadeTestUtil by lazy { kosmos.shadeTestUtil }
- val userRepository by lazy { kosmos.fakeUserRepository }
- val userSetupRepository by lazy { kosmos.fakeUserSetupRepository }
- val dozeParameters by lazy { kosmos.dozeParameters }
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
+ private val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
+ private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+ private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+ private val powerRepository by lazy { kosmos.fakePowerRepository }
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+ private val userRepository by lazy { kosmos.fakeUserRepository }
+ private val userSetupRepository by lazy { kosmos.fakeUserSetupRepository }
+ private val dozeParameters by lazy { kosmos.dozeParameters }
lateinit var underTest: ShadeInteractorImpl
@@ -142,9 +135,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
userSetupRepository.setUserSetUp(true)
disableFlagsRepository.disableFlags.value =
- DisableFlagsModel(
- disable2 = DISABLE2_NOTIFICATION_SHADE,
- )
+ DisableFlagsModel(disable2 = DISABLE2_NOTIFICATION_SHADE)
val actual by collectLastValue(underTest.isExpandToQsEnabled)
@@ -158,9 +149,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
userSetupRepository.setUserSetUp(true)
disableFlagsRepository.disableFlags.value =
- DisableFlagsModel(
- disable2 = DISABLE2_QUICK_SETTINGS,
- )
+ DisableFlagsModel(disable2 = DISABLE2_QUICK_SETTINGS)
val actual by collectLastValue(underTest.isExpandToQsEnabled)
assertThat(actual).isFalse()
@@ -171,10 +160,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
testScope.runTest {
deviceProvisioningRepository.setDeviceProvisioned(true)
userSetupRepository.setUserSetUp(true)
- disableFlagsRepository.disableFlags.value =
- DisableFlagsModel(
- disable2 = DISABLE2_NONE,
- )
+ disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE)
keyguardRepository.setIsDozing(true)
@@ -188,10 +174,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
testScope.runTest {
deviceProvisioningRepository.setDeviceProvisioned(true)
keyguardRepository.setIsDozing(false)
- disableFlagsRepository.disableFlags.value =
- DisableFlagsModel(
- disable2 = DISABLE2_NONE,
- )
+ disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE)
userSetupRepository.setUserSetUp(true)
@@ -205,10 +188,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
testScope.runTest {
deviceProvisioningRepository.setDeviceProvisioned(true)
keyguardRepository.setIsDozing(false)
- disableFlagsRepository.disableFlags.value =
- DisableFlagsModel(
- disable2 = DISABLE2_NONE,
- )
+ disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE)
userRepository.setSettings(UserSwitcherSettingsModel(isSimpleUserSwitcher = false))
@@ -222,10 +202,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
testScope.runTest {
deviceProvisioningRepository.setDeviceProvisioned(true)
keyguardRepository.setIsDozing(false)
- disableFlagsRepository.disableFlags.value =
- DisableFlagsModel(
- disable2 = DISABLE2_NONE,
- )
+ disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE)
userSetupRepository.setUserSetUp(true)
val actual by collectLastValue(underTest.isExpandToQsEnabled)
@@ -250,10 +227,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
testScope.runTest {
deviceProvisioningRepository.setDeviceProvisioned(true)
keyguardRepository.setIsDozing(false)
- disableFlagsRepository.disableFlags.value =
- DisableFlagsModel(
- disable2 = DISABLE2_NONE,
- )
+ disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE)
userSetupRepository.setUserSetUp(true)
val actual by collectLastValue(underTest.isExpandToQsEnabled)
@@ -262,17 +236,12 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
// WHEN QS is disabled
disableFlagsRepository.disableFlags.value =
- DisableFlagsModel(
- disable2 = DISABLE2_QUICK_SETTINGS,
- )
+ DisableFlagsModel(disable2 = DISABLE2_QUICK_SETTINGS)
// THEN expand is disabled
assertThat(actual).isFalse()
// WHEN QS is enabled
- disableFlagsRepository.disableFlags.value =
- DisableFlagsModel(
- disable2 = DISABLE2_NONE,
- )
+ disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE)
// THEN expand is enabled
assertThat(actual).isTrue()
}
@@ -282,10 +251,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
testScope.runTest {
deviceProvisioningRepository.setDeviceProvisioned(true)
keyguardRepository.setIsDozing(false)
- disableFlagsRepository.disableFlags.value =
- DisableFlagsModel(
- disable2 = DISABLE2_NONE,
- )
+ disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE)
userSetupRepository.setUserSetUp(true)
val actual by collectLastValue(underTest.isExpandToQsEnabled)
@@ -359,9 +325,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
)
)
keyguardRepository.setDozeTransitionModel(
- DozeTransitionModel(
- to = DozeStateModel.DOZE_AOD,
- )
+ DozeTransitionModel(to = DozeStateModel.DOZE_AOD)
)
val isShadeTouchable by collectLastValue(underTest.isShadeTouchable)
assertThat(isShadeTouchable).isFalse()
@@ -385,9 +349,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
)
)
keyguardRepository.setDozeTransitionModel(
- DozeTransitionModel(
- to = DozeStateModel.DOZE_PULSING,
- )
+ DozeTransitionModel(to = DozeStateModel.DOZE_PULSING)
)
val isShadeTouchable by collectLastValue(underTest.isShadeTouchable)
assertThat(isShadeTouchable).isTrue()
@@ -450,51 +412,9 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
lastSleepReason = WakeSleepReason.OTHER,
)
keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.STARTED,
- )
+ TransitionStep(transitionState = TransitionState.STARTED)
)
val isShadeTouchable by collectLastValue(underTest.isShadeTouchable)
assertThat(isShadeTouchable).isTrue()
}
-
- @Test
- @DisableFlags(DualShade.FLAG_NAME)
- fun legacyShadeMode_narrowScreen_singleShade() =
- testScope.runTest {
- val shadeMode by collectLastValue(underTest.shadeMode)
- kosmos.shadeRepository.setShadeLayoutWide(false)
-
- assertThat(shadeMode).isEqualTo(ShadeMode.Single)
- }
-
- @Test
- @DisableFlags(DualShade.FLAG_NAME)
- fun legacyShadeMode_wideScreen_splitShade() =
- testScope.runTest {
- val shadeMode by collectLastValue(underTest.shadeMode)
- kosmos.shadeRepository.setShadeLayoutWide(true)
-
- assertThat(shadeMode).isEqualTo(ShadeMode.Split)
- }
-
- @Test
- @EnableFlags(DualShade.FLAG_NAME)
- fun shadeMode_wideScreen_isDual() =
- testScope.runTest {
- val shadeMode by collectLastValue(underTest.shadeMode)
- kosmos.shadeRepository.setShadeLayoutWide(true)
-
- assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
- }
-
- @Test
- @EnableFlags(DualShade.FLAG_NAME)
- fun shadeMode_narrowScreen_isDual() =
- testScope.runTest {
- val shadeMode by collectLastValue(underTest.shadeMode)
- kosmos.shadeRepository.setShadeLayoutWide(false)
-
- assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
new file mode 100644
index 000000000000..2a2817b9af73
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShadeModeInteractorImplTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private lateinit var underTest: ShadeModeInteractor
+
+ @Before
+ fun setUp() {
+ underTest = kosmos.shadeModeInteractor
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun legacyShadeMode_narrowScreen_singleShade() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(underTest.shadeMode)
+ kosmos.shadeRepository.setShadeLayoutWide(false)
+
+ assertThat(shadeMode).isEqualTo(ShadeMode.Single)
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun legacyShadeMode_wideScreen_splitShade() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(underTest.shadeMode)
+ kosmos.shadeRepository.setShadeLayoutWide(true)
+
+ assertThat(shadeMode).isEqualTo(ShadeMode.Split)
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun shadeMode_wideScreen_isDual() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(underTest.shadeMode)
+ kosmos.shadeRepository.setShadeLayoutWide(true)
+
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun shadeMode_narrowScreen_isDual() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(underTest.shadeMode)
+ kosmos.shadeRepository.setShadeLayoutWide(false)
+
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+ }
+
+ @Test
+ fun getTopEdgeSplitFraction_narrowScreen_splitInHalf() =
+ testScope.runTest {
+ // Ensure isShadeLayoutWide is collected.
+ val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
+ kosmos.shadeRepository.setShadeLayoutWide(false)
+
+ assertThat(underTest.getTopEdgeSplitFraction()).isEqualTo(0.5f)
+ }
+
+ @Test
+ fun getTopEdgeSplitFraction_wideScreen_leftSideLarger() =
+ testScope.runTest {
+ // Ensure isShadeLayoutWide is collected.
+ val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
+ kosmos.shadeRepository.setShadeLayoutWide(true)
+
+ assertThat(underTest.getTopEdgeSplitFraction()).isGreaterThan(0.5f)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 840aa92548c8..26e1a4d9e961 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -26,6 +26,7 @@ import androidx.test.filters.SmallTest
import com.android.settingslib.notification.data.repository.updateNotificationPolicy
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.Flags
import com.android.systemui.flags.andSceneContainer
@@ -36,6 +37,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
@@ -51,6 +53,7 @@ import com.android.systemui.util.ui.isAnimating
import com.android.systemui.util.ui.value
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
@@ -153,7 +156,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
fun shouldShowEmptyShadeView_trueWhenNoNotifs() =
testScope.runTest {
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -196,7 +199,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
fun shouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
testScope.runTest {
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -217,7 +220,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
fun shouldShowEmptyShadeView_trueWhenLockedShade() =
testScope.runTest {
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -315,7 +318,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
@Test
fun shouldIncludeFooterView_trueWhenShade() =
testScope.runTest {
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
@@ -333,7 +336,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
@Test
fun shouldIncludeFooterView_trueWhenLockedShade() =
testScope.runTest {
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
@@ -351,7 +354,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
@Test
fun shouldIncludeFooterView_falseWhenKeyguard() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -366,7 +369,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
@Test
fun shouldIncludeFooterView_falseWhenUserNotSetUp() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -384,7 +387,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
@Test
fun shouldIncludeFooterView_falseWhenStartingToSleep() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -402,7 +405,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
@Test
fun shouldIncludeFooterView_falseWhenQsExpandedDefault() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -421,7 +424,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
@Test
fun shouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
testScope.runTest {
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
@@ -444,7 +447,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
@Test
fun shouldIncludeFooterView_falseWhenRemoteInputActive() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -462,7 +465,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
@Test
fun shouldIncludeFooterView_animatesWhenShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -478,7 +481,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
@Test
fun shouldIncludeFooterView_notAnimatingOnKeyguard() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -492,6 +495,22 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
}
@Test
+ @EnableSceneContainer
+ fun shouldShowFooterView_falseWhenShadeIsClosed() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN shade is closed
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ shadeTestUtil.setShadeExpansion(0f)
+ runCurrent()
+
+ // THEN footer is hidden
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ @DisableSceneContainer
fun shouldHideFooterView_trueWhenShadeIsClosed() =
testScope.runTest {
val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -506,6 +525,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
}
@Test
+ @DisableSceneContainer
fun shouldHideFooterView_falseWhenShadeIsOpen() =
testScope.runTest {
val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -520,6 +540,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
}
@Test
+ @DisableSceneContainer
fun shouldHideFooterView_falseWhenQSPartiallyOpen() =
testScope.runTest {
val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -642,4 +663,10 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas
assertThat(animationsEnabled).isTrue()
}
+
+ private fun TestScope.collectFooterViewVisibility() =
+ collectLastValue(
+ if (SceneContainerFlag.isEnabled) underTest.shouldShowFooterView
+ else underTest.shouldIncludeFooterView
+ )
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index dd84bc6989a4..92e5432ad243 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -271,7 +271,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mLatencyTracker, mFalsingCollector,
emergencyButtonController, mMessageAreaControllerFactory,
- mDevicePostureController, mFeatureFlags, mSelectedUserInteractor);
+ mDevicePostureController, mFeatureFlags, mSelectedUserInteractor,
+ mMSDLPlayer);
} else if (keyguardInputView instanceof KeyguardPasswordView) {
return new KeyguardPasswordViewController((KeyguardPasswordView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index caa74780538e..f74d93e1d88d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -36,6 +36,7 @@ import com.android.internal.widget.LockPatternView.Cell;
import com.android.internal.widget.LockscreenCredential;
import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.systemui.bouncer.ui.helper.BouncerHapticHelper;
import com.android.systemui.classifier.FalsingClassifier;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
@@ -43,6 +44,8 @@ import com.android.systemui.res.R;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
+import com.google.android.msdl.domain.MSDLPlayer;
+
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -67,6 +70,7 @@ public class KeyguardPatternViewController
private LockPatternView mLockPatternView;
private CountDownTimer mCountdownTimer;
private AsyncTask<?, ?, ?> mPendingLockCheck;
+ private MSDLPlayer mMSDLPlayer;
private EmergencyButtonCallback mEmergencyButtonCallback = new EmergencyButtonCallback() {
@Override
@@ -75,6 +79,10 @@ public class KeyguardPatternViewController
}
};
+ private final LockPatternView.ExternalHapticsPlayer mExternalHapticsPlayer = () -> {
+ BouncerHapticHelper.INSTANCE.playPatternDotFeedback(mMSDLPlayer, mView);
+ };
+
/**
* Useful for clearing out the wrong pattern after a delay
*/
@@ -166,6 +174,10 @@ public class KeyguardPatternViewController
boolean isValidPattern) {
boolean dismissKeyguard = mSelectedUserInteractor.getSelectedUserId() == userId;
if (matched) {
+ BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback(
+ /* authenticationSucceeded= */true,
+ /* player =*/mMSDLPlayer
+ );
getKeyguardSecurityCallback().reportUnlockAttempt(userId, true, 0);
if (dismissKeyguard) {
mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
@@ -173,6 +185,10 @@ public class KeyguardPatternViewController
getKeyguardSecurityCallback().dismiss(true, userId, SecurityMode.Pattern);
}
} else {
+ BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback(
+ /* authenticationSucceeded= */false,
+ /* player =*/mMSDLPlayer
+ );
mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
if (isValidPattern) {
getKeyguardSecurityCallback().reportUnlockAttempt(userId, false, timeoutMs);
@@ -200,7 +216,7 @@ public class KeyguardPatternViewController
EmergencyButtonController emergencyButtonController,
KeyguardMessageAreaController.Factory messageAreaControllerFactory,
DevicePostureController postureController, FeatureFlags featureFlags,
- SelectedUserInteractor selectedUserInteractor) {
+ SelectedUserInteractor selectedUserInteractor, MSDLPlayer msdlPlayer) {
super(view, securityMode, keyguardSecurityCallback, emergencyButtonController,
messageAreaControllerFactory, featureFlags, selectedUserInteractor);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -212,6 +228,7 @@ public class KeyguardPatternViewController
featureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE));
mLockPatternView = mView.findViewById(R.id.lockPatternView);
mPostureController = postureController;
+ mMSDLPlayer = msdlPlayer;
}
@Override
@@ -249,6 +266,7 @@ public class KeyguardPatternViewController
if (deadline != 0) {
handleAttemptLockout(deadline);
}
+ mLockPatternView.setExternalHapticsPlayer(mExternalHapticsPlayer);
}
@Override
@@ -262,6 +280,7 @@ public class KeyguardPatternViewController
cancelBtn.setOnClickListener(null);
}
mPostureController.removeCallback(mPostureCallback);
+ mLockPatternView.setExternalHapticsPlayer(null);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
index 2c97d62d690e..4d5e717536f6 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java
@@ -28,6 +28,7 @@ import android.util.Log;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -68,8 +69,9 @@ public class AccessibilityButtonModeObserver extends
}
@Inject
- public AccessibilityButtonModeObserver(Context context, UserTracker userTracker) {
- super(context, userTracker, Settings.Secure.ACCESSIBILITY_BUTTON_MODE);
+ public AccessibilityButtonModeObserver(
+ Context context, UserTracker userTracker, SecureSettings secureSettings) {
+ super(context, userTracker, secureSettings, Settings.Secure.ACCESSIBILITY_BUTTON_MODE);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java
index 53a21b329594..1363b1c12332 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java
@@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
import javax.inject.Inject;
@@ -49,8 +50,9 @@ public class AccessibilityButtonTargetsObserver extends
}
@Inject
- public AccessibilityButtonTargetsObserver(Context context, UserTracker userTracker) {
- super(context, userTracker, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
+ public AccessibilityButtonTargetsObserver(
+ Context context, UserTracker userTracker, SecureSettings secureSettings) {
+ super(context, userTracker, secureSettings, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java
index c94487848b81..736217a699fd 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java
@@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
import javax.inject.Inject;
@@ -49,8 +50,9 @@ public class AccessibilityGestureTargetsObserver extends
}
@Inject
- public AccessibilityGestureTargetsObserver(Context context, UserTracker userTracker) {
- super(context, userTracker, Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS);
+ public AccessibilityGestureTargetsObserver(
+ Context context, UserTracker userTracker, SecureSettings secureSettings) {
+ super(context, userTracker, secureSettings, Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java
index 443441f1ef48..eb4de6837d41 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java
@@ -18,6 +18,9 @@ package com.android.systemui.accessibility;
import static android.view.WindowManager.LayoutParams;
+import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -29,8 +32,8 @@ import android.util.MathUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
-import android.view.WindowManager;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.systemui.res.R;
/**
@@ -70,11 +73,12 @@ public abstract class MirrorWindowControl {
* @see #setDefaultPosition(LayoutParams)
*/
private final Point mControlPosition = new Point();
- private final WindowManager mWindowManager;
+ private final ViewCaptureAwareWindowManager mWindowManager;
MirrorWindowControl(Context context) {
mContext = context;
- mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext,
+ enableViewCaptureTracing());
}
public void setWindowDelegate(@Nullable MirrorWindowDelegate windowDelegate) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java
index 326773fb5bef..c50cf85feccb 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java
@@ -28,6 +28,7 @@ import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
import java.util.ArrayList;
import java.util.List;
@@ -48,6 +49,7 @@ public abstract class SecureSettingsContentObserver<T> {
private final UserTracker mUserTracker;
@VisibleForTesting
final ContentObserver mContentObserver;
+ private final SecureSettings mSecureSettings;
private final String mKey;
@@ -55,7 +57,7 @@ public abstract class SecureSettingsContentObserver<T> {
final List<T> mListeners = new ArrayList<>();
protected SecureSettingsContentObserver(Context context, UserTracker userTracker,
- String secureSettingsKey) {
+ SecureSettings secureSettings, String secureSettingsKey) {
mKey = secureSettingsKey;
mContentResolver = context.getContentResolver();
mUserTracker = userTracker;
@@ -65,6 +67,7 @@ public abstract class SecureSettingsContentObserver<T> {
updateValueChanged();
}
};
+ mSecureSettings = secureSettings;
}
/**
@@ -80,9 +83,8 @@ public abstract class SecureSettingsContentObserver<T> {
}
if (mListeners.size() == 1) {
- mContentResolver.registerContentObserver(
- Settings.Secure.getUriFor(mKey), /* notifyForDescendants= */
- false, mContentObserver, UserHandle.USER_ALL);
+ mSecureSettings.registerContentObserverForUserAsync(Settings.Secure.getUriFor(mKey),
+ /* notifyForDescendants= */ false, mContentObserver, UserHandle.USER_ALL);
}
}
@@ -97,7 +99,7 @@ public abstract class SecureSettingsContentObserver<T> {
mListeners.remove(listener);
if (mListeners.isEmpty()) {
- mContentResolver.unregisterContentObserver(mContentObserver);
+ mSecureSettings.unregisterContentObserverAsync(mContentObserver);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt
new file mode 100644
index 000000000000..4eb2274cf129
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.model
+
+data class CaptioningModel(
+ val isSystemAudioCaptioningUiEnabled: Boolean,
+ val isSystemAudioCaptioningEnabled: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
index bf749d4cfc35..5414b623ff97 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
@@ -16,98 +16,90 @@
package com.android.systemui.accessibility.data.repository
+import android.annotation.SuppressLint
import android.view.accessibility.CaptioningManager
+import com.android.systemui.accessibility.data.model.CaptioningModel
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
interface CaptioningRepository {
- /** The system audio caption enabled state. */
- val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
+ /** Current state of Live Captions. */
+ val captioningModel: StateFlow<CaptioningModel?>
- /** The system audio caption UI enabled state. */
- val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
-
- /** Sets [isSystemAudioCaptioningEnabled]. */
+ /** Sets [CaptioningModel.isSystemAudioCaptioningEnabled]. */
suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean)
}
-class CaptioningRepositoryImpl(
- private val captioningManager: CaptioningManager,
- private val backgroundCoroutineContext: CoroutineContext,
- coroutineScope: CoroutineScope,
+@OptIn(ExperimentalCoroutinesApi::class)
+class CaptioningRepositoryImpl
+@Inject
+constructor(
+ private val userScopedCaptioningManagerProvider: UserScopedService<CaptioningManager>,
+ userRepository: UserRepository,
+ @Background private val backgroundCoroutineContext: CoroutineContext,
+ @Application coroutineScope: CoroutineScope,
) : CaptioningRepository {
- private val captioningChanges: SharedFlow<CaptioningChange> =
- callbackFlow {
- val listener = CaptioningChangeProducingListener(this)
- captioningManager.addCaptioningChangeListener(listener)
- awaitClose { captioningManager.removeCaptioningChangeListener(listener) }
- }
- .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
-
- override val isSystemAudioCaptioningEnabled: StateFlow<Boolean> =
- captioningChanges
- .filterIsInstance(CaptioningChange.IsSystemAudioCaptioningEnabled::class)
- .map { it.isEnabled }
- .onStart { emit(captioningManager.isSystemAudioCaptioningEnabled) }
- .stateIn(
- coroutineScope,
- SharingStarted.WhileSubscribed(),
- captioningManager.isSystemAudioCaptioningEnabled,
- )
+ @SuppressLint("NonInjectedService") // this uses user-aware context
+ private val captioningManager: StateFlow<CaptioningManager?> =
+ userRepository.selectedUser
+ .map { userScopedCaptioningManagerProvider.forUser(it.userInfo.userHandle) }
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
- override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> =
- captioningChanges
- .filterIsInstance(CaptioningChange.IsSystemUICaptioningEnabled::class)
- .map { it.isEnabled }
- .onStart { emit(captioningManager.isSystemAudioCaptioningUiEnabled) }
- .stateIn(
- coroutineScope,
- SharingStarted.WhileSubscribed(),
- captioningManager.isSystemAudioCaptioningUiEnabled,
- )
+ override val captioningModel: StateFlow<CaptioningModel?> =
+ captioningManager
+ .filterNotNull()
+ .flatMapLatest { it.captioningModel() }
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) {
withContext(backgroundCoroutineContext) {
- captioningManager.isSystemAudioCaptioningEnabled = isEnabled
+ captioningManager.value?.isSystemAudioCaptioningEnabled = isEnabled
}
}
- private sealed interface CaptioningChange {
-
- data class IsSystemAudioCaptioningEnabled(val isEnabled: Boolean) : CaptioningChange
-
- data class IsSystemUICaptioningEnabled(val isEnabled: Boolean) : CaptioningChange
- }
-
- private class CaptioningChangeProducingListener(
- private val scope: ProducerScope<CaptioningChange>
- ) : CaptioningManager.CaptioningChangeListener() {
-
- override fun onSystemAudioCaptioningChanged(enabled: Boolean) {
- emitChange(CaptioningChange.IsSystemAudioCaptioningEnabled(enabled))
- }
-
- override fun onSystemAudioCaptioningUiChanged(enabled: Boolean) {
- emitChange(CaptioningChange.IsSystemUICaptioningEnabled(enabled))
- }
-
- private fun emitChange(change: CaptioningChange) {
- scope.launch { scope.send(change) }
- }
+ private fun CaptioningManager.captioningModel(): Flow<CaptioningModel> {
+ return conflatedCallbackFlow {
+ val listener =
+ object : CaptioningManager.CaptioningChangeListener() {
+
+ override fun onSystemAudioCaptioningChanged(enabled: Boolean) {
+ trySend(Unit)
+ }
+
+ override fun onSystemAudioCaptioningUiChanged(enabled: Boolean) {
+ trySend(Unit)
+ }
+ }
+ addCaptioningChangeListener(listener)
+ awaitClose { removeCaptioningChangeListener(listener) }
+ }
+ .onStart { emit(Unit) }
+ .map {
+ CaptioningModel(
+ isSystemAudioCaptioningEnabled = isSystemAudioCaptioningEnabled,
+ isSystemAudioCaptioningUiEnabled = isSystemAudioCaptioningUiEnabled,
+ )
+ }
+ .flowOn(backgroundCoroutineContext)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt
index 1d493c697652..840edf44ecf5 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt
@@ -17,16 +17,22 @@
package com.android.systemui.accessibility.domain.interactor
import com.android.systemui.accessibility.data.repository.CaptioningRepository
-import kotlinx.coroutines.flow.StateFlow
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
-class CaptioningInteractor(private val repository: CaptioningRepository) {
+@SysUISingleton
+class CaptioningInteractor @Inject constructor(private val repository: CaptioningRepository) {
- val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
- get() = repository.isSystemAudioCaptioningEnabled
+ val isSystemAudioCaptioningEnabled: Flow<Boolean> =
+ repository.captioningModel.filterNotNull().map { it.isSystemAudioCaptioningEnabled }
- val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
- get() = repository.isSystemAudioCaptioningUiEnabled
+ val isSystemAudioCaptioningUiEnabled: Flow<Boolean> =
+ repository.captioningModel.filterNotNull().map { it.isSystemAudioCaptioningUiEnabled }
- suspend fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) =
+ suspend fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) {
repository.setIsSystemAudioCaptioningEnabled(enabled)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt
new file mode 100644
index 000000000000..1faacff996ca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.helper
+
+import android.view.HapticFeedbackConstants
+import android.view.View
+import com.android.keyguard.AuthInteractionProperties
+import com.android.systemui.Flags
+//noinspection CleanArchitectureDependencyViolation: Data layer only referenced for this enum class
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.MSDLPlayer
+
+/** A helper object to deliver haptic feedback in bouncer interactions. */
+object BouncerHapticHelper {
+
+ private val authInteractionProperties = AuthInteractionProperties()
+
+ /**
+ * Deliver MSDL feedback as a result of authenticating through a bouncer.
+ *
+ * @param[authenticationSucceeded] Whether the authentication was successful or not.
+ * @param[player] The [MSDLPlayer] that delivers the correct feedback.
+ */
+ fun playMSDLAuthenticationFeedback(
+ authenticationSucceeded: Boolean,
+ player: MSDLPlayer?,
+ ) {
+ if (player == null || !Flags.msdlFeedback()) {
+ return
+ }
+
+ val token =
+ if (authenticationSucceeded) {
+ MSDLToken.UNLOCK
+ } else {
+ MSDLToken.FAILURE
+ }
+ player.playToken(token, authInteractionProperties)
+ }
+
+ /**
+ * Deliver feedback when dragging through cells in the pattern bouncer. This function can play
+ * MSDL feedback using a [MSDLPlayer], or fallback to a default haptic feedback using the
+ * [View.performHapticFeedback] API and a [View].
+ *
+ * @param[player] [MSDLPlayer] for MSDL feedback.
+ * @param[view] A [View] for default haptic feedback using [View.performHapticFeedback]
+ */
+ fun playPatternDotFeedback(player: MSDLPlayer?, view: View?) {
+ if (player == null || !Flags.msdlFeedback()) {
+ view?.performHapticFeedback(
+ HapticFeedbackConstants.VIRTUAL_KEY,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
+ )
+ } else {
+ player.playToken(MSDLToken.DRAG_INDICATOR)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
index c69cea4a6a5a..04393feaae37 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
@@ -21,6 +21,7 @@ import android.app.DreamManager
import com.android.systemui.CoreStartable
import com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming
import com.android.systemui.Flags.restartDreamOnUnocclude
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -55,6 +56,7 @@ constructor(
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val dreamManager: DreamManager,
+ private val communalSceneInteractor: CommunalSceneInteractor,
@Background private val bgScope: CoroutineScope,
) : CoreStartable {
/** Flow that emits when the dream should be started underneath the glanceable hub. */
@@ -66,6 +68,8 @@ constructor(
not(keyguardInteractor.isDreaming),
// TODO(b/362830856): Remove this workaround.
keyguardInteractor.isKeyguardShowing,
+ not(communalSceneInteractor.isLaunchingWidget),
+ not(keyguardInteractor.isKeyguardOccluded),
)
.filter { it }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 21a704df074e..8818c3af4916 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -202,6 +202,13 @@ public class FrameworkServicesModule {
return context.getSystemService(CaptioningManager.class);
}
+ @Provides
+ @Singleton
+ static UserScopedService<CaptioningManager> provideUserScopedCaptioningManager(
+ Context context) {
+ return new UserScopedServiceImpl<>(context, CaptioningManager.class);
+ }
+
/** */
@Provides
@Singleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index db5a63bbf446..58c8a0456241 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -73,7 +73,7 @@ constructor(
if (SceneContainerFlag.isEnabled) return
listenForGoneToAodOrDozing()
listenForGoneToDreaming()
- listenForGoneToLockscreenOrHub()
+ listenForGoneToLockscreenOrHubOrOccluded()
listenForGoneToOccluded()
listenForGoneToDreamingLockscreenHosted()
}
@@ -89,22 +89,19 @@ constructor(
*/
private fun listenForGoneToOccluded() {
scope.launch("$TAG#listenForGoneToOccluded") {
- keyguardInteractor.showDismissibleKeyguard
- .filterRelevantKeyguardState()
- .sample(keyguardInteractor.isKeyguardOccluded, ::Pair)
- .collect { (_, isKeyguardOccluded) ->
- if (isKeyguardOccluded) {
- startTransitionTo(
- KeyguardState.OCCLUDED,
- ownerReason = "Dismissible keyguard with occlusion"
- )
- }
+ keyguardInteractor.showDismissibleKeyguard.filterRelevantKeyguardState().collect {
+ if (keyguardInteractor.isKeyguardOccluded.value) {
+ startTransitionTo(
+ KeyguardState.OCCLUDED,
+ ownerReason = "Dismissible keyguard with occlusion"
+ )
}
+ }
}
}
// Primarily for when the user chooses to lock down the device
- private fun listenForGoneToLockscreenOrHub() {
+ private fun listenForGoneToLockscreenOrHubOrOccluded() {
if (KeyguardWmStateRefactor.isEnabled) {
scope.launch("$TAG#listenForGoneToLockscreenOrHub") {
biometricSettingsRepository.isCurrentUserInLockdown
@@ -137,7 +134,7 @@ constructor(
}
}
} else {
- scope.launch("$TAG#listenForGoneToLockscreenOrHub") {
+ scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded") {
keyguardInteractor.isKeyguardShowing
.filterRelevantKeyguardStateAnd { isKeyguardShowing -> isKeyguardShowing }
.sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair)
@@ -145,6 +142,8 @@ constructor(
val to =
if (isIdleOnCommunal) {
KeyguardState.GLANCEABLE_HUB
+ } else if (keyguardInteractor.isKeyguardOccluded.value) {
+ KeyguardState.OCCLUDED
} else {
KeyguardState.LOCKSCREEN
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index e2bb540f6645..7afc7596a994 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -80,10 +80,7 @@ constructor(
}
applicationScope.launch {
val refreshConfig =
- Config(
- Type.NoTransition,
- rebuildSections = listOf(smartspaceSection),
- )
+ Config(Type.NoTransition, rebuildSections = listOf(smartspaceSection))
configurationInteractor.onAnyConfigurationChange.collect {
refreshBlueprint(refreshConfig)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt
index f33752fc04d4..12bcc7ecbab8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.util.MathUtils
+import com.android.systemui.Flags.lightRevealMigration
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
@@ -55,8 +56,18 @@ constructor(
var currentAlpha = 0f
return transitionAnimation.sharedFlow(
duration = 250.milliseconds,
- startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements.
- onStart = { currentAlpha = viewState.alpha() },
+ startTime = if (lightRevealMigration()) {
+ 100.milliseconds // Wait for the light reveal to "hit" the LS elements.
+ } else {
+ 0.milliseconds
+ },
+ onStart = {
+ if (lightRevealMigration()) {
+ currentAlpha = viewState.alpha()
+ } else {
+ currentAlpha = 0f
+ }
+ },
onStep = { MathUtils.lerp(currentAlpha, 0f, it) },
onCancel = { 0f },
)
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
index a5c07bc2fdbf..11854d9317c9 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
@@ -18,9 +18,13 @@ package com.android.systemui.notifications.ui.viewmodel
import com.android.compose.animation.scene.Back
import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -34,8 +38,10 @@ class NotificationsShadeUserActionsViewModel @AssistedInject constructor() :
override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
setActions(
mapOf(
- Swipe.Up to SceneFamilies.Home,
Back to SceneFamilies.Home,
+ Swipe.Up to SceneFamilies.Home,
+ Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
+ ReplaceByOverlay(Overlays.QuickSettingsShade),
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index cbcf68c27bf8..2f843ac610a3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -50,10 +50,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
+import com.android.systemui.Flags;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.time.SystemClock;
import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
@@ -95,6 +97,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements
// Bind retry control.
private static final int MAX_BIND_RETRIES = 5;
private static final long DEFAULT_BIND_RETRY_DELAY = 5 * DateUtils.SECOND_IN_MILLIS;
+ private static final long ACTIVE_TILE_BIND_RETRY_DELAY = 1 * DateUtils.SECOND_IN_MILLIS;
private static final long LOW_MEMORY_BIND_RETRY_DELAY = 20 * DateUtils.SECOND_IN_MILLIS;
private static final long TILE_SERVICE_ONCLICK_ALLOW_LIST_DEFAULT_DURATION_MS = 15_000;
private static final String PROPERTY_TILE_SERVICE_ONCLICK_ALLOW_LIST_DURATION =
@@ -107,6 +110,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements
private final Intent mIntent;
private final UserHandle mUser;
private final DelayableExecutor mExecutor;
+ private final SystemClock mSystemClock;
private final IBinder mToken = new Binder();
private final PackageManagerAdapter mPackageManagerAdapter;
private final BroadcastDispatcher mBroadcastDispatcher;
@@ -120,7 +124,6 @@ public class TileLifecycleManager extends BroadcastReceiver implements
private IBinder mClickBinder;
private int mBindTryCount;
- private long mBindRetryDelay = DEFAULT_BIND_RETRY_DELAY;
private AtomicBoolean isDeathRebindScheduled = new AtomicBoolean(false);
private AtomicBoolean mBound = new AtomicBoolean(false);
private AtomicBoolean mPackageReceiverRegistered = new AtomicBoolean(false);
@@ -138,7 +141,8 @@ public class TileLifecycleManager extends BroadcastReceiver implements
TileLifecycleManager(@Main Handler handler, Context context, IQSService service,
PackageManagerAdapter packageManagerAdapter, BroadcastDispatcher broadcastDispatcher,
@Assisted Intent intent, @Assisted UserHandle user, ActivityManager activityManager,
- IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor) {
+ IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor,
+ SystemClock systemClock) {
mContext = context;
mHandler = handler;
mIntent = intent;
@@ -146,6 +150,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements
mIntent.putExtra(TileService.EXTRA_TOKEN, mToken);
mUser = user;
mExecutor = executor;
+ mSystemClock = systemClock;
mPackageManagerAdapter = packageManagerAdapter;
mBroadcastDispatcher = broadcastDispatcher;
mActivityManager = activityManager;
@@ -436,25 +441,31 @@ public class TileLifecycleManager extends BroadcastReceiver implements
// If mBound is true (meaning that we should be bound), then reschedule binding for
// later.
if (mBound.get() && checkComponentState()) {
- if (isDeathRebindScheduled.compareAndSet(false, true)) {
+ if (isDeathRebindScheduled.compareAndSet(false, true)) { // if already not scheduled
+
+
mExecutor.executeDelayed(() -> {
// Only rebind if we are supposed to, but remove the scheduling anyway.
if (mBound.get()) {
setBindService(true);
}
- isDeathRebindScheduled.set(false);
+ isDeathRebindScheduled.set(false); // allow scheduling again
}, getRebindDelay());
}
}
});
}
+ private long mLastRebind = 0;
/**
* @return the delay to automatically rebind after a service died. It provides a longer delay if
* the device is a low memory state because the service is likely to get killed again by the
* system. In this case we want to rebind later and not to cause a loop of a frequent rebinds.
+ * It also provides a longer delay if called quickly (a few seconds) after a first call.
*/
private long getRebindDelay() {
+ final long now = mSystemClock.currentTimeMillis();
+
final ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
mActivityManager.getMemoryInfo(info);
@@ -462,7 +473,20 @@ public class TileLifecycleManager extends BroadcastReceiver implements
if (info.lowMemory) {
delay = LOW_MEMORY_BIND_RETRY_DELAY;
} else {
- delay = mBindRetryDelay;
+ if (Flags.qsQuickRebindActiveTiles()) {
+ final long elapsedTimeSinceLastRebind = now - mLastRebind;
+ final boolean justAttemptedRebind =
+ elapsedTimeSinceLastRebind < DEFAULT_BIND_RETRY_DELAY;
+ if (isActiveTile() && !justAttemptedRebind) {
+ delay = ACTIVE_TILE_BIND_RETRY_DELAY;
+ } else {
+ delay = DEFAULT_BIND_RETRY_DELAY;
+ }
+ } else {
+ delay = DEFAULT_BIND_RETRY_DELAY;
+ }
+
+ mLastRebind = now;
}
if (mDebug) Log.i(TAG, "Rebinding with a delay=" + delay + " - " + getComponent());
return delay;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
index d10471d86d0b..c5fa8cf05fd0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -44,7 +44,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
/**
* Manages the priority which lets {@link TileServices} make decisions about which tiles
* to bind. Also holds on to and manages the {@link TileLifecycleManager}, informing it
- * of when it is allowed to bind based on decisions frome the {@link TileServices}.
+ * of when it is allowed to bind based on decisions from the {@link TileServices}.
*/
public class TileServiceManager {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
index d3dc302d44ca..bd1872d455d0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
@@ -18,9 +18,13 @@ package com.android.systemui.qs.ui.viewmodel
import com.android.compose.animation.scene.Back
import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -43,6 +47,13 @@ constructor(
.map { editing ->
buildMap {
put(Swipe.Up, UserActionResult(SceneFamilies.Home))
+ put(
+ Swipe(
+ direction = SwipeDirection.Down,
+ fromSource = SceneContainerEdge.TopLeft
+ ),
+ ReplaceByOverlay(Overlays.NotificationsShade)
+ )
if (!editing) {
put(Back, UserActionResult(SceneFamilies.Home))
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index 00944b8d0849..834db98263f5 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -16,6 +16,7 @@
package com.android.systemui.scene
+import androidx.compose.ui.unit.dp
import com.android.systemui.CoreStartable
import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,6 +31,8 @@ import com.android.systemui.scene.domain.startable.StatusBarStartable
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
import dagger.Binds
import dagger.Module
@@ -119,5 +122,15 @@ interface KeyguardlessSceneContainerFrameworkModule {
.mapValues { checkNotNull(it.value) }
)
}
+
+ @Provides
+ fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
+ return SplitEdgeDetector(
+ topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+ // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
+ // replace this constant with dynamic window insets.
+ edgeSize = 40.dp
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 4061ad851f57..a4c7d00d0e80 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,6 +16,7 @@
package com.android.systemui.scene
+import androidx.compose.ui.unit.dp
import com.android.systemui.CoreStartable
import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,6 +31,8 @@ import com.android.systemui.scene.domain.startable.StatusBarStartable
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
import dagger.Binds
import dagger.Module
@@ -129,5 +132,15 @@ interface SceneContainerFrameworkModule {
.mapValues { checkNotNull(it.value) }
)
}
+
+ @Provides
+ fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
+ return SplitEdgeDetector(
+ topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+ // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
+ // replace this constant with dynamic window insets.
+ edgeSize = 40.dp
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt
index 99e554ea5595..a3132736fe33 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt
@@ -21,7 +21,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import dagger.Binds
import dagger.Module
@@ -38,17 +38,17 @@ class NotifShadeSceneFamilyResolver
@Inject
constructor(
@Application applicationScope: CoroutineScope,
- shadeInteractor: ShadeInteractor,
+ shadeModeInteractor: ShadeModeInteractor,
) : SceneResolver {
override val targetFamily: SceneKey = SceneFamilies.NotifShade
override val resolvedScene: StateFlow<SceneKey> =
- shadeInteractor.shadeMode
+ shadeModeInteractor.shadeMode
.map(::notifShadeScene)
.stateIn(
applicationScope,
started = SharingStarted.Eagerly,
- initialValue = notifShadeScene(shadeInteractor.shadeMode.value),
+ initialValue = notifShadeScene(shadeModeInteractor.shadeMode.value),
)
override fun includesScene(scene: SceneKey): Boolean = scene in notifShadeScenes
@@ -61,11 +61,7 @@ constructor(
}
companion object {
- val notifShadeScenes =
- setOf(
- Scenes.NotificationsShade,
- Scenes.Shade,
- )
+ val notifShadeScenes = setOf(Scenes.NotificationsShade, Scenes.Shade)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt
index 2962a3ec903d..923e712af15d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt
@@ -21,7 +21,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import dagger.Binds
import dagger.Module
@@ -38,17 +38,17 @@ class QuickSettingsSceneFamilyResolver
@Inject
constructor(
@Application applicationScope: CoroutineScope,
- shadeInteractor: ShadeInteractor,
+ shadeModeInteractor: ShadeModeInteractor,
) : SceneResolver {
override val targetFamily: SceneKey = SceneFamilies.QuickSettings
override val resolvedScene: StateFlow<SceneKey> =
- shadeInteractor.shadeMode
+ shadeModeInteractor.shadeMode
.map(::quickSettingsScene)
.stateIn(
applicationScope,
started = SharingStarted.Eagerly,
- initialValue = quickSettingsScene(shadeInteractor.shadeMode.value),
+ initialValue = quickSettingsScene(shadeModeInteractor.shadeMode.value),
)
override fun includesScene(scene: SceneKey): Boolean = scene in quickSettingsScenes
@@ -62,11 +62,7 @@ constructor(
companion object {
val quickSettingsScenes =
- setOf(
- Scenes.QuickSettings,
- Scenes.QuickSettingsShade,
- Scenes.Shade,
- )
+ setOf(Scenes.QuickSettings, Scenes.QuickSettingsShade, Scenes.Shade)
}
}
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 4c6341b672ad..54823945a827 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
@@ -19,9 +19,11 @@ package com.android.systemui.scene.ui.viewmodel
import android.view.MotionEvent
import androidx.compose.runtime.getValue
import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SwipeSourceDetector
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.classifier.Classifier
@@ -33,12 +35,15 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.logger.SceneLogger
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.Overlay
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
/** Models UI state for the scene container. */
class SceneContainerViewModel
@@ -47,6 +52,8 @@ constructor(
private val sceneInteractor: SceneInteractor,
private val falsingInteractor: FalsingInteractor,
private val powerInteractor: PowerInteractor,
+ private val shadeInteractor: ShadeInteractor,
+ private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
@@ -59,6 +66,20 @@ constructor(
/** Whether the container is visible. */
val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible)
+ /**
+ * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the
+ * [UserAction]s for this container.
+ */
+ val edgeDetector: SwipeSourceDetector by
+ hydrator.hydratedStateOf(
+ traceName = "edgeDetector",
+ initialValue = DefaultEdgeDetector,
+ source =
+ shadeInteractor.shadeMode.map {
+ if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector
+ }
+ )
+
override suspend fun onActivated(): Nothing {
try {
// Sends a MotionEventHandler to the owner of the view-model so they can report
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
new file mode 100644
index 000000000000..f88bcb57a27d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.FixedSizeEdgeDetector
+import com.android.compose.animation.scene.SwipeSource
+import com.android.compose.animation.scene.SwipeSourceDetector
+
+/**
+ * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into
+ * top-left and top-right.
+ */
+enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) :
+ SwipeSource {
+ TopLeft(resolveEdge = { Resolved.TopLeft }),
+ TopRight(resolveEdge = { Resolved.TopRight }),
+ TopStart(
+ resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight }
+ ),
+ TopEnd(
+ resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft }
+ ),
+ Bottom(resolveEdge = { Resolved.Bottom }),
+ Left(resolveEdge = { Resolved.Left }),
+ Right(resolveEdge = { Resolved.Right }),
+ Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }),
+ End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left });
+
+ override fun resolve(layoutDirection: LayoutDirection): Resolved {
+ return resolveEdge(layoutDirection)
+ }
+
+ enum class Resolved : SwipeSource.Resolved {
+ TopLeft,
+ TopRight,
+ Bottom,
+ Left,
+ Right,
+ }
+}
+
+/**
+ * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the
+ * top edge is split in two: top-left and top-right. The split point between the two is dynamic and
+ * may change during runtime.
+ *
+ * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL)
+ * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These
+ * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and
+ * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and
+ * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and
+ * [SceneContainerEdge.Resolved.Right].
+ *
+ * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e.,
+ * percentage) of screen width to consider the split point between "top-left" and "top-right"
+ * edges. It is called on each source detection event.
+ * @param edgeSize The fixed size of each edge.
+ */
+class SplitEdgeDetector(
+ val topEdgeSplitFraction: () -> Float,
+ val edgeSize: Dp,
+) : SwipeSourceDetector {
+
+ private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize)
+
+ override fun source(
+ layoutSize: IntSize,
+ position: IntOffset,
+ density: Density,
+ orientation: Orientation,
+ ): SceneContainerEdge.Resolved? {
+ val fixedEdge =
+ fixedEdgeDetector.source(
+ layoutSize,
+ position,
+ density,
+ orientation,
+ )
+ return when (fixedEdge) {
+ Edge.Resolved.Top -> {
+ val topEdgeSplitFraction = topEdgeSplitFraction()
+ require(topEdgeSplitFraction in 0f..1f) {
+ "topEdgeSplitFraction must return a value between 0.0 and 1.0"
+ }
+ val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction
+ if (isLeftSide) SceneContainerEdge.Resolved.TopLeft
+ else SceneContainerEdge.Resolved.TopRight
+ }
+ Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left
+ Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom
+ Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right
+ null -> null
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
index a77375c14f26..f69b0cb630d3 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
@@ -19,7 +19,6 @@ package com.android.systemui.screenshot;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
-import static com.android.systemui.Flags.screenshotSaveImageExporter;
import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
@@ -133,7 +132,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler
private final MessageContainerController mMessageContainerController;
private final AnnouncementResolver mAnnouncementResolver;
private Bitmap mScreenBitmap;
- private SaveImageInBackgroundTask mSaveInBgTask;
private boolean mScreenshotTakenInPortrait;
private boolean mAttachRequested;
private boolean mDetachRequested;
@@ -393,10 +391,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler
// Any cleanup needed when the service is being destroyed.
@Override
public void onDestroy() {
- if (mSaveInBgTask != null) {
- // just log success/failure for the pre-existing screenshot
- mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
- }
removeWindow();
releaseMediaPlayer();
releaseContext();
@@ -598,36 +592,12 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler
// Play the shutter sound to notify that we've taken a screenshot
playCameraSoundIfNeeded();
- if (screenshotSaveImageExporter()) {
- saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> {
- if (result.uri != null) {
- mScreenshotHandler.post(() -> Toast.makeText(mContext,
- R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
- }
- });
- } else {
- saveScreenshotInWorkerThread(
- screenshot.getUserHandle(),
- /* onComplete */ finisher,
- /* actionsReadyListener */ imageData -> {
- if (DEBUG_CALLBACK) {
- Log.d(TAG,
- "returning URI to finisher (Consumer<URI>): " + imageData.uri);
- }
- finisher.accept(imageData.uri);
- if (imageData.uri == null) {
- mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0,
- mPackageName);
- mNotificationsController.notifyScreenshotError(
- R.string.screenshot_failed_to_save_text);
- } else {
- mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
- mScreenshotHandler.post(() -> Toast.makeText(mContext,
- R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
- }
- },
- null);
- }
+ saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> {
+ if (result.uri != null) {
+ mScreenshotHandler.post(() -> Toast.makeText(mContext,
+ R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
+ }
+ });
}
/**
@@ -700,35 +670,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler
}
/**
- * Creates a new worker thread and saves the screenshot to the media store.
- */
- private void saveScreenshotInWorkerThread(
- UserHandle owner,
- @NonNull Consumer<Uri> finisher,
- @Nullable SaveImageInBackgroundTask.ActionsReadyListener actionsReadyListener,
- @Nullable SaveImageInBackgroundTask.QuickShareActionReadyListener
- quickShareActionsReadyListener) {
- SaveImageInBackgroundTask.SaveImageInBackgroundData
- data = new SaveImageInBackgroundTask.SaveImageInBackgroundData();
- data.image = mScreenBitmap;
- data.finisher = finisher;
- data.mActionsReadyListener = actionsReadyListener;
- data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
- data.owner = owner;
- data.displayId = mDisplay.getDisplayId();
-
- if (mSaveInBgTask != null) {
- // just log success/failure for the pre-existing screenshot
- mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
- }
-
- mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter,
- mScreenshotSmartActions, data,
- mScreenshotNotificationSmartActionsProvider);
- mSaveInBgTask.execute();
- }
-
- /**
* Logs success/failure of the screenshot saving task, and shows an error if it failed.
*/
private void logScreenshotResultStatus(Uri uri, UserHandle owner) {
@@ -745,13 +686,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler
}
}
- /**
- * Logs success/failure of the screenshot saving task, and shows an error if it failed.
- */
- private void logSuccessOnActionsReady(SaveImageInBackgroundTask.SavedImageData imageData) {
- logScreenshotResultStatus(imageData.uri, imageData.owner);
- }
-
private boolean isUserSetupComplete(UserHandle owner) {
return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
deleted file mode 100644
index 9bc3bd842664..000000000000
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ /dev/null
@@ -1,399 +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.systemui.screenshot;
-
-import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE;
-import static com.android.systemui.screenshot.LogConfig.logTag;
-import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.UserHandle;
-import android.provider.DeviceConfig;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
-import com.android.systemui.flags.FeatureFlags;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.text.DateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Random;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-
-/**
- * An AsyncTask that saves an image to the media store in the background.
- */
-class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
- private static final String TAG = logTag(SaveImageInBackgroundTask.class);
-
- private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s";
- private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
-
- /**
- * POD used in the AsyncTask which saves an image in the background.
- */
- static class SaveImageInBackgroundData {
- public Bitmap image;
- public Consumer<Uri> finisher;
- public ActionsReadyListener mActionsReadyListener;
- public QuickShareActionReadyListener mQuickShareActionsReadyListener;
- public UserHandle owner;
- public int displayId;
-
- void clearImage() {
- image = null;
- }
- }
-
- /**
- * Structure returned by the SaveImageInBackgroundTask
- */
- public static class SavedImageData {
- public Uri uri;
- public List<Notification.Action> smartActions;
- public Notification.Action quickShareAction;
- public UserHandle owner;
- public String subject; // Title for sharing
- public Long imageTime; // Time at which screenshot was saved
-
- /**
- * Used to reset the return data on error
- */
- public void reset() {
- uri = null;
- smartActions = null;
- quickShareAction = null;
- subject = null;
- imageTime = null;
- }
- }
-
- /**
- * Structure returned by the QueryQuickShareInBackgroundTask
- */
- static class QuickShareData {
- public Notification.Action quickShareAction;
-
- /**
- * Used to reset the return data on error
- */
- public void reset() {
- quickShareAction = null;
- }
- }
-
- interface ActionsReadyListener {
- void onActionsReady(SavedImageData imageData);
- }
-
- interface QuickShareActionReadyListener {
- void onActionsReady(QuickShareData quickShareData);
- }
-
- private final Context mContext;
- private FeatureFlags mFlags;
- private final ScreenshotSmartActions mScreenshotSmartActions;
- private final SaveImageInBackgroundData mParams;
- private final SavedImageData mImageData;
- private final QuickShareData mQuickShareData;
-
- private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
- private String mScreenshotId;
- private final Random mRandom = new Random();
- private final ImageExporter mImageExporter;
- private long mImageTime;
-
- SaveImageInBackgroundTask(
- Context context,
- FeatureFlags flags,
- ImageExporter exporter,
- ScreenshotSmartActions screenshotSmartActions,
- SaveImageInBackgroundData data,
- ScreenshotNotificationSmartActionsProvider
- screenshotNotificationSmartActionsProvider
- ) {
- mContext = context;
- mFlags = flags;
- mScreenshotSmartActions = screenshotSmartActions;
- mImageData = new SavedImageData();
- mQuickShareData = new QuickShareData();
- mImageExporter = exporter;
-
- // Prepare all the output metadata
- mParams = data;
-
- // Initialize screenshot notification smart actions provider.
- mSmartActionsProvider = screenshotNotificationSmartActionsProvider;
- }
-
- @Override
- protected Void doInBackground(Void... paramsUnused) {
- if (isCancelled()) {
- if (DEBUG_STORAGE) {
- Log.d(TAG, "cancelled! returning null");
- }
- return null;
- }
- // TODO: move to constructor / from ScreenshotRequest
- final UUID requestId = UUID.randomUUID();
-
- Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
-
- Bitmap image = mParams.image;
- mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, requestId);
-
- boolean savingToOtherUser = mParams.owner != Process.myUserHandle();
- // Smart actions don't yet work for cross-user saves.
- boolean smartActionsEnabled = !savingToOtherUser
- && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS,
- true);
- try {
- if (smartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) {
- // Since Quick Share target recommendation does not rely on image URL, it is
- // queried and surfaced before image compress/export. Action intent would not be
- // used, because it does not contain image URL.
- Notification.Action quickShare =
- queryQuickShareAction(mScreenshotId, image, mParams.owner, null);
- if (quickShare != null) {
- mQuickShareData.quickShareAction = quickShare;
- mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
- }
- }
-
- // Call synchronously here since already on a background thread.
- ListenableFuture<ImageExporter.Result> future =
- mImageExporter.export(Runnable::run, requestId, image, mParams.owner,
- mParams.displayId);
- ImageExporter.Result result = future.get();
- Log.d(TAG, "Saved screenshot: " + result);
- final Uri uri = result.uri;
- mImageTime = result.timestamp;
-
- CompletableFuture<List<Notification.Action>> smartActionsFuture =
- mScreenshotSmartActions.getSmartActionsFuture(
- mScreenshotId, uri, image, mSmartActionsProvider,
- ScreenshotSmartActionType.REGULAR_SMART_ACTIONS,
- smartActionsEnabled, mParams.owner);
- List<Notification.Action> smartActions = new ArrayList<>();
- if (smartActionsEnabled) {
- int timeoutMs = DeviceConfig.getInt(
- DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS,
- 1000);
- smartActions.addAll(buildSmartActions(
- mScreenshotSmartActions.getSmartActions(
- mScreenshotId, smartActionsFuture, timeoutMs,
- mSmartActionsProvider,
- ScreenshotSmartActionType.REGULAR_SMART_ACTIONS),
- mContext));
- }
-
- mImageData.uri = uri;
- mImageData.owner = mParams.owner;
- mImageData.smartActions = smartActions;
- mImageData.quickShareAction = createQuickShareAction(
- mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image,
- mParams.owner);
- mImageData.subject = getSubjectString(mImageTime);
- mImageData.imageTime = mImageTime;
-
- mParams.mActionsReadyListener.onActionsReady(mImageData);
- if (DEBUG_CALLBACK) {
- Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) "
- + "finisher.accept(\"" + mImageData.uri + "\"");
- }
- mParams.finisher.accept(mImageData.uri);
- mParams.image = null;
- } catch (Exception e) {
- // IOException/UnsupportedOperationException may be thrown if external storage is
- // not mounted
- Log.d(TAG, "Failed to store screenshot", e);
- mParams.clearImage();
- mImageData.reset();
- mQuickShareData.reset();
- mParams.mActionsReadyListener.onActionsReady(mImageData);
- if (DEBUG_CALLBACK) {
- Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)");
- }
- mParams.finisher.accept(null);
- }
-
- return null;
- }
-
- /**
- * Update the listener run when the saving task completes. Used to avoid showing UI for the
- * first screenshot when a second one is taken.
- */
- void setActionsReadyListener(ActionsReadyListener listener) {
- mParams.mActionsReadyListener = listener;
- }
-
- @Override
- protected void onCancelled(Void params) {
- // If we are cancelled while the task is running in the background, we may get null
- // params. The finisher is expected to always be called back, so just use the baked-in
- // params from the ctor in any case.
- mImageData.reset();
- mQuickShareData.reset();
- mParams.mActionsReadyListener.onActionsReady(mImageData);
- if (DEBUG_CALLBACK) {
- Log.d(TAG, "onCancelled, calling (Consumer<Uri>) finisher.accept(null)");
- }
- mParams.finisher.accept(null);
- mParams.clearImage();
- }
-
- private List<Notification.Action> buildSmartActions(
- List<Notification.Action> actions, Context context) {
- List<Notification.Action> broadcastActions = new ArrayList<>();
- for (Notification.Action action : actions) {
- // Proxy smart actions through {@link SmartActionsReceiver} for logging smart actions.
- Bundle extras = action.getExtras();
- String actionType = extras.getString(
- ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
- ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE);
- Intent intent = new Intent(context, SmartActionsReceiver.class)
- .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT, action.actionIntent)
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */);
- PendingIntent broadcastIntent = PendingIntent.getBroadcast(context,
- mRandom.nextInt(),
- intent,
- PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
- broadcastActions.add(new Notification.Action.Builder(action.getIcon(), action.title,
- broadcastIntent).setContextual(true).addExtras(extras).build());
- }
- return broadcastActions;
- }
-
- private static void addIntentExtras(String screenshotId, Intent intent, String actionType,
- boolean smartActionsEnabled) {
- intent
- .putExtra(SmartActionsReceiver.EXTRA_ACTION_TYPE, actionType)
- .putExtra(SmartActionsReceiver.EXTRA_ID, screenshotId)
- .putExtra(SmartActionsReceiver.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled);
- }
-
- /**
- * Wrap the quickshare intent and populate the fillin intent with the URI
- */
- @VisibleForTesting
- Notification.Action createQuickShareAction(
- Notification.Action quickShare, String screenshotId, Uri uri, long imageTime,
- Bitmap image, UserHandle user) {
- if (quickShare == null) {
- return null;
- } else if (quickShare.actionIntent.isImmutable()) {
- Notification.Action quickShareWithUri =
- queryQuickShareAction(screenshotId, image, user, uri);
- if (quickShareWithUri == null
- || !quickShareWithUri.title.toString().contentEquals(quickShare.title)) {
- return null;
- }
- quickShare = quickShareWithUri;
- }
-
- Intent wrappedIntent = new Intent(mContext, SmartActionsReceiver.class)
- .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT, quickShare.actionIntent)
- .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT_FILLIN,
- createFillInIntent(uri, imageTime))
- .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- Bundle extras = quickShare.getExtras();
- String actionType = extras.getString(
- ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
- ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE);
- // We only query for quick share actions when smart actions are enabled, so we can assert
- // that it's true here.
- addIntentExtras(screenshotId, wrappedIntent, actionType, true /* smartActionsEnabled */);
- PendingIntent broadcastIntent =
- PendingIntent.getBroadcast(mContext, mRandom.nextInt(), wrappedIntent,
- PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
- return new Notification.Action.Builder(quickShare.getIcon(), quickShare.title,
- broadcastIntent)
- .setContextual(true)
- .addExtras(extras)
- .build();
- }
-
- private Intent createFillInIntent(Uri uri, long imageTime) {
- Intent fillIn = new Intent();
- fillIn.setType("image/png");
- fillIn.putExtra(Intent.EXTRA_STREAM, uri);
- fillIn.putExtra(Intent.EXTRA_SUBJECT, getSubjectString(imageTime));
- // Include URI in ClipData also, so that grantPermission picks it up.
- // We don't use setData here because some apps interpret this as "to:".
- ClipData clipData = new ClipData(
- new ClipDescription("content", new String[]{"image/png"}),
- new ClipData.Item(uri));
- fillIn.setClipData(clipData);
- fillIn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- return fillIn;
- }
-
- /**
- * Query and surface Quick Share chip if it is available. Action intent would not be used,
- * because it does not contain image URL which would be populated in {@link
- * #createQuickShareAction(Notification.Action, String, Uri, long, Bitmap, UserHandle)}
- */
-
- @VisibleForTesting
- Notification.Action queryQuickShareAction(
- String screenshotId, Bitmap image, UserHandle user, Uri uri) {
- CompletableFuture<List<Notification.Action>> quickShareActionsFuture =
- mScreenshotSmartActions.getSmartActionsFuture(
- screenshotId, uri, image, mSmartActionsProvider,
- ScreenshotSmartActionType.QUICK_SHARE_ACTION,
- true /* smartActionsEnabled */, user);
- int timeoutMs = DeviceConfig.getInt(
- DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_QUICK_SHARE_ACTIONS_TIMEOUT_MS,
- 500);
- List<Notification.Action> quickShareActions =
- mScreenshotSmartActions.getSmartActions(
- screenshotId, quickShareActionsFuture, timeoutMs,
- mSmartActionsProvider,
- ScreenshotSmartActionType.QUICK_SHARE_ACTION);
- if (!quickShareActions.isEmpty()) {
- return quickShareActions.get(0);
- }
- return null;
- }
-
- private static String getSubjectString(long imageTime) {
- String subjectDate = DateFormat.getDateTimeInstance().format(new Date(imageTime));
- return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 7b802a2a40aa..fe58bc9f34a9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -18,7 +18,6 @@ package com.android.systemui.screenshot;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static com.android.systemui.Flags.screenshotSaveImageExporter;
import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
@@ -124,7 +123,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
private final MessageContainerController mMessageContainerController;
private final AnnouncementResolver mAnnouncementResolver;
private Bitmap mScreenBitmap;
- private SaveImageInBackgroundTask mSaveInBgTask;
private boolean mScreenshotTakenInPortrait;
private Animator mScreenshotAnimation;
private RequestCallback mCurrentRequestCallback;
@@ -373,10 +371,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
// Any cleanup needed when the service is being destroyed.
@Override
public void onDestroy() {
- if (mSaveInBgTask != null) {
- // just log success/failure for the pre-existing screenshot
- mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
- }
removeWindow();
releaseMediaPlayer();
releaseContext();
@@ -525,36 +519,12 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
// Play the shutter sound to notify that we've taken a screenshot
playCameraSoundIfNeeded();
- if (screenshotSaveImageExporter()) {
- saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> {
- if (result.uri != null) {
- mScreenshotHandler.post(() -> Toast.makeText(mContext,
- R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
- }
- });
- } else {
- saveScreenshotInWorkerThread(
- screenshot.getUserHandle(),
- /* onComplete */ finisher,
- /* actionsReadyListener */ imageData -> {
- if (DEBUG_CALLBACK) {
- Log.d(TAG,
- "returning URI to finisher (Consumer<URI>): " + imageData.uri);
- }
- finisher.accept(imageData.uri);
- if (imageData.uri == null) {
- mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0,
- mPackageName);
- mNotificationsController.notifyScreenshotError(
- R.string.screenshot_failed_to_save_text);
- } else {
- mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
- mScreenshotHandler.post(() -> Toast.makeText(mContext,
- R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
- }
- },
- null);
- }
+ saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> {
+ if (result.uri != null) {
+ mScreenshotHandler.post(() -> Toast.makeText(mContext,
+ R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
+ }
+ });
}
/**
@@ -627,35 +597,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
}
/**
- * Creates a new worker thread and saves the screenshot to the media store.
- */
- private void saveScreenshotInWorkerThread(
- UserHandle owner,
- @NonNull Consumer<Uri> finisher,
- @Nullable SaveImageInBackgroundTask.ActionsReadyListener actionsReadyListener,
- @Nullable SaveImageInBackgroundTask.QuickShareActionReadyListener
- quickShareActionsReadyListener) {
- SaveImageInBackgroundTask.SaveImageInBackgroundData
- data = new SaveImageInBackgroundTask.SaveImageInBackgroundData();
- data.image = mScreenBitmap;
- data.finisher = finisher;
- data.mActionsReadyListener = actionsReadyListener;
- data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
- data.owner = owner;
- data.displayId = mDisplay.getDisplayId();
-
- if (mSaveInBgTask != null) {
- // just log success/failure for the pre-existing screenshot
- mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
- }
-
- mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter,
- mScreenshotSmartActions, data,
- mScreenshotNotificationSmartActionsProvider);
- mSaveInBgTask.execute();
- }
-
- /**
* Logs success/failure of the screenshot saving task, and shows an error if it failed.
*/
private void logScreenshotResultStatus(Uri uri, UserHandle owner) {
@@ -672,13 +613,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
}
}
- /**
- * Logs success/failure of the screenshot saving task, and shows an error if it failed.
- */
- private void logSuccessOnActionsReady(SaveImageInBackgroundTask.SavedImageData imageData) {
- logScreenshotResultStatus(imageData.uri, imageData.owner);
- }
-
private boolean isUserSetupComplete(UserHandle owner) {
return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
index 7425807b716d..99ff94605c39 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
@@ -28,6 +28,8 @@ import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractorEmptyImpl
import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractorEmptyImpl
import dagger.Binds
import dagger.Module
@@ -75,4 +77,8 @@ abstract class ShadeEmptyImplModule {
@Binds
@SysUISingleton
abstract fun bindsPrivacyChipRepository(impl: PrivacyChipRepositoryImpl): PrivacyChipRepository
+
+ @Binds
+ @SysUISingleton
+ abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorEmptyImpl): ShadeModeInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index da2024b4ef18..2348a110eb3a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -41,6 +41,8 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl
import com.android.systemui.shade.domain.interactor.ShadeInteractorSceneContainerImpl
import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor
import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractorImpl
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractorImpl
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -54,7 +56,7 @@ abstract class ShadeModule {
@SysUISingleton
fun provideBaseShadeInteractor(
sceneContainerOn: Provider<ShadeInteractorSceneContainerImpl>,
- sceneContainerOff: Provider<ShadeInteractorLegacyImpl>
+ sceneContainerOff: Provider<ShadeInteractorLegacyImpl>,
): BaseShadeInteractor {
return if (SceneContainerFlag.isEnabled) {
sceneContainerOn.get()
@@ -67,7 +69,7 @@ abstract class ShadeModule {
@SysUISingleton
fun provideShadeController(
sceneContainerOn: Provider<ShadeControllerSceneImpl>,
- sceneContainerOff: Provider<ShadeControllerImpl>
+ sceneContainerOff: Provider<ShadeControllerImpl>,
): ShadeController {
return if (SceneContainerFlag.isEnabled) {
sceneContainerOn.get()
@@ -80,7 +82,7 @@ abstract class ShadeModule {
@SysUISingleton
fun provideShadeAnimationInteractor(
sceneContainerOn: Provider<ShadeAnimationInteractorSceneContainerImpl>,
- sceneContainerOff: Provider<ShadeAnimationInteractorLegacyImpl>
+ sceneContainerOff: Provider<ShadeAnimationInteractorLegacyImpl>,
): ShadeAnimationInteractor {
return if (SceneContainerFlag.isEnabled) {
sceneContainerOn.get()
@@ -93,7 +95,7 @@ abstract class ShadeModule {
@SysUISingleton
fun provideShadeBackActionInteractor(
sceneContainerOn: Provider<ShadeBackActionInteractorImpl>,
- sceneContainerOff: Provider<NotificationPanelViewController>
+ sceneContainerOff: Provider<NotificationPanelViewController>,
): ShadeBackActionInteractor {
return if (SceneContainerFlag.isEnabled) {
sceneContainerOn.get()
@@ -106,7 +108,7 @@ abstract class ShadeModule {
@SysUISingleton
fun provideShadeLockscreenInteractor(
sceneContainerOn: Provider<ShadeLockscreenInteractorImpl>,
- sceneContainerOff: Provider<NotificationPanelViewController>
+ sceneContainerOff: Provider<NotificationPanelViewController>,
): ShadeLockscreenInteractor {
return if (SceneContainerFlag.isEnabled) {
sceneContainerOn.get()
@@ -119,7 +121,7 @@ abstract class ShadeModule {
@SysUISingleton
fun providePanelExpansionInteractor(
sceneContainerOn: Provider<PanelExpansionInteractorImpl>,
- sceneContainerOff: Provider<NotificationPanelViewController>
+ sceneContainerOff: Provider<NotificationPanelViewController>,
): PanelExpansionInteractor {
return if (SceneContainerFlag.isEnabled) {
sceneContainerOn.get()
@@ -170,4 +172,8 @@ abstract class ShadeModule {
@Binds
@SysUISingleton
abstract fun bindsPrivacyChipRepository(impl: PrivacyChipRepositoryImpl): PrivacyChipRepository
+
+ @Binds
+ @SysUISingleton
+ abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorImpl): ShadeModeInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index 73e86a2be4aa..6fb96da2c186 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade.domain.interactor
+import androidx.annotation.FloatRange
import com.android.systemui.shade.shared.model.ShadeMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -69,6 +70,20 @@ interface ShadeInteractor : BaseShadeInteractor {
* wide as the entire screen.
*/
val isShadeLayoutWide: StateFlow<Boolean>
+
+ /**
+ * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold
+ * between "top-left" and "top-right" for the purposes of dual-shade invocation.
+ *
+ * When the dual-shade is not wide, this always returns 0.5 (the top edge is evenly split). On
+ * wide layouts however, a larger fraction is returned because only the area of the system
+ * status icons is considered top-right.
+ *
+ * Note that this fraction only determines the split between the absolute left and right
+ * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end"
+ * will resolve to "top-left".
+ */
+ @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float
}
/** ShadeInteractor methods with implementations that differ between non-empty impls. */
@@ -130,7 +145,7 @@ interface BaseShadeInteractor {
fun createAnyExpansionFlow(
scope: CoroutineScope,
shadeExpansion: Flow<Float>,
- qsExpansion: Flow<Float>
+ qsExpansion: Flow<Float>,
): StateFlow<Float> {
return combine(shadeExpansion, qsExpansion) { shadeExp, qsExp -> maxOf(shadeExp, qsExp) }
.stateIn(scope, SharingStarted.Eagerly, 0f)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index d51fd28d5458..6c0b55a5dd57 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -47,4 +47,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor {
override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean
override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single)
override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean
+
+ override fun getTopEdgeSplitFraction(): Float = 0.5f
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index 3552092d24e7..3eab02ad30d5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -24,9 +24,6 @@ import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.Edge
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.shade.shared.flag.DualShade
-import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
@@ -54,11 +51,14 @@ constructor(
keyguardRepository: KeyguardRepository,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
powerInteractor: PowerInteractor,
- private val shadeRepository: ShadeRepository,
userSetupRepository: UserSetupRepository,
userSwitcherInteractor: UserSwitcherInteractor,
private val baseShadeInteractor: BaseShadeInteractor,
-) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor {
+ shadeModeInteractor: ShadeModeInteractor,
+) :
+ ShadeInteractor,
+ BaseShadeInteractor by baseShadeInteractor,
+ ShadeModeInteractor by shadeModeInteractor {
override val isShadeEnabled: StateFlow<Boolean> =
disableFlagsRepository.disableFlags
.map { it.isShadeEnabled() }
@@ -102,17 +102,6 @@ constructor(
}
}
- override val isShadeLayoutWide: StateFlow<Boolean> = shadeRepository.isShadeLayoutWide
-
- override val shadeMode: StateFlow<ShadeMode> =
- isShadeLayoutWide
- .map(this::determineShadeMode)
- .stateIn(
- scope,
- SharingStarted.Eagerly,
- initialValue = determineShadeMode(isShadeLayoutWide.value)
- )
-
override val isExpandToQsEnabled: Flow<Boolean> =
combine(
disableFlagsRepository.disableFlags,
@@ -129,12 +118,4 @@ constructor(
disableFlags.isQuickSettingsEnabled() &&
!isDozing
}
-
- private fun determineShadeMode(isShadeLayoutWide: Boolean): ShadeMode {
- return when {
- DualShade.isEnabled -> ShadeMode.Dual
- isShadeLayoutWide -> ShadeMode.Split
- else -> ShadeMode.Single
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
new file mode 100644
index 000000000000..77ae679bf018
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import androidx.annotation.FloatRange
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.shade.shared.model.ShadeMode
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Defines interface for classes that can provide state and business logic related to the mode of
+ * the shade.
+ */
+interface ShadeModeInteractor {
+
+ /**
+ * The version of the shade layout to use.
+ *
+ * Note: Most likely, you want to read [isShadeLayoutWide] instead of this.
+ */
+ val shadeMode: StateFlow<ShadeMode>
+
+ /**
+ * Whether the shade layout should be wide (true) or narrow (false).
+ *
+ * In a wide layout, notifications and quick settings each take up only half the screen width
+ * (whether they are shown at the same time or not). In a narrow layout, they can each be as
+ * wide as the entire screen.
+ */
+ val isShadeLayoutWide: StateFlow<Boolean>
+
+ /**
+ * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold
+ * between "top-left" and "top-right" for the purposes of dual-shade invocation.
+ *
+ * When the dual-shade is not wide, this always returns 0.5 (the top edge is evenly split). On
+ * wide layouts however, a larger fraction is returned because only the area of the system
+ * status icons is considered top-right.
+ *
+ * Note that this fraction only determines the split between the absolute left and right
+ * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end"
+ * will resolve to "top-left".
+ */
+ @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float
+}
+
+class ShadeModeInteractorImpl
+@Inject
+constructor(
+ @Application applicationScope: CoroutineScope,
+ private val repository: ShadeRepository,
+) : ShadeModeInteractor {
+
+ override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide
+
+ override val shadeMode: StateFlow<ShadeMode> =
+ isShadeLayoutWide
+ .map(this::determineShadeMode)
+ .stateIn(
+ applicationScope,
+ SharingStarted.Eagerly,
+ initialValue = determineShadeMode(isShadeLayoutWide.value),
+ )
+
+ @FloatRange(from = 0.0, to = 1.0)
+ override fun getTopEdgeSplitFraction(): Float {
+ // Note: this implicitly relies on isShadeLayoutWide being hot (i.e. collected). This
+ // assumption allows us to query its value on demand (during swipe source detection) instead
+ // of running another infinite coroutine.
+ // TODO(b/338577208): Instead of being fixed at 0.8f, this should dynamically updated based
+ // on the position of system-status icons in the status bar.
+ return if (repository.isShadeLayoutWide.value) 0.8f else 0.5f
+ }
+
+ private fun determineShadeMode(isShadeLayoutWide: Boolean): ShadeMode {
+ return when {
+ DualShade.isEnabled -> ShadeMode.Dual
+ isShadeLayoutWide -> ShadeMode.Split
+ else -> ShadeMode.Single
+ }
+ }
+}
+
+class ShadeModeInteractorEmptyImpl @Inject constructor() : ShadeModeInteractor {
+
+ override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single)
+
+ override val isShadeLayoutWide: StateFlow<Boolean> = MutableStateFlow(false)
+
+ override fun getTopEdgeSplitFraction(): Float = 0.5f
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
index 37ac7c4330af..38cab820c133 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
@@ -108,7 +108,7 @@ constructor(@NotificationInterruptLog val buffer: LogBuffer) {
TAG,
INFO,
{ bool1 = isEnabled },
- { "Cooldown enabled: $isEnabled" }
+ { "Cooldown enabled: $bool1" }
)
}
}
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 8ff1ab640442..1214440a6b65 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
@@ -5404,7 +5404,9 @@ public class NotificationStackScrollLayout
println(pw, "intrinsicContentHeight", mIntrinsicContentHeight);
println(pw, "contentHeight", mContentHeight);
println(pw, "intrinsicPadding", mIntrinsicPadding);
- println(pw, "topPadding", getTopPadding());
+ if (!SceneContainerFlag.isEnabled()) {
+ println(pw, "topPadding", getTopPadding());
+ }
println(pw, "bottomPadding", mBottomPadding);
dumpRoundedRectClipping(pw);
println(pw, "requestedClipBounds", mRequestedClipBounds);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
index 580431a13d1b..969ff1b4ffe7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
@@ -68,6 +68,7 @@ public class SectionHeaderView extends StackScrollerDecorView {
if (mLabelTextId != null) {
mLabelView.setText(mLabelTextId);
}
+ mLabelView.setAccessibilityHeading(true);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index ef1bcfc45879..cccac4b479dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -682,7 +682,10 @@ public class StackScrollAlgorithm {
// doesn't get updated quickly enough and can cause the footer to flash when
// closing the shade. As such, we temporarily also check the ambientState directly.
if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) {
- viewState.hidden = true;
+ // Note: This is no longer necessary in flexiglass.
+ if (!SceneContainerFlag.isEnabled()) {
+ viewState.hidden = true;
+ }
} else {
final float footerEnd = algorithmState.mCurrentExpandedYPosition
+ view.getIntrinsicHeight();
@@ -691,7 +694,6 @@ public class StackScrollAlgorithm {
noSpaceForFooter || (ambientState.isClearAllInProgress()
&& !hasNonClearableNotifs(algorithmState));
}
-
} else {
final boolean shadeClosed = !ambientState.isShadeExpanded();
final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index d770b2003f3b..dc9615c25ada 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -188,15 +188,26 @@ constructor(
.startHistoryIntent(view, /* showHistory= */ true)
},
)
- launch {
- viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
- footerView.setVisible(
- /* visible = */ animatedVisibility.value,
- /* animate = */ animatedVisibility.isAnimating,
- )
+ if (SceneContainerFlag.isEnabled) {
+ launch {
+ viewModel.shouldShowFooterView.collect { animatedVisibility ->
+ footerView.setVisible(
+ /* visible = */ animatedVisibility.value,
+ /* animate = */ animatedVisibility.isAnimating,
+ )
+ }
+ }
+ } else {
+ launch {
+ viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
+ footerView.setVisible(
+ /* visible = */ animatedVisibility.value,
+ /* animate = */ animatedVisibility.isAnimating,
+ )
+ }
}
+ launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
}
- launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
disposableHandle.awaitCancellationThenDispose()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index e55492e67d02..4e2a46d78a5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -32,6 +32,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.Notif
import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.util.kotlin.combine
import com.android.systemui.util.kotlin.sample
import com.android.systemui.util.ui.AnimatableEvent
import com.android.systemui.util.ui.AnimatedValue
@@ -120,6 +121,7 @@ constructor(
* This essentially corresponds to having the view set to INVISIBLE.
*/
val shouldHideFooterView: Flow<Boolean> by lazy {
+ SceneContainerFlag.assertInLegacyMode()
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
flowOf(false)
} else {
@@ -143,6 +145,7 @@ constructor(
* be hidden by another condition (see [shouldHideFooterView] above).
*/
val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+ SceneContainerFlag.assertInLegacyMode()
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
flowOf(AnimatedValue.NotAnimating(false))
} else {
@@ -207,6 +210,76 @@ constructor(
}
}
+ // This flow replaces shouldHideFooterView+shouldIncludeFooterView in flexiglass.
+ val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+ flowOf(AnimatedValue.NotAnimating(false))
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ userSetupInteractor.isUserSetUp,
+ notificationStackInteractor.isShowingOnLockscreen,
+ shadeInteractor.isQsFullscreen,
+ remoteInputInteractor.isRemoteInputActive,
+ shadeInteractor.shadeExpansion.map { it < 0.5f }.distinctUntilChanged(),
+ ) {
+ hasNotifications,
+ isUserSetUp,
+ isShowingOnLockscreen,
+ qsFullScreen,
+ isRemoteInputActive,
+ shadeLessThanHalfwayExpanded ->
+ when {
+ !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ // Hide the footer until the user setup is complete, to prevent access
+ // to settings (b/193149550).
+ !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ // Do not show the footer if the lockscreen is visible (incl. AOD),
+ // except if the shade is opened on top. See also b/219680200.
+ // Do not animate, as that makes the footer appear briefly when
+ // transitioning between the shade and keyguard.
+ isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
+ // Do not show the footer if quick settings are fully expanded (except
+ // for the foldable split shade view). See b/201427195 && b/222699879.
+ qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ // Hide the footer if remote input is active (i.e. user is replying to a
+ // notification). See b/75984847.
+ isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ // If the shade is not expanded enough, the footer shouldn't be visible.
+ shadeLessThanHalfwayExpanded -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ else -> VisibilityChange.APPEAR_WITH_ANIMATION
+ }
+ }
+ .distinctUntilChanged(
+ // Equivalent unless visibility changes
+ areEquivalent = { a: VisibilityChange, b: VisibilityChange ->
+ a.visible == b.visible
+ }
+ )
+ // Should we animate the visibility change?
+ .sample(
+ // TODO(b/322167853): This check is currently duplicated in FooterViewModel,
+ // but instead it should be a field in ShadeAnimationInteractor.
+ combine(
+ shadeInteractor.isShadeFullyExpanded,
+ shadeInteractor.isShadeTouchable,
+ ::Pair
+ )
+ .onStart { emit(Pair(false, false)) }
+ ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
+ // Animate if the shade is interactive, but NOT on the lockscreen. Having
+ // animations enabled while on the lockscreen makes the footer appear briefly
+ // when transitioning between the shade and keyguard.
+ val shouldAnimate =
+ isShadeFullyExpanded && animationsEnabled && visibilityChange.canAnimate
+ AnimatableEvent(visibilityChange.visible, shouldAnimate)
+ }
+ .toAnimatedValueFlow()
+ .dumpWhileCollecting("shouldShowFooterView")
+ .flowOn(bgDispatcher)
+ }
+ }
+
enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index dd4b0005b034..f3b937100db2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -182,6 +182,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
private boolean mBouncerShowingOverDream;
private int mAttemptsToShowBouncer = 0;
private DelayableExecutor mExecutor;
+ private boolean mIsSleeping = false;
private final PrimaryBouncerExpansionCallback mExpansionCallback =
new PrimaryBouncerExpansionCallback() {
@@ -713,7 +714,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
* {@link #needsFullscreenBouncer()}.
*/
protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
- if (needsFullscreenBouncer() && !mDozing) {
+ boolean showBouncer = needsFullscreenBouncer() && !mDozing;
+ if (Flags.simPinRaceConditionOnRestart()) {
+ showBouncer = showBouncer && !mIsSleeping;
+ }
+ if (showBouncer) {
// The keyguard might be showing (already). So we need to hide it.
if (!primaryBouncerIsShowing()) {
if (SceneContainerFlag.isEnabled()) {
@@ -1041,6 +1046,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
@Override
public void onStartedWakingUp() {
+ mIsSleeping = false;
setRootViewAnimationDisabled(false);
NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView();
if (navBarView != null) {
@@ -1054,6 +1060,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
@Override
public void onStartedGoingToSleep() {
+ mIsSleeping = true;
setRootViewAnimationDisabled(true);
NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView();
if (navBarView != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
index 9715772f089f..28a43df2bfb3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
@@ -16,35 +16,16 @@
package com.android.systemui.volume.dagger
-import android.view.accessibility.CaptioningManager
import com.android.systemui.accessibility.data.repository.CaptioningRepository
import com.android.systemui.accessibility.data.repository.CaptioningRepositoryImpl
-import com.android.systemui.accessibility.domain.interactor.CaptioningInteractor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
+import dagger.Binds
import dagger.Module
-import dagger.Provides
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.CoroutineScope
@Module
interface CaptioningModule {
- companion object {
-
- @Provides
- @SysUISingleton
- fun provideCaptioningRepository(
- captioningManager: CaptioningManager,
- @Background coroutineContext: CoroutineContext,
- @Application coroutineScope: CoroutineScope,
- ): CaptioningRepository =
- CaptioningRepositoryImpl(captioningManager, coroutineContext, coroutineScope)
-
- @Provides
- @SysUISingleton
- fun provideCaptioningInteractor(repository: CaptioningRepository): CaptioningInteractor =
- CaptioningInteractor(repository)
- }
+ @Binds
+ @SysUISingleton
+ fun bindCaptioningRepository(impl: CaptioningRepositoryImpl): CaptioningRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt
index 52f2ce63ba21..2e5e389eba9c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt
@@ -26,7 +26,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
@VolumePanelScope
class CaptioningAvailabilityCriteria
@@ -45,7 +45,7 @@ constructor(
else VolumePanelUiEvent.VOLUME_PANEL_LIVE_CAPTION_TOGGLE_GONE
)
}
- .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
override fun isAvailable(): Flow<Boolean> = availability
}
diff --git a/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json
new file mode 100644
index 000000000000..f37580dd47d4
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json
@@ -0,0 +1,831 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272,
+ 288,
+ 304,
+ 320,
+ 336,
+ 352,
+ 368,
+ 384,
+ 400,
+ 416,
+ 432,
+ 448,
+ 464,
+ 480,
+ 496,
+ 512,
+ 528,
+ 544,
+ 560,
+ 576,
+ 592,
+ 608,
+ 624,
+ 640,
+ 656,
+ 672,
+ 688,
+ 704,
+ 720,
+ 736,
+ 752,
+ 768,
+ 784,
+ 800,
+ 816,
+ 832,
+ 848,
+ 864,
+ 880,
+ 896,
+ 912,
+ 928,
+ 944,
+ 960,
+ 976,
+ 992,
+ 1008,
+ 1024,
+ "after"
+ ],
+ "features": [
+ {
+ "name": "content_alpha",
+ "type": "float",
+ "data_points": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0.9954499,
+ 0.9805035,
+ 0.9527822,
+ 0.9092045,
+ 0.84588075,
+ 0.7583043,
+ 0.6424476,
+ 0.49766344,
+ 0.33080608,
+ 0.15650165,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ {
+ "type": "not_found"
+ }
+ ]
+ },
+ {
+ "name": "content_scale",
+ "type": "scale",
+ "data_points": [
+ "default",
+ {
+ "x": 0.9995097,
+ "y": 0.9995097,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.997352,
+ "y": 0.997352,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.990635,
+ "y": 0.990635,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.97249764,
+ "y": 0.97249764,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.94287145,
+ "y": 0.94287145,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.9128026,
+ "y": 0.9128026,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8859569,
+ "y": 0.8859569,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8629254,
+ "y": 0.8629254,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8442908,
+ "y": 0.8442908,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8303209,
+ "y": 0.8303209,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8205137,
+ "y": 0.8205137,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.81387186,
+ "y": 0.81387186,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80941653,
+ "y": 0.80941653,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80641484,
+ "y": 0.80641484,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80437464,
+ "y": 0.80437464,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80297637,
+ "y": 0.80297637,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80201286,
+ "y": 0.80201286,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8013477,
+ "y": 0.8013477,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8008894,
+ "y": 0.8008894,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8005756,
+ "y": 0.8005756,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80036324,
+ "y": 0.80036324,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8002219,
+ "y": 0.8002219,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80012995,
+ "y": 0.80012995,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8000721,
+ "y": 0.8000721,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80003715,
+ "y": 0.80003715,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8000173,
+ "y": 0.8000173,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.800007,
+ "y": 0.800007,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8000022,
+ "y": 0.8000022,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8000004,
+ "y": 0.8000004,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.79999995,
+ "y": 0.79999995,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "type": "not_found"
+ }
+ ]
+ },
+ {
+ "name": "content_offset",
+ "type": "dpOffset",
+ "data_points": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0.5714286
+ },
+ {
+ "x": 0,
+ "y": 2.857143
+ },
+ {
+ "x": 0,
+ "y": 7.142857
+ },
+ {
+ "x": 0,
+ "y": 13.714286
+ },
+ {
+ "x": 0,
+ "y": 23.142857
+ },
+ {
+ "x": 0,
+ "y": 36.285713
+ },
+ {
+ "x": 0,
+ "y": 53.714287
+ },
+ {
+ "x": 0,
+ "y": 75.42857
+ },
+ {
+ "x": 0,
+ "y": 100.28571
+ },
+ {
+ "x": 0,
+ "y": 126.57143
+ },
+ {
+ "x": 0,
+ "y": 151.42857
+ },
+ {
+ "x": 0,
+ "y": 174
+ },
+ {
+ "x": 0,
+ "y": 193.42857
+ },
+ {
+ "x": 0,
+ "y": 210.28572
+ },
+ {
+ "x": 0,
+ "y": 224.85715
+ },
+ {
+ "x": 0,
+ "y": 237.14285
+ },
+ {
+ "x": 0,
+ "y": 247.71428
+ },
+ {
+ "x": 0,
+ "y": 256.85715
+ },
+ {
+ "x": 0,
+ "y": 264.57144
+ },
+ {
+ "x": 0,
+ "y": 271.42856
+ },
+ {
+ "x": 0,
+ "y": 277.14285
+ },
+ {
+ "x": 0,
+ "y": 282
+ },
+ {
+ "x": 0,
+ "y": 286.2857
+ },
+ {
+ "x": 0,
+ "y": 289.7143
+ },
+ {
+ "x": 0,
+ "y": 292.57144
+ },
+ {
+ "x": 0,
+ "y": 294.85715
+ },
+ {
+ "x": 0,
+ "y": 296.85715
+ },
+ {
+ "x": 0,
+ "y": 298.2857
+ },
+ {
+ "x": 0,
+ "y": 299.14285
+ },
+ {
+ "x": 0,
+ "y": 299.7143
+ },
+ {
+ "x": 0,
+ "y": 300
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "type": "not_found"
+ }
+ ]
+ },
+ {
+ "name": "background_alpha",
+ "type": "float",
+ "data_points": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0.9900334,
+ 0.8403853,
+ 0.71002257,
+ 0.5979084,
+ 0.50182605,
+ 0.41945767,
+ 0.34874845,
+ 0.28797746,
+ 0.23573697,
+ 0.19087732,
+ 0.1524564,
+ 0.11970067,
+ 0.091962695,
+ 0.068702936,
+ 0.049464583,
+ 0.033859253,
+ 0.021552086,
+ 0.012255073,
+ 0.005717635,
+ 0.0017191172,
+ 6.711483e-05,
+ 0,
+ {
+ "type": "not_found"
+ }
+ ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java
index 4a5c1bed7b44..038ec406c3d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java
@@ -32,12 +32,14 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -66,7 +68,7 @@ public class AccessibilityButtonModeObserverTest extends SysuiTestCase {
Settings.Secure.ACCESSIBILITY_BUTTON_MODE,
Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, MY_USER_ID);
mAccessibilityButtonModeObserver = new AccessibilityButtonModeObserver(mContext,
- mUserTracker);
+ mUserTracker, Mockito.mock(SecureSettings.class));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java
index a5a7a4a09227..f5649266d0a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java
@@ -31,12 +31,14 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -62,7 +64,7 @@ public class AccessibilityButtonTargetsObserverTest extends SysuiTestCase {
public void setUp() {
when(mUserTracker.getUserId()).thenReturn(MY_USER_ID);
mAccessibilityButtonTargetsObserver = new AccessibilityButtonTargetsObserver(mContext,
- mUserTracker);
+ mUserTracker, Mockito.mock(SecureSettings.class));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java
index ba990efd5162..afed12fb700b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java
@@ -31,12 +31,14 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -62,7 +64,7 @@ public class AccessibilityGestureTargetsObserverTest extends SysuiTestCase {
public void setUp() {
when(mUserTracker.getUserId()).thenReturn(MY_USER_ID);
mAccessibilityGestureTargetsObserver = new AccessibilityGestureTargetsObserver(mContext,
- mUserTracker);
+ mUserTracker, Mockito.mock(SecureSettings.class));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java
index 9222fc2222be..1d88b904668c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java
@@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
import org.junit.Before;
import org.junit.Test;
@@ -72,7 +73,7 @@ public class SecureSettingsContentObserverTest extends SysuiTestCase {
protected FakeSecureSettingsContentObserver(Context context, UserTracker userTracker,
String secureSettingsKey) {
- super(context, userTracker, secureSettingsKey);
+ super(context, userTracker, Mockito.mock(SecureSettings.class), secureSettingsKey);
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
new file mode 100644
index 000000000000..22946c8e6ad0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.composable
+
+import android.app.AlertDialog
+import android.platform.test.annotations.MotionTest
+import android.testing.TestableLooper.RunWithLooper
+import androidx.activity.BackEventCompat
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isFinite
+import androidx.compose.ui.geometry.isUnspecified
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.Scale
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.isElement
+import com.android.compose.animation.scene.testing.lastAlphaForTesting
+import com.android.compose.animation.scene.testing.lastScaleForTesting
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
+import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.motion.createSysUiComposeMotionTestRule
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.shared.model.SceneContainerConfig
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
+import com.android.systemui.scene.ui.composable.Scene
+import com.android.systemui.scene.ui.composable.SceneContainer
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import org.json.JSONObject
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot
+import platform.test.motion.compose.ComposeRecordingSpec
+import platform.test.motion.compose.MotionControl
+import platform.test.motion.compose.feature
+import platform.test.motion.compose.recordMotion
+import platform.test.motion.compose.runTest
+import platform.test.motion.golden.DataPoint
+import platform.test.motion.golden.DataPointType
+import platform.test.motion.golden.DataPointTypes
+import platform.test.motion.golden.FeatureCapture
+import platform.test.motion.golden.UnknownTypeException
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays.Phone
+
+/** MotionTest for the Bouncer Predictive Back animation */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+@EnableSceneContainer
+@MotionTest
+class BouncerPredictiveBackTest : SysuiTestCase() {
+
+ private val deviceSpec = DeviceEmulationSpec(Phone)
+ private val kosmos = testKosmos()
+
+ @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos, deviceSpec)
+ private val androidComposeTestRule =
+ motionTestRule.toolkit.composeContentTestRule as AndroidComposeTestRule<*, *>
+
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val Kosmos.sceneKeys by Fixture { listOf(Scenes.Lockscreen, Scenes.Bouncer) }
+ private val Kosmos.initialSceneKey by Fixture { Scenes.Bouncer }
+ private val Kosmos.sceneContainerConfig by Fixture {
+ val navigationDistances =
+ mapOf(
+ Scenes.Lockscreen to 1,
+ Scenes.Bouncer to 0,
+ )
+ SceneContainerConfig(sceneKeys, initialSceneKey, emptyList(), navigationDistances)
+ }
+
+ private val transitionState by lazy {
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey)
+ )
+ }
+ private val sceneContainerViewModel by lazy {
+ SceneContainerViewModel(
+ sceneInteractor = kosmos.sceneInteractor,
+ falsingInteractor = kosmos.falsingInteractor,
+ powerInteractor = kosmos.powerInteractor,
+ shadeInteractor = kosmos.shadeInteractor,
+ splitEdgeDetector = kosmos.splitEdgeDetector,
+ logger = kosmos.sceneLogger,
+ motionEventHandlerReceiver = {},
+ )
+ .apply { setTransitionState(transitionState) }
+ }
+
+ private val bouncerDialogFactory =
+ object : BouncerDialogFactory {
+ override fun invoke(): AlertDialog {
+ throw AssertionError()
+ }
+ }
+ private val bouncerSceneActionsViewModelFactory =
+ object : BouncerUserActionsViewModel.Factory {
+ override fun create() = BouncerUserActionsViewModel(kosmos.bouncerInteractor)
+ }
+ private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel
+ private val bouncerSceneContentViewModelFactory =
+ object : BouncerSceneContentViewModel.Factory {
+ override fun create() = bouncerSceneContentViewModel
+ }
+ private val bouncerScene =
+ BouncerScene(
+ bouncerSceneActionsViewModelFactory,
+ bouncerSceneContentViewModelFactory,
+ bouncerDialogFactory
+ )
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel
+
+ val startable = kosmos.sceneContainerStartable
+ startable.start()
+ }
+
+ @Test
+ fun bouncerPredictiveBackMotion() =
+ motionTestRule.runTest {
+ val motion =
+ recordMotion(
+ content = { play ->
+ PlatformTheme {
+ BackGestureAnimation(play)
+ SceneContainer(
+ viewModel =
+ rememberViewModel("BouncerPredictiveBackTest") {
+ sceneContainerViewModel
+ },
+ sceneByKey =
+ mapOf(
+ Scenes.Lockscreen to FakeLockscreen(),
+ Scenes.Bouncer to bouncerScene
+ ),
+ initialSceneKey = Scenes.Bouncer,
+ overlayByKey = emptyMap(),
+ dataSourceDelegator = kosmos.sceneDataSourceDelegator
+ )
+ }
+ },
+ ComposeRecordingSpec(
+ MotionControl(
+ delayRecording = {
+ awaitCondition {
+ sceneInteractor.transitionState.value.isTransitioning()
+ }
+ }
+ ) {
+ awaitCondition {
+ sceneInteractor.transitionState.value.isIdle(Scenes.Lockscreen)
+ }
+ }
+ ) {
+ feature(isElement(Bouncer.Elements.Content), elementAlpha, "content_alpha")
+ feature(isElement(Bouncer.Elements.Content), elementScale, "content_scale")
+ feature(
+ isElement(Bouncer.Elements.Content),
+ positionInRoot,
+ "content_offset"
+ )
+ feature(
+ isElement(Bouncer.Elements.Background),
+ elementAlpha,
+ "background_alpha"
+ )
+ }
+ )
+
+ assertThat(motion).timeSeriesMatchesGolden()
+ }
+
+ @Composable
+ private fun BackGestureAnimation(play: Boolean) {
+ val backProgress = remember { Animatable(0f) }
+
+ LaunchedEffect(play) {
+ if (play) {
+ val dispatcher = androidComposeTestRule.activity.onBackPressedDispatcher
+ androidComposeTestRule.runOnUiThread {
+ dispatcher.dispatchOnBackStarted(backEvent())
+ }
+ backProgress.animateTo(
+ targetValue = 1f,
+ animationSpec = tween(durationMillis = 500)
+ ) {
+ androidComposeTestRule.runOnUiThread {
+ dispatcher.dispatchOnBackProgressed(
+ backEvent(progress = backProgress.value)
+ )
+ if (backProgress.value == 1f) {
+ dispatcher.onBackPressed()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun backEvent(progress: Float = 0f): BackEventCompat {
+ return BackEventCompat(
+ touchX = 0f,
+ touchY = 0f,
+ progress = progress,
+ swipeEdge = BackEventCompat.EDGE_LEFT,
+ )
+ }
+
+ private class FakeLockscreen : ExclusiveActivatable(), Scene {
+ override val key: SceneKey = Scenes.Lockscreen
+ override val userActions: Flow<Map<UserAction, UserActionResult>> = flowOf()
+
+ @Composable
+ override fun SceneScope.Content(modifier: Modifier) {
+ Box(modifier = modifier, contentAlignment = Alignment.Center) {
+ Text(text = "Fake Lockscreen")
+ }
+ }
+
+ override suspend fun onActivated() = awaitCancellation()
+ }
+
+ companion object {
+ private val elementAlpha =
+ FeatureCapture<SemanticsNode, Float>("alpha") {
+ DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float)
+ }
+
+ private val elementScale =
+ FeatureCapture<SemanticsNode, Scale>("scale") {
+ DataPoint.of(it.lastScaleForTesting, scale)
+ }
+
+ private val scale: DataPointType<Scale> =
+ DataPointType(
+ "scale",
+ jsonToValue = {
+ when (it) {
+ "unspecified" -> Scale.Unspecified
+ "default" -> Scale.Default
+ "zero" -> Scale.Zero
+ is JSONObject -> {
+ val pivot = it.get("pivot")
+ Scale(
+ scaleX = it.getDouble("x").toFloat(),
+ scaleY = it.getDouble("y").toFloat(),
+ pivot =
+ when (pivot) {
+ "unspecified" -> Offset.Unspecified
+ "infinite" -> Offset.Infinite
+ is JSONObject ->
+ Offset(
+ pivot.getDouble("x").toFloat(),
+ pivot.getDouble("y").toFloat()
+ )
+ else -> throw UnknownTypeException()
+ }
+ )
+ }
+ else -> throw UnknownTypeException()
+ }
+ },
+ valueToJson = {
+ when (it) {
+ Scale.Unspecified -> "unspecified"
+ Scale.Default -> "default"
+ Scale.Zero -> "zero"
+ else -> {
+ JSONObject().apply {
+ put("x", it.scaleX)
+ put("y", it.scaleY)
+ put(
+ "pivot",
+ when {
+ it.pivot.isUnspecified -> "unspecified"
+ !it.pivot.isFinite -> "infinite"
+ else ->
+ JSONObject().apply {
+ put("x", it.pivot.x)
+ put("y", it.pivot.y)
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index c1cf91d6520c..bc0ec2d784f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -22,6 +22,7 @@ import static android.service.quicksettings.TileService.START_ACTIVITY_NEEDS_PEN
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX;
+import static com.android.systemui.Flags.FLAG_QS_QUICK_REBIND_ACTIVE_TILES;
import static com.google.common.truth.Truth.assertThat;
@@ -75,6 +76,8 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
+import com.google.common.truth.Truth;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -95,7 +98,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
- return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX);
+ return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX,
+ FLAG_QS_QUICK_REBIND_ACTIVE_TILES);
}
private final PackageManagerAdapter mMockPackageManagerAdapter =
@@ -154,7 +158,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
}
@After
@@ -169,12 +174,12 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mStateManager.handleDestroy();
}
- private void setPackageEnabled(boolean enabled) throws Exception {
+ private void setPackageEnabledAndActive(boolean enabled, boolean active) throws Exception {
ServiceInfo defaultServiceInfo = null;
if (enabled) {
defaultServiceInfo = new ServiceInfo();
defaultServiceInfo.metaData = new Bundle();
- defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, true);
+ defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, active);
defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_TOGGLEABLE_TILE, true);
}
when(mMockPackageManagerAdapter.getServiceInfo(any(), anyInt(), anyInt()))
@@ -186,6 +191,10 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
.thenReturn(defaultPackageInfo);
}
+ private void setPackageEnabled(boolean enabled) throws Exception {
+ setPackageEnabledAndActive(enabled, true);
+ }
+
private void setPackageInstalledForUser(
boolean installed,
boolean active,
@@ -396,18 +405,125 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
}
@Test
- public void testKillProcess() throws Exception {
+ public void testKillProcessWhenTileServiceIsNotActive() throws Exception {
+ setPackageEnabledAndActive(true, false);
mStateManager.onStartListening();
mStateManager.executeSetBindService(true);
mExecutor.runAllReady();
+ verifyBind(1);
+ verify(mMockTileService, times(1)).onStartListening();
+
mStateManager.onBindingDied(mTileServiceComponentName);
mExecutor.runAllReady();
- mClock.advanceTime(5000);
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+
+ // still 4 seconds left because non active tile service rebind time is 5 seconds
+ Truth.assertThat(mContext.isBound(mTileServiceComponentName)).isFalse();
+
+ mClock.advanceTime(4000); // 5 seconds delay for nonActive service rebinding
+ mExecutor.runAllReady();
+ verifyBind(2);
+ verify(mMockTileService, times(2)).onStartListening();
+ }
+
+ @EnableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+ @Test
+ public void testKillProcessWhenTileServiceIsActive_withRebindFlagOn() throws Exception {
+ mStateManager.onStartListening();
+ mStateManager.executeSetBindService(true);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ verify(mMockTileService, times(1)).onStartListening();
+
+ mStateManager.onBindingDied(mTileServiceComponentName);
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+
+ // Two calls: one for the first bind, one for the restart.
+ verifyBind(2);
+ verify(mMockTileService, times(2)).onStartListening();
+ }
+
+ @DisableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+ @Test
+ public void testKillProcessWhenTileServiceIsActive_withRebindFlagOff() throws Exception {
+ mStateManager.onStartListening();
+ mStateManager.executeSetBindService(true);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ verify(mMockTileService, times(1)).onStartListening();
+
+ mStateManager.onBindingDied(mTileServiceComponentName);
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+ verifyBind(0); // the rebind happens after 4 more seconds
+
+ mClock.advanceTime(4000);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ }
+
+ @EnableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+ @Test
+ public void testKillProcessWhenTileServiceIsActiveTwice_withRebindFlagOn_delaysSecondRebind()
+ throws Exception {
+ mStateManager.onStartListening();
+ mStateManager.executeSetBindService(true);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ verify(mMockTileService, times(1)).onStartListening();
+
+ mStateManager.onBindingDied(mTileServiceComponentName);
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
mExecutor.runAllReady();
// Two calls: one for the first bind, one for the restart.
verifyBind(2);
verify(mMockTileService, times(2)).onStartListening();
+
+ mStateManager.onBindingDied(mTileServiceComponentName);
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+ // because active tile will take 5 seconds to bind the second time, not 1
+ verifyBind(0);
+
+ mClock.advanceTime(4000);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ }
+
+ @DisableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+ @Test
+ public void testKillProcessWhenTileServiceIsActiveTwice_withRebindFlagOff_rebindsFromFirstKill()
+ throws Exception {
+ mStateManager.onStartListening();
+ mStateManager.executeSetBindService(true);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ verify(mMockTileService, times(1)).onStartListening();
+
+ mStateManager.onBindingDied(mTileServiceComponentName); // rebind scheduled for 5 seconds
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+
+ verifyBind(0); // it would bind in 4 more seconds
+
+ mStateManager.onBindingDied(mTileServiceComponentName); // this does not affect the rebind
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+
+ verifyBind(0); // only 2 seconds passed from first kill
+
+ mClock.advanceTime(3000);
+ mExecutor.runAllReady();
+ verifyBind(1); // the rebind scheduled 5 seconds from the first kill should now happen
}
@Test
@@ -510,7 +626,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
manager.executeSetBindService(true);
mExecutor.runAllReady();
@@ -533,7 +650,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
manager.executeSetBindService(true);
mExecutor.runAllReady();
@@ -556,7 +674,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
manager.executeSetBindService(true);
mExecutor.runAllReady();
@@ -581,7 +700,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
manager.executeSetBindService(true);
mExecutor.runAllReady();
@@ -607,7 +727,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
assertThat(manager.isActiveTile()).isTrue();
}
@@ -626,7 +747,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
assertThat(manager.isActiveTile()).isTrue();
}
@@ -644,7 +766,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
assertThat(manager.isToggleableTile()).isTrue();
}
@@ -663,7 +786,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
assertThat(manager.isToggleableTile()).isTrue();
}
@@ -682,7 +806,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
assertThat(manager.isToggleableTile()).isFalse();
assertThat(manager.isActiveTile()).isFalse();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt
deleted file mode 100644
index 5e07aef7d8e9..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt
+++ /dev/null
@@ -1,278 +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.screenshot
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.content.ComponentName
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.drawable.Icon
-import android.net.Uri
-import android.os.UserHandle
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType
-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.whenever
-import java.util.concurrent.CompletableFuture
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNull
-import org.junit.Before
-import org.junit.Test
-
-@SmallTest
-class SaveImageInBackgroundTaskTest : SysuiTestCase() {
- private val imageExporter = mock<ImageExporter>()
- private val smartActions = mock<ScreenshotSmartActions>()
- private val smartActionsProvider = mock<ScreenshotNotificationSmartActionsProvider>()
- private val saveImageData = SaveImageInBackgroundTask.SaveImageInBackgroundData()
- private val testScreenshotId: String = "testScreenshotId"
- private val testBitmap = mock<Bitmap>()
- private val testUser = UserHandle.getUserHandleForUid(0)
- private val testIcon = mock<Icon>()
- private val testImageTime = 1234.toLong()
- private val flags = FakeFeatureFlags()
-
- private val smartActionsUriFuture = mock<CompletableFuture<List<Notification.Action>>>()
- private val smartActionsFuture = mock<CompletableFuture<List<Notification.Action>>>()
-
- private val testUri: Uri = Uri.parse("testUri")
- private val intent =
- Intent(Intent.ACTION_SEND)
- .setComponent(
- ComponentName.unflattenFromString(
- "com.google.android.test/com.google.android.test.TestActivity"
- )
- )
- private val immutablePendingIntent =
- PendingIntent.getBroadcast(
- mContext,
- 0,
- intent,
- PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
- )
- private val mutablePendingIntent =
- PendingIntent.getBroadcast(
- mContext,
- 0,
- intent,
- PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
- )
-
- private val saveImageTask =
- SaveImageInBackgroundTask(
- mContext,
- flags,
- imageExporter,
- smartActions,
- saveImageData,
- smartActionsProvider,
- )
-
- @Before
- fun setup() {
- whenever(
- smartActions.getSmartActionsFuture(
- eq(testScreenshotId),
- any(Uri::class.java),
- eq(testBitmap),
- eq(smartActionsProvider),
- any(ScreenshotSmartActionType::class.java),
- any(Boolean::class.java),
- eq(testUser)
- )
- )
- .thenReturn(smartActionsUriFuture)
- whenever(
- smartActions.getSmartActionsFuture(
- eq(testScreenshotId),
- eq(null),
- eq(testBitmap),
- eq(smartActionsProvider),
- any(ScreenshotSmartActionType::class.java),
- any(Boolean::class.java),
- eq(testUser)
- )
- )
- .thenReturn(smartActionsFuture)
- }
-
- @Test
- fun testQueryQuickShare_noAction() {
- whenever(
- smartActions.getSmartActions(
- eq(testScreenshotId),
- eq(smartActionsFuture),
- any(Int::class.java),
- eq(smartActionsProvider),
- eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
- )
- )
- .thenReturn(ArrayList<Notification.Action>())
-
- val quickShareAction =
- saveImageTask.queryQuickShareAction(testScreenshotId, testBitmap, testUser, testUri)
-
- assertNull(quickShareAction)
- }
-
- @Test
- fun testQueryQuickShare_withActions() {
- val actions = ArrayList<Notification.Action>()
- actions.add(constructAction("Action One", mutablePendingIntent))
- actions.add(constructAction("Action Two", mutablePendingIntent))
- whenever(
- smartActions.getSmartActions(
- eq(testScreenshotId),
- eq(smartActionsUriFuture),
- any(Int::class.java),
- eq(smartActionsProvider),
- eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
- )
- )
- .thenReturn(actions)
-
- val quickShareAction =
- saveImageTask.queryQuickShareAction(testScreenshotId, testBitmap, testUser, testUri)!!
-
- assertEquals("Action One", quickShareAction.title)
- assertEquals(mutablePendingIntent, quickShareAction.actionIntent)
- }
-
- @Test
- fun testCreateQuickShareAction_originalWasNull_returnsNull() {
- val quickShareAction =
- saveImageTask.createQuickShareAction(
- null,
- testScreenshotId,
- testUri,
- testImageTime,
- testBitmap,
- testUser
- )
-
- assertNull(quickShareAction)
- }
-
- @Test
- fun testCreateQuickShareAction_immutableIntentDifferentAction_returnsNull() {
- val actions = ArrayList<Notification.Action>()
- actions.add(constructAction("New Test Action", immutablePendingIntent))
- whenever(
- smartActions.getSmartActions(
- eq(testScreenshotId),
- eq(smartActionsUriFuture),
- any(Int::class.java),
- eq(smartActionsProvider),
- eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
- )
- )
- .thenReturn(actions)
- val origAction = constructAction("Old Test Action", immutablePendingIntent)
-
- val quickShareAction =
- saveImageTask.createQuickShareAction(
- origAction,
- testScreenshotId,
- testUri,
- testImageTime,
- testBitmap,
- testUser,
- )
-
- assertNull(quickShareAction)
- }
-
- @Test
- fun testCreateQuickShareAction_mutableIntent_returnsSafeIntent() {
- val actions = ArrayList<Notification.Action>()
- val action = constructAction("Action One", mutablePendingIntent)
- actions.add(action)
- whenever(
- smartActions.getSmartActions(
- eq(testScreenshotId),
- eq(smartActionsUriFuture),
- any(Int::class.java),
- eq(smartActionsProvider),
- eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
- )
- )
- .thenReturn(actions)
-
- val quickShareAction =
- saveImageTask.createQuickShareAction(
- constructAction("Test Action", mutablePendingIntent),
- testScreenshotId,
- testUri,
- testImageTime,
- testBitmap,
- testUser
- )
- val quickSharePendingIntent =
- quickShareAction.actionIntent.intent.extras!!.getParcelable(
- SmartActionsReceiver.EXTRA_ACTION_INTENT,
- PendingIntent::class.java
- )
-
- assertEquals("Test Action", quickShareAction.title)
- assertEquals(mutablePendingIntent, quickSharePendingIntent)
- }
-
- @Test
- fun testCreateQuickShareAction_immutableIntent_returnsSafeIntent() {
- val actions = ArrayList<Notification.Action>()
- val action = constructAction("Test Action", immutablePendingIntent)
- actions.add(action)
- whenever(
- smartActions.getSmartActions(
- eq(testScreenshotId),
- eq(smartActionsUriFuture),
- any(Int::class.java),
- eq(smartActionsProvider),
- eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
- )
- )
- .thenReturn(actions)
-
- val quickShareAction =
- saveImageTask.createQuickShareAction(
- constructAction("Test Action", immutablePendingIntent),
- testScreenshotId,
- testUri,
- testImageTime,
- testBitmap,
- testUser,
- )!!
-
- assertEquals("Test Action", quickShareAction.title)
- assertEquals(
- immutablePendingIntent,
- quickShareAction.actionIntent.intent.extras!!.getParcelable(
- SmartActionsReceiver.EXTRA_ACTION_INTENT,
- PendingIntent::class.java
- )
- )
- }
-
- private fun constructAction(title: String, intent: PendingIntent): Notification.Action {
- return Notification.Action.Builder(testIcon, title, intent).build()
- }
-}
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 9481e5a52098..e0c4ab737511 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -431,7 +431,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
mFakeKeyguardRepository,
mKeyguardTransitionInteractor,
mPowerInteractor,
- mShadeRepository,
new FakeUserSetupRepository(),
mock(UserSwitcherInteractor.class),
new ShadeInteractorLegacyImpl(
@@ -447,8 +446,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
() -> mLargeScreenHeaderHelper
),
mShadeRepository
- )
- );
+ ),
+ mKosmos.getShadeModeInteractor());
SystemClock systemClock = new FakeSystemClock();
mStatusBarStateController = new StatusBarStateControllerImpl(
mUiEventLogger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index 3f6617b32131..a52f1737117a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -217,7 +217,6 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase {
mKeyguardRepository,
keyguardTransitionInteractor,
powerInteractor,
- mShadeRepository,
new FakeUserSetupRepository(),
mUserSwitcherInteractor,
new ShadeInteractorLegacyImpl(
@@ -232,8 +231,8 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase {
deviceEntryUdfpsInteractor,
() -> mLargeScreenHeaderHelper),
mShadeRepository
- )
- );
+ ),
+ mKosmos.getShadeModeInteractor());
mActiveNotificationsInteractor = new ActiveNotificationsInteractor(
new ActiveNotificationListRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 01a3d36a05ec..1d74331e429b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1112,9 +1112,11 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
public void testShowBouncerOrKeyguard_showsKeyguardIfShowBouncerReturnsFalse() {
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
+ // Returning false means unable to show the bouncer
when(mPrimaryBouncerInteractor.show(true)).thenReturn(false);
when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
.thenReturn(KeyguardState.LOCKSCREEN);
+ mStatusBarKeyguardViewManager.onStartedWakingUp();
reset(mCentralSurfaces);
// Advance past reattempts
@@ -1127,6 +1129,23 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
@Test
@DisableSceneContainer
+ @EnableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART)
+ public void testShowBouncerOrKeyguard_showsKeyguardIfSleeping() {
+ when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
+ .thenReturn(KeyguardState.LOCKSCREEN);
+ mStatusBarKeyguardViewManager.onStartedGoingToSleep();
+
+ reset(mCentralSurfaces);
+ reset(mPrimaryBouncerInteractor);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(
+ /* hideBouncerWhenShowing= */true, false);
+ verify(mCentralSurfaces).showKeyguard();
+ verify(mPrimaryBouncerInteractor).hide();
+ }
+
+
+ @Test
+ @DisableSceneContainer
public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
boolean isFalsingReset = false;
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt
index 2a0e764279d6..a6394631d236 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt
@@ -16,25 +16,31 @@
package com.android.systemui.accessibility.data.repository
+import com.android.systemui.accessibility.data.model.CaptioningModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeCaptioningRepository : CaptioningRepository {
- private val mutableIsSystemAudioCaptioningEnabled = MutableStateFlow(false)
- override val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
- get() = mutableIsSystemAudioCaptioningEnabled.asStateFlow()
-
- private val mutableIsSystemAudioCaptioningUiEnabled = MutableStateFlow(false)
- override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
- get() = mutableIsSystemAudioCaptioningUiEnabled.asStateFlow()
+ private val mutableCaptioningModel = MutableStateFlow<CaptioningModel?>(null)
+ override val captioningModel: StateFlow<CaptioningModel?> = mutableCaptioningModel.asStateFlow()
override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) {
- mutableIsSystemAudioCaptioningEnabled.value = isEnabled
+ mutableCaptioningModel.value =
+ CaptioningModel(
+ isSystemAudioCaptioningEnabled = isEnabled,
+ isSystemAudioCaptioningUiEnabled =
+ mutableCaptioningModel.value?.isSystemAudioCaptioningUiEnabled == true,
+ )
}
- fun setIsSystemAudioCaptioningUiEnabled(isSystemAudioCaptioningUiEnabled: Boolean) {
- mutableIsSystemAudioCaptioningUiEnabled.value = isSystemAudioCaptioningUiEnabled
+ fun setIsSystemAudioCaptioningUiEnabled(isEnabled: Boolean) {
+ mutableCaptioningModel.value =
+ CaptioningModel(
+ isSystemAudioCaptioningEnabled =
+ mutableCaptioningModel.value?.isSystemAudioCaptioningEnabled == true,
+ isSystemAudioCaptioningUiEnabled = isEnabled,
+ )
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 457bd284ea8d..c60305e85b22 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -61,6 +61,7 @@ import com.android.systemui.scene.shared.model.sceneDataSource
import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.shade.shadeController
import com.android.systemui.shade.ui.viewmodel.notificationShadeWindowModel
import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
@@ -156,4 +157,5 @@ class KosmosJavaAdapter() {
val scrimStartable by lazy { kosmos.scrimStartable }
val sceneContainerOcclusionInteractor by lazy { kosmos.sceneContainerOcclusionInteractor }
val msdlPlayer by lazy { kosmos.fakeMSDLPlayer }
+ val shadeModeInteractor by lazy { kosmos.shadeModeInteractor }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
index a0fc76b3d7de..4978558ff8a2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
@@ -24,6 +24,7 @@ import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.fakeSystemClock
val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by
Kosmos.Fixture {
@@ -39,6 +40,7 @@ val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by
activityManager,
mock(),
fakeExecutor,
+ fakeSystemClock,
)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 55f3ed7062aa..874463819c73 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -12,6 +12,8 @@ import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.FakeOverlay
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import kotlinx.coroutines.flow.MutableStateFlow
var Kosmos.sceneKeys by Fixture {
@@ -70,6 +72,8 @@ val Kosmos.sceneContainerViewModel by Fixture {
sceneInteractor = sceneInteractor,
falsingInteractor = falsingInteractor,
powerInteractor = powerInteractor,
+ shadeInteractor = shadeInteractor,
+ splitEdgeDetector = splitEdgeDetector,
motionEventHandlerReceiver = {},
logger = sceneLogger
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
index ae33aead67a7..d17b5750b937 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
@@ -24,7 +24,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.scene.shared.model.SceneFamilies
-import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import kotlinx.coroutines.ExperimentalCoroutinesApi
val Kosmos.sceneFamilyResolvers: Map<SceneKey, SceneResolver>
@@ -48,7 +48,7 @@ val Kosmos.notifShadeSceneFamilyResolver by
Kosmos.Fixture {
NotifShadeSceneFamilyResolver(
applicationScope = applicationCoroutineScope,
- shadeInteractor = shadeInteractor,
+ shadeModeInteractor = shadeModeInteractor,
)
}
@@ -56,6 +56,6 @@ val Kosmos.quickSettingsSceneFamilyResolver by
Kosmos.Fixture {
QuickSettingsSceneFamilyResolver(
applicationScope = applicationCoroutineScope,
- shadeInteractor = shadeInteractor,
+ shadeModeInteractor = shadeModeInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
new file mode 100644
index 000000000000..e0b529261c4d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.ui.unit.dp
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+
+var Kosmos.splitEdgeDetector: SplitEdgeDetector by
+ Kosmos.Fixture {
+ SplitEdgeDetector(
+ topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+ edgeSize = 40.dp,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
index 54208b9cdaef..04d930c72792 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
@@ -53,7 +53,7 @@ val Kosmos.shadeInteractorLegacyImpl by
scope = applicationCoroutineScope,
keyguardRepository = keyguardRepository,
sharedNotificationContainerInteractor = sharedNotificationContainerInteractor,
- repository = shadeRepository
+ repository = shadeRepository,
)
}
var Kosmos.shadeInteractor: ShadeInteractor by Kosmos.Fixture { shadeInteractorImpl }
@@ -70,6 +70,6 @@ val Kosmos.shadeInteractorImpl by
userSetupRepository = userSetupRepository,
userSwitcherInteractor = userSwitcherInteractor,
baseShadeInteractor = baseShadeInteractor,
- shadeRepository = shadeRepository,
+ shadeModeInteractor = shadeModeInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt
new file mode 100644
index 000000000000..7892e962d63d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.shade.data.repository.shadeRepository
+
+val Kosmos.shadeModeInteractor by Fixture {
+ ShadeModeInteractorImpl(
+ applicationScope = applicationCoroutineScope,
+ repository = shadeRepository,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt
new file mode 100644
index 000000000000..78763f97adc3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.user.utils
+
+import android.os.UserHandle
+
+class FakeUserScopedService<T>(private val defaultImplementation: T) : UserScopedService<T> {
+
+ private val implementations = mutableMapOf<UserHandle, T>()
+
+ fun addImplementation(user: UserHandle, implementation: T) {
+ implementations[user] = implementation
+ }
+
+ fun removeImplementation(user: UserHandle): T? = implementations.remove(user)
+
+ override fun forUser(user: UserHandle): T =
+ implementations.getOrDefault(user, defaultImplementation)
+}
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index 26b0f617d971..136738fcb343 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -22,6 +22,7 @@ import android.content.pm.PackageManager;
import android.graphics.GraphicBuffer;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
+import android.hardware.SyncFence;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraExtensionCharacteristics;
@@ -2525,6 +2526,19 @@ public class CameraExtensionsProxyService extends Service {
}
@Override
+ public SyncFence getFence() {
+ if (mParcelImage.fence != null) {
+ try {
+ return SyncFence.create(mParcelImage.fence.dup());
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to parcel buffer fence!");
+ }
+ }
+
+ return SyncFence.createEmpty();
+ }
+
+ @Override
protected final void finalize() throws Throwable {
try {
close();
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 333fe4c8147f..eebe5e9fc054 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -94,6 +94,9 @@ java_library {
libs: [
"ravenwood-runtime-common-ravenwood",
],
+ static_libs: [
+ "framework-annotations-lib", // should it be "libs" instead?
+ ],
visibility: ["//visibility:private"],
}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java
new file mode 100644
index 000000000000..f9794ad5941e
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.bivalenttest.listenertests;
+
+import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood;
+
+import org.junit.AfterClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+/**
+ * Test that throws from @AfterClass.
+ *
+ * Tradefed would ignore it, so instead RavenwoodAwareTestRunner would detect it and kill
+ * the self (test) process.
+ *
+ * Unfortunately, this behavior can't easily be tested from within this class, so for now
+ * it's only used for a manual test, which you can run by removing the @Ignore.
+ *
+ * TODO(b/364948126) Improve the tests and automate it.
+ */
+@Ignore
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodAfterClassFailureTest {
+ public RavenwoodAfterClassFailureTest(String param) {
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ if (!isOnRavenwood()) return; // Don't do anything on real device.
+
+ throw new RuntimeException("FAILURE");
+ }
+
+ @Parameters
+ public static List<String> getParams() {
+ var params = new ArrayList<String>();
+ params.add("foo");
+ params.add("bar");
+ return params;
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java
new file mode 100644
index 000000000000..61fb06865545
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.bivalenttest.listenertests;
+
+import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood;
+
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+/**
+ * Test that fails in assumption in @BeforeClass.
+ *
+ * This is only used for manual tests. Make sure `atest` shows 4 test results with
+ * "ASSUMPTION_FAILED".
+ *
+ * TODO(b/364948126) Improve the tests and automate it.
+ */
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodBeforeClassAssumptionFailureTest {
+ public RavenwoodBeforeClassAssumptionFailureTest(String param) {
+ }
+
+ @BeforeClass
+ public static void beforeClass() {
+ if (!isOnRavenwood()) return; // Don't do anything on real device.
+
+ Assume.assumeTrue(false);
+ }
+
+ @Parameters
+ public static List<String> getParams() {
+ var params = new ArrayList<String>();
+ params.add("foo");
+ params.add("bar");
+ return params;
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java
new file mode 100644
index 000000000000..626ce8198eeb
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.bivalenttest.listenertests;
+
+import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood;
+
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+/**
+ * Test that fails throws from @BeforeClass.
+ *
+ * This is only used for manual tests. Make sure `atest` shows 4 test results with
+ * a "FAILURE" runtime exception.
+ *
+ * In order to run the test, you'll need to remove the @Ignore.
+ *
+ * TODO(b/364948126) Improve the tests and automate it.
+ */
+@Ignore
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodBeforeClassFailureTest {
+ public static final String TAG = "RavenwoodBeforeClassFailureTest";
+
+ public RavenwoodBeforeClassFailureTest(String param) {
+ }
+
+ @BeforeClass
+ public static void beforeClass() {
+ if (!isOnRavenwood()) return; // Don't do anything on real device.
+
+ throw new RuntimeException("FAILURE");
+ }
+
+ @Parameters
+ public static List<String> getParams() {
+ var params = new ArrayList<String>();
+ params.add("foo");
+ params.add("bar");
+ return params;
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java
new file mode 100644
index 000000000000..dc949c466110
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.bivalenttest.listenertests;
+
+import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood;
+
+import static org.junit.Assume.assumeTrue;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+/**
+ * Test that fails in assumption from a class rule.
+ *
+ * This is only used for manual tests. Make sure `atest` shows 4 test results with
+ * "ASSUMPTION_FAILED".
+ *
+ * TODO(b/364948126) Improve the tests and automate it.
+ */
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodClassRuleAssumptionFailureTest {
+ public static final String TAG = "RavenwoodClassRuleFailureTest";
+
+ @ClassRule
+ public static final TestRule sClassRule = new TestRule() {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ if (!isOnRavenwood()) {
+ return base; // Just run the test as-is on a real device.
+ }
+
+ assumeTrue(false);
+ return null; // unreachable
+ }
+ };
+
+ public RavenwoodClassRuleAssumptionFailureTest(String param) {
+ }
+
+ @Parameters
+ public static List<String> getParams() {
+ var params = new ArrayList<String>();
+ params.add("foo");
+ params.add("bar");
+ return params;
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java
new file mode 100644
index 000000000000..9996bec41525
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.bivalenttest.listenertests;
+
+import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood;
+
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+/**
+ * Test that fails throws from a class rule.
+ *
+ * This is only used for manual tests. Make sure `atest` shows 4 test results with
+ * a "FAILURE" runtime exception.
+ *
+ * In order to run the test, you'll need to remove the @Ignore.
+ *
+ * TODO(b/364948126) Improve the tests and automate it.
+ */
+@Ignore
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodClassRuleFailureTest {
+ public static final String TAG = "RavenwoodClassRuleFailureTest";
+
+ @ClassRule
+ public static final TestRule sClassRule = new TestRule() {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ if (!isOnRavenwood()) {
+ return base; // Just run the test as-is on a real device.
+ }
+
+ throw new RuntimeException("FAILURE");
+ }
+ };
+
+ public RavenwoodClassRuleFailureTest(String param) {
+ }
+
+ @Parameters
+ public static List<String> getParams() {
+ var params = new ArrayList<String>();
+ params.add("foo");
+ params.add("bar");
+ return params;
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
index 1d182da5e7fd..6d21e440e911 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
@@ -34,6 +34,8 @@ import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runners.model.TestClass;
+import java.util.Stack;
+
/**
* Provide hook points created by {@link RavenwoodAwareTestRunner}.
*/
@@ -44,6 +46,11 @@ public class RavenwoodAwareTestRunnerHook {
}
private static RavenwoodTestStats sStats; // lazy initialization.
+
+ // Keep track of the current class description.
+
+ // Test classes can be nested because of "Suite", so we need a stack to keep track.
+ private static final Stack<Description> sClassDescriptions = new Stack<>();
private static Description sCurrentClassDescription;
private static RavenwoodTestStats getStats() {
@@ -108,14 +115,15 @@ public class RavenwoodAwareTestRunnerHook {
Scope scope, Order order) {
Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order);
- if (scope == Scope.Class && order == Order.First) {
+ if (scope == Scope.Class && order == Order.Outer) {
// Keep track of the current class.
sCurrentClassDescription = description;
+ sClassDescriptions.push(description);
}
// Class-level annotations are checked by the runner already, so we only check
// method-level annotations here.
- if (scope == Scope.Instance && order == Order.First) {
+ if (scope == Scope.Instance && order == Order.Outer) {
if (!RavenwoodEnablementChecker.shouldEnableOnRavenwood(
description, true)) {
getStats().onTestFinished(sCurrentClassDescription, description, Result.Skipped);
@@ -134,17 +142,20 @@ public class RavenwoodAwareTestRunnerHook {
Scope scope, Order order, Throwable th) {
Log.v(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th);
- if (scope == Scope.Instance && order == Order.First) {
+ if (scope == Scope.Instance && order == Order.Outer) {
getStats().onTestFinished(sCurrentClassDescription, description,
th == null ? Result.Passed : Result.Failed);
- } else if (scope == Scope.Class && order == Order.Last) {
+ } else if (scope == Scope.Class && order == Order.Outer) {
getStats().onClassFinished(sCurrentClassDescription);
+ sClassDescriptions.pop();
+ sCurrentClassDescription =
+ sClassDescriptions.size() == 0 ? null : sClassDescriptions.peek();
}
// If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error.
if (RavenwoodRule.private$ravenwood().isRunningDisabledTests()
- && scope == Scope.Instance && order == Order.First) {
+ && scope == Scope.Instance && order == Order.Outer) {
boolean isTestEnabled = RavenwoodEnablementChecker.shouldEnableOnRavenwood(
description, false);
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
index 631f68ff1dec..3ffabefb7681 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
@@ -127,7 +127,11 @@ public class RavenwoodTestStats {
int passed = 0;
int skipped = 0;
int failed = 0;
- for (var e : mStats.get(classDescription).values()) {
+ var stats = mStats.get(classDescription);
+ if (stats == null) {
+ return;
+ }
+ for (var e : stats.values()) {
switch (e) {
case Passed: passed++; break;
case Skipped: skipped++; break;
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
index bfde9cb7099e..dffb263e77cb 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
@@ -15,20 +15,25 @@
*/
package android.platform.test.ravenwood;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicVoidMethod;
import static com.android.ravenwood.common.RavenwoodCommonUtils.isOnRavenwood;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.util.Log;
import com.android.ravenwood.common.SneakyThrow;
import org.junit.Assume;
+import org.junit.AssumptionViolatedException;
import org.junit.internal.builders.AllDefaultPossibilitiesBuilder;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
+import org.junit.runner.Result;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.Filterable;
@@ -39,8 +44,11 @@ import org.junit.runner.manipulation.Orderer;
import org.junit.runner.manipulation.Sortable;
import org.junit.runner.manipulation.Sorter;
import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
+import org.junit.runner.notification.StoppedByUserException;
import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.RunnerBuilder;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;
@@ -51,6 +59,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Stack;
/**
* A test runner used for Ravenwood.
@@ -61,7 +71,7 @@ import java.lang.reflect.InvocationTargetException;
* the inner runner gets a chance to run. This can be used to initialize stuff used by the
* inner runner.
* - Add hook points, which are handed by RavenwoodAwareTestRunnerHook, with help from
- * the four test rules such as {@link #sImplicitClassMinRule}, which are also injected by
+ * the four test rules such as {@link #sImplicitClassOuterRule}, which are also injected by
* the ravenizer tool.
*
* We use this runner to:
@@ -102,28 +112,50 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde
/** Order of a hook. */
public enum Order {
- First,
- Last,
+ Outer,
+ Inner,
}
// The following four rule instances will be injected to tests by the Ravenizer tool.
+ private static class RavenwoodClassOuterRule implements TestRule {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Outer);
+ }
+ }
- public static final TestRule sImplicitClassMinRule = (base, description) ->
- getCurrentRunner().updateStatement(base, description, Scope.Class, Order.First);
+ private static class RavenwoodClassInnerRule implements TestRule {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Inner);
+ }
+ }
- public static final TestRule sImplicitClassMaxRule = (base, description) ->
- getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Last);
+ private static class RavenwoodInstanceOuterRule implements TestRule {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return getCurrentRunner().updateStatement(
+ base, description, Scope.Instance, Order.Outer);
+ }
+ }
- public static final TestRule sImplicitInstMinRule = (base, description) ->
- getCurrentRunner().updateStatement(base, description, Scope.Instance, Order.First);
+ private static class RavenwoodInstanceInnerRule implements TestRule {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return getCurrentRunner().updateStatement(
+ base, description, Scope.Instance, Order.Inner);
+ }
+ }
- public static final TestRule sImplicitInstMaxRule = (base, description) ->
- getCurrentRunner().updateStatement(base, description, Scope.Instance, Order.Last);
+ public static final TestRule sImplicitClassOuterRule = new RavenwoodClassOuterRule();
+ public static final TestRule sImplicitClassInnerRule = new RavenwoodClassInnerRule();
+ public static final TestRule sImplicitInstOuterRule = new RavenwoodInstanceOuterRule();
+ public static final TestRule sImplicitInstInnerRule = new RavenwoodInstanceOuterRule();
- public static final String IMPLICIT_CLASS_MIN_RULE_NAME = "sImplicitClassMinRule";
- public static final String IMPLICIT_CLASS_MAX_RULE_NAME = "sImplicitClassMaxRule";
- public static final String IMPLICIT_INST_MIN_RULE_NAME = "sImplicitInstMinRule";
- public static final String IMPLICIT_INST_MAX_RULE_NAME = "sImplicitInstMaxRule";
+ public static final String IMPLICIT_CLASS_OUTER_RULE_NAME = "sImplicitClassOuterRule";
+ public static final String IMPLICIT_CLASS_INNER_RULE_NAME = "sImplicitClassInnerRule";
+ public static final String IMPLICIT_INST_OUTER_RULE_NAME = "sImplicitInstOuterRule";
+ public static final String IMPLICIT_INST_INNER_RULE_NAME = "sImplicitInstInnerRule";
/** Keeps track of the runner on the current thread. */
private static final ThreadLocal<RavenwoodAwareTestRunner> sCurrentRunner = new ThreadLocal<>();
@@ -157,6 +189,8 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde
try {
mTestClass = new TestClass(testClass);
+ Log.v(TAG, "RavenwoodAwareTestRunner starting for " + testClass.getCanonicalName());
+
onRunnerInitializing();
/*
@@ -261,20 +295,27 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde
}
@Override
- public void run(RunNotifier notifier) {
+ public void run(RunNotifier realNotifier) {
+ final RunNotifier notifier = new RavenwoodRunNotifier(realNotifier);
+
if (mRealRunner instanceof ClassSkippingTestRunner) {
mRealRunner.run(notifier);
RavenwoodAwareTestRunnerHook.onClassSkipped(getDescription());
return;
}
+ Log.v(TAG, "Starting " + mTestClass.getJavaClass().getCanonicalName());
+ if (RAVENWOOD_VERBOSE_LOGGING) {
+ dumpDescription(getDescription());
+ }
+
if (maybeReportExceptionFromConstructor(notifier)) {
return;
}
sCurrentRunner.set(this);
try {
- runWithHooks(getDescription(), Scope.Runner, Order.First,
+ runWithHooks(getDescription(), Scope.Runner, Order.Outer,
() -> mRealRunner.run(notifier));
} finally {
sCurrentRunner.remove();
@@ -399,4 +440,217 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde
}
}
}
+
+ private void dumpDescription(Description desc) {
+ dumpDescription(desc, "[TestDescription]=", " ");
+ }
+
+ private void dumpDescription(Description desc, String header, String indent) {
+ Log.v(TAG, indent + header + desc);
+
+ var children = desc.getChildren();
+ var childrenIndent = " " + indent;
+ for (int i = 0; i < children.size(); i++) {
+ dumpDescription(children.get(i), "#" + i + ": ", childrenIndent);
+ }
+ }
+
+ /**
+ * A run notifier that wraps another notifier and provides the following features:
+ * - Handle a failure that happened before testStarted and testEnded (typically that means
+ * it's from @BeforeClass or @AfterClass, or a @ClassRule) and deliver it as if
+ * individual tests in the class reported it. This is for b/364395552.
+ *
+ * - Logging.
+ */
+ private class RavenwoodRunNotifier extends RunNotifier {
+ private final RunNotifier mRealNotifier;
+
+ private final Stack<Description> mSuiteStack = new Stack<>();
+ private Description mCurrentSuite = null;
+ private final ArrayList<Throwable> mOutOfTestFailures = new ArrayList<>();
+
+ private boolean mBeforeTest = true;
+ private boolean mAfterTest = false;
+
+ private RavenwoodRunNotifier(RunNotifier realNotifier) {
+ mRealNotifier = realNotifier;
+ }
+
+ private boolean isInTest() {
+ return !mBeforeTest && !mAfterTest;
+ }
+
+ @Override
+ public void addListener(RunListener listener) {
+ mRealNotifier.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(RunListener listener) {
+ mRealNotifier.removeListener(listener);
+ }
+
+ @Override
+ public void addFirstListener(RunListener listener) {
+ mRealNotifier.addFirstListener(listener);
+ }
+
+ @Override
+ public void fireTestRunStarted(Description description) {
+ Log.i(TAG, "testRunStarted: " + description);
+ mRealNotifier.fireTestRunStarted(description);
+ }
+
+ @Override
+ public void fireTestRunFinished(Result result) {
+ Log.i(TAG, "testRunFinished: "
+ + result.getRunCount() + ","
+ + result.getFailureCount() + ","
+ + result.getAssumptionFailureCount() + ","
+ + result.getIgnoreCount());
+ mRealNotifier.fireTestRunFinished(result);
+ }
+
+ @Override
+ public void fireTestSuiteStarted(Description description) {
+ Log.i(TAG, "testSuiteStarted: " + description);
+ mRealNotifier.fireTestSuiteStarted(description);
+
+ mBeforeTest = true;
+ mAfterTest = false;
+
+ // Keep track of the current suite, needed if the outer test is a Suite,
+ // in which case its children are test classes. (not test methods)
+ mCurrentSuite = description;
+ mSuiteStack.push(description);
+
+ mOutOfTestFailures.clear();
+ }
+
+ @Override
+ public void fireTestSuiteFinished(Description description) {
+ Log.i(TAG, "testSuiteFinished: " + description);
+ mRealNotifier.fireTestSuiteFinished(description);
+
+ maybeHandleOutOfTestFailures();
+
+ mBeforeTest = true;
+ mAfterTest = false;
+
+ // Restore the upper suite.
+ mSuiteStack.pop();
+ mCurrentSuite = mSuiteStack.size() == 0 ? null : mSuiteStack.peek();
+ }
+
+ @Override
+ public void fireTestStarted(Description description) throws StoppedByUserException {
+ Log.i(TAG, "testStarted: " + description);
+ mRealNotifier.fireTestStarted(description);
+
+ mAfterTest = false;
+ mBeforeTest = false;
+ }
+
+ @Override
+ public void fireTestFailure(Failure failure) {
+ Log.i(TAG, "testFailure: " + failure);
+
+ if (isInTest()) {
+ mRealNotifier.fireTestFailure(failure);
+ } else {
+ mOutOfTestFailures.add(failure.getException());
+ }
+ }
+
+ @Override
+ public void fireTestAssumptionFailed(Failure failure) {
+ Log.i(TAG, "testAssumptionFailed: " + failure);
+
+ if (isInTest()) {
+ mRealNotifier.fireTestAssumptionFailed(failure);
+ } else {
+ mOutOfTestFailures.add(failure.getException());
+ }
+ }
+
+ @Override
+ public void fireTestIgnored(Description description) {
+ Log.i(TAG, "testIgnored: " + description);
+ mRealNotifier.fireTestIgnored(description);
+ }
+
+ @Override
+ public void fireTestFinished(Description description) {
+ Log.i(TAG, "testFinished: " + description);
+ mRealNotifier.fireTestFinished(description);
+
+ mAfterTest = true;
+ }
+
+ @Override
+ public void pleaseStop() {
+ Log.w(TAG, "pleaseStop:");
+ mRealNotifier.pleaseStop();
+ }
+
+ /**
+ * At the end of each Suite, we handle failures happened out of test methods.
+ * (typically in @BeforeClass or @AfterClasses)
+ *
+ * This is to work around b/364395552.
+ */
+ private boolean maybeHandleOutOfTestFailures() {
+ if (mOutOfTestFailures.size() == 0) {
+ return false;
+ }
+ Throwable th;
+ if (mOutOfTestFailures.size() == 1) {
+ th = mOutOfTestFailures.get(0);
+ } else {
+ th = new MultipleFailureException(mOutOfTestFailures);
+ }
+ if (mBeforeTest) {
+ reportBeforeTestFailure(mCurrentSuite, th);
+ return true;
+ }
+ if (mAfterTest) {
+ // Unfortunately, there's no good way to report it, so kill the own process.
+ onCriticalError(
+ "Failures detected in @AfterClass, which would be swalloed by tradefed",
+ th);
+ return true; // unreachable
+ }
+ return false;
+ }
+
+ private void reportBeforeTestFailure(Description suiteDesc, Throwable th) {
+ // If a failure happens befere running any tests, we'll need to pretend
+ // as if each test in the suite reported the failure, to work around b/364395552.
+ for (var child : suiteDesc.getChildren()) {
+ if (child.isSuite()) {
+ // If the chiil is still a "parent" -- a test class or a test suite
+ // -- propagate to its children.
+ mRealNotifier.fireTestSuiteStarted(child);
+ reportBeforeTestFailure(child, th);
+ mRealNotifier.fireTestSuiteFinished(child);
+ } else {
+ mRealNotifier.fireTestStarted(child);
+ Failure f = new Failure(child, th);
+ if (th instanceof AssumptionViolatedException) {
+ mRealNotifier.fireTestAssumptionFailed(f);
+ } else {
+ mRealNotifier.fireTestFailure(f);
+ }
+ mRealNotifier.fireTestFinished(child);
+ }
+ }
+ }
+ }
+
+ private void onCriticalError(@NonNull String message, @Nullable Throwable th) {
+ Log.e(TAG, "Critical error! Ravenwood cannot continue. Killing self process: "
+ + message, th);
+ System.exit(1);
+ }
}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index 7b5bc5aeb7b6..875ce71149cd 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -15,12 +15,17 @@
*/
package com.android.ravenwood.common;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
import com.android.ravenwood.common.divergence.RavenwoodDivergence;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
@@ -33,6 +38,14 @@ public class RavenwoodCommonUtils {
private static final Object sLock = new Object();
+ /**
+ * If set to "1", we enable the verbose logging.
+ *
+ * (See also InitLogging() in http://ac/system/libbase/logging.cpp)
+ */
+ public static final boolean RAVENWOOD_VERBOSE_LOGGING = "1".equals(System.getenv(
+ "RAVENWOOD_VERBOSE"));
+
/** Name of `libravenwood_runtime` */
private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
@@ -265,4 +278,12 @@ public class RavenwoodCommonUtils {
method.getDeclaringClass().getName(), method.getName(),
(isStatic ? "static " : "")));
}
+
+ @NonNull
+ public static String getStackTraceString(@Nullable Throwable th) {
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter writer = new PrintWriter(stringWriter);
+ th.printStackTrace(writer);
+ return stringWriter.toString();
+ }
}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
index 97e99e50ea75..790bb1c2373b 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
@@ -15,6 +15,8 @@
*/
package com.android.platform.test.ravenwood.runtimehelper;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
+
import android.system.ErrnoException;
import android.system.Os;
@@ -40,14 +42,6 @@ public class ClassLoadHook {
private static final boolean SKIP_LOADING_LIBANDROID = "1".equals(System.getenv(
"RAVENWOOD_SKIP_LOADING_LIBANDROID"));
- /**
- * If set to 1, and if $ANDROID_LOG_TAGS isn't set, we enable the verbose logging.
- *
- * (See also InitLogging() in http://ac/system/libbase/logging.cpp)
- */
- private static final boolean RAVENWOOD_VERBOSE_LOGGING = "1".equals(System.getenv(
- "RAVENWOOD_VERBOSE"));
-
public static final String CORE_NATIVE_CLASSES = "core_native_classes";
public static final String ICU_DATA_PATH = "icu.data.path";
public static final String KEYBOARD_PATHS = "keyboard_paths";
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
index eaef2cf6a956..bd9d96d81604 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
@@ -302,7 +302,7 @@ class RunnerRewritingAdapter private constructor(
override fun visitCode() {
visitFieldInsn(Opcodes.GETSTATIC,
ravenwoodTestRunnerType.internlName,
- RavenwoodAwareTestRunner.IMPLICIT_CLASS_MIN_RULE_NAME,
+ RavenwoodAwareTestRunner.IMPLICIT_CLASS_OUTER_RULE_NAME,
testRuleType.desc
)
visitFieldInsn(Opcodes.PUTSTATIC,
@@ -313,7 +313,7 @@ class RunnerRewritingAdapter private constructor(
visitFieldInsn(Opcodes.GETSTATIC,
ravenwoodTestRunnerType.internlName,
- RavenwoodAwareTestRunner.IMPLICIT_CLASS_MAX_RULE_NAME,
+ RavenwoodAwareTestRunner.IMPLICIT_CLASS_INNER_RULE_NAME,
testRuleType.desc
)
visitFieldInsn(Opcodes.PUTSTATIC,
@@ -361,7 +361,7 @@ class RunnerRewritingAdapter private constructor(
visitVarInsn(ALOAD, 0)
visitFieldInsn(Opcodes.GETSTATIC,
ravenwoodTestRunnerType.internlName,
- RavenwoodAwareTestRunner.IMPLICIT_INST_MIN_RULE_NAME,
+ RavenwoodAwareTestRunner.IMPLICIT_INST_OUTER_RULE_NAME,
testRuleType.desc
)
visitFieldInsn(Opcodes.PUTFIELD,
@@ -373,7 +373,7 @@ class RunnerRewritingAdapter private constructor(
visitVarInsn(ALOAD, 0)
visitFieldInsn(Opcodes.GETSTATIC,
ravenwoodTestRunnerType.internlName,
- RavenwoodAwareTestRunner.IMPLICIT_INST_MAX_RULE_NAME,
+ RavenwoodAwareTestRunner.IMPLICIT_INST_INNER_RULE_NAME,
testRuleType.desc
)
visitFieldInsn(Opcodes.PUTFIELD,
diff --git a/services/appfunctions/TEST_MAPPING b/services/appfunctions/TEST_MAPPING
new file mode 100644
index 000000000000..c7f5eeef8fa0
--- /dev/null
+++ b/services/appfunctions/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "postsubmit": [
+ {
+ "name": "FrameworksAppFunctionsTests"
+ }
+ ]
+} \ No newline at end of file
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
index eba628dc1fba..094723814e17 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
@@ -16,15 +16,15 @@
package com.android.server.appfunctions;
-import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
-
-import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.app.appsearch.AppSearchBatchResult;
import android.app.appsearch.AppSearchManager;
import android.app.appsearch.AppSearchManager.SearchContext;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.BatchResultCallback;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByDocumentIdRequest;
import android.app.appsearch.GetSchemaResponse;
import android.app.appsearch.PutDocumentsRequest;
import android.app.appsearch.SearchResult;
@@ -42,10 +42,7 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
-/**
- * A future API wrapper of {@link AppSearchSession} APIs.
- */
-@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
+/** A future API wrapper of {@link AppSearchSession} APIs. */
public class FutureAppSearchSession implements Closeable {
private static final String TAG = FutureAppSearchSession.class.getSimpleName();
private final Executor mExecutor;
@@ -67,14 +64,14 @@ public class FutureAppSearchSession implements Closeable {
/** Converts a failed app search result codes into an exception. */
@NonNull
- private static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) {
+ public static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) {
return switch (appSearchResult.getResultCode()) {
- case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException(
- appSearchResult.getErrorMessage());
- case AppSearchResult.RESULT_IO_ERROR -> new IOException(
- appSearchResult.getErrorMessage());
- case AppSearchResult.RESULT_SECURITY_ERROR -> new SecurityException(
- appSearchResult.getErrorMessage());
+ case AppSearchResult.RESULT_INVALID_ARGUMENT ->
+ new IllegalArgumentException(appSearchResult.getErrorMessage());
+ case AppSearchResult.RESULT_IO_ERROR ->
+ new IOException(appSearchResult.getErrorMessage());
+ case AppSearchResult.RESULT_SECURITY_ERROR ->
+ new SecurityException(appSearchResult.getErrorMessage());
default -> new IllegalStateException(appSearchResult.getErrorMessage());
};
}
@@ -137,14 +134,16 @@ public class FutureAppSearchSession implements Closeable {
/** Indexes documents into the AppSearchSession database. */
public AndroidFuture<AppSearchBatchResult<String, Void>> put(
@NonNull PutDocumentsRequest putDocumentsRequest) {
- return getSessionAsync().thenCompose(
- session -> {
- AndroidFuture<AppSearchBatchResult<String, Void>> batchResultFuture =
- new AndroidFuture<>();
+ return getSessionAsync()
+ .thenCompose(
+ session -> {
+ AndroidFuture<AppSearchBatchResult<String, Void>> batchResultFuture =
+ new AndroidFuture<>();
- session.put(putDocumentsRequest, mExecutor, batchResultFuture::complete);
- return batchResultFuture;
- });
+ session.put(
+ putDocumentsRequest, mExecutor, batchResultFuture::complete);
+ return batchResultFuture;
+ });
}
/**
@@ -152,10 +151,9 @@ public class FutureAppSearchSession implements Closeable {
* of search provided.
*/
public AndroidFuture<FutureSearchResults> search(
- @NonNull String queryExpression,
- @NonNull SearchSpec searchSpec) {
- return getSessionAsync().thenApply(
- session -> session.search(queryExpression, searchSpec))
+ @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ return getSessionAsync()
+ .thenApply(session -> session.search(queryExpression, searchSpec))
.thenApply(result -> new FutureSearchResults(result, mExecutor));
}
@@ -173,8 +171,8 @@ public class FutureAppSearchSession implements Closeable {
private final SearchResults mSearchResults;
private final Executor mExecutor;
- public FutureSearchResults(@NonNull SearchResults searchResults,
- @NonNull Executor executor) {
+ public FutureSearchResults(
+ @NonNull SearchResults searchResults, @NonNull Executor executor) {
mSearchResults = Objects.requireNonNull(searchResults);
mExecutor = Objects.requireNonNull(executor);
}
@@ -184,15 +182,68 @@ public class FutureAppSearchSession implements Closeable {
new AndroidFuture<>();
mSearchResults.getNextPage(mExecutor, nextPageFuture::complete);
- return nextPageFuture.thenApply(result -> {
- if (result.isSuccess()) {
- return result.getResultValue();
- } else {
- throw new RuntimeException(
- failedResultToException(result));
- }
- });
+ return nextPageFuture.thenApply(
+ result -> {
+ if (result.isSuccess()) {
+ return result.getResultValue();
+ } else {
+ throw new RuntimeException(failedResultToException(result));
+ }
+ });
}
+ }
+ /** A future API to retrieve a document by its id from the local AppSearch session. */
+ public AndroidFuture<GenericDocument> getByDocumentId(
+ @NonNull String documentId, @NonNull String namespace) {
+ Objects.requireNonNull(documentId);
+ Objects.requireNonNull(namespace);
+
+ GetByDocumentIdRequest request =
+ new GetByDocumentIdRequest.Builder(namespace)
+ .addIds(documentId)
+ .build();
+ return getSessionAsync()
+ .thenCompose(
+ session -> {
+ AndroidFuture<AppSearchBatchResult<String, GenericDocument>>
+ batchResultFuture = new AndroidFuture<>();
+ session.getByDocumentId(
+ request,
+ mExecutor,
+ new BatchResultCallbackAdapter<>(batchResultFuture));
+
+ return batchResultFuture.thenApply(
+ batchResult ->
+ getGenericDocumentFromBatchResult(
+ batchResult, documentId));
+ });
+ }
+
+ private static GenericDocument getGenericDocumentFromBatchResult(
+ AppSearchBatchResult<String, GenericDocument> result, String documentId) {
+ if (result.isSuccess()) {
+ return result.getSuccesses().get(documentId);
+ }
+ throw new IllegalArgumentException("No document in the result for id: " + documentId);
+ }
+
+ private static final class BatchResultCallbackAdapter<K, V>
+ implements BatchResultCallback<K, V> {
+ private final AndroidFuture<AppSearchBatchResult<K, V>> mFuture;
+
+ BatchResultCallbackAdapter(AndroidFuture<AppSearchBatchResult<K, V>> future) {
+ mFuture = future;
+ }
+
+ @Override
+ public void onResult(@NonNull AppSearchBatchResult<K, V> result) {
+ mFuture.complete(result);
+ }
+
+ @Override
+ public void onSystemError(Throwable t) {
+ mFuture.completeExceptionally(t);
+ }
}
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java
new file mode 100644
index 000000000000..0c2262456032
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.GlobalSearchSession;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+import android.util.Slog;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+/** A wrapper around {@link GlobalSearchSession} that provides a future-based API. */
+public class FutureGlobalSearchSession implements Closeable {
+ private static final String TAG = FutureGlobalSearchSession.class.getSimpleName();
+ private final Executor mExecutor;
+ private final AndroidFuture<AppSearchResult<GlobalSearchSession>> mSettableSessionFuture;
+
+ public FutureGlobalSearchSession(
+ @NonNull AppSearchManager appSearchManager, @NonNull Executor executor) {
+ this.mExecutor = executor;
+ mSettableSessionFuture = new AndroidFuture<>();
+ appSearchManager.createGlobalSearchSession(mExecutor, mSettableSessionFuture::complete);
+ }
+
+ private AndroidFuture<GlobalSearchSession> getSessionAsync() {
+ return mSettableSessionFuture.thenApply(
+ result -> {
+ if (result.isSuccess()) {
+ return result.getResultValue();
+ } else {
+ throw new RuntimeException(
+ FutureAppSearchSession.failedResultToException(result));
+ }
+ });
+ }
+
+ /**
+ * Registers an observer callback for the given target package name.
+ *
+ * @param targetPackageName The package name of the target app.
+ * @param spec The observer spec.
+ * @param executor The executor to run the observer callback on.
+ * @param observer The observer callback to register.
+ * @return A future that completes once the observer is registered.
+ */
+ public AndroidFuture<Void> registerObserverCallbackAsync(
+ String targetPackageName,
+ ObserverSpec spec,
+ Executor executor,
+ ObserverCallback observer) {
+ return getSessionAsync()
+ .thenCompose(
+ session -> {
+ try {
+ session.registerObserverCallback(
+ targetPackageName, spec, executor, observer);
+ return AndroidFuture.completedFuture(null);
+ } catch (AppSearchException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ getSessionAsync().get().close();
+ } catch (Exception ex) {
+ Slog.e(TAG, "Failed to close global search session", ex);
+ }
+ }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
new file mode 100644
index 000000000000..be5770b280dc
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchSpec;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+/**
+ * This class implements helper methods for synchronously interacting with AppSearch while
+ * synchronizing AppFunction runtime and static metadata.
+ */
+public class MetadataSyncAdapter {
+ private final FutureAppSearchSession mFutureAppSearchSession;
+ private final Executor mSyncExecutor;
+
+ public MetadataSyncAdapter(
+ @NonNull Executor syncExecutor,
+ @NonNull FutureAppSearchSession futureAppSearchSession) {
+ mSyncExecutor = Objects.requireNonNull(syncExecutor);
+ mFutureAppSearchSession = Objects.requireNonNull(futureAppSearchSession);
+ }
+
+ /**
+ * This method returns a map of package names to a set of function ids that are in the static
+ * metadata but not in the runtime metadata.
+ *
+ * @param staticPackageToFunctionMap A map of package names to a set of function ids from the
+ * static metadata.
+ * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the
+ * runtime metadata.
+ * @return A map of package names to a set of function ids that are in the static metadata but
+ * not in the runtime metadata.
+ */
+ @NonNull
+ @VisibleForTesting
+ static ArrayMap<String, ArraySet<String>> getAddedFunctionsDiffMap(
+ ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap,
+ ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) {
+ return getFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap);
+ }
+
+ /**
+ * This method returns a map of package names to a set of function ids that are in the runtime
+ * metadata but not in the static metadata.
+ *
+ * @param staticPackageToFunctionMap A map of package names to a set of function ids from the
+ * static metadata.
+ * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the
+ * runtime metadata.
+ * @return A map of package names to a set of function ids that are in the runtime metadata but
+ * not in the static metadata.
+ */
+ @NonNull
+ @VisibleForTesting
+ static ArrayMap<String, ArraySet<String>> getRemovedFunctionsDiffMap(
+ ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap,
+ ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) {
+ return getFunctionsDiffMap(runtimePackageToFunctionMap, staticPackageToFunctionMap);
+ }
+
+ @NonNull
+ private static ArrayMap<String, ArraySet<String>> getFunctionsDiffMap(
+ ArrayMap<String, ArraySet<String>> packageToFunctionMapA,
+ ArrayMap<String, ArraySet<String>> packageToFunctionMapB) {
+ ArrayMap<String, ArraySet<String>> diffMap = new ArrayMap<>();
+ for (String packageName : packageToFunctionMapA.keySet()) {
+ if (!packageToFunctionMapB.containsKey(packageName)) {
+ diffMap.put(packageName, packageToFunctionMapA.get(packageName));
+ continue;
+ }
+ ArraySet<String> diffFunctions = new ArraySet<>();
+ for (String functionId :
+ Objects.requireNonNull(packageToFunctionMapA.get(packageName))) {
+ if (!Objects.requireNonNull(packageToFunctionMapB.get(packageName))
+ .contains(functionId)) {
+ diffFunctions.add(functionId);
+ }
+ }
+ if (!diffFunctions.isEmpty()) {
+ diffMap.put(packageName, diffFunctions);
+ }
+ }
+ return diffMap;
+ }
+
+ /**
+ * This method returns a map of package names to a set of function ids.
+ *
+ * @param queryExpression The query expression to use when searching for AppFunction metadata.
+ * @param metadataSearchSpec The search spec to use when searching for AppFunction metadata.
+ * @return A map of package names to a set of function ids.
+ * @throws ExecutionException If the future search results fail to execute.
+ * @throws InterruptedException If the future search results are interrupted.
+ */
+ @NonNull
+ @VisibleForTesting
+ @WorkerThread
+ ArrayMap<String, ArraySet<String>> getPackageToFunctionIdMap(
+ @NonNull String queryExpression,
+ @NonNull SearchSpec metadataSearchSpec,
+ @NonNull String propertyFunctionId,
+ @NonNull String propertyPackageName)
+ throws ExecutionException, InterruptedException {
+ ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>();
+ FutureSearchResults futureSearchResults =
+ mFutureAppSearchSession.search(queryExpression, metadataSearchSpec).get();
+ List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get();
+ // TODO(b/357551503): This could be expensive if we have more functions
+ while (!searchResultsList.isEmpty()) {
+ for (SearchResult searchResult : searchResultsList) {
+ String packageName =
+ searchResult.getGenericDocument().getPropertyString(propertyPackageName);
+ String functionId =
+ searchResult.getGenericDocument().getPropertyString(propertyFunctionId);
+ packageToFunctionIds
+ .computeIfAbsent(packageName, k -> new ArraySet<>())
+ .add(functionId);
+ }
+ searchResultsList = futureSearchResults.getNextPage().get();
+ }
+ return packageToFunctionIds;
+ }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
index 98903ae57a39..58597c38bb94 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
@@ -25,7 +25,6 @@ import android.os.UserHandle;
* services are properly unbound after the operation completes or a timeout occurs.
*
* @param <T> Class of wrapped service.
- * @hide
*/
public interface RemoteServiceCaller<T> {
diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
index 0e18705c40b0..eea17eeca371 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
@@ -34,7 +34,6 @@ import java.util.function.Function;
* Context#bindService}.
*
* @param <T> Class of wrapped service.
- * @hide
*/
public class RemoteServiceCallerImpl<T> implements RemoteServiceCaller<T> {
private static final String TAG = "AppFunctionsServiceCall";
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 955b75d8bba0..3f4902db70f5 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -1122,7 +1122,11 @@ public final class BatteryStatsService extends IBatteryStats.Stub
throw new UnsupportedOperationException("Unknown tagId=" + atomTag);
}
final byte[] statsProto = bus.getStatsProto();
-
+ try {
+ bus.close();
+ } catch (IOException e) {
+ Slog.w(TAG, "Failure close BatteryUsageStats", e);
+ }
data.add(FrameworkStatsLog.buildStatsEvent(atomTag, statsProto));
return StatsManager.PULL_SUCCESS;
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 8f52f67ff7e0..416c11090515 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -86,7 +86,6 @@ import com.android.server.ServiceThread;
import dalvik.annotation.optimization.NeverCompile;
-import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -2318,6 +2317,7 @@ public final class CachedAppOptimizer {
Slog.d(TAG_AM, "Skipping freeze because process is marked "
+ "should not be frozen");
}
+ reportProcessFreezableChangedLocked(proc);
return;
}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 29373076c3b8..99c3ecaba2e0 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -139,6 +139,7 @@ public class SettingsToPropertiesMapper {
static final String[] sDeviceConfigAconfigScopes = new String[] {
"accessibility",
"android_core_networking",
+ "android_health_services",
"android_sdk",
"android_stylus",
"aoc",
@@ -235,7 +236,6 @@ public class SettingsToPropertiesMapper {
"wear_connectivity",
"wear_esim_carriers",
"wear_frameworks",
- "wear_health_services",
"wear_media",
"wear_offload",
"wear_security",
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index bd0024169324..6daf0d0b7d3b 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -12712,11 +12712,6 @@ public class AudioService extends IAudioService.Stub
if (mController == null)
return;
try {
- // TODO: remove this when deprecating STREAM_BLUETOOTH_SCO
- if (isStreamBluetoothSco(streamType)) {
- // TODO: notify both sco and voice_call about volume changes
- streamType = AudioSystem.STREAM_BLUETOOTH_SCO;
- }
mController.volumeChanged(streamType, flags);
} catch (RemoteException e) {
Log.w(TAG, "Error calling volumeChanged", e);
diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
index ac3c02823d0a..b2c616ae5b3c 100644
--- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java
+++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
@@ -316,6 +316,7 @@ class PreAuthInfo {
Pair<BiometricSensor, Integer> sensorNotEnrolled = null;
Pair<BiometricSensor, Integer> sensorLockout = null;
Pair<BiometricSensor, Integer> hardwareNotDetected = null;
+ Pair<BiometricSensor, Integer> biometricAppNotAllowed = null;
for (Pair<BiometricSensor, Integer> pair : ineligibleSensors) {
final int status = pair.second;
if (status == BIOMETRIC_LOCKOUT_TIMED || status == BIOMETRIC_LOCKOUT_PERMANENT) {
@@ -327,6 +328,9 @@ class PreAuthInfo {
if (status == BIOMETRIC_HARDWARE_NOT_DETECTED) {
hardwareNotDetected = pair;
}
+ if (status == BIOMETRIC_NOT_ENABLED_FOR_APPS) {
+ biometricAppNotAllowed = pair;
+ }
}
// If there is a sensor locked out, prioritize lockout over other sensor's error.
@@ -339,6 +343,10 @@ class PreAuthInfo {
return hardwareNotDetected;
}
+ if (Flags.mandatoryBiometrics() && biometricAppNotAllowed != null) {
+ return biometricAppNotAllowed;
+ }
+
// If the caller requested STRONG, and the device contains both STRONG and non-STRONG
// sensors, prioritize BIOMETRIC_NOT_ENROLLED over the weak sensor's
// BIOMETRIC_INSUFFICIENT_STRENGTH error.
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 871121472938..407ef1e41aa6 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -321,6 +321,9 @@ public class Utils {
case BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE:
biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE;
break;
+ case BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS:
+ biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS;
+ break;
default:
Slog.e(BiometricService.TAG, "Unhandled result code: " + biometricConstantsCode);
biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
@@ -384,9 +387,12 @@ public class Utils {
return BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED;
case MANDATORY_BIOMETRIC_UNAVAILABLE_ERROR:
return BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE;
+ case BIOMETRIC_NOT_ENABLED_FOR_APPS:
+ if (Flags.mandatoryBiometrics()) {
+ return BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS;
+ }
case BIOMETRIC_DISABLED_BY_DEVICE_POLICY:
case BIOMETRIC_HARDWARE_NOT_DETECTED:
- case BIOMETRIC_NOT_ENABLED_FOR_APPS:
default:
return BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE;
}
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index 17835b2d085b..ec61d4d39aa0 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -379,9 +379,7 @@ public class CameraServiceProxy extends SystemService
streamCount = mStreamStats.size();
}
if (CameraServiceProxy.DEBUG) {
- String ultrawideDebug = Flags.logUltrawideUsage()
- ? ", wideAngleUsage " + mUsedUltraWide
- : "";
+ String ultrawideDebug = ", wideAngleUsage " + mUsedUltraWide;
String zoomOverrideDebug = Flags.logZoomOverrideUsage()
? ", zoomOverrideUsage " + mUsedZoomOverride
: "";
@@ -1338,7 +1336,7 @@ public class CameraServiceProxy extends SystemService
List<CameraStreamStats> streamStats = cameraState.getStreamStats();
String userTag = cameraState.getUserTag();
int videoStabilizationMode = cameraState.getVideoStabilizationMode();
- boolean usedUltraWide = Flags.logUltrawideUsage() ? cameraState.getUsedUltraWide() : false;
+ boolean usedUltraWide = cameraState.getUsedUltraWide();
boolean usedZoomOverride =
Flags.logZoomOverrideUsage() ? cameraState.getUsedZoomOverride() : false;
long logId = cameraState.getLogId();
diff --git a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
index 5ab22e1dcd61..e6abcb958b55 100644
--- a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
+++ b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
@@ -60,6 +60,12 @@ final class SetArcTransmissionStateAction extends HdmiCecFeatureAction {
boolean start() {
// Seq #37.
if (mEnabled) {
+ // Avoid triggering duplicate RequestSadAction events.
+ // This could lead to unexpected responses from the AVR and cause the TV to receive data
+ // out of order. The SAD report does not provide information about the order of events.
+ if ((tv().hasAction(RequestSadAction.class))) {
+ return true;
+ }
// Request SADs before enabling ARC
RequestSadAction action = new RequestSadAction(
localDevice(), Constants.ADDR_AUDIO_SYSTEM,
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 84cee7ecbd05..1285a61d08f2 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2269,13 +2269,15 @@ public class InputManagerService extends IInputManager.Stub
// Native callback.
@SuppressWarnings("unused")
private void notifyTouchpadHardwareState(TouchpadHardwareState hardwareStates, int deviceId) {
- // TODO(b/286551975): sent the touchpad hardware state data here to TouchpadDebugActivity
Slog.d(TAG, "notifyTouchpadHardwareState: Time: "
+ hardwareStates.getTimestamp() + ", No. Buttons: "
+ hardwareStates.getButtonsDown() + ", No. Fingers: "
+ hardwareStates.getFingerCount() + ", No. Touch: "
+ hardwareStates.getTouchCount() + ", Id: "
+ deviceId);
+ if (mTouchpadDebugViewController != null) {
+ mTouchpadDebugViewController.updateTouchpadHardwareState(hardwareStates);
+ }
}
// Native callback.
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index 7785ffb4b17a..ba56ad073e6a 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -30,6 +30,9 @@ import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.TextView;
+import com.android.server.input.TouchpadFingerState;
+import com.android.server.input.TouchpadHardwareState;
+
import java.util.Objects;
public class TouchpadDebugView extends LinearLayout {
@@ -52,6 +55,10 @@ public class TouchpadDebugView extends LinearLayout {
private int mScreenHeight;
private int mWindowLocationBeforeDragX;
private int mWindowLocationBeforeDragY;
+ @NonNull
+ private TouchpadHardwareState mLastTouchpadState =
+ new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
+ new TouchpadFingerState[0]);
public TouchpadDebugView(Context context, int touchpadId) {
super(context);
@@ -83,14 +90,14 @@ public class TouchpadDebugView extends LinearLayout {
private void init(Context context) {
setOrientation(VERTICAL);
- setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT));
- setBackgroundColor(Color.TRANSPARENT);
+ setLayoutParams(new LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+ setBackgroundColor(Color.RED);
// TODO(b/286551975): Replace this content with the touchpad debug view.
TextView textView1 = new TextView(context);
- textView1.setBackgroundColor(Color.parseColor("#FFFF0000"));
+ textView1.setBackgroundColor(Color.TRANSPARENT);
textView1.setTextSize(20);
textView1.setText("Touchpad Debug View 1");
textView1.setGravity(Gravity.CENTER);
@@ -98,7 +105,7 @@ public class TouchpadDebugView extends LinearLayout {
textView1.setLayoutParams(new LayoutParams(1000, 200));
TextView textView2 = new TextView(context);
- textView2.setBackgroundColor(Color.BLUE);
+ textView2.setBackgroundColor(Color.TRANSPARENT);
textView2.setTextSize(20);
textView2.setText("Touchpad Debug View 2");
textView2.setGravity(Gravity.CENTER);
@@ -126,9 +133,7 @@ public class TouchpadDebugView extends LinearLayout {
case MotionEvent.ACTION_MOVE:
deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX;
deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY;
- Slog.d("TouchpadDebugView", "Slop = " + mTouchSlop);
if (isSlopExceeded(deltaX, deltaY)) {
- Slog.d("TouchpadDebugView", "Slop exceeded");
mWindowLayoutParams.x =
Math.max(0, Math.min((int) (event.getRawX() - mTouchDownX),
mScreenWidth - this.getWidth()));
@@ -136,9 +141,6 @@ public class TouchpadDebugView extends LinearLayout {
Math.max(0, Math.min((int) (event.getRawY() - mTouchDownY),
mScreenHeight - this.getHeight()));
- Slog.d("TouchpadDebugView", "New position X: "
- + mWindowLayoutParams.x + ", Y: " + mWindowLayoutParams.y);
-
mWindowManager.updateViewLayout(this, mWindowLayoutParams);
}
return true;
@@ -166,7 +168,7 @@ public class TouchpadDebugView extends LinearLayout {
@Override
public boolean performClick() {
super.performClick();
- Slog.d("TouchpadDebugView", "You clicked me!");
+ Slog.d("TouchpadDebugView", "You tapped the window!");
return true;
}
@@ -201,4 +203,34 @@ public class TouchpadDebugView extends LinearLayout {
public WindowManager.LayoutParams getWindowLayoutParams() {
return mWindowLayoutParams;
}
+
+ public void updateHardwareState(TouchpadHardwareState touchpadHardwareState) {
+ if (mLastTouchpadState.getButtonsDown() == 0) {
+ if (touchpadHardwareState.getButtonsDown() > 0) {
+ onTouchpadButtonPress();
+ }
+ } else {
+ if (touchpadHardwareState.getButtonsDown() == 0) {
+ onTouchpadButtonRelease();
+ }
+ }
+ mLastTouchpadState = touchpadHardwareState;
+ }
+
+ private void onTouchpadButtonPress() {
+ Slog.d("TouchpadDebugView", "You clicked me!");
+
+ // Iterate through all child views
+ // Temporary demonstration for testing
+ for (int i = 0; i < getChildCount(); i++) {
+ getChildAt(i).setBackgroundColor(Color.BLUE);
+ }
+ }
+
+ private void onTouchpadButtonRelease() {
+ Slog.d("TouchpadDebugView", "You released the click");
+ for (int i = 0; i < getChildCount(); i++) {
+ getChildAt(i).setBackgroundColor(Color.RED);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
index c28e74a02071..bc53c4947a71 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
@@ -27,6 +27,7 @@ import android.view.WindowManager;
import com.android.server.input.InputManagerService;
import com.android.server.input.TouchpadHardwareProperties;
+import com.android.server.input.TouchpadHardwareState;
import java.util.Objects;
@@ -132,4 +133,10 @@ public class TouchpadDebugViewController implements InputManager.InputDeviceList
mTouchpadDebugView = null;
Slog.d(TAG, "Touchpad debug view removed.");
}
+
+ public void updateTouchpadHardwareState(TouchpadHardwareState touchpadHardwareState) {
+ if (mTouchpadDebugView != null) {
+ mTouchpadDebugView.updateHardwareState(touchpadHardwareState);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 38ef5b8cedb9..7d44ba199119 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -889,8 +889,14 @@ public class LockSettingsService extends ILockSettings.Stub {
// Hide notification first, as tie profile lock takes time
hideEncryptionNotification(new UserHandle(userId));
- if (isCredentialSharableWithParent(userId)) {
- tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
+ if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) {
+ synchronized (mSpManager) {
+ tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
+ }
+ } else {
+ if (isCredentialSharableWithParent(userId)) {
+ tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
+ }
}
}
});
@@ -1287,7 +1293,13 @@ public class LockSettingsService extends ILockSettings.Stub {
mStorage.removeChildProfileLock(userId);
removeKeystoreProfileKey(userId);
} else {
- tieProfileLockIfNecessary(userId, profileUserPassword);
+ if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) {
+ synchronized (mSpManager) {
+ tieProfileLockIfNecessary(userId, profileUserPassword);
+ }
+ } else {
+ tieProfileLockIfNecessary(userId, profileUserPassword);
+ }
}
} catch (IllegalStateException e) {
setBoolean(SEPARATE_PROFILE_CHALLENGE_KEY, old, userId);
diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
index bad959af7aad..925ba1752fe2 100644
--- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
+++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
@@ -22,6 +22,7 @@ import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF;
import static com.android.server.notification.ZenLog.traceApplyDeviceEffect;
import static com.android.server.notification.ZenLog.traceScheduleApplyDeviceEffect;
+import android.app.KeyguardManager;
import android.app.UiModeManager;
import android.app.WallpaperManager;
import android.content.BroadcastReceiver;
@@ -53,6 +54,7 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier {
private final Context mContext;
private final ColorDisplayManager mColorDisplayManager;
+ private final KeyguardManager mKeyguardManager;
private final PowerManager mPowerManager;
private final UiModeManager mUiModeManager;
private final WallpaperManager mWallpaperManager;
@@ -67,6 +69,7 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier {
DefaultDeviceEffectsApplier(Context context) {
mContext = context;
mColorDisplayManager = context.getSystemService(ColorDisplayManager.class);
+ mKeyguardManager = context.getSystemService(KeyguardManager.class);
mPowerManager = context.getSystemService(PowerManager.class);
mUiModeManager = context.getSystemService(UiModeManager.class);
WallpaperManager wallpaperManager = context.getSystemService(WallpaperManager.class);
@@ -133,12 +136,14 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier {
// Changing the theme can be disruptive for the user (Activities are likely recreated, may
// lose some state). Therefore we only apply the change immediately if the rule was
- // activated manually, or we are initializing, or the screen is currently off/dreaming.
+ // activated manually, or we are initializing, or the screen is currently off/dreaming,
+ // or if the device is locked.
if (origin == ZenModeConfig.ORIGIN_INIT
|| origin == ZenModeConfig.ORIGIN_INIT_USER
|| origin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI
|| origin == ZenModeConfig.ORIGIN_USER_IN_APP
- || !mPowerManager.isInteractive()) {
+ || !mPowerManager.isInteractive()
+ || (android.app.Flags.modesUi() && mKeyguardManager.isKeyguardLocked())) {
unregisterScreenOffReceiver();
updateNightModeImmediately(useNightMode);
} else {
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 3523a3336a63..03fc60cad8d6 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -1094,7 +1094,7 @@ abstract public class ManagedServices {
return info;
}
throw new SecurityException("Disallowed call from unknown " + getCaption() + ": "
- + service + " " + service.getClass());
+ + service.asBinder() + " " + service.getClass());
}
public boolean isSameUser(IInterface service, int userId) {
@@ -1585,6 +1585,9 @@ abstract public class ManagedServices {
// after the rebind delay
if (isPackageOrComponentAllowedWithPermission(cn, userId)) {
registerService(cn, userId);
+ } else {
+ if (DEBUG) Slog.v(TAG, "skipped reregisterService cn=" + cn + " u=" + userId
+ + " because of isPackageOrComponentAllowedWithPermission check");
}
}
@@ -1918,6 +1921,7 @@ abstract public class ManagedServices {
.append(",targetSdkVersion=").append(targetSdkVersion)
.append(",connection=").append(connection == null ? null : "<connection>")
.append(",service=").append(service)
+ .append(",serviceAsBinder=").append(service != null ? service.asBinder() : null)
.append(']').toString();
}
@@ -1956,7 +1960,7 @@ abstract public class ManagedServices {
@Override
public void binderDied() {
- if (DEBUG) Slog.d(TAG, "binderDied");
+ if (DEBUG) Slog.d(TAG, "binderDied " + this);
// Remove the service, but don't unbind from the service. The system will bring the
// service back up, and the onServiceConnected handler will read the service with the
// new binding. If this isn't a bound service, and is just a registered
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 43a285cba4b9..2856eb45ebd1 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1019,7 +1019,9 @@ final class InstallPackageHelper {
&& scanInstallPackages(requests, createdAppId, versionInfos)) {
List<ReconciledPackage> reconciledPackages =
reconcileInstallPackages(requests, versionInfos);
- if (reconciledPackages != null && commitInstallPackages(reconciledPackages)) {
+ if (reconciledPackages != null
+ && renameAndUpdatePaths(requests)
+ && commitInstallPackages(reconciledPackages)) {
success = true;
}
}
@@ -1029,24 +1031,49 @@ final class InstallPackageHelper {
}
}
- private boolean prepareInstallPackages(List<InstallRequest> requests) {
- // TODO: will remove the locking after doRename is moved out of prepare
+ private boolean renameAndUpdatePaths(List<InstallRequest> requests) {
try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
for (InstallRequest request : requests) {
+ ParsedPackage parsedPackage = request.getParsedPackage();
+ final boolean isApex = (request.getScanFlags() & SCAN_AS_APEX) != 0;
+ if (isApex) {
+ continue;
+ }
try {
- Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
- request.onPrepareStarted();
- preparePackageLI(request);
- } catch (PrepareFailure prepareFailure) {
- request.setError(prepareFailure.error,
- prepareFailure.getMessage());
- request.setOriginPackage(prepareFailure.mConflictingPackage);
- request.setOriginPermission(prepareFailure.mConflictingPermission);
+ doRenameLI(request, parsedPackage);
+ setUpFsVerity(parsedPackage);
+ } catch (Installer.InstallerException | IOException | DigestException
+ | NoSuchAlgorithmException | PrepareFailure e) {
+ request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP,
+ "Failed to set up verity: " + e);
return false;
- } finally {
- request.onPrepareFinished();
- Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
+
+ // update paths that are set before renaming
+ PackageSetting scannedPackageSetting = request.getScannedPackageSetting();
+ scannedPackageSetting.setPath(new File(parsedPackage.getPath()));
+ scannedPackageSetting.setLegacyNativeLibraryPath(
+ parsedPackage.getNativeLibraryRootDir());
+ }
+ return true;
+ }
+ }
+
+ private boolean prepareInstallPackages(List<InstallRequest> requests) {
+ for (InstallRequest request : requests) {
+ try {
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
+ request.onPrepareStarted();
+ preparePackage(request);
+ } catch (PrepareFailure prepareFailure) {
+ request.setError(prepareFailure.error,
+ prepareFailure.getMessage());
+ request.setOriginPackage(prepareFailure.mConflictingPackage);
+ request.setOriginPermission(prepareFailure.mConflictingPermission);
+ return false;
+ } finally {
+ request.onPrepareFinished();
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}
return true;
@@ -1231,8 +1258,7 @@ final class InstallPackageHelper {
return newProp != null && newProp.getBoolean();
}
- @GuardedBy("mPm.mInstallLock")
- private void preparePackageLI(InstallRequest request) throws PrepareFailure {
+ private void preparePackage(InstallRequest request) throws PrepareFailure {
final int[] allUsers = mPm.mUserManager.getUserIds();
final int installFlags = request.getInstallFlags();
final boolean onExternal = request.getVolumeUuid() != null;
@@ -1739,18 +1765,7 @@ final class InstallPackageHelper {
}
}
- if (!isApex) {
- doRenameLI(request, parsedPackage);
-
- try {
- setUpFsVerity(parsedPackage);
- } catch (Installer.InstallerException | IOException | DigestException
- | NoSuchAlgorithmException e) {
- throw PrepareFailure.ofInternalError(
- "Failed to set up verity: " + e,
- PackageManagerException.INTERNAL_ERROR_VERITY_SETUP);
- }
- } else {
+ if (isApex) {
// Use the path returned by apexd
parsedPackage.setPath(request.getApexInfo().modulePath);
parsedPackage.setBaseApkPath(request.getApexInfo().modulePath);
@@ -1882,10 +1897,16 @@ final class InstallPackageHelper {
}
if (!oldSharedUid.equals(newSharedUid)) {
- throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
- "Package " + parsedPackage.getPackageName()
- + " shared user changed from "
- + oldSharedUid + " to " + newSharedUid);
+ if (!(oldSharedUid.equals("<nothing>") && ps.getPkg() == null
+ && ps.isArchivedOnAnyUser(allUsers))) {
+ // Only allow changing sharedUserId if unarchiving
+ // TODO(b/361558423): remove this check after pre-archiving installs
+ // accept a sharedUserId param in the API
+ throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
+ "Package " + parsedPackage.getPackageName()
+ + " shared user changed from "
+ + oldSharedUid + " to " + newSharedUid);
+ }
}
// APK should not re-join shared UID
@@ -2086,7 +2107,21 @@ final class InstallPackageHelper {
// Reflect the rename in scanned details
try {
- parsedPackage.setPath(afterCodeFile.getCanonicalPath());
+ String afterCanonicalPath = afterCodeFile.getCanonicalPath();
+ String beforeCanonicalPath = beforeCodeFile.getCanonicalPath();
+ parsedPackage.setPath(afterCanonicalPath);
+
+ parsedPackage.setNativeLibraryDir(
+ parsedPackage.getNativeLibraryDir()
+ .replace(beforeCanonicalPath, afterCanonicalPath));
+ parsedPackage.setNativeLibraryRootDir(
+ parsedPackage.getNativeLibraryRootDir()
+ .replace(beforeCanonicalPath, afterCanonicalPath));
+ String secondaryNativeLibraryDir = parsedPackage.getSecondaryNativeLibraryDir();
+ if (secondaryNativeLibraryDir != null) {
+ parsedPackage.setSecondaryNativeLibraryDir(
+ secondaryNativeLibraryDir.replace(beforeCanonicalPath, afterCanonicalPath));
+ }
} catch (IOException e) {
Slog.e(TAG, "Failed to get path: " + afterCodeFile, e);
throw new PrepareFailure(PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE,
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 5e45b4c2d5af..f53234215fc6 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -936,13 +936,9 @@ public class PackageArchiver {
* <p> In the rare case the app had multiple launcher activities, only one of the icons is
* returned arbitrarily.
*
- * <p> By default, the icon will be overlay'd with a cloud icon on top. A launcher app can
+ * <p> By default, the icon will be overlay'd with a cloud icon on top. An app can
* disable the cloud overlay via the
* {@link LauncherApps.ArchiveCompatibilityParams#setEnableIconOverlay(boolean)} API.
- * The default launcher's cloud overlay mode determines the cloud overlay status returned by
- * any other callers. That is, if the current launcher has the cloud overlay disabled, any other
- * app that fetches the app icon will also get an icon that has the cloud overlay disabled.
- * This is to prevent style mismatch caused by icons that are fetched by different callers.
*/
@Nullable
public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 9fb9e717fe4d..9428de700385 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -925,6 +925,18 @@ public class PackageSetting extends SettingBase implements PackageStateInternal
return PackageArchiver.isArchived(readUserState(userId));
}
+ /**
+ * @return if the package is archived in any of the users
+ */
+ boolean isArchivedOnAnyUser(int[] userIds) {
+ for (int user : userIds) {
+ if (isArchived(user)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
int getInstallReason(int userId) {
return readUserState(userId).getInstallReason();
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 1fcd7f1ef861..ed9dcfadab83 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -535,7 +535,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
volatile boolean mRequestedOrSleepingDefaultDisplay;
/**
- * This is used to check whether to invoke {@link #updateScreenOffSleepToken} when screen is
+ * This is used to check whether to acquire screen-off sleep token when screen is
* turned off. E.g. if it is false when screen is turned off and the display is swapping, it
* is expected that the screen will be on in a short time. Then it is unnecessary to acquire
* screen-off-sleep-token, so it can avoid intermediate visibility or lifecycle changes.
@@ -610,7 +610,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private boolean mPendingKeyguardOccluded;
private boolean mKeyguardOccludedChanged;
- private ActivityTaskManagerInternal.SleepTokenAcquirer mScreenOffSleepTokenAcquirer;
Intent mHomeIntent;
Intent mCarDockIntent;
Intent mDeskDockIntent;
@@ -2220,9 +2219,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mLockPatternUtils = new LockPatternUtils(mContext);
mLogger = new MetricsLogger();
- mScreenOffSleepTokenAcquirer = mActivityTaskManagerInternal
- .createSleepTokenAcquirer("ScreenOff");
-
Resources res = mContext.getResources();
mWakeOnDpadKeyPress =
res.getBoolean(com.android.internal.R.bool.config_wakeOnDpadKeyPress);
@@ -5521,13 +5517,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mRequestedOrSleepingDefaultDisplay = true;
mIsGoingToSleepDefaultDisplay = true;
- // In case startedGoingToSleep is called after screenTurnedOff (the source caller is in
- // order but the methods run on different threads) and updateScreenOffSleepToken was
- // skipped. Then acquire sleep token if screen was off.
- if (!mDefaultDisplayPolicy.isScreenOnFully() && !mDefaultDisplayPolicy.isScreenOnEarly()) {
- updateScreenOffSleepToken(true /* acquire */);
- }
-
if (mKeyguardDelegate != null) {
mKeyguardDelegate.onStartedGoingToSleep(pmSleepReason);
}
@@ -5688,11 +5677,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (DEBUG_WAKEUP) Slog.i(TAG, "Display" + displayId + " turned off...");
if (displayId == DEFAULT_DISPLAY) {
- if (!isSwappingDisplay || mIsGoingToSleepDefaultDisplay) {
- updateScreenOffSleepToken(true /* acquire */);
- }
+ final boolean acquireSleepToken = !isSwappingDisplay || mIsGoingToSleepDefaultDisplay;
mRequestedOrSleepingDefaultDisplay = false;
- mDefaultDisplayPolicy.screenTurnedOff();
+ mDefaultDisplayPolicy.screenTurnedOff(acquireSleepToken);
synchronized (mLock) {
if (mKeyguardDelegate != null) {
mKeyguardDelegate.onScreenTurnedOff();
@@ -5748,7 +5735,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (displayId == DEFAULT_DISPLAY) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "screenTurningOn",
0 /* cookie */);
- updateScreenOffSleepToken(false /* acquire */);
mDefaultDisplayPolicy.screenTurningOn(screenOnListener);
mBootAnimationDismissable = false;
@@ -6255,15 +6241,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- // TODO (multidisplay): Support multiple displays in WindowManagerPolicy.
- private void updateScreenOffSleepToken(boolean acquire) {
- if (acquire) {
- mScreenOffSleepTokenAcquirer.acquire(DEFAULT_DISPLAY);
- } else {
- mScreenOffSleepTokenAcquirer.release(DEFAULT_DISPLAY);
- }
- }
-
/** {@inheritDoc} */
@Override
public void enableScreenAfterBoot() {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index f53dda6ee35b..4dcc6e112ecc 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -3169,7 +3169,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
final WallpaperDestinationChangeHandler
liveSync = new WallpaperDestinationChangeHandler(
newWallpaper);
- boolean same = changingToSame(name, newWallpaper);
+ boolean same = changingToSame(name, newWallpaper.connection,
+ newWallpaper.wallpaperComponent);
/*
* If we have a shared system+lock wallpaper, and we reapply the same wallpaper
@@ -3257,14 +3258,15 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
return name == null || name.equals(mDefaultWallpaperComponent);
}
- private boolean changingToSame(ComponentName componentName, WallpaperData wallpaper) {
- if (wallpaper.connection != null) {
- final ComponentName wallpaperName = wallpaper.wallpaperComponent;
- if (isDefaultComponent(componentName) && isDefaultComponent(wallpaperName)) {
+ private boolean changingToSame(ComponentName newComponentName,
+ WallpaperConnection currentConnection, ComponentName currentComponentName) {
+ if (currentConnection != null) {
+ if (isDefaultComponent(newComponentName) && isDefaultComponent(currentComponentName)) {
if (DEBUG) Slog.v(TAG, "changingToSame: still using default");
// Still using default wallpaper.
return true;
- } else if (wallpaperName != null && wallpaperName.equals(componentName)) {
+ } else if (currentComponentName != null && currentComponentName.equals(
+ newComponentName)) {
// Changing to same wallpaper.
if (DEBUG) Slog.v(TAG, "same wallpaper");
return true;
@@ -3279,7 +3281,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
Slog.v(TAG, "bindWallpaperComponentLocked: componentName=" + componentName);
}
// Has the component changed?
- if (!force && changingToSame(componentName, wallpaper)) {
+ if (!force && changingToSame(componentName, wallpaper.connection,
+ wallpaper.wallpaperComponent)) {
try {
if (DEBUG_LIVE) {
Slog.v(TAG, "Changing to the same component, ignoring");
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 99747e05e7f0..0be6471f189e 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8149,7 +8149,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
*/
@Override
protected int getOverrideOrientation() {
- if (mWmService.mConstants.mIgnoreActivityOrientationRequest) {
+ if (mWmService.mConstants.mIgnoreActivityOrientationRequest
+ && info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {
return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
}
return mAppCompatController.getOrientationPolicy()
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index b8ce02ed5937..3d6b64b2e536 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -130,35 +130,6 @@ public abstract class ActivityTaskManagerInternal {
}
/**
- * Sleep tokens cause the activity manager to put the top activity to sleep.
- * They are used by components such as dreams that may hide and block interaction
- * with underlying activities.
- * The Acquirer provides an interface that encapsulates the underlying work, so the user does
- * not need to handle the token by him/herself.
- */
- public interface SleepTokenAcquirer {
-
- /**
- * Acquires a sleep token.
- * @param displayId The display to apply to.
- */
- void acquire(int displayId);
-
- /**
- * Releases the sleep token.
- * @param displayId The display to apply to.
- */
- void release(int displayId);
- }
-
- /**
- * Creates a sleep token acquirer for the specified display with the specified tag.
- *
- * @param tag A string identifying the purpose (eg. "Dream").
- */
- public abstract SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag);
-
- /**
* Returns home activity for the specified user.
*
* @param userId ID of the user or {@link android.os.UserHandle#USER_ALL}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index e25d940d9781..49ca698e36e2 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4356,6 +4356,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
mTaskOrganizerController.dump(pw, " ");
mVisibleActivityProcessTracker.dump(pw, " ");
mActiveUids.dump(pw, " ");
+ pw.println(" SleepTokens=" + mRootWindowContainer.mSleepTokens);
if (mDemoteTopAppReasons != 0) {
pw.println(" mDemoteTopAppReasons=" + mDemoteTopAppReasons);
}
@@ -5071,17 +5072,16 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
EventLogTags.writeWmSetResumedActivity(r.mUserId, r.shortComponentName, reason);
}
- final class SleepTokenAcquirerImpl implements ActivityTaskManagerInternal.SleepTokenAcquirer {
+ final class SleepTokenAcquirer {
private final String mTag;
private final SparseArray<RootWindowContainer.SleepToken> mSleepTokens =
new SparseArray<>();
- SleepTokenAcquirerImpl(@NonNull String tag) {
+ SleepTokenAcquirer(@NonNull String tag) {
mTag = tag;
}
- @Override
- public void acquire(int displayId) {
+ void acquire(int displayId) {
synchronized (mGlobalLock) {
if (!mSleepTokens.contains(displayId)) {
mSleepTokens.append(displayId,
@@ -5091,8 +5091,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
}
}
- @Override
- public void release(int displayId) {
+ void release(int displayId) {
synchronized (mGlobalLock) {
final RootWindowContainer.SleepToken token = mSleepTokens.get(displayId);
if (token != null) {
@@ -5955,11 +5954,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
}
final class LocalService extends ActivityTaskManagerInternal {
- @Override
- public SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag) {
- Objects.requireNonNull(tag);
- return new SleepTokenAcquirerImpl(tag);
- }
@Override
public ComponentName getHomeActivityForUser(int userId) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index e8a3951a93d4..10e0641b0582 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -735,8 +735,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
/** All tokens used to put activities on this root task to sleep (including mOffToken) */
final ArrayList<RootWindowContainer.SleepToken> mAllSleepTokens = new ArrayList<>();
- /** The token acquirer to put root tasks on the display to sleep */
- private final ActivityTaskManagerInternal.SleepTokenAcquirer mOffTokenAcquirer;
private boolean mSleeping;
@@ -1131,7 +1129,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
mDisplay = display;
mDisplayId = display.getDisplayId();
mCurrentUniqueDisplayId = display.getUniqueId();
- mOffTokenAcquirer = mRootWindowContainer.mDisplayOffTokenAcquirer;
mWallpaperController = new WallpaperController(mWmService, this);
mWallpaperController.resetLargestDisplay(display);
display.getDisplayInfo(mDisplayInfo);
@@ -6157,9 +6154,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
final int displayState = mDisplayInfo.state;
if (displayId != DEFAULT_DISPLAY) {
if (displayState == Display.STATE_OFF) {
- mOffTokenAcquirer.acquire(mDisplayId);
+ mRootWindowContainer.mDisplayOffTokenAcquirer.acquire(mDisplayId);
} else if (displayState == Display.STATE_ON) {
- mOffTokenAcquirer.release(mDisplayId);
+ mRootWindowContainer.mDisplayOffTokenAcquirer.release(mDisplayId);
}
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Content Recording: Display %d state was (%d), is now (%d), so update "
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 5c621208c4db..107d31e4e25c 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -804,6 +804,14 @@ public class DisplayPolicy {
mAwake /* waiting */);
if (!awake) {
onDisplaySwitchFinished();
+ // In case PhoneWindowManager's startedGoingToSleep is called after screenTurnedOff
+ // (the source caller is in order but the methods run on different threads) and
+ // updateScreenOffSleepToken was skipped by mIsGoingToSleepDefaultDisplay. Then
+ // acquire sleep token if screen is off.
+ if (!mScreenOnEarly && !mScreenOnFully && !mDisplayContent.isSleeping()) {
+ Slog.w(TAG, "Late acquire sleep token for " + mDisplayContent);
+ mService.mRoot.mDisplayOffTokenAcquirer.acquire(mDisplayContent.mDisplayId);
+ }
}
}
}
@@ -851,6 +859,7 @@ public class DisplayPolicy {
public void screenTurningOn(ScreenOnListener screenOnListener) {
WindowProcessController visibleDozeUiProcess = null;
synchronized (mLock) {
+ mService.mRoot.mDisplayOffTokenAcquirer.release(mDisplayContent.mDisplayId);
mScreenOnEarly = true;
mScreenOnFully = false;
mKeyguardDrawComplete = false;
@@ -875,8 +884,12 @@ public class DisplayPolicy {
onDisplaySwitchFinished();
}
- public void screenTurnedOff() {
+ /** It is called after {@link #screenTurningOn}. This runs on PowerManager's thread. */
+ public void screenTurnedOff(boolean acquireSleepToken) {
synchronized (mLock) {
+ if (acquireSleepToken) {
+ mService.mRoot.mDisplayOffTokenAcquirer.acquire(mDisplayContent.mDisplayId);
+ }
mScreenOnEarly = false;
mScreenOnFully = false;
mKeyguardDrawComplete = false;
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 5d8a96c530ef..0c489d6207e9 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -87,7 +87,7 @@ class KeyguardController {
private final SparseArray<KeyguardDisplayState> mDisplayStates = new SparseArray<>();
private final ActivityTaskManagerService mService;
private RootWindowContainer mRootWindowContainer;
- private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer;
+ private final ActivityTaskManagerService.SleepTokenAcquirer mSleepTokenAcquirer;
private boolean mWaitingForWakeTransition;
private Transition.ReadyCondition mWaitAodHide = null;
@@ -95,7 +95,7 @@ class KeyguardController {
ActivityTaskSupervisor taskSupervisor) {
mService = service;
mTaskSupervisor = taskSupervisor;
- mSleepTokenAcquirer = mService.new SleepTokenAcquirerImpl(KEYGUARD_SLEEP_TOKEN_TAG);
+ mSleepTokenAcquirer = mService.new SleepTokenAcquirer(KEYGUARD_SLEEP_TOKEN_TAG);
}
void setWindowManager(WindowManagerService windowManager) {
@@ -658,10 +658,10 @@ class KeyguardController {
private boolean mRequestDismissKeyguard;
private final ActivityTaskManagerService mService;
- private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer;
+ private final ActivityTaskManagerService.SleepTokenAcquirer mSleepTokenAcquirer;
KeyguardDisplayState(ActivityTaskManagerService service, int displayId,
- ActivityTaskManagerInternal.SleepTokenAcquirer acquirer) {
+ ActivityTaskManagerService.SleepTokenAcquirer acquirer) {
mService = service;
mDisplayId = displayId;
mSleepTokenAcquirer = acquirer;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 866dcd56ea91..8f5612c61e1c 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -215,7 +215,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
private static final String DISPLAY_OFF_SLEEP_TOKEN_TAG = "Display-off";
/** The token acquirer to put root tasks on the displays to sleep */
- final ActivityTaskManagerInternal.SleepTokenAcquirer mDisplayOffTokenAcquirer;
+ final ActivityTaskManagerService.SleepTokenAcquirer mDisplayOffTokenAcquirer;
/**
* The modes which affect which tasks are returned when calling
@@ -450,7 +450,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
mService = service.mAtmService;
mTaskSupervisor = mService.mTaskSupervisor;
mTaskSupervisor.mRootWindowContainer = this;
- mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirerImpl(DISPLAY_OFF_SLEEP_TOKEN_TAG);
+ mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirer(DISPLAY_OFF_SLEEP_TOKEN_TAG);
mDeviceStateController = new DeviceStateController(service.mContext, service.mGlobalLock);
mDisplayRotationCoordinator = new DisplayRotationCoordinator();
}
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 5aa34d22f00f..92953e5a5041 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -17,6 +17,8 @@
package com.android.server.wm;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENTS_INFO;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO;
import static android.window.TaskFragmentOrganizer.putErrorInfoInBundle;
import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
@@ -206,7 +208,13 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr
mOrganizerPid = pid;
mAppThread = getAppThread(pid, mOrganizerUid);
for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) {
- mOrganizedTaskFragments.get(i).onTaskFragmentOrganizerRestarted(organizer);
+ final TaskFragment taskFragment = mOrganizedTaskFragments.get(i);
+ if (taskFragment.isAttached()
+ && taskFragment.getTopNonFinishingActivity() != null) {
+ taskFragment.onTaskFragmentOrganizerRestarted(organizer);
+ } else {
+ mOrganizedTaskFragments.remove(taskFragment);
+ }
}
try {
mOrganizer.asBinder().linkToDeath(this, 0 /*flags*/);
@@ -575,8 +583,29 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr
}
mCachedTaskFragmentOrganizerStates.remove(cachedState);
- outSavedState.putAll(cachedState.mSavedState);
cachedState.restore(organizer, pid);
+ outSavedState.putAll(cachedState.mSavedState);
+
+ // Collect the organized TfInfo and TfParentInfo in the system.
+ final ArrayList<TaskFragmentInfo> infos = new ArrayList<>();
+ final ArrayMap<Integer, Task> tasks = new ArrayMap<>();
+ final int fragmentCount = cachedState.mOrganizedTaskFragments.size();
+ for (int j = 0; j < fragmentCount; j++) {
+ final TaskFragment tf = cachedState.mOrganizedTaskFragments.get(j);
+ infos.add(tf.getTaskFragmentInfo());
+ if (!tasks.containsKey(tf.getTask().mTaskId)) {
+ tasks.put(tf.getTask().mTaskId, tf.getTask());
+ }
+ }
+ outSavedState.putParcelableArrayList(KEY_RESTORE_TASK_FRAGMENTS_INFO, infos);
+
+ final ArrayList<TaskFragmentParentInfo> parentInfos = new ArrayList<>();
+ for (int j = tasks.size() - 1; j >= 0; j--) {
+ parentInfos.add(tasks.valueAt(j).getTaskFragmentParentInfo());
+ }
+ outSavedState.putParcelableArrayList(KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO,
+ parentInfos);
+
mTaskFragmentOrganizerState.put(organizer.asBinder(), cachedState);
mPendingTaskFragmentEvents.put(organizer.asBinder(), new ArrayList<>());
return true;
diff --git a/services/core/java/com/android/server/wm/WindowManagerConstants.java b/services/core/java/com/android/server/wm/WindowManagerConstants.java
index 47c42f4292f1..e0f24d8bf447 100644
--- a/services/core/java/com/android/server/wm/WindowManagerConstants.java
+++ b/services/core/java/com/android/server/wm/WindowManagerConstants.java
@@ -34,7 +34,7 @@ import java.util.concurrent.Executor;
*/
final class WindowManagerConstants {
- /** The orientation of activity will be always "unspecified". */
+ /** The orientation of activity will be always "unspecified" except for game apps. */
private static final String KEY_IGNORE_ACTIVITY_ORIENTATION_REQUEST =
"ignore_activity_orientation_request";
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
index 5233f194d6c5..a0f1a559bb52 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
@@ -16,6 +16,7 @@
package com.android.server.appfunctions
import android.app.appfunctions.AppFunctionRuntimeMetadata
+import android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE
import android.app.appfunctions.AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema
import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema
import android.app.appsearch.AppSearchManager
@@ -42,7 +43,7 @@ class FutureAppSearchSessionTest {
fun clearData() {
val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
- val setSchemaRequest = SetSchemaRequest.Builder().build()
+ val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
it.setSchema(setSchemaRequest)
}
}
@@ -123,6 +124,38 @@ class FutureAppSearchSessionTest {
}
}
+ @Test
+ fun getByDocumentId() {
+ val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+ val setSchemaRequest =
+ SetSchemaRequest.Builder()
+ .addSchemas(
+ createParentAppFunctionRuntimeSchema(),
+ createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME)
+ )
+ .build()
+ val schema = session.setSchema(setSchemaRequest)
+ val appFunctionRuntimeMetadata =
+ AppFunctionRuntimeMetadata.Builder(TEST_PACKAGE_NAME, TEST_FUNCTION_ID, "").build()
+ val putDocumentsRequest: PutDocumentsRequest =
+ PutDocumentsRequest.Builder()
+ .addGenericDocuments(appFunctionRuntimeMetadata)
+ .build()
+ val putResult = session.put(putDocumentsRequest)
+
+ val genricDocument = session
+ .getByDocumentId(
+ /* documentId= */ "${TEST_PACKAGE_NAME}/${TEST_FUNCTION_ID}",
+ APP_FUNCTION_RUNTIME_NAMESPACE
+ )
+ .get()
+
+ val foundAppFunctionRuntimeMetadata = AppFunctionRuntimeMetadata(genricDocument)
+ assertThat(foundAppFunctionRuntimeMetadata.functionId).isEqualTo(TEST_FUNCTION_ID)
+ }
+ }
+
private companion object {
const val TEST_DB: String = "test_db"
const val TEST_PACKAGE_NAME: String = "test_pkg"
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt
new file mode 100644
index 000000000000..1fa55c7090aa
--- /dev/null
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.appfunctions
+
+import android.app.appfunctions.AppFunctionRuntimeMetadata
+import android.app.appfunctions.AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema
+import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema
+import android.app.appsearch.AppSearchManager
+import android.app.appsearch.AppSearchManager.SearchContext
+import android.app.appsearch.PutDocumentsRequest
+import android.app.appsearch.SetSchemaRequest
+import android.app.appsearch.observer.DocumentChangeInfo
+import android.app.appsearch.observer.ObserverCallback
+import android.app.appsearch.observer.ObserverSpec
+import android.app.appsearch.observer.SchemaChangeInfo
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.infra.AndroidFuture
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class FutureGlobalSearchSessionTest {
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+ private val appSearchManager = context.getSystemService(AppSearchManager::class.java)
+ private val testExecutor = MoreExecutors.directExecutor()
+
+ @Before
+ @After
+ fun clearData() {
+ val searchContext = SearchContext.Builder(TEST_DB).build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+ val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
+ it.setSchema(setSchemaRequest)
+ }
+ }
+
+ @Test
+ fun registerDocumentChangeObserverCallback() {
+ val packageObserverSpec: ObserverSpec =
+ ObserverSpec.Builder()
+ .addFilterSchemas(
+ AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(TEST_TARGET_PKG_NAME)
+ )
+ .build()
+ val settableDocumentChangeInfo: AndroidFuture<DocumentChangeInfo> = AndroidFuture()
+ val observer: ObserverCallback =
+ object : ObserverCallback {
+ override fun onSchemaChanged(changeInfo: SchemaChangeInfo) {}
+
+ override fun onDocumentChanged(changeInfo: DocumentChangeInfo) {
+ settableDocumentChangeInfo.complete(changeInfo)
+ }
+ }
+ val futureGlobalSearchSession = FutureGlobalSearchSession(appSearchManager, testExecutor)
+
+ val registerPackageObserver: Void? =
+ futureGlobalSearchSession
+ .registerObserverCallbackAsync(
+ TEST_TARGET_PKG_NAME,
+ packageObserverSpec,
+ testExecutor,
+ observer,
+ )
+ .get()
+ assertThat(registerPackageObserver).isNull()
+ // Trigger document change
+ val searchContext = SearchContext.Builder(TEST_DB).build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+ val setSchemaRequest =
+ SetSchemaRequest.Builder()
+ .addSchemas(
+ createParentAppFunctionRuntimeSchema(),
+ createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME),
+ )
+ .build()
+ val schema = session.setSchema(setSchemaRequest)
+ assertThat(schema.get()).isNotNull()
+ val appFunctionRuntimeMetadata =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, TEST_FUNCTION_ID, "")
+ .build()
+ val putDocumentsRequest: PutDocumentsRequest =
+ PutDocumentsRequest.Builder()
+ .addGenericDocuments(appFunctionRuntimeMetadata)
+ .build()
+ val putResult = session.put(putDocumentsRequest).get()
+ assertThat(putResult.isSuccess).isTrue()
+ }
+ assertThat(
+ settableDocumentChangeInfo
+ .get()
+ .changedDocumentIds
+ .contains(
+ AppFunctionRuntimeMetadata.getDocumentIdForAppFunction(
+ TEST_TARGET_PKG_NAME,
+ TEST_FUNCTION_ID,
+ )
+ )
+ )
+ .isTrue()
+ }
+
+ private companion object {
+ const val TEST_DB: String = "test_db"
+ const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests"
+ const val TEST_FUNCTION_ID: String = "print"
+ }
+}
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
new file mode 100644
index 000000000000..1061da28f799
--- /dev/null
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -0,0 +1,296 @@
+/*
+ * 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.appfunctions
+
+import android.app.appfunctions.AppFunctionRuntimeMetadata
+import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID
+import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME
+import android.app.appsearch.AppSearchManager
+import android.app.appsearch.AppSearchManager.SearchContext
+import android.app.appsearch.PutDocumentsRequest
+import android.app.appsearch.SearchSpec
+import android.app.appsearch.SetSchemaRequest
+import android.util.ArrayMap
+import android.util.ArraySet
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MetadataSyncAdapterTest {
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+ private val appSearchManager = context.getSystemService(AppSearchManager::class.java)
+ private val testExecutor = MoreExecutors.directExecutor()
+
+ @Before
+ @After
+ fun clearData() {
+ val searchContext = SearchContext.Builder(TEST_DB).build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+ val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
+ it.setSchema(setSchemaRequest)
+ }
+ }
+
+ @Test
+ fun getPackageToFunctionIdMap() {
+ val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build()
+ val functionRuntimeMetadata =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build()
+ val setSchemaRequest =
+ SetSchemaRequest.Builder()
+ .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema())
+ .addSchemas(
+ AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+ )
+ .build()
+ val putDocumentsRequest: PutDocumentsRequest =
+ PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+ val setSchemaResponse = it.setSchema(setSchemaRequest).get()
+ assertThat(setSchemaResponse).isNotNull()
+ val appSearchBatchResult = it.put(putDocumentsRequest).get()
+ assertThat(appSearchBatchResult.isSuccess).isTrue()
+ }
+
+ val metadataSyncAdapter =
+ MetadataSyncAdapter(
+ testExecutor,
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
+ )
+ val searchSpec: SearchSpec =
+ SearchSpec.Builder()
+ .addFilterSchemas(
+ AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
+ AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+ .schemaType,
+ )
+ .build()
+ val packageToFunctionIdMap =
+ metadataSyncAdapter.getPackageToFunctionIdMap(
+ "",
+ searchSpec,
+ PROPERTY_FUNCTION_ID,
+ PROPERTY_PACKAGE_NAME,
+ )
+
+ assertThat(packageToFunctionIdMap).isNotNull()
+ assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunctionId")
+ }
+
+ @Test
+ fun getPackageToFunctionIdMap_multipleDocuments() {
+ val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build()
+ val functionRuntimeMetadata =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build()
+ val functionRuntimeMetadata1 =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId1", "").build()
+ val functionRuntimeMetadata2 =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId2", "").build()
+ val functionRuntimeMetadata3 =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId3", "").build()
+ val setSchemaRequest =
+ SetSchemaRequest.Builder()
+ .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema())
+ .addSchemas(
+ AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+ )
+ .build()
+ val putDocumentsRequest: PutDocumentsRequest =
+ PutDocumentsRequest.Builder()
+ .addGenericDocuments(
+ functionRuntimeMetadata,
+ functionRuntimeMetadata1,
+ functionRuntimeMetadata2,
+ functionRuntimeMetadata3,
+ )
+ .build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+ val setSchemaResponse = it.setSchema(setSchemaRequest).get()
+ assertThat(setSchemaResponse).isNotNull()
+ val appSearchBatchResult = it.put(putDocumentsRequest).get()
+ assertThat(appSearchBatchResult.isSuccess).isTrue()
+ }
+
+ val metadataSyncAdapter =
+ MetadataSyncAdapter(
+ testExecutor,
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
+ )
+ val searchSpec: SearchSpec =
+ SearchSpec.Builder()
+ .setResultCountPerPage(1)
+ .addFilterSchemas(
+ AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
+ AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+ .schemaType,
+ )
+ .build()
+ val packageToFunctionIdMap =
+ metadataSyncAdapter.getPackageToFunctionIdMap(
+ "",
+ searchSpec,
+ PROPERTY_FUNCTION_ID,
+ PROPERTY_PACKAGE_NAME,
+ )
+
+ assertThat(packageToFunctionIdMap).isNotNull()
+ assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME])
+ .containsExactly(
+ "testFunctionId",
+ "testFunctionId1",
+ "testFunctionId2",
+ "testFunctionId3",
+ )
+ }
+
+ @Test
+ fun getAddedFunctionsDiffMap_noDiff() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ staticPackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> =
+ ArrayMap(staticPackageToFunctionMap)
+
+ val addedFunctionsDiffMap =
+ MetadataSyncAdapter.getAddedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+ }
+
+ @Test
+ fun getAddedFunctionsDiffMap_addedFunction() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ staticPackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1", "testFunction2")))
+ )
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ runtimePackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+
+ val addedFunctionsDiffMap =
+ MetadataSyncAdapter.getAddedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(addedFunctionsDiffMap.size).isEqualTo(1)
+ assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction2")
+ }
+
+ @Test
+ fun getAddedFunctionsDiffMap_addedFunctionNewPackage() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ staticPackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+
+ val addedFunctionsDiffMap =
+ MetadataSyncAdapter.getAddedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(addedFunctionsDiffMap.size).isEqualTo(1)
+ assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1")
+ }
+
+ @Test
+ fun getAddedFunctionsDiffMap_removedFunction() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ runtimePackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+
+ val addedFunctionsDiffMap =
+ MetadataSyncAdapter.getAddedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+ }
+
+ @Test
+ fun getRemovedFunctionsDiffMap_noDiff() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ staticPackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> =
+ ArrayMap(staticPackageToFunctionMap)
+
+ val removedFunctionsDiffMap =
+ MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+ }
+
+ @Test
+ fun getRemovedFunctionsDiffMap_removedFunction() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ runtimePackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+
+ val removedFunctionsDiffMap =
+ MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(removedFunctionsDiffMap.size).isEqualTo(1)
+ assertThat(removedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1")
+ }
+
+ @Test
+ fun getRemovedFunctionsDiffMap_addedFunction() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ staticPackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+
+ val removedFunctionsDiffMap =
+ MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+ }
+
+ private companion object {
+ const val TEST_DB: String = "test_db"
+ const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests"
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java
new file mode 100644
index 000000000000..c8e4f89aaee6
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.job;
+
+import static android.app.job.Flags.FLAG_CLEANUP_EMPTY_JOBS;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+
+import android.app.job.IJobCallback;
+import android.app.job.JobParameters;
+import android.net.Uri;
+import android.os.Parcel;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+public class JobParametersTest {
+ private static final String TAG = JobParametersTest.class.getSimpleName();
+ private static final int TEST_JOB_ID_1 = 123;
+ private static final String TEST_NAMESPACE = "TEST_NAMESPACE";
+ private static final String TEST_DEBUG_STOP_REASON = "TEST_DEBUG_STOP_REASON";
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private MockitoSession mMockingSession;
+ @Mock private Parcel mMockParcel;
+ @Mock private IJobCallback.Stub mMockJobCallbackStub;
+
+ @Before
+ public void setUp() throws Exception {
+ mMockingSession =
+ mockitoSession().initMocks(this).strictness(Strictness.LENIENT).startMocking();
+ }
+
+ @After
+ public void tearDown() {
+ if (mMockingSession != null) {
+ mMockingSession.finishMocking();
+ }
+
+ when(mMockParcel.readInt())
+ .thenReturn(TEST_JOB_ID_1) // Job ID
+ .thenReturn(0) // No clip data
+ .thenReturn(0) // No deadline expired
+ .thenReturn(0) // No network
+ .thenReturn(0) // No stop reason
+ .thenReturn(0); // Internal stop reason
+ when(mMockParcel.readString())
+ .thenReturn(TEST_NAMESPACE) // Job namespace
+ .thenReturn(TEST_DEBUG_STOP_REASON); // Debug stop reason
+ when(mMockParcel.readPersistableBundle()).thenReturn(null);
+ when(mMockParcel.readBundle()).thenReturn(null);
+ when(mMockParcel.readStrongBinder()).thenReturn(mMockJobCallbackStub);
+ when(mMockParcel.readBoolean())
+ .thenReturn(false) // expedited
+ .thenReturn(false); // user initiated
+ when(mMockParcel.createTypedArray(any())).thenReturn(new Uri[0]);
+ when(mMockParcel.createStringArray()).thenReturn(new String[0]);
+ }
+
+ /**
+ * Test to verify that the JobParameters created using Non-Parcelable constructor has not
+ * cleaner attached
+ */
+ @Test
+ public void testJobParametersNonParcelableConstructor_noCleaner() {
+ JobParameters jobParameters =
+ new JobParameters(
+ null,
+ TEST_NAMESPACE,
+ TEST_JOB_ID_1,
+ null,
+ null,
+ null,
+ 0,
+ false,
+ false,
+ false,
+ null,
+ null,
+ null);
+
+ // Verify that cleaner is not registered
+ assertThat(jobParameters.getCleanable()).isNull();
+ assertThat(jobParameters.getJobCleanupCallback()).isNull();
+ }
+
+ /**
+ * Test to verify that the JobParameters created using Parcelable constructor has not cleaner
+ * attached
+ */
+ @Test
+ public void testJobParametersParcelableConstructor_noCleaner() {
+ JobParameters jobParameters = JobParameters.CREATOR.createFromParcel(mMockParcel);
+
+ // Verify that cleaner is not registered
+ assertThat(jobParameters.getCleanable()).isNull();
+ assertThat(jobParameters.getJobCleanupCallback()).isNull();
+ }
+
+ /** Test to verify that the JobParameters Cleaner is disabled */
+ @RequiresFlagsEnabled(FLAG_CLEANUP_EMPTY_JOBS)
+ @Test
+ public void testCleanerWithLeakedJobCleanerDisabled_flagCleanupEmptyJobsEnabled() {
+ // Inject real JobCallbackCleanup
+ JobParameters jobParameters = JobParameters.CREATOR.createFromParcel(mMockParcel);
+
+ // Enable the cleaner
+ jobParameters.enableCleaner();
+
+ // Verify the cleaner is enabled
+ assertThat(jobParameters.getCleanable()).isNotNull();
+ assertThat(jobParameters.getJobCleanupCallback()).isNotNull();
+ assertThat(jobParameters.getJobCleanupCallback().isCleanerEnabled()).isTrue();
+
+ // Disable the cleaner
+ jobParameters.disableCleaner();
+
+ // Verify the cleaner is disabled
+ assertThat(jobParameters.getCleanable()).isNull();
+ assertThat(jobParameters.getJobCleanupCallback()).isNull();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index 6b8e414255cd..b4b36125f770 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -558,7 +558,9 @@ public class BiometricServiceTest {
waitForIdle();
verify(mReceiver1).onError(
eq(BiometricAuthenticator.TYPE_NONE),
- eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE),
+ eq(Flags.mandatoryBiometrics()
+ ? BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS
+ : BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE),
eq(0 /* vendorCode */));
// Enrolled, not disabled in settings, user requires confirmation in settings
@@ -1450,7 +1452,9 @@ public class BiometricServiceTest {
}
@Test
- public void testCanAuthenticate_whenBiometricsNotEnabledForApps() throws Exception {
+ @RequiresFlagsDisabled(Flags.FLAG_MANDATORY_BIOMETRICS)
+ public void testCanAuthenticate_whenBiometricsNotEnabledForApps_returnsHardwareUnavailable()
+ throws Exception {
setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false);
when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
@@ -1468,6 +1472,25 @@ public class BiometricServiceTest {
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS)
+ public void testCanAuthenticate_whenBiometricsNotEnabledForApps() throws Exception {
+ setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
+ when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false);
+ when(mTrustManager.isDeviceSecure(anyInt(), anyInt()))
+ .thenReturn(true);
+
+ // When only biometric is requested
+ int authenticators = Authenticators.BIOMETRIC_STRONG;
+ assertEquals(BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS,
+ invokeCanAuthenticate(mBiometricService, authenticators));
+
+ // When credential and biometric are requested
+ authenticators = Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL;
+ assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
+ invokeCanAuthenticate(mBiometricService, authenticators));
+ }
+
+ @Test
public void testCanAuthenticate_whenNoBiometricSensor() throws Exception {
mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider);
mBiometricService.onStart();
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
index 760d38e855a6..b758f57ff407 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
@@ -20,6 +20,7 @@ import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NO
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
+import static android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS;
import static com.android.server.biometrics.sensors.LockoutTracker.LOCKOUT_NONE;
@@ -266,6 +267,29 @@ public class PreAuthInfoTest {
@Test
@RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS)
+ public void testCalculateByPriority()
+ throws Exception {
+ when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(false);
+ when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false);
+
+ BiometricSensor faceSensor = getFaceSensor();
+ BiometricSensor fingerprintSensor = getFingerprintSensor();
+ PromptInfo promptInfo = new PromptInfo();
+ promptInfo.setConfirmationRequested(false /* requireConfirmation */);
+ promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
+ promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */);
+ PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
+ mSettingObserver, List.of(faceSensor, fingerprintSensor),
+ 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
+ false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
+
+ assertThat(preAuthInfo.eligibleSensors).hasSize(0);
+ assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo(
+ BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS)
public void testMandatoryBiometricsNegativeButtonText_whenSet()
throws Exception {
when(mTrustManager.isInSignificantPlace()).thenReturn(false);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
index 4a199738cccd..1890879da69d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
@@ -39,6 +39,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
+import android.app.KeyguardManager;
import android.app.UiModeManager;
import android.app.WallpaperManager;
import android.content.BroadcastReceiver;
@@ -78,6 +79,7 @@ public class DefaultDeviceEffectsApplierTest {
private DefaultDeviceEffectsApplier mApplier;
@Mock PowerManager mPowerManager;
@Mock ColorDisplayManager mColorDisplayManager;
+ @Mock KeyguardManager mKeyguardManager;
@Mock UiModeManager mUiModeManager;
@Mock WallpaperManager mWallpaperManager;
@@ -87,6 +89,7 @@ public class DefaultDeviceEffectsApplierTest {
mContext = spy(new TestableContext(InstrumentationRegistry.getContext(), null));
mContext.addMockSystemService(PowerManager.class, mPowerManager);
mContext.addMockSystemService(ColorDisplayManager.class, mColorDisplayManager);
+ mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager);
mContext.addMockSystemService(UiModeManager.class, mUiModeManager);
mContext.addMockSystemService(WallpaperManager.class, mWallpaperManager);
when(mWallpaperManager.isWallpaperSupported()).thenReturn(true);
@@ -311,6 +314,22 @@ public class DefaultDeviceEffectsApplierTest {
}
@Test
+ @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+ public void apply_nightModeWithScreenOnAndKeyguardShowing_appliedImmediately(
+ @TestParameter ZenChangeOrigin origin) {
+
+ when(mPowerManager.isInteractive()).thenReturn(true);
+ when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+
+ mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(),
+ origin.value());
+
+ // Effect was applied, and no broadcast receiver was registered.
+ verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT));
+ verify(mContext, never()).registerReceiver(any(), any(), anyInt());
+ }
+
+ @Test
@TestParameters({"{origin: ORIGIN_USER_IN_SYSTEMUI}", "{origin: ORIGIN_USER_IN_APP}",
"{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}"})
public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin(
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index e694c0b4afc1..536dcfb3579c 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -42,7 +42,6 @@ import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
import android.app.ActivityManager;
import android.app.AppOpsManager;
@@ -135,15 +134,13 @@ public class PhoneWindowManagerTests {
doNothing().when(mPhoneWindowManager).initializeHdmiState();
final boolean[] isScreenTurnedOff = { false };
final DisplayPolicy displayPolicy = mock(DisplayPolicy.class);
- doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff();
+ doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff(
+ anyBoolean());
doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnEarly();
doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnFully();
mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy;
mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class);
- final ActivityTaskManagerInternal.SleepTokenAcquirer tokenAcquirer =
- mock(ActivityTaskManagerInternal.SleepTokenAcquirer.class);
- doReturn(tokenAcquirer).when(mAtmInternal).createSleepTokenAcquirer(anyString());
final PowerManager pm = mock(PowerManager.class);
doReturn(true).when(pm).isInteractive();
doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE));
@@ -155,9 +152,8 @@ public class PhoneWindowManagerTests {
assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
// Skip sleep-token for non-sleep-screen-off.
- clearInvocations(tokenAcquirer);
mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
- verify(tokenAcquirer, never()).acquire(anyInt());
+ verify(displayPolicy).screenTurnedOff(false /* acquireSleepToken */);
assertThat(isScreenTurnedOff[0]).isTrue();
// Apply sleep-token for sleep-screen-off.
@@ -165,21 +161,10 @@ public class PhoneWindowManagerTests {
mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isTrue();
mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
- verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY));
+ verify(displayPolicy).screenTurnedOff(true /* acquireSleepToken */);
mPhoneWindowManager.finishedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
-
- // Simulate unexpected reversed order: screenTurnedOff -> startedGoingToSleep. The sleep
- // token can still be acquired.
- isScreenTurnedOff[0] = false;
- clearInvocations(tokenAcquirer);
- mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
- verify(tokenAcquirer, never()).acquire(anyInt());
- assertThat(displayPolicy.isScreenOnEarly()).isFalse();
- assertThat(displayPolicy.isScreenOnFully()).isFalse();
- mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
- verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY));
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 1e035dab3c5e..e2e76d6ef4e5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3231,7 +3231,7 @@ public class ActivityRecordTests extends WindowTestsBase {
mDisplayContent.mOpeningApps.remove(activity);
mDisplayContent.mClosingApps.remove(activity);
activity.commitVisibility(false /* visible */, false /* performLayout */);
- mDisplayContent.getDisplayPolicy().screenTurnedOff();
+ mDisplayContent.getDisplayPolicy().screenTurnedOff(false /* acquireSleepToken */);
final KeyguardController controller = mSupervisor.getKeyguardController();
doReturn(true).when(controller).isKeyguardGoingAway(anyInt());
activity.setVisibility(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index caeb41c78967..f32a234f3e40 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -284,11 +284,11 @@ public class DisplayPolicyTests extends WindowTestsBase {
final DisplayPolicy policy = mDisplayContent.getDisplayPolicy();
policy.addWindowLw(mNotificationShadeWindow, mNotificationShadeWindow.mAttrs);
- policy.screenTurnedOff();
+ policy.screenTurnedOff(false /* acquireSleepToken */);
policy.setAwake(false);
policy.screenTurningOn(null /* screenOnListener */);
assertTrue(wpc.isShowingUiWhileDozing());
- policy.screenTurnedOff();
+ policy.screenTurnedOff(false /* acquireSleepToken */);
assertFalse(wpc.isShowingUiWhileDozing());
policy.screenTurningOn(null /* screenOnListener */);
@@ -393,7 +393,7 @@ public class DisplayPolicyTests extends WindowTestsBase {
info.logicalWidth, info.logicalHeight).mConfigFrame);
// If screen is not fully turned on, then the cache should be preserved.
- displayPolicy.screenTurnedOff();
+ displayPolicy.screenTurnedOff(false /* acquireSleepToken */);
final TransitionController transitionController = mDisplayContent.mTransitionController;
spyOn(transitionController);
doReturn(true).when(transitionController).isCollecting();
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 cc1805aa933c..fd959b950e16 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -242,7 +242,7 @@ public class TaskFragmentTest extends WindowTestsBase {
final Rect relStartBounds = new Rect(mTaskFragment.getRelativeEmbeddedBounds());
final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
- displayPolicy.screenTurnedOff();
+ displayPolicy.screenTurnedOff(false /* acquireSleepToken */);
assertFalse(mTaskFragment.okToAnimate());
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index 112471b2af57..c85374e0b660 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -376,7 +376,8 @@ interface ITelecomService {
*/
void requestLogMark(in String message);
- void setTestPhoneAcctSuggestionComponent(String flattenedComponentName);
+ void setTestPhoneAcctSuggestionComponent(String flattenedComponentName,
+ in UserHandle userHandle);
void setTestDefaultCallScreeningApp(String packageName);
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 3944b8e0d0cc..284e2bd8aa6c 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -420,6 +420,14 @@ public final class SatelliteManager {
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
public static final int SATELLITE_RESULT_DISABLE_IN_PROGRESS = 28;
+ /**
+ * Enabling satellite is in progress.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public static final int SATELLITE_RESULT_ENABLE_IN_PROGRESS = 29;
+
/** @hide */
@IntDef(prefix = {"SATELLITE_RESULT_"}, value = {
SATELLITE_RESULT_SUCCESS,
@@ -450,7 +458,8 @@ public final class SatelliteManager {
SATELLITE_RESULT_LOCATION_DISABLED,
SATELLITE_RESULT_LOCATION_NOT_AVAILABLE,
SATELLITE_RESULT_EMERGENCY_CALL_IN_PROGRESS,
- SATELLITE_RESULT_DISABLE_IN_PROGRESS
+ SATELLITE_RESULT_DISABLE_IN_PROGRESS,
+ SATELLITE_RESULT_ENABLE_IN_PROGRESS
})
@Retention(RetentionPolicy.SOURCE)
public @interface SatelliteResult {}
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index ad0ef1b3a37f..0f08be215033 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -26,7 +26,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.graphics.Color;
import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
import android.testing.TestableContext;
import android.view.MotionEvent;
import android.view.View;
@@ -40,6 +42,8 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.cts.input.MotionEventBuilder;
import com.android.cts.input.PointerBuilder;
+import com.android.server.input.TouchpadFingerState;
+import com.android.server.input.TouchpadHardwareState;
import org.junit.Before;
import org.junit.Test;
@@ -289,4 +293,36 @@ public class TouchpadDebugViewTest {
assertEquals(initialX, mWindowLayoutParamsCaptor.getValue().x);
assertEquals(initialY, mWindowLayoutParamsCaptor.getValue().y);
}
+
+ @Test
+ public void testTouchpadClick() {
+ View child;
+
+ mTouchpadDebugView.updateHardwareState(
+ new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0,
+ new TouchpadFingerState[0]));
+
+ for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+ child = mTouchpadDebugView.getChildAt(i);
+ assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
+ }
+
+ mTouchpadDebugView.updateHardwareState(
+ new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
+ new TouchpadFingerState[0]));
+
+ for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+ child = mTouchpadDebugView.getChildAt(i);
+ assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.RED);
+ }
+
+ mTouchpadDebugView.updateHardwareState(
+ new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0,
+ new TouchpadFingerState[0]));
+
+ for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+ child = mTouchpadDebugView.getChildAt(i);
+ assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
+ }
+ }
}
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index 189de6bdb44a..e841d9ea0880 100644
--- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -399,16 +399,12 @@ public class PerfettoProtoLogImplTest {
TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
- var assertion = assertThrows(RuntimeException.class, () -> implSpy.log(
- LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
- new Object[]{5}));
+ implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+ new Object[]{5});
- verify(implSpy, never()).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
- LogLevel.INFO), any());
+ verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+ LogLevel.INFO), eq("UNKNOWN MESSAGE args = (5)"));
verify(sReader).getViewerString(eq(1234L));
-
- Truth.assertThat(assertion).hasMessageThat()
- .contains("Failed to get log message with hash 1234 and args (5)");
}
@Test
@@ -866,19 +862,6 @@ public class PerfettoProtoLogImplTest {
.isEqualTo("This message should also be logged 567");
}
- @Test
- public void throwsOnLogToLogcatForProcessedMessageMissingLoadedDefinition() {
- TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
- var protolog = new PerfettoProtoLogImpl(TestProtoLogGroup.values());
-
- var exception = assertThrows(RuntimeException.class, () -> {
- protolog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 123, 0, new Object[0]);
- });
-
- Truth.assertThat(exception).hasMessageThat()
- .contains("Failed to get log message with hash 123");
- }
-
private enum TestProtoLogGroup implements IProtoLogGroup {
TEST_GROUP(true, true, false, "TEST_TAG");
diff --git a/tools/aapt/Package.cpp b/tools/aapt/Package.cpp
index 5e0f87f0dcaf..60c4bf5c4131 100644
--- a/tools/aapt/Package.cpp
+++ b/tools/aapt/Package.cpp
@@ -292,13 +292,12 @@ bool processFile(Bundle* bundle, ZipFile* zip,
}
if (!hasData) {
const String8& srcName = file->getSourceFile();
- time_t fileModWhen;
- fileModWhen = getFileModDate(srcName.c_str());
- if (fileModWhen == (time_t) -1) { // file existence tested earlier,
- return false; // not expecting an error here
+ auto fileModWhen = getFileModDate(srcName.c_str());
+ if (fileModWhen == kInvalidModDate) { // file existence tested earlier,
+ return false; // not expecting an error here
}
-
- if (fileModWhen > entry->getModWhen()) {
+
+ if (toTimeT(fileModWhen) > entry->getModWhen()) {
// mark as deleted so add() will succeed
if (bundle->getVerbose()) {
printf(" (removing old '%s')\n", storageName.c_str());
diff --git a/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
index 24d203fd1116..f5af99ec39ac 100644
--- a/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
+++ b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
@@ -24,20 +24,31 @@ import com.intellij.psi.PsiReferenceList
import org.jetbrains.uast.UMethod
/**
- * Given a UMethod, determine if this method is the entrypoint to an interface
- * generated by AIDL, returning the interface name if so, otherwise returning
- * null
+ * Given a UMethod, determine if this method is the entrypoint to an interface generated by AIDL,
+ * returning the interface name if so, otherwise returning null.
*/
fun getContainingAidlInterface(context: JavaContext, node: UMethod): String? {
+ return containingAidlInterfacePsiClass(context, node)?.name
+}
+
+/**
+ * Given a UMethod, determine if this method is the entrypoint to an interface generated by AIDL,
+ * returning the fully qualified interface name if so, otherwise returning null.
+ */
+fun getContainingAidlInterfaceQualified(context: JavaContext, node: UMethod): String? {
+ return containingAidlInterfacePsiClass(context, node)?.qualifiedName
+}
+
+private fun containingAidlInterfacePsiClass(context: JavaContext, node: UMethod): PsiClass? {
val containingStub = containingStub(context, node) ?: return null
val superMethod = node.findSuperMethods(containingStub)
if (superMethod.isEmpty()) return null
- return containingStub.containingClass?.name
+ return containingStub.containingClass
}
-/* Returns the containing Stub class if any. This is not sufficient to infer
- * that the method itself extends an AIDL generated method. See
- * getContainingAidlInterface for that purpose.
+/**
+ * Returns the containing Stub class if any. This is not sufficient to infer that the method itself
+ * extends an AIDL generated method. See getContainingAidlInterface for that purpose.
*/
fun containingStub(context: JavaContext, node: UMethod?): PsiClass? {
var superClass = node?.containingClass?.superClass
@@ -48,7 +59,7 @@ fun containingStub(context: JavaContext, node: UMethod?): PsiClass? {
return null
}
-private fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean {
+fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean {
if (psiClass == null) return false
if (psiClass.name != "Stub") return false
if (!context.evaluator.isStatic(psiClass)) return false
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
new file mode 100644
index 000000000000..8777712b0f04
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+/**
+ * The exemptAidlInterfaces set was generated by running ExemptAidlInterfacesGenerator on the
+ * entire source tree. To reproduce the results, run generate-exempt-aidl-interfaces.sh
+ * located in tools/lint/utils.
+ *
+ * TODO: b/363248121 - Use the exemptAidlInterfaces set inside PermissionAnnotationDetector when it
+ * gets migrated to a global lint check.
+ */
+val exemptAidlInterfaces = setOf(
+ "android.accessibilityservice.IAccessibilityServiceConnection",
+ "android.accessibilityservice.IBrailleDisplayConnection",
+ "android.accounts.IAccountAuthenticatorResponse",
+ "android.accounts.IAccountManager",
+ "android.accounts.IAccountManagerResponse",
+ "android.adservices.adid.IAdIdProviderService",
+ "android.adservices.adid.IAdIdService",
+ "android.adservices.adid.IGetAdIdCallback",
+ "android.adservices.adid.IGetAdIdProviderCallback",
+ "android.adservices.adselection.AdSelectionCallback",
+ "android.adservices.adselection.AdSelectionOverrideCallback",
+ "android.adservices.adselection.AdSelectionService",
+ "android.adservices.adselection.GetAdSelectionDataCallback",
+ "android.adservices.adselection.PersistAdSelectionResultCallback",
+ "android.adservices.adselection.ReportImpressionCallback",
+ "android.adservices.adselection.ReportInteractionCallback",
+ "android.adservices.adselection.SetAppInstallAdvertisersCallback",
+ "android.adservices.adselection.UpdateAdCounterHistogramCallback",
+ "android.adservices.appsetid.IAppSetIdProviderService",
+ "android.adservices.appsetid.IAppSetIdService",
+ "android.adservices.appsetid.IGetAppSetIdCallback",
+ "android.adservices.appsetid.IGetAppSetIdProviderCallback",
+ "android.adservices.cobalt.IAdServicesCobaltUploadService",
+ "android.adservices.common.IAdServicesCommonCallback",
+ "android.adservices.common.IAdServicesCommonService",
+ "android.adservices.common.IAdServicesCommonStatesCallback",
+ "android.adservices.common.IEnableAdServicesCallback",
+ "android.adservices.common.IUpdateAdIdCallback",
+ "android.adservices.customaudience.CustomAudienceOverrideCallback",
+ "android.adservices.customaudience.FetchAndJoinCustomAudienceCallback",
+ "android.adservices.customaudience.ICustomAudienceCallback",
+ "android.adservices.customaudience.ICustomAudienceService",
+ "android.adservices.customaudience.ScheduleCustomAudienceUpdateCallback",
+ "android.adservices.extdata.IAdServicesExtDataStorageService",
+ "android.adservices.extdata.IGetAdServicesExtDataCallback",
+ "android.adservices.measurement.IMeasurementApiStatusCallback",
+ "android.adservices.measurement.IMeasurementCallback",
+ "android.adservices.measurement.IMeasurementService",
+ "android.adservices.ondevicepersonalization.aidl.IDataAccessService",
+ "android.adservices.ondevicepersonalization.aidl.IDataAccessServiceCallback",
+ "android.adservices.ondevicepersonalization.aidl.IExecuteCallback",
+ "android.adservices.ondevicepersonalization.aidl.IFederatedComputeCallback",
+ "android.adservices.ondevicepersonalization.aidl.IFederatedComputeService",
+ "android.adservices.ondevicepersonalization.aidl.IIsolatedModelService",
+ "android.adservices.ondevicepersonalization.aidl.IIsolatedModelServiceCallback",
+ "android.adservices.ondevicepersonalization.aidl.IIsolatedService",
+ "android.adservices.ondevicepersonalization.aidl.IIsolatedServiceCallback",
+ "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigService",
+ "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigServiceCallback",
+ "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationDebugService",
+ "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationManagingService",
+ "android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback",
+ "android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback",
+ "android.adservices.shell.IShellCommand",
+ "android.adservices.shell.IShellCommandCallback",
+ "android.adservices.signals.IProtectedSignalsService",
+ "android.adservices.signals.UpdateSignalsCallback",
+ "android.adservices.topics.IGetTopicsCallback",
+ "android.adservices.topics.ITopicsService",
+ "android.app.admin.IDevicePolicyManager",
+ "android.app.adservices.IAdServicesManager",
+ "android.app.ambientcontext.IAmbientContextManager",
+ "android.app.ambientcontext.IAmbientContextObserver",
+ "android.app.appsearch.aidl.IAppFunctionService",
+ "android.app.appsearch.aidl.IAppSearchBatchResultCallback",
+ "android.app.appsearch.aidl.IAppSearchManager",
+ "android.app.appsearch.aidl.IAppSearchObserverProxy",
+ "android.app.appsearch.aidl.IAppSearchResultCallback",
+ "android.app.backup.IBackupCallback",
+ "android.app.backup.IBackupManager",
+ "android.app.backup.IRestoreSession",
+ "android.app.blob.IBlobCommitCallback",
+ "android.app.blob.IBlobStoreManager",
+ "android.app.blob.IBlobStoreSession",
+ "android.app.contentsuggestions.IContentSuggestionsManager",
+ "android.app.contextualsearch.IContextualSearchManager",
+ "android.app.ecm.IEnhancedConfirmationManager",
+ "android.apphibernation.IAppHibernationService",
+ "android.app.IActivityClientController",
+ "android.app.IActivityController",
+ "android.app.IActivityTaskManager",
+ "android.app.IAlarmCompleteListener",
+ "android.app.IAlarmListener",
+ "android.app.IAlarmManager",
+ "android.app.IApplicationThread",
+ "android.app.IAppTask",
+ "android.app.IAppTraceRetriever",
+ "android.app.IAssistDataReceiver",
+ "android.app.IForegroundServiceObserver",
+ "android.app.IGameManagerService",
+ "android.app.IGrammaticalInflectionManager",
+ "android.app.ILocaleManager",
+ "android.app.INotificationManager",
+ "android.app.IParcelFileDescriptorRetriever",
+ "android.app.IProcessObserver",
+ "android.app.ISearchManager",
+ "android.app.IStopUserCallback",
+ "android.app.ITaskStackListener",
+ "android.app.IUiModeManager",
+ "android.app.IUriGrantsManager",
+ "android.app.IUserSwitchObserver",
+ "android.app.IWallpaperManager",
+ "android.app.job.IJobCallback",
+ "android.app.job.IJobScheduler",
+ "android.app.job.IJobService",
+ "android.app.ondeviceintelligence.IDownloadCallback",
+ "android.app.ondeviceintelligence.IFeatureCallback",
+ "android.app.ondeviceintelligence.IFeatureDetailsCallback",
+ "android.app.ondeviceintelligence.IListFeaturesCallback",
+ "android.app.ondeviceintelligence.IOnDeviceIntelligenceManager",
+ "android.app.ondeviceintelligence.IProcessingSignal",
+ "android.app.ondeviceintelligence.IResponseCallback",
+ "android.app.ondeviceintelligence.IStreamingResponseCallback",
+ "android.app.ondeviceintelligence.ITokenInfoCallback",
+ "android.app.people.IPeopleManager",
+ "android.app.pinner.IPinnerService",
+ "android.app.prediction.IPredictionManager",
+ "android.app.role.IOnRoleHoldersChangedListener",
+ "android.app.role.IRoleController",
+ "android.app.role.IRoleManager",
+ "android.app.sdksandbox.ILoadSdkCallback",
+ "android.app.sdksandbox.IRequestSurfacePackageCallback",
+ "android.app.sdksandbox.ISdkSandboxManager",
+ "android.app.sdksandbox.ISdkSandboxProcessDeathCallback",
+ "android.app.sdksandbox.ISdkToServiceCallback",
+ "android.app.sdksandbox.ISharedPreferencesSyncCallback",
+ "android.app.sdksandbox.IUnloadSdkCallback",
+ "android.app.sdksandbox.testutils.testscenario.ISdkSandboxTestExecutor",
+ "android.app.search.ISearchUiManager",
+ "android.app.slice.ISliceManager",
+ "android.app.smartspace.ISmartspaceManager",
+ "android.app.timedetector.ITimeDetectorService",
+ "android.app.timezonedetector.ITimeZoneDetectorService",
+ "android.app.trust.ITrustManager",
+ "android.app.usage.IStorageStatsManager",
+ "android.app.usage.IUsageStatsManager",
+ "android.app.wallpapereffectsgeneration.IWallpaperEffectsGenerationManager",
+ "android.app.wearable.IWearableSensingCallback",
+ "android.app.wearable.IWearableSensingManager",
+ "android.bluetooth.IBluetooth",
+ "android.bluetooth.IBluetoothA2dp",
+ "android.bluetooth.IBluetoothA2dpSink",
+ "android.bluetooth.IBluetoothActivityEnergyInfoListener",
+ "android.bluetooth.IBluetoothAvrcpController",
+ "android.bluetooth.IBluetoothCallback",
+ "android.bluetooth.IBluetoothConnectionCallback",
+ "android.bluetooth.IBluetoothCsipSetCoordinator",
+ "android.bluetooth.IBluetoothCsipSetCoordinatorLockCallback",
+ "android.bluetooth.IBluetoothGatt",
+ "android.bluetooth.IBluetoothGattCallback",
+ "android.bluetooth.IBluetoothGattServerCallback",
+ "android.bluetooth.IBluetoothHapClient",
+ "android.bluetooth.IBluetoothHapClientCallback",
+ "android.bluetooth.IBluetoothHeadset",
+ "android.bluetooth.IBluetoothHeadsetClient",
+ "android.bluetooth.IBluetoothHearingAid",
+ "android.bluetooth.IBluetoothHidDevice",
+ "android.bluetooth.IBluetoothHidDeviceCallback",
+ "android.bluetooth.IBluetoothHidHost",
+ "android.bluetooth.IBluetoothLeAudio",
+ "android.bluetooth.IBluetoothLeAudioCallback",
+ "android.bluetooth.IBluetoothLeBroadcastAssistant",
+ "android.bluetooth.IBluetoothLeBroadcastAssistantCallback",
+ "android.bluetooth.IBluetoothLeBroadcastCallback",
+ "android.bluetooth.IBluetoothLeCallControl",
+ "android.bluetooth.IBluetoothLeCallControlCallback",
+ "android.bluetooth.IBluetoothManager",
+ "android.bluetooth.IBluetoothManagerCallback",
+ "android.bluetooth.IBluetoothMap",
+ "android.bluetooth.IBluetoothMapClient",
+ "android.bluetooth.IBluetoothMcpServiceManager",
+ "android.bluetooth.IBluetoothMetadataListener",
+ "android.bluetooth.IBluetoothOobDataCallback",
+ "android.bluetooth.IBluetoothPan",
+ "android.bluetooth.IBluetoothPanCallback",
+ "android.bluetooth.IBluetoothPbap",
+ "android.bluetooth.IBluetoothPbapClient",
+ "android.bluetooth.IBluetoothPreferredAudioProfilesCallback",
+ "android.bluetooth.IBluetoothQualityReportReadyCallback",
+ "android.bluetooth.IBluetoothSap",
+ "android.bluetooth.IBluetoothScan",
+ "android.bluetooth.IBluetoothSocketManager",
+ "android.bluetooth.IBluetoothVolumeControl",
+ "android.bluetooth.IBluetoothVolumeControlCallback",
+ "android.bluetooth.le.IAdvertisingSetCallback",
+ "android.bluetooth.le.IDistanceMeasurementCallback",
+ "android.bluetooth.le.IPeriodicAdvertisingCallback",
+ "android.bluetooth.le.IScannerCallback",
+ "android.companion.ICompanionDeviceManager",
+ "android.companion.IOnMessageReceivedListener",
+ "android.companion.IOnTransportsChangedListener",
+ "android.companion.virtualcamera.IVirtualCameraCallback",
+ "android.companion.virtual.IVirtualDevice",
+ "android.companion.virtual.IVirtualDeviceManager",
+ "android.companion.virtualnative.IVirtualDeviceManagerNative",
+ "android.content.IClipboard",
+ "android.content.IContentService",
+ "android.content.IIntentReceiver",
+ "android.content.IIntentSender",
+ "android.content.integrity.IAppIntegrityManager",
+ "android.content.IRestrictionsManager",
+ "android.content.ISyncAdapterUnsyncableAccountCallback",
+ "android.content.ISyncContext",
+ "android.content.om.IOverlayManager",
+ "android.content.pm.dex.IArtManager",
+ "android.content.pm.dex.ISnapshotRuntimeProfileCallback",
+ "android.content.pm.IBackgroundInstallControlService",
+ "android.content.pm.ICrossProfileApps",
+ "android.content.pm.IDataLoaderManager",
+ "android.content.pm.IDataLoaderStatusListener",
+ "android.content.pm.ILauncherApps",
+ "android.content.pm.IOnChecksumsReadyListener",
+ "android.content.pm.IOtaDexopt",
+ "android.content.pm.IPackageDataObserver",
+ "android.content.pm.IPackageDeleteObserver",
+ "android.content.pm.IPackageInstaller",
+ "android.content.pm.IPackageInstallerSession",
+ "android.content.pm.IPackageInstallerSessionFileSystemConnector",
+ "android.content.pm.IPackageInstallObserver2",
+ "android.content.pm.IPackageLoadingProgressCallback",
+ "android.content.pm.IPackageManager",
+ "android.content.pm.IPackageManagerNative",
+ "android.content.pm.IPackageMoveObserver",
+ "android.content.pm.IPinItemRequest",
+ "android.content.pm.IShortcutService",
+ "android.content.pm.IStagedApexObserver",
+ "android.content.pm.verify.domain.IDomainVerificationManager",
+ "android.content.res.IResourcesManager",
+ "android.content.rollback.IRollbackManager",
+ "android.credentials.ICredentialManager",
+ "android.debug.IAdbTransport",
+ "android.devicelock.IDeviceLockService",
+ "android.devicelock.IGetDeviceIdCallback",
+ "android.devicelock.IGetKioskAppsCallback",
+ "android.devicelock.IIsDeviceLockedCallback",
+ "android.devicelock.IVoidResultCallback",
+ "android.federatedcompute.aidl.IExampleStoreCallback",
+ "android.federatedcompute.aidl.IExampleStoreIterator",
+ "android.federatedcompute.aidl.IExampleStoreIteratorCallback",
+ "android.federatedcompute.aidl.IExampleStoreService",
+ "android.federatedcompute.aidl.IFederatedComputeCallback",
+ "android.federatedcompute.aidl.IFederatedComputeService",
+ "android.federatedcompute.aidl.IResultHandlingService",
+ "android.flags.IFeatureFlags",
+ "android.frameworks.location.altitude.IAltitudeService",
+ "android.frameworks.vibrator.IVibratorController",
+ "android.frameworks.vibrator.IVibratorControlService",
+ "android.gsi.IGsiServiceCallback",
+ "android.hardware.biometrics.AuthenticationStateListener",
+ "android.hardware.biometrics.common.ICancellationSignal",
+ "android.hardware.biometrics.face.IFace",
+ "android.hardware.biometrics.face.ISession",
+ "android.hardware.biometrics.face.ISessionCallback",
+ "android.hardware.biometrics.fingerprint.IFingerprint",
+ "android.hardware.biometrics.fingerprint.ISession",
+ "android.hardware.biometrics.fingerprint.ISessionCallback",
+ "android.hardware.biometrics.IAuthService",
+ "android.hardware.biometrics.IBiometricAuthenticator",
+ "android.hardware.biometrics.IBiometricContextListener",
+ "android.hardware.biometrics.IBiometricSensorReceiver",
+ "android.hardware.biometrics.IBiometricService",
+ "android.hardware.biometrics.IBiometricStateListener",
+ "android.hardware.biometrics.IBiometricSysuiReceiver",
+ "android.hardware.biometrics.IInvalidationCallback",
+ "android.hardware.biometrics.ITestSession",
+ "android.hardware.broadcastradio.IAnnouncementListener",
+ "android.hardware.broadcastradio.ITunerCallback",
+ "android.hardware.contexthub.IContextHubCallback",
+ "android.hardware.devicestate.IDeviceStateManager",
+ "android.hardware.display.IColorDisplayManager",
+ "android.hardware.display.IDisplayManager",
+ "android.hardware.face.IFaceAuthenticatorsRegisteredCallback",
+ "android.hardware.face.IFaceService",
+ "android.hardware.face.IFaceServiceReceiver",
+ "android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback",
+ "android.hardware.fingerprint.IFingerprintClientActiveCallback",
+ "android.hardware.fingerprint.IFingerprintService",
+ "android.hardware.fingerprint.IFingerprintServiceReceiver",
+ "android.hardware.fingerprint.IUdfpsOverlayControllerCallback",
+ "android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback",
+ "android.hardware.hdmi.IHdmiControlCallback",
+ "android.hardware.hdmi.IHdmiControlService",
+ "android.hardware.hdmi.IHdmiDeviceEventListener",
+ "android.hardware.hdmi.IHdmiHotplugEventListener",
+ "android.hardware.hdmi.IHdmiSystemAudioModeChangeListener",
+ "android.hardware.health.IHealthInfoCallback",
+ "android.hardware.ICameraServiceProxy",
+ "android.hardware.IConsumerIrService",
+ "android.hardware.input.IInputManager",
+ "android.hardware.iris.IIrisService",
+ "android.hardware.ISensorPrivacyManager",
+ "android.hardware.ISerialManager",
+ "android.hardware.lights.ILightsManager",
+ "android.hardware.location.IContextHubClient",
+ "android.hardware.location.IContextHubClientCallback",
+ "android.hardware.location.IContextHubService",
+ "android.hardware.location.IContextHubTransactionCallback",
+ "android.hardware.location.ISignificantPlaceProviderManager",
+ "android.hardware.radio.IAnnouncementListener",
+ "android.hardware.radio.ICloseHandle",
+ "android.hardware.radio.ims.media.IImsMedia",
+ "android.hardware.radio.ims.media.IImsMediaListener",
+ "android.hardware.radio.ims.media.IImsMediaSession",
+ "android.hardware.radio.ims.media.IImsMediaSessionListener",
+ "android.hardware.radio.IRadioService",
+ "android.hardware.radio.ITuner",
+ "android.hardware.radio.sap.ISapCallback",
+ "android.hardware.soundtrigger3.ISoundTriggerHw",
+ "android.hardware.soundtrigger3.ISoundTriggerHwCallback",
+ "android.hardware.soundtrigger3.ISoundTriggerHwGlobalCallback",
+ "android.hardware.soundtrigger.IRecognitionStatusCallback",
+ "android.hardware.tetheroffload.ITetheringOffloadCallback",
+ "android.hardware.thermal.IThermalChangedCallback",
+ "android.hardware.tv.hdmi.cec.IHdmiCecCallback",
+ "android.hardware.tv.hdmi.connection.IHdmiConnectionCallback",
+ "android.hardware.tv.hdmi.earc.IEArcCallback",
+ "android.hardware.usb.gadget.IUsbGadgetCallback",
+ "android.hardware.usb.IUsbCallback",
+ "android.hardware.usb.IUsbManager",
+ "android.hardware.usb.IUsbSerialReader",
+ "android.hardware.wifi.hostapd.IHostapdCallback",
+ "android.hardware.wifi.IWifiChipEventCallback",
+ "android.hardware.wifi.IWifiEventCallback",
+ "android.hardware.wifi.IWifiNanIfaceEventCallback",
+ "android.hardware.wifi.IWifiRttControllerEventCallback",
+ "android.hardware.wifi.IWifiStaIfaceEventCallback",
+ "android.hardware.wifi.supplicant.INonStandardCertCallback",
+ "android.hardware.wifi.supplicant.ISupplicantP2pIfaceCallback",
+ "android.hardware.wifi.supplicant.ISupplicantStaIfaceCallback",
+ "android.hardware.wifi.supplicant.ISupplicantStaNetworkCallback",
+ "android.health.connect.aidl.IAccessLogsResponseCallback",
+ "android.health.connect.aidl.IActivityDatesResponseCallback",
+ "android.health.connect.aidl.IAggregateRecordsResponseCallback",
+ "android.health.connect.aidl.IApplicationInfoResponseCallback",
+ "android.health.connect.aidl.IChangeLogsResponseCallback",
+ "android.health.connect.aidl.IDataStagingFinishedCallback",
+ "android.health.connect.aidl.IEmptyResponseCallback",
+ "android.health.connect.aidl.IGetChangeLogTokenCallback",
+ "android.health.connect.aidl.IGetHealthConnectDataStateCallback",
+ "android.health.connect.aidl.IGetHealthConnectMigrationUiStateCallback",
+ "android.health.connect.aidl.IGetPriorityResponseCallback",
+ "android.health.connect.aidl.IHealthConnectService",
+ "android.health.connect.aidl.IInsertRecordsResponseCallback",
+ "android.health.connect.aidl.IMedicalDataSourceResponseCallback",
+ "android.health.connect.aidl.IMedicalResourcesResponseCallback",
+ "android.health.connect.aidl.IMigrationCallback",
+ "android.health.connect.aidl.IReadMedicalResourcesResponseCallback",
+ "android.health.connect.aidl.IReadRecordsResponseCallback",
+ "android.health.connect.aidl.IRecordTypeInfoResponseCallback",
+ "android.health.connect.exportimport.IImportStatusCallback",
+ "android.health.connect.exportimport.IQueryDocumentProvidersCallback",
+ "android.health.connect.exportimport.IScheduledExportStatusCallback",
+ "android.location.ICountryDetector",
+ "android.location.IGpsGeofenceHardware",
+ "android.location.ILocationManager",
+ "android.location.provider.ILocationProviderManager",
+ "android.media.IAudioRoutesObserver",
+ "android.media.IMediaCommunicationService",
+ "android.media.IMediaCommunicationServiceCallback",
+ "android.media.IMediaController2",
+ "android.media.IMediaRoute2ProviderServiceCallback",
+ "android.media.IMediaRouterService",
+ "android.media.IMediaSession2",
+ "android.media.IMediaSession2Service",
+ "android.media.INativeSpatializerCallback",
+ "android.media.IPlaybackConfigDispatcher",
+ "android.media.IRecordingConfigDispatcher",
+ "android.media.IRemoteDisplayCallback",
+ "android.media.ISoundDoseCallback",
+ "android.media.ISpatializerHeadTrackingCallback",
+ "android.media.ITranscodingClientCallback",
+ "android.media.metrics.IMediaMetricsManager",
+ "android.media.midi.IMidiManager",
+ "android.media.musicrecognition.IMusicRecognitionAttributionTagCallback",
+ "android.media.musicrecognition.IMusicRecognitionManager",
+ "android.media.musicrecognition.IMusicRecognitionServiceCallback",
+ "android.media.projection.IMediaProjection",
+ "android.media.projection.IMediaProjectionCallback",
+ "android.media.projection.IMediaProjectionManager",
+ "android.media.projection.IMediaProjectionWatcherCallback",
+ "android.media.session.ISession",
+ "android.media.session.ISessionController",
+ "android.media.session.ISessionManager",
+ "android.media.soundtrigger.ISoundTriggerDetectionServiceClient",
+ "android.media.soundtrigger_middleware.IInjectGlobalEvent",
+ "android.media.soundtrigger_middleware.IInjectModelEvent",
+ "android.media.soundtrigger_middleware.IInjectRecognitionEvent",
+ "android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService",
+ "android.media.soundtrigger_middleware.ISoundTriggerModule",
+ "android.media.tv.ad.ITvAdManager",
+ "android.media.tv.ad.ITvAdSessionCallback",
+ "android.media.tv.interactive.ITvInteractiveAppManager",
+ "android.media.tv.interactive.ITvInteractiveAppServiceCallback",
+ "android.media.tv.interactive.ITvInteractiveAppSessionCallback",
+ "android.media.tv.ITvInputHardware",
+ "android.media.tv.ITvInputManager",
+ "android.media.tv.ITvInputServiceCallback",
+ "android.media.tv.ITvInputSessionCallback",
+ "android.media.tv.ITvRemoteServiceInput",
+ "android.nearby.aidl.IOffloadCallback",
+ "android.nearby.IBroadcastListener",
+ "android.nearby.INearbyManager",
+ "android.nearby.IScanListener",
+ "android.net.connectivity.aidl.ConnectivityNative",
+ "android.net.dhcp.IDhcpEventCallbacks",
+ "android.net.dhcp.IDhcpServer",
+ "android.net.dhcp.IDhcpServerCallbacks",
+ "android.net.ICaptivePortal",
+ "android.net.IConnectivityDiagnosticsCallback",
+ "android.net.IConnectivityManager",
+ "android.net.IEthernetManager",
+ "android.net.IEthernetServiceListener",
+ "android.net.IIntResultListener",
+ "android.net.IIpConnectivityMetrics",
+ "android.net.IIpMemoryStore",
+ "android.net.IIpMemoryStoreCallbacks",
+ "android.net.IIpSecService",
+ "android.net.INetdEventCallback",
+ "android.net.INetdUnsolicitedEventListener",
+ "android.net.INetworkActivityListener",
+ "android.net.INetworkAgent",
+ "android.net.INetworkAgentRegistry",
+ "android.net.INetworkInterfaceOutcomeReceiver",
+ "android.net.INetworkManagementEventObserver",
+ "android.net.INetworkMonitor",
+ "android.net.INetworkMonitorCallbacks",
+ "android.net.INetworkOfferCallback",
+ "android.net.INetworkPolicyListener",
+ "android.net.INetworkPolicyManager",
+ "android.net.INetworkScoreService",
+ "android.net.INetworkStackConnector",
+ "android.net.INetworkStackStatusCallback",
+ "android.net.INetworkStatsService",
+ "android.net.INetworkStatsSession",
+ "android.net.IOnCompleteListener",
+ "android.net.IPacProxyManager",
+ "android.net.ip.IIpClient",
+ "android.net.ip.IIpClientCallbacks",
+ "android.net.ipmemorystore.IOnBlobRetrievedListener",
+ "android.net.ipmemorystore.IOnL2KeyResponseListener",
+ "android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener",
+ "android.net.ipmemorystore.IOnSameL3NetworkResponseListener",
+ "android.net.ipmemorystore.IOnStatusAndCountListener",
+ "android.net.ipmemorystore.IOnStatusListener",
+ "android.net.IQosCallback",
+ "android.net.ISocketKeepaliveCallback",
+ "android.net.ITestNetworkManager",
+ "android.net.ITetheredInterfaceCallback",
+ "android.net.ITetheringConnector",
+ "android.net.ITetheringEventCallback",
+ "android.net.IVpnManager",
+ "android.net.mdns.aidl.IMDnsEventListener",
+ "android.net.metrics.INetdEventListener",
+ "android.net.netstats.IUsageCallback",
+ "android.net.netstats.provider.INetworkStatsProvider",
+ "android.net.netstats.provider.INetworkStatsProviderCallback",
+ "android.net.nsd.INsdManager",
+ "android.net.nsd.INsdManagerCallback",
+ "android.net.nsd.INsdServiceConnector",
+ "android.net.nsd.IOffloadEngine",
+ "android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener",
+ "android.net.thread.IActiveOperationalDatasetReceiver",
+ "android.net.thread.IConfigurationReceiver",
+ "android.net.thread.IOperationalDatasetCallback",
+ "android.net.thread.IOperationReceiver",
+ "android.net.thread.IStateCallback",
+ "android.net.thread.IThreadNetworkController",
+ "android.net.thread.IThreadNetworkManager",
+ "android.net.vcn.IVcnManagementService",
+ "android.net.wear.ICompanionDeviceManagerProxy",
+ "android.net.wifi.aware.IWifiAwareDiscoverySessionCallback",
+ "android.net.wifi.aware.IWifiAwareEventCallback",
+ "android.net.wifi.aware.IWifiAwareMacAddressProvider",
+ "android.net.wifi.aware.IWifiAwareManager",
+ "android.net.wifi.hotspot2.IProvisioningCallback",
+ "android.net.wifi.IActionListener",
+ "android.net.wifi.IBooleanListener",
+ "android.net.wifi.IByteArrayListener",
+ "android.net.wifi.ICoexCallback",
+ "android.net.wifi.IDppCallback",
+ "android.net.wifi.IIntegerListener",
+ "android.net.wifi.IInterfaceCreationInfoCallback",
+ "android.net.wifi.ILastCallerListener",
+ "android.net.wifi.IListListener",
+ "android.net.wifi.ILocalOnlyConnectionStatusListener",
+ "android.net.wifi.ILocalOnlyHotspotCallback",
+ "android.net.wifi.IMacAddressListListener",
+ "android.net.wifi.IMapListener",
+ "android.net.wifi.INetworkRequestMatchCallback",
+ "android.net.wifi.INetworkRequestUserSelectionCallback",
+ "android.net.wifi.IOnWifiActivityEnergyInfoListener",
+ "android.net.wifi.IOnWifiDriverCountryCodeChangedListener",
+ "android.net.wifi.IOnWifiUsabilityStatsListener",
+ "android.net.wifi.IPnoScanResultsCallback",
+ "android.net.wifi.IScanDataListener",
+ "android.net.wifi.IScanResultsCallback",
+ "android.net.wifi.IScoreUpdateObserver",
+ "android.net.wifi.ISoftApCallback",
+ "android.net.wifi.IStringListener",
+ "android.net.wifi.ISubsystemRestartCallback",
+ "android.net.wifi.ISuggestionConnectionStatusListener",
+ "android.net.wifi.ISuggestionUserApprovalStatusListener",
+ "android.net.wifi.ITrafficStateCallback",
+ "android.net.wifi.ITwtCallback",
+ "android.net.wifi.ITwtCapabilitiesListener",
+ "android.net.wifi.ITwtStatsListener",
+ "android.net.wifi.IWifiBandsListener",
+ "android.net.wifi.IWifiConnectedNetworkScorer",
+ "android.net.wifi.IWifiLowLatencyLockListener",
+ "android.net.wifi.IWifiManager",
+ "android.net.wifi.IWifiNetworkSelectionConfigListener",
+ "android.net.wifi.IWifiNetworkStateChangedListener",
+ "android.net.wifi.IWifiScanner",
+ "android.net.wifi.IWifiScannerListener",
+ "android.net.wifi.IWifiVerboseLoggingStatusChangedListener",
+ "android.net.wifi.p2p.IWifiP2pListener",
+ "android.net.wifi.p2p.IWifiP2pManager",
+ "android.net.wifi.rtt.IRttCallback",
+ "android.net.wifi.rtt.IWifiRttManager",
+ "android.ondevicepersonalization.IOnDevicePersonalizationSystemService",
+ "android.ondevicepersonalization.IOnDevicePersonalizationSystemServiceCallback",
+ "android.os.IBatteryPropertiesRegistrar",
+ "android.os.ICancellationSignal",
+ "android.os.IDeviceIdentifiersPolicyService",
+ "android.os.IDeviceIdleController",
+ "android.os.IDumpstate",
+ "android.os.IDumpstateListener",
+ "android.os.IExternalVibratorService",
+ "android.os.IHardwarePropertiesManager",
+ "android.os.IHintManager",
+ "android.os.IHintSession",
+ "android.os.IIncidentCompanion",
+ "android.os.image.IDynamicSystemService",
+ "android.os.incremental.IStorageHealthListener",
+ "android.os.INetworkManagementService",
+ "android.os.IPendingIntentRef",
+ "android.os.IPowerStatsService",
+ "android.os.IProfilingResultCallback",
+ "android.os.IProfilingService",
+ "android.os.IProgressListener",
+ "android.os.IPullAtomCallback",
+ "android.os.IRecoverySystem",
+ "android.os.IRemoteCallback",
+ "android.os.ISecurityStateManager",
+ "android.os.IServiceCallback",
+ "android.os.IStatsCompanionService",
+ "android.os.IStatsManagerService",
+ "android.os.IStatsQueryCallback",
+ "android.os.ISystemConfig",
+ "android.os.ISystemUpdateManager",
+ "android.os.IThermalEventListener",
+ "android.os.IUpdateLock",
+ "android.os.IUserManager",
+ "android.os.IUserRestrictionsListener",
+ "android.os.IVibratorManagerService",
+ "android.os.IVoldListener",
+ "android.os.IVoldMountCallback",
+ "android.os.IVoldTaskListener",
+ "android.os.logcat.ILogcatManagerService",
+ "android.permission.ILegacyPermissionManager",
+ "android.permission.IPermissionChecker",
+ "android.permission.IPermissionManager",
+ "android.print.IPrintManager",
+ "android.print.IPrintSpoolerCallbacks",
+ "android.print.IPrintSpoolerClient",
+ "android.printservice.IPrintServiceClient",
+ "android.printservice.recommendation.IRecommendationServiceCallbacks",
+ "android.provider.aidl.IDeviceConfigManager",
+ "android.remoteauth.IDeviceDiscoveryListener",
+ "android.safetycenter.IOnSafetyCenterDataChangedListener",
+ "android.safetycenter.ISafetyCenterManager",
+ "android.scheduling.IRebootReadinessManager",
+ "android.scheduling.IRequestRebootReadinessStatusListener",
+ "android.security.attestationverification.IAttestationVerificationManagerService",
+ "android.security.IFileIntegrityService",
+ "android.security.keystore.IKeyAttestationApplicationIdProvider",
+ "android.security.rkp.IRegistration",
+ "android.security.rkp.IRemoteProvisioning",
+ "android.service.appprediction.IPredictionService",
+ "android.service.assist.classification.IFieldClassificationCallback",
+ "android.service.attention.IAttentionCallback",
+ "android.service.attention.IProximityUpdateCallback",
+ "android.service.autofill.augmented.IFillCallback",
+ "android.service.autofill.IConvertCredentialCallback",
+ "android.service.autofill.IFillCallback",
+ "android.service.autofill.IInlineSuggestionUiCallback",
+ "android.service.autofill.ISaveCallback",
+ "android.service.autofill.ISurfacePackageResultCallback",
+ "android.service.contentcapture.IContentCaptureServiceCallback",
+ "android.service.contentcapture.IContentProtectionAllowlistCallback",
+ "android.service.contentcapture.IDataShareCallback",
+ "android.service.credentials.IBeginCreateCredentialCallback",
+ "android.service.credentials.IBeginGetCredentialCallback",
+ "android.service.credentials.IClearCredentialStateCallback",
+ "android.service.dreams.IDreamManager",
+ "android.service.games.IGameServiceController",
+ "android.service.games.IGameSessionController",
+ "android.service.notification.IStatusBarNotificationHolder",
+ "android.service.oemlock.IOemLockService",
+ "android.service.ondeviceintelligence.IProcessingUpdateStatusCallback",
+ "android.service.ondeviceintelligence.IRemoteProcessingService",
+ "android.service.ondeviceintelligence.IRemoteStorageService",
+ "android.service.persistentdata.IPersistentDataBlockService",
+ "android.service.resolver.IResolverRankerResult",
+ "android.service.rotationresolver.IRotationResolverCallback",
+ "android.service.textclassifier.ITextClassifierCallback",
+ "android.service.textclassifier.ITextClassifierService",
+ "android.service.timezone.ITimeZoneProviderManager",
+ "android.service.trust.ITrustAgentServiceCallback",
+ "android.service.voice.IDetectorSessionStorageService",
+ "android.service.voice.IDetectorSessionVisualQueryDetectionCallback",
+ "android.service.voice.IDspHotwordDetectionCallback",
+ "android.service.wallpaper.IWallpaperConnection",
+ "android.speech.IRecognitionListener",
+ "android.speech.IRecognitionService",
+ "android.speech.IRecognitionServiceManager",
+ "android.speech.tts.ITextToSpeechManager",
+ "android.speech.tts.ITextToSpeechSession",
+ "android.system.composd.ICompilationTaskCallback",
+ "android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback",
+ "android.system.virtualizationservice.IVirtualMachineCallback",
+ "android.system.vmtethering.IVmTethering",
+ "android.telephony.imsmedia.IImsAudioSession",
+ "android.telephony.imsmedia.IImsAudioSessionCallback",
+ "android.telephony.imsmedia.IImsMedia",
+ "android.telephony.imsmedia.IImsMediaCallback",
+ "android.telephony.imsmedia.IImsTextSession",
+ "android.telephony.imsmedia.IImsTextSessionCallback",
+ "android.telephony.imsmedia.IImsVideoSession",
+ "android.telephony.imsmedia.IImsVideoSessionCallback",
+ "android.tracing.ITracingServiceProxy",
+ "android.uwb.IOnUwbActivityEnergyInfoListener",
+ "android.uwb.IUwbAdapter",
+ "android.uwb.IUwbAdapterStateCallbacks",
+ "android.uwb.IUwbAdfProvisionStateCallbacks",
+ "android.uwb.IUwbOemExtensionCallback",
+ "android.uwb.IUwbRangingCallbacks",
+ "android.uwb.IUwbVendorUciCallback",
+ "android.view.accessibility.IAccessibilityInteractionConnectionCallback",
+ "android.view.accessibility.IAccessibilityManager",
+ "android.view.accessibility.IMagnificationConnectionCallback",
+ "android.view.accessibility.IRemoteMagnificationAnimationCallback",
+ "android.view.autofill.IAutoFillManager",
+ "android.view.autofill.IAutofillWindowPresenter",
+ "android.view.contentcapture.IContentCaptureManager",
+ "android.view.IDisplayChangeWindowCallback",
+ "android.view.IDisplayWindowListener",
+ "android.view.IInputFilter",
+ "android.view.IInputFilterHost",
+ "android.view.IInputMonitorHost",
+ "android.view.IRecentsAnimationController",
+ "android.view.IRemoteAnimationFinishedCallback",
+ "android.view.ISensitiveContentProtectionManager",
+ "android.view.IWindowId",
+ "android.view.IWindowManager",
+ "android.view.IWindowSession",
+ "android.view.translation.ITranslationManager",
+ "android.view.translation.ITranslationServiceCallback",
+ "android.webkit.IWebViewUpdateService",
+ "android.window.IBackAnimationFinishedCallback",
+ "android.window.IDisplayAreaOrganizerController",
+ "android.window.ITaskFragmentOrganizerController",
+ "android.window.ITaskOrganizerController",
+ "android.window.ITransitionMetricsReporter",
+ "android.window.IUnhandledDragCallback",
+ "android.window.IWindowContainerToken",
+ "android.window.IWindowlessStartingSurfaceCallback",
+ "android.window.IWindowOrganizerController",
+ "androidx.core.uwb.backend.IUwb",
+ "androidx.core.uwb.backend.IUwbClient",
+ "com.android.clockwork.modes.IModeManager",
+ "com.android.clockwork.modes.IStateChangeListener",
+ "com.android.clockwork.power.IWearPowerService",
+ "com.android.devicelockcontroller.IDeviceLockControllerService",
+ "com.android.devicelockcontroller.storage.IGlobalParametersService",
+ "com.android.devicelockcontroller.storage.ISetupParametersService",
+ "com.android.federatedcompute.services.training.aidl.IIsolatedTrainingService",
+ "com.android.federatedcompute.services.training.aidl.ITrainingResultCallback",
+ "com.android.internal.app.IAppOpsActiveCallback",
+ "com.android.internal.app.ILogAccessDialogCallback",
+ "com.android.internal.app.ISoundTriggerService",
+ "com.android.internal.app.ISoundTriggerSession",
+ "com.android.internal.app.IVoiceInteractionAccessibilitySettingsListener",
+ "com.android.internal.app.IVoiceInteractionManagerService",
+ "com.android.internal.app.IVoiceInteractionSessionListener",
+ "com.android.internal.app.IVoiceInteractionSessionShowCallback",
+ "com.android.internal.app.IVoiceInteractionSoundTriggerSession",
+ "com.android.internal.app.procstats.IProcessStats",
+ "com.android.internal.appwidget.IAppWidgetService",
+ "com.android.internal.backup.ITransportStatusCallback",
+ "com.android.internal.compat.IOverrideValidator",
+ "com.android.internal.compat.IPlatformCompat",
+ "com.android.internal.compat.IPlatformCompatNative",
+ "com.android.internal.graphics.fonts.IFontManager",
+ "com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback",
+ "com.android.internal.inputmethod.IConnectionlessHandwritingCallback",
+ "com.android.internal.inputmethod.IImeTracker",
+ "com.android.internal.inputmethod.IInlineSuggestionsRequestCallback",
+ "com.android.internal.inputmethod.IInputContentUriToken",
+ "com.android.internal.inputmethod.IInputMethodPrivilegedOperations",
+ "com.android.internal.inputmethod.IInputMethodSessionCallback",
+ "com.android.internal.net.INetworkWatchlistManager",
+ "com.android.internal.os.IBinaryTransparencyService",
+ "com.android.internal.os.IDropBoxManagerService",
+ "com.android.internal.policy.IKeyguardDismissCallback",
+ "com.android.internal.policy.IKeyguardDrawnCallback",
+ "com.android.internal.policy.IKeyguardExitCallback",
+ "com.android.internal.policy.IKeyguardStateCallback",
+ "com.android.internal.statusbar.IAddTileResultCallback",
+ "com.android.internal.statusbar.ISessionListener",
+ "com.android.internal.statusbar.IStatusBarService",
+ "com.android.internal.telecom.IDeviceIdleControllerAdapter",
+ "com.android.internal.telecom.IInternalServiceRetriever",
+ "com.android.internal.telephony.IMms",
+ "com.android.internal.telephony.ITelephonyRegistry",
+ "com.android.internal.textservice.ISpellCheckerServiceCallback",
+ "com.android.internal.textservice.ITextServicesManager",
+ "com.android.internal.view.IDragAndDropPermissions",
+ "com.android.internal.view.IInputMethodManager",
+ "com.android.internal.view.inline.IInlineContentProvider",
+ "com.android.internal.widget.ILockSettings",
+ "com.android.net.IProxyPortListener",
+ "com.android.net.module.util.IRoutingCoordinator",
+ "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginCallback",
+ "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginExecutorService",
+ "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginStateCallback",
+ "com.android.rkpdapp.IGetKeyCallback",
+ "com.android.rkpdapp.IGetRegistrationCallback",
+ "com.android.rkpdapp.IRegistration",
+ "com.android.rkpdapp.IRemoteProvisioning",
+ "com.android.rkpdapp.IStoreUpgradedKeyCallback",
+ "com.android.sdksandbox.IComputeSdkStorageCallback",
+ "com.android.sdksandbox.ILoadSdkInSandboxCallback",
+ "com.android.sdksandbox.IRequestSurfacePackageFromSdkCallback",
+ "com.android.sdksandbox.ISdkSandboxManagerToSdkSandboxCallback",
+ "com.android.sdksandbox.ISdkSandboxService",
+ "com.android.sdksandbox.IUnloadSdkInSandboxCallback",
+ "com.android.server.profcollect.IProviderStatusCallback",
+ "com.android.server.thread.openthread.IChannelMasksReceiver",
+ "com.android.server.thread.openthread.INsdPublisher",
+ "com.android.server.thread.openthread.IOtDaemonCallback",
+ "com.android.server.thread.openthread.IOtStatusReceiver",
+ "com.google.android.clockwork.ambient.offload.IDisplayOffloadService",
+ "com.google.android.clockwork.ambient.offload.IDisplayOffloadTransitionFinishedCallbacks",
+ "com.google.android.clockwork.healthservices.IHealthService",
+ "vendor.google_clockwork.healthservices.IHealthServicesCallback",
+)
diff --git a/tools/lint/utils/README.md b/tools/lint/utils/README.md
new file mode 100644
index 000000000000..b5583c54b25c
--- /dev/null
+++ b/tools/lint/utils/README.md
@@ -0,0 +1,11 @@
+# Utility Android Lint Checks for AOSP
+
+This directory contains scripts that execute utility Android Lint Checks for AOSP, specifically:
+* `enforce_permission_counter.py`: Provides statistics regarding the percentage of annotated/not
+ annotated `AIDL` methods with `@EnforcePermission` annotations.
+* `generate-exempt-aidl-interfaces.sh`: Provides a list of all `AIDL` interfaces in the entire
+ source tree.
+
+When adding a new utility Android Lint check to this directory, consider adding any utility or
+data processing tool you might require. Make sure that your contribution is documented in this
+README file.
diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
index fa61c42ef8e6..98428810c0fc 100644
--- a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
+++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
@@ -19,6 +19,7 @@ package com.google.android.lint
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.android.lint.aidl.ExemptAidlInterfacesGenerator
import com.google.android.lint.aidl.AnnotatedAidlCounter
import com.google.auto.service.AutoService
@@ -27,6 +28,7 @@ import com.google.auto.service.AutoService
class AndroidUtilsIssueRegistry : IssueRegistry() {
override val issues = listOf(
AnnotatedAidlCounter.ISSUE_ANNOTATED_AIDL_COUNTER,
+ ExemptAidlInterfacesGenerator.ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
)
override val api: Int
@@ -38,6 +40,6 @@ class AndroidUtilsIssueRegistry : IssueRegistry() {
override val vendor: Vendor = Vendor(
vendorName = "Android",
feedbackUrl = "http://b/issues/new?component=315013",
- contact = "tweek@google.com"
+ contact = "android-platform-abuse-prevention-withfriends@google.com"
)
}
diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt
new file mode 100644
index 000000000000..6ad223c87a29
--- /dev/null
+++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UMethod
+
+/**
+ * Generates a set of fully qualified AIDL Interface names present in the entire source tree with
+ * the following requirement: their implementations have to be inside directories whose path
+ * prefixes match `systemServicePathPrefixes`.
+ */
+class ExemptAidlInterfacesGenerator : AidlImplementationDetector() {
+ private val targetExemptAidlInterfaceNames = mutableSetOf<String>()
+ private val systemServicePathPrefixes = setOf(
+ "frameworks/base/services",
+ "frameworks/base/apex",
+ "frameworks/opt/wear",
+ "packages/modules"
+ )
+
+ // We could've improved performance by visiting classes rather than methods, however, this lint
+ // check won't be run regularly, hence we've decided not to add extra overrides to
+ // AidlImplementationDetector.
+ override fun visitAidlMethod(
+ context: JavaContext,
+ node: UMethod,
+ interfaceName: String,
+ body: UBlockExpression
+ ) {
+ val filePath = context.file.path
+
+ // We perform `filePath.contains` instead of `filePath.startsWith` since getting the
+ // relative path of a source file is non-trivial. That is because `context.file.path`
+ // returns the path to where soong builds the file (i.e. /out/soong/...). Moreover, the
+ // logic to extract the relative path would need to consider several /out/soong/...
+ // locations patterns.
+ if (systemServicePathPrefixes.none { filePath.contains(it) }) return
+
+ val fullyQualifiedInterfaceName =
+ getContainingAidlInterfaceQualified(context, node) ?: return
+
+ targetExemptAidlInterfaceNames.add("\"$fullyQualifiedInterfaceName\",")
+ }
+
+ override fun afterCheckEachProject(context: Context) {
+ if (targetExemptAidlInterfaceNames.isEmpty()) return
+
+ val message = targetExemptAidlInterfaceNames.joinToString("\n")
+
+ context.report(
+ ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
+ context.getLocation(context.project.dir),
+ "\n" + message + "\n",
+ )
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES = Issue.create(
+ id = "PermissionAnnotationExemptAidlInterfaces",
+ briefDescription = "Returns a set of all AIDL interfaces",
+ explanation = """
+ Produces the exemptAidlInterfaces set used by PermissionAnnotationDetector
+ """.trimIndent(),
+ category = Category.SECURITY,
+ priority = 5,
+ severity = Severity.INFORMATIONAL,
+ implementation = Implementation(
+ ExemptAidlInterfacesGenerator::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
diff --git a/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt
new file mode 100644
index 000000000000..9a17bb4c8d3e
--- /dev/null
+++ b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+class ExemptAidlInterfacesGeneratorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = ExemptAidlInterfacesGenerator()
+
+ override fun getIssues(): List<Issue> = listOf(
+ ExemptAidlInterfacesGenerator.ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ fun testMultipleAidlInterfacesImplemented() {
+ lint()
+ .files(
+ java(
+ createVisitedPath("TestClass1.java"),
+ """
+ package com.android.server;
+ public class TestClass1 extends IFoo.Stub {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ java(
+ createVisitedPath("TestClass2.java"),
+ """
+ package com.android.server;
+ public class TestClass2 extends IBar.Stub {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ *stubs,
+ )
+ .run()
+ .expect(
+ """
+ app: Information: "IFoo",
+ "IBar", [PermissionAnnotationExemptAidlInterfaces]
+ 0 errors, 0 warnings
+ """
+ )
+ }
+
+ fun testSingleAidlInterfaceRepeated() {
+ lint()
+ .files(
+ java(
+ createVisitedPath("TestClass1.java"),
+ """
+ package com.android.server;
+ public class TestClass1 extends IFoo.Stub {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ java(
+ createVisitedPath("TestClass2.java"),
+ """
+ package com.android.server;
+ public class TestClass2 extends IFoo.Stub {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ *stubs,
+ )
+ .run()
+ .expect(
+ """
+ app: Information: "IFoo", [PermissionAnnotationExemptAidlInterfaces]
+ 0 errors, 0 warnings
+ """
+ )
+ }
+
+ fun testAnonymousClassExtendsAidlStub() {
+ lint()
+ .files(
+ java(
+ createVisitedPath("TestClass.java"),
+ """
+ package com.android.server;
+ public class TestClass {
+ private IBinder aidlImpl = new IFoo.Stub() {
+ public void testMethod() {}
+ };
+ }
+ """
+ )
+ .indented(),
+ *stubs,
+ )
+ .run()
+ .expect(
+ """
+ app: Information: "IFoo", [PermissionAnnotationExemptAidlInterfaces]
+ 0 errors, 0 warnings
+ """
+ )
+ }
+
+ fun testNoAidlInterfacesImplemented() {
+ lint()
+ .files(
+ java(
+ createVisitedPath("TestClass.java"),
+ """
+ package com.android.server;
+ public class TestClass {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testAidlInterfaceImplementedInIgnoredDirectory() {
+ lint()
+ .files(
+ java(
+ ignoredPath,
+ """
+ package com.android.server;
+ public class TestClass1 extends IFoo.Stub {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ *stubs,
+ )
+ .run()
+ .expectClean()
+ }
+
+ private val interfaceIFoo: TestFile = java(
+ """
+ public interface IFoo extends android.os.IInterface {
+ public static abstract class Stub extends android.os.Binder implements IFoo {}
+ public void testMethod();
+ }
+ """
+ ).indented()
+
+ private val interfaceIBar: TestFile = java(
+ """
+ public interface IBar extends android.os.IInterface {
+ public static abstract class Stub extends android.os.Binder implements IBar {}
+ public void testMethod();
+ }
+ """
+ ).indented()
+
+ private val stubs = arrayOf(interfaceIFoo, interfaceIBar)
+
+ private fun createVisitedPath(filename: String) =
+ "src/frameworks/base/services/java/com/android/server/$filename"
+
+ private val ignoredPath = "src/test/pkg/TestClass.java"
+}
diff --git a/tools/lint/utils/generate-exempt-aidl-interfaces.sh b/tools/lint/utils/generate-exempt-aidl-interfaces.sh
new file mode 100755
index 000000000000..44dcdd74fe06
--- /dev/null
+++ b/tools/lint/utils/generate-exempt-aidl-interfaces.sh
@@ -0,0 +1,59 @@
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Create a directory for the results and a nested temporary directory.
+mkdir -p $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Create a copy of `AndroidGlobalLintChecker.jar` to restore it afterwards.
+cp $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar \
+ $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar
+
+# Configure the environment variable required for running the lint check on the entire source tree.
+export ANDROID_LINT_CHECK=PermissionAnnotationExemptAidlInterfaces
+
+# Build the target corresponding to the lint checks present in the `utils` directory.
+m AndroidUtilsLintChecker
+
+# Replace `AndroidGlobalLintChecker.jar` with the newly built `jar` file.
+cp $ANDROID_BUILD_TOP/out/host/linux-x86/framework/AndroidUtilsLintChecker.jar \
+ $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar;
+
+# Run the lint check on the entire source tree.
+m lint-check
+
+# Copy the archive containing the results of `lint-check` into the temporary directory.
+cp $ANDROID_BUILD_TOP/out/soong/lint-report-text.zip \
+ $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+cd $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Unzip the archive containing the results of `lint-check`.
+unzip lint-report-text.zip
+
+# Concatenate the results of `lint-check` into a single string.
+concatenated_reports=$(find . -type f | xargs cat)
+
+# Extract the fully qualified names of the AIDL Interfaces from the concatenated results. Output
+# this list into `out/soong/exempt_aidl_interfaces_generator_output/exempt_aidl_interfaces`.
+echo $concatenated_reports | grep -Eo '\"([a-zA-Z0-9_]*\.)+[a-zA-Z0-9_]*\",' | sort | uniq > ../exempt_aidl_interfaces
+
+# Remove the temporary directory.
+rm -rf $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Restore the original copy of `AndroidGlobalLintChecker.jar` and delete the copy.
+cp $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar \
+ $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar
+rm $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar