summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/aconfig/job.aconfig7
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java157
-rw-r--r--core/api/current.txt5
-rw-r--r--core/api/system-current.txt13
-rw-r--r--core/java/android/app/ActivityManager.java4
-rw-r--r--core/java/android/app/ActivityManagerInternal.java3
-rw-r--r--core/java/android/app/ActivityThread.java18
-rw-r--r--core/java/android/app/LoadedApk.java2
-rw-r--r--core/java/android/app/PropertyInvalidatedCache.java725
-rw-r--r--core/java/android/app/appfunctions/AppFunctionManager.java34
-rw-r--r--core/java/android/app/appfunctions/AppFunctionService.java74
-rw-r--r--core/java/android/companion/AssociationInfo.java2
-rw-r--r--core/java/android/companion/AssociationRequest.java4
-rw-r--r--core/java/android/companion/CompanionDeviceManager.java19
-rw-r--r--core/java/android/content/Context.java8
-rw-r--r--core/java/android/content/Intent.java45
-rw-r--r--core/java/android/content/pm/IPackageInstaller.aidl6
-rw-r--r--core/java/android/content/pm/PackageInstaller.java135
-rw-r--r--core/java/android/content/pm/flags.aconfig8
-rw-r--r--core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl2
-rw-r--r--core/java/android/content/pm/verify/pkg/VerificationSession.java54
-rw-r--r--core/java/android/os/BaseBundle.java4
-rw-r--r--core/java/android/os/Bundle.java11
-rw-r--r--core/java/android/os/Debug.java19
-rw-r--r--core/java/android/os/Process.java2
-rw-r--r--core/java/android/security/forensic/IForensicService.aidl32
-rw-r--r--core/java/android/security/forensic/IForensicServiceCommandCallback.aidl33
-rw-r--r--core/java/android/security/forensic/IForensicServiceStateCallback.aidl31
-rw-r--r--core/java/android/util/NtpTrustedTime.java18
-rw-r--r--core/java/android/view/ViewRootImpl.java29
-rw-r--r--core/java/android/view/accessibility/flags/accessibility_flags.aconfig10
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig7
-rw-r--r--core/res/AndroidManifest.xml12
-rw-r--r--core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java30
-rw-r--r--core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java47
-rw-r--r--core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java6
-rw-r--r--core/tests/coretests/src/android/os/IpcDataCacheTest.java35
-rw-r--r--core/tests/coretests/src/android/view/ViewRootImplTest.java38
-rw-r--r--data/etc/privapp-permissions-platform.xml1
-rw-r--r--errorprone/OWNERS3
-rw-r--r--graphics/java/android/graphics/text/PositionedGlyphs.java39
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java3
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt34
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml4
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml8
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java44
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java78
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt111
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt57
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java22
-rw-r--r--libs/appfunctions/api/current.txt5
-rw-r--r--libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java27
-rw-r--r--libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java71
-rw-r--r--libs/hwui/jni/text/TextShaper.cpp97
-rw-r--r--packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml4
-rw-r--r--packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml13
-rw-r--r--packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml13
-rw-r--r--packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes_bridge.xml176
-rw-r--r--packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt6
-rw-r--r--packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt2
-rw-r--r--packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt104
-rw-r--r--packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt2
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt8
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt2
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SuggestionCard.kt166
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt8
-rw-r--r--packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/banner/SettingsBannerTest.kt (renamed from packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsBannerTest.kt)0
-rw-r--r--packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/banner/SettingsCollapsibleBannerTest.kt (renamed from packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleBannerTest.kt)0
-rw-r--r--packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SuggestionCardTest.kt84
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt91
-rw-r--r--packages/SettingsLib/res/values/strings.xml5
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java4
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java16
-rw-r--r--packages/Shell/AndroidManifest.xml1
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig10
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt11
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt47
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt10
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementMatcherTest.kt56
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt4
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt6
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt6
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java17
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt)59
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt113
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt20
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt21
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt23
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt140
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt29
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt2
-rw-r--r--packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml5
-rw-r--r--packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml17
-rw-r--r--packages/SystemUI/res/layout/hearing_tool_item.xml10
-rw-r--r--packages/SystemUI/res/values/config.xml18
-rw-r--r--packages/SystemUI/res/values/dimens.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java19
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt108
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt70
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt179
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt9
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarInitializerStoreTest.kt97
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt49
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt2
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java17
-rw-r--r--ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java3
-rw-r--r--ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java3
-rw-r--r--ravenwood/tests/bivalenttest/Android.bp62
-rw-r--r--services/Android.bp16
-rw-r--r--services/accessibility/accessibility.aconfig15
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java13
-rw-r--r--services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java4
-rw-r--r--services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java3
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java32
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java76
-rw-r--r--services/core/java/com/android/server/am/BroadcastController.java1
-rw-r--r--services/core/java/com/android/server/am/OomAdjuster.java7
-rw-r--r--services/core/java/com/android/server/am/PendingIntentRecord.java8
-rw-r--r--services/core/java/com/android/server/am/ProcessList.java1
-rw-r--r--services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java477
-rw-r--r--services/core/java/com/android/server/notification/flags.aconfig7
-rw-r--r--services/core/java/com/android/server/pm/BroadcastHelper.java95
-rw-r--r--services/core/java/com/android/server/pm/PackageInstallerService.java40
-rw-r--r--services/core/java/com/android/server/pm/PackageInstallerSession.java177
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerService.java25
-rw-r--r--services/core/java/com/android/server/pm/verify/pkg/VerifierController.java58
-rw-r--r--services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java3
-rw-r--r--services/core/java/com/android/server/security/forensic/ForensicService.java294
-rw-r--r--services/core/java/com/android/server/tv/TvInputManagerService.java114
-rw-r--r--services/core/java/com/android/server/uri/NeededUriGrants.java25
-rw-r--r--services/core/java/com/android/server/wm/ActivityClientController.java4
-rw-r--r--services/core/java/com/android/server/wm/ActivityStartController.java32
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java159
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerService.java17
-rw-r--r--services/core/java/com/android/server/wm/AppTaskImpl.java2
-rw-r--r--services/core/java/com/android/server/wm/BackNavigationController.java6
-rw-r--r--services/core/java/com/android/server/wm/InputManagerCallback.java4
-rw-r--r--services/core/java/com/android/server/wm/InsetsSourceProvider.java3
-rw-r--r--services/core/java/com/android/server/wm/Task.java4
-rw-r--r--services/core/java/com/android/server/wm/WindowProcessController.java7
-rw-r--r--services/java/com/android/server/SystemServer.java27
-rw-r--r--services/people/java/com/android/server/people/data/CallLogQueryHelper.java2
-rw-r--r--services/people/java/com/android/server/people/data/ContactsQueryHelper.java8
-rw-r--r--services/people/java/com/android/server/people/data/MmsQueryHelper.java4
-rw-r--r--services/people/java/com/android/server/people/data/SmsQueryHelper.java2
-rw-r--r--services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt15
-rw-r--r--services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java106
-rw-r--r--services/tests/appfunctions/Android.bp2
-rw-r--r--services/tests/appfunctions/src/com/android/server/appfunctions/AppFunctionManagerServiceImplTest.kt89
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java49
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java686
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt3
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java3
-rw-r--r--services/tests/security/forensic/Android.bp41
-rw-r--r--services/tests/security/forensic/AndroidManifest.xml27
-rw-r--r--services/tests/security/forensic/AndroidTest.xml32
-rw-r--r--services/tests/security/forensic/TEST_MAPPING7
-rw-r--r--services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java387
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java20
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java8
-rw-r--r--services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java20
-rw-r--r--services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java23
-rw-r--r--services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java20
-rw-r--r--services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java19
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java22
195 files changed, 5718 insertions, 2192 deletions
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index 11c5b51e23ae..98e53ab97872 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -82,3 +82,10 @@ flag {
description: "Applies the normal quota policy to FGS jobs"
bug: "341201311"
}
+
+flag {
+ name: "adjust_quota_default_constants"
+ namespace: "backstage_power"
+ description: "Adjust quota default parameters"
+ bug: "347058927"
+} \ No newline at end of file
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 46cc3f01d261..885bad5e31c8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -394,13 +394,13 @@ public final class QuotaController extends StateController {
* minutes to run its jobs.
*/
private final long[] mBucketPeriodsMs = new long[]{
- QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS,
- QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS,
- QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS,
+ QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS,
+ QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS,
+ QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS,
QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS,
0, // NEVER
QcConstants.DEFAULT_WINDOW_SIZE_RESTRICTED_MS,
- QcConstants.DEFAULT_WINDOW_SIZE_EXEMPTED_MS
+ QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS
};
/** The maximum period any bucket can have. */
@@ -454,7 +454,7 @@ public final class QuotaController extends StateController {
*/
private final long[] mEJLimitsMs = new long[]{
QcConstants.DEFAULT_EJ_LIMIT_ACTIVE_MS,
- QcConstants.DEFAULT_EJ_LIMIT_WORKING_MS,
+ QcConstants.DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS,
QcConstants.DEFAULT_EJ_LIMIT_FREQUENT_MS,
QcConstants.DEFAULT_EJ_LIMIT_RARE_MS,
0, // NEVER
@@ -476,7 +476,8 @@ public final class QuotaController extends StateController {
/**
* Length of time used to split an app's top time into chunks.
*/
- private long mEJTopAppTimeChunkSizeMs = QcConstants.DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS;
+ private long mEJTopAppTimeChunkSizeMs =
+ QcConstants.DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS;
/**
* How much EJ quota to give back to an app based on the number of top app time chunks it had.
@@ -486,7 +487,7 @@ public final class QuotaController extends StateController {
/**
* How much EJ quota to give back to an app based on each non-top user interaction.
*/
- private long mEJRewardInteractionMs = QcConstants.DEFAULT_EJ_REWARD_INTERACTION_MS;
+ private long mEJRewardInteractionMs = QcConstants.DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS;
/**
* How much EJ quota to give back to an app based on each notification seen event.
@@ -570,6 +571,8 @@ public final class QuotaController extends StateController {
} catch (RemoteException e) {
// ignored; both services live in system_server
}
+
+ processQuotaConstantsAdjustment();
}
@Override
@@ -1411,6 +1414,13 @@ public final class QuotaController extends StateController {
}
}
+ void processQuotaConstantsAdjustment() {
+ if (Flags.adjustQuotaDefaultConstants()) {
+ mQcConstants.adjustDefaultBucketWindowSizes();
+ mQcConstants.adjustDefaultEjLimits();
+ }
+ }
+
@VisibleForTesting
void incrementJobCountLocked(final int userId, @NonNull final String packageName, int count) {
final long now = sElapsedRealtimeClock.millis();
@@ -3112,14 +3122,28 @@ public final class QuotaController extends StateController {
10 * 60 * 1000L; // 10 minutes
private static final long DEFAULT_IN_QUOTA_BUFFER_MS =
30 * 1000L; // 30 seconds
- private static final long DEFAULT_WINDOW_SIZE_EXEMPTED_MS =
+ // Legacy default window size for EXEMPTED bucket
+ private static final long DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS =
DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS; // EXEMPT apps can run jobs at any time
- private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS =
+ // Legacy default window size for ACTIVE bucket
+ private static final long DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS =
DEFAULT_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS; // ACTIVE apps can run jobs at any time
- private static final long DEFAULT_WINDOW_SIZE_WORKING_MS =
+ // Legacy default window size for WORKING bucket
+ private static final long DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS =
2 * 60 * 60 * 1000L; // 2 hours
- private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS =
+ // Legacy default window size for FREQUENT bucket
+ private static final long DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS =
8 * 60 * 60 * 1000L; // 8 hours
+
+ private static final long DEFAULT_CURRENT_WINDOW_SIZE_EXEMPTED_MS =
+ 20 * 60 * 1000L; // 20 minutes.
+ private static final long DEFAULT_CURRENT_WINDOW_SIZE_ACTIVE_MS =
+ 30 * 60 * 1000L; // 30 minutes.
+ private static final long DEFAULT_CURRENT_WINDOW_SIZE_WORKING_MS =
+ 4 * 60 * 60 * 1000L; // 4 hours
+ private static final long DEFAULT_CURRENT_WINDOW_SIZE_FREQUENT_MS =
+ 12 * 60 * 60 * 1000L; // 12 hours
+
private static final long DEFAULT_WINDOW_SIZE_RARE_MS =
24 * 60 * 60 * 1000L; // 24 hours
private static final long DEFAULT_WINDOW_SIZE_RESTRICTED_MS =
@@ -3133,9 +3157,9 @@ public final class QuotaController extends StateController {
75; // 75/window = 450/hr = 1/session
private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = DEFAULT_MAX_JOB_COUNT_EXEMPTED;
private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session
- (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS);
+ (int) (60.0 * DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS);
private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session
- (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS);
+ (int) (25.0 * DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS);
private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session
(int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS);
private static final int DEFAULT_MAX_JOB_COUNT_RESTRICTED = 10;
@@ -3156,16 +3180,21 @@ public final class QuotaController extends StateController {
// TODO(267949143): set a different limit for headless system apps
private static final long DEFAULT_EJ_LIMIT_EXEMPTED_MS = 60 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_ACTIVE_MS = 30 * MINUTE_IN_MILLIS;
- private static final long DEFAULT_EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_ACTIVE_MS;
+ private static final long DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_ACTIVE_MS;
+ private static final long DEFAULT_CURRENT_EJ_LIMIT_WORKING_MS = 15 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_FREQUENT_MS = 10 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_RARE_MS = DEFAULT_EJ_LIMIT_FREQUENT_MS;
private static final long DEFAULT_EJ_LIMIT_RESTRICTED_MS = 5 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_ADDITION_SPECIAL_MS = 15 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_LIMIT_ADDITION_INSTALLER_MS = 30 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_WINDOW_SIZE_MS = 24 * HOUR_IN_MILLIS;
- private static final long DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS = 30 * SECOND_IN_MILLIS;
+ private static final long DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS =
+ 30 * SECOND_IN_MILLIS;
+ private static final long DEFAULT_CURRENT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS =
+ 5 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_REWARD_TOP_APP_MS = 10 * SECOND_IN_MILLIS;
- private static final long DEFAULT_EJ_REWARD_INTERACTION_MS = 15 * SECOND_IN_MILLIS;
+ private static final long DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS = 15 * SECOND_IN_MILLIS;
+ private static final long DEFAULT_CURRENT_EJ_REWARD_INTERACTION_MS = 5 * SECOND_IN_MILLIS;
private static final long DEFAULT_EJ_REWARD_NOTIFICATION_SEEN_MS = 0;
private static final long DEFAULT_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS = 3 * MINUTE_IN_MILLIS;
private static final long DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS = 1 * MINUTE_IN_MILLIS;
@@ -3215,28 +3244,28 @@ public final class QuotaController extends StateController {
* expected to run only {@link #ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS} within the past
* WINDOW_SIZE_MS.
*/
- public long WINDOW_SIZE_EXEMPTED_MS = DEFAULT_WINDOW_SIZE_EXEMPTED_MS;
+ public long WINDOW_SIZE_EXEMPTED_MS = DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS;
/**
* The quota window size of the particular standby bucket. Apps in this standby bucket are
* expected to run only {@link #ALLOWED_TIME_PER_PERIOD_ACTIVE_MS} within the past
* WINDOW_SIZE_MS.
*/
- public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_WINDOW_SIZE_ACTIVE_MS;
+ public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS;
/**
* The quota window size of the particular standby bucket. Apps in this standby bucket are
* expected to run only {@link #ALLOWED_TIME_PER_PERIOD_WORKING_MS} within the past
* WINDOW_SIZE_MS.
*/
- public long WINDOW_SIZE_WORKING_MS = DEFAULT_WINDOW_SIZE_WORKING_MS;
+ public long WINDOW_SIZE_WORKING_MS = DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS;
/**
* The quota window size of the particular standby bucket. Apps in this standby bucket are
* expected to run only {@link #ALLOWED_TIME_PER_PERIOD_FREQUENT_MS} within the past
* WINDOW_SIZE_MS.
*/
- public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_WINDOW_SIZE_FREQUENT_MS;
+ public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS;
/**
* The quota window size of the particular standby bucket. Apps in this standby bucket are
@@ -3397,7 +3426,7 @@ public final class QuotaController extends StateController {
* standby bucket can only have expedited job sessions totalling EJ_LIMIT (without factoring
* in any rewards or free EJs).
*/
- public long EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_WORKING_MS;
+ public long EJ_LIMIT_WORKING_MS = DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS;
/**
* The total expedited job session limit of the particular standby bucket. Apps in this
@@ -3441,7 +3470,7 @@ public final class QuotaController extends StateController {
/**
* Length of time used to split an app's top time into chunks.
*/
- public long EJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS;
+ public long EJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS;
/**
* How much EJ quota to give back to an app based on the number of top app time chunks it
@@ -3452,7 +3481,7 @@ public final class QuotaController extends StateController {
/**
* How much EJ quota to give back to an app based on each non-top user interaction.
*/
- public long EJ_REWARD_INTERACTION_MS = DEFAULT_EJ_REWARD_INTERACTION_MS;
+ public long EJ_REWARD_INTERACTION_MS = DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS;
/**
* How much EJ quota to give back to an app based on each notification seen event.
@@ -3470,6 +3499,52 @@ public final class QuotaController extends StateController {
*/
public long EJ_GRACE_PERIOD_TOP_APP_MS = DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS;
+ void adjustDefaultBucketWindowSizes() {
+ WINDOW_SIZE_EXEMPTED_MS = DEFAULT_CURRENT_WINDOW_SIZE_EXEMPTED_MS;
+ WINDOW_SIZE_ACTIVE_MS = DEFAULT_CURRENT_WINDOW_SIZE_ACTIVE_MS;
+ WINDOW_SIZE_WORKING_MS = DEFAULT_CURRENT_WINDOW_SIZE_WORKING_MS;
+ WINDOW_SIZE_FREQUENT_MS = DEFAULT_CURRENT_WINDOW_SIZE_FREQUENT_MS;
+
+ mBucketPeriodsMs[EXEMPTED_INDEX] = Math.max(
+ mAllowedTimePerPeriodMs[EXEMPTED_INDEX],
+ Math.min(MAX_PERIOD_MS, WINDOW_SIZE_EXEMPTED_MS));
+ mBucketPeriodsMs[ACTIVE_INDEX] = Math.max(
+ mAllowedTimePerPeriodMs[ACTIVE_INDEX],
+ Math.min(MAX_PERIOD_MS, WINDOW_SIZE_ACTIVE_MS));
+ mBucketPeriodsMs[WORKING_INDEX] = Math.max(
+ mAllowedTimePerPeriodMs[WORKING_INDEX],
+ Math.min(MAX_PERIOD_MS, WINDOW_SIZE_WORKING_MS));
+ mBucketPeriodsMs[FREQUENT_INDEX] = Math.max(
+ mAllowedTimePerPeriodMs[FREQUENT_INDEX],
+ Math.min(MAX_PERIOD_MS, WINDOW_SIZE_FREQUENT_MS));
+ }
+
+ void adjustDefaultEjLimits() {
+ EJ_LIMIT_WORKING_MS = DEFAULT_CURRENT_EJ_LIMIT_WORKING_MS;
+ EJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_CURRENT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS;
+ EJ_REWARD_INTERACTION_MS = DEFAULT_CURRENT_EJ_REWARD_INTERACTION_MS;
+
+ // The limit must be in the range [15 minutes, active limit].
+ mEJLimitsMs[WORKING_INDEX] = Math.max(15 * MINUTE_IN_MILLIS,
+ Math.min(mEJLimitsMs[ACTIVE_INDEX], EJ_LIMIT_WORKING_MS));
+
+ // Limit interaction reward to be in the range [5 seconds, 15 minutes] per event.
+ mEJRewardInteractionMs = Math.min(15 * MINUTE_IN_MILLIS,
+ Math.max(5 * SECOND_IN_MILLIS, EJ_REWARD_INTERACTION_MS));
+
+ // Limit chunking to be in the range [1 millisecond, 15 minutes] per event.
+ long newChunkSizeMs = Math.min(15 * MINUTE_IN_MILLIS,
+ Math.max(1, EJ_TOP_APP_TIME_CHUNK_SIZE_MS));
+ mEJTopAppTimeChunkSizeMs = newChunkSizeMs;
+ if (mEJTopAppTimeChunkSizeMs < mEJRewardTopAppMs) {
+ // Not making chunk sizes and top rewards to be the upper/lower
+ // limits of the other to allow trying different policies. Just log
+ // the discrepancy.
+ Slog.w(TAG, "EJ top app time chunk less than reward: "
+ + mEJTopAppTimeChunkSizeMs + " vs " + mEJRewardTopAppMs);
+ }
+ }
+
public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
@NonNull String key) {
switch (key) {
@@ -3638,7 +3713,9 @@ public final class QuotaController extends StateController {
case KEY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS:
// We don't need to re-evaluate execution stats or constraint status for this.
EJ_TOP_APP_TIME_CHUNK_SIZE_MS =
- properties.getLong(key, DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS);
+ properties.getLong(key, Flags.adjustQuotaDefaultConstants()
+ ? DEFAULT_CURRENT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS :
+ DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS);
// Limit chunking to be in the range [1 millisecond, 15 minutes] per event.
long newChunkSizeMs = Math.min(15 * MINUTE_IN_MILLIS,
Math.max(1, EJ_TOP_APP_TIME_CHUNK_SIZE_MS));
@@ -3674,7 +3751,9 @@ public final class QuotaController extends StateController {
case KEY_EJ_REWARD_INTERACTION_MS:
// We don't need to re-evaluate execution stats or constraint status for this.
EJ_REWARD_INTERACTION_MS =
- properties.getLong(key, DEFAULT_EJ_REWARD_INTERACTION_MS);
+ properties.getLong(key, Flags.adjustQuotaDefaultConstants()
+ ? DEFAULT_CURRENT_EJ_REWARD_INTERACTION_MS :
+ DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS);
// Limit interaction reward to be in the range [5 seconds, 15 minutes] per
// event.
mEJRewardInteractionMs = Math.min(15 * MINUTE_IN_MILLIS,
@@ -3748,14 +3827,23 @@ public final class QuotaController extends StateController {
MAX_EXECUTION_TIME_MS = properties.getLong(KEY_MAX_EXECUTION_TIME_MS,
DEFAULT_MAX_EXECUTION_TIME_MS);
WINDOW_SIZE_EXEMPTED_MS = properties.getLong(KEY_WINDOW_SIZE_EXEMPTED_MS,
- DEFAULT_WINDOW_SIZE_EXEMPTED_MS);
+ Flags.adjustQuotaDefaultConstants()
+ ? DEFAULT_CURRENT_WINDOW_SIZE_EXEMPTED_MS :
+ DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS);
WINDOW_SIZE_ACTIVE_MS = properties.getLong(KEY_WINDOW_SIZE_ACTIVE_MS,
- DEFAULT_WINDOW_SIZE_ACTIVE_MS);
+ Flags.adjustQuotaDefaultConstants()
+ ? DEFAULT_CURRENT_WINDOW_SIZE_ACTIVE_MS :
+ DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS);
WINDOW_SIZE_WORKING_MS =
- properties.getLong(KEY_WINDOW_SIZE_WORKING_MS, DEFAULT_WINDOW_SIZE_WORKING_MS);
+ properties.getLong(KEY_WINDOW_SIZE_WORKING_MS,
+ Flags.adjustQuotaDefaultConstants()
+ ? DEFAULT_CURRENT_WINDOW_SIZE_WORKING_MS :
+ DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS);
WINDOW_SIZE_FREQUENT_MS =
properties.getLong(KEY_WINDOW_SIZE_FREQUENT_MS,
- DEFAULT_WINDOW_SIZE_FREQUENT_MS);
+ Flags.adjustQuotaDefaultConstants()
+ ? DEFAULT_CURRENT_WINDOW_SIZE_FREQUENT_MS :
+ DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS);
WINDOW_SIZE_RARE_MS = properties.getLong(KEY_WINDOW_SIZE_RARE_MS,
DEFAULT_WINDOW_SIZE_RARE_MS);
WINDOW_SIZE_RESTRICTED_MS =
@@ -3926,7 +4014,9 @@ public final class QuotaController extends StateController {
EJ_LIMIT_ACTIVE_MS = properties.getLong(
KEY_EJ_LIMIT_ACTIVE_MS, DEFAULT_EJ_LIMIT_ACTIVE_MS);
EJ_LIMIT_WORKING_MS = properties.getLong(
- KEY_EJ_LIMIT_WORKING_MS, DEFAULT_EJ_LIMIT_WORKING_MS);
+ KEY_EJ_LIMIT_WORKING_MS, Flags.adjustQuotaDefaultConstants()
+ ? DEFAULT_CURRENT_EJ_LIMIT_WORKING_MS :
+ DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS);
EJ_LIMIT_FREQUENT_MS = properties.getLong(
KEY_EJ_LIMIT_FREQUENT_MS, DEFAULT_EJ_LIMIT_FREQUENT_MS);
EJ_LIMIT_RARE_MS = properties.getLong(
@@ -4289,6 +4379,13 @@ public final class QuotaController extends StateController {
@Override
public void dumpControllerStateLocked(final IndentingPrintWriter pw,
final Predicate<JobStatus> predicate) {
+ pw.println("Flags: ");
+ pw.println(" " + Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS
+ + ": " + Flags.adjustQuotaDefaultConstants());
+ pw.println(" " + Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS
+ + ": " + Flags.enforceQuotaPolicyToFgsJobs());
+ pw.println();
+
pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis());
pw.println();
diff --git a/core/api/current.txt b/core/api/current.txt
index f59b57528ef6..14a91c9033c5 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -8781,7 +8781,6 @@ package android.app.admin {
package android.app.appfunctions {
@FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager {
- method @Deprecated @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>);
method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>);
@@ -8793,9 +8792,7 @@ package android.app.appfunctions {
@FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service {
ctor public AppFunctionService();
method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
- method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
- method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
- method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+ method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index fa4fc43c3418..349d06ca8ad6 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -4170,9 +4170,11 @@ package android.content.pm {
}
public class PackageInstaller {
+ method @FlaggedApi("android.content.pm.verification_service") @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public final int getVerificationPolicy();
method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull java.io.File, int) throws android.content.pm.PackageInstaller.PackageParsingException;
method @FlaggedApi("android.content.pm.read_install_info") @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull android.os.ParcelFileDescriptor, @Nullable String, int) throws android.content.pm.PackageInstaller.PackageParsingException;
method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setPermissionsResult(int, boolean);
+ method @FlaggedApi("android.content.pm.verification_service") @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public final boolean setVerificationPolicy(int);
field public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL";
field public static final String ACTION_CONFIRM_PRE_APPROVAL = "android.content.pm.action.CONFIRM_PRE_APPROVAL";
field public static final int DATA_LOADER_TYPE_INCREMENTAL = 2; // 0x2
@@ -4183,12 +4185,20 @@ package android.content.pm {
field @FlaggedApi("android.content.pm.archiving") public static final String EXTRA_DELETE_FLAGS = "android.content.pm.extra.DELETE_FLAGS";
field public static final String EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS";
field @Deprecated public static final String EXTRA_RESOLVED_BASE_PATH = "android.content.pm.extra.RESOLVED_BASE_PATH";
+ field @FlaggedApi("android.content.pm.verification_service") public static final String EXTRA_VERIFICATION_FAILURE_REASON = "android.content.pm.extra.VERIFICATION_FAILURE_REASON";
field public static final int LOCATION_DATA_APP = 0; // 0x0
field public static final int LOCATION_MEDIA_DATA = 2; // 0x2
field public static final int LOCATION_MEDIA_OBB = 1; // 0x1
field public static final int REASON_CONFIRM_PACKAGE_CHANGE = 0; // 0x0
field public static final int REASON_OWNERSHIP_CHANGED = 1; // 0x1
field public static final int REASON_REMIND_OWNERSHIP = 2; // 0x2
+ field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE = 1; // 0x1
+ field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED = 2; // 0x2
+ field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_FAILED_REASON_UNKNOWN = 0; // 0x0
+ field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_POLICY_BLOCK_FAIL_CLOSED = 3; // 0x3
+ field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_POLICY_BLOCK_FAIL_OPEN = 1; // 0x1
+ field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_POLICY_BLOCK_FAIL_WARN = 2; // 0x2
+ field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_POLICY_NONE = 0; // 0x0
}
public static class PackageInstaller.InstallInfo {
@@ -4635,12 +4645,13 @@ package android.content.pm.verify.pkg {
method @NonNull public android.content.pm.SigningInfo getSigningInfo();
method @NonNull public android.net.Uri getStagedPackageUri();
method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long getTimeoutTime();
+ method public int getVerificationPolicy();
method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus);
method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus, @NonNull android.os.PersistableBundle);
method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationIncomplete(int);
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public boolean setVerificationPolicy(int);
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationSession> CREATOR;
- field public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2; // 0x2
field public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1; // 0x1
field public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0; // 0x0
}
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 7273e64846c0..36fc65a76d53 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -1031,7 +1031,9 @@ public class ActivityManager {
| PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL;
/**
- * All implicit capabilities. There are capabilities that process automatically have.
+ * All implicit capabilities. This capability set is currently only used for processes under
+ * active instrumentation. The intent is to allow CTS tests to always have these capabilities
+ * so that every test doesn't need to launch FGS.
* @hide
*/
@TestApi
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 3bd121a4a19b..f80121d0c9b6 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -1328,7 +1328,8 @@ public abstract class ActivityManagerInternal {
* Add a creator token for all embedded intents (stored as extra) of the given intent.
*
* @param intent The given intent
+ * @param creatorPackage the package name of the creator app.
* @hide
*/
- public abstract void addCreatorToken(Intent intent);
+ public abstract void addCreatorToken(Intent intent, String creatorPackage);
}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 99625ac20e60..e7f4dbc24022 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -7346,6 +7346,8 @@ public final class ActivityThread extends ClientTransactionHandler
}
}
+ VMDebug.setUserId(UserHandle.myUserId());
+ VMDebug.addApplication(data.appInfo.packageName);
// send up app name; do this *before* waiting for debugger
Process.setArgV0(data.processName);
android.ddm.DdmHandleAppName.setAppName(data.processName,
@@ -7868,9 +7870,20 @@ public final class ActivityThread extends ClientTransactionHandler
file.getParentFile().mkdirs();
Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024);
}
+
+ if (ii.packageName != null) {
+ VMDebug.addApplication(ii.packageName);
+ }
}
private void handleFinishInstrumentationWithoutRestart() {
+ LoadedApk loadedApk = getApplication().mLoadedApk;
+ // Only remove instrumentation app if this was not a self-testing app.
+ if (mInstrumentationPackageName != null && loadedApk != null && !mInstrumentationPackageName
+ .equals(loadedApk.mPackageName)) {
+ VMDebug.removeApplication(mInstrumentationPackageName);
+ }
+
mInstrumentation.onDestroy();
mInstrumentationPackageName = null;
mInstrumentationAppDir = null;
@@ -8904,6 +8917,11 @@ public final class ActivityThread extends ClientTransactionHandler
return false;
}
+ void addApplication(@NonNull Application app) {
+ mAllApplications.add(app);
+ VMDebug.addApplication(app.mLoadedApk.mPackageName);
+ }
+
@Override
public boolean isInDensityCompatMode() {
return mDensityCompatMode;
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 1df8f63aa402..1e45d6fd1674 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -1478,7 +1478,7 @@ public final class LoadedApk {
+ " package " + mPackageName + ": " + e.toString(), e);
}
}
- mActivityThread.mAllApplications.add(app);
+ mActivityThread.addApplication(app);
mApplication = app;
if (!allowDuplicateInstances) {
synchronized (sApplications) {
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 55296ebbf18e..c17da249f322 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -16,8 +16,6 @@
package android.app;
-import static android.text.TextUtils.formatSimple;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
@@ -32,10 +30,11 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.FastPrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -43,14 +42,12 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.WeakHashMap;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
@@ -227,24 +224,12 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
- * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note that all
- * reserved values cause the cache to be skipped.
+ * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note
+ * that all values cause the cache to be skipped.
*/
- // This is the initial value of all cache keys. It is changed when a cache is invalidated.
private static final int NONCE_UNSET = 0;
- // This value is used in two ways. First, it is used internally to indicate that the cache is
- // disabled for the current query. Secondly, it is used to global disable the cache across the
- // entire system. Once a cache is disabled, there is no way to enable it again. The global
- // behavior is unused and will likely be removed in the future.
private static final int NONCE_DISABLED = 1;
- // The cache is corked, which means that clients must act as though the cache is always
- // invalid. This is used when the server is processing updates that continuously invalidate
- // caches. Rather than issuing individual invalidations (which has a performance penalty),
- // the server corks the caches at the start of the process and uncorks at the end of the
- // process.
private static final int NONCE_CORKED = 2;
- // The cache is bypassed for the current query. Unlike UNSET and CORKED, this value is never
- // written to global store.
private static final int NONCE_BYPASS = 3;
private static boolean isReservedNonce(long n) {
@@ -252,7 +237,7 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
- * The names of the reserved nonces.
+ * The names of the nonces
*/
private static final String[] sNonceName =
new String[]{ "unset", "disabled", "corked", "bypass" };
@@ -292,17 +277,32 @@ public class PropertyInvalidatedCache<Query, Result> {
private static final Object sCorkLock = new Object();
/**
+ * Record the number of invalidate or cork calls that were nops because the cache was already
+ * corked. This is static because invalidation is done in a static context. Entries are
+ * indexed by the cache property.
+ */
+ @GuardedBy("sCorkLock")
+ private static final HashMap<String, Long> sCorkedInvalidates = new HashMap<>();
+
+ /**
+ * A map of cache keys that we've "corked". (The values are counts.) When a cache key is
+ * corked, we skip the cache invalidate when the cache key is in the unset state --- that
+ * is, when a cache key is corked, an invalidation does not enable the cache if somebody
+ * else hasn't disabled it.
+ */
+ @GuardedBy("sCorkLock")
+ private static final HashMap<String, Integer> sCorks = new HashMap<>();
+
+ /**
* A lock for the global list of caches and cache keys. This must never be taken inside mLock
* or sCorkLock.
*/
private static final Object sGlobalLock = new Object();
/**
- * A map of cache keys that have been disabled in the local process. When a key is disabled
- * locally, existing caches are disabled and the key is saved in this map. Future cache
- * instances that use the same key will be disabled in their constructor. Note that "disabled"
- * means the cache is not used in this process. Invalidation still proceeds normally, because
- * the cache may be used in other processes.
+ * A map of cache keys that have been disabled in the local process. When a key is
+ * disabled locally, existing caches are disabled and the key is saved in this map.
+ * Future cache instances that use the same key will be disabled in their constructor.
*/
@GuardedBy("sGlobalLock")
private static final HashSet<String> sDisabledKeys = new HashSet<>();
@@ -315,6 +315,14 @@ public class PropertyInvalidatedCache<Query, Result> {
private static final WeakHashMap<PropertyInvalidatedCache, Void> sCaches = new WeakHashMap<>();
/**
+ * Counts of the number of times a cache key was invalidated. Invalidation occurs in a static
+ * context with no cache object available, so this is a static map. Entries are indexed by
+ * the cache property.
+ */
+ @GuardedBy("sGlobalLock")
+ private static final HashMap<String, Long> sInvalidates = new HashMap<>();
+
+ /**
* If sEnabled is false then all cache operations are stubbed out. Set
* it to false inside test processes.
*/
@@ -326,6 +334,12 @@ public class PropertyInvalidatedCache<Query, Result> {
private final String mPropertyName;
/**
+ * Handle to the {@code mPropertyName} property, transitioning to non-{@code null} once the
+ * property exists on the system.
+ */
+ private volatile SystemProperties.Handle mPropertyHandle;
+
+ /**
* The name by which this cache is known. This should normally be the
* binder call that is being cached, but the constructors default it to
* the property name.
@@ -355,13 +369,7 @@ public class PropertyInvalidatedCache<Query, Result> {
private final LinkedHashMap<Query, Result> mCache;
/**
- * The nonce handler for this cache.
- */
- @GuardedBy("mLock")
- private final NonceHandler mNonce;
-
- /**
- * The last nonce value that was observed.
+ * The last value of the {@code mPropertyHandle} that we observed.
*/
@GuardedBy("mLock")
private long mLastSeenNonce = NONCE_UNSET;
@@ -377,297 +385,6 @@ public class PropertyInvalidatedCache<Query, Result> {
private final int mMaxEntries;
/**
- * A class to manage cache keys. There is a single instance of this class for each unique key
- * that is shared by all cache instances that use that key. This class is abstract; subclasses
- * use different storage mechanisms for the nonces.
- */
- private static abstract class NonceHandler {
- // The name of the nonce.
- final String mName;
-
- // A lock to synchronize corking and invalidation.
- protected final Object mLock = new Object();
-
- // Count the number of times the property name was invalidated.
- @GuardedBy("mLock")
- private int mInvalidated = 0;
-
- // Count the number of times invalidate or cork calls were nops because the cache was
- // already corked.
- @GuardedBy("mLock")
- private int mCorkedInvalidates = 0;
-
- // Count the number of corks against this property name. This is not a statistic. It
- // increases when the property is corked and decreases when the property is uncorked.
- // Invalidation requests are ignored when the cork count is greater than zero.
- @GuardedBy("mLock")
- private int mCorks = 0;
-
- // The methods to get and set a nonce from whatever storage is being used.
- abstract long getNonce();
- abstract void setNonce(long value);
-
- NonceHandler(@NonNull String name) {
- mName = name;
- }
-
- /**
- * Write the invalidation nonce for the property.
- */
- void invalidate() {
- if (!sEnabled) {
- if (DEBUG) {
- Log.d(TAG, formatSimple("cache invalidate %s suppressed", mName));
- }
- return;
- }
-
- synchronized (mLock) {
- if (mCorks > 0) {
- if (DEBUG) {
- Log.d(TAG, "ignoring invalidation due to cork: " + mName);
- }
- mCorkedInvalidates++;
- return;
- }
-
- final long nonce = getNonce();
- if (nonce == NONCE_DISABLED) {
- if (DEBUG) {
- Log.d(TAG, "refusing to invalidate disabled cache: " + mName);
- }
- return;
- }
-
- long newValue;
- do {
- newValue = NoPreloadHolder.next();
- } while (isReservedNonce(newValue));
- if (DEBUG) {
- Log.d(TAG, formatSimple(
- "invalidating cache [%s]: [%s] -> [%s]",
- mName, nonce, Long.toString(newValue)));
- }
- // There is a small race with concurrent disables here. A compare-and-exchange
- // property operation would be required to eliminate the race condition.
- setNonce(newValue);
- mInvalidated++;
- }
- }
-
- void cork() {
- if (!sEnabled) {
- if (DEBUG) {
- Log.d(TAG, formatSimple("cache corking %s suppressed", mName));
- }
- return;
- }
-
- synchronized (mLock) {
- int numberCorks = mCorks;
- if (DEBUG) {
- Log.d(TAG, formatSimple(
- "corking %s: numberCorks=%s", mName, numberCorks));
- }
-
- // If we're the first ones to cork this cache, set the cache to the corked state so
- // existing caches talk directly to their services while we've corked updates.
- // Make sure we don't clobber a disabled cache value.
-
- // TODO: we can skip this property write and leave the cache enabled if the
- // caller promises not to make observable changes to the cache backing state before
- // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair.
- // Implement this more dangerous mode of operation if necessary.
- if (numberCorks == 0) {
- final long nonce = getNonce();
- if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) {
- setNonce(NONCE_CORKED);
- }
- } else {
- mCorkedInvalidates++;
- }
- mCorks++;
- if (DEBUG) {
- Log.d(TAG, "corked: " + mName);
- }
- }
- }
-
- void uncork() {
- if (!sEnabled) {
- if (DEBUG) {
- Log.d(TAG, formatSimple("cache uncorking %s suppressed", mName));
- }
- return;
- }
-
- synchronized (mLock) {
- int numberCorks = --mCorks;
- if (DEBUG) {
- Log.d(TAG, formatSimple(
- "uncorking %s: numberCorks=%s", mName, numberCorks));
- }
-
- if (numberCorks < 0) {
- throw new AssertionError("cork underflow: " + mName);
- }
- if (numberCorks == 0) {
- // The property is fully uncorked and can be invalidated normally.
- invalidate();
- if (DEBUG) {
- Log.d(TAG, "uncorked: " + mName);
- }
- }
- }
- }
-
- void disable() {
- if (!sEnabled) {
- return;
- }
- synchronized (mLock) {
- setNonce(NONCE_DISABLED);
- }
- }
-
- record Stats(int invalidated, int corkedInvalidates) {}
- Stats getStats() {
- synchronized (mLock) {
- return new Stats(mInvalidated, mCorkedInvalidates);
- }
- }
- }
-
- /**
- * Manage nonces that are stored in a system property.
- */
- private static final class NonceSysprop extends NonceHandler {
- // A handle to the property, for fast lookups.
- private volatile SystemProperties.Handle mHandle;
-
- NonceSysprop(@NonNull String name) {
- super(name);
- }
-
- @Override
- long getNonce() {
- if (mHandle == null) {
- synchronized (mLock) {
- mHandle = SystemProperties.find(mName);
- if (mHandle == null) {
- return NONCE_UNSET;
- }
- }
- }
- return mHandle.getLong(NONCE_UNSET);
- }
-
- @Override
- void setNonce(long value) {
- // Failing to set the nonce is a fatal error. Failures setting a system property have
- // been reported; given that the failure is probably transient, this function includes
- // a retry.
- final String str = Long.toString(value);
- RuntimeException failure = null;
- for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) {
- try {
- SystemProperties.set(mName, str);
- if (attempt > 0) {
- // This log is not guarded. Based on known bug reports, it should
- // occur once a week or less. The purpose of the log message is to
- // identify the retries as a source of delay that might be otherwise
- // be attributed to the cache itself.
- Log.w(TAG, "Nonce set after " + attempt + " tries");
- }
- return;
- } catch (RuntimeException e) {
- if (failure == null) {
- failure = e;
- }
- try {
- Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS);
- } catch (InterruptedException x) {
- // Ignore this exception. The desired delay is only approximate and
- // there is no issue if the sleep sometimes terminates early.
- }
- }
- }
- // This point is reached only if SystemProperties.set() fails at least once.
- // Rethrow the first exception that was received.
- throw failure;
- }
- }
-
- /**
- * SystemProperties and shared storage are protected and cannot be written by random
- * processes. So, for testing purposes, the NonceTest handler stores the nonce locally.
- */
- private static class NonceTest extends NonceHandler {
- // The saved nonce.
- private long mValue;
-
- // If this flag is false, the handler has been shutdown during a test. Access to the
- // handler in this state is an error.
- private boolean mIsActive = true;
-
- NonceTest(@NonNull String name) {
- super(name);
- }
-
- void shutdown() {
- // The handler has been discarded as part of test cleanup. Further access is an
- // error.
- mIsActive = false;
- }
-
- @Override
- long getNonce() {
- if (!mIsActive) {
- throw new IllegalStateException("handler " + mName + " is shutdown");
- }
- return mValue;
- }
-
- @Override
- void setNonce(long value) {
- if (!mIsActive) {
- throw new IllegalStateException("handler " + mName + " is shutdown");
- }
- mValue = value;
- }
- }
-
- /**
- * A static list of nonce handlers, indexed by name. NonceHandlers can be safely shared by
- * multiple threads, and can therefore be shared by multiple instances of the same cache, and
- * with static calls (see {@link #invalidateCache}. Addition and removal are guarded by the
- * global lock, to ensure that duplicates are not created.
- */
- private static final ConcurrentHashMap<String, NonceHandler> sHandlers
- = new ConcurrentHashMap<>();
-
- /**
- * Return the proper nonce handler, based on the property name.
- */
- private static NonceHandler getNonceHandler(@NonNull String name) {
- NonceHandler h = sHandlers.get(name);
- if (h == null) {
- synchronized (sGlobalLock) {
- h = sHandlers.get(name);
- if (h == null) {
- if (name.startsWith("cache_key.test.")) {
- h = new NonceTest(name);
- } else {
- h = new NonceSysprop(name);
- }
- sHandlers.put(name, h);
- }
- }
- }
- return h;
- }
-
- /**
* Make a new property invalidated cache. This constructor names the cache after the
* property name. New clients should prefer the constructor that takes an explicit
* cache name.
@@ -700,7 +417,6 @@ public class PropertyInvalidatedCache<Query, Result> {
mPropertyName = propertyName;
validateCacheKey(mPropertyName);
mCacheName = cacheName;
- mNonce = getNonceHandler(mPropertyName);
mMaxEntries = maxEntries;
mComputer = new DefaultComputer<>(this);
mCache = createMap();
@@ -725,7 +441,6 @@ public class PropertyInvalidatedCache<Query, Result> {
mPropertyName = createPropertyName(module, api);
validateCacheKey(mPropertyName);
mCacheName = cacheName;
- mNonce = getNonceHandler(mPropertyName);
mMaxEntries = maxEntries;
mComputer = computer;
mCache = createMap();
@@ -769,58 +484,130 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
- * Enable or disable testing. At this time, no action is taken when testing begins.
+ * SystemProperties are protected and cannot be written (or read, usually) by random
+ * processes. So, for testing purposes, the methods have a bypass mode that reads and
+ * writes to a HashMap and does not go out to the SystemProperties at all.
+ */
+
+ // If true, the cache might be under test. If false, there is no testing in progress.
+ private static volatile boolean sTesting = false;
+
+ // If sTesting is true then keys that are under test are in this map.
+ private static final HashMap<String, Long> sTestingPropertyMap = new HashMap<>();
+
+ /**
+ * Enable or disable testing. The testing property map is cleared every time this
+ * method is called.
* @hide
*/
@TestApi
public static void setTestMode(boolean mode) {
- if (mode) {
- // No action when testing begins.
- } else {
- resetAfterTest();
+ sTesting = mode;
+ synchronized (sTestingPropertyMap) {
+ sTestingPropertyMap.clear();
}
}
/**
- * Enable testing the specific cache key. This is a legacy API that will be removed as part of
- * b/360897450.
- * @hide
+ * Enable testing the specific cache key. Only keys in the map are subject to testing.
+ * There is no method to stop testing a property name. Just disable the test mode.
*/
- @TestApi
- public void testPropertyName() {
+ private static void testPropertyName(@NonNull String name) {
+ synchronized (sTestingPropertyMap) {
+ sTestingPropertyMap.put(name, (long) NONCE_UNSET);
+ }
}
/**
- * Clean up when testing ends. All NonceTest handlers are erased from the global list and are
- * poisoned, just in case the test program has retained a handle to one of the associated
- * caches.
+ * Enable testing the specific cache key. Only keys in the map are subject to testing.
+ * There is no method to stop testing a property name. Just disable the test mode.
* @hide
*/
- @VisibleForTesting
- public static void resetAfterTest() {
- synchronized (sGlobalLock) {
- for (Iterator<String> e = sHandlers.keys().asIterator(); e.hasNext(); ) {
- String s = e.next();
- final NonceHandler h = sHandlers.get(s);
- if (h instanceof NonceTest t) {
- t.shutdown();
- sHandlers.remove(s);
+ @TestApi
+ public void testPropertyName() {
+ testPropertyName(mPropertyName);
+ }
+
+ // Read the system property associated with the current cache. This method uses the
+ // handle for faster reading.
+ private long getCurrentNonce() {
+ if (sTesting) {
+ synchronized (sTestingPropertyMap) {
+ Long n = sTestingPropertyMap.get(mPropertyName);
+ if (n != null) {
+ return n;
+ }
+ }
+ }
+
+ SystemProperties.Handle handle = mPropertyHandle;
+ if (handle == null) {
+ handle = SystemProperties.find(mPropertyName);
+ if (handle == null) {
+ return NONCE_UNSET;
+ }
+ mPropertyHandle = handle;
+ }
+ return handle.getLong(NONCE_UNSET);
+ }
+
+ // Write the nonce in a static context. No handle is available.
+ private static void setNonce(String name, long val) {
+ if (sTesting) {
+ synchronized (sTestingPropertyMap) {
+ Long n = sTestingPropertyMap.get(name);
+ if (n != null) {
+ sTestingPropertyMap.put(name, val);
+ return;
}
}
}
+ RuntimeException failure = null;
+ for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) {
+ try {
+ SystemProperties.set(name, Long.toString(val));
+ if (attempt > 0) {
+ // This log is not guarded. Based on known bug reports, it should
+ // occur once a week or less. The purpose of the log message is to
+ // identify the retries as a source of delay that might be otherwise
+ // be attributed to the cache itself.
+ Log.w(TAG, "Nonce set after " + attempt + " tries");
+ }
+ return;
+ } catch (RuntimeException e) {
+ if (failure == null) {
+ failure = e;
+ }
+ try {
+ Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS);
+ } catch (InterruptedException x) {
+ // Ignore this exception. The desired delay is only approximate and
+ // there is no issue if the sleep sometimes terminates early.
+ }
+ }
+ }
+ // This point is reached only if SystemProperties.set() fails at least once.
+ // Rethrow the first exception that was received.
+ throw failure;
}
- // Read the nonce associated with the current cache.
- @GuardedBy("mLock")
- private long getCurrentNonce() {
- return mNonce.getNonce();
+ // Set the nonce in a static context. No handle is available.
+ private static long getNonce(String name) {
+ if (sTesting) {
+ synchronized (sTestingPropertyMap) {
+ Long n = sTestingPropertyMap.get(name);
+ if (n != null) {
+ return n;
+ }
+ }
+ }
+ return SystemProperties.getLong(name, NONCE_UNSET);
}
/**
- * Forget all cached values. This is used by a client when the server exits. Since the
- * server has exited, the cache values are no longer valid, but the server is no longer
- * present to invalidate the cache. Note that this is not necessary if the server is
- * system_server, because the entire operating system reboots if that process exits.
+ * Forget all cached values.
+ * TODO(216112648) remove this as a public API. Clients should invalidate caches, not clear
+ * them.
* @hide
*/
public final void clear() {
@@ -887,7 +674,7 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
- * Disable the use of this cache in this process. This method is used internally and during
+ * Disable the use of this cache in this process. This method is using internally and during
* testing. To disable a cache in normal code, use disableLocal(). A disabled cache cannot
* be re-enabled.
* @hide
@@ -996,7 +783,7 @@ public class PropertyInvalidatedCache<Query, Result> {
if (DEBUG) {
if (!mDisabled) {
- Log.d(TAG, formatSimple(
+ Log.d(TAG, TextUtils.formatSimple(
"cache %s %s for %s",
cacheName(), sNonceName[(int) currentNonce], queryToString(query)));
}
@@ -1011,7 +798,7 @@ public class PropertyInvalidatedCache<Query, Result> {
if (cachedResult != null) mHits++;
} else {
if (DEBUG) {
- Log.d(TAG, formatSimple(
+ Log.d(TAG, TextUtils.formatSimple(
"clearing cache %s of %d entries because nonce changed [%s] -> [%s]",
cacheName(), mCache.size(),
mLastSeenNonce, currentNonce));
@@ -1037,7 +824,7 @@ public class PropertyInvalidatedCache<Query, Result> {
if (currentNonce != afterRefreshNonce) {
currentNonce = afterRefreshNonce;
if (DEBUG) {
- Log.d(TAG, formatSimple(
+ Log.d(TAG, TextUtils.formatSimple(
"restarting %s %s because nonce changed in refresh",
cacheName(),
queryToString(query)));
@@ -1108,7 +895,10 @@ public class PropertyInvalidatedCache<Query, Result> {
* @param name Name of the cache-key property to invalidate
*/
private static void disableSystemWide(@NonNull String name) {
- getNonceHandler(name).disable();
+ if (!sEnabled) {
+ return;
+ }
+ setNonce(name, NONCE_DISABLED);
}
/**
@@ -1118,7 +908,7 @@ public class PropertyInvalidatedCache<Query, Result> {
*/
@TestApi
public void invalidateCache() {
- mNonce.invalidate();
+ invalidateCache(mPropertyName);
}
/**
@@ -1141,7 +931,59 @@ public class PropertyInvalidatedCache<Query, Result> {
* @hide
*/
public static void invalidateCache(@NonNull String name) {
- getNonceHandler(name).invalidate();
+ if (!sEnabled) {
+ if (DEBUG) {
+ Log.w(TAG, TextUtils.formatSimple(
+ "cache invalidate %s suppressed", name));
+ }
+ return;
+ }
+
+ // Take the cork lock so invalidateCache() racing against corkInvalidations() doesn't
+ // clobber a cork-written NONCE_UNSET with a cache key we compute before the cork.
+ // The property service is single-threaded anyway, so we don't lose any concurrency by
+ // taking the cork lock around cache invalidations. If we see contention on this lock,
+ // we're invalidating too often.
+ synchronized (sCorkLock) {
+ Integer numberCorks = sCorks.get(name);
+ if (numberCorks != null && numberCorks > 0) {
+ if (DEBUG) {
+ Log.d(TAG, "ignoring invalidation due to cork: " + name);
+ }
+ final long count = sCorkedInvalidates.getOrDefault(name, (long) 0);
+ sCorkedInvalidates.put(name, count + 1);
+ return;
+ }
+ invalidateCacheLocked(name);
+ }
+ }
+
+ @GuardedBy("sCorkLock")
+ private static void invalidateCacheLocked(@NonNull String name) {
+ // There's no race here: we don't require that values strictly increase, but instead
+ // only that each is unique in a single runtime-restart session.
+ final long nonce = getNonce(name);
+ if (nonce == NONCE_DISABLED) {
+ if (DEBUG) {
+ Log.d(TAG, "refusing to invalidate disabled cache: " + name);
+ }
+ return;
+ }
+
+ long newValue;
+ do {
+ newValue = NoPreloadHolder.next();
+ } while (isReservedNonce(newValue));
+ if (DEBUG) {
+ Log.d(TAG, TextUtils.formatSimple(
+ "invalidating cache [%s]: [%s] -> [%s]",
+ name, nonce, Long.toString(newValue)));
+ }
+ // There is a small race with concurrent disables here. A compare-and-exchange
+ // property operation would be required to eliminate the race condition.
+ setNonce(name, newValue);
+ long invalidateCount = sInvalidates.getOrDefault(name, (long) 0);
+ sInvalidates.put(name, ++invalidateCount);
}
/**
@@ -1158,7 +1000,43 @@ public class PropertyInvalidatedCache<Query, Result> {
* @hide
*/
public static void corkInvalidations(@NonNull String name) {
- getNonceHandler(name).cork();
+ if (!sEnabled) {
+ if (DEBUG) {
+ Log.w(TAG, TextUtils.formatSimple(
+ "cache cork %s suppressed", name));
+ }
+ return;
+ }
+
+ synchronized (sCorkLock) {
+ int numberCorks = sCorks.getOrDefault(name, 0);
+ if (DEBUG) {
+ Log.d(TAG, TextUtils.formatSimple(
+ "corking %s: numberCorks=%s", name, numberCorks));
+ }
+
+ // If we're the first ones to cork this cache, set the cache to the corked state so
+ // existing caches talk directly to their services while we've corked updates.
+ // Make sure we don't clobber a disabled cache value.
+
+ // TODO(dancol): we can skip this property write and leave the cache enabled if the
+ // caller promises not to make observable changes to the cache backing state before
+ // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair.
+ // Implement this more dangerous mode of operation if necessary.
+ if (numberCorks == 0) {
+ final long nonce = getNonce(name);
+ if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) {
+ setNonce(name, NONCE_CORKED);
+ }
+ } else {
+ final long count = sCorkedInvalidates.getOrDefault(name, (long) 0);
+ sCorkedInvalidates.put(name, count + 1);
+ }
+ sCorks.put(name, numberCorks + 1);
+ if (DEBUG) {
+ Log.d(TAG, "corked: " + name);
+ }
+ }
}
/**
@@ -1170,7 +1048,34 @@ public class PropertyInvalidatedCache<Query, Result> {
* @hide
*/
public static void uncorkInvalidations(@NonNull String name) {
- getNonceHandler(name).uncork();
+ if (!sEnabled) {
+ if (DEBUG) {
+ Log.w(TAG, TextUtils.formatSimple(
+ "cache uncork %s suppressed", name));
+ }
+ return;
+ }
+
+ synchronized (sCorkLock) {
+ int numberCorks = sCorks.getOrDefault(name, 0);
+ if (DEBUG) {
+ Log.d(TAG, TextUtils.formatSimple(
+ "uncorking %s: numberCorks=%s", name, numberCorks));
+ }
+
+ if (numberCorks < 1) {
+ throw new AssertionError("cork underflow: " + name);
+ }
+ if (numberCorks == 1) {
+ sCorks.remove(name);
+ invalidateCacheLocked(name);
+ if (DEBUG) {
+ Log.d(TAG, "uncorked: " + name);
+ }
+ } else {
+ sCorks.put(name, numberCorks - 1);
+ }
+ }
}
/**
@@ -1199,8 +1104,6 @@ public class PropertyInvalidatedCache<Query, Result> {
@GuardedBy("mLock")
private Handler mHandler;
- private NonceHandler mNonce;
-
public AutoCorker(@NonNull String propertyName) {
this(propertyName, DEFAULT_AUTO_CORK_DELAY_MS);
}
@@ -1214,35 +1117,31 @@ public class PropertyInvalidatedCache<Query, Result> {
}
public void autoCork() {
- synchronized (mLock) {
- if (mNonce == null) {
- mNonce = getNonceHandler(mPropertyName);
- }
- }
-
if (getLooper() == null) {
// We're not ready to auto-cork yet, so just invalidate the cache immediately.
if (DEBUG) {
Log.w(TAG, "invalidating instead of autocorking early in init: "
+ mPropertyName);
}
- mNonce.invalidate();
+ PropertyInvalidatedCache.invalidateCache(mPropertyName);
return;
}
synchronized (mLock) {
boolean alreadyQueued = mUncorkDeadlineMs >= 0;
if (DEBUG) {
- Log.d(TAG, formatSimple(
+ Log.w(TAG, TextUtils.formatSimple(
"autoCork %s mUncorkDeadlineMs=%s", mPropertyName,
mUncorkDeadlineMs));
}
mUncorkDeadlineMs = SystemClock.uptimeMillis() + mAutoCorkDelayMs;
if (!alreadyQueued) {
getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs);
- mNonce.cork();
+ PropertyInvalidatedCache.corkInvalidations(mPropertyName);
} else {
- // Count this as a corked invalidation.
- mNonce.invalidate();
+ synchronized (sCorkLock) {
+ final long count = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0);
+ sCorkedInvalidates.put(mPropertyName, count + 1);
+ }
}
}
}
@@ -1250,7 +1149,7 @@ public class PropertyInvalidatedCache<Query, Result> {
private void handleMessage(Message msg) {
synchronized (mLock) {
if (DEBUG) {
- Log.d(TAG, formatSimple(
+ Log.w(TAG, TextUtils.formatSimple(
"handleMsesage %s mUncorkDeadlineMs=%s",
mPropertyName, mUncorkDeadlineMs));
}
@@ -1262,7 +1161,7 @@ public class PropertyInvalidatedCache<Query, Result> {
if (mUncorkDeadlineMs > nowMs) {
mUncorkDeadlineMs = nowMs + mAutoCorkDelayMs;
if (DEBUG) {
- Log.d(TAG, formatSimple(
+ Log.w(TAG, TextUtils.formatSimple(
"scheduling uncork at %s",
mUncorkDeadlineMs));
}
@@ -1270,10 +1169,10 @@ public class PropertyInvalidatedCache<Query, Result> {
return;
}
if (DEBUG) {
- Log.d(TAG, "automatic uncorking " + mPropertyName);
+ Log.w(TAG, "automatic uncorking " + mPropertyName);
}
mUncorkDeadlineMs = -1;
- mNonce.uncork();
+ PropertyInvalidatedCache.uncorkInvalidations(mPropertyName);
}
}
@@ -1308,7 +1207,7 @@ public class PropertyInvalidatedCache<Query, Result> {
Result resultToCompare = recompute(query);
boolean nonceChanged = (getCurrentNonce() != mLastSeenNonce);
if (!nonceChanged && !resultEquals(proposedResult, resultToCompare)) {
- Log.e(TAG, formatSimple(
+ Log.e(TAG, TextUtils.formatSimple(
"cache %s inconsistent for %s is %s should be %s",
cacheName(), queryToString(query),
proposedResult, resultToCompare));
@@ -1385,9 +1284,17 @@ public class PropertyInvalidatedCache<Query, Result> {
/**
* Returns a list of caches alive at the current time.
*/
+ @GuardedBy("sGlobalLock")
private static @NonNull ArrayList<PropertyInvalidatedCache> getActiveCaches() {
- synchronized (sGlobalLock) {
- return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet());
+ return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet());
+ }
+
+ /**
+ * Returns a list of the active corks in a process.
+ */
+ private static @NonNull ArrayList<Map.Entry<String, Integer>> getActiveCorks() {
+ synchronized (sCorkLock) {
+ return new ArrayList<Map.Entry<String, Integer>>(sCorks.entrySet());
}
}
@@ -1454,27 +1361,32 @@ public class PropertyInvalidatedCache<Query, Result> {
return;
}
- NonceHandler.Stats stats = mNonce.getStats();
+ long invalidateCount;
+ long corkedInvalidates;
+ synchronized (sCorkLock) {
+ invalidateCount = sInvalidates.getOrDefault(mPropertyName, (long) 0);
+ corkedInvalidates = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0);
+ }
synchronized (mLock) {
- pw.println(formatSimple(" Cache Name: %s", cacheName()));
- pw.println(formatSimple(" Property: %s", mPropertyName));
+ pw.println(TextUtils.formatSimple(" Cache Name: %s", cacheName()));
+ pw.println(TextUtils.formatSimple(" Property: %s", mPropertyName));
final long skips = mSkips[NONCE_CORKED] + mSkips[NONCE_UNSET] + mSkips[NONCE_DISABLED]
+ mSkips[NONCE_BYPASS];
- pw.println(formatSimple(
+ pw.println(TextUtils.formatSimple(
" Hits: %d, Misses: %d, Skips: %d, Clears: %d",
mHits, mMisses, skips, mClears));
- pw.println(formatSimple(
+ pw.println(TextUtils.formatSimple(
" Skip-corked: %d, Skip-unset: %d, Skip-bypass: %d, Skip-other: %d",
mSkips[NONCE_CORKED], mSkips[NONCE_UNSET],
mSkips[NONCE_BYPASS], mSkips[NONCE_DISABLED]));
- pw.println(formatSimple(
+ pw.println(TextUtils.formatSimple(
" Nonce: 0x%016x, Invalidates: %d, CorkedInvalidates: %d",
- mLastSeenNonce, stats.invalidated, stats.corkedInvalidates));
- pw.println(formatSimple(
+ mLastSeenNonce, invalidateCount, corkedInvalidates));
+ pw.println(TextUtils.formatSimple(
" Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d",
mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow));
- pw.println(formatSimple(" Enabled: %s", mDisabled ? "false" : "true"));
+ pw.println(TextUtils.formatSimple(" Enabled: %s", mDisabled ? "false" : "true"));
pw.println("");
// No specific cache was requested. This is the default, and no details
@@ -1492,7 +1404,23 @@ public class PropertyInvalidatedCache<Query, Result> {
String key = Objects.toString(entry.getKey());
String value = Objects.toString(entry.getValue());
- pw.println(formatSimple(" Key: %s\n Value: %s\n", key, value));
+ pw.println(TextUtils.formatSimple(" Key: %s\n Value: %s\n", key, value));
+ }
+ }
+ }
+
+ /**
+ * Dump the corking status.
+ */
+ @GuardedBy("sCorkLock")
+ private static void dumpCorkInfo(PrintWriter pw) {
+ ArrayList<Map.Entry<String, Integer>> activeCorks = getActiveCorks();
+ if (activeCorks.size() > 0) {
+ pw.println(" Corking Status:");
+ for (int i = 0; i < activeCorks.size(); i++) {
+ Map.Entry<String, Integer> entry = activeCorks.get(i);
+ pw.println(TextUtils.formatSimple(" Property Name: %s Count: %d",
+ entry.getKey(), entry.getValue()));
}
}
}
@@ -1513,7 +1441,14 @@ public class PropertyInvalidatedCache<Query, Result> {
// then only that cache is reported.
boolean detail = anyDetailed(args);
- ArrayList<PropertyInvalidatedCache> activeCaches = getActiveCaches();
+ ArrayList<PropertyInvalidatedCache> activeCaches;
+ synchronized (sGlobalLock) {
+ activeCaches = getActiveCaches();
+ if (!detail) {
+ dumpCorkInfo(pw);
+ }
+ }
+
for (int i = 0; i < activeCaches.size(); i++) {
PropertyInvalidatedCache currentCache = activeCaches.get(i);
currentCache.dumpContents(pw, detail, args);
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 439d988e2588..dca433696fe7 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -110,40 +110,6 @@ public final class AppFunctionManager {
*
* @param request the request to execute the app function
* @param executor the executor to run the callback
- * @param callback the callback to receive the function execution result. if the calling app
- * does not own the app function or does not have {@code
- * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
- * android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code
- * ExecuteAppFunctionResponse.RESULT_DENIED}.
- * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor,
- * CancellationSignal, Consumer)} instead. This method will be removed once usage references
- * are updated.
- */
- @RequiresPermission(
- anyOf = {
- Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,
- Manifest.permission.EXECUTE_APP_FUNCTIONS
- },
- conditional = true)
- @UserHandleAware
- @Deprecated
- public void executeAppFunction(
- @NonNull ExecuteAppFunctionRequest request,
- @NonNull @CallbackExecutor Executor executor,
- @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
- executeAppFunction(request, executor, new CancellationSignal(), callback);
- }
-
- /**
- * Executes the app function.
- *
- * <p>Note: Applications can execute functions they define. To execute functions defined in
- * another component, apps would need to have {@code
- * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
- * android.permission.EXECUTE_APP_FUNCTIONS}.
- *
- * @param request the request to execute the app function
- * @param executor the executor to run the callback
* @param cancellationSignal the cancellation signal to cancel the execution.
* @param callback the callback to receive the function execution result. if the calling app
* does not own the app function or does not have {@code
diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
index ceca850a1037..63d187aa11ef 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -158,74 +158,6 @@ public abstract class AppFunctionService extends Service {
* thread and dispatch the result with the given callback. You should always report back the
* result using the callback, no matter if the execution was successful or not.
*
- * @param request The function execution request.
- * @param callback A callback to report back the result.
- * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal,
- * Consumer)} instead. This method will be removed once usage references are updated.
- */
- @MainThread
- @Deprecated
- public void onExecuteFunction(
- @NonNull ExecuteAppFunctionRequest request,
- @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
- Log.w(
- "AppFunctionService",
- "Calling deprecated default implementation of onExecuteFunction");
- }
-
- /**
- * Called by the system to execute a specific app function.
- *
- * <p>This method is triggered when the system requests your AppFunctionService to handle a
- * particular function you have registered and made available.
- *
- * <p>To ensure proper routing of function requests, assign a unique identifier to each
- * function. This identifier doesn't need to be globally unique, but it must be unique within
- * your app. For example, a function to order food could be identified as "orderFood". In most
- * cases this identifier should come from the ID automatically generated by the AppFunctions
- * SDK. You can determine the specific function to invoke by calling {@link
- * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
- *
- * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
- * thread and dispatch the result with the given callback. You should always report back the
- * result using the callback, no matter if the execution was successful or not.
- *
- * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel
- * the execution of function if requested by the system.
- *
- * @param request The function execution request.
- * @param cancellationSignal A signal to cancel the execution.
- * @param callback A callback to report back the result.
- * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String,
- * CancellationSignal, Consumer)} instead. This method will be removed once usage references
- * are updated.
- */
- @MainThread
- @Deprecated
- public void onExecuteFunction(
- @NonNull ExecuteAppFunctionRequest request,
- @NonNull CancellationSignal cancellationSignal,
- @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
- onExecuteFunction(request, callback);
- }
-
- /**
- * Called by the system to execute a specific app function.
- *
- * <p>This method is triggered when the system requests your AppFunctionService to handle a
- * particular function you have registered and made available.
- *
- * <p>To ensure proper routing of function requests, assign a unique identifier to each
- * function. This identifier doesn't need to be globally unique, but it must be unique within
- * your app. For example, a function to order food could be identified as "orderFood". In most
- * cases this identifier should come from the ID automatically generated by the AppFunctions
- * SDK. You can determine the specific function to invoke by calling {@link
- * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
- *
- * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
- * thread and dispatch the result with the given callback. You should always report back the
- * result using the callback, no matter if the execution was successful or not.
- *
* <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel
* the execution of function if requested by the system.
*
@@ -235,11 +167,9 @@ public abstract class AppFunctionService extends Service {
* @param callback A callback to report back the result.
*/
@MainThread
- public void onExecuteFunction(
+ public abstract void onExecuteFunction(
@NonNull ExecuteAppFunctionRequest request,
@NonNull String callingPackage,
@NonNull CancellationSignal cancellationSignal,
- @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
- onExecuteFunction(request, cancellationSignal, callback);
- }
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback);
}
diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java
index 7f30d7cccb57..124973489dd1 100644
--- a/core/java/android/companion/AssociationInfo.java
+++ b/core/java/android/companion/AssociationInfo.java
@@ -287,7 +287,7 @@ public final class AssociationInfo implements Parcelable {
/**
* Get the device icon of the associated device. The device icon represents the device type.
*
- * @return the device icon, or {@code null} if no device icon is has been set for the
+ * @return the device icon, or {@code null} if no device icon has been set for the
* associated device.
*
* @see AssociationRequest.Builder#setDeviceIcon(Icon)
diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java
index 41a6791d8a7b..f368935a74c8 100644
--- a/core/java/android/companion/AssociationRequest.java
+++ b/core/java/android/companion/AssociationRequest.java
@@ -475,8 +475,8 @@ public final class AssociationRequest implements Parcelable {
}
/**
- * Set the device icon for the self-managed device and this icon will be
- * displayed in the self-managed association dialog.
+ * Set the device icon for the self-managed device and to display the icon in the
+ * self-managed association dialog.
*
* @throws IllegalArgumentException if the icon is not exactly 24dp by 24dp
* or if it is {@link Icon#TYPE_URI} or {@link Icon#TYPE_URI_ADAPTIVE_BITMAP}.
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index dfad6de4ba16..4472c3d13d7c 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -478,6 +478,15 @@ public final class CompanionDeviceManager {
Objects.requireNonNull(callback, "Callback cannot be null");
handler = Handler.mainIfNull(handler);
+ if (Flags.associationDeviceIcon()) {
+ final Icon deviceIcon = request.getDeviceIcon();
+
+ if (deviceIcon != null && !isValidIcon(deviceIcon, mContext)) {
+ throw new IllegalArgumentException("The size of the device icon must be "
+ + "24dp x 24dp to ensure proper display");
+ }
+ }
+
try {
mService.associate(request, new AssociationRequestCallbackProxy(handler, callback),
mContext.getOpPackageName(), mContext.getUserId());
@@ -542,11 +551,13 @@ public final class CompanionDeviceManager {
Objects.requireNonNull(executor, "Executor cannot be null");
Objects.requireNonNull(callback, "Callback cannot be null");
- final Icon deviceIcon = request.getDeviceIcon();
+ if (Flags.associationDeviceIcon()) {
+ final Icon deviceIcon = request.getDeviceIcon();
- if (deviceIcon != null && !isValidIcon(deviceIcon, mContext)) {
- throw new IllegalArgumentException("The size of the device icon must be 24dp x 24dp to"
- + "ensure proper display");
+ if (deviceIcon != null && !isValidIcon(deviceIcon, mContext)) {
+ throw new IllegalArgumentException("The size of the device icon must be "
+ + "24dp x 24dp to ensure proper display");
+ }
}
try {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 91f7a8bae163..628435da3a37 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5658,6 +5658,14 @@ public abstract class Context {
public static final String BINARY_TRANSPARENCY_SERVICE = "transparency";
/**
+ * System service name for ForensicService.
+ * The service manages the forensic info on device.
+ * @hide
+ */
+ @FlaggedApi(android.security.Flags.FLAG_AFL_API)
+ public static final String FORENSIC_SERVICE = "forensic";
+
+ /**
* System service name for the DeviceIdleManager.
* @see #getSystemService(String)
* @hide
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 0bb0027fb0c3..f71952849872 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -888,6 +888,22 @@ public class Intent implements Parcelable, Cloneable {
public static final String ACTION_ACTIVITY_RECOGNIZER =
"android.intent.action.ACTIVITY_RECOGNIZER";
+ /** @hide */
+ public static void maybeMarkAsMissingCreatorToken(Object object) {
+ if (object instanceof Intent intent) {
+ maybeMarkAsMissingCreatorTokenInternal(intent);
+ }
+ }
+
+ private static void maybeMarkAsMissingCreatorTokenInternal(Intent intent) {
+ boolean isForeign = (intent.mLocalFlags & LOCAL_FLAG_FROM_PARCEL) != 0;
+ boolean isWithoutTrustedCreatorToken =
+ (intent.mLocalFlags & Intent.LOCAL_FLAG_TRUSTED_CREATOR_TOKEN_PRESENT) == 0;
+ if (isForeign && isWithoutTrustedCreatorToken) {
+ intent.addExtendedFlags(EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN);
+ }
+ }
+
/**
* Represents a shortcut/live folder icon resource.
*
@@ -7684,10 +7700,8 @@ public class Intent implements Parcelable, Cloneable {
/**
* This flag indicates the creator token of this intent has been verified.
- *
- * @hide
*/
- public static final int LOCAL_FLAG_CREATOR_TOKEN_VERIFIED = 1 << 6;
+ private static final int LOCAL_FLAG_TRUSTED_CREATOR_TOKEN_PRESENT = 1 << 6;
/** @hide */
@IntDef(flag = true, prefix = { "EXTENDED_FLAG_" }, value = {
@@ -12243,6 +12257,30 @@ public class Intent implements Parcelable, Cloneable {
}
}
+ /** @hide */
+ public void checkCreatorToken() {
+ if (mExtras == null) return;
+ if (mCreatorTokenInfo != null && mCreatorTokenInfo.mExtraIntentKeys != null) {
+ for (String key : mCreatorTokenInfo.mExtraIntentKeys) {
+ try {
+ Intent extraIntent = mExtras.getParcelable(key, Intent.class);
+ if (extraIntent == null) {
+ Log.w(TAG, "The key {" + key
+ + "} does not correspond to an intent in the bundle.");
+ continue;
+ }
+ extraIntent.mLocalFlags |= LOCAL_FLAG_TRUSTED_CREATOR_TOKEN_PRESENT;
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to validate creator token. key: " + key + ".", e);
+ }
+ }
+ }
+ // mark the bundle as intent extras after calls to getParcelable.
+ // otherwise, the logic to mark missing token would run before
+ // mark trusted creator token present.
+ mExtras.setIsIntentExtra();
+ }
+
public void writeToParcel(Parcel out, int flags) {
out.writeString8(mAction);
Uri.writeToParcel(out, mData);
@@ -12730,6 +12768,7 @@ public class Intent implements Parcelable, Cloneable {
}
mLocalFlags |= localFlags;
+ checkCreatorToken();
// Special attribution fix-up logic for any BluetoothDevice extras
// passed via Bluetooth intents
diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl
index 451c0e5e079a..c911326ccffd 100644
--- a/core/java/android/content/pm/IPackageInstaller.aidl
+++ b/core/java/android/content/pm/IPackageInstaller.aidl
@@ -93,4 +93,10 @@ interface IPackageInstaller {
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES,android.Manifest.permission.REQUEST_INSTALL_PACKAGES})")
void reportUnarchivalStatus(int unarchiveId, int status, long requiredStorageBytes, in PendingIntent userActionIntent, in UserHandle userHandle);
+
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ int getVerificationPolicy();
+
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ boolean setVerificationPolicy(int policy);
}
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index c673d5846d5d..cba7bc912666 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -62,6 +62,8 @@ import android.content.pm.parsing.PackageLite;
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
import android.content.pm.verify.domain.DomainSet;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerificationStatus;
import android.graphics.Bitmap;
import android.icu.util.ULocale;
import android.net.Uri;
@@ -418,6 +420,21 @@ public class PackageInstaller {
public static final String EXTRA_WARNINGS = "android.content.pm.extra.WARNINGS";
/**
+ * When verification is blocked as part of the installation, additional reason for the block
+ * will be provided to the installer with a {@link VerificationFailedReason} as part of the
+ * installation result returned via the {@link IntentSender} in
+ * {@link Session#commit(IntentSender)}. This extra is provided only when the installation has
+ * failed. Installers can use this extra to check if the installation failure was caused by a
+ * verification failure.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+ @SystemApi
+ public static final String EXTRA_VERIFICATION_FAILURE_REASON =
+ "android.content.pm.extra.VERIFICATION_FAILURE_REASON";
+
+ /**
* Streaming installation pending.
* Caller should make sure DataLoader is able to prepare image and reinitiate the operation.
*
@@ -760,6 +777,90 @@ public class PackageInstaller {
@Retention(RetentionPolicy.SOURCE)
public @interface UnarchivalStatus {}
+ /**
+ * Verification failed because of unknown reasons, such as when the verifier times out or cannot
+ * be connected. It can also corresponds to the status of
+ * {@link VerificationSession#VERIFICATION_INCOMPLETE_UNKNOWN} reported by the verifier via
+ * {@link VerificationSession#reportVerificationIncomplete(int)}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+ @SystemApi
+ public static final int VERIFICATION_FAILED_REASON_UNKNOWN = 0;
+
+ /**
+ * Verification failed because the network is unavailable. This corresponds to the status of
+ * {@link VerificationSession#VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE} reported by the
+ * verifier via {@link VerificationSession#reportVerificationIncomplete(int)}.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+ @SystemApi
+ public static final int VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE = 1;
+
+ /**
+ * Verification failed because the package is blocked, as reported by the verifier via
+ * {@link VerificationSession#reportVerificationComplete(VerificationStatus)} or
+ * {@link VerificationSession#reportVerificationComplete(VerificationStatus, PersistableBundle)}
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+ @SystemApi
+ public static final int VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED = 2;
+
+ /**
+ * @hide
+ */
+ @IntDef(value = {
+ VERIFICATION_FAILED_REASON_UNKNOWN,
+ VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE,
+ VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED,
+ })
+ public @interface VerificationFailedReason {
+ }
+
+ /**
+ * Do not block installs, regardless of verification status.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+ @SystemApi
+ public static final int VERIFICATION_POLICY_NONE = 0; // platform default
+ /**
+ * Only block installations on {@link #VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED}.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+ @SystemApi
+ public static final int VERIFICATION_POLICY_BLOCK_FAIL_OPEN = 1;
+ /**
+ * Only block installations on {@link #VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED} and ask the
+ * user if they'd like to install anyway when the verification is blocked for other reason.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+ @SystemApi
+ public static final int VERIFICATION_POLICY_BLOCK_FAIL_WARN = 2;
+ /**
+ * Block installations whose verification status is blocked for any reason.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+ @SystemApi
+ public static final int VERIFICATION_POLICY_BLOCK_FAIL_CLOSED = 3;
+ /**
+ * @hide
+ */
+ @IntDef(value = {
+ VERIFICATION_POLICY_NONE,
+ VERIFICATION_POLICY_BLOCK_FAIL_OPEN,
+ VERIFICATION_POLICY_BLOCK_FAIL_WARN,
+ VERIFICATION_POLICY_BLOCK_FAIL_CLOSED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface VerificationPolicy {
+ }
/** Default set of checksums - includes all available checksums.
* @see Session#requestChecksums */
@@ -1503,6 +1604,40 @@ public class PackageInstaller {
}
/**
+ * Return the current verification enforcement policy. This may only be called by the
+ * package currently set by the system as the verifier agent.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public final @VerificationPolicy int getVerificationPolicy() {
+ try {
+ return mInstaller.getVerificationPolicy();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Set the current verification enforcement policy which will be applied to all the future
+ * installation sessions. This may only be called by the package currently set by the system as
+ * the verifier agent.
+ * @hide
+ * @return whether the new policy was successfully set.
+ */
+ @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public final boolean setVerificationPolicy(@VerificationPolicy int policy) {
+ try {
+ return mInstaller.setVerificationPolicy(policy);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* An installation that is being actively staged. For an install to succeed,
* all existing and new packages must have identical package names, version
* codes, and signing certificates.
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 52d733314eb6..5b38942d468d 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -334,3 +334,11 @@ flag {
bug: "364771256"
is_fixed_read_only: true
}
+
+flag {
+ name: "reduce_broadcasts_for_component_state_changes"
+ namespace: "package_manager_service"
+ description: "Feature flag to limit sending of the PACKAGE_CHANGED broadcast to only the system and the application itself during component state changes."
+ bug: "292261144"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl
index 7a9484abd1b1..036c1e69cb0d 100644
--- a/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl
+++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl
@@ -25,4 +25,6 @@ interface IVerificationSessionInterface {
long getTimeoutTime(int verificationId);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
long extendTimeRemaining(int verificationId, long additionalMs);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ boolean setVerificationPolicy(int verificationId, int policy);
} \ No newline at end of file
diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.java b/core/java/android/content/pm/verify/pkg/VerificationSession.java
index 70b4a022f521..f393be829aed 100644
--- a/core/java/android/content/pm/verify/pkg/VerificationSession.java
+++ b/core/java/android/content/pm/verify/pkg/VerificationSession.java
@@ -22,6 +22,7 @@ import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.content.pm.Flags;
+import android.content.pm.PackageInstaller;
import android.content.pm.SharedLibraryInfo;
import android.content.pm.SigningInfo;
import android.net.Uri;
@@ -52,17 +53,12 @@ public final class VerificationSession implements Parcelable {
* The verification cannot be completed because the network is unavailable.
*/
public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1;
- /**
- * The verification cannot be completed because the network is limited.
- */
- public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2;
/**
* @hide
*/
@IntDef(prefix = {"VERIFICATION_INCOMPLETE_"}, value = {
VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE,
- VERIFICATION_INCOMPLETE_NETWORK_LIMITED,
VERIFICATION_INCOMPLETE_UNKNOWN,
})
@Retention(RetentionPolicy.SOURCE)
@@ -85,6 +81,15 @@ public final class VerificationSession implements Parcelable {
private final IVerificationSessionInterface mSession;
@NonNull
private final IVerificationSessionCallback mCallback;
+ /**
+ * The current policy that is active for the session. It might not be
+ * the same as the original policy that was initially assigned for this verification session,
+ * because the active policy can be overridden by {@link #setVerificationPolicy(int)}.
+ * <p>To improve the latency, store the original policy value and any changes made to it,
+ * so that {@link #getVerificationPolicy()} does not need to make a binder call to retrieve the
+ * currently active policy.</p>
+ */
+ private volatile @PackageInstaller.VerificationPolicy int mVerificationPolicy;
/**
* Constructor used by the system to describe the details of a verification session.
@@ -94,6 +99,7 @@ public final class VerificationSession implements Parcelable {
@NonNull Uri stagedPackageUri, @NonNull SigningInfo signingInfo,
@NonNull List<SharedLibraryInfo> declaredLibraries,
@NonNull PersistableBundle extensionParams,
+ @PackageInstaller.VerificationPolicy int defaultPolicy,
@NonNull IVerificationSessionInterface session,
@NonNull IVerificationSessionCallback callback) {
mId = id;
@@ -103,6 +109,7 @@ public final class VerificationSession implements Parcelable {
mSigningInfo = signingInfo;
mDeclaredLibraries = declaredLibraries;
mExtensionParams = extensionParams;
+ mVerificationPolicy = defaultPolicy;
mSession = session;
mCallback = callback;
}
@@ -144,7 +151,7 @@ public final class VerificationSession implements Parcelable {
/**
* Returns a mapping of any shared libraries declared in the manifest
- * to the {@link SharedLibraryInfo#Type} that is declared. This will be an empty
+ * to the {@link SharedLibraryInfo.Type} that is declared. This will be an empty
* map if no shared libraries are declared by the package.
*/
@NonNull
@@ -174,6 +181,39 @@ public final class VerificationSession implements Parcelable {
}
/**
+ * Return the current policy that is active for this session.
+ * <p>If the policy for this session has been changed by {@link #setVerificationPolicy},
+ * the return value of this method is the current policy that is active for this session.
+ * Otherwise, the return value is the same as the initial policy that was assigned to the
+ * session when it was first created.</p>
+ */
+ public @PackageInstaller.VerificationPolicy int getVerificationPolicy() {
+ return mVerificationPolicy;
+ }
+
+ /**
+ * Override the verification policy for this session.
+ * @return True if the override was successful, False otherwise.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public boolean setVerificationPolicy(@PackageInstaller.VerificationPolicy int policy) {
+ if (mVerificationPolicy == policy) {
+ // No effective policy change
+ return true;
+ }
+ try {
+ if (mSession.setVerificationPolicy(mId, policy)) {
+ mVerificationPolicy = policy;
+ return true;
+ } else {
+ return false;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Extend the timeout for this session by the provided additionalMs to
* fetch relevant information over the network or wait for the network.
* This may be called multiple times. If the request would bypass any max
@@ -239,6 +279,7 @@ public final class VerificationSession implements Parcelable {
mSigningInfo = SigningInfo.CREATOR.createFromParcel(in);
mDeclaredLibraries = in.createTypedArrayList(SharedLibraryInfo.CREATOR);
mExtensionParams = in.readPersistableBundle(getClass().getClassLoader());
+ mVerificationPolicy = in.readInt();
mSession = IVerificationSessionInterface.Stub.asInterface(in.readStrongBinder());
mCallback = IVerificationSessionCallback.Stub.asInterface(in.readStrongBinder());
}
@@ -257,6 +298,7 @@ public final class VerificationSession implements Parcelable {
mSigningInfo.writeToParcel(dest, flags);
dest.writeTypedList(mDeclaredLibraries);
dest.writePersistableBundle(mExtensionParams);
+ dest.writeInt(mVerificationPolicy);
dest.writeStrongBinder(mSession.asBinder());
dest.writeStrongBinder(mCallback.asBinder());
}
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index 49ab15a40a8e..50121278f0e6 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -21,6 +21,7 @@ import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Intent;
import android.util.ArrayMap;
import android.util.Log;
import android.util.MathUtils;
@@ -401,6 +402,9 @@ public class BaseBundle {
synchronized (this) {
object = unwrapLazyValueFromMapLocked(i, clazz, itemTypes);
}
+ if ((mFlags & Bundle.FLAG_VERIFY_TOKENS_PRESENT) != 0) {
+ Intent.maybeMarkAsMissingCreatorToken(object);
+ }
}
return (clazz != null) ? clazz.cast(object) : (T) object;
}
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index ed4037c7d246..c18fb0c38b82 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -62,6 +62,12 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
@VisibleForTesting
static final int FLAG_HAS_BINDERS = 1 << 12;
+ /**
+ * Indicates there may be intents with creator tokens contained in this bundle. When unparceled,
+ * they should be verified if tokens are missing or invalid.
+ */
+ static final int FLAG_VERIFY_TOKENS_PRESENT = 1 << 13;
+
/**
* Status when the Bundle can <b>assert</b> that the underlying Parcel DOES NOT contain
@@ -274,6 +280,11 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
return orig;
}
+ /** {@hide} */
+ public void setIsIntentExtra() {
+ mFlags |= FLAG_VERIFY_TOKENS_PRESENT;
+ }
+
/**
* Mark if this Bundle is okay to "defuse." That is, it's okay for system
* processes to ignore any {@link BadParcelableException} encountered when
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
index a55398ac9752..ef1e6c9405f3 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -114,6 +114,7 @@ public final class Debug
"opengl-tracing",
"view-hierarchy",
"support_boot_stages",
+ "app_info",
};
/**
@@ -1016,14 +1017,14 @@ public final class Debug
// send VM_START.
System.out.println("Waiting for debugger first packet");
- mWaiting = true;
+ setWaitingForDebugger(true);
while (!isDebuggerConnected()) {
try {
Thread.sleep(100);
} catch (InterruptedException ie) {
}
}
- mWaiting = false;
+ setWaitingForDebugger(false);
System.out.println("Debug.suspendAllAndSentVmStart");
VMDebug.suspendAllAndSendVmStart();
@@ -1049,12 +1050,12 @@ public final class Debug
Chunk waitChunk = new Chunk(ChunkHandler.type("WAIT"), data, 0, 1);
DdmServer.sendChunk(waitChunk);
- mWaiting = true;
+ setWaitingForDebugger(true);
while (!isDebuggerConnected()) {
try { Thread.sleep(SPIN_DELAY); }
catch (InterruptedException ie) {}
}
- mWaiting = false;
+ setWaitingForDebugger(false);
System.out.println("Debugger has connected");
@@ -1112,6 +1113,16 @@ public final class Debug
}
/**
+ * Set whether the app is waiting for a debugger to connect
+ *
+ * @hide
+ */
+ private static void setWaitingForDebugger(boolean waiting) {
+ mWaiting = waiting;
+ VMDebug.setWaitingForDebugger(waiting);
+ }
+
+ /**
* Returns an array of strings that identify Framework features. This is
* used by DDMS to determine what sorts of operations the Framework can
* perform.
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index 346ee7ca4f87..71d29af6cf02 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -41,6 +41,7 @@ import com.android.internal.os.SomeArgs;
import com.android.internal.util.Preconditions;
import com.android.sdksandbox.flags.Flags;
+import dalvik.system.VMDebug;
import dalvik.system.VMRuntime;
import libcore.io.IoUtils;
@@ -1410,6 +1411,7 @@ public class Process {
public static void setArgV0(@NonNull String text) {
sArgV0 = text;
setArgV0Native(text);
+ VMDebug.setCurrentProcessName(text);
}
private static native void setArgV0Native(String text);
diff --git a/core/java/android/security/forensic/IForensicService.aidl b/core/java/android/security/forensic/IForensicService.aidl
new file mode 100644
index 000000000000..a944b18cb26d
--- /dev/null
+++ b/core/java/android/security/forensic/IForensicService.aidl
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.forensic;
+
+import android.security.forensic.IForensicServiceCommandCallback;
+import android.security.forensic.IForensicServiceStateCallback;
+
+/**
+ * Binder interface to communicate with ForensicService.
+ * @hide
+ */
+interface IForensicService {
+ void monitorState(IForensicServiceStateCallback callback);
+ void makeVisible(IForensicServiceCommandCallback callback);
+ void makeInvisible(IForensicServiceCommandCallback callback);
+ void enable(IForensicServiceCommandCallback callback);
+ void disable(IForensicServiceCommandCallback callback);
+}
diff --git a/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl b/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl
new file mode 100644
index 000000000000..7fa0c7f72690
--- /dev/null
+++ b/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.forensic;
+
+/**
+ * @hide
+ */
+ oneway interface IForensicServiceCommandCallback {
+ @Backing(type="int")
+ enum ErrorCode{
+ UNKNOWN = 0,
+ PERMISSION_DENIED = 1,
+ INVALID_STATE_TRANSITION = 2,
+ BACKUP_TRANSPORT_UNAVAILABLE = 3,
+ DATA_SOURCE_UNAVAILABLE = 3,
+ }
+ void onSuccess();
+ void onFailure(ErrorCode error);
+ }
diff --git a/core/java/android/security/forensic/IForensicServiceStateCallback.aidl b/core/java/android/security/forensic/IForensicServiceStateCallback.aidl
new file mode 100644
index 000000000000..0cda35083ffd
--- /dev/null
+++ b/core/java/android/security/forensic/IForensicServiceStateCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.forensic;
+
+/**
+ * @hide
+ */
+ oneway interface IForensicServiceStateCallback {
+ @Backing(type="int")
+ enum State{
+ UNKNOWN = 0,
+ INVISIBLE = 1,
+ VISIBLE = 2,
+ ENABLED = 3,
+ }
+ void onStateChange(State state);
+ }
diff --git a/core/java/android/util/NtpTrustedTime.java b/core/java/android/util/NtpTrustedTime.java
index 9f54d9fca24b..3adbd686cd2c 100644
--- a/core/java/android/util/NtpTrustedTime.java
+++ b/core/java/android/util/NtpTrustedTime.java
@@ -24,7 +24,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.Network;
-import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
import android.net.SntpClient;
import android.os.Build;
import android.os.SystemClock;
@@ -687,16 +687,8 @@ public abstract class NtpTrustedTime implements TrustedTime {
if (connectivityManager == null) {
return false;
}
- final NetworkCapabilities networkCapabilities =
- connectivityManager.getNetworkCapabilities(network);
- if (networkCapabilities == null) {
- if (LOGD) Log.d(TAG, "getNetwork: failed to get network capabilities");
- return false;
- }
- final boolean isConnectedToInternet = networkCapabilities.hasCapability(
- NetworkCapabilities.NET_CAPABILITY_INTERNET)
- && networkCapabilities.hasCapability(
- NetworkCapabilities.NET_CAPABILITY_VALIDATED);
+ final NetworkInfo ni = connectivityManager.getNetworkInfo(network);
+
// This connectivity check is to avoid performing a DNS lookup for the time server on a
// unconnected network. There are races to obtain time in Android when connectivity
// changes, which means that forceRefresh() can be called by various components before
@@ -706,8 +698,8 @@ public abstract class NtpTrustedTime implements TrustedTime {
// A side effect of check is that tests that run a fake NTP server on the device itself
// will only be able to use it if the active network is connected, even though loopback
// addresses are actually reachable.
- if (!isConnectedToInternet) {
- if (LOGD) Log.d(TAG, "getNetwork: no internet connectivity");
+ if (ni == null || !ni.isConnected()) {
+ if (LOGD) Log.d(TAG, "getNetwork: no connectivity");
return false;
}
return true;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 182ed1ebad59..1f17e8ec7b85 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -5952,7 +5952,34 @@ public final class ViewRootImpl implements ViewParent,
// If no intersection, set bounds to empty.
bounds.setEmpty();
}
- return !bounds.isEmpty();
+
+ if (bounds.isEmpty()) {
+ return false;
+ }
+
+ if (android.view.accessibility.Flags.focusRectMinSize()) {
+ adjustAccessibilityFocusedRectBoundsIfNeeded(bounds);
+ }
+
+ return true;
+ }
+
+ /**
+ * Adjusts accessibility focused rect bounds so that they are not invisible.
+ *
+ * <p>Focus bounds smaller than double the stroke width are very hard to see (or invisible).
+ * Expand the focus bounds if necessary to at least double the stroke width.
+ * @param bounds The bounds to adjust
+ */
+ @VisibleForTesting
+ public void adjustAccessibilityFocusedRectBoundsIfNeeded(Rect bounds) {
+ final int minRectLength = mAccessibilityManager.getAccessibilityFocusStrokeWidth() * 2;
+ if (bounds.width() < minRectLength || bounds.height() < minRectLength) {
+ final float missingWidth = Math.max(0, minRectLength - bounds.width());
+ final float missingHeight = Math.max(0, minRectLength - bounds.height());
+ bounds.inset(-1 * (int) Math.ceil(missingWidth / 2),
+ -1 * (int) Math.ceil(missingHeight / 2));
+ }
}
private Drawable getAccessibilityFocusedDrawable() {
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 8ffae845de1f..820a1fba11ad 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -96,6 +96,16 @@ flag {
flag {
namespace: "accessibility"
+ name: "focus_rect_min_size"
+ description: "Ensures the a11y focus rect is big enough to be drawn as visible"
+ bug: "368667566"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ namespace: "accessibility"
name: "force_invert_color"
description: "Enable force force-dark for smart inversion and dark theme everywhere"
bug: "282821643"
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 18129530978f..45f6480b0b7f 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -308,6 +308,13 @@ flag {
}
flag {
+ name: "enable_restore_to_previous_size_from_desktop_immersive"
+ namespace: "lse_desktop_experience"
+ description: "Restores the window bounds to their previous size when exiting desktop immersive"
+ bug: "372318163"
+}
+
+flag {
name: "enable_display_focus_in_shell_transitions"
namespace: "lse_desktop_experience"
description: "Creates a shell transition when display focus switches."
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 5693d666adc2..5522aa09a32e 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8479,6 +8479,18 @@
android:protectionLevel="internal"
android:featureFlag="android.content.pm.verification_service" />
+ <!--
+ This permission allows the system to receive PACKAGE_CHANGED broadcasts when the component
+ state of a non-exported component has been changed.
+ <p>Not for use by third-party applications. </p>
+ <p>Protection level: internal
+ @hide
+ -->
+ <permission
+ android:name="android.permission.RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED"
+ android:protectionLevel="internal"
+ android:featureFlag="android.content.pm.reduce_broadcasts_for_component_state_changes"/>
+
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index 228647ae9094..b5ee1302fc1d 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -19,7 +19,6 @@ package android.app;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
-import static org.junit.Assert.fail;
import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
@@ -27,7 +26,6 @@ import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import org.junit.After;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -92,10 +90,11 @@ public class PropertyInvalidatedCacheTests {
}
}
- // Ensure all test nonces are cleared after the test ends.
+ // Clear the test mode after every test, in case this process is used for other
+ // tests. This also resets the test property map.
@After
public void tearDown() throws Exception {
- PropertyInvalidatedCache.resetAfterTest();
+ PropertyInvalidatedCache.setTestMode(false);
}
// This test is disabled pending an sepolicy change that allows any app to set the
@@ -112,6 +111,9 @@ public class PropertyInvalidatedCacheTests {
new PropertyInvalidatedCache<>(4, MODULE, API, "cache1",
new ServerQuery(tester));
+ PropertyInvalidatedCache.setTestMode(true);
+ testCache.testPropertyName();
+
tester.verify(0);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(1);
@@ -221,16 +223,22 @@ public class PropertyInvalidatedCacheTests {
TestCache(String module, String api) {
this(module, api, new TestQuery());
+ setTestMode(true);
+ testPropertyName();
}
TestCache(String module, String api, TestQuery query) {
super(4, module, api, api, query);
mQuery = query;
+ setTestMode(true);
+ testPropertyName();
}
public int getRecomputeCount() {
return mQuery.getRecomputeCount();
}
+
+
}
@Test
@@ -367,18 +375,4 @@ public class PropertyInvalidatedCacheTests {
PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState");
assertEquals(n1, "cache_key.bluetooth.get_state");
}
-
- // It is illegal to continue to use a cache with a test key after calling setTestMode(false).
- // This test verifies the code detects errors in calling setTestMode().
- @Test
- public void testTestMode() {
- TestCache cache = new TestCache();
- cache.invalidateCache();
- PropertyInvalidatedCache.resetAfterTest();
- try {
- cache.invalidateCache();
- fail("expected an IllegalStateException");
- } catch (IllegalStateException expected) {
- }
- }
}
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java
index 987f68d4f9e1..80255c5f6600 100644
--- a/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java
+++ b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java
@@ -16,6 +16,9 @@
package android.content.pm.verify;
+import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED;
+import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_WARN;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -23,6 +26,8 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.content.pm.SharedLibraryInfo;
@@ -73,6 +78,7 @@ public class VerificationSessionTest {
private static final long TEST_EXTEND_TIME = 2000L;
private static final String TEST_KEY = "test key";
private static final String TEST_VALUE = "test value";
+ private static final int TEST_POLICY = VERIFICATION_POLICY_BLOCK_FAIL_CLOSED;
private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>();
private final PersistableBundle mTestExtensionParams = new PersistableBundle();
@@ -90,7 +96,7 @@ public class VerificationSessionTest {
mTestExtensionParams.putString(TEST_KEY, TEST_VALUE);
mTestSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID,
TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO, mTestDeclaredLibraries,
- mTestExtensionParams, mTestSessionInterface, mTestCallback);
+ mTestExtensionParams, TEST_POLICY, mTestSessionInterface, mTestCallback);
}
@Test
@@ -118,6 +124,7 @@ public class VerificationSessionTest {
// structure is different, but all the key/value pairs should be preserved as before.
assertThat(sessionFromParcel.getExtensionParams().getString(TEST_KEY))
.isEqualTo(mTestExtensionParams.getString(TEST_KEY));
+ assertThat(sessionFromParcel.getVerificationPolicy()).isEqualTo(TEST_POLICY);
}
@Test
@@ -152,4 +159,42 @@ public class VerificationSessionTest {
verify(mTestCallback, times(1)).reportVerificationIncomplete(
eq(TEST_ID), eq(reason));
}
+
+ @Test
+ public void testPolicyNoOverride() {
+ assertThat(mTestSession.getVerificationPolicy()).isEqualTo(TEST_POLICY);
+ // This "set" is a no-op
+ assertThat(mTestSession.setVerificationPolicy(TEST_POLICY)).isTrue();
+ assertThat(mTestSession.getVerificationPolicy()).isEqualTo(TEST_POLICY);
+ verifyZeroInteractions(mTestSessionInterface);
+ }
+
+ @Test
+ public void testPolicyOverrideFail() throws Exception {
+ final int newPolicy = VERIFICATION_POLICY_BLOCK_FAIL_WARN;
+ when(mTestSessionInterface.setVerificationPolicy(anyInt(), anyInt())).thenReturn(false);
+ assertThat(mTestSession.setVerificationPolicy(newPolicy)).isFalse();
+ verify(mTestSessionInterface, times(1))
+ .setVerificationPolicy(eq(TEST_ID), eq(newPolicy));
+ // Next "get" should not trigger binder call because the previous "set" has failed
+ assertThat(mTestSession.getVerificationPolicy()).isEqualTo(TEST_POLICY);
+ verifyNoMoreInteractions(mTestSessionInterface);
+ }
+
+ @Test
+ public void testPolicyOverrideSuccess() throws Exception {
+ final int newPolicy = VERIFICATION_POLICY_BLOCK_FAIL_WARN;
+ when(mTestSessionInterface.setVerificationPolicy(anyInt(), anyInt())).thenReturn(true);
+ assertThat(mTestSession.setVerificationPolicy(newPolicy)).isTrue();
+ verify(mTestSessionInterface, times(1))
+ .setVerificationPolicy(eq(TEST_ID), eq(newPolicy));
+ assertThat(mTestSession.getVerificationPolicy()).isEqualTo(newPolicy);
+ assertThat(mTestSession.getVerificationPolicy()).isEqualTo(newPolicy);
+
+ // Setting back to the original policy should still trigger binder calls
+ assertThat(mTestSession.setVerificationPolicy(TEST_POLICY)).isTrue();
+ verify(mTestSessionInterface, times(1))
+ .setVerificationPolicy(eq(TEST_ID), eq(TEST_POLICY));
+ assertThat(mTestSession.getVerificationPolicy()).isEqualTo(TEST_POLICY);
+ }
}
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java
index 7f73a1eb4b48..7807c8a94530 100644
--- a/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java
+++ b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java
@@ -16,6 +16,8 @@
package android.content.pm.verify;
+import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;
@@ -52,6 +54,7 @@ public class VerifierServiceTest {
private static final String TEST_PACKAGE_NAME = "com.foo";
private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test");
private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo();
+ private static final int TEST_POLICY = VERIFICATION_POLICY_BLOCK_FAIL_CLOSED;
private VerifierService mService;
private VerificationSession mSession;
@@ -60,8 +63,7 @@ public class VerifierServiceTest {
mService = Mockito.mock(VerifierService.class, Answers.CALLS_REAL_METHODS);
mSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID,
TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO,
- new ArrayList<>(),
- new PersistableBundle(), null, null);
+ new ArrayList<>(), new PersistableBundle(), TEST_POLICY, null, null);
}
@Test
diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
index 5852bee53778..64f77b309829 100644
--- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java
+++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
@@ -17,7 +17,6 @@
package android.os;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
import android.multiuser.Flags;
import android.platform.test.annotations.IgnoreUnderRavenwood;
@@ -27,7 +26,6 @@ import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import org.junit.After;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -94,17 +92,17 @@ public class IpcDataCacheTest {
public Boolean apply(Integer x) {
return mServer.query(x);
}
-
@Override
public boolean shouldBypassCache(Integer x) {
return x % 13 == 0;
}
}
- // Ensure all test nonces are cleared after the test ends.
+ // Clear the test mode after every test, in case this process is used for other
+ // tests. This also resets the test property map.
@After
public void tearDown() throws Exception {
- IpcDataCache.resetAfterTest();
+ IpcDataCache.setTestMode(false);
}
// This test is disabled pending an sepolicy change that allows any app to set the
@@ -121,6 +119,9 @@ public class IpcDataCacheTest {
new IpcDataCache<>(4, MODULE, API, "testCache1",
new ServerQuery(tester));
+ IpcDataCache.setTestMode(true);
+ testCache.testPropertyName();
+
tester.verify(0);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(1);
@@ -164,6 +165,9 @@ public class IpcDataCacheTest {
IpcDataCache<Integer, Boolean> testCache =
new IpcDataCache<>(config, (x) -> tester.query(x, x % 10 == 9));
+ IpcDataCache.setTestMode(true);
+ testCache.testPropertyName();
+
tester.verify(0);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(1);
@@ -201,6 +205,9 @@ public class IpcDataCacheTest {
IpcDataCache<Integer, Boolean> testCache =
new IpcDataCache<>(config, (x) -> tester.query(x), (x) -> x % 9 == 0);
+ IpcDataCache.setTestMode(true);
+ testCache.testPropertyName();
+
tester.verify(0);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(1);
@@ -306,6 +313,8 @@ public class IpcDataCacheTest {
TestCache(String module, String api, TestQuery query) {
super(4, module, api, "testCache7", query);
mQuery = query;
+ setTestMode(true);
+ testPropertyName();
}
TestCache(IpcDataCache.Config c) {
@@ -315,6 +324,8 @@ public class IpcDataCacheTest {
TestCache(IpcDataCache.Config c, TestQuery query) {
super(c, query);
mQuery = query;
+ setTestMode(true);
+ testPropertyName();
}
int getRecomputeCount() {
@@ -445,18 +456,4 @@ public class IpcDataCacheTest {
TestCache ec = new TestCache(e);
assertEquals(ec.isDisabled(), true);
}
-
- // It is illegal to continue to use a cache with a test key after calling setTestMode(false).
- // This test verifies the code detects errors in calling setTestMode().
- @Test
- public void testTestMode() {
- TestCache cache = new TestCache();
- cache.invalidateCache();
- IpcDataCache.resetAfterTest();
- try {
- cache.invalidateCache();
- fail("expected an IllegalStateException");
- } catch (IllegalStateException expected) {
- }
- }
}
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index 632721126714..ed9fc1c9e547 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -71,6 +71,7 @@ import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Binder;
import android.os.SystemProperties;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
@@ -82,6 +83,7 @@ import android.util.DisplayMetrics;
import android.util.Log;
import android.view.WindowInsets.Side;
import android.view.WindowInsets.Type;
+import android.view.accessibility.AccessibilityManager;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -1628,6 +1630,42 @@ public class ViewRootImplTest {
});
}
+ @Test
+ @EnableFlags(android.view.accessibility.Flags.FLAG_FOCUS_RECT_MIN_SIZE)
+ public void testAdjustAccessibilityFocusedBounds_largeEnoughBoundsAreUnchanged() {
+ final int strokeWidth = sContext.getSystemService(AccessibilityManager.class)
+ .getAccessibilityFocusStrokeWidth();
+ final int left, top, width, height;
+ left = top = 100;
+ width = height = strokeWidth * 2;
+ final Rect bounds = new Rect(left, top, left + width, top + height);
+ final Rect originalBounds = new Rect(bounds);
+
+ mViewRootImpl.adjustAccessibilityFocusedRectBoundsIfNeeded(bounds);
+
+ assertThat(bounds).isEqualTo(originalBounds);
+ }
+
+ @Test
+ @EnableFlags(android.view.accessibility.Flags.FLAG_FOCUS_RECT_MIN_SIZE)
+ public void testAdjustAccessibilityFocusedBounds_smallBoundsAreExpanded() {
+ final int strokeWidth = sContext.getSystemService(AccessibilityManager.class)
+ .getAccessibilityFocusStrokeWidth();
+ final int left, top, width, height;
+ left = top = 100;
+ width = height = strokeWidth;
+ final Rect bounds = new Rect(left, top, left + width, top + height);
+ final Rect originalBounds = new Rect(bounds);
+
+ mViewRootImpl.adjustAccessibilityFocusedRectBoundsIfNeeded(bounds);
+
+ // Bounds should be centered on the same point, but expanded to at least strokeWidth * 2
+ assertThat(bounds.centerX()).isEqualTo(originalBounds.centerX());
+ assertThat(bounds.centerY()).isEqualTo(originalBounds.centerY());
+ assertThat(bounds.width()).isAtLeast(strokeWidth * 2);
+ assertThat(bounds.height()).isAtLeast(strokeWidth * 2);
+ }
+
private boolean setForceDarkSysProp(boolean isForceDarkEnabled) {
try {
SystemProperties.set(
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 2e72f0ec90b8..4aa7e96f352d 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -572,6 +572,7 @@ applications that come with the platform
<permission name="android.permission.READ_BLOCKED_NUMBERS" />
<!-- Permission required for CTS test - PackageManagerTest -->
<permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/>
+ <permission name="android.permission.VERIFICATION_AGENT"/>
<!-- Permission required for CTS test CtsInputTestCases -->
<permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" />
<!-- Permission required for CTS test - PackageManagerShellCommandInstallTest -->
diff --git a/errorprone/OWNERS b/errorprone/OWNERS
index bddbdb364683..aa8c126a32e1 100644
--- a/errorprone/OWNERS
+++ b/errorprone/OWNERS
@@ -1,2 +1 @@
-jsharkey@android.com
-jsharkey@google.com
+colefaust@google.com
diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java
index 671eb6e514c5..ed17fdefcb53 100644
--- a/graphics/java/android/graphics/text/PositionedGlyphs.java
+++ b/graphics/java/android/graphics/text/PositionedGlyphs.java
@@ -26,6 +26,7 @@ import android.graphics.Typeface;
import android.graphics.fonts.Font;
import com.android.internal.util.Preconditions;
+import com.android.text.flags.Flags;
import dalvik.annotation.optimization.CriticalNative;
@@ -132,6 +133,9 @@ public final class PositionedGlyphs {
@NonNull
public Font getFont(@IntRange(from = 0) int index) {
Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
+ if (Flags.typefaceRedesign()) {
+ return mFonts.get(nGetFontId(mLayoutPtr, index));
+ }
return mFonts.get(index);
}
@@ -245,20 +249,29 @@ public final class PositionedGlyphs {
*/
public PositionedGlyphs(long layoutPtr, float xOffset, float yOffset) {
mLayoutPtr = layoutPtr;
- int glyphCount = nGetGlyphCount(layoutPtr);
- mFonts = new ArrayList<>(glyphCount);
mXOffset = xOffset;
mYOffset = yOffset;
- long prevPtr = 0;
- Font prevFont = null;
- for (int i = 0; i < glyphCount; ++i) {
- long ptr = nGetFont(layoutPtr, i);
- if (prevPtr != ptr) {
- prevPtr = ptr;
- prevFont = new Font(ptr);
+ if (Flags.typefaceRedesign()) {
+ int fontCount = nGetFontCount(layoutPtr);
+ mFonts = new ArrayList<>(fontCount);
+ for (int i = 0; i < fontCount; ++i) {
+ mFonts.add(new Font(nGetFontRef(layoutPtr, i)));
+ }
+ } else {
+ int glyphCount = nGetGlyphCount(layoutPtr);
+ mFonts = new ArrayList<>(glyphCount);
+
+ long prevPtr = 0;
+ Font prevFont = null;
+ for (int i = 0; i < glyphCount; ++i) {
+ long ptr = nGetFont(layoutPtr, i);
+ if (prevPtr != ptr) {
+ prevPtr = ptr;
+ prevFont = new Font(ptr);
+ }
+ mFonts.add(prevFont);
}
- mFonts.add(prevFont);
}
NoImagePreloadHolder.REGISTRY.registerNativeAllocation(this, layoutPtr);
@@ -290,6 +303,12 @@ public final class PositionedGlyphs {
private static native float nGetWeightOverride(long minikinLayout, int i);
@CriticalNative
private static native float nGetItalicOverride(long minikinLayout, int i);
+ @CriticalNative
+ private static native int nGetFontCount(long minikinLayout);
+ @CriticalNative
+ private static native long nGetFontRef(long minikinLayout, int fontId);
+ @CriticalNative
+ private static native int nGetFontId(long minikinLayout, int glyphIndex);
@Override
public boolean equals(Object o) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index 882a8d035e93..ad194f707cf3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -1255,7 +1255,8 @@ class DividerPresenter implements View.OnTouchListener {
// Update divider line surface visibility and color.
// If a container is fully expanded, the divider line is invisible unless dragging.
- final boolean isDividerLineVisible = !mProperties.mIsDraggableExpandType || mIsDragging;
+ final boolean isDividerLineVisible = mProperties.mDividerWidthPx > 0
+ && (!mProperties.mIsDraggableExpandType || mIsDragging);
t.setVisibility(mDividerLineSurface, isDividerLineVisible);
t.setColor(mDividerLineSurface, colorToFloatArray(
Color.valueOf(mProperties.mDividerAttributes.getDividerColor())));
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt
index f181ce004478..fa9d2baa78d9 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt
@@ -18,6 +18,7 @@ package com.android.wm.shell.bubbles.bar
import android.app.ActivityManager
import android.content.Context
+import android.content.pm.ShortcutInfo
import android.graphics.Insets
import android.graphics.Rect
import android.view.LayoutInflater
@@ -45,11 +46,14 @@ import com.android.wm.shell.shared.handles.RegionSamplingHelper
import com.android.wm.shell.taskview.TaskView
import com.android.wm.shell.taskview.TaskViewTaskController
import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import java.util.Collections
import java.util.concurrent.Executor
@@ -72,14 +76,18 @@ class BubbleBarExpandedViewTest {
private lateinit var expandedViewManager: BubbleExpandedViewManager
private lateinit var positioner: BubblePositioner
private lateinit var bubbleTaskView: BubbleTaskView
+ private lateinit var bubble: Bubble
private lateinit var bubbleExpandedView: BubbleBarExpandedView
private var testableRegionSamplingHelper: TestableRegionSamplingHelper? = null
private var regionSamplingProvider: TestRegionSamplingProvider? = null
+ private val bubbleLogger = spy(BubbleLogger(UiEventLoggerFake()))
+
@Before
fun setUp() {
ProtoLog.REQUIRE_PROTOLOGTOOL = false
+ ProtoLog.init()
mainExecutor = TestExecutor()
bgExecutor = TestExecutor()
positioner = BubblePositioner(context, windowManager)
@@ -108,7 +116,7 @@ class BubbleBarExpandedViewTest {
bubbleExpandedView.initialize(
expandedViewManager,
positioner,
- BubbleLogger(UiEventLoggerFake()),
+ bubbleLogger,
false /* isOverflow */,
bubbleTaskView,
mainExecutor,
@@ -121,6 +129,20 @@ class BubbleBarExpandedViewTest {
// Helper should be created once attached to window
testableRegionSamplingHelper = regionSamplingProvider!!.helper
})
+
+ bubble = Bubble(
+ "key",
+ ShortcutInfo.Builder(context, "id").build(),
+ 100 /* desiredHeight */,
+ 0 /* desiredHeightResId */,
+ "title",
+ 0 /* taskId */,
+ null /* locus */,
+ true /* isDismissable */,
+ directExecutor(),
+ directExecutor()
+ ) {}
+ bubbleExpandedView.update(bubble)
}
@After
@@ -194,6 +216,16 @@ class BubbleBarExpandedViewTest {
assertThat(testableRegionSamplingHelper!!.isStopped).isTrue()
}
+ @Test
+ fun testEventLogging_dismissBubbleViaAppMenu() {
+ getInstrumentation().runOnMainSync { bubbleExpandedView.handleView.performClick() }
+ val dismissMenuItem =
+ bubbleExpandedView.findViewWithTag<View>(BubbleBarMenuView.DISMISS_ACTION_TAG)
+ assertThat(dismissMenuItem).isNotNull()
+ getInstrumentation().runOnMainSync { dismissMenuItem.performClick() }
+ verify(bubbleLogger).log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_DISMISSED_APP_MENU)
+ }
+
private inner class FakeBubbleTaskViewFactory : BubbleTaskViewFactory {
override fun create(): BubbleTaskView {
val taskViewTaskController = mock<TaskViewTaskController>()
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 35ef2393bb9b..37596182f05b 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -38,7 +38,7 @@
<Button
android:layout_width="94dp"
android:layout_height="60dp"
- android:id="@+id/maximize_menu_maximize_button"
+ android:id="@+id/maximize_menu_size_toggle_button"
style="?android:attr/buttonBarButtonStyle"
android:stateListAnimator="@null"
android:importantForAccessibility="yes"
@@ -48,7 +48,7 @@
android:alpha="0"/>
<TextView
- android:id="@+id/maximize_menu_maximize_window_text"
+ android:id="@+id/maximize_menu_size_toggle_button_text"
android:layout_width="94dp"
android:layout_height="18dp"
android:textSize="11sp"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 1f1565160965..df1e2248872b 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -492,8 +492,12 @@
<dimen name="desktop_mode_maximize_menu_buttons_outline_stroke">1dp</dimen>
<!-- The radius of the inner fill of the maximize menu buttons. -->
<dimen name="desktop_mode_maximize_menu_buttons_fill_radius">4dp</dimen>
- <!-- The padding between the outline and fill of the maximize menu buttons. -->
- <dimen name="desktop_mode_maximize_menu_buttons_fill_padding">4dp</dimen>
+ <!-- The padding between the outline and fill of the maximize menu snap and maximize buttons. -->
+ <dimen name="desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding">4dp</dimen>
+ <!-- The vertical padding between the outline and fill of the maximize menu restore button. -->
+ <dimen name="desktop_mode_maximize_menu_restore_button_fill_vertical_padding">13dp</dimen>
+ <!-- The horizontal padding between the outline and fill of the maximize menu restore button. -->
+ <dimen name="desktop_mode_maximize_menu_restore_button_fill_horizontal_padding">21dp</dimen>
<!-- The corner radius of the maximize menu. -->
<dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 621e2aacd673..afac9f6433a3 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -319,6 +319,8 @@
<string name="desktop_mode_non_resizable_snap_text">App can\'t be moved here</string>
<!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] -->
<string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string>
+ <!-- Accessibility text for the Maximize Menu's restore button [CHAR LIMIT=NONE] -->
+ <string name="desktop_mode_maximize_menu_restore_button_text">Restore</string>
<!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] -->
<string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string>
<!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 84405bbe5823..0ce651c3f1fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -252,6 +252,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
@Override
public void onDismissBubble(Bubble bubble) {
mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE);
+ mBubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_DISMISSED_APP_MENU);
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
index 0300869cbbe1..52b807abddd6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.bubbles.bar;
import android.annotation.ColorInt;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
@@ -41,6 +42,9 @@ import java.util.ArrayList;
* Bubble bar expanded view menu
*/
public class BubbleBarMenuView extends LinearLayout {
+
+ public static final Object DISMISS_ACTION_TAG = new Object();
+
private ViewGroup mBubbleSectionView;
private ViewGroup mActionsSectionView;
private ImageView mBubbleIconView;
@@ -119,6 +123,9 @@ public class BubbleBarMenuView extends LinearLayout {
R.layout.bubble_bar_menu_item, mActionsSectionView, false);
itemView.update(action.mIcon, action.mTitle, action.mTint);
itemView.setOnClickListener(action.mOnClick);
+ if (action.mTag != null) {
+ itemView.setTag(action.mTag);
+ }
mActionsSectionView.addView(itemView);
}
}
@@ -159,6 +166,8 @@ public class BubbleBarMenuView extends LinearLayout {
private Icon mIcon;
private @ColorInt int mTint;
private String mTitle;
+ @Nullable
+ private Object mTag;
private OnClickListener mOnClick;
MenuAction(Icon icon, String title, OnClickListener onClick) {
@@ -171,5 +180,14 @@ public class BubbleBarMenuView extends LinearLayout {
this.mTint = tint;
this.mOnClick = onClick;
}
+
+ MenuAction(Icon icon, String title, @ColorInt int tint, @Nullable Object tag,
+ OnClickListener onClick) {
+ this.mIcon = icon;
+ this.mTitle = title;
+ this.mTint = tint;
+ this.mTag = tag;
+ this.mOnClick = onClick;
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
index 514810745e10..5ed01b66ec67 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
@@ -212,6 +212,7 @@ class BubbleBarMenuViewController {
Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow),
resources.getString(R.string.bubble_dismiss_text),
tintColor,
+ BubbleBarMenuView.DISMISS_ACTION_TAG,
view -> {
hideMenu(true /* animated */);
if (mListener != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index b723337cc894..4440778a5a45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -1185,7 +1185,13 @@ class DesktopTasksController(
val options = createNewWindowOptions(callingTask)
if (options.launchWindowingMode == WINDOWING_MODE_FREEFORM) {
wct.startTask(requestedTaskId, options.toBundle())
- transitions.startTransition(TRANSIT_OPEN, wct, null)
+ val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
+ callingTask.displayId, wct, requestedTaskId)
+ val runOnTransit = immersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, callingTask.displayId)
+ val transition = transitions.startTransition(TRANSIT_OPEN, wct, null)
+ addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
} else {
val splitPosition = splitScreenController.determineNewInstancePosition(callingTask)
splitScreenController.startTask(requestedTaskId, splitPosition,
@@ -1219,7 +1225,8 @@ class DesktopTasksController(
.determineNewInstancePosition(callingTaskInfo)
splitScreenController.startIntent(
launchIntent, context.userId, fillIn, splitPosition,
- options.toBundle(), null /* hideTaskToken */
+ options.toBundle(), null /* hideTaskToken */,
+ true /* forceLaunchNewTask */
)
}
WINDOWING_MODE_FREEFORM -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index cbb08b804dfe..6da39951efbe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -463,7 +463,7 @@ public class PipTransition extends PipTransitionController {
// so update fixed rotation state to default.
mFixedRotationState = FIXED_ROTATION_UNDEFINED;
- if (transition != mExitTransition) {
+ if (transition != mExitTransition && transition != mMoveToBackTransition) {
return;
}
// This means an expand happened before enter-pip finished and we are now "merging" a
@@ -477,8 +477,10 @@ public class PipTransition extends PipTransitionController {
cancelled = true;
}
- // Unset exitTransition AFTER cancel so that finishResize knows we are merging.
+ // Unset both exitTransition and moveToBackTransition AFTER cancel so that
+ // finishResize knows we are merging.
mExitTransition = null;
+ mMoveToBackTransition = null;
if (!cancelled) return;
final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo();
if (taskInfo != null) {
@@ -515,7 +517,8 @@ public class PipTransition extends PipTransitionController {
// means we're expecting the exit transition will be "merged" into another transition
// (likely a remote like launcher), so don't fire the finish-callback here -- wait until
// the exit transition is merged.
- if ((mExitTransition == null || isAnimatingLocally()) && mFinishCallback != null) {
+ if ((mExitTransition == null || mMoveToBackTransition == null || isAnimatingLocally())
+ && mFinishCallback != null) {
final SurfaceControl leash = mPipOrganizer.getSurfaceControl();
final boolean hasValidLeash = leash != null && leash.isValid();
WindowContainerTransaction wct = null;
@@ -665,17 +668,6 @@ public class PipTransition extends PipTransitionController {
return null;
}
- @Nullable
- private TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) {
- for (int i = info.getChanges().size() - 1; i >= 0; --i) {
- final TransitionInfo.Change change = info.getChanges().get(i);
- if (change.getEndFixedRotation() != ROTATION_UNDEFINED) {
- return change;
- }
- }
- return null;
- }
-
private void startExitAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index 9b815817d4d3..94b344fb575a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.pip;
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.WindowManager.TRANSIT_PIP;
@@ -346,6 +347,21 @@ public abstract class PipTransitionController implements Transitions.TransitionH
return false;
}
+ /**
+ * Gets a change amongst the transition targets that is in a different final orientation than
+ * the display, signalling a potential fixed rotation transition.
+ */
+ @Nullable
+ public TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getEndFixedRotation() != ROTATION_UNDEFINED) {
+ return change;
+ }
+ }
+ return null;
+ }
+
/** End the currently-playing PiP animation. */
public void end() {
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
index f40a87c39aef..fcd5c3baab5d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
@@ -16,10 +16,14 @@
package com.android.wm.shell.pip2.animation;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
import android.animation.Animator;
import android.animation.RectEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
+import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.view.Surface;
@@ -60,6 +64,11 @@ public class PipEnterAnimator extends ValueAnimator
private final PointF mInitScale = new PointF();
private final PointF mInitPos = new PointF();
private final Rect mInitCrop = new Rect();
+ private final PointF mInitActivityScale = new PointF();
+ private final PointF mInitActivityPos = new PointF();
+
+ Matrix mTransformTensor = new Matrix();
+ final float[] mMatrixTmp = new float[9];
public PipEnterAnimator(Context context,
@NonNull SurfaceControl leash,
@@ -109,6 +118,10 @@ public class PipEnterAnimator extends ValueAnimator
@Override
public void onAnimationEnd(@NonNull Animator animation) {
+ if (mFinishTransaction != null) {
+ onEnterAnimationUpdate(mInitScale, mInitPos, mInitCrop,
+ 1f /* fraction */, mFinishTransaction);
+ }
if (mAnimationEndCallback != null) {
mAnimationEndCallback.run();
}
@@ -126,16 +139,24 @@ public class PipEnterAnimator extends ValueAnimator
float fraction, SurfaceControl.Transaction tx) {
float scaleX = 1 + (initScale.x - 1) * (1 - fraction);
float scaleY = 1 + (initScale.y - 1) * (1 - fraction);
- tx.setScale(mLeash, scaleX, scaleY);
-
float posX = initPos.x + (mEndBounds.left - initPos.x) * fraction;
float posY = initPos.y + (mEndBounds.top - initPos.y) * fraction;
- tx.setPosition(mLeash, posX, posY);
+
+ int normalizedRotation = mRotation;
+ if (normalizedRotation == ROTATION_270) {
+ normalizedRotation = -ROTATION_90;
+ }
+ float degrees = -normalizedRotation * 90f * fraction;
Rect endCrop = new Rect(mEndBounds);
endCrop.offsetTo(0, 0);
mRectEvaluator.evaluate(fraction, initCrop, endCrop);
tx.setCrop(mLeash, mAnimatedRect);
+
+ mTransformTensor.setScale(scaleX, scaleY);
+ mTransformTensor.postTranslate(posX, posY);
+ mTransformTensor.postRotate(degrees);
+ tx.setMatrix(mLeash, mTransformTensor, mMatrixTmp);
}
// no-ops
@@ -153,7 +174,22 @@ public class PipEnterAnimator extends ValueAnimator
* calculated differently from generic transitions.
* @param pipChange PiP change received as a transition target.
*/
- public void setEnterStartState(@NonNull TransitionInfo.Change pipChange) {
+ public void setEnterStartState(@NonNull TransitionInfo.Change pipChange,
+ @NonNull TransitionInfo.Change pipActivityChange) {
+ PipUtils.calcEndTransform(pipActivityChange, pipChange, mInitActivityScale,
+ mInitActivityPos);
+ if (mStartTransaction != null && pipActivityChange.getLeash() != null) {
+ mStartTransaction.setCrop(pipActivityChange.getLeash(), null);
+ mStartTransaction.setScale(pipActivityChange.getLeash(), mInitActivityScale.x,
+ mInitActivityScale.y);
+ mStartTransaction.setPosition(pipActivityChange.getLeash(), mInitActivityPos.x,
+ mInitActivityPos.y);
+ mFinishTransaction.setCrop(pipActivityChange.getLeash(), null);
+ mFinishTransaction.setScale(pipActivityChange.getLeash(), mInitActivityScale.x,
+ mInitActivityScale.y);
+ mFinishTransaction.setPosition(pipActivityChange.getLeash(), mInitActivityPos.x,
+ mInitActivityPos.y);
+ }
PipUtils.calcStartTransform(pipChange, mInitScale, mInitPos, mInitCrop);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java
index 8fa5aa933929..a93ef12cb7fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java
@@ -157,6 +157,7 @@ public class PipExpandAnimator extends ValueAnimator
.shadow(tx, mLeash, false /* applyCornerRadius */);
tx.apply();
}
+
private Rect getInsets(float fraction) {
final Rect startInsets = mSourceRectHintInsets;
final Rect endInsets = mZeroInsets;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 73be8db0ea8a..0427294579dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -292,23 +292,34 @@ public class PipController implements ConfigurationChangeListener,
setDisplayLayout(mDisplayController.getDisplayLayout(displayId));
if (!mPipTransitionState.isInPip()) {
+ // Skip the PiP-relevant updates if we aren't in a valid PiP state.
+ if (mPipTransitionState.isInFixedRotation()) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Fixed rotation flag shouldn't be set while in an invalid PiP state");
+ }
return;
}
mPipTouchHandler.updateMinMaxSize(mPipBoundsState.getAspectRatio());
- // Update the caches to reflect the new display layout in the movement bounds;
- // temporarily update bounds to be at the top left for the movement bounds calculation.
- Rect toBounds = new Rect(0, 0,
- (int) Math.ceil(mPipBoundsState.getMaxSize().x * boundsScale),
- (int) Math.ceil(mPipBoundsState.getMaxSize().y * boundsScale));
- mPipBoundsState.setBounds(toBounds);
- mPipTouchHandler.updateMovementBounds();
-
- // The policy is to keep PiP snap fraction invariant.
- mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction);
- mPipBoundsState.setBounds(toBounds);
- t.setBounds(mPipTransitionState.mPipTaskToken, toBounds);
+ if (mPipTransitionState.isInFixedRotation()) {
+ // Do not change the bounds when in fixed rotation, but do update the movement bounds
+ // based on the current bounds state and potentially new display layout.
+ mPipTouchHandler.updateMovementBounds();
+ mPipTransitionState.setInFixedRotation(false);
+ } else {
+ Rect toBounds = new Rect(0, 0,
+ (int) Math.ceil(mPipBoundsState.getMaxSize().x * boundsScale),
+ (int) Math.ceil(mPipBoundsState.getMaxSize().y * boundsScale));
+ // Update the caches to reflect the new display layout in the movement bounds;
+ // temporarily update bounds to be at the top left for the movement bounds calculation.
+ mPipBoundsState.setBounds(toBounds);
+ mPipTouchHandler.updateMovementBounds();
+ // The policy is to keep PiP snap fraction invariant.
+ mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction);
+ mPipBoundsState.setBounds(toBounds);
+ }
+ t.setBounds(mPipTransitionState.mPipTaskToken, mPipBoundsState.getBounds());
}
private void setDisplayLayout(DisplayLayout layout) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
index a4a7973ef4bb..4d0432e1066e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
@@ -406,12 +406,9 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha
// We need to remove the callback even if the shelf is visible, in case it the delayed
// callback hasn't been executed yet to avoid the wrong final state.
mMainExecutor.removeCallbacks(mMoveOnShelVisibilityChanged);
- if (shelfVisible) {
- mMoveOnShelVisibilityChanged.run();
- } else {
- // Postpone moving in response to hide of Launcher in case there's another change
- mMainExecutor.executeDelayed(mMoveOnShelVisibilityChanged, PIP_KEEP_CLEAR_AREAS_DELAY);
- }
+
+ // Postpone moving in response to hide of Launcher in case there's another change
+ mMainExecutor.executeDelayed(mMoveOnShelVisibilityChanged, PIP_KEEP_CLEAR_AREAS_DELAY);
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index ac1567aba6e9..779e4ea51347 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -16,7 +16,9 @@
package com.android.wm.shell.pip2.phone;
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_270;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
@@ -36,6 +38,7 @@ import android.app.ActivityManager;
import android.app.PictureInPictureParams;
import android.content.Context;
import android.graphics.Matrix;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
@@ -388,8 +391,15 @@ public class PipTransition extends PipTransitionController implements
return false;
}
- Rect startBounds = pipChange.getStartAbsBounds();
+ // We expect the PiP activity as a separate change in a config-at-end transition.
+ TransitionInfo.Change pipActivityChange = getDeferConfigActivityChange(info,
+ pipChange.getTaskInfo().getToken());
+ if (pipActivityChange == null) {
+ return false;
+ }
+
Rect endBounds = pipChange.getEndAbsBounds();
+ Rect activityEndBounds = pipActivityChange.getEndAbsBounds();
SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition.");
@@ -411,14 +421,63 @@ public class PipTransition extends PipTransitionController implements
}
}
+ final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
+ int startRotation = pipChange.getStartRotation();
+ int endRotation = fixedRotationChange != null
+ ? fixedRotationChange.getEndFixedRotation() : ROTATION_UNDEFINED;
+ final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
+ : startRotation - endRotation;
+
+ if (delta != ROTATION_0) {
+ mPipTransitionState.setInFixedRotation(true);
+ handleBoundsTypeFixedRotation(pipChange, pipActivityChange, fixedRotationChange);
+ }
+
PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash,
- startTransaction, finishTransaction, endBounds, sourceRectHint, Surface.ROTATION_0);
- animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange));
+ startTransaction, finishTransaction, endBounds, sourceRectHint, delta);
+ animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange,
+ pipActivityChange));
animator.setAnimationEndCallback(this::finishInner);
animator.start();
return true;
}
+ private void handleBoundsTypeFixedRotation(TransitionInfo.Change pipTaskChange,
+ TransitionInfo.Change pipActivityChange,
+ TransitionInfo.Change fixedRotationChange) {
+ final Rect endBounds = pipTaskChange.getEndAbsBounds();
+ final Rect endActivityBounds = pipActivityChange.getEndAbsBounds();
+ int startRotation = pipTaskChange.getStartRotation();
+ int endRotation = fixedRotationChange.getEndFixedRotation();
+
+ // Cache the task to activity offset to potentially restore later.
+ Point activityEndOffset = new Point(endActivityBounds.left - endBounds.left,
+ endActivityBounds.top - endBounds.top);
+
+ // If we are running a fixed rotation bounds enter PiP animation,
+ // then update the display layout rotation, and recalculate the end rotation bounds.
+ // Update the endBounds in place, so that the PiP change is up-to-date.
+ mPipDisplayLayoutState.rotateTo(endRotation);
+ float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
+ mPipBoundsAlgorithm.getEntryDestinationBounds());
+ mPipBoundsAlgorithm.applySnapFraction(endBounds, snapFraction);
+ mPipBoundsState.setBounds(endBounds);
+
+ // Display bounds were already updated to represent the final orientation,
+ // so we just need to readjust the origin, and perform rotation about (0, 0).
+ boolean isClockwise = (endRotation - startRotation) == -ROTATION_270;
+ Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds();
+ int originTranslateX = isClockwise ? 0 : -displayBounds.width();
+ int originTranslateY = isClockwise ? -displayBounds.height() : 0;
+ endBounds.offset(originTranslateX, originTranslateY);
+
+ // Update the activity end bounds in place as well, as this is used for transform
+ // calculation later.
+ endActivityBounds.offsetTo(endBounds.left + activityEndOffset.x,
+ endBounds.top + activityEndOffset.y);
+ }
+
+
private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -533,6 +592,19 @@ public class PipTransition extends PipTransitionController implements
}
@Nullable
+ private TransitionInfo.Change getDeferConfigActivityChange(TransitionInfo info,
+ @NonNull WindowContainerToken parent) {
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getTaskInfo() == null
+ && change.hasFlags(TransitionInfo.FLAG_CONFIG_AT_END)
+ && change.getParent() != null && change.getParent().equals(parent)) {
+ return change;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
private TransitionInfo.Change getChangeByToken(TransitionInfo info,
WindowContainerToken token) {
for (TransitionInfo.Change change : info.getChanges()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
index a132796f4a84..ccdd66b5d1a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
@@ -155,6 +155,8 @@ public class PipTransitionState {
@Nullable
private Runnable mOnIdlePipTransitionStateRunnable;
+ private boolean mInFixedRotation = false;
+
/**
* An interface to track state updates as we progress through PiP transitions.
*/
@@ -256,7 +258,7 @@ public class PipTransitionState {
private void maybeRunOnIdlePipTransitionStateCallback() {
if (mOnIdlePipTransitionStateRunnable != null && isPipStateIdle()) {
- mOnIdlePipTransitionStateRunnable.run();
+ mMainHandler.post(mOnIdlePipTransitionStateRunnable);
mOnIdlePipTransitionStateRunnable = null;
}
}
@@ -303,6 +305,23 @@ public class PipTransitionState {
}
/**
+ * @return true if either in swipe or button-nav fixed rotation.
+ */
+ public boolean isInFixedRotation() {
+ return mInFixedRotation;
+ }
+
+ /**
+ * Sets the fixed rotation flag.
+ */
+ public void setInFixedRotation(boolean inFixedRotation) {
+ mInFixedRotation = inFixedRotation;
+ if (!inFixedRotation) {
+ maybeRunOnIdlePipTransitionStateCallback();
+ }
+ }
+
+ /**
* @return true if in swipe PiP to home. Note that this is true until overlay fades if used too.
*/
public boolean isInSwipePipToHomeTransition() {
@@ -351,7 +370,7 @@ public class PipTransitionState {
public boolean isPipStateIdle() {
// This needs to be a valid in-PiP state that isn't a transient state.
- return mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS;
+ return (mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS) && !isInFixedRotation();
}
@Override
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 9e39f440915c..a23b576beebc 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
@@ -772,15 +772,25 @@ public class SplitScreenController implements SplitDragPolicy.Starter,
instanceId);
}
+ @Override
+ public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent,
+ @SplitPosition int position, @Nullable Bundle options,
+ @Nullable WindowContainerToken hideTaskToken) {
+ startIntent(intent, userId1, fillInIntent, position, options, hideTaskToken,
+ false /* forceLaunchNewTask */);
+ }
+
/**
* Starts the given intent into split.
+ *
* @param hideTaskToken If non-null, a task matching this token will be moved to back in the
* same window container transaction as the starting of the intent.
+ * @param forceLaunchNewTask If true, this method will skip the check for a background task
+ * matching the intent and launch a new task.
*/
- @Override
public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent,
@SplitPosition int position, @Nullable Bundle options,
- @Nullable WindowContainerToken hideTaskToken) {
+ @Nullable WindowContainerToken hideTaskToken, boolean forceLaunchNewTask) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
"startIntent(): intent=%s user=%d fillInIntent=%s position=%d", intent, userId1,
fillInIntent, position);
@@ -798,8 +808,9 @@ public class SplitScreenController implements SplitDragPolicy.Starter,
// To prevent accumulating large number of instances in the background, reuse task
// in the background. If we don't explicitly reuse, new may be created even if the app
// isn't multi-instance because WM won't automatically remove/reuse the previous instance
- final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
- .map(recentTasks -> recentTasks.findTaskInBackground(component, userId1,
+ final ActivityManager.RecentTaskInfo taskInfo = forceLaunchNewTask ? null :
+ mRecentTasksOptional
+ .map(recentTasks -> recentTasks.findTaskInBackground(component, userId1,
hideTaskToken))
.orElse(null);
if (taskInfo != null) {
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 ae4bd1615ae1..6313231b449e 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
@@ -244,8 +244,9 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener {
return;
}
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
+ mVisible = taskInfo.isVisible && taskInfo.isVisibleRequested;
mCallbacks.onChildTaskStatusChanged(this, taskInfo.taskId, true /* present */,
- taskInfo.isVisible && taskInfo.isVisibleRequested);
+ mVisible);
} else {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 0cb219ae4b81..3ae5a1afc7e2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -62,6 +62,7 @@ import com.android.wm.shell.R
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.calculateMaximizeBounds
import com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.wm.shell.shared.animation.Interpolators.FAST_OUT_LINEAR_IN
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer
@@ -73,7 +74,8 @@ import java.util.function.Supplier
/**
* Menu that appears when user long clicks the maximize button. Gives the user the option to
- * maximize the task or snap the task to the right or left half of the screen.
+ * maximize the task or restore previous task bounds from the maximized state and to snap the task
+ * to the right or left half of the screen.
*/
class MaximizeMenu(
private val syncQueue: SyncTransactionQueue,
@@ -176,6 +178,7 @@ class MaximizeMenu(
"MaximizeMenu")
maximizeMenuView = MaximizeMenuView(
context = decorWindowContext,
+ sizeToggleDirection = getSizeToggleDirection(),
menuHeight = menuHeight,
menuPadding = menuPadding,
).also { menuView ->
@@ -202,6 +205,18 @@ class MaximizeMenu(
}
}
+ private fun getSizeToggleDirection(): MaximizeMenuView.SizeToggleDirection {
+ val maximizeBounds = calculateMaximizeBounds(
+ displayController.getDisplayLayout(taskInfo.displayId)!!,
+ taskInfo
+ )
+ val maximized = taskInfo.configuration.windowConfiguration.bounds.equals(maximizeBounds)
+ return if (maximized)
+ MaximizeMenuView.SizeToggleDirection.RESTORE
+ else
+ MaximizeMenuView.SizeToggleDirection.MAXIMIZE
+ }
+
private fun loadDimensionPixelSize(resourceId: Int): Int {
return if (resourceId == Resources.ID_NULL) {
0
@@ -236,18 +251,19 @@ class MaximizeMenu(
* resizing a Task.
*/
class MaximizeMenuView(
- context: Context,
+ private val context: Context,
+ private val sizeToggleDirection: SizeToggleDirection,
private val menuHeight: Int,
- private val menuPadding: Int,
+ private val menuPadding: Int
) {
val rootView = LayoutInflater.from(context)
.inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) as ViewGroup
private val container = requireViewById(R.id.container)
private val overlay = requireViewById(R.id.maximize_menu_overlay)
- private val maximizeText =
- requireViewById(R.id.maximize_menu_maximize_window_text) as TextView
- private val maximizeButton =
- requireViewById(R.id.maximize_menu_maximize_button) as Button
+ private val sizeToggleButtonText =
+ requireViewById(R.id.maximize_menu_size_toggle_button_text) as TextView
+ private val sizeToggleButton =
+ requireViewById(R.id.maximize_menu_size_toggle_button) as Button
private val snapWindowText =
requireViewById(R.id.maximize_menu_snap_window_text) as TextView
private val snapRightButton =
@@ -263,8 +279,6 @@ class MaximizeMenu(
.getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_radius)
private val outlineStroke = context.resources
.getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_stroke)
- private val fillPadding = context.resources
- .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_padding)
private val fillRadius = context.resources
.getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius)
@@ -324,7 +338,7 @@ class MaximizeMenu(
return@setOnHoverListener false
}
- maximizeButton.setOnClickListener { onMaximizeClickListener?.invoke() }
+ sizeToggleButton.setOnClickListener { onMaximizeClickListener?.invoke() }
snapRightButton.setOnClickListener { onRightSnapClickListener?.invoke() }
snapLeftButton.setOnClickListener { onLeftSnapClickListener?.invoke() }
rootView.setOnTouchListener { _, event ->
@@ -335,9 +349,17 @@ class MaximizeMenu(
true
}
+ val btnTextId = if (sizeToggleDirection == SizeToggleDirection.RESTORE)
+ R.string.desktop_mode_maximize_menu_restore_button_text
+ else
+ R.string.desktop_mode_maximize_menu_maximize_button_text
+ val btnText = context.resources.getText(btnTextId)
+ sizeToggleButton.contentDescription = btnText
+ sizeToggleButtonText.text = btnText
+
// To prevent aliasing.
- maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
- maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}
/** Bind the menu views to the new [RunningTaskInfo] data. */
@@ -348,8 +370,8 @@ class MaximizeMenu(
rootView.background.setTint(style.backgroundColor)
// Maximize option.
- maximizeButton.background = style.maximizeOption.drawable
- maximizeText.setTextColor(style.textColor)
+ sizeToggleButton.background = style.maximizeOption.drawable
+ sizeToggleButtonText.setTextColor(style.textColor)
// Snap options.
snapWindowText.setTextColor(style.textColor)
@@ -358,8 +380,8 @@ class MaximizeMenu(
/** Animate the opening of the menu */
fun animateOpenMenu(onEnd: () -> Unit) {
- maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
- maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
+ sizeToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
+ sizeToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
menuAnimatorSet = AnimatorSet()
menuAnimatorSet?.playTogether(
ObjectAnimator.ofFloat(rootView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f)
@@ -388,9 +410,9 @@ class MaximizeMenu(
// Scale up the children of the maximize menu so that the menu
// scale is cancelled out and only the background is scaled.
val value = animatedValue as Float
- maximizeButton.scaleY = value
+ sizeToggleButton.scaleY = value
snapButtonsLayout.scaleY = value
- maximizeText.scaleY = value
+ sizeToggleButtonText.scaleY = value
snapWindowText.scaleY = value
}
},
@@ -409,9 +431,9 @@ class MaximizeMenu(
startDelay = CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS
addUpdateListener {
val value = animatedValue as Float
- maximizeButton.alpha = value
+ sizeToggleButton.alpha = value
snapButtonsLayout.alpha = value
- maximizeText.alpha = value
+ sizeToggleButtonText.alpha = value
snapWindowText.alpha = value
}
},
@@ -423,8 +445,8 @@ class MaximizeMenu(
)
menuAnimatorSet?.addListener(
onEnd = {
- maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
- maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
onEnd.invoke()
}
)
@@ -433,8 +455,8 @@ class MaximizeMenu(
/** Animate the closing of the menu */
fun animateCloseMenu(onEnd: (() -> Unit)) {
- maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
- maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
+ sizeToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
+ sizeToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
cancelAnimation()
menuAnimatorSet = AnimatorSet()
menuAnimatorSet?.playTogether(
@@ -464,9 +486,9 @@ class MaximizeMenu(
// Scale up the children of the maximize menu so that the menu
// scale is cancelled out and only the background is scaled.
val value = animatedValue as Float
- maximizeButton.scaleY = value
+ sizeToggleButton.scaleY = value
snapButtonsLayout.scaleY = value
- maximizeText.scaleY = value
+ sizeToggleButtonText.scaleY = value
snapWindowText.scaleY = value
}
},
@@ -485,9 +507,9 @@ class MaximizeMenu(
duration = ALPHA_ANIMATION_DURATION_MS
addUpdateListener {
val value = animatedValue as Float
- maximizeButton.alpha = value
+ sizeToggleButton.alpha = value
snapButtonsLayout.alpha = value
- maximizeText.alpha = value
+ sizeToggleButtonText.alpha = value
snapWindowText.alpha = value
}
},
@@ -498,8 +520,8 @@ class MaximizeMenu(
)
menuAnimatorSet?.addListener(
onEnd = {
- maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
- maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
onEnd?.invoke()
}
)
@@ -509,8 +531,8 @@ class MaximizeMenu(
/** Request that the accessibility service focus on the menu. */
fun requestAccessibilityFocus() {
// Focus the first button in the menu by default.
- maximizeButton.post {
- maximizeButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
+ sizeToggleButton.post {
+ sizeToggleButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}
}
@@ -685,15 +707,31 @@ class MaximizeMenu(
paint.color = strokeAndFillColor
paint.style = Paint.Style.FILL
})
+
+ val (horizontalFillPadding, verticalFillPadding) =
+ if (sizeToggleDirection == SizeToggleDirection.MAXIMIZE) {
+ context.resources.getDimensionPixelSize(R.dimen
+ .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding) to
+ context.resources.getDimensionPixelSize(R.dimen
+ .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding)
+ } else {
+ context.resources.getDimensionPixelSize(R.dimen
+ .desktop_mode_maximize_menu_restore_button_fill_horizontal_padding) to
+ context.resources.getDimensionPixelSize(R.dimen
+ .desktop_mode_maximize_menu_restore_button_fill_vertical_padding)
+ }
+
return LayerDrawable(layers.toTypedArray()).apply {
when (numberOfLayers) {
3 -> {
setLayerInset(1, outlineStroke)
- setLayerInset(2, fillPadding)
+ setLayerInset(2, horizontalFillPadding, verticalFillPadding,
+ horizontalFillPadding, verticalFillPadding)
}
4 -> {
setLayerInset(intArrayOf(1, 2), outlineStroke)
- setLayerInset(3, fillPadding)
+ setLayerInset(3, horizontalFillPadding, verticalFillPadding,
+ horizontalFillPadding, verticalFillPadding)
}
else -> error("Unexpected number of layers: $numberOfLayers")
}
@@ -737,6 +775,11 @@ class MaximizeMenu(
enum class SnapToHalfSelection {
NONE, LEFT, RIGHT
}
+
+ /** The possible selection states of the size toggle button in the maximize menu. */
+ enum class SizeToggleDirection {
+ MAXIMIZE, RESTORE
+ }
}
companion object {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index b3c10d64c3a3..f9376570dc83 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -2902,7 +2902,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
runOpenNewWindow(task)
verify(splitScreenController)
.startIntent(any(), anyInt(), any(), any(),
- optionsCaptor.capture(), anyOrNull())
+ optionsCaptor.capture(), anyOrNull(), eq(true)
+ )
assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
.isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
}
@@ -2917,7 +2918,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
verify(splitScreenController)
.startIntent(
any(), anyInt(), any(), any(),
- optionsCaptor.capture(), anyOrNull()
+ optionsCaptor.capture(), anyOrNull(), eq(true)
)
assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
.isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
@@ -2984,6 +2985,58 @@ class DesktopTasksControllerTest : ShellTestCase() {
.launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+ fun openInstance_fromFreeform_minimizesIfNeeded() {
+ setUpLandscapeDisplay()
+ val homeTask = setUpHomeTask()
+ val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() }
+ val oldestTask = freeformTasks.first()
+ val newestTask = freeformTasks.last()
+
+ runOpenInstance(newestTask, freeformTasks[1].taskId)
+
+ val wct = getLatestWct(type = TRANSIT_OPEN)
+ // Home is moved to front of everything.
+ assertThat(
+ wct.hierarchyOps.any { hop ->
+ hop.container == homeTask.token.asBinder() && hop.toTop
+ }
+ ).isTrue()
+ // And the oldest task isn't moved in front of home, effectively minimizing it.
+ assertThat(
+ wct.hierarchyOps.none { hop ->
+ hop.container == oldestTask.token.asBinder() && hop.toTop
+ }
+ ).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+ fun openInstance_fromFreeform_exitsImmersiveIfNeeded() {
+ setUpLandscapeDisplay()
+ val homeTask = setUpHomeTask()
+ val freeformTask = setUpFreeformTask()
+ val immersiveTask = setUpFreeformTask()
+ taskRepository.setTaskInFullImmersiveState(
+ displayId = immersiveTask.displayId,
+ taskId = immersiveTask.taskId,
+ immersive = true
+ )
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(transitions.startTransition(eq(TRANSIT_OPEN), any(), anyOrNull()))
+ .thenReturn(transition)
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId))).thenReturn(runOnStartTransit)
+
+ runOpenInstance(immersiveTask, freeformTask.taskId)
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId))
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
private fun runOpenInstance(
callingTask: RunningTaskInfo,
requestedTaskId: Int
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 9260a07fd945..ef3af8e7bdac 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -297,6 +297,28 @@ public class SplitScreenControllerTests extends ShellTestCase {
}
@Test
+ public void startIntent_forceLaunchNewTaskTrue_skipsBackgroundTasks() {
+ Intent startIntent = createStartIntent("startActivity");
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+ mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
+ SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */,
+ true /* forceLaunchNewTask */);
+ verify(mRecentTasks, never()).findTaskInBackground(any(), anyInt(), any());
+ }
+
+ @Test
+ public void startIntent_forceLaunchNewTaskFalse_checksBackgroundTasks() {
+ Intent startIntent = createStartIntent("startActivity");
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+ mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
+ SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */,
+ false /* forceLaunchNewTask */);
+ verify(mRecentTasks).findTaskInBackground(any(), anyInt(), any());
+ }
+
+ @Test
public void testSwitchSplitPosition_checksIsSplitScreenVisible() {
final String reason = "test";
when(mSplitScreenController.isSplitScreenVisible()).thenReturn(true, false);
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index e9845c1d9f13..27817e9eb984 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -4,7 +4,6 @@ package com.google.android.appfunctions.sidecar {
public final class AppFunctionManager {
ctor public AppFunctionManager(android.content.Context);
method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
- method @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>);
method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>);
field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0
@@ -15,9 +14,7 @@ package com.google.android.appfunctions.sidecar {
public abstract class AppFunctionService extends android.app.Service {
ctor public AppFunctionService();
method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
- method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
- method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
- method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
index d660926575d1..43377d8eb91c 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
@@ -126,33 +126,6 @@ public final class AppFunctionManager {
}
/**
- * Executes the app function.
- *
- * <p>Proxies request and response to the underlying {@link
- * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and
- * response in the appropriate type required by the function.
- *
- * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor,
- * CancellationSignal, Consumer)} instead. This method will be removed once usage references
- * are updated.
- */
- @Deprecated
- public void executeAppFunction(
- @NonNull ExecuteAppFunctionRequest sidecarRequest,
- @NonNull @CallbackExecutor Executor executor,
- @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
- Objects.requireNonNull(sidecarRequest);
- Objects.requireNonNull(executor);
- Objects.requireNonNull(callback);
-
- executeAppFunction(
- sidecarRequest,
- executor,
- new CancellationSignal(),
- callback);
- }
-
- /**
* Returns a boolean through a callback, indicating whether the app function is enabled.
*
* <p>* This method can only check app functions owned by the caller, or those where the caller
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
index 2a168e871713..0dc87e45b7e3 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
@@ -119,76 +119,9 @@ public abstract class AppFunctionService extends Service {
* @param callback A callback to report back the result.
*/
@MainThread
- public void onExecuteFunction(
+ public abstract void onExecuteFunction(
@NonNull ExecuteAppFunctionRequest request,
@NonNull String callingPackage,
@NonNull CancellationSignal cancellationSignal,
- @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
- onExecuteFunction(request, cancellationSignal, callback);
- }
-
- /**
- * Called by the system to execute a specific app function.
- *
- * <p>This method is triggered when the system requests your AppFunctionService to handle a
- * particular function you have registered and made available.
- *
- * <p>To ensure proper routing of function requests, assign a unique identifier to each
- * function. This identifier doesn't need to be globally unique, but it must be unique within
- * your app. For example, a function to order food could be identified as "orderFood". In most
- * cases this identifier should come from the ID automatically generated by the AppFunctions
- * SDK. You can determine the specific function to invoke by calling {@link
- * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
- *
- * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
- * thread and dispatch the result with the given callback. You should always report back the
- * result using the callback, no matter if the execution was successful or not.
- *
- * @param request The function execution request.
- * @param cancellationSignal A {@link CancellationSignal} to cancel the request.
- * @param callback A callback to report back the result.
- * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String,
- * CancellationSignal, Consumer)} instead. This method will be removed once usage references
- * are updated.
- */
- @MainThread
- @Deprecated
- public void onExecuteFunction(
- @NonNull ExecuteAppFunctionRequest request,
- @NonNull CancellationSignal cancellationSignal,
- @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
- onExecuteFunction(request, callback);
- }
-
- /**
- * Called by the system to execute a specific app function.
- *
- * <p>This method is triggered when the system requests your AppFunctionService to handle a
- * particular function you have registered and made available.
- *
- * <p>To ensure proper routing of function requests, assign a unique identifier to each
- * function. This identifier doesn't need to be globally unique, but it must be unique within
- * your app. For example, a function to order food could be identified as "orderFood". In most
- * cases this identifier should come from the ID automatically generated by the AppFunctions
- * SDK. You can determine the specific function to invoke by calling {@link
- * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
- *
- * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
- * thread and dispatch the result with the given callback. You should always report back the
- * result using the callback, no matter if the execution was successful or not.
- *
- * @param request The function execution request.
- * @param callback A callback to report back the result.
- * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal,
- * Consumer)} instead. This method will be removed once usage references are updated.
- */
- @MainThread
- @Deprecated
- public void onExecuteFunction(
- @NonNull ExecuteAppFunctionRequest request,
- @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
- Log.w(
- "AppFunctionService",
- "Calling deprecated default implementation of onExecuteFunction");
- }
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback);
}
diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp
index 456338631ae4..70e6beda6cb9 100644
--- a/libs/hwui/jni/text/TextShaper.cpp
+++ b/libs/hwui/jni/text/TextShaper.cpp
@@ -31,12 +31,34 @@
namespace android {
+struct FakedFontKey {
+ uint32_t operator()(const minikin::FakedFont& fakedFont) const {
+ return minikin::Hasher()
+ .update(reinterpret_cast<uintptr_t>(fakedFont.font.get()))
+ .update(fakedFont.fakery.bits())
+ .update(fakedFont.fakery.variationSettings())
+ .hash();
+ }
+};
+
struct LayoutWrapper {
LayoutWrapper(minikin::Layout&& layout, float ascent, float descent)
: layout(std::move(layout)), ascent(ascent), descent(descent) {}
+
+ LayoutWrapper(minikin::Layout&& layout, float ascent, float descent, std::vector<jlong>&& fonts,
+ std::vector<uint32_t>&& fontIds)
+ : layout(std::move(layout))
+ , ascent(ascent)
+ , descent(descent)
+ , fonts(std::move(fonts))
+ , fontIds(std::move(fontIds)) {}
+
minikin::Layout layout;
float ascent;
float descent;
+
+ std::vector<jlong> fonts;
+ std::vector<uint32_t> fontIds; // per glyph
};
static void releaseLayout(jlong ptr) {
@@ -64,6 +86,43 @@ static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int cou
overallDescent = std::max(overallDescent, extent.descent);
}
+ if (text_feature::typeface_redesign()) {
+ uint32_t runCount = layout.getFontRunCount();
+
+ std::unordered_map<minikin::FakedFont, uint32_t, FakedFontKey> fakedToFontIds;
+ std::vector<jlong> fonts;
+ std::vector<uint32_t> fontIds;
+
+ fontIds.resize(layout.nGlyphs());
+ for (uint32_t ri = 0; ri < runCount; ++ri) {
+ const minikin::FakedFont& fakedFont = layout.getFontRunFont(ri);
+
+ auto it = fakedToFontIds.find(fakedFont);
+ uint32_t fontId;
+ if (it != fakedToFontIds.end()) {
+ fontId = it->second; // We've seen it.
+ } else {
+ fontId = fonts.size(); // This is new to us. Create new one.
+ std::shared_ptr<minikin::Font> font = std::make_shared<minikin::Font>(
+ fakedFont.font, fakedFont.fakery.variationSettings());
+ fonts.push_back(reinterpret_cast<jlong>(new FontWrapper(std::move(font))));
+ fakedToFontIds.insert(std::make_pair(fakedFont, fontId));
+ }
+
+ const uint32_t runStart = layout.getFontRunStart(ri);
+ const uint32_t runEnd = layout.getFontRunEnd(ri);
+ for (uint32_t i = runStart; i < runEnd; ++i) {
+ fontIds[i] = fontId;
+ }
+ }
+
+ std::unique_ptr<LayoutWrapper> ptr =
+ std::make_unique<LayoutWrapper>(std::move(layout), overallAscent, overallDescent,
+ std::move(fonts), std::move(fontIds));
+
+ return reinterpret_cast<jlong>(ptr.release());
+ }
+
std::unique_ptr<LayoutWrapper> ptr = std::make_unique<LayoutWrapper>(
std::move(layout), overallAscent, overallDescent
);
@@ -156,6 +215,8 @@ static jboolean TextShaper_Result_getFakeItalic(CRITICAL_JNI_PARAMS_COMMA jlong
return layout->layout.getFakery(i).isFakeItalic();
}
+constexpr float NO_OVERRIDE = -1;
+
float findValueFromVariationSettings(const minikin::FontFakery& fakery, minikin::AxisTag tag) {
for (const minikin::FontVariation& fv : fakery.variationSettings()) {
if (fv.axisTag == tag) {
@@ -171,12 +232,7 @@ static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlon
if (text_feature::typeface_redesign()) {
float value =
findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_wght);
- if (!std::isnan(value)) {
- return value;
- } else {
- const std::shared_ptr<minikin::Font>& font = layout->layout.getFontRef(i);
- return font->style().weight();
- }
+ return std::isnan(value) ? NO_OVERRIDE : value;
} else {
return layout->layout.getFakery(i).wghtAdjustment();
}
@@ -188,12 +244,7 @@ static jfloat TextShaper_Result_getItalicOverride(CRITICAL_JNI_PARAMS_COMMA jlon
if (text_feature::typeface_redesign()) {
float value =
findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_ital);
- if (!std::isnan(value)) {
- return value;
- } else {
- const std::shared_ptr<minikin::Font>& font = layout->layout.getFontRef(i);
- return font->style().isItalic();
- }
+ return std::isnan(value) ? NO_OVERRIDE : value;
} else {
return layout->layout.getFakery(i).italAdjustment();
}
@@ -207,6 +258,24 @@ static jlong TextShaper_Result_getFont(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint
}
// CriticalNative
+static jint TextShaper_Result_getFontCount(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return layout->fonts.size();
+}
+
+// CriticalNative
+static jlong TextShaper_Result_getFontRef(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint fontId) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return layout->fonts[fontId];
+}
+
+// CriticalNative
+static jint TextShaper_Result_getFontId(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint glyphIdx) {
+ const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+ return layout->fontIds[glyphIdx];
+}
+
+// CriticalNative
static jlong TextShaper_Result_nReleaseFunc(CRITICAL_JNI_PARAMS) {
return reinterpret_cast<jlong>(releaseLayout);
}
@@ -250,6 +319,10 @@ static const JNINativeMethod gResultMethods[] = {
{"nGetWeightOverride", "(JI)F", (void*)TextShaper_Result_getWeightOverride},
{"nGetItalicOverride", "(JI)F", (void*)TextShaper_Result_getItalicOverride},
{"nReleaseFunc", "()J", (void*)TextShaper_Result_nReleaseFunc},
+
+ {"nGetFontCount", "(J)I", (void*)TextShaper_Result_getFontCount},
+ {"nGetFontRef", "(JI)J", (void*)TextShaper_Result_getFontRef},
+ {"nGetFontId", "(JI)I", (void*)TextShaper_Result_getFontId},
};
int register_android_graphics_text_TextShaper(JNIEnv* env) {
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml
index 33519cba2940..dd7eac776583 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml
@@ -26,12 +26,12 @@
android:outlineAmbientShadowColor="@android:color/transparent"
android:outlineSpotShadowColor="@android:color/transparent"
android:background="@android:color/transparent"
- android:theme="@style/Theme.CollapsingToolbar.Settings">
+ android:theme="@style/ThemeOverlay.MaterialComponents.PlatformBridge.CollapsingToolbar">
<Toolbar
android:id="@+id/action_bar"
android:layout_width="match_parent"
- android:layout_height="?attr/actionBarSize"
+ android:layout_height="?android:attr/actionBarSize"
android:theme="?android:attr/actionBarTheme"
android:transitionName="shared_element_view"
app:layout_collapseMode="pin"/>
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml
index c20beaf9bf93..02f171cf0d9e 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml
@@ -21,4 +21,15 @@
<item name="colorPrimary">@color/settingslib_primary_dark_device_default_settings</item>
<item name="colorAccent">@color/settingslib_accent_device_default_dark</item>
</style>
-</resources> \ No newline at end of file
+
+ <!--
+ ~ TODO(b/349675008): Remove this theme overlay once the platform bridge theme properly sets
+ ~ the MaterialComponents colors based on the platform theme.
+ -->
+ <style name="ThemeOverlay.MaterialComponents.PlatformBridge.CollapsingToolbar">
+ <item name="elevationOverlayEnabled">true</item>
+ <item name="elevationOverlayColor">?attr/colorPrimary</item>
+ <item name="colorPrimary">@color/settingslib_primary_dark_device_default_settings</item>
+ <item name="colorAccent">@color/settingslib_accent_device_default_dark</item>
+ </style>
+</resources>
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml
index 9ecc297c6d36..403931764d7e 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml
@@ -21,4 +21,15 @@
<item name="colorPrimary">@color/settingslib_primary_device_default_settings_light</item>
<item name="colorAccent">@color/settingslib_accent_device_default_light</item>
</style>
-</resources> \ No newline at end of file
+
+ <!--
+ ~ TODO(b/349675008): Remove this theme overlay once the platform bridge theme properly sets
+ ~ the MaterialComponents colors based on the platform theme.
+ -->
+ <style name="ThemeOverlay.MaterialComponents.PlatformBridge.CollapsingToolbar">
+ <item name="elevationOverlayEnabled">true</item>
+ <item name="elevationOverlayColor">?attr/colorPrimary</item>
+ <item name="colorPrimary">@color/settingslib_primary_device_default_settings_light</item>
+ <item name="colorAccent">@color/settingslib_accent_device_default_light</item>
+ </style>
+</resources>
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes_bridge.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes_bridge.xml
new file mode 100644
index 000000000000..bcb9baf94706
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes_bridge.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- See appcompat/appcompat/THEMES for the theme structure. -->
+<resources>
+ <!--
+ ~ Bridge theme overlay to simulate AppCompat themes based on a platform theme.
+ ~ Only non-widget attributes are included here since we should still use the platform widgets.
+ ~ Only public theme attributes (as in platform public-final.xml) can be referenced here since
+ ~ this is used in modules.
+ -->
+ <style name="Base.V31.ThemeOverlay.AppCompat.PlatformBridge" parent="">
+ <!-- START Base.V7.Theme.AppCompat -->
+
+ <item name="colorBackgroundFloating">?android:colorBackgroundFloating</item>
+
+ <item name="isLightTheme">?android:isLightTheme</item>
+
+ <item name="selectableItemBackground">?android:selectableItemBackground</item>
+ <item name="selectableItemBackgroundBorderless">?android:selectableItemBackgroundBorderless</item>
+ <item name="homeAsUpIndicator">?android:homeAsUpIndicator</item>
+
+ <item name="dividerVertical">?android:dividerVertical</item>
+ <item name="dividerHorizontal">?android:dividerHorizontal</item>
+
+ <!-- List attributes -->
+ <item name="textAppearanceListItem">?android:textAppearanceListItem</item>
+ <item name="textAppearanceListItemSmall">?android:textAppearanceListItemSmall</item>
+ <item name="textAppearanceListItemSecondary">?android:textAppearanceListItemSecondary</item>
+ <item name="listPreferredItemHeight">?android:listPreferredItemHeight</item>
+ <item name="listPreferredItemHeightSmall">?android:listPreferredItemHeightSmall</item>
+ <item name="listPreferredItemHeightLarge">?android:listPreferredItemHeightLarge</item>
+ <item name="listPreferredItemPaddingLeft">?android:listPreferredItemPaddingLeft</item>
+ <item name="listPreferredItemPaddingRight">?android:listPreferredItemPaddingRight</item>
+ <item name="listPreferredItemPaddingStart">?android:listPreferredItemPaddingStart</item>
+ <item name="listPreferredItemPaddingEnd">?android:listPreferredItemPaddingEnd</item>
+
+ <!-- Color palette -->
+ <item name="colorPrimaryDark">?android:colorPrimaryDark</item>
+ <item name="colorPrimary">?android:colorPrimary</item>
+ <item name="colorAccent">?android:colorAccent</item>
+
+ <item name="colorControlNormal">?android:colorControlNormal</item>
+ <item name="colorControlActivated">?android:colorControlActivated</item>
+ <item name="colorControlHighlight">?android:colorControlHighlight</item>
+ <item name="colorButtonNormal">?android:colorButtonNormal</item>
+
+ <item name="colorError">?android:colorError</item>
+
+ <!-- END Base.V7.Theme.AppCompat -->
+ </style>
+ <style name="Base.ThemeOverlay.AppCompat.PlatformBridge" parent="Base.V31.ThemeOverlay.AppCompat.PlatformBridge" />
+ <style name="ThemeOverlay.AppCompat.PlatformBridge" parent="Base.ThemeOverlay.AppCompat.PlatformBridge" />
+
+ <!--
+ ~ Bridge theme overlay to simulate MaterialComponents themes based on a platform theme.
+ -->
+ <style name="Base.V31.ThemeOverlay.MaterialComponents.PlatformBridge" parent="ThemeOverlay.AppCompat.PlatformBridge">
+ <!-- START Base.V14.Theme.MaterialComponents.Bridge -->
+ <!--
+ ~ This is copied as-is from the original bridge theme since it is guaranteed to not affect
+ ~ existing widgets.
+ -->
+
+ <item name="isMaterialTheme">true</item>
+
+ <item name="colorPrimaryVariant">@color/design_dark_default_color_primary_variant</item>
+ <item name="colorSecondary">@color/design_dark_default_color_secondary</item>
+ <item name="colorSecondaryVariant">@color/design_dark_default_color_secondary_variant</item>
+ <item name="colorSurface">@color/design_dark_default_color_surface</item>
+ <item name="colorPrimarySurface">?attr/colorSurface</item>
+ <item name="colorOnPrimary">@color/design_dark_default_color_on_primary</item>
+ <item name="colorOnSecondary">@color/design_dark_default_color_on_secondary</item>
+ <item name="colorOnBackground">@color/design_dark_default_color_on_background</item>
+ <item name="colorOnError">@color/design_dark_default_color_on_error</item>
+ <item name="colorOnSurface">@color/design_dark_default_color_on_surface</item>
+ <item name="colorOnPrimarySurface">?attr/colorOnSurface</item>
+
+ <item name="scrimBackground">@color/mtrl_scrim_color</item>
+ <item name="popupMenuBackground">@drawable/mtrl_popupmenu_background_overlay</item>
+
+ <item name="minTouchTargetSize">@dimen/mtrl_min_touch_target_size</item>
+
+ <!-- MaterialComponents Widget styles -->
+ <item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item>
+ <item name="bottomAppBarStyle">@style/Widget.MaterialComponents.BottomAppBar</item>
+ <item name="chipStyle">@style/Widget.MaterialComponents.Chip.Action</item>
+ <item name="chipGroupStyle">@style/Widget.MaterialComponents.ChipGroup</item>
+ <item name="chipStandaloneStyle">@style/Widget.MaterialComponents.Chip.Entry</item>
+ <item name="circularProgressIndicatorStyle">@style/Widget.MaterialComponents.CircularProgressIndicator</item>
+ <item name="extendedFloatingActionButtonStyle">@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon</item>
+ <item name="linearProgressIndicatorStyle">@style/Widget.MaterialComponents.LinearProgressIndicator</item>
+ <item name="materialButtonStyle">@style/Widget.MaterialComponents.Button</item>
+ <item name="materialButtonOutlinedStyle">@style/Widget.MaterialComponents.Button.OutlinedButton</item>
+ <item name="materialButtonToggleGroupStyle">@style/Widget.MaterialComponents.MaterialButtonToggleGroup</item>
+ <item name="materialCardViewStyle">@style/Widget.MaterialComponents.CardView</item>
+ <item name="navigationRailStyle">@style/Widget.MaterialComponents.NavigationRailView</item>
+ <item name="sliderStyle">@style/Widget.MaterialComponents.Slider</item>
+
+ <!-- Type styles -->
+ <item name="textAppearanceHeadline1">@style/TextAppearance.MaterialComponents.Headline1</item>
+ <item name="textAppearanceHeadline2">@style/TextAppearance.MaterialComponents.Headline2</item>
+ <item name="textAppearanceHeadline3">@style/TextAppearance.MaterialComponents.Headline3</item>
+ <item name="textAppearanceHeadline4">@style/TextAppearance.MaterialComponents.Headline4</item>
+ <item name="textAppearanceHeadline5">@style/TextAppearance.MaterialComponents.Headline5</item>
+ <item name="textAppearanceHeadline6">@style/TextAppearance.MaterialComponents.Headline6</item>
+ <item name="textAppearanceSubtitle1">@style/TextAppearance.MaterialComponents.Subtitle1</item>
+ <item name="textAppearanceSubtitle2">@style/TextAppearance.MaterialComponents.Subtitle2</item>
+ <item name="textAppearanceBody1">@style/TextAppearance.MaterialComponents.Body1</item>
+ <item name="textAppearanceBody2">@style/TextAppearance.MaterialComponents.Body2</item>
+ <item name="textAppearanceCaption">@style/TextAppearance.MaterialComponents.Caption</item>
+ <item name="textAppearanceButton">@style/TextAppearance.MaterialComponents.Button</item>
+ <item name="textAppearanceOverline">@style/TextAppearance.MaterialComponents.Overline</item>
+
+ <!-- Shape styles -->
+ <item name="shapeAppearanceSmallComponent">
+ @style/ShapeAppearance.MaterialComponents.SmallComponent
+ </item>
+ <item name="shapeAppearanceMediumComponent">
+ @style/ShapeAppearance.MaterialComponents.MediumComponent
+ </item>
+ <item name="shapeAppearanceLargeComponent">
+ @style/ShapeAppearance.MaterialComponents.LargeComponent
+ </item>
+
+ <!-- Motion -->
+ <item name="motionEasingStandard">@string/material_motion_easing_standard</item>
+ <item name="motionEasingEmphasized">@string/material_motion_easing_emphasized</item>
+ <item name="motionEasingDecelerated">@string/material_motion_easing_decelerated</item>
+ <item name="motionEasingAccelerated">@string/material_motion_easing_accelerated</item>
+ <item name="motionEasingLinear">@string/material_motion_easing_linear</item>
+
+ <item name="motionDurationShort1">@integer/material_motion_duration_short_1</item>
+ <item name="motionDurationShort2">@integer/material_motion_duration_short_2</item>
+ <item name="motionDurationMedium1">@integer/material_motion_duration_medium_1</item>
+ <item name="motionDurationMedium2">@integer/material_motion_duration_medium_2</item>
+ <item name="motionDurationLong1">@integer/material_motion_duration_long_1</item>
+ <item name="motionDurationLong2">@integer/material_motion_duration_long_2</item>
+
+ <item name="motionPath">@integer/material_motion_path</item>
+
+ <!-- Elevation Overlays -->
+ <item name="elevationOverlayEnabled">true</item>
+ <item name="elevationOverlayColor">?attr/colorOnSurface</item>
+
+ <!-- END Base.V14.Theme.MaterialComponents.Bridge -->
+
+ <!-- START Base.V14.Theme.MaterialComponents -->
+ <!--
+ ~ Only a subset of widget attributes being actually used are included here since there are
+ ~ too many of them and they need to be investigated on a case-by-case basis.
+ -->
+
+ <!-- Framework, AppCompat, or Design Widget styles -->
+ <item name="appBarLayoutStyle">@style/Widget.MaterialComponents.AppBarLayout.Surface</item>
+
+ <!-- END Base.V14.Theme.MaterialComponents -->
+ </style>
+ <style name="Base.ThemeOverlay.MaterialComponents.PlatformBridge" parent="Base.V31.ThemeOverlay.AppCompat.PlatformBridge" />
+ <style name="ThemeOverlay.MaterialComponents.PlatformBridge" parent="Base.ThemeOverlay.AppCompat.PlatformBridge" />
+</resources>
diff --git a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
index 1412c84c137b..89881f4d74bb 100644
--- a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
+++ b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
@@ -37,7 +37,7 @@ import org.junit.runner.RunWith
abstract class CatalystScreenTestCase {
@get:Rule val setFlagsRule = SetFlagsRule()
- protected val context: Context = ApplicationProvider.getApplicationContext()
+ protected val appContext: Context = ApplicationProvider.getApplicationContext()
/** Catalyst screen. */
protected abstract val preferenceScreenCreator: PreferenceScreenCreator
@@ -52,12 +52,12 @@ abstract class CatalystScreenTestCase {
@Test
open fun migration() {
enableCatalystScreen()
- assertThat(preferenceScreenCreator.isFlagEnabled(context)).isTrue()
+ assertThat(preferenceScreenCreator.isFlagEnabled(appContext)).isTrue()
val catalystScreen = dumpPreferenceScreen()
Log.i(TAG, catalystScreen)
disableCatalystScreen()
- assertThat(preferenceScreenCreator.isFlagEnabled(context)).isFalse()
+ assertThat(preferenceScreenCreator.isFlagEnabled(appContext)).isFalse()
val legacyScreen = dumpPreferenceScreen()
assertThat(catalystScreen).isEqualTo(legacyScreen)
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index dfd296fe006f..8636524ed23c 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -23,6 +23,7 @@ import com.android.settingslib.spa.framework.common.SpaEnvironment
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
import com.android.settingslib.spa.gallery.banner.BannerPageProvider
+import com.android.settingslib.spa.gallery.card.CardPageProvider
import com.android.settingslib.spa.gallery.chart.ChartPageProvider
import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider
import com.android.settingslib.spa.gallery.dialog.NavDialogProvider
@@ -109,6 +110,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) {
TopIntroPreferencePageProvider,
CheckBoxPreferencePageProvider,
TwoTargetButtonPreferencePageProvider,
+ CardPageProvider,
),
rootPages = listOf(
HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
new file mode 100644
index 000000000000..5659e2f33156
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.card
+
+import android.os.Bundle
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Stars
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.card.SuggestionCard
+import com.android.settingslib.spa.widget.card.SuggestionCardModel
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+
+object CardPageProvider : SettingsPageProvider {
+ override val name = "Card"
+
+ override fun getTitle(arguments: Bundle?) = TITLE
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ RegularScaffold(title = TITLE) {
+ SuggestionCard()
+ SuggestionCardWithLongTitle()
+ SuggestionCardDismissible()
+ }
+ }
+
+ @Composable
+ private fun SuggestionCard() {
+ SuggestionCard(
+ SuggestionCardModel(
+ title = "Suggestion card",
+ description = "Suggestion card description",
+ imageVector = Icons.Filled.Stars,
+ )
+ )
+ }
+
+ @Composable
+ private fun SuggestionCardWithLongTitle() {
+ SuggestionCard(
+ SuggestionCardModel(
+ title = "Top level suggestion card with a really, really long title",
+ imageVector = Icons.Filled.Stars,
+ onClick = {},
+ )
+ )
+ }
+
+ @Composable
+ private fun SuggestionCardDismissible() {
+ var isVisible by rememberSaveable { mutableStateOf(true) }
+ SuggestionCard(
+ SuggestionCardModel(
+ title = "Suggestion card",
+ description = "Suggestion card description",
+ imageVector = Icons.Filled.Stars,
+ onDismiss = { isVisible = false },
+ isVisible = isVisible,
+ )
+ )
+ }
+
+ @Composable
+ fun Entry() {
+ Preference(
+ object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ }
+ )
+ }
+
+ private const val TITLE = "Sample Card"
+}
+
+@Preview
+@Composable
+private fun CardPagePreview() {
+ SettingsTheme { CardPageProvider.Page(null) }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
index 4d77ea173a85..ebfc0c536868 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
@@ -27,6 +27,7 @@ import com.android.settingslib.spa.gallery.R
import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
import com.android.settingslib.spa.gallery.banner.BannerPageProvider
import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
+import com.android.settingslib.spa.gallery.card.CardPageProvider
import com.android.settingslib.spa.gallery.chart.ChartPageProvider
import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider
import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
@@ -80,6 +81,7 @@ object HomePageProvider : SettingsPageProvider {
DialogMainPageProvider.Entry()
EditorMainPageProvider.Entry()
BannerPageProvider.Entry()
+ CardPageProvider.Entry()
CopyablePageProvider.Entry()
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
index 395748384b85..08bedf99519d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
@@ -28,6 +28,7 @@ object SettingsDimension {
val paddingExtraSmall6 = 12.dp
val paddingLarge = 16.dp
val paddingExtraLarge = 24.dp
+ val paddingExtraLarge1 = 28.dp
val spinnerHorizontalPadding = paddingExtraLarge
val spinnerVerticalPadding = paddingLarge
@@ -37,6 +38,7 @@ object SettingsDimension {
val actionIconPadding = 4.dp
val itemIconSize = 24.dp
+ val itemIconContainerSizeSmall = 40.dp
val itemIconContainerSize = 72.dp
val itemPaddingStart = if (isSpaExpressiveEnabled) paddingLarge else paddingExtraLarge
val itemPaddingEnd = paddingLarge
@@ -47,6 +49,12 @@ object SettingsDimension {
end = itemPaddingEnd,
bottom = itemPaddingVertical,
)
+ val footerItemPadding = PaddingValues(
+ start = paddingExtraLarge1,
+ top = itemPaddingVertical,
+ end = itemPaddingEnd,
+ bottom = itemPaddingVertical,
+ )
val textFieldPadding = PaddingValues(
start = itemPaddingStart,
end = itemPaddingEnd,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
index 86ba6864574c..61607bc8ae8a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
@@ -29,4 +29,6 @@ object SettingsShape {
val CornerLarge = RoundedCornerShape(24.dp)
val CornerExtraLarge = RoundedCornerShape(28.dp)
+
+ val CornerExtraLarge1 = RoundedCornerShape(40.dp)
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SuggestionCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SuggestionCard.kt
new file mode 100644
index 000000000000..2126634ebd4d
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SuggestionCard.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.card
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Stars
+import androidx.compose.material.icons.outlined.Stars
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsShape
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.framework.theme.toSemiBoldWeight
+
+data class SuggestionCardModel(
+ val title: String,
+ val description: String? = null,
+ val imageVector: ImageVector,
+
+ /**
+ * A dismiss button will be displayed if this is not null.
+ *
+ * And this callback will be called when user clicks the button.
+ */
+ val onDismiss: (() -> Unit)? = null,
+ val isVisible: Boolean = true,
+ val onClick: (() -> Unit)? = null,
+)
+
+@Composable
+fun SuggestionCard(model: SuggestionCardModel) {
+ AnimatedVisibility(visible = model.isVisible) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ Modifier.padding(
+ horizontal = SettingsDimension.paddingLarge,
+ vertical = SettingsDimension.paddingSmall,
+ )
+ .fillMaxWidth()
+ .heightIn(min = SettingsDimension.preferenceMinHeight)
+ .clip(SettingsShape.CornerExtraLarge1)
+ .background(MaterialTheme.colorScheme.secondaryContainer)
+ .then(model.onClick?.let { Modifier.clickable(onClick = it) } ?: Modifier)
+ .padding(SettingsDimension.paddingExtraSmall6),
+ ) {
+ SuggestionCardIcon(model.imageVector)
+ Spacer(Modifier.padding(SettingsDimension.paddingSmall))
+ Column(modifier = Modifier.weight(1f).semantics(mergeDescendants = true) {}) {
+ SuggestionCardTitle(model.title)
+ if (model.description != null) SuggestionCardDescription(model.description)
+ }
+ if (model.onDismiss != null) {
+ Spacer(Modifier.padding(SettingsDimension.paddingSmall))
+ SuggestionCardDismissButton(model.onDismiss)
+ }
+ }
+ }
+}
+
+@Composable
+private fun SuggestionCardIcon(imageVector: ImageVector) {
+ Box(
+ modifier =
+ Modifier.padding(SettingsDimension.paddingSmall)
+ .size(SettingsDimension.itemIconContainerSizeSmall)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.secondary),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ imageVector = imageVector,
+ contentDescription = null,
+ modifier = Modifier.size(SettingsDimension.itemIconSize),
+ tint = MaterialTheme.colorScheme.onSecondary,
+ )
+ }
+}
+
+@Composable
+private fun SuggestionCardTitle(title: String) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium.toSemiBoldWeight(),
+ modifier = Modifier.padding(vertical = SettingsDimension.paddingTiny),
+ color = MaterialTheme.colorScheme.onSecondaryContainer,
+ )
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun SuggestionCardDescription(description: String) {
+ Text(
+ text = description,
+ style = MaterialTheme.typography.bodySmallEmphasized,
+ modifier = Modifier.padding(vertical = SettingsDimension.paddingTiny),
+ color = MaterialTheme.colorScheme.onSecondaryContainer,
+ )
+}
+
+@Composable
+private fun SuggestionCardDismissButton(onDismiss: () -> Unit) {
+ IconButton(shape = CircleShape, onClick = onDismiss) {
+ Icon(
+ imageVector = Icons.Filled.Close,
+ contentDescription =
+ stringResource(androidx.compose.material3.R.string.m3c_snackbar_dismiss),
+ modifier = Modifier.size(SettingsDimension.itemIconSize),
+ tint = MaterialTheme.colorScheme.onSecondaryContainer,
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun SuggestionCardPreview() {
+ SettingsTheme {
+ SuggestionCard(
+ SuggestionCardModel(
+ title = "Suggestion card",
+ description = "Suggestion card description",
+ imageVector = Icons.Outlined.Stars,
+ onDismiss = {},
+ onClick = {},
+ )
+ )
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
index acbdec0b30aa..66680fa547b1 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
@@ -49,7 +49,9 @@ fun CategoryTitle(title: String) {
text = title,
modifier =
Modifier.padding(
- start = SettingsDimension.itemPaddingStart,
+ start =
+ if (isSpaExpressiveEnabled) SettingsDimension.paddingSmall
+ else SettingsDimension.itemPaddingStart,
top = 20.dp,
end =
if (isSpaExpressiveEnabled) SettingsDimension.paddingSmall
@@ -67,16 +69,16 @@ fun CategoryTitle(title: String) {
*/
@Composable
fun Category(title: String? = null, content: @Composable ColumnScope.() -> Unit) {
+ var displayTitle by remember { mutableStateOf(false) }
Column(
modifier =
- if (isSpaExpressiveEnabled)
+ if (isSpaExpressiveEnabled && displayTitle)
Modifier.padding(
horizontal = SettingsDimension.paddingLarge,
vertical = SettingsDimension.paddingSmall,
)
else Modifier
) {
- var displayTitle by remember { mutableStateOf(false) }
if (title != null && displayTitle) CategoryTitle(title = title)
Column(
modifier =
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsBannerTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/banner/SettingsBannerTest.kt
index a8479b01a861..a8479b01a861 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsBannerTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/banner/SettingsBannerTest.kt
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleBannerTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/banner/SettingsCollapsibleBannerTest.kt
index 1080fdea9455..1080fdea9455 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleBannerTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/banner/SettingsCollapsibleBannerTest.kt
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SuggestionCardTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SuggestionCardTest.kt
new file mode 100644
index 000000000000..96bfb3d71642
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SuggestionCardTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.card
+
+import android.content.Context
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Star
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.isNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SuggestionCardTest {
+ @get:Rule val composeTestRule = createComposeRule()
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun suggestionCard_contentDisplayed() {
+ setContent()
+
+ composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
+ composeTestRule.onNodeWithText(DESCRIPTION).assertIsDisplayed()
+ }
+
+ @Test
+ fun suggestionCard_dismiss() {
+ setContent()
+ composeTestRule
+ .onNodeWithContentDescription(
+ context.getString(androidx.compose.material3.R.string.m3c_snackbar_dismiss)
+ )
+ .performClick()
+
+ composeTestRule.onNodeWithText(TITLE).isNotDisplayed()
+ composeTestRule.onNodeWithText(DESCRIPTION).isNotDisplayed()
+ }
+
+ private fun setContent() {
+ composeTestRule.setContent {
+ var isVisible by rememberSaveable { mutableStateOf(true) }
+ SuggestionCard(
+ SuggestionCardModel(
+ title = TITLE,
+ description = DESCRIPTION,
+ imageVector = Icons.Outlined.Star,
+ isVisible = isVisible,
+ onDismiss = { isVisible = false },
+ )
+ )
+ }
+ }
+
+ private companion object {
+ const val TITLE = "Title"
+ const val DESCRIPTION = "Description"
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
index f306918ec72f..d89d3977cac3 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
@@ -39,6 +39,8 @@ import androidx.compose.ui.unit.Dp
import com.android.settingslib.development.DevelopmentSettingsEnabler
import com.android.settingslib.spa.framework.compose.rememberDrawablePainter
import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
+import com.android.settingslib.spa.widget.preference.IntroAppPreference
import com.android.settingslib.spa.widget.ui.CopyableBody
import com.android.settingslib.spa.widget.ui.SettingsBody
import com.android.settingslib.spa.widget.ui.SettingsTitle
@@ -48,23 +50,53 @@ import com.android.settingslib.spaprivileged.model.app.rememberAppRepository
class AppInfoProvider(private val packageInfo: PackageInfo) {
@Composable
fun AppInfo(displayVersion: Boolean = false, isClonedAppPage: Boolean = false) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(
- horizontal = SettingsDimension.itemPaddingStart,
- vertical = SettingsDimension.itemPaddingVertical,
- )
- .semantics(mergeDescendants = true) {},
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
+ if (isSpaExpressiveEnabled) {
+ val appRepository = rememberAppRepository()
val app = checkNotNull(packageInfo.applicationInfo)
- Box(modifier = Modifier.padding(SettingsDimension.itemPaddingAround)) {
- AppIcon(app = app, size = SettingsDimension.appIconInfoSize)
+ val title = appRepository.produceLabel(app, isClonedAppPage).value
+
+ val descriptions = mutableListOf<String>()
+ if (app.isInstantApp) {
+ descriptions.add(
+ stringResource(
+ com.android.settingslib.widget.preference.app.R.string.install_type_instant
+ )
+ )
+ }
+ if (displayVersion) {
+ val versionName = packageInfo.versionNameBidiWrapped
+ if (versionName != null) descriptions.add(versionName)
+ }
+
+ IntroAppPreference(
+ title = title,
+ descriptions = descriptions,
+ appIcon = {
+ Image(
+ painter = rememberDrawablePainter(appRepository.produceIcon(app).value),
+ contentDescription = appRepository.produceIconContentDescription(app).value,
+ )
+ },
+ )
+ } else {
+ Column(
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(
+ horizontal = SettingsDimension.itemPaddingStart,
+ vertical = SettingsDimension.itemPaddingVertical,
+ )
+ .semantics(mergeDescendants = true) {},
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ val app = checkNotNull(packageInfo.applicationInfo)
+ Box(modifier = Modifier.padding(SettingsDimension.itemPaddingAround)) {
+ AppIcon(app = app, size = SettingsDimension.appIconInfoSize)
+ }
+ AppLabel(app, isClonedAppPage)
+ InstallType(app)
+ if (displayVersion) AppVersion()
}
- AppLabel(app, isClonedAppPage)
- InstallType(app)
- if (displayVersion) AppVersion()
}
}
@@ -89,19 +121,24 @@ class AppInfoProvider(private val packageInfo: PackageInfo) {
@Composable
fun FooterAppVersion(showPackageName: Boolean = rememberIsDevelopmentSettingsEnabled()) {
val context = LocalContext.current
- val footer = remember(packageInfo, showPackageName) {
- val list = mutableListOf<String>()
- packageInfo.versionNameBidiWrapped?.let {
- list += context.getString(R.string.version_text, it)
+ val footer =
+ remember(packageInfo, showPackageName) {
+ val list = mutableListOf<String>()
+ packageInfo.versionNameBidiWrapped?.let {
+ list += context.getString(R.string.version_text, it)
+ }
+ if (showPackageName) {
+ list += packageInfo.packageName
+ }
+ list.joinToString(separator = System.lineSeparator())
}
- if (showPackageName) {
- list += packageInfo.packageName
- }
- list.joinToString(separator = System.lineSeparator())
- }
if (footer.isBlank()) return
HorizontalDivider()
- Column(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
+ Column(
+ modifier =
+ if (isSpaExpressiveEnabled) Modifier.padding(SettingsDimension.footerItemPadding)
+ else Modifier.padding(SettingsDimension.itemPadding)
+ ) {
CopyableBody(footer)
}
}
@@ -109,9 +146,7 @@ class AppInfoProvider(private val packageInfo: PackageInfo) {
@Composable
private fun rememberIsDevelopmentSettingsEnabled(): Boolean {
val context = LocalContext.current
- return remember {
- DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context)
- }
+ return remember { DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context) }
}
private companion object {
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index abc58ee99904..fd2a1cb14edd 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -819,6 +819,11 @@
<!-- Summary of checkbox setting that enables the terminal app. [CHAR LIMIT=64] -->
<string name="enable_terminal_summary">Enable terminal app that offers local shell access</string>
+ <!-- Title of checkbox setting that enables the Linux terminal app. [CHAR LIMIT=32] -->
+ <string name="enable_linux_terminal_title">Linux development environment</string>
+ <!-- Summary of checkbox setting that enables the Linux terminal app. [CHAR LIMIT=64] -->
+ <string name="enable_linux_terminal_summary">Run Linux terminal on Android</string>
+
<!-- HDCP checking title, used for debug purposes only. [CHAR LIMIT=25] -->
<string name="hdcp_checking_title">HDCP checking</string>
<!-- HDCP checking dialog title, used for debug purposes only. [CHAR LIMIT=25] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
index 6335e712f904..83ee9751329f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
@@ -125,7 +125,9 @@ public class InputMediaDevice extends MediaDevice {
? mProductName
: mContext.getString(R.string.media_transfer_usb_device_mic_name);
case TYPE_BLUETOOTH_SCO ->
- mContext.getString(R.string.media_transfer_bt_device_mic_name);
+ mProductName != null
+ ? mProductName
+ : mContext.getString(R.string.media_transfer_bt_device_mic_name);
default -> mContext.getString(R.string.media_transfer_this_device_name_desktop);
};
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
index 6c1cb7015225..7775b912e51d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
@@ -44,6 +44,7 @@ public class InputMediaDeviceTest {
private static final String PRODUCT_NAME_BUILTIN_MIC = "Built-in Mic";
private static final String PRODUCT_NAME_WIRED_HEADSET = "My Wired Headset";
private static final String PRODUCT_NAME_USB_HEADSET = "My USB Headset";
+ private static final String PRODUCT_NAME_BT_HEADSET = "My Bluetooth Headset";
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -142,6 +143,21 @@ public class InputMediaDeviceTest {
MAX_VOLUME,
CURRENT_VOLUME,
IS_VOLUME_FIXED,
+ PRODUCT_NAME_BT_HEADSET);
+ assertThat(btMediaDevice).isNotNull();
+ assertThat(btMediaDevice.getName()).isEqualTo(PRODUCT_NAME_BT_HEADSET);
+ }
+
+ @Test
+ public void getName_returnCorrectName_btHeadset_nullProductName() {
+ InputMediaDevice btMediaDevice =
+ InputMediaDevice.create(
+ mContext,
+ String.valueOf(BT_HEADSET_ID),
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+ MAX_VOLUME,
+ CURRENT_VOLUME,
+ IS_VOLUME_FIXED,
null);
assertThat(btMediaDevice).isNotNull();
assertThat(btMediaDevice.getName())
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 408ed1e861c3..b385aaa5c7d9 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -924,6 +924,7 @@
<!-- Permission required for CTS test - CtsPackageManagerTestCases-->
<uses-permission android:name="android.permission.DOMAIN_VERIFICATION_AGENT" />
+ <uses-permission android:name="android.permission.VERIFICATION_AGENT" />
<!-- Permission required for Cts test - CtsInputTestCases -->
<uses-permission
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 540115de8830..651244a9e52a 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1497,3 +1497,13 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "secondary_user_widget_host"
+ namespace: "systemui"
+ description: "Host communal widgets in the current secondary user on HSUM."
+ bug: "373874416"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 9dc93484a638..4cf264253bf8 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -486,8 +486,8 @@ class TransitionAnimator(
endState: State,
windowBackgroundLayer: GradientDrawable,
fadeWindowBackgroundLayer: Boolean = true,
- useSpring: Boolean = false,
drawHole: Boolean = false,
+ useSpring: Boolean = false,
): Animation {
val transitionContainer = controller.transitionContainer
val transitionContainerOverlay = transitionContainer.overlay
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt
index 9444664885c8..71230f9cde12 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt
@@ -17,8 +17,9 @@
package com.android.systemui.keyguard.ui.composable.modifier
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
@@ -41,15 +42,17 @@ fun Modifier.burnInAware(
params: BurnInParameters,
isClock: Boolean = false,
): Modifier {
- val translationYState = remember { mutableStateOf(0F) }
- viewModel.updateBurnInParams(params.copy(translationY = { translationYState.value }))
+ val cachedYTranslation = remember { mutableFloatStateOf(0f) }
+ LaunchedEffect(Unit) {
+ viewModel.updateBurnInParams(params.copy(translationY = { cachedYTranslation.floatValue }))
+ }
val burnIn = viewModel.movement
val translationX by
burnIn.map { it.translationX.toFloat() }.collectAsStateWithLifecycle(initialValue = 0f)
val translationY by
burnIn.map { it.translationY.toFloat() }.collectAsStateWithLifecycle(initialValue = 0f)
- translationYState.value = translationY
+ cachedYTranslation.floatValue = translationY
val scaleViewModel by
burnIn
.map { BurnInScaleViewModel(scale = it.scale, scaleClockOnly = it.scaleClockOnly) }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
index ca68c256fd73..772872719ebe 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
@@ -22,19 +22,52 @@ interface ElementMatcher {
fun matches(key: ElementKey, content: ContentKey): Boolean
}
+/** Returns an [ElementMatcher] that matches any element in [content]. */
+fun inContent(content: ContentKey): ElementMatcher {
+ val matcherContent = content
+ return object : ElementMatcher {
+ override fun matches(key: ElementKey, content: ContentKey): Boolean {
+ return content == matcherContent
+ }
+ }
+}
+
+/** Returns an [ElementMatcher] that matches all elements not matching [this] matcher. */
+operator fun ElementMatcher.not(): ElementMatcher {
+ val delegate = this
+ return object : ElementMatcher {
+ override fun matches(key: ElementKey, content: ContentKey): Boolean {
+ return !delegate.matches(key, content)
+ }
+ }
+}
+
+/**
+ * Returns an [ElementMatcher] that matches all elements matching both [this] matcher and [other].
+ */
+infix fun ElementMatcher.and(other: ElementMatcher): ElementMatcher {
+ val delegate = this
+ return object : ElementMatcher {
+ override fun matches(key: ElementKey, content: ContentKey): Boolean {
+ return delegate.matches(key, content) && other.matches(key, content)
+ }
+ }
+}
+
/**
- * Returns an [ElementMatcher] that matches elements in [content] also matching [this]
- * [ElementMatcher].
+ * Returns an [ElementMatcher] that matches all elements either [this] matcher, or [other], or both.
*/
-fun ElementMatcher.inContent(content: ContentKey): ElementMatcher {
+infix fun ElementMatcher.or(other: ElementMatcher): ElementMatcher {
val delegate = this
- val matcherScene = content
return object : ElementMatcher {
override fun matches(key: ElementKey, content: ContentKey): Boolean {
- return content == matcherScene && delegate.matches(key, content)
+ return delegate.matches(key, content) || other.matches(key, content)
}
}
}
-@Deprecated("Use inContent() instead", replaceWith = ReplaceWith("inContent(scene)"))
-fun ElementMatcher.inScene(scene: SceneKey) = inContent(scene)
+@Deprecated(
+ "Use `this and inContent()` instead",
+ replaceWith = ReplaceWith("this and inContent(scene)"),
+)
+fun ElementMatcher.inScene(scene: SceneKey) = this and inContent(scene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 061583a64702..a3f2a434cff7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -81,16 +81,16 @@ private class SwipeToSceneRootNode(
draggableHandler: DraggableHandlerImpl,
swipeDetector: SwipeDetector,
) : DelegatingNode() {
- private var delegate = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+ private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) {
- if (draggableHandler == delegate.draggableHandler) {
+ if (draggableHandler == delegateNode.draggableHandler) {
// Simple update, just update the swipe detector directly and keep the node.
- delegate.swipeDetector = swipeDetector
+ delegateNode.swipeDetector = swipeDetector
} else {
// The draggableHandler changed, force recreate the underlying SwipeToSceneNode.
- undelegate(delegate)
- delegate = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+ undelegate(delegateNode)
+ delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
}
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementMatcherTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementMatcherTest.kt
new file mode 100644
index 000000000000..af0962361fb2
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementMatcherTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements.Bar
+import com.android.compose.animation.scene.TestElements.Foo
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.TestScenes.SceneB
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ElementMatcherTest {
+ @Test
+ fun and() {
+ val matcher = Foo and inContent(SceneA)
+ assertThat(matcher.matches(Foo, SceneA)).isTrue()
+ assertThat(matcher.matches(Foo, SceneB)).isFalse()
+ assertThat(matcher.matches(Bar, SceneA)).isFalse()
+ assertThat(matcher.matches(Bar, SceneB)).isFalse()
+ }
+
+ @Test
+ fun or() {
+ val matcher = Foo or inContent(SceneA)
+ assertThat(matcher.matches(Foo, SceneA)).isTrue()
+ assertThat(matcher.matches(Foo, SceneB)).isTrue()
+ assertThat(matcher.matches(Bar, SceneA)).isTrue()
+ assertThat(matcher.matches(Bar, SceneB)).isFalse()
+ }
+
+ @Test
+ fun not() {
+ val matcher = !Foo
+ assertThat(matcher.matches(Foo, SceneA)).isFalse()
+ assertThat(matcher.matches(Foo, SceneB)).isFalse()
+ assertThat(matcher.matches(Bar, SceneA)).isTrue()
+ assertThat(matcher.matches(Bar, SceneB)).isTrue()
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 39d46990dc4b..ee807e6a7ede 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -2221,8 +2221,8 @@ class ElementTest {
// In A => B, Foo is not shared and first fades out from A then fades in
// B.
sharedElement(TestElements.Foo, enabled = false)
- fractionRange(end = 0.5f) { fade(TestElements.Foo.inContent(SceneA)) }
- fractionRange(start = 0.5f) { fade(TestElements.Foo.inContent(SceneB)) }
+ fractionRange(end = 0.5f) { fade(TestElements.Foo.inScene(SceneA)) }
+ fractionRange(start = 0.5f) { fade(TestElements.Foo.inScene(SceneB)) }
}
from(SceneB, to = SceneA) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
index cae6617cb11b..7ea414d6b8cd 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
@@ -35,6 +35,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
@@ -205,7 +206,8 @@ class OverlayTest {
val key = MovableElementKey("MovableBar", contents = setOf(SceneA, OverlayA, OverlayB))
val elementChildTag = "elementChildTag"
- fun elementChild(content: ContentKey) = hasTestTag(elementChildTag) and inContent(content)
+ fun elementChild(content: ContentKey) =
+ hasTestTag(elementChildTag) and SemanticsMatcher.inContent(content)
@Composable
fun ContentScope.MovableBar() {
@@ -773,7 +775,7 @@ class OverlayTest {
// Overscroll on Overlay A.
scope.launch { state.startTransition(transition(SceneA, OverlayA, progress = { 1.5f })) }
rule
- .onNode(hasTestTag(movableElementChildTag) and inContent(SceneA))
+ .onNode(hasTestTag(movableElementChildTag) and SemanticsMatcher.inContent(SceneA))
.assertPositionInRootIsEqualTo(0.dp, 0.dp)
.assertSizeIsEqualTo(100.dp)
.assertIsDisplayed()
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
index 4877cd610875..2e3a934c2701 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
@@ -31,7 +31,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TestElements
import com.android.compose.animation.scene.TestScenes
-import com.android.compose.animation.scene.inContent
+import com.android.compose.animation.scene.inScene
import com.android.compose.animation.scene.testTransition
import com.android.compose.test.assertSizeIsEqualTo
import org.junit.Rule
@@ -125,10 +125,10 @@ class SharedElementTest {
sharedElement(TestElements.Foo, enabled = false)
// In SceneA, Foo leaves to the left edge.
- translate(TestElements.Foo.inContent(TestScenes.SceneA), Edge.Left)
+ translate(TestElements.Foo.inScene(TestScenes.SceneA), Edge.Left)
// In SceneB, Foo comes from the bottom edge.
- translate(TestElements.Foo.inContent(TestScenes.SceneB), Edge.Bottom)
+ translate(TestElements.Foo.inScene(TestScenes.SceneB), Edge.Bottom)
},
) {
before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
index 22450d32ea62..b3201d0daffe 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
@@ -25,11 +25,11 @@ fun isElement(element: ElementKey, content: ContentKey? = null): SemanticsMatche
return if (content == null) {
hasTestTag(element.testTag)
} else {
- hasTestTag(element.testTag) and inContent(content)
+ hasTestTag(element.testTag) and SemanticsMatcher.inContent(content)
}
}
/** A [SemanticsMatcher] that matches anything inside [content]. */
-fun inContent(content: ContentKey): SemanticsMatcher {
+fun SemanticsMatcher.Companion.inContent(content: ContentKey): SemanticsMatcher {
return hasAnyAncestor(hasTestTag(content.testTag))
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index 662815ee7cbe..fcb433b5db4e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -46,7 +46,9 @@ import android.platform.test.annotations.EnableFlags;
import android.provider.Settings;
import android.testing.TestableLooper;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.LinearLayout;
+import android.widget.Space;
import android.widget.Spinner;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -226,7 +228,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
mDialog.show();
LinearLayout relatedToolsView = (LinearLayout) getRelatedToolsView(mDialog);
- assertThat(relatedToolsView.getChildCount()).isEqualTo(1);
+ assertThat(countChildWithoutSpace(relatedToolsView)).isEqualTo(1);
}
@Test
@@ -244,12 +246,13 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
when(mActivityInfo.loadLabel(mPackageManager)).thenReturn(TEST_LABEL);
when(mActivityInfo.loadIcon(mPackageManager)).thenReturn(mDrawable);
when(mActivityInfo.getComponentName()).thenReturn(TEST_COMPONENT);
+ when(mDrawable.mutate()).thenReturn(mDrawable);
setUpPairNewDeviceDialog();
mDialog.show();
LinearLayout relatedToolsView = (LinearLayout) getRelatedToolsView(mDialog);
- assertThat(relatedToolsView.getChildCount()).isEqualTo(2);
+ assertThat(countChildWithoutSpace(relatedToolsView)).isEqualTo(2);
}
@Test
@@ -364,6 +367,16 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
return dialog.requireViewById(R.id.preset_spinner);
}
+ private int countChildWithoutSpace(ViewGroup viewGroup) {
+ int spaceCount = 0;
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ if (viewGroup.getChildAt(i) instanceof Space) {
+ spaceCount++;
+ }
+ }
+ return viewGroup.getChildCount() - spaceCount;
+ }
+
@After
public void reset() {
if (mDialogDelegate != null) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java
index 17ce1ddee87a..77369f7ec881 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java
@@ -85,7 +85,7 @@ public class HearingDevicesToolItemParserTest extends SysuiTestCase {
}
@Test
- public void parseStringArray_noString_emptyResult() {
+ public void parseStringArray_noToolName_emptyResult() {
assertThat(HearingDevicesToolItemParser.parseStringArray(mContext, new String[]{},
new String[]{})).isEqualTo(emptyList());
}
@@ -103,14 +103,14 @@ public class HearingDevicesToolItemParserTest extends SysuiTestCase {
}
@Test
- public void parseStringArray_fourToolName_maxThreeToolItem() {
+ public void parseStringArray_threeToolNames_maxTwoToolItems() {
String componentNameString = TEST_PKG + "/" + TEST_CLS;
- String[] fourToolName =
- new String[]{componentNameString, componentNameString, componentNameString,
- componentNameString};
+ String[] threeToolNames =
+ new String[]{componentNameString, componentNameString, componentNameString};
List<ToolItem> toolItemList = HearingDevicesToolItemParser.parseStringArray(mContext,
- fourToolName, new String[]{});
+ threeToolNames, new String[]{});
+
assertThat(toolItemList.size()).isEqualTo(HearingDevicesToolItemParser.MAX_NUM);
}
@@ -120,6 +120,7 @@ public class HearingDevicesToolItemParserTest extends SysuiTestCase {
List<ToolItem> toolItemList = HearingDevicesToolItemParser.parseStringArray(mContext,
wrongFormatToolName, new String[]{});
+
assertThat(toolItemList.size()).isEqualTo(0);
}
@@ -129,6 +130,7 @@ public class HearingDevicesToolItemParserTest extends SysuiTestCase {
List<ToolItem> toolItemList = HearingDevicesToolItemParser.parseStringArray(mContext,
notExistToolName, new String[]{});
+
assertThat(toolItemList.size()).isEqualTo(0);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt
index faaa4c415d28..1dd8ca9221a4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt
@@ -14,23 +14,15 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.window
+package com.android.systemui.display.data.repository
-import android.platform.test.annotations.EnableFlags
import android.view.Display
-import android.view.WindowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.app.viewcapture.ViewCaptureAwareWindowManager
-import com.android.app.viewcapture.mockViewCaptureAwareWindowManager
import com.android.systemui.SysuiTestCase
-import com.android.systemui.display.data.repository.displayRepository
-import com.android.systemui.display.data.repository.fakeDisplayWindowPropertiesRepository
-import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.unconfinedTestDispatcher
-import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
@@ -43,28 +35,13 @@ import org.mockito.kotlin.mock
@RunWith(AndroidJUnit4::class)
@SmallTest
-@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
-class MultiDisplayStatusBarWindowControllerStoreTest : SysuiTestCase() {
+class PerDisplayStoreImplTest : SysuiTestCase() {
private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
private val testScope = kosmos.testScope
private val fakeDisplayRepository = kosmos.displayRepository
- private val store =
- MultiDisplayStatusBarWindowControllerStore(
- backgroundApplicationScope = kosmos.applicationCoroutineScope,
- controllerFactory = kosmos.fakeStatusBarWindowControllerFactory,
- displayWindowPropertiesRepository = kosmos.fakeDisplayWindowPropertiesRepository,
- viewCaptureAwareWindowManagerFactory =
- object : ViewCaptureAwareWindowManager.Factory {
- override fun create(
- windowManager: WindowManager
- ): ViewCaptureAwareWindowManager {
- return kosmos.mockViewCaptureAwareWindowManager
- }
- },
- displayRepository = fakeDisplayRepository,
- )
+ private val store = kosmos.fakePerDisplayStore
@Before
fun start() {
@@ -80,34 +57,52 @@ class MultiDisplayStatusBarWindowControllerStoreTest : SysuiTestCase() {
@Test
fun forDisplay_defaultDisplay_multipleCalls_returnsSameInstance() =
testScope.runTest {
- val controller = store.defaultDisplay
+ val instance = store.defaultDisplay
- assertThat(store.defaultDisplay).isSameInstanceAs(controller)
+ assertThat(store.defaultDisplay).isSameInstanceAs(instance)
}
@Test
fun forDisplay_nonDefaultDisplay_multipleCalls_returnsSameInstance() =
testScope.runTest {
- val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
+ val instance = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
- assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isSameInstanceAs(controller)
+ assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isSameInstanceAs(instance)
}
@Test
fun forDisplay_nonDefaultDisplay_afterDisplayRemoved_returnsNewInstance() =
testScope.runTest {
- val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
+ val instance = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID)
fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
- assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isNotSameInstanceAs(controller)
+ assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isNotSameInstanceAs(instance)
}
@Test(expected = IllegalArgumentException::class)
fun forDisplay_nonExistingDisplayId_throws() =
testScope.runTest { store.forDisplay(NON_EXISTING_DISPLAY_ID) }
+ @Test
+ fun forDisplay_afterDisplayRemoved_onDisplayRemovalActionInvoked() =
+ testScope.runTest {
+ val instance = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
+
+ fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID)
+
+ assertThat(store.removalActions).containsExactly(instance)
+ }
+
+ @Test
+ fun forDisplay_withoutDisplayRemoval_onDisplayRemovalActionIsNotInvoked() =
+ testScope.runTest {
+ store.forDisplay(NON_DEFAULT_DISPLAY_ID)
+
+ assertThat(store.removalActions).isEmpty()
+ }
+
private fun createDisplay(displayId: Int): Display = mock {
on { getDisplayId() } doReturn displayId
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
index c804fc6990ae..ba5fb7f8df00 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
@@ -98,7 +98,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
var chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl)
@@ -135,7 +135,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl)
@@ -164,8 +164,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
val chipWidth = 30
val dotWidth = 10
val isRtl = false
- val contentRect =
- Rect(/* left = */ 0, /* top = */ 10, /* right = */ 1000, /* bottom = */ 100)
+ val contentRect = Rect(/* left= */ 0, /* top= */ 10, /* right= */ 1000, /* bottom= */ 100)
val chipBounds =
getPrivacyChipBoundingRectForInsets(contentRect, dotWidth, chipWidth, isRtl)
@@ -207,7 +206,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -228,7 +227,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -251,7 +250,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -263,7 +262,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
minLeftPadding,
0,
screenBounds.height() - dcBounds.height() - dotWidth,
- sbHeightLandscape
+ sbHeightLandscape,
)
bounds =
@@ -278,7 +277,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -320,7 +319,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -331,7 +330,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
protectionBounds.bottom,
0,
screenBounds.height() - minRightPadding,
- sbHeightLandscape
+ sbHeightLandscape,
)
bounds =
@@ -346,7 +345,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -369,7 +368,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -381,7 +380,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
minLeftPadding,
0,
screenBounds.height() - protectionBounds.bottom - dotWidth,
- sbHeightLandscape
+ sbHeightLandscape,
)
bounds =
@@ -396,7 +395,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -415,7 +414,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
left = screenBounds.right - dcWidth,
top = 0,
right = screenBounds.right,
- bottom = dcHeight
+ bottom = dcHeight,
)
val dcBoundsLandscape = Rect(left = 0, top = 0, right = dcHeight, bottom = dcWidth)
val dcBoundsSeascape =
@@ -423,14 +422,14 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
left = screenBounds.right - dcHeight,
top = screenBounds.bottom - dcWidth,
right = screenBounds.right - dcHeight,
- bottom = screenBounds.bottom - dcWidth
+ bottom = screenBounds.bottom - dcWidth,
)
val dcBoundsUpsideDown =
Rect(
left = 0,
top = screenBounds.bottom - dcHeight,
right = dcWidth,
- bottom = screenBounds.bottom - dcHeight
+ bottom = screenBounds.bottom - dcHeight,
)
val minLeftPadding = 20
val minRightPadding = 20
@@ -448,7 +447,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
left = minLeftPadding,
top = 0,
right = dcBoundsPortrait.left - dotWidth,
- bottom = sbHeightPortrait
+ bottom = sbHeightPortrait,
)
var bounds =
@@ -463,7 +462,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -475,7 +474,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
left = dcBoundsLandscape.height(),
top = 0,
right = screenBounds.height() - minRightPadding,
- bottom = sbHeightLandscape
+ bottom = sbHeightLandscape,
)
bounds =
@@ -490,7 +489,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -502,7 +501,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
left = minLeftPadding,
top = 0,
right = screenBounds.width() - minRightPadding,
- bottom = sbHeightPortrait
+ bottom = sbHeightPortrait,
)
bounds =
@@ -517,7 +516,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -529,7 +528,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
left = minLeftPadding,
top = 0,
right = screenBounds.height() - minRightPadding,
- bottom = sbHeightLandscape
+ bottom = sbHeightLandscape,
)
bounds =
@@ -544,7 +543,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -584,7 +583,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -595,7 +594,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
protectionBounds.bottom,
0,
screenBounds.height() - minRightPadding,
- sbHeightLandscape
+ sbHeightLandscape,
)
bounds =
@@ -610,7 +609,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -633,7 +632,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -645,7 +644,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
minLeftPadding,
0,
screenBounds.height() - protectionBounds.bottom - dotWidth,
- sbHeightLandscape
+ sbHeightLandscape,
)
bounds =
@@ -660,7 +659,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -682,7 +681,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl = false,
dotWidth = 10,
bottomAlignedMargin = BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight = 15
+ statusBarContentHeight = 15,
)
assertThat(bounds.top).isEqualTo(0)
@@ -704,7 +703,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl = false,
dotWidth = 10,
bottomAlignedMargin = 5,
- statusBarContentHeight = 15
+ statusBarContentHeight = 15,
)
// Content in the status bar is centered vertically. To achieve the bottom margin we want,
@@ -756,7 +755,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -777,7 +776,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -798,7 +797,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -809,7 +808,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
minLeftPadding,
0,
screenBounds.height() - dcBounds.height() - dotWidth,
- sbHeightLandscape
+ sbHeightLandscape,
)
bounds =
@@ -824,7 +823,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -860,7 +859,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -880,7 +879,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -900,7 +899,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -920,7 +919,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
}
@@ -958,7 +957,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
isRtl,
dotWidth,
BOTTOM_ALIGNED_MARGIN_NONE,
- statusBarContentHeight
+ statusBarContentHeight,
)
assertRects(expectedBounds, bounds, currentRotation, targetRotation)
@@ -968,12 +967,12 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
fun testDisplayChanged_returnsUpdatedInsets() {
// GIVEN: get insets on the first display and switch to the second display
val provider =
- StatusBarContentInsetsProvider(
+ StatusBarContentInsetsProviderImpl(
contextMock,
configurationController,
mock<DumpManager>(),
mock<CommandRegistry>(),
- mock<SysUICutoutProvider>()
+ mock<SysUICutoutProvider>(),
)
configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160))
@@ -993,12 +992,12 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
// GIVEN: get insets on the first display, switch to the second display,
// get insets and switch back
val provider =
- StatusBarContentInsetsProvider(
+ StatusBarContentInsetsProviderImpl(
contextMock,
configurationController,
mock<DumpManager>(),
mock<CommandRegistry>(),
- mock<SysUICutoutProvider>()
+ mock<SysUICutoutProvider>(),
)
configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160))
@@ -1024,12 +1023,12 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100)
configurationController.onConfigurationChanged(configuration)
val provider =
- StatusBarContentInsetsProvider(
+ StatusBarContentInsetsProviderImpl(
contextMock,
configurationController,
mock<DumpManager>(),
mock<CommandRegistry>(),
- mock<SysUICutoutProvider>()
+ mock<SysUICutoutProvider>(),
)
val listener =
object : StatusBarContentInsetsChangedListener {
@@ -1053,12 +1052,12 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
fun onDensityOrFontScaleChanged_listenerNotified() {
configuration.densityDpi = 12
val provider =
- StatusBarContentInsetsProvider(
+ StatusBarContentInsetsProviderImpl(
contextMock,
configurationController,
mock<DumpManager>(),
mock<CommandRegistry>(),
- mock<SysUICutoutProvider>()
+ mock<SysUICutoutProvider>(),
)
val listener =
object : StatusBarContentInsetsChangedListener {
@@ -1081,12 +1080,12 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
@Test
fun onThemeChanged_listenerNotified() {
val provider =
- StatusBarContentInsetsProvider(
+ StatusBarContentInsetsProviderImpl(
contextMock,
configurationController,
mock<DumpManager>(),
mock<CommandRegistry>(),
- mock<SysUICutoutProvider>()
+ mock<SysUICutoutProvider>(),
)
val listener =
object : StatusBarContentInsetsChangedListener {
@@ -1108,13 +1107,13 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
expected: Rect,
actual: Rect,
@Rotation currentRotation: Int,
- @Rotation targetRotation: Int
+ @Rotation targetRotation: Int,
) {
assertTrue(
"Rects must match. currentRotation=${RotationUtils.toString(currentRotation)}" +
" targetRotation=${RotationUtils.toString(targetRotation)}" +
" expected=$expected actual=$actual",
- expected.equals(actual)
+ expected.equals(actual),
)
}
@@ -1126,7 +1125,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() {
left: Rect = Rect(),
top: Rect = Rect(),
right: Rect = Rect(),
- bottom: Rect = Rect()
+ bottom: Rect = Rect(),
) {
whenever(dc.boundingRects)
.thenReturn(listOf(left, top, right, bottom).filter { !it.isEmpty })
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt
index 40c3f221e2df..29e9ba752b36 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt
@@ -61,6 +61,26 @@ class BackGestureRecognizerTest : SysuiTestCase() {
}
@Test
+ fun triggersProgressRelativeToDistance() {
+ assertProgressWhileMovingFingers(deltaX = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f)
+ assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE / 2, expectedProgress = 0.5f)
+ assertProgressWhileMovingFingers(deltaX = -SWIPE_DISTANCE, expectedProgress = 1f)
+ assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE, expectedProgress = 1f)
+ }
+
+ private fun assertProgressWhileMovingFingers(deltaX: Float, expectedProgress: Float) {
+ assertStateAfterEvents(
+ events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaX = deltaX) },
+ expectedState = InProgress(progress = expectedProgress),
+ )
+ }
+
+ @Test
+ fun triggeredProgressIsNoBiggerThanOne() {
+ assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE * 2, expectedProgress = 1f)
+ }
+
+ @Test
fun doesntTriggerGestureFinished_onGestureDistanceTooShort() {
assertStateAfterEvents(
events = ThreeFingerGesture.swipeLeft(distancePx = SWIPE_DISTANCE / 2),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
index 8406d3b99bac..ff0cec5e06e9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
@@ -104,7 +104,8 @@ class EasterEggGestureTest : SysuiTestCase() {
}
private fun assertStateAfterTwoFingerGesture(gesturePath: List<Point>, wasTriggered: Boolean) {
- val events = TwoFingerGesture.createEvents { gesturePath.forEach { (x, y) -> move(x, y) } }
+ val events =
+ TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { (x, y) -> move(x, y) } }
assertStateAfterEvents(events = events, wasTriggered = wasTriggered)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt
index 043b77577978..7d3ed92cecc6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt
@@ -56,6 +56,27 @@ class HomeGestureRecognizerTest : SysuiTestCase() {
}
@Test
+ fun triggersProgressRelativeToDistance() {
+ assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f)
+ assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE, expectedProgress = 1f)
+ }
+
+ private fun assertProgressWhileMovingFingers(deltaY: Float, expectedProgress: Float) {
+ assertStateAfterEvents(
+ events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaY = deltaY) },
+ expectedState = InProgress(progress = expectedProgress),
+ )
+ }
+
+ @Test
+ fun triggeredProgressIsBetweenZeroAndOne() {
+ // going in the wrong direction
+ assertProgressWhileMovingFingers(deltaY = SWIPE_DISTANCE / 2, expectedProgress = 0f)
+ // going further than required distance
+ assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE * 2, expectedProgress = 1f)
+ }
+
+ @Test
fun doesntTriggerGestureFinished_onGestureDistanceTooShort() {
assertStateAfterEvents(
events = ThreeFingerGesture.swipeUp(distancePx = SWIPE_DISTANCE / 2),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt
index 7095a91a4e5d..c5c0d59ea48b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt
@@ -77,11 +77,32 @@ class RecentAppsGestureRecognizerTest : SysuiTestCase() {
fun triggersGestureProgressForThreeFingerGestureStarted() {
assertStateAfterEvents(
events = ThreeFingerGesture.startEvents(x = 0f, y = 0f),
- expectedState = InProgress(),
+ expectedState = InProgress(progress = 0f),
)
}
@Test
+ fun triggersProgressRelativeToDistance() {
+ assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f)
+ assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE, expectedProgress = 1f)
+ }
+
+ private fun assertProgressWhileMovingFingers(deltaY: Float, expectedProgress: Float) {
+ assertStateAfterEvents(
+ events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaY = deltaY) },
+ expectedState = InProgress(progress = expectedProgress),
+ )
+ }
+
+ @Test
+ fun triggeredProgressIsBetweenZeroAndOne() {
+ // going in the wrong direction
+ assertProgressWhileMovingFingers(deltaY = SWIPE_DISTANCE / 2, expectedProgress = 0f)
+ // going further than required distance
+ assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE * 2, expectedProgress = 1f)
+ }
+
+ @Test
fun doesntTriggerGestureFinished_onGestureDistanceTooShort() {
assertStateAfterEvents(
events = ThreeFingerGesture.swipeUp(distancePx = SWIPE_DISTANCE / 2),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt
index 296d4dce8ce4..42fe1e5d6bec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt
@@ -25,11 +25,23 @@ import android.view.MotionEvent.ACTION_UP
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.DEFAULT_X
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.DEFAULT_Y
-/**
- * Interface for gesture builders which support creating list of [MotionEvent] for common swipe
- * gestures. For simple usage see swipe* methods or use [createEvents] for more specific scenarios.
- */
-interface MultiFingerGesture {
+/** Given gesture move events can build list of [MotionEvent]s included in that gesture */
+interface GestureEventsBuilder {
+ /**
+ * Creates full gesture including provided move events. This means returned events include DOWN,
+ * MOVE and UP. Note that move event's x and y is always relative to the starting one.
+ */
+ fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent>
+
+ /**
+ * Creates partial gesture including provided move events. This means returned events include
+ * DOWN and MOVE. Note that move event's x and y is always relative to the starting one.
+ */
+ fun eventsForGestureInProgress(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent>
+}
+
+/** Support creating list of [MotionEvent] for common swipe gestures. */
+interface MultiFingerGesture : GestureEventsBuilder {
companion object {
const val SWIPE_DISTANCE = 100f
@@ -37,27 +49,41 @@ interface MultiFingerGesture {
const val DEFAULT_Y = 500f
}
- fun swipeUp(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaY = -distancePx) }
-
- fun swipeDown(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaY = distancePx) }
+ fun swipeUp(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture {
+ move(deltaY = -distancePx)
+ }
- fun swipeRight(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaX = distancePx) }
+ fun swipeDown(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture {
+ move(deltaY = distancePx)
+ }
- fun swipeLeft(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaX = -distancePx) }
+ fun swipeRight(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture {
+ move(deltaX = distancePx)
+ }
- /**
- * Creates gesture with provided move events. Note that move event's x and y is always relative
- * to the starting one
- */
- fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent>
+ fun swipeLeft(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture {
+ move(deltaX = -distancePx)
+ }
}
object ThreeFingerGesture : MultiFingerGesture {
- override fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> {
- return touchpadGesture(
+
+ private val moveEventsBuilder = MoveEventsBuilder(::threeFingerEvent)
+
+ override fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> {
+ return buildGesture(
+ startEvents = { x, y -> startEvents(x, y) },
+ moveEvents = moveEventsBuilder.getEvents(moveEvents),
+ endEvents = { x, y -> endEvents(x, y) },
+ )
+ }
+
+ override fun eventsForGestureInProgress(
+ moveEvents: MoveEventsBuilder.() -> Unit
+ ): List<MotionEvent> {
+ return buildGesture(
startEvents = { x, y -> startEvents(x, y) },
- moveEvents = GestureBuilder(::threeFingerEvent).apply { moveEvents() }.events,
- endEvents = { x, y -> endEvents(x, y) }
+ moveEvents = moveEventsBuilder.getEvents(moveEvents),
)
}
@@ -65,7 +91,7 @@ object ThreeFingerGesture : MultiFingerGesture {
return listOf(
threeFingerEvent(ACTION_DOWN, x, y),
threeFingerEvent(ACTION_POINTER_DOWN, x, y),
- threeFingerEvent(ACTION_POINTER_DOWN, x, y)
+ threeFingerEvent(ACTION_POINTER_DOWN, x, y),
)
}
@@ -73,32 +99,43 @@ object ThreeFingerGesture : MultiFingerGesture {
return listOf(
threeFingerEvent(ACTION_POINTER_UP, x, y),
threeFingerEvent(ACTION_POINTER_UP, x, y),
- threeFingerEvent(ACTION_UP, x, y)
+ threeFingerEvent(ACTION_UP, x, y),
)
}
private fun threeFingerEvent(
action: Int,
x: Float = DEFAULT_X,
- y: Float = DEFAULT_Y
+ y: Float = DEFAULT_Y,
): MotionEvent {
return touchpadEvent(
action = action,
x = x,
y = y,
classification = MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE,
- axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 3f)
+ axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 3f),
)
}
}
object FourFingerGesture : MultiFingerGesture {
- override fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> {
- return touchpadGesture(
+ private val moveEventsBuilder = MoveEventsBuilder(::fourFingerEvent)
+
+ override fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> {
+ return buildGesture(
+ startEvents = { x, y -> startEvents(x, y) },
+ moveEvents = moveEventsBuilder.getEvents(moveEvents),
+ endEvents = { x, y -> endEvents(x, y) },
+ )
+ }
+
+ override fun eventsForGestureInProgress(
+ moveEvents: MoveEventsBuilder.() -> Unit
+ ): List<MotionEvent> {
+ return buildGesture(
startEvents = { x, y -> startEvents(x, y) },
- moveEvents = GestureBuilder(::fourFingerEvent).apply { moveEvents() }.events,
- endEvents = { x, y -> endEvents(x, y) }
+ moveEvents = moveEventsBuilder.getEvents(moveEvents),
)
}
@@ -107,7 +144,7 @@ object FourFingerGesture : MultiFingerGesture {
fourFingerEvent(ACTION_DOWN, x, y),
fourFingerEvent(ACTION_POINTER_DOWN, x, y),
fourFingerEvent(ACTION_POINTER_DOWN, x, y),
- fourFingerEvent(ACTION_POINTER_DOWN, x, y)
+ fourFingerEvent(ACTION_POINTER_DOWN, x, y),
)
}
@@ -116,61 +153,74 @@ object FourFingerGesture : MultiFingerGesture {
fourFingerEvent(ACTION_POINTER_UP, x, y),
fourFingerEvent(ACTION_POINTER_UP, x, y),
fourFingerEvent(ACTION_POINTER_UP, x, y),
- fourFingerEvent(ACTION_UP, x, y)
+ fourFingerEvent(ACTION_UP, x, y),
)
}
private fun fourFingerEvent(
action: Int,
x: Float = DEFAULT_X,
- y: Float = DEFAULT_Y
+ y: Float = DEFAULT_Y,
): MotionEvent {
return touchpadEvent(
action = action,
x = x,
y = y,
classification = MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE,
- axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 4f)
+ axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 4f),
)
}
}
object TwoFingerGesture : MultiFingerGesture {
- override fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> {
- return touchpadGesture(
- startEvents = { x, y -> listOf(twoFingerEvent(ACTION_DOWN, x, y)) },
- moveEvents = GestureBuilder(::twoFingerEvent).apply { moveEvents() }.events,
- endEvents = { x, y -> listOf(twoFingerEvent(ACTION_UP, x, y)) }
+ private val moveEventsBuilder = MoveEventsBuilder(::twoFingerEvent)
+
+ override fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> {
+ return buildGesture(
+ startEvents = { x, y -> startEvents(x, y) },
+ moveEvents = moveEventsBuilder.getEvents(moveEvents),
+ endEvents = { x, y -> listOf(twoFingerEvent(ACTION_UP, x, y)) },
+ )
+ }
+
+ override fun eventsForGestureInProgress(
+ moveEvents: MoveEventsBuilder.() -> Unit
+ ): List<MotionEvent> {
+ return buildGesture(
+ startEvents = { x, y -> startEvents(x, y) },
+ moveEvents = moveEventsBuilder.getEvents(moveEvents),
)
}
+ private fun startEvents(x: Float, y: Float) = listOf(twoFingerEvent(ACTION_DOWN, x, y))
+
private fun twoFingerEvent(
action: Int,
x: Float = DEFAULT_X,
- y: Float = DEFAULT_Y
+ y: Float = DEFAULT_Y,
): MotionEvent {
return touchpadEvent(
action = action,
x = x,
y = y,
classification = MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE,
- axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 2f)
+ axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 2f),
)
}
}
-private fun touchpadGesture(
+private fun buildGesture(
startEvents: (Float, Float) -> List<MotionEvent>,
moveEvents: List<MotionEvent>,
- endEvents: (Float, Float) -> List<MotionEvent>
+ endEvents: (Float, Float) -> List<MotionEvent> = { _, _ -> emptyList() },
): List<MotionEvent> {
val lastX = moveEvents.last().x
val lastY = moveEvents.last().y
return startEvents(DEFAULT_X, DEFAULT_Y) + moveEvents + endEvents(lastX, lastY)
}
-class GestureBuilder internal constructor(val eventBuilder: (Int, Float, Float) -> MotionEvent) {
+class MoveEventsBuilder internal constructor(val eventBuilder: (Int, Float, Float) -> MotionEvent) {
val events = mutableListOf<MotionEvent>()
@@ -178,3 +228,11 @@ class GestureBuilder internal constructor(val eventBuilder: (Int, Float, Float)
events.add(eventBuilder(ACTION_MOVE, DEFAULT_X + deltaX, DEFAULT_Y + deltaY))
}
}
+
+private fun MoveEventsBuilder.getEvents(
+ moveEvents: MoveEventsBuilder.() -> Unit
+): List<MotionEvent> {
+ events.clear()
+ this.moveEvents()
+ return events
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt
index 13ebb42531b8..64136775b4eb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt
@@ -54,7 +54,28 @@ class TouchpadGestureBuilderTest : SysuiTestCase() {
ACTION_MOVE,
ACTION_POINTER_UP,
ACTION_POINTER_UP,
- ACTION_UP
+ ACTION_UP,
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun threeFingerGestureInProgressProducesCorrectEvents() {
+ val events =
+ ThreeFingerGesture.eventsForGestureInProgress {
+ move(deltaX = 10f)
+ move(deltaX = 20f)
+ }
+
+ val actions = events.map { it.actionMasked }
+ assertWithMessage("Events have expected action type")
+ .that(actions)
+ .containsExactly(
+ ACTION_DOWN,
+ ACTION_POINTER_DOWN,
+ ACTION_POINTER_DOWN,
+ ACTION_MOVE,
+ ACTION_MOVE,
)
.inOrder()
}
@@ -80,7 +101,7 @@ class TouchpadGestureBuilderTest : SysuiTestCase() {
ACTION_POINTER_UP,
ACTION_POINTER_UP,
ACTION_POINTER_UP,
- ACTION_UP
+ ACTION_UP,
)
.inOrder()
}
@@ -109,7 +130,7 @@ class TouchpadGestureBuilderTest : SysuiTestCase() {
@Test
fun gestureBuilderProducesCorrectEventCoordinates() {
val events =
- ThreeFingerGesture.createEvents {
+ ThreeFingerGesture.eventsForFullGesture {
move(deltaX = 50f)
move(deltaX = 100f)
}
@@ -127,7 +148,7 @@ class TouchpadGestureBuilderTest : SysuiTestCase() {
// up events
DEFAULT_X + 100f to DEFAULT_Y,
DEFAULT_X + 100f to DEFAULT_Y,
- DEFAULT_X + 100f to DEFAULT_Y
+ DEFAULT_X + 100f to DEFAULT_Y,
)
.inOrder()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index a867eb38b44c..c302b40fc4d7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -85,7 +85,7 @@ class TouchpadGestureHandlerTest : SysuiTestCase() {
}
private fun backGestureEvents(): List<MotionEvent> {
- return ThreeFingerGesture.createEvents {
+ return ThreeFingerGesture.eventsForFullGesture {
move(deltaX = SWIPE_DISTANCE / 4)
move(deltaX = SWIPE_DISTANCE / 2)
move(deltaX = SWIPE_DISTANCE)
diff --git a/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml b/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml
index 627b92b8a779..3c1668405909 100644
--- a/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml
+++ b/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml
@@ -21,11 +21,8 @@
android:color="?android:attr/colorControlHighlight">
<item>
<shape android:shape="rectangle">
- <solid android:color="@android:color/transparent"/>
+ <solid android:color="?androidprv:attr/materialColorPrimaryContainer"/>
<corners android:radius="@dimen/hearing_devices_preset_spinner_background_radius"/>
- <stroke
- android:width="1dp"
- android:color="?androidprv:attr/textColorTertiary" />
</shape>
</item>
</ripple>
diff --git a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
index 4a7bef9f48b9..80692f9481b7 100644
--- a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
@@ -72,7 +72,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/device_function_barrier"
- app:layout_constraintBottom_toTopOf="@id/related_tools_scroll"
android:drawableStart="@drawable/ic_add"
android:drawablePadding="20dp"
android:drawableTint="?android:attr/textColorPrimary"
@@ -92,24 +91,16 @@
app:barrierDirection="bottom"
app:constraint_referenced_ids="device_function_barrier, pair_new_device_button" />
- <HorizontalScrollView
- android:id="@+id/related_tools_scroll"
+ <LinearLayout
+ android:id="@+id/related_tools_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/bluetooth_dialog_layout_margin"
android:layout_marginEnd="@dimen/bluetooth_dialog_layout_margin"
android:layout_marginTop="@dimen/hearing_devices_layout_margin"
- android:scrollbars="none"
- android:fillViewport="true"
+ android:orientation="horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@id/preset_spinner">
- <LinearLayout
- android:id="@+id/related_tools_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="horizontal">
- </LinearLayout>
- </HorizontalScrollView>
+ app:layout_constraintTop_toBottomOf="@id/device_barrier" />
</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/hearing_tool_item.xml b/packages/SystemUI/res/layout/hearing_tool_item.xml
index ff2fbe070e0f..f5baf2aaf6dc 100644
--- a/packages/SystemUI/res/layout/hearing_tool_item.xml
+++ b/packages/SystemUI/res/layout/hearing_tool_item.xml
@@ -17,8 +17,8 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tool_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="top|center_horizontal"
android:focusable="true"
@@ -26,8 +26,10 @@
android:layout_weight="1">
<FrameLayout
android:id="@+id/icon_frame"
- android:layout_width="@dimen/hearing_devices_tool_icon_frame_width"
- android:layout_height="@dimen/hearing_devices_tool_icon_frame_height"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="20dp"
+ android:paddingBottom="20dp"
android:background="@drawable/qs_hearing_devices_related_tools_background"
android:focusable="false" >
<ImageView
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 16a8bc5b034f..7d840cfe949a 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -134,17 +134,19 @@
<!-- Use collapsed layout for media player in landscape QQS -->
<bool name="config_quickSettingsMediaLandscapeCollapsed">true</bool>
- <!-- For hearing devices related tool list. Need to be in ComponentName format (package/class).
- Should be activity to be launched.
- Already contains tool that holds intent: "com.android.settings.action.live_caption".
- Maximum number is 3. -->
+ <!-- Hearing devices related tool list. Each entry must be in ComponentName format
+ (package/class), indicating the specific application component to launch.
+ Already contains tool that handles intent: "com.android.settings.action.live_caption" by
+ default. You can add up to 2 additional related tools. -->
<string-array name="config_quickSettingsHearingDevicesRelatedToolName" translatable="false">
</string-array>
- <!-- The drawable resource names. If provided, it will replace the corresponding icons in
- config_quickSettingsHearingDevicesRelatedToolName. Can be empty to use original icons.
- Already contains tool that holds intent: "com.android.settings.action.live_caption".
- Maximum number is 3. -->
+ <!-- Hearing devices related tool icon list. Provide drawable resource names in the same order
+ as the component names in config_quickSettingsHearingDevicesRelatedToolName. The icons
+ should be monochrome and will be tinted according to the system's material color. Ensure
+ the number of icon resources matches the number of components specified in
+ config_quickSettingsHearingDevicesRelatedToolName. If this array is empty or the sizes
+ don't match, the original application icons will be used. -->
<string-array name="config_quickSettingsHearingDevicesRelatedToolIcon" translatable="false">
</string-array>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 6c8a7403953e..209a971814f4 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1798,8 +1798,6 @@
<dimen name="hearing_devices_preset_spinner_text_padding_vertical">15dp</dimen>
<dimen name="hearing_devices_preset_spinner_arrow_size">24dp</dimen>
<dimen name="hearing_devices_preset_spinner_background_radius">28dp</dimen>
- <dimen name="hearing_devices_tool_icon_frame_width">94dp</dimen>
- <dimen name="hearing_devices_tool_icon_frame_height">64dp</dimen>
<dimen name="hearing_devices_tool_icon_size">28dp</dimen>
<!-- Height percentage of the parent container occupied by the communal view -->
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 158623fa80af..1978bb89b5b2 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -42,6 +42,7 @@ import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import android.widget.Space;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
@@ -52,6 +53,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.settingslib.Utils;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HapClientProfile;
@@ -428,10 +430,16 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
} catch (Resources.NotFoundException e) {
Log.i(TAG, "No hearing devices related tool config resource");
}
- final int listSize = toolItemList.size();
- for (int i = 0; i < listSize; i++) {
+ for (int i = 0; i < toolItemList.size(); i++) {
View view = createHearingToolView(context, toolItemList.get(i));
mRelatedToolsContainer.addView(view);
+ if (i != toolItemList.size() - 1) {
+ final int spaceSize = context.getResources().getDimensionPixelSize(
+ R.dimen.hearing_devices_layout_margin);
+ Space space = new Space(context);
+ space.setLayoutParams(new LinearLayout.LayoutParams(spaceSize, 0));
+ mRelatedToolsContainer.addView(space);
+ }
}
}
@@ -492,6 +500,10 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
TextView text = view.requireViewById(R.id.tool_name);
view.setContentDescription(item.getToolName());
icon.setImageDrawable(item.getToolIcon());
+ if (item.isCustomIcon()) {
+ icon.getDrawable().mutate().setTint(Utils.getColorAttr(context,
+ com.android.internal.R.attr.materialColorOnPrimaryContainer).getDefaultColor());
+ }
text.setText(item.getToolName());
Intent intent = item.getToolIntent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
@@ -517,7 +529,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
return new ToolItem(
context.getString(R.string.quick_settings_hearing_devices_live_caption_title),
context.getDrawable(R.drawable.ic_volume_odi_captions),
- LIVE_CAPTION_INTENT);
+ LIVE_CAPTION_INTENT,
+ /* isCustomIcon= */ true);
}
return null;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java
index 2006726e6847..7e4c1e5e7b6e 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java
@@ -41,7 +41,7 @@ public class HearingDevicesToolItemParser {
private static final String SPLIT_DELIMITER = "/";
private static final String RES_TYPE = "drawable";
@VisibleForTesting
- static final int MAX_NUM = 3;
+ static final int MAX_NUM = 2;
/**
* Parses the string arrays to create a list of {@link ToolItem}.
@@ -82,7 +82,8 @@ public class HearingDevicesToolItemParser {
useCustomIcons ? iconList.get(i)
: activityInfoList.get(i).loadIcon(packageManager),
new Intent(Intent.ACTION_MAIN).setComponent(
- activityInfoList.get(i).getComponentName())
+ activityInfoList.get(i).getComponentName()),
+ useCustomIcons
));
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt
index 66bb2b5e2328..ef03f0cdef79 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt
@@ -23,4 +23,5 @@ data class ToolItem(
var toolName: String = "",
var toolIcon: Drawable,
var toolIntent: Intent,
+ var isCustomIcon: Boolean,
)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
index 2c026c0bb5ce..7337e5af51a1 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
@@ -30,6 +30,7 @@ import com.android.internal.util.EmergencyAffordanceManager
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository
import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
@@ -74,6 +75,7 @@ constructor(
private val metricsLogger: MetricsLogger,
private val dozeLogger: DozeLogger,
private val sceneInteractor: Lazy<SceneInteractor>,
+ private val bouncerHapticPlayer: BouncerHapticPlayer,
) {
/** The bouncer action button. If `null`, the button should not be shown. */
val actionButton: Flow<BouncerActionButtonModel?> =
@@ -111,6 +113,8 @@ constructor(
BouncerActionButtonModel(
label = applicationContext.getString(R.string.lockscreen_emergency_call),
onClick = {
+ // TODO(b/373930432): haptics should be played at the UI layer -> refactor
+ bouncerHapticPlayer.playEmergencyButtonClickFeedback()
prepareToPerformAction()
dozeLogger.logEmergencyCall()
startEmergencyDialerActivity()
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
index d6b92115c64b..837390730c7a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
@@ -81,4 +81,11 @@ class BouncerHapticPlayer @Inject constructor(private val msdlPlayer: dagger.Laz
/** Deliver MSDL feedback when a numpad key is pressed on the pin bouncer */
fun playNumpadKeyFeedback() = msdlPlayer.get().playToken(MSDLToken.KEYPRESS_STANDARD)
+
+ /** Deliver MSDL feedback when clicking on the emergency button */
+ fun playEmergencyButtonClickFeedback() {
+ if (isEnabled) {
+ msdlPlayer.get().playToken(MSDLToken.KEYPRESS_RETURN)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt
new file mode 100644
index 000000000000..2ce3e43389fa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import android.view.Display
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.qualifiers.Background
+import java.io.PrintWriter
+import java.util.concurrent.ConcurrentHashMap
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** Provides per display instances of [T]. */
+interface PerDisplayStore<T> {
+
+ /**
+ * The instance for the default/main display of the device. For example, on a phone or a tablet,
+ * the default display is the internal/built-in display of the device.
+ *
+ * Note that the id of the default display is [Display.DEFAULT_DISPLAY].
+ */
+ val defaultDisplay: T
+
+ /**
+ * Returns an instance for a specific display id.
+ *
+ * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing
+ * displays.
+ */
+ fun forDisplay(displayId: Int): T
+}
+
+abstract class PerDisplayStoreImpl<T>(
+ @Background private val backgroundApplicationScope: CoroutineScope,
+ private val displayRepository: DisplayRepository,
+) : PerDisplayStore<T>, CoreStartable {
+
+ private val perDisplayInstances = ConcurrentHashMap<Int, T>()
+
+ /**
+ * The instance for the default/main display of the device. For example, on a phone or a tablet,
+ * the default display is the internal/built-in display of the device.
+ *
+ * Note that the id of the default display is [Display.DEFAULT_DISPLAY].
+ */
+ override val defaultDisplay: T
+ get() = forDisplay(Display.DEFAULT_DISPLAY)
+
+ /**
+ * Returns an instance for a specific display id.
+ *
+ * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing
+ * displays.
+ */
+ override fun forDisplay(displayId: Int): T {
+ if (displayRepository.getDisplay(displayId) == null) {
+ throw IllegalArgumentException("Display with id $displayId doesn't exist.")
+ }
+ return perDisplayInstances.computeIfAbsent(displayId) {
+ createInstanceForDisplay(displayId)
+ }
+ }
+
+ abstract fun createInstanceForDisplay(displayId: Int): T
+
+ override fun start() {
+ val instanceType = instanceClass.simpleName
+ backgroundApplicationScope.launch(CoroutineName("PerDisplayStore#<$instanceType>start")) {
+ displayRepository.displayRemovalEvent.collect { removedDisplayId ->
+ val removedInstance = perDisplayInstances.remove(removedDisplayId)
+ removedInstance?.let { onDisplayRemovalAction(it) }
+ }
+ }
+ }
+
+ abstract val instanceClass: Class<T>
+
+ /**
+ * Will be called when the display associated with [instance] was removed. It allows to perform
+ * any clean up if needed.
+ */
+ open suspend fun onDisplayRemovalAction(instance: T) {}
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println(perDisplayInstances)
+ }
+}
+
+class SingleDisplayStore<T>(defaultInstance: T) : PerDisplayStore<T> {
+ override val defaultDisplay: T = defaultInstance
+
+ override fun forDisplay(displayId: Int): T = defaultDisplay
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
index 801a0ce4b744..537b56bccae8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
@@ -39,7 +39,7 @@ constructor(@Main private val resources: Resources, val theme: Resources.Theme)
val loadedIcon: Icon.Loaded =
when (val dataIcon = data.icon) {
is Icon.Resource -> {
- if (iconRes != dataIcon.res) {
+ if (data.iconResId != dataIcon.res) {
Log.wtf(
"ModesTileMapper",
"Icon.Resource.res & iconResId are not identical",
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt
index 6c3802676f26..041f0b0fdf93 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt
@@ -16,88 +16,52 @@
package com.android.systemui.statusbar.core
-import android.view.Display
-import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.data.repository.PerDisplayStore
+import com.android.systemui.display.data.repository.PerDisplayStoreImpl
+import com.android.systemui.display.data.repository.SingleDisplayStore
import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
-import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
/** Provides per display instances of [StatusBarInitializer]. */
-interface StatusBarInitializerStore {
- /**
- * The instance for the default/main display of the device. For example, on a phone or a tablet,
- * the default display is the internal/built-in display of the device.
- *
- * Note that the id of the default display is [Display.DEFAULT_DISPLAY].
- */
- val defaultDisplay: StatusBarInitializer
-
- /**
- * Returns an instance for a specific display id.
- *
- * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing
- * displays.
- */
- fun forDisplay(displayId: Int): StatusBarInitializer
-}
+interface StatusBarInitializerStore : PerDisplayStore<StatusBarInitializer>
@SysUISingleton
class MultiDisplayStatusBarInitializerStore
@Inject
constructor(
- @Background private val backgroundApplicationScope: CoroutineScope,
+ @Background backgroundApplicationScope: CoroutineScope,
+ displayRepository: DisplayRepository,
private val factory: StatusBarInitializer.Factory,
- private val displayRepository: DisplayRepository,
private val statusBarWindowControllerStore: StatusBarWindowControllerStore,
-) : StatusBarInitializerStore, CoreStartable {
+) :
+ StatusBarInitializerStore,
+ PerDisplayStoreImpl<StatusBarInitializer>(backgroundApplicationScope, displayRepository) {
init {
StatusBarConnectedDisplays.assertInNewMode()
}
- private val perDisplayInitializers = ConcurrentHashMap<Int, StatusBarInitializer>()
-
- override val defaultDisplay: StatusBarInitializer
- get() = forDisplay(Display.DEFAULT_DISPLAY)
-
- override fun forDisplay(displayId: Int): StatusBarInitializer {
- if (displayRepository.getDisplay(displayId) == null) {
- throw IllegalArgumentException("Display with id $displayId doesn't exist.")
- }
- return perDisplayInitializers.computeIfAbsent(displayId) {
- factory.create(
- statusBarWindowController = statusBarWindowControllerStore.forDisplay(displayId)
- )
- }
+ override fun createInstanceForDisplay(displayId: Int): StatusBarInitializer {
+ return factory.create(
+ statusBarWindowController = statusBarWindowControllerStore.forDisplay(displayId)
+ )
}
- override fun start() {
- backgroundApplicationScope.launch(
- CoroutineName("MultiDisplayStatusBarInitializerStore#start")
- ) {
- displayRepository.displayRemovalEvent.collect { removedDisplayId ->
- perDisplayInitializers.remove(removedDisplayId)
- }
- }
- }
+ override val instanceClass = StatusBarInitializer::class.java
}
@SysUISingleton
class SingleDisplayStatusBarInitializerStore
@Inject
-constructor(private val defaultInstance: StatusBarInitializer) : StatusBarInitializerStore {
+constructor(defaultInitializer: StatusBarInitializer) :
+ StatusBarInitializerStore,
+ PerDisplayStore<StatusBarInitializer> by SingleDisplayStore(defaultInitializer) {
init {
StatusBarConnectedDisplays.assertInLegacyMode()
}
-
- override val defaultDisplay: StatusBarInitializer = defaultInstance
-
- override fun forDisplay(displayId: Int): StatusBarInitializer = defaultInstance
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index 5aad11fe1034..f6f4503b210a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -17,17 +17,20 @@
package com.android.systemui.statusbar.dagger
import android.content.Context
-import com.android.app.viewcapture.ViewCaptureAwareWindowManager
import com.android.systemui.CoreStartable
+import com.android.systemui.SysUICutoutProvider
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.StatusBarDataLayerModule
import com.android.systemui.statusbar.phone.LightBarController
+import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
+import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl
import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore
import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore
@@ -108,5 +111,16 @@ abstract class StatusBarModule {
fun provideOngoingCallLogBuffer(factory: LogBufferFactory): LogBuffer {
return factory.create("OngoingCall", 75)
}
+
+ @Provides
+ @SysUISingleton
+ fun contentInsetsProvider(
+ factory: StatusBarContentInsetsProviderImpl.Factory,
+ context: Context,
+ configurationController: ConfigurationController,
+ sysUICutoutProvider: SysUICutoutProvider,
+ ): StatusBarContentInsetsProvider {
+ return factory.create(context, configurationController, sysUICutoutProvider)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
index 613efaa148f5..c6f6bd90fce6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
@@ -34,10 +34,10 @@ import com.android.systemui.Dumpable
import com.android.systemui.StatusBarInsetsCommand
import com.android.systemui.SysUICutoutInformation
import com.android.systemui.SysUICutoutProvider
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.commandline.CommandRegistry
+import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl.CacheKey
import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
@@ -47,9 +47,11 @@ import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
import com.android.systemui.util.leak.RotationUtils.Rotation
import com.android.systemui.util.leak.RotationUtils.getExactRotation
import com.android.systemui.util.leak.RotationUtils.getResourcesForRotation
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import java.io.PrintWriter
import java.lang.Math.max
-import javax.inject.Inject
/**
* Encapsulates logic that can solve for the left/right insets required for the status bar contents.
@@ -64,19 +66,87 @@ import javax.inject.Inject
*
* NOTE: This class is not threadsafe
*/
-@SysUISingleton
-class StatusBarContentInsetsProvider
-@Inject
+interface StatusBarContentInsetsProvider :
+ CallbackController<StatusBarContentInsetsChangedListener> {
+
+ /**
+ * Some views may need to care about whether or not the current top display cutout is located in
+ * the corner rather than somewhere in the center. In the case of a corner cutout, the status
+ * bar area is contiguous.
+ */
+ fun currentRotationHasCornerCutout(): Boolean
+
+ /**
+ * Calculates the maximum bounding rectangle for the privacy chip animation + ongoing privacy
+ * dot in the coordinates relative to the given rotation.
+ *
+ * @param rotation the rotation for which the bounds are required. This is an absolute value
+ * (i.e., ROTATION_NONE will always return the same bounds regardless of the context from
+ * which this method is called)
+ */
+ fun getBoundingRectForPrivacyChipForRotation(
+ @Rotation rotation: Int,
+ displayCutout: DisplayCutout?,
+ ): Rect
+
+ /**
+ * Calculate the distance from the left, right and top edges of the screen to the status bar
+ * content area. This differs from the content area rects in that these values can be used
+ * directly as padding.
+ *
+ * @param rotation the target rotation for which to calculate insets
+ */
+ fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Insets
+
+ /**
+ * Calculate the insets for the status bar content in the device's current rotation
+ *
+ * @see getStatusBarContentAreaForRotation
+ */
+ fun getStatusBarContentInsetsForCurrentRotation(): Insets
+
+ /**
+ * Calculates the area of the status bar contents invariant of the current device rotation, in
+ * the target rotation's coordinates
+ *
+ * @param rotation the rotation for which the bounds are required. This is an absolute value
+ * (i.e., ROTATION_NONE will always return the same bounds regardless of the context from
+ * which this method is called)
+ */
+ fun getStatusBarContentAreaForRotation(@Rotation rotation: Int): Rect
+
+ /** Get the status bar content area for the given rotation, in absolute bounds */
+ fun getStatusBarContentAreaForCurrentRotation(): Rect
+
+ fun getStatusBarPaddingTop(@Rotation rotation: Int? = null): Int
+
+ interface Factory {
+ fun create(
+ context: Context,
+ configurationController: ConfigurationController,
+ sysUICutoutProvider: SysUICutoutProvider,
+ ): StatusBarContentInsetsProvider
+ }
+}
+
+class StatusBarContentInsetsProviderImpl
+@AssistedInject
constructor(
- val context: Context,
- val configurationController: ConfigurationController,
+ @Assisted val context: Context,
+ @Assisted val configurationController: ConfigurationController,
val dumpManager: DumpManager,
val commandRegistry: CommandRegistry,
- val sysUICutoutProvider: SysUICutoutProvider,
-) :
- CallbackController<StatusBarContentInsetsChangedListener>,
- ConfigurationController.ConfigurationListener,
- Dumpable {
+ @Assisted val sysUICutoutProvider: SysUICutoutProvider,
+) : StatusBarContentInsetsProvider, ConfigurationController.ConfigurationListener, Dumpable {
+
+ @AssistedFactory
+ interface Factory : StatusBarContentInsetsProvider.Factory {
+ override fun create(
+ context: Context,
+ configurationController: ConfigurationController,
+ sysUICutoutProvider: SysUICutoutProvider,
+ ): StatusBarContentInsetsProviderImpl
+ }
// Limit cache size as potentially we may connect large number of displays
// (e.g. network displays)
@@ -95,7 +165,7 @@ constructor(
object : StatusBarInsetsCommand.Callback {
override fun onExecute(
command: StatusBarInsetsCommand,
- printWriter: PrintWriter
+ printWriter: PrintWriter,
) {
executeCommand(command, printWriter)
}
@@ -133,12 +203,7 @@ constructor(
listeners.forEach { it.onStatusBarContentInsetsChanged() }
}
- /**
- * Some views may need to care about whether or not the current top display cutout is located in
- * the corner rather than somewhere in the center. In the case of a corner cutout, the status
- * bar area is contiguous.
- */
- fun currentRotationHasCornerCutout(): Boolean {
+ override fun currentRotationHasCornerCutout(): Boolean {
val cutout = checkNotNull(context.display).cutout ?: return false
val topBounds = cutout.boundingRectTop
@@ -148,17 +213,9 @@ constructor(
return topBounds.left <= 0 || topBounds.right >= point.x
}
- /**
- * Calculates the maximum bounding rectangle for the privacy chip animation + ongoing privacy
- * dot in the coordinates relative to the given rotation.
- *
- * @param rotation the rotation for which the bounds are required. This is an absolute value
- * (i.e., ROTATION_NONE will always return the same bounds regardless of the context from
- * which this method is called)
- */
- fun getBoundingRectForPrivacyChipForRotation(
+ override fun getBoundingRectForPrivacyChipForRotation(
@Rotation rotation: Int,
- displayCutout: DisplayCutout?
+ displayCutout: DisplayCutout?,
): Rect {
val key = getCacheKey(rotation, displayCutout)
var insets = insetsCache[key]
@@ -176,14 +233,7 @@ constructor(
return getPrivacyChipBoundingRectForInsets(insets, dotWidth, chipWidth, isRtl)
}
- /**
- * Calculate the distance from the left, right and top edges of the screen to the status bar
- * content area. This differs from the content area rects in that these values can be used
- * directly as padding.
- *
- * @param rotation the target rotation for which to calculate insets
- */
- fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Insets =
+ override fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Insets =
traceSection(tag = "StatusBarContentInsetsProvider.getStatusBarContentInsetsForRotation") {
val sysUICutout = sysUICutoutProvider.cutoutInfoForCurrentDisplayAndRotation()
val displayCutout = sysUICutout?.cutout
@@ -202,31 +252,17 @@ constructor(
rotation,
sysUICutout,
getResourcesForRotation(rotation, context),
- key
+ key,
)
Insets.of(area.left, area.top, /* right= */ width - area.right, /* bottom= */ 0)
}
- /**
- * Calculate the insets for the status bar content in the device's current rotation
- *
- * @see getStatusBarContentAreaForRotation
- */
- fun getStatusBarContentInsetsForCurrentRotation(): Insets {
+ override fun getStatusBarContentInsetsForCurrentRotation(): Insets {
return getStatusBarContentInsetsForRotation(getExactRotation(context))
}
- /**
- * Calculates the area of the status bar contents invariant of the current device rotation, in
- * the target rotation's coordinates
- *
- * @param rotation the rotation for which the bounds are required. This is an absolute value
- * (i.e., ROTATION_NONE will always return the same bounds regardless of the context from
- * which this method is called)
- */
- @JvmOverloads
- fun getStatusBarContentAreaForRotation(@Rotation rotation: Int): Rect {
+ override fun getStatusBarContentAreaForRotation(@Rotation rotation: Int): Rect {
val sysUICutout = sysUICutoutProvider.cutoutInfoForCurrentDisplayAndRotation()
val displayCutout = sysUICutout?.cutout
val key = getCacheKey(rotation, displayCutout)
@@ -235,12 +271,11 @@ constructor(
rotation,
sysUICutout,
getResourcesForRotation(rotation, context),
- key
+ key,
)
}
- /** Get the status bar content area for the given rotation, in absolute bounds */
- fun getStatusBarContentAreaForCurrentRotation(): Rect {
+ override fun getStatusBarContentAreaForCurrentRotation(): Rect {
val rotation = getExactRotation(context)
return getStatusBarContentAreaForRotation(rotation)
}
@@ -249,7 +284,7 @@ constructor(
@Rotation targetRotation: Int,
sysUICutout: SysUICutoutInformation?,
rotatedResources: Resources,
- key: CacheKey
+ key: CacheKey,
): Rect {
return getCalculatedAreaForRotation(sysUICutout, targetRotation, rotatedResources).also {
insetsCache.put(key, it)
@@ -259,7 +294,7 @@ constructor(
private fun getCalculatedAreaForRotation(
sysUICutout: SysUICutoutInformation?,
@Rotation targetRotation: Int,
- rotatedResources: Resources
+ rotatedResources: Resources,
): Rect {
val currentRotation = getExactRotation(context)
@@ -299,7 +334,7 @@ constructor(
configurationController.isLayoutRtl,
dotWidth,
bottomAlignedMargin,
- statusBarContentHeight
+ statusBarContentHeight,
)
}
@@ -349,7 +384,7 @@ constructor(
return resources.getDimensionPixelSize(dimenRes)
}
- fun getStatusBarPaddingTop(@Rotation rotation: Int? = null): Int {
+ override fun getStatusBarPaddingTop(@Rotation rotation: Int?): Int {
val res = rotation?.let { it -> getResourcesForRotation(it, context) } ?: context.resources
return res.getDimensionPixelSize(R.dimen.status_bar_padding_top)
}
@@ -364,13 +399,13 @@ constructor(
CacheKey(
rotation = rotation,
displaySize = Rect(context.resources.configuration.windowConfiguration.maxBounds),
- displayCutout = displayCutout
+ displayCutout = displayCutout,
)
private data class CacheKey(
@Rotation val rotation: Int,
val displaySize: Rect,
- val displayCutout: DisplayCutout?
+ val displayCutout: DisplayCutout?,
)
}
@@ -395,21 +430,21 @@ fun getPrivacyChipBoundingRectForInsets(
contentRect: Rect,
dotWidth: Int,
chipWidth: Int,
- isRtl: Boolean
+ isRtl: Boolean,
): Rect {
return if (isRtl) {
Rect(
contentRect.left - dotWidth,
contentRect.top,
contentRect.left + chipWidth,
- contentRect.bottom
+ contentRect.bottom,
)
} else {
Rect(
contentRect.right - chipWidth,
contentRect.top,
contentRect.right + dotWidth,
- contentRect.bottom
+ contentRect.bottom,
)
}
}
@@ -443,7 +478,7 @@ fun calculateInsetsForRotationWithRotatedResources(
isRtl: Boolean,
dotWidth: Int,
bottomAlignedMargin: Int,
- statusBarContentHeight: Int
+ statusBarContentHeight: Int,
): Rect {
/*
TODO: Check if this is ever used for devices with no rounded corners
@@ -467,7 +502,7 @@ fun calculateInsetsForRotationWithRotatedResources(
targetRotation,
currentRotation,
bottomAlignedMargin,
- statusBarContentHeight
+ statusBarContentHeight,
)
}
@@ -503,7 +538,7 @@ private fun getStatusBarContentBounds(
@Rotation targetRotation: Int,
@Rotation currentRotation: Int,
bottomAlignedMargin: Int,
- statusBarContentHeight: Int
+ statusBarContentHeight: Int,
): Rect {
val insetTop = getInsetTop(bottomAlignedMargin, statusBarContentHeight, sbHeight)
@@ -597,7 +632,7 @@ private val DisplayCutout.boundingRectsLeftRightTop
private fun getInsetTop(
bottomAlignedMargin: Int,
statusBarContentHeight: Int,
- statusBarHeight: Int
+ statusBarHeight: Int,
): Int {
val bottomAlignmentEnabled = bottomAlignedMargin >= 0
if (!bottomAlignmentEnabled) {
@@ -610,7 +645,7 @@ private fun getInsetTop(
private fun sbRect(
@Rotation relativeRotation: Int,
sbHeight: Int,
- displaySize: Pair<Int, Int>
+ displaySize: Pair<Int, Int>,
): Rect {
val w = displaySize.first
val h = displaySize.second
@@ -626,7 +661,7 @@ private fun shareShortEdge(
sbRect: Rect,
cutoutRect: Rect,
currentWidth: Int,
- currentHeight: Int
+ currentHeight: Int,
): Boolean {
if (currentWidth < currentHeight) {
// Check top/bottom edges by extending the width of the display cutout rect and checking
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt
index 7d0dadcf8c6e..7a88dcd92b88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt
@@ -17,78 +17,40 @@
package com.android.systemui.statusbar.window
import android.content.Context
-import android.view.Display
import android.view.WindowManager
import com.android.app.viewcapture.ViewCaptureAwareWindowManager
-import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
+import com.android.systemui.display.data.repository.PerDisplayStore
+import com.android.systemui.display.data.repository.PerDisplayStoreImpl
+import com.android.systemui.display.data.repository.SingleDisplayStore
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
-import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
/** Store that allows to retrieve per display instances of [StatusBarWindowController]. */
-interface StatusBarWindowControllerStore {
- /**
- * The instance for the default/main display of the device. For example, on a phone or a tablet,
- * the default display is the internal/built-in display of the device.
- *
- * Note that the id of the default display is [Display.DEFAULT_DISPLAY].
- */
- val defaultDisplay: StatusBarWindowController
-
- /**
- * Returns an instance for a specific display id.
- *
- * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing
- * displays.
- */
- fun forDisplay(displayId: Int): StatusBarWindowController
-}
+interface StatusBarWindowControllerStore : PerDisplayStore<StatusBarWindowController>
@SysUISingleton
class MultiDisplayStatusBarWindowControllerStore
@Inject
constructor(
- @Background private val backgroundApplicationScope: CoroutineScope,
+ @Background backgroundApplicationScope: CoroutineScope,
private val controllerFactory: StatusBarWindowController.Factory,
private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository,
private val viewCaptureAwareWindowManagerFactory: ViewCaptureAwareWindowManager.Factory,
- private val displayRepository: DisplayRepository,
-) : StatusBarWindowControllerStore, CoreStartable {
+ displayRepository: DisplayRepository,
+) :
+ StatusBarWindowControllerStore,
+ PerDisplayStoreImpl<StatusBarWindowController>(backgroundApplicationScope, displayRepository) {
init {
StatusBarConnectedDisplays.assertInNewMode()
}
- private val perDisplayControllers = ConcurrentHashMap<Int, StatusBarWindowController>()
-
- override fun start() {
- backgroundApplicationScope.launch(CoroutineName("StatusBarWindowController#start")) {
- displayRepository.displayRemovalEvent.collect { displayId ->
- perDisplayControllers.remove(displayId)
- }
- }
- }
-
- override val defaultDisplay: StatusBarWindowController
- get() = forDisplay(Display.DEFAULT_DISPLAY)
-
- override fun forDisplay(displayId: Int): StatusBarWindowController {
- if (displayRepository.getDisplay(displayId) == null) {
- throw IllegalArgumentException("Display with id $displayId doesn't exist.")
- }
- return perDisplayControllers.computeIfAbsent(displayId) {
- createControllerForDisplay(displayId)
- }
- }
-
- private fun createControllerForDisplay(displayId: Int): StatusBarWindowController {
+ override fun createInstanceForDisplay(displayId: Int): StatusBarWindowController {
val statusBarDisplayContext =
displayWindowPropertiesRepository.get(
displayId = displayId,
@@ -101,6 +63,8 @@ constructor(
viewCaptureAwareWindowManager,
)
}
+
+ override val instanceClass = StatusBarWindowController::class.java
}
@SysUISingleton
@@ -110,16 +74,13 @@ constructor(
context: Context,
viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
factory: StatusBarWindowControllerImpl.Factory,
-) : StatusBarWindowControllerStore {
+) :
+ StatusBarWindowControllerStore,
+ PerDisplayStore<StatusBarWindowController> by SingleDisplayStore(
+ factory.create(context, viewCaptureAwareWindowManager)
+ ) {
init {
StatusBarConnectedDisplays.assertInLegacyMode()
}
-
- private val controller: StatusBarWindowController =
- factory.create(context, viewCaptureAwareWindowManager)
-
- override val defaultDisplay = controller
-
- override fun forDisplay(displayId: Int) = controller
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
index 75c66f234bdc..90c005139c56 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
@@ -67,7 +67,7 @@ class DistanceBasedGestureRecognizerProvider(
val distanceThresholdPx =
resources.getDimensionPixelSize(
com.android.internal.R.dimen.system_gestures_distance_threshold
- )
+ ) * 5
return remember(distanceThresholdPx) {
recognizerFactory(distanceThresholdPx, gestureStateChangedCallback)
}
@@ -77,7 +77,8 @@ class DistanceBasedGestureRecognizerProvider(
fun GestureState.toTutorialActionState(): TutorialActionState {
return when (this) {
NotStarted -> TutorialActionState.NotStarted
- is InProgress -> TutorialActionState.InProgress(progress)
+ // progress is disabled for now as views are not ready to handle varying progress
+ is InProgress -> TutorialActionState.InProgress(0f)
Finished -> TutorialActionState.Finished
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
index 56e97a357d67..80f800390852 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
@@ -16,10 +16,14 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
+import android.util.MathUtils
import android.view.MotionEvent
import kotlin.math.abs
-/** Recognizes touchpad back gesture, that is three fingers swiping left or right */
+/**
+ * Recognizes touchpad back gesture, that is - using three fingers on touchpad - swiping left or
+ * right.
+ */
class BackGestureRecognizer(private val gestureDistanceThresholdPx: Int) : GestureRecognizer {
private val distanceTracker = DistanceTracker()
@@ -36,7 +40,7 @@ class BackGestureRecognizer(private val gestureDistanceThresholdPx: Int) : Gestu
gestureStateChangedCallback,
gestureState,
isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx },
- progress = { 0f },
+ progress = { MathUtils.saturate(abs(it.deltaX / gestureDistanceThresholdPx)) },
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
index 3db9d7ccc8f7..2b84a4c50613 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
@@ -16,9 +16,10 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
+import android.util.MathUtils
import android.view.MotionEvent
-/** Recognizes touchpad home gesture, that is three fingers swiping up */
+/** Recognizes touchpad home gesture, that is - using three fingers on touchpad - swiping up. */
class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : GestureRecognizer {
private val distanceTracker = DistanceTracker()
@@ -35,7 +36,7 @@ class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : Gestu
gestureStateChangedCallback,
gestureState,
isFinished = { -it.deltaY >= gestureDistanceThresholdPx },
- progress = { 0f },
+ progress = { MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx) },
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
index a194ad6a8016..69b7c5edd750 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
@@ -16,13 +16,14 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
+import android.util.MathUtils
import android.view.MotionEvent
import kotlin.math.abs
/**
- * Recognizes apps gesture completion. That is - using three fingers on touchpad - swipe up over
- * some distance threshold and then slow down gesture before fingers are lifted. Implementation is
- * based on [com.android.quickstep.util.TriggerSwipeUpTouchTracker]
+ * Recognizes recent apps gesture, that is - using three fingers on touchpad - swipe up over some
+ * distance threshold and then slow down gesture before fingers are lifted. Implementation is based
+ * on [com.android.quickstep.util.TriggerSwipeUpTouchTracker]
*/
class RecentAppsGestureRecognizer(
private val gestureDistanceThresholdPx: Int,
@@ -49,7 +50,7 @@ class RecentAppsGestureRecognizer(
-state.deltaY >= gestureDistanceThresholdPx &&
abs(velocityTracker.calculateVelocity().value) <= velocityThresholdPxPerMs
},
- progress = { 0f },
+ progress = { MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx) },
)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
index 8b427fbc5fb8..071acfa44650 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
@@ -156,7 +156,7 @@ class TransitionAnimatorTest(val useSpring: Boolean) : SysuiTestCase() {
createEndState(transitionContainer),
backgroundLayer,
fadeWindowBackgroundLayer,
- useSpring,
+ useSpring = useSpring,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarInitializerStoreTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarInitializerStoreTest.kt
deleted file mode 100644
index 0d1d37af7e5b..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarInitializerStoreTest.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.core
-
-import android.platform.test.annotations.EnableFlags
-import android.view.Display
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.display.data.repository.displayRepository
-import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
-class MultiDisplayStatusBarInitializerStoreTest : SysuiTestCase() {
-
- private val kosmos =
- testKosmos().also {
- // Using unconfinedTestDispatcher to avoid having to call `runCurrent` in the tests.
- it.testDispatcher = it.unconfinedTestDispatcher
- }
- private val testScope = kosmos.testScope
- private val fakeDisplayRepository = kosmos.displayRepository
- private val store = kosmos.multiDisplayStatusBarInitializerStore
-
- @Before
- fun start() {
- store.start()
- }
-
- @Before
- fun addDisplays() = runBlocking {
- fakeDisplayRepository.addDisplay(DEFAULT_DISPLAY_ID)
- fakeDisplayRepository.addDisplay(NON_DEFAULT_DISPLAY_ID)
- }
-
- @Test
- fun forDisplay_defaultDisplay_multipleCalls_returnsSameInstance() =
- testScope.runTest {
- val controller = store.defaultDisplay
-
- assertThat(store.defaultDisplay).isSameInstanceAs(controller)
- }
-
- @Test
- fun forDisplay_nonDefaultDisplay_multipleCalls_returnsSameInstance() =
- testScope.runTest {
- val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
-
- assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isSameInstanceAs(controller)
- }
-
- @Test
- fun forDisplay_nonDefaultDisplay_afterDisplayRemoved_returnsNewInstance() =
- testScope.runTest {
- val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
-
- fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID)
- fakeDisplayRepository.addDisplay(NON_DEFAULT_DISPLAY_ID)
-
- assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isNotSameInstanceAs(controller)
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun forDisplay_nonExistingDisplayId_throws() =
- testScope.runTest { store.forDisplay(NON_EXISTING_DISPLAY_ID) }
-
- companion object {
- private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
- private const val NON_DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY + 1
- private const val NON_EXISTING_DISPLAY_ID = Display.DEFAULT_DISPLAY + 2
- }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt
index 3087d01a2479..77ec83871016 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt
@@ -23,6 +23,7 @@ import com.android.internal.logging.metricsLogger
import com.android.internal.util.emergencyAffordanceManager
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.bouncer.data.repository.emergencyServicesRepository
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
@@ -52,5 +53,6 @@ val Kosmos.bouncerActionButtonInteractor by Fixture {
metricsLogger = metricsLogger,
dozeLogger = mock(),
sceneInteractor = { sceneInteractor },
+ bouncerHapticPlayer,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt
new file mode 100644
index 000000000000..e3797260ed6d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import kotlinx.coroutines.CoroutineScope
+
+class FakePerDisplayStore(
+ backgroundApplicationScope: CoroutineScope,
+ displayRepository: DisplayRepository,
+) : PerDisplayStoreImpl<TestPerDisplayInstance>(backgroundApplicationScope, displayRepository) {
+
+ val removalActions = mutableListOf<TestPerDisplayInstance>()
+
+ override fun createInstanceForDisplay(displayId: Int): TestPerDisplayInstance {
+ return TestPerDisplayInstance(displayId)
+ }
+
+ override val instanceClass = TestPerDisplayInstance::class.java
+
+ override suspend fun onDisplayRemovalAction(instance: TestPerDisplayInstance) {
+ removalActions += instance
+ }
+}
+
+data class TestPerDisplayInstance(val displayId: Int)
+
+val Kosmos.fakePerDisplayStore by
+ Kosmos.Fixture {
+ FakePerDisplayStore(
+ backgroundApplicationScope = applicationCoroutineScope,
+ displayRepository = displayRepository,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt
index 8066b9138c99..303529b7f7b0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt
@@ -34,8 +34,8 @@ val Kosmos.multiDisplayStatusBarInitializerStore by
Kosmos.Fixture {
MultiDisplayStatusBarInitializerStore(
applicationCoroutineScope,
- fakeStatusBarInitializerFactory,
displayRepository,
+ fakeStatusBarInitializerFactory,
fakeStatusBarWindowControllerStore,
)
}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index 24950e65c174..afa7a6c51abb 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -225,14 +225,9 @@ public class RavenwoodRuntimeEnvironmentController {
ActivityManager.init$ravenwood(config.mCurrentUser);
- final HandlerThread main;
- if (config.mProvideMainThread) {
- main = new HandlerThread(MAIN_THREAD_NAME);
- main.start();
- Looper.setMainLooperForTest(main.getLooper());
- } else {
- main = null;
- }
+ final var main = new HandlerThread(MAIN_THREAD_NAME);
+ main.start();
+ Looper.setMainLooperForTest(main.getLooper());
final boolean isSelfInstrumenting =
Objects.equals(config.mTestPackageName, config.mTargetPackageName);
@@ -324,10 +319,8 @@ public class RavenwoodRuntimeEnvironmentController {
}
sMockUiAutomation.dropShellPermissionIdentity();
- if (config.mProvideMainThread) {
- Looper.getMainLooper().quit();
- Looper.clearMainLooperForTest();
- }
+ Looper.getMainLooper().quit();
+ Looper.clearMainLooperForTest();
ActivityManager.reset$ravenwood();
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
index 446f819ad41b..1f6e11dd5cf2 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
@@ -152,7 +152,10 @@ public final class RavenwoodConfig {
/**
* Configure a "main" thread to be available for the duration of the test, as defined
* by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments.
+ *
+ * @deprecated
*/
+ @Deprecated
public Builder setProvideMainThread(boolean provideMainThread) {
mConfig.mProvideMainThread = provideMainThread;
return this;
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 4196d8e22610..93a6806ed1f4 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -139,7 +139,10 @@ public final class RavenwoodRule implements TestRule {
/**
* Configure a "main" thread to be available for the duration of the test, as defined
* by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments.
+ *
+ * @deprecated
*/
+ @Deprecated
public Builder setProvideMainThread(boolean provideMainThread) {
mBuilder.setProvideMainThread(provideMainThread);
return this;
diff --git a/ravenwood/tests/bivalenttest/Android.bp b/ravenwood/tests/bivalenttest/Android.bp
index ac499b966afe..d7f4b3e2955d 100644
--- a/ravenwood/tests/bivalenttest/Android.bp
+++ b/ravenwood/tests/bivalenttest/Android.bp
@@ -54,34 +54,36 @@ android_ravenwood_test {
auto_gen_config: true,
}
-// TODO(b/371215487): migrate bivalenttest.ravenizer tests to another architecture
+android_test {
+ name: "RavenwoodBivalentTest_device",
-// android_test {
-// name: "RavenwoodBivalentTest_device",
-//
-// srcs: [
-// "test/**/*.java",
-// ],
-// static_libs: [
-// "junit",
-// "truth",
-//
-// "androidx.annotation_annotation",
-// "androidx.test.ext.junit",
-// "androidx.test.rules",
-//
-// "junit-params",
-// "platform-parametric-runner-lib",
-//
-// "ravenwood-junit",
-// ],
-// jni_libs: [
-// "libravenwoodbivalenttest_jni",
-// ],
-// test_suites: [
-// "device-tests",
-// ],
-// optimize: {
-// enabled: false,
-// },
-// }
+ srcs: [
+ "test/**/*.java",
+ ],
+ // TODO(b/371215487): migrate bivalenttest.ravenizer tests to another architecture
+ exclude_srcs: [
+ "test/**/ravenizer/*.java",
+ ],
+ static_libs: [
+ "junit",
+ "truth",
+
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+
+ "junit-params",
+ "platform-parametric-runner-lib",
+
+ "ravenwood-junit",
+ ],
+ jni_libs: [
+ "libravenwoodbivalenttest_jni",
+ ],
+ test_suites: [
+ "device-tests",
+ ],
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/services/Android.bp b/services/Android.bp
index 653cd3c3b680..f04c692c12d0 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -195,7 +195,17 @@ soong_config_module_type {
module_type: "java_library",
config_namespace: "system_services",
bool_variables: ["without_vibrator"],
- properties: ["vintf_fragments"],
+ properties: ["vintf_fragment_modules"],
+}
+
+vintf_fragment {
+ name: "manifest_services.xml",
+ src: "manifest_services.xml",
+}
+
+vintf_fragment {
+ name: "manifest_services_android.frameworks.vibrator.xml",
+ src: "manifest_services_android.frameworks.vibrator.xml",
}
system_java_library {
@@ -264,11 +274,11 @@ system_java_library {
soong_config_variables: {
without_vibrator: {
- vintf_fragments: [
+ vintf_fragment_modules: [
"manifest_services.xml",
],
conditions_default: {
- vintf_fragments: [
+ vintf_fragment_modules: [
"manifest_services.xml",
"manifest_services_android.frameworks.vibrator.xml",
],
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 034127c0420e..7057cc361a1a 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -45,6 +45,16 @@ flag {
}
flag {
+ name: "clear_shortcuts_when_activity_updates_to_service"
+ namespace: "accessibility"
+ description: "When an a11y activity is updated to an a11y service, clears the associated shortcuts so that we don't skip the AccessibilityServiceWarning."
+ bug: "358092445"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "compute_window_changes_on_a11y_v2"
namespace: "accessibility"
description: "Computes accessibility window changes in accessibility instead of wm package."
@@ -114,10 +124,13 @@ flag {
}
flag {
- name: "enable_magnification_follows_mouse"
+ name: "enable_magnification_follows_mouse_bugfix"
namespace: "accessibility"
description: "Whether to enable mouse following for fullscreen magnification"
bug: "354696546"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
flag {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 1451dfaa7964..ec8908bc7c91 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2513,6 +2513,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private boolean readInstalledAccessibilityShortcutLocked(AccessibilityUserState userState,
List<AccessibilityShortcutInfo> parsedAccessibilityShortcutInfos) {
if (!parsedAccessibilityShortcutInfos.equals(userState.mInstalledShortcuts)) {
+ if (Flags.clearShortcutsWhenActivityUpdatesToService()) {
+ List<String> componentNames = userState.mInstalledShortcuts.stream()
+ .filter(a11yActivity ->
+ !parsedAccessibilityShortcutInfos.contains(a11yActivity))
+ .map(a11yActivity -> a11yActivity.getComponentName().flattenToString())
+ .toList();
+ if (!componentNames.isEmpty()) {
+ enableShortcutsForTargets(
+ /* enable= */ false, UserShortcutType.ALL,
+ componentNames, userState.mUserId);
+ }
+ }
+
userState.mInstalledShortcuts.clear();
userState.mInstalledShortcuts.addAll(parsedAccessibilityShortcutInfos);
userState.updateTileServiceMapForAccessibilityActivityLocked();
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index a19fdddea49c..963334b07ea6 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -345,7 +345,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
@Override
void handleMouseOrStylusEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
- if (Flags.enableMagnificationFollowsMouse()) {
+ if (Flags.enableMagnificationFollowsMouseBugfix()) {
if (mFullScreenMagnificationController.isActivated(mDisplayId)) {
// TODO(b/354696546): Allow mouse/stylus to activate whichever display they are
// over, rather than only interacting with the current display.
@@ -1206,7 +1206,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
protected void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
int policyFlags) {
- if (Flags.enableMagnificationFollowsMouse()
+ if (Flags.enableMagnificationFollowsMouseBugfix()
&& !event.isFromSource(SOURCE_TOUCHSCREEN)) {
// Only touch events need to be cached and sent later.
return;
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java
index 446123f07f64..fa86ba39bb1a 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java
@@ -146,7 +146,8 @@ public abstract class MagnificationGestureHandler extends BaseEventStreamTransfo
} break;
case SOURCE_MOUSE:
case SOURCE_STYLUS: {
- if (magnificationShortcutExists() && Flags.enableMagnificationFollowsMouse()) {
+ if (magnificationShortcutExists()
+ && Flags.enableMagnificationFollowsMouseBugfix()) {
handleMouseOrStylusEvent(event, rawEvent, policyFlags);
}
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 28e57775523b..89f14b09d397 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -68,7 +68,10 @@ import com.android.server.SystemService.TargetUser;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.Map;
import java.util.Objects;
+import java.util.WeakHashMap;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
@@ -81,7 +84,8 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
private final ServiceHelper mInternalServiceHelper;
private final ServiceConfig mServiceConfig;
private final Context mContext;
- private final Object mLock = new Object();
+ private final Map<String, Object> mLocks = new WeakHashMap<>();
+
public AppFunctionManagerServiceImpl(@NonNull Context context) {
this(
@@ -316,9 +320,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
THREAD_POOL_EXECUTOR.execute(
() -> {
try {
- // TODO(357551503): Instead of holding a global lock, hold a per-package
- // lock.
- synchronized (mLock) {
+ synchronized (getLockForPackage(callingPackage)) {
setAppFunctionEnabledInternalLocked(
callingPackage, functionIdentifier, userHandle, enabledState);
}
@@ -346,7 +348,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
* process.
*/
@WorkerThread
- @GuardedBy("mLock")
+ @GuardedBy("getLockForPackage(callingPackage)")
private void setAppFunctionEnabledInternalLocked(
@NonNull String callingPackage,
@NonNull String functionIdentifier,
@@ -541,6 +543,26 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
});
}
}
+ /**
+ * Retrieves the lock object associated with the given package name.
+ *
+ * This method returns the lock object from the {@code mLocks} map if it exists.
+ * If no lock is found for the given package name, a new lock object is created,
+ * stored in the map, and returned.
+ */
+ @VisibleForTesting
+ @NonNull
+ Object getLockForPackage(String callingPackage) {
+ // Synchronized the access to mLocks to prevent race condition.
+ synchronized (mLocks) {
+ // By using a WeakHashMap, we allow the garbage collector to reclaim memory by removing
+ // entries associated with unused callingPackage keys. Therefore, we remove the null
+ // values before getting/computing a new value. The goal is to not let the size of this
+ // map grow without an upper bound.
+ mLocks.values().removeAll(Collections.singleton(null)); // Remove null values
+ return mLocks.computeIfAbsent(callingPackage, k -> new Object());
+ }
+ }
private static class AppFunctionMetadataObserver implements ObserverCallback {
@Nullable private final MetadataSyncAdapter mPerUserMetadataSyncAdapter;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 7f1d912d9a79..746c55f8fc9d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -5542,7 +5542,6 @@ public class ActivityManagerService extends IActivityManager.Stub
public int sendIntentSender(IApplicationThread caller, IIntentSender target,
IBinder allowlistToken, int code, Intent intent, String resolvedType,
IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
- addCreatorToken(intent);
if (target instanceof PendingIntentRecord) {
final PendingIntentRecord originalRecord = (PendingIntentRecord) target;
@@ -5584,19 +5583,23 @@ public class ActivityManagerService extends IActivityManager.Stub
intent = new Intent(Intent.ACTION_MAIN);
}
try {
+ final int callingUid = Binder.getCallingUid();
+ final String packageName;
+ final long token = Binder.clearCallingIdentity();
+ try {
+ packageName = AppGlobals.getPackageManager().getNameForUid(callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
if (allowlistToken != null) {
- final int callingUid = Binder.getCallingUid();
- final String packageName;
- final long token = Binder.clearCallingIdentity();
- try {
- packageName = AppGlobals.getPackageManager().getNameForUid(callingUid);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
Slog.wtf(TAG, "Send a non-null allowlistToken to a non-PI target."
+ " Calling package: " + packageName + "; intent: " + intent
+ "; options: " + options);
}
+
+ addCreatorToken(intent, packageName);
+
target.send(code, intent, resolvedType, null, null,
requiredPermission, options);
} catch (RemoteException e) {
@@ -12371,7 +12374,7 @@ public class ActivityManagerService extends IActivityManager.Stub
continue;
}
endTime = SystemClock.currentThreadTimeMillis();
- hasSwapPss = mi.hasSwappedOutPss;
+ hasSwapPss = hasSwapPss || mi.hasSwappedOutPss;
memtrackGraphics = mi.getOtherPrivate(Debug.MemoryInfo.OTHER_GRAPHICS);
memtrackGl = mi.getOtherPrivate(Debug.MemoryInfo.OTHER_GL);
} else {
@@ -13049,7 +13052,7 @@ public class ActivityManagerService extends IActivityManager.Stub
continue;
}
endTime = SystemClock.currentThreadTimeMillis();
- hasSwapPss = mi.hasSwappedOutPss;
+ hasSwapPss = hasSwapPss || mi.hasSwappedOutPss;
} else {
reportType = ProcessStats.ADD_PSS_EXTERNAL;
startTime = SystemClock.currentThreadTimeMillis();
@@ -13628,7 +13631,7 @@ public class ActivityManagerService extends IActivityManager.Stub
throws TransactionTooLargeException {
enforceNotIsolatedCaller("startService");
enforceAllowedToStartOrBindServiceIfSdkSandbox(service);
- addCreatorToken(service);
+ addCreatorToken(service, callingPackage);
if (service != null) {
// Refuse possible leaked file descriptors
if (service.hasFileDescriptors()) {
@@ -13890,7 +13893,7 @@ public class ActivityManagerService extends IActivityManager.Stub
validateServiceInstanceName(instanceName);
- addCreatorToken(service);
+ addCreatorToken(service, callingPackage);
try {
if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
final ComponentName cn = service.getComponent();
@@ -17174,7 +17177,7 @@ public class ActivityManagerService extends IActivityManager.Stub
Slog.v(TAG_SERVICE,
"startServiceInPackage: " + service + " type=" + resolvedType);
}
- addCreatorToken(service);
+ addCreatorToken(service, callingPackage);
final long origId = Binder.clearCallingIdentity();
ComponentName res;
try {
@@ -18002,8 +18005,8 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public void addCreatorToken(Intent intent) {
- ActivityManagerService.this.addCreatorToken(intent);
+ public void addCreatorToken(Intent intent, String creatorPackage) {
+ ActivityManagerService.this.addCreatorToken(intent, creatorPackage);
}
}
@@ -19160,9 +19163,9 @@ public class ActivityManagerService extends IActivityManager.Stub
private final Key mKeyFields;
private final WeakReference<IntentCreatorToken> mRef;
- public IntentCreatorToken(int creatorUid, Intent intent) {
+ public IntentCreatorToken(int creatorUid, String creatorPackage, Intent intent) {
super();
- this.mKeyFields = new Key(creatorUid, intent);
+ this.mKeyFields = new Key(creatorUid, creatorPackage, intent);
mRef = new WeakReference<>(this);
}
@@ -19170,7 +19173,10 @@ public class ActivityManagerService extends IActivityManager.Stub
return mKeyFields.mCreatorUid;
}
- /** {@hide} */
+ public String getCreatorPackage() {
+ return mKeyFields.mCreatorPackage;
+ }
+
public static boolean isValid(@NonNull Intent intent) {
IBinder binder = intent.getCreatorToken();
IntentCreatorToken token = null;
@@ -19178,7 +19184,8 @@ public class ActivityManagerService extends IActivityManager.Stub
token = (IntentCreatorToken) binder;
}
return token != null && token.mKeyFields.equals(
- new Key(token.mKeyFields.mCreatorUid, intent));
+ new Key(token.mKeyFields.mCreatorUid, token.mKeyFields.mCreatorPackage,
+ intent));
}
@Override
@@ -19202,8 +19209,9 @@ public class ActivityManagerService extends IActivityManager.Stub
}
private static class Key {
- private Key(int creatorUid, Intent intent) {
+ private Key(int creatorUid, String creatorPackage, Intent intent) {
this.mCreatorUid = creatorUid;
+ this.mCreatorPackage = creatorPackage;
this.mAction = intent.getAction();
this.mData = intent.getData();
this.mType = intent.getType();
@@ -19220,6 +19228,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
private final int mCreatorUid;
+ private final String mCreatorPackage;
private final String mAction;
private final Uri mData;
private final String mType;
@@ -19233,17 +19242,20 @@ public class ActivityManagerService extends IActivityManager.Stub
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
- return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags && Objects.equals(
- mAction, key.mAction) && Objects.equals(mData, key.mData)
- && Objects.equals(mType, key.mType) && Objects.equals(mPackage,
- key.mPackage) && Objects.equals(mComponent, key.mComponent)
+ return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags
+ && Objects.equals(mCreatorPackage, key.mCreatorPackage)
+ && Objects.equals(mAction, key.mAction)
+ && Objects.equals(mData, key.mData)
+ && Objects.equals(mType, key.mType)
+ && Objects.equals(mPackage, key.mPackage)
+ && Objects.equals(mComponent, key.mComponent)
&& Objects.equals(mClipDataUris, key.mClipDataUris);
}
@Override
public int hashCode() {
- return Objects.hash(mCreatorUid, mAction, mData, mType, mPackage, mComponent,
- mFlags, mClipDataUris);
+ return Objects.hash(mCreatorUid, mCreatorPackage, mAction, mData, mType, mPackage,
+ mComponent, mFlags, mClipDataUris);
}
}
}
@@ -19254,7 +19266,7 @@ public class ActivityManagerService extends IActivityManager.Stub
* @param intent The given intent
* @hide
*/
- public void addCreatorToken(@Nullable Intent intent) {
+ public void addCreatorToken(@Nullable Intent intent, String creatorPackage) {
if (!preventIntentRedirect()) return;
if (intent == null || intent.getExtraIntentKeys() == null) return;
@@ -19267,7 +19279,7 @@ public class ActivityManagerService extends IActivityManager.Stub
continue;
}
Slog.wtf(TAG, "A creator token is added to an intent.");
- IBinder creatorToken = createIntentCreatorToken(extraIntent);
+ IBinder creatorToken = createIntentCreatorToken(extraIntent, creatorPackage);
if (creatorToken != null) {
extraIntent.setCreatorToken(creatorToken);
}
@@ -19280,15 +19292,15 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
- private IBinder createIntentCreatorToken(Intent intent) {
+ private IBinder createIntentCreatorToken(Intent intent, String creatorPackage) {
if (IntentCreatorToken.isValid(intent)) return null;
int creatorUid = getCallingUid();
- IntentCreatorToken.Key key = new IntentCreatorToken.Key(creatorUid, intent);
+ IntentCreatorToken.Key key = new IntentCreatorToken.Key(creatorUid, creatorPackage, intent);
IntentCreatorToken token;
synchronized (sIntentCreatorTokenCache) {
WeakReference<IntentCreatorToken> ref = sIntentCreatorTokenCache.get(key);
if (ref == null || ref.get() == null) {
- token = new IntentCreatorToken(creatorUid, intent);
+ token = new IntentCreatorToken(creatorUid, creatorPackage, intent);
sIntentCreatorTokenCache.put(key, token.mRef);
} else {
token = ref.get();
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index 15f1085b7125..a00cac6aba4f 100644
--- a/services/core/java/com/android/server/am/BroadcastController.java
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -258,6 +258,7 @@ class BroadcastController {
final StringBuilder sb = new StringBuilder("registerReceiver: ");
sb.append(Binder.getCallingUid()); sb.append('/');
sb.append(receiverId == null ? "null" : receiverId); sb.append('/');
+ sb.append("p:"); sb.append(filter.getPriority()); sb.append('/');
final int actionsCount = filter.safeCountActions();
if (actionsCount > 0) {
for (int i = 0; i < actionsCount; ++i) {
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 776a3455acc4..f60ee66cb236 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -3283,7 +3283,12 @@ public class OomAdjuster {
baseCapabilities = PROCESS_CAPABILITY_ALL; // BFSL allowed
break;
case PROCESS_STATE_BOUND_TOP:
- baseCapabilities = PROCESS_CAPABILITY_BFSL;
+ if (app.getActiveInstrumentation() != null) {
+ baseCapabilities = PROCESS_CAPABILITY_BFSL |
+ PROCESS_CAPABILITY_ALL_IMPLICIT;
+ } else {
+ baseCapabilities = PROCESS_CAPABILITY_BFSL;
+ }
break;
case PROCESS_STATE_FOREGROUND_SERVICE:
if (app.getActiveInstrumentation() != null) {
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 6857b6bcde15..3fb06a75d79f 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -432,6 +432,14 @@ public final class PendingIntentRecord extends IIntentSender.Stub {
}
}
+ /**
+ * get package name of the PendingIntent sender.
+ * @return package name of the PendingIntent sender.
+ */
+ public String getPackageName() {
+ return key.packageName;
+ }
+
@Deprecated
public int sendInner(int code, Intent intent, String resolvedType, IBinder allowlistToken,
IIntentReceiver finishedReceiver, String requiredPermission, IBinder resultTo,
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 7831c393844b..cdb01889c139 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -5187,6 +5187,7 @@ public final class ProcessList {
if (ai != null) {
if (ai.packageName.equals(app.info.packageName)) {
app.info = ai;
+ app.getWindowProcessController().updateApplicationInfo(ai);
PlatformCompatCache.getInstance()
.onApplicationInfoChanged(ai);
}
diff --git a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
index 636854b85ee4..d1576c5cca4f 100644
--- a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
+++ b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
@@ -17,118 +17,63 @@
package com.android.server.integrity;
import static android.content.Intent.ACTION_PACKAGE_NEEDS_INTEGRITY_VERIFICATION;
-import static android.content.Intent.EXTRA_LONG_VERSION_CODE;
-import static android.content.Intent.EXTRA_ORIGINATING_UID;
-import static android.content.Intent.EXTRA_PACKAGE_NAME;
import static android.content.integrity.AppIntegrityManager.EXTRA_STATUS;
import static android.content.integrity.AppIntegrityManager.STATUS_FAILURE;
import static android.content.integrity.AppIntegrityManager.STATUS_SUCCESS;
-import static android.content.integrity.InstallerAllowedByManifestFormula.INSTALLER_CERTIFICATE_NOT_EVALUATED;
import static android.content.integrity.IntegrityUtils.getHexDigest;
import static android.content.pm.PackageManager.EXTRA_VERIFICATION_ID;
import android.annotation.BinderThread;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
-import android.content.integrity.AppInstallMetadata;
import android.content.integrity.IAppIntegrityManager;
import android.content.integrity.Rule;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
-import android.content.pm.Signature;
-import android.content.pm.SigningDetails;
-import android.content.pm.parsing.result.ParseResult;
-import android.content.pm.parsing.result.ParseTypeImpl;
import android.net.Uri;
import android.os.Binder;
-import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.provider.Settings;
import android.util.Pair;
import android.util.Slog;
-import android.util.apk.SourceStampVerificationResult;
-import android.util.apk.SourceStampVerifier;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.pm.parsing.PackageParser2;
-import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
-import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
-import com.android.server.integrity.model.IntegrityCheckResult;
import com.android.server.integrity.model.RuleMetadata;
-import com.android.server.pm.PackageManagerServiceUtils;
-import com.android.server.pm.parsing.PackageParserUtils;
-import java.io.ByteArrayInputStream;
import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
-import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
/** Implementation of {@link AppIntegrityManagerService}. */
public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
- /**
- * This string will be used as the "installer" for formula evaluation when the app's installer
- * cannot be determined.
- *
- * <p>This may happen for various reasons. e.g., the installing app's package name may not match
- * its UID.
- */
- private static final String UNKNOWN_INSTALLER = "";
- /**
- * This string will be used as the "installer" for formula evaluation when the app is being
- * installed via ADB.
- */
- public static final String ADB_INSTALLER = "adb";
private static final String TAG = "AppIntegrityManagerServiceImpl";
private static final String PACKAGE_MIME_TYPE = "application/vnd.android.package-archive";
- private static final String BASE_APK_FILE = "base.apk";
- private static final String ALLOWED_INSTALLERS_METADATA_NAME = "allowed-installers";
- private static final String ALLOWED_INSTALLER_DELIMITER = ",";
- private static final String INSTALLER_PACKAGE_CERT_DELIMITER = "\\|";
public static final boolean DEBUG_INTEGRITY_COMPONENT = false;
- private static final Set<String> PACKAGE_INSTALLER =
- new HashSet<>(
- Arrays.asList(
- "com.google.android.packageinstaller", "com.android.packageinstaller"));
-
// Access to files inside mRulesDir is protected by mRulesLock;
private final Context mContext;
private final Handler mHandler;
private final PackageManagerInternal mPackageManagerInternal;
- private final Supplier<PackageParser2> mParserSupplier;
private final IntegrityFileManager mIntegrityFileManager;
/** Create an instance of {@link AppIntegrityManagerServiceImpl}. */
@@ -139,7 +84,6 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
return new AppIntegrityManagerServiceImpl(
context,
LocalServices.getService(PackageManagerInternal.class),
- PackageParserUtils::forParsingFileWithDefaults,
IntegrityFileManager.getInstance(),
handlerThread.getThreadHandler());
}
@@ -148,12 +92,10 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
AppIntegrityManagerServiceImpl(
Context context,
PackageManagerInternal packageManagerInternal,
- Supplier<PackageParser2> parserSupplier,
IntegrityFileManager integrityFileManager,
Handler handler) {
mContext = context;
mPackageManagerInternal = packageManagerInternal;
- mParserSupplier = parserSupplier;
mIntegrityFileManager = integrityFileManager;
mHandler = handler;
@@ -263,148 +205,8 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
private void handleIntegrityVerification(Intent intent) {
int verificationId = intent.getIntExtra(EXTRA_VERIFICATION_ID, -1);
-
- try {
- if (DEBUG_INTEGRITY_COMPONENT) {
- Slog.d(TAG, "Received integrity verification intent " + intent.toString());
- Slog.d(TAG, "Extras " + intent.getExtras());
- }
-
- String installerPackageName = getInstallerPackageName(intent);
-
- // Skip integrity verification if the verifier is doing the install.
- if (!integrityCheckIncludesRuleProvider() && isRuleProvider(installerPackageName)) {
- if (DEBUG_INTEGRITY_COMPONENT) {
- Slog.i(TAG, "Verifier doing the install. Skipping integrity check.");
- }
- mPackageManagerInternal.setIntegrityVerificationResult(
- verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW);
- return;
- }
-
- String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
-
- Pair<SigningDetails, Bundle> packageSigningAndMetadata =
- getPackageSigningAndMetadata(intent.getData());
- if (packageSigningAndMetadata == null) {
- Slog.w(TAG, "Cannot parse package " + packageName);
- // We can't parse the package.
- mPackageManagerInternal.setIntegrityVerificationResult(
- verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW);
- return;
- }
-
- var signingDetails = packageSigningAndMetadata.first;
- List<String> appCertificates = getCertificateFingerprint(packageName, signingDetails);
- List<String> appCertificateLineage = getCertificateLineage(packageName, signingDetails);
- List<String> installerCertificates =
- getInstallerCertificateFingerprint(installerPackageName);
-
- AppInstallMetadata.Builder builder = new AppInstallMetadata.Builder();
-
- builder.setPackageName(getPackageNameNormalized(packageName));
- builder.setAppCertificates(appCertificates);
- builder.setAppCertificateLineage(appCertificateLineage);
- builder.setVersionCode(intent.getLongExtra(EXTRA_LONG_VERSION_CODE, -1));
- builder.setInstallerName(getPackageNameNormalized(installerPackageName));
- builder.setInstallerCertificates(installerCertificates);
- builder.setIsPreInstalled(isSystemApp(packageName));
-
- Map<String, String> allowedInstallers =
- getAllowedInstallers(packageSigningAndMetadata.second);
- builder.setAllowedInstallersAndCert(allowedInstallers);
- extractSourceStamp(intent.getData(), builder);
-
- AppInstallMetadata appInstallMetadata = builder.build();
-
- if (DEBUG_INTEGRITY_COMPONENT) {
- Slog.i(
- TAG,
- "To be verified: "
- + appInstallMetadata
- + " installers "
- + allowedInstallers);
- }
- IntegrityCheckResult result = IntegrityCheckResult.allow();
- if (!result.getMatchedRules().isEmpty() || DEBUG_INTEGRITY_COMPONENT) {
- Slog.i(
- TAG,
- String.format(
- "Integrity check of %s result: %s due to %s",
- packageName, result.getEffect(), result.getMatchedRules()));
- }
-
- mPackageManagerInternal.setIntegrityVerificationResult(
- verificationId,
- result.getEffect() == IntegrityCheckResult.Effect.ALLOW
- ? PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW
- : PackageManagerInternal.INTEGRITY_VERIFICATION_REJECT);
- } catch (IllegalArgumentException e) {
- // This exception indicates something is wrong with the input passed by package manager.
- // e.g., someone trying to trick the system. We block installs in this case.
- Slog.e(TAG, "Invalid input to integrity verification", e);
- mPackageManagerInternal.setIntegrityVerificationResult(
- verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_REJECT);
- } catch (Exception e) {
- // Other exceptions indicate an error within the integrity component implementation and
- // we allow them.
- Slog.e(TAG, "Error handling integrity verification", e);
- mPackageManagerInternal.setIntegrityVerificationResult(
- verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW);
- }
- }
-
- /**
- * Verify the UID and return the installer package name.
- *
- * @return the package name of the installer, or null if it cannot be determined or it is
- * installed via adb.
- */
- @Nullable
- private String getInstallerPackageName(Intent intent) {
- String installer =
- intent.getStringExtra(PackageManager.EXTRA_VERIFICATION_INSTALLER_PACKAGE);
- if (PackageManagerServiceUtils.isInstalledByAdb(installer)) {
- return ADB_INSTALLER;
- }
- int installerUid = intent.getIntExtra(PackageManager.EXTRA_VERIFICATION_INSTALLER_UID, -1);
- if (installerUid < 0) {
- Slog.e(
- TAG,
- "Installer cannot be determined: installer: "
- + installer
- + " installer UID: "
- + installerUid);
- return UNKNOWN_INSTALLER;
- }
-
- // Verify that the installer UID actually contains the package. Note that comparing UIDs
- // is not safe since context's uid can change in different settings; e.g. Android Auto.
- if (!getPackageListForUid(installerUid).contains(installer)) {
- return UNKNOWN_INSTALLER;
- }
-
- // At this time we can trust "installer".
-
- // A common way for apps to install packages is to send an intent to PackageInstaller. In
- // that case, the installer will always show up as PackageInstaller which is not what we
- // want.
- if (PACKAGE_INSTALLER.contains(installer)) {
- int originatingUid = intent.getIntExtra(EXTRA_ORIGINATING_UID, -1);
- if (originatingUid < 0) {
- Slog.e(TAG, "Installer is package installer but originating UID not found.");
- return UNKNOWN_INSTALLER;
- }
- List<String> installerPackages = getPackageListForUid(originatingUid);
- if (installerPackages.isEmpty()) {
- Slog.e(TAG, "No package found associated with originating UID " + originatingUid);
- return UNKNOWN_INSTALLER;
- }
- // In the case of multiple package sharing a UID, we just return the first one.
- return installerPackages.get(0);
- }
-
- return installer;
+ mPackageManagerInternal.setIntegrityVerificationResult(
+ verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW);
}
/** We will use the SHA256 digest of a package name if it is more than 32 bytes long. */
@@ -422,264 +224,6 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
}
}
- private List<String> getInstallerCertificateFingerprint(String installer) {
- if (installer.equals(ADB_INSTALLER) || installer.equals(UNKNOWN_INSTALLER)) {
- return Collections.emptyList();
- }
- var installerPkg = mPackageManagerInternal.getPackage(installer);
- if (installerPkg == null) {
- Slog.w(TAG, "Installer package " + installer + " not found.");
- return Collections.emptyList();
- }
- return getCertificateFingerprint(installerPkg.getPackageName(),
- installerPkg.getSigningDetails());
- }
-
- private List<String> getCertificateFingerprint(@NonNull String packageName,
- @NonNull SigningDetails signingDetails) {
- ArrayList<String> certificateFingerprints = new ArrayList();
- for (Signature signature : getSignatures(packageName, signingDetails)) {
- certificateFingerprints.add(getFingerprint(signature));
- }
- return certificateFingerprints;
- }
-
- private List<String> getCertificateLineage(@NonNull String packageName,
- @NonNull SigningDetails signingDetails) {
- ArrayList<String> certificateLineage = new ArrayList();
- for (Signature signature : getSignatureLineage(packageName, signingDetails)) {
- certificateLineage.add(getFingerprint(signature));
- }
- return certificateLineage;
- }
-
- /** Get the allowed installers and their associated certificate hashes from <meta-data> tag. */
- private Map<String, String> getAllowedInstallers(@Nullable Bundle metaData) {
- Map<String, String> packageCertMap = new HashMap<>();
- if (metaData != null) {
- String allowedInstallers = metaData.getString(ALLOWED_INSTALLERS_METADATA_NAME);
- if (allowedInstallers != null) {
- // parse the metadata for certs.
- String[] installerCertPairs = allowedInstallers.split(ALLOWED_INSTALLER_DELIMITER);
- for (String packageCertPair : installerCertPairs) {
- String[] packageAndCert =
- packageCertPair.split(INSTALLER_PACKAGE_CERT_DELIMITER);
- if (packageAndCert.length == 2) {
- String packageName = getPackageNameNormalized(packageAndCert[0]);
- String cert = packageAndCert[1];
- packageCertMap.put(packageName, cert);
- } else if (packageAndCert.length == 1) {
- packageCertMap.put(
- getPackageNameNormalized(packageAndCert[0]),
- INSTALLER_CERTIFICATE_NOT_EVALUATED);
- }
- }
- }
- }
-
- return packageCertMap;
- }
-
- /** Extract the source stamp embedded in the APK, if present. */
- private void extractSourceStamp(Uri dataUri, AppInstallMetadata.Builder appInstallMetadata) {
- File installationPath = getInstallationPath(dataUri);
- if (installationPath == null) {
- throw new IllegalArgumentException("Installation path is null, package not found");
- }
-
- SourceStampVerificationResult sourceStampVerificationResult;
- if (installationPath.isDirectory()) {
- try (Stream<Path> filesList = Files.list(installationPath.toPath())) {
- List<String> apkFiles =
- filesList
- .map(path -> path.toAbsolutePath().toString())
- .filter(str -> str.endsWith(".apk"))
- .collect(Collectors.toList());
- sourceStampVerificationResult = SourceStampVerifier.verify(apkFiles);
- } catch (IOException e) {
- throw new IllegalArgumentException("Could not read APK directory");
- }
- } else {
- sourceStampVerificationResult =
- SourceStampVerifier.verify(installationPath.getAbsolutePath());
- }
-
- appInstallMetadata.setIsStampPresent(sourceStampVerificationResult.isPresent());
- appInstallMetadata.setIsStampVerified(sourceStampVerificationResult.isVerified());
- // A verified stamp is set to be trusted.
- appInstallMetadata.setIsStampTrusted(sourceStampVerificationResult.isVerified());
- if (sourceStampVerificationResult.isVerified()) {
- X509Certificate sourceStampCertificate =
- (X509Certificate) sourceStampVerificationResult.getCertificate();
- // Sets source stamp certificate digest.
- try {
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
- byte[] certificateDigest = digest.digest(sourceStampCertificate.getEncoded());
- appInstallMetadata.setStampCertificateHash(getHexDigest(certificateDigest));
- } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
- throw new IllegalArgumentException(
- "Error computing source stamp certificate digest", e);
- }
- }
- }
-
- private static Signature[] getSignatures(@NonNull String packageName,
- @NonNull SigningDetails signingDetails) {
- Signature[] signatures = signingDetails.getSignatures();
- if (signatures == null || signatures.length < 1) {
- throw new IllegalArgumentException("Package signature not found in " + packageName);
- }
-
- // We are only interested in evaluating the active signatures.
- return signatures;
- }
-
- private static Signature[] getSignatureLineage(@NonNull String packageName,
- @NonNull SigningDetails signingDetails) {
- // Obtain the active signatures of the package.
- Signature[] signatureLineage = getSignatures(packageName, signingDetails);
-
- var pastSignatures = signingDetails.getPastSigningCertificates();
- // Obtain the past signatures of the package.
- if (signatureLineage.length == 1 && !ArrayUtils.isEmpty(pastSignatures)) {
- // Merge the signatures and return.
- Signature[] allSignatures =
- new Signature[signatureLineage.length + pastSignatures.length];
- int i;
- for (i = 0; i < signatureLineage.length; i++) {
- allSignatures[i] = signatureLineage[i];
- }
- for (int j = 0; j < pastSignatures.length; j++) {
- allSignatures[i] = pastSignatures[j];
- i++;
- }
- signatureLineage = allSignatures;
- }
-
- return signatureLineage;
- }
-
- private static String getFingerprint(Signature cert) {
- InputStream input = new ByteArrayInputStream(cert.toByteArray());
-
- CertificateFactory factory;
- try {
- factory = CertificateFactory.getInstance("X509");
- } catch (CertificateException e) {
- throw new RuntimeException("Error getting CertificateFactory", e);
- }
- X509Certificate certificate = null;
- try {
- if (factory != null) {
- certificate = (X509Certificate) factory.generateCertificate(input);
- }
- } catch (CertificateException e) {
- throw new RuntimeException("Error getting X509Certificate", e);
- }
-
- if (certificate == null) {
- throw new RuntimeException("X509 Certificate not found");
- }
-
- try {
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
- byte[] publicKey = digest.digest(certificate.getEncoded());
- return getHexDigest(publicKey);
- } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
- throw new IllegalArgumentException("Error error computing fingerprint", e);
- }
- }
-
- @Nullable
- private Pair<SigningDetails, Bundle> getPackageSigningAndMetadata(Uri dataUri) {
- File installationPath = getInstallationPath(dataUri);
- if (installationPath == null) {
- throw new IllegalArgumentException("Installation path is null, package not found");
- }
-
- try (PackageParser2 parser = mParserSupplier.get()) {
- var pkg = parser.parsePackage(installationPath, 0, false);
- // APK signatures is already verified elsewhere in PackageManager. We do not need to
- // verify it again since it could cause a timeout for large APKs.
- final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
- final ParseResult<SigningDetails> result = ParsingPackageUtils.getSigningDetails(
- input, pkg, /* skipVerify= */ true);
- if (result.isError()) {
- Slog.w(TAG, result.getErrorMessage(), result.getException());
- return null;
- }
- return Pair.create(result.getResult(), pkg.getMetaData());
- } catch (Exception e) {
- Slog.w(TAG, "Exception reading " + dataUri, e);
- return null;
- }
- }
-
- private PackageInfo getMultiApkInfo(File multiApkDirectory) {
- // The base apk will normally be called base.apk
- File baseFile = new File(multiApkDirectory, BASE_APK_FILE);
- PackageInfo basePackageInfo =
- mContext.getPackageManager()
- .getPackageArchiveInfo(
- baseFile.getAbsolutePath(),
- PackageManager.GET_SIGNING_CERTIFICATES
- | PackageManager.GET_META_DATA);
-
- if (basePackageInfo == null) {
- for (File apkFile : multiApkDirectory.listFiles()) {
- if (apkFile.isDirectory()) {
- continue;
- }
-
- // If we didn't find a base.apk, then try to parse each apk until we find the one
- // that succeeds.
- try {
- basePackageInfo =
- mContext.getPackageManager()
- .getPackageArchiveInfo(
- apkFile.getAbsolutePath(),
- PackageManager.GET_SIGNING_CERTIFICATES
- | PackageManager.GET_META_DATA);
- } catch (Exception e) {
- // Some of the splits may not contain a valid android manifest. It is an
- // expected exception. We still log it nonetheless but we should keep looking.
- Slog.w(TAG, "Exception reading " + apkFile, e);
- }
- if (basePackageInfo != null) {
- Slog.i(TAG, "Found package info from " + apkFile);
- break;
- }
- }
- }
-
- if (basePackageInfo == null) {
- throw new IllegalArgumentException(
- "Base package info cannot be found from installation directory");
- }
-
- return basePackageInfo;
- }
-
- private File getInstallationPath(Uri dataUri) {
- if (dataUri == null) {
- throw new IllegalArgumentException("Null data uri");
- }
-
- String scheme = dataUri.getScheme();
- if (!"file".equalsIgnoreCase(scheme)) {
- throw new IllegalArgumentException("Unsupported scheme for " + dataUri);
- }
-
- File installationPath = new File(dataUri.getPath());
- if (!installationPath.exists()) {
- throw new IllegalArgumentException("Cannot find file for " + dataUri);
- }
- if (!installationPath.canRead()) {
- throw new IllegalArgumentException("Cannot read file for " + dataUri);
- }
- return installationPath;
- }
-
private String getCallerPackageNameOrThrow(int callingUid) {
String callerPackageName = getCallingRulePusherPackageName(callingUid);
if (callerPackageName == null) {
@@ -715,15 +259,6 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
return allowedCallingPackages.isEmpty() ? null : allowedCallingPackages.get(0);
}
- private boolean isRuleProvider(String installerPackageName) {
- for (String ruleProvider : getAllowedRuleProviderSystemApps()) {
- if (ruleProvider.matches(installerPackageName)) {
- return true;
- }
- }
- return false;
- }
-
private List<String> getAllowedRuleProviderSystemApps() {
List<String> integrityRuleProviders =
Arrays.asList(
@@ -751,14 +286,6 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
}
}
- private boolean integrityCheckIncludesRuleProvider() {
- return Settings.Global.getInt(
- mContext.getContentResolver(),
- Settings.Global.INTEGRITY_CHECK_INCLUDES_RULE_PROVIDER,
- 0)
- == 1;
- }
-
private List<String> getPackageListForUid(int uid) {
try {
return Arrays.asList(mContext.getPackageManager().getPackagesForUid(uid));
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index a24c743929b5..f79d9ef174ea 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -165,6 +165,13 @@ flag {
}
flag {
+ name: "notification_lock_screen_settings"
+ namespace: "systemui"
+ description: "This flag enables the new settings page for the notifications on lock screen."
+ bug: "367455695"
+}
+
+flag {
name: "notification_vibration_in_sound_uri"
namespace: "systemui"
description: "This flag enables sound uri with vibration source"
diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index 369029adac59..84a5f2b0e8bc 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -80,11 +80,6 @@ import java.util.function.BiFunction;
*/
public final class BroadcastHelper {
private static final boolean DEBUG_BROADCASTS = false;
- /**
- * Permissions required in order to receive instant application lifecycle broadcasts.
- */
- private static final String[] INSTANT_APP_BROADCAST_PERMISSION =
- new String[]{android.Manifest.permission.ACCESS_INSTANT_APPS};
private final UserManagerInternal mUmInternal;
private final ActivityManagerInternal mAmInternal;
@@ -115,7 +110,7 @@ public final class BroadcastHelper {
SparseArray<int[]> broadcastAllowList = new SparseArray<>();
broadcastAllowList.put(userId, visibilityAllowList);
broadcastIntent(intent, finishedReceiver, isInstantApp, userId, broadcastAllowList,
- filterExtrasForReceiver, bOptions);
+ filterExtrasForReceiver, bOptions, null /* requiredPermissions */);
}
void sendPackageBroadcast(final String action, final String pkg, final Bundle extras,
@@ -123,7 +118,7 @@ public final class BroadcastHelper {
final int[] userIds, int[] instantUserIds,
@Nullable SparseArray<int[]> broadcastAllowList,
@Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
- @Nullable Bundle bOptions) {
+ @Nullable Bundle bOptions, @Nullable String[] requiredPermissions) {
try {
final IActivityManager am = ActivityManager.getService();
if (am == null) return;
@@ -137,12 +132,12 @@ public final class BroadcastHelper {
if (ArrayUtils.isEmpty(instantUserIds)) {
doSendBroadcast(action, pkg, extras, flags, targetPkg, finishedReceiver,
resolvedUserIds, false /* isInstantApp */, broadcastAllowList,
- filterExtrasForReceiver, bOptions);
+ filterExtrasForReceiver, bOptions, requiredPermissions);
} else {
// send restricted broadcasts for instant apps
doSendBroadcast(action, pkg, extras, flags, targetPkg, finishedReceiver,
- instantUserIds, true /* isInstantApp */, null,
- null /* filterExtrasForReceiver */, bOptions);
+ instantUserIds, true /* isInstantApp */, null /* broadcastAllowList */,
+ null /* filterExtrasForReceiver */, bOptions, requiredPermissions);
}
} catch (RemoteException ex) {
}
@@ -166,7 +161,8 @@ public final class BroadcastHelper {
boolean isInstantApp,
@Nullable SparseArray<int[]> broadcastAllowList,
@Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
- @Nullable Bundle bOptions) {
+ @Nullable Bundle bOptions,
+ @Nullable String[] requiredPermissions) {
for (int userId : userIds) {
final Intent intent = new Intent(action,
pkg != null ? Uri.fromParts(PACKAGE_SCHEME, pkg, null) : null);
@@ -189,17 +185,18 @@ public final class BroadcastHelper {
intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | flags);
broadcastIntent(intent, finishedReceiver, isInstantApp, userId, broadcastAllowList,
- filterExtrasForReceiver, bOptions);
+ filterExtrasForReceiver, bOptions, requiredPermissions);
}
}
-
private void broadcastIntent(Intent intent, IIntentReceiver finishedReceiver,
boolean isInstantApp, int userId, @Nullable SparseArray<int[]> broadcastAllowList,
@Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
- @Nullable Bundle bOptions) {
- final String[] requiredPermissions =
- isInstantApp ? INSTANT_APP_BROADCAST_PERMISSION : null;
+ @Nullable Bundle bOptions, @Nullable String[] requiredPermissions) {
+ if (isInstantApp) {
+ requiredPermissions = ArrayUtils.appendElement(String.class, requiredPermissions,
+ android.Manifest.permission.ACCESS_INSTANT_APPS);
+ }
if (DEBUG_BROADCASTS) {
RuntimeException here = new RuntimeException("here");
here.fillInStackTrace();
@@ -234,7 +231,7 @@ public final class BroadcastHelper {
null /* instantUserIds */, null /* broadcastAllowList */,
(callingUid, intentExtras) -> filterExtrasChangedPackageList(
snapshot, callingUid, intentExtras),
- null /* bOptions */);
+ null /* bOptions */, null /* requiredPermissions */);
}
/**
@@ -294,14 +291,29 @@ public final class BroadcastHelper {
return bOptions;
}
- private void sendPackageChangedBroadcast(@NonNull String packageName,
- boolean dontKillApp,
- @NonNull ArrayList<String> componentNames,
- int packageUid,
- @Nullable String reason,
- @Nullable int[] userIds,
- @Nullable int[] instantUserIds,
- @Nullable SparseArray<int[]> broadcastAllowList) {
+ private void sendPackageChangedBroadcastInternal(@NonNull String packageName,
+ boolean dontKillApp,
+ @NonNull ArrayList<String> componentNames,
+ int packageUid,
+ @Nullable String reason,
+ @Nullable int[] userIds,
+ @Nullable int[] instantUserIds,
+ @Nullable SparseArray<int[]> broadcastAllowList) {
+ sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, componentNames,
+ packageUid, reason, userIds, instantUserIds, broadcastAllowList,
+ null /* targetPackageName */, null /* requiredPermissions */);
+ }
+
+ private void sendPackageChangedBroadcastWithPermissions(@NonNull String packageName,
+ boolean dontKillApp,
+ @NonNull ArrayList<String> componentNames,
+ int packageUid,
+ @Nullable String reason,
+ @Nullable int[] userIds,
+ @Nullable int[] instantUserIds,
+ @Nullable SparseArray<int[]> broadcastAllowList,
+ @Nullable String targetPackageName,
+ @Nullable String[] requiredPermissions) {
if (DEBUG_INSTALL) {
Log.v(TAG, "Sending package changed: package=" + packageName + " components="
+ componentNames);
@@ -321,9 +333,10 @@ public final class BroadcastHelper {
// little component state change.
final int flags = !componentNames.contains(packageName)
? Intent.FLAG_RECEIVER_REGISTERED_ONLY : 0;
- sendPackageBroadcast(Intent.ACTION_PACKAGE_CHANGED, packageName, extras, flags, null, null,
- userIds, instantUserIds, broadcastAllowList, null /* filterExtrasForReceiver */,
- null /* bOptions */);
+ sendPackageBroadcast(Intent.ACTION_PACKAGE_CHANGED, packageName, extras, flags,
+ targetPackageName, null /* finishedReceiver */, userIds, instantUserIds,
+ broadcastAllowList, null /* filterExtrasForReceiver */, null /* bOptions */,
+ requiredPermissions);
}
static void sendDeviceCustomizationReadyBroadcast() {
@@ -680,7 +693,8 @@ public final class BroadcastHelper {
sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
packageName, extras, 0, null, null, userIds, instantUserIds,
- broadcastAllowlist, null /* filterExtrasForReceiver */, null);
+ broadcastAllowlist, null /* filterExtrasForReceiver */, null /* bOptions */,
+ null /* requiredPermissions */);
// Send to PermissionController for all new users, even if it may not be running for some
// users
if (isPrivacySafetyLabelChangeNotificationsEnabled(mContext)) {
@@ -688,7 +702,8 @@ public final class BroadcastHelper {
packageName, extras, 0,
mContext.getPackageManager().getPermissionControllerPackageName(),
null, userIds, instantUserIds,
- broadcastAllowlist, null /* filterExtrasForReceiver */, null);
+ broadcastAllowlist, null /* filterExtrasForReceiver */, null /* bOptions */,
+ null /* requiredPermissions */);
}
}
@@ -719,7 +734,8 @@ public final class BroadcastHelper {
int[] userIds, int[] instantUserIds) {
sendPackageBroadcast(Intent.ACTION_PACKAGE_FIRST_LAUNCH, pkgName, null, 0,
installerPkg, null, userIds, instantUserIds, null /* broadcastAllowList */,
- null /* filterExtrasForReceiver */, null);
+ null /* filterExtrasForReceiver */, null /* bOptions */,
+ null /* requiredPermissions */);
}
/**
@@ -824,7 +840,7 @@ public final class BroadcastHelper {
final int[] instantUserIds = isInstantApp ? new int[] { userId } : EMPTY_INT_ARRAY;
final SparseArray<int[]> broadcastAllowList =
isInstantApp ? null : snapshot.getVisibilityAllowLists(packageName, userIds);
- mHandler.post(() -> sendPackageChangedBroadcast(
+ mHandler.post(() -> sendPackageChangedBroadcastInternal(
packageName, dontKillApp, componentNames, packageUid, reason, userIds,
instantUserIds, broadcastAllowList));
mPackageMonitorCallbackHelper.notifyPackageChanged(packageName, dontKillApp, componentNames,
@@ -843,7 +859,7 @@ public final class BroadcastHelper {
@Nullable Bundle bOptions) {
mHandler.post(() -> sendPackageBroadcast(action, pkg, extras, flags,
targetPkg, finishedReceiver, userIds, instantUserIds, broadcastAllowList,
- null /* filterExtrasForReceiver */, bOptions));
+ null /* filterExtrasForReceiver */, bOptions, null /* requiredPermissions */));
if (targetPkg == null) {
// For some broadcast action, e.g. ACTION_PACKAGE_ADDED, this method will be called
// many times to different targets, e.g. installer app, permission controller, other
@@ -1014,7 +1030,7 @@ public final class BroadcastHelper {
extras, flags, null /* targetPkg */, null /* finishedReceiver */,
new int[]{userId}, null /* instantUserIds */, null /* broadcastAllowList */,
filterExtrasForReceiver,
- options));
+ options, null /* requiredPermissions */));
notifyPackageMonitor(intent, null /* pkg */, extras, new int[]{userId},
null /* instantUserIds */, null /* broadcastAllowList */, filterExtrasForReceiver);
}
@@ -1046,9 +1062,12 @@ public final class BroadcastHelper {
} else {
intentExtras = null;
}
- doSendBroadcast(action, null, intentExtras,
- Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, packageName, null,
- targetUserIds, false, null, null, null);
+ doSendBroadcast(action, null /* pkg */, intentExtras,
+ Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, packageName,
+ null /* finishedReceiver */,
+ targetUserIds, false /* isInstantApp */, null /* broadcastAllowList */,
+ null /* filterExtrasForReceiver */, null /* bOptions */,
+ null /* requiredPermissions */);
}
});
}
@@ -1077,7 +1096,7 @@ public final class BroadcastHelper {
null /* broadcastAllowList */,
(callingUid, intentExtras) -> filterExtrasChangedPackageList(
snapshot, callingUid, intentExtras),
- null /* bOptions */));
+ null /* bOptions */, null /* requiredPermissions */));
}
void sendResourcesChangedBroadcastAndNotify(@NonNull Computer snapshot,
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 34d939b07187..f6a808b6c33e 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -25,12 +25,14 @@ import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_NO_CONNECTIVI
import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED;
import static android.content.pm.PackageInstaller.UNARCHIVAL_GENERIC_ERROR;
import static android.content.pm.PackageInstaller.UNARCHIVAL_OK;
+import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_WARN;
import static android.content.pm.PackageManager.DELETE_ARCHIVE;
import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
import static android.os.Process.INVALID_UID;
import static android.os.Process.SYSTEM_UID;
import static com.android.server.pm.PackageArchiver.isArchivingEnabled;
+import static com.android.server.pm.PackageInstallerSession.isValidVerificationPolicy;
import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
@@ -150,6 +152,7 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntPredicate;
import java.util.function.Supplier;
@@ -275,6 +278,13 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements
}
};
+ /**
+ * Default verification policy for incoming installation sessions.
+ * TODO(b/360129657): update the default policy.
+ */
+ private final AtomicInteger mVerificationPolicy = new AtomicInteger(
+ VERIFICATION_POLICY_BLOCK_FAIL_WARN);
+
private static final class Lifecycle extends SystemService {
private final PackageInstallerService mPackageInstallerService;
@@ -1042,7 +1052,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements
userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid,
null, null, false, false, false, false, null, SessionInfo.INVALID_ID,
false, false, false, PackageManager.INSTALL_UNKNOWN, "", null,
- mVerifierController);
+ mVerifierController, mVerificationPolicy.get());
synchronized (mSessions) {
mSessions.put(sessionId, session);
@@ -1866,6 +1876,34 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements
}
}
+ @Override
+ public @PackageInstaller.VerificationPolicy int getVerificationPolicy() {
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.VERIFICATION_AGENT)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("You need the "
+ + "com.android.permission.VERIFICATION_AGENT permission "
+ + "to get the verification policy");
+ }
+ return mVerificationPolicy.get();
+ }
+
+ @Override
+ public boolean setVerificationPolicy(@PackageInstaller.VerificationPolicy int policy) {
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.VERIFICATION_AGENT)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("You need the "
+ + "com.android.permission.VERIFICATION_AGENT permission "
+ + "to set the verification policy");
+ }
+ if (!isValidVerificationPolicy(policy)) {
+ return false;
+ }
+ if (policy != mVerificationPolicy.get()) {
+ mVerificationPolicy.set(policy);
+ }
+ return true;
+ }
+
private static int getSessionCount(SparseArray<PackageInstallerSession> sessions,
int installerUid) {
int count = 0;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 9e0ba8492ab9..04d0d182d3b2 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -21,9 +21,17 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_INSTA
import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_UPDATED_BY_DO;
import static android.content.pm.DataLoaderType.INCREMENTAL;
import static android.content.pm.DataLoaderType.STREAMING;
+import static android.content.pm.PackageInstaller.EXTRA_VERIFICATION_FAILURE_REASON;
import static android.content.pm.PackageInstaller.LOCATION_DATA_APP;
import static android.content.pm.PackageInstaller.UNARCHIVAL_OK;
import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET;
+import static android.content.pm.PackageInstaller.VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE;
+import static android.content.pm.PackageInstaller.VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED;
+import static android.content.pm.PackageInstaller.VERIFICATION_FAILED_REASON_UNKNOWN;
+import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED;
+import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_OPEN;
+import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_WARN;
+import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_NONE;
import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH;
import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED;
import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE;
@@ -38,7 +46,7 @@ import static android.content.pm.PackageManager.INSTALL_FAILED_VERIFICATION_FAIL
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
import static android.content.pm.PackageManager.INSTALL_STAGED;
import static android.content.pm.PackageManager.INSTALL_SUCCEEDED;
-import static android.content.pm.verify.pkg.VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN;
+import static android.content.pm.verify.pkg.VerificationSession.VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE;
import static android.os.Process.INVALID_UID;
import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE;
import static android.system.OsConstants.O_CREAT;
@@ -313,6 +321,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
private static final String ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT =
"applicationEnabledSettingPersistent";
private static final String ATTR_DOMAIN = "domain";
+ private static final String ATTR_VERIFICATION_POLICY = "verificationPolicy";
private static final String PROPERTY_NAME_INHERIT_NATIVE = "pi.inherit_native_on_dont_kill";
private static final int[] EMPTY_CHILD_SESSION_ARRAY = EmptyArray.INT;
@@ -410,6 +419,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
private final PackageSessionProvider mSessionProvider;
private final SilentUpdatePolicy mSilentUpdatePolicy;
/**
+ * The verification policy applied to this session, which might be different from the default
+ * verification policy used by the system.
+ */
+ private final AtomicInteger mVerificationPolicy;
+ /**
* Note all calls must be done outside {@link #mLock} to prevent lock inversion.
*/
private final StagingManager mStagingManager;
@@ -791,7 +805,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
if (errorMsg != null) {
Slog.e(TAG, "verifySession error: " + errorMsg);
setSessionFailed(INSTALL_FAILED_INTERNAL_ERROR, errorMsg);
- onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR, errorMsg);
+ onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR, errorMsg,
+ /* extras= */ null);
return false;
}
return true;
@@ -1167,7 +1182,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
@Nullable int[] childSessionIds, int parentSessionId, boolean isReady,
boolean isFailed, boolean isApplied, int sessionErrorCode,
String sessionErrorMessage, DomainSet preVerifiedDomains,
- @NonNull VerifierController verifierController) {
+ @NonNull VerifierController verifierController,
+ @PackageInstaller.VerificationPolicy int verificationPolicy) {
mCallback = callback;
mContext = context;
mPm = pm;
@@ -1177,6 +1193,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
mHandler = new Handler(looper, mHandlerCallback);
mStagingManager = stagingManager;
mVerifierController = verifierController;
+ mVerificationPolicy = new AtomicInteger(verificationPolicy);
this.sessionId = sessionId;
this.userId = userId;
@@ -2580,10 +2597,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
dispatchSessionFinished(error, detailMessage, null);
}
- private void onSessionVerificationFailure(int error, String msg) {
+ private void onSessionVerificationFailure(int error, String msg, Bundle extras) {
Slog.e(TAG, "Failed to verify session " + sessionId);
// Dispatch message to remove session from PackageInstallerService.
- dispatchSessionFinished(error, msg, null);
+ dispatchSessionFinished(error, msg, extras);
maybeFinishChildSessions(error, msg);
}
@@ -2856,7 +2873,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
final String completeMsg = ExceptionUtils.getCompleteMessage(e);
final String errorMsg = PackageManager.installStatusToString(e.error, completeMsg);
setSessionFailed(e.error, errorMsg);
- onSessionVerificationFailure(e.error, errorMsg);
+ onSessionVerificationFailure(e.error, errorMsg, /* extras= */ null);
}
if (Flags.verificationService()) {
final Supplier<Computer> snapshotSupplier = mPm::snapshotComputer;
@@ -2872,11 +2889,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
// the installation can proceed.
if (!mVerifierController.startVerificationSession(snapshotSupplier, userId,
sessionId, getPackageName(), Uri.fromFile(stageDir), signingInfo,
- declaredLibraries, /* extensionParams= */ null,
+ declaredLibraries, mVerificationPolicy.get(), /* extensionParams= */ null,
new VerifierCallback(), /* retry= */ false)) {
// A verifier is installed but cannot be connected. Installation disallowed.
onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR,
- "A verifier agent is available on device but cannot be connected.");
+ "A verifier agent is available on device but cannot be connected.",
+ /* extras= */ null);
}
} else {
// Verifier is not installed. Let the installation pass for now.
@@ -2917,7 +2935,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
final String completeMsg = ExceptionUtils.getCompleteMessage(e);
final String errorMsg = PackageManager.installStatusToString(e.error, completeMsg);
setSessionFailed(e.error, errorMsg);
- onSessionVerificationFailure(e.error, errorMsg);
+ onSessionVerificationFailure(e.error, errorMsg, /* extras= */ null);
}
}
@@ -2926,24 +2944,57 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
*/
public class VerifierCallback {
/**
+ * Called by the VerifierController when the verifier requests to get the current
+ * verification policy for this session.
+ */
+ public @PackageInstaller.VerificationPolicy int getVerificationPolicy() {
+ return mVerificationPolicy.get();
+ }
+ /**
+ * Called by the VerifierController when the verifier requests to change the verification
+ * policy for this session.
+ */
+ public boolean setVerificationPolicy(@PackageInstaller.VerificationPolicy int policy) {
+ if (!isValidVerificationPolicy(policy)) {
+ return false;
+ }
+ mVerificationPolicy.set(policy);
+ return true;
+ }
+ /**
* Called by the VerifierController when the connection has failed.
*/
public void onConnectionFailed() {
- mHandler.post(() -> {
- onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
- "A verifier agent is available on device but cannot be connected.");
- });
+ // TODO(b/360129657): prompt user on fail warning
+ handleNonPackageBlockedFailure(
+ /* onFailWarning= */ PackageInstallerSession.this::resumeVerify,
+ /* onFailClosed= */ () -> {
+ Bundle bundle = new Bundle();
+ bundle.putInt(EXTRA_VERIFICATION_FAILURE_REASON,
+ VERIFICATION_FAILED_REASON_UNKNOWN);
+ onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
+ "A verifier agent is available on device but cannot be connected.",
+ bundle);
+
+ });
}
/**
* Called by the VerifierController when the verification request has timed out.
*/
public void onTimeout() {
- mHandler.post(() -> {
- mVerifierController.notifyVerificationTimeout(sessionId);
- onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
- "Verification timed out; missing a response from the verifier within the"
- + " time limit");
- });
+ // Always notify the verifier, regardless of the policy.
+ mVerifierController.notifyVerificationTimeout(sessionId);
+ // TODO(b/360129657): prompt user on fail warning
+ handleNonPackageBlockedFailure(
+ /* onFailWarning= */ PackageInstallerSession.this::resumeVerify,
+ /* onFailClosed= */ () -> {
+ Bundle bundle = new Bundle();
+ bundle.putInt(EXTRA_VERIFICATION_FAILURE_REASON,
+ VERIFICATION_FAILED_REASON_UNKNOWN);
+ onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
+ "Verification timed out; missing a response from the verifier"
+ + " within the time limit", bundle);
+ });
}
/**
* Called by the VerifierController when the verification request has received a complete
@@ -2953,17 +3004,22 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
@Nullable PersistableBundle extensionResponse) {
// TODO: handle extension response
mHandler.post(() -> {
- if (statusReceived.isVerified()) {
+ if (statusReceived.isVerified()
+ || mVerificationPolicy.get() == VERIFICATION_POLICY_NONE) {
// Continue with the rest of the verification and installation.
resumeVerify();
- } else {
- StringBuilder sb = new StringBuilder("Verifier rejected the installation");
- if (!TextUtils.isEmpty(statusReceived.getFailureMessage())) {
- sb.append(" with message: ").append(statusReceived.getFailureMessage());
- }
- onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
- sb.toString());
+ return;
+ }
+ // Package is blocked.
+ StringBuilder sb = new StringBuilder("Verifier rejected the installation");
+ if (!TextUtils.isEmpty(statusReceived.getFailureMessage())) {
+ sb.append(" with message: ").append(statusReceived.getFailureMessage());
}
+ Bundle bundle = new Bundle();
+ bundle.putInt(EXTRA_VERIFICATION_FAILURE_REASON,
+ VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED);
+ onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
+ sb.toString(), bundle);
});
}
/**
@@ -2971,16 +3027,51 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
* response.
*/
public void onVerificationIncompleteReceived(int incompleteReason) {
- mHandler.post(() -> {
- if (incompleteReason == VERIFICATION_INCOMPLETE_UNKNOWN) {
- // TODO: change this to a user confirmation and handle other incomplete reasons
- onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR,
- "Verification cannot be completed for unknown reasons.");
+ // TODO(b/360129657): prompt user on fail warning
+ handleNonPackageBlockedFailure(
+ /* onFailWarning= */ PackageInstallerSession.this::resumeVerify,
+ /* onFailClosed= */ () -> {
+ final int failureReason;
+ StringBuilder sb = new StringBuilder(
+ "Verification cannot be completed because of ");
+ if (incompleteReason == VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE) {
+ failureReason = VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE;
+ sb.append("unavailable network.");
+ } else {
+ failureReason = VERIFICATION_FAILED_REASON_UNKNOWN;
+ sb.append("unknown reasons.");
+ }
+ Bundle bundle = new Bundle();
+ bundle.putInt(EXTRA_VERIFICATION_FAILURE_REASON, failureReason);
+ onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
+ sb.toString(), bundle);
+ });
+ }
+
+ private void handleNonPackageBlockedFailure(Runnable onFailWarning, Runnable onFailClosed) {
+ final Runnable r = switch (mVerificationPolicy.get()) {
+ case VERIFICATION_POLICY_NONE, VERIFICATION_POLICY_BLOCK_FAIL_OPEN ->
+ PackageInstallerSession.this::resumeVerify;
+ case VERIFICATION_POLICY_BLOCK_FAIL_WARN -> onFailWarning;
+ case VERIFICATION_POLICY_BLOCK_FAIL_CLOSED -> onFailClosed;
+ default -> {
+ Log.wtf(TAG, "Unknown verification policy: " + mVerificationPolicy.get());
+ yield onFailClosed;
}
- });
+ };
+ mHandler.post(r);
}
}
+ /**
+ * Returns whether a policy is a valid verification policy.
+ */
+ public static boolean isValidVerificationPolicy(
+ @PackageInstaller.VerificationPolicy int policy) {
+ return policy >= VERIFICATION_POLICY_NONE
+ && policy <= VERIFICATION_POLICY_BLOCK_FAIL_CLOSED;
+ }
+
private IntentSender getRemoteStatusReceiver() {
synchronized (mLock) {
return mRemoteStatusReceiver;
@@ -3156,7 +3247,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
if (error == INSTALL_SUCCEEDED) {
onVerificationComplete();
} else {
- onSessionVerificationFailure(error, msg);
+ onSessionVerificationFailure(error, msg, /* extras= */ null);
}
});
});
@@ -5328,6 +5419,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
}
+ /**
+ * @return the current policy for the verification request associated with this session.
+ */
+ @VisibleForTesting
+ public @PackageInstaller.VerificationPolicy int getVerificationPolicy() {
+ assertCallerIsOwnerOrRoot();
+ return mVerificationPolicy.get();
+ }
void setSessionReady() {
synchronized (mLock) {
@@ -5631,6 +5730,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
if (!ArrayUtils.isEmpty(warnings)) {
fillIn.putStringArrayListExtra(PackageInstaller.EXTRA_WARNINGS, warnings);
}
+ if (extras.containsKey(EXTRA_VERIFICATION_FAILURE_REASON)) {
+ fillIn.putExtra(EXTRA_VERIFICATION_FAILURE_REASON,
+ extras.getInt(EXTRA_VERIFICATION_FAILURE_REASON));
+ }
}
try {
final BroadcastOptions options = BroadcastOptions.makeBasic();
@@ -5786,6 +5889,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
out.attributeInt(null, ATTR_INSTALL_REASON, params.installReason);
writeBooleanAttribute(out, ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT,
params.applicationEnabledSettingPersistent);
+ out.attributeInt(null, ATTR_VERIFICATION_POLICY, mVerificationPolicy.get());
final boolean isDataLoader = params.dataLoaderParams != null;
writeBooleanAttribute(out, ATTR_IS_DATALOADER, isDataLoader);
@@ -5936,6 +6040,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
final boolean sealed = in.getAttributeBoolean(null, ATTR_SEALED, false);
final int parentSessionId = in.getAttributeInt(null, ATTR_PARENT_SESSION_ID,
SessionInfo.INVALID_ID);
+ final int verificationPolicy = in.getAttributeInt(null, ATTR_VERIFICATION_POLICY,
+ VERIFICATION_POLICY_NONE);
final SessionParams params = new SessionParams(
SessionParams.MODE_INVALID);
@@ -6110,6 +6216,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
installerUid, installSource, params, createdMillis, committedMillis, stageDir,
stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed,
childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied,
- sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController);
+ sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController,
+ verificationPolicy);
}
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 455776993c56..d78f12217271 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -4709,10 +4709,11 @@ public class PackageManagerService implements PackageSender, TestUtilityService
extras.putLong(Intent.EXTRA_TIME, SystemClock.elapsedRealtime());
mHandler.post(() -> {
mBroadcastHelper.sendPackageBroadcast(Intent.ACTION_PACKAGE_UNSTOPPED,
- packageName, extras,
- Intent.FLAG_RECEIVER_REGISTERED_ONLY, null, null,
- userIds, null, broadcastAllowList, null,
- null);
+ packageName, extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY,
+ null /* targetPkg */, null /* finishedReceiver */, userIds,
+ null /* instantUserIds */, broadcastAllowList,
+ null /* filterExtrasForReceiver */, null /* bOptions */,
+ null /* requiredPermissions */);
});
mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_UNSTOPPED,
packageName, extras, userIds, null /* instantUserIds */,
@@ -7169,17 +7170,17 @@ public class PackageManagerService implements PackageSender, TestUtilityService
// Sent async using the PM handler, to maintain ordering with PACKAGE_UNSTOPPED
mHandler.post(() -> {
mBroadcastHelper.sendPackageBroadcast(Intent.ACTION_PACKAGE_RESTARTED,
- packageName, extras,
- flags, null, null,
- userIds, null, broadcastAllowList, null,
- null);
+ packageName, extras, flags, null /* targetPkg */,
+ null /* finishedReceiver */, userIds, null /* instantUserIds */,
+ broadcastAllowList, null /* filterExtrasForReceiver */,
+ null /* bOptions */, null /* requiredPermissions */);
});
} else {
mBroadcastHelper.sendPackageBroadcast(Intent.ACTION_PACKAGE_RESTARTED,
- packageName, extras,
- flags, null, null,
- userIds, null, broadcastAllowList, null,
- null);
+ packageName, extras, flags, null /* targetPkg */,
+ null /* finishedReceiver */, userIds, null /* instantUserIds */,
+ broadcastAllowList, null /* filterExtrasForReceiver */, null /* bOptions */,
+ null /* requiredPermissions */);
}
mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_RESTARTED,
packageName, extras, userIds, null /* instantUserIds */,
diff --git a/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java b/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java
index 7eac940933c2..b7cc7ccead89 100644
--- a/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java
+++ b/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java
@@ -30,6 +30,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.SharedLibraryInfo;
@@ -94,9 +95,22 @@ public class VerifierController {
// Max duration allowed to wait for a verifier to respond to a verification request.
private static final long DEFAULT_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS =
TimeUnit.MINUTES.toMillis(10);
+ /**
+ * Configurable maximum amount of time in milliseconds for the system to wait from the moment
+ * when the installation session requires a verification, till when the request is delivered to
+ * the verifier, pending the connection to be established. If the request has not been delivered
+ * to the verifier within this amount of time, e.g., because the verifier has crashed or ANR'd,
+ * the controller then sends a failure status back to the installation session.
+ * Flag type: {@code long}
+ * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+ */
+ private static final String PROPERTY_VERIFIER_CONNECTION_TIMEOUT_MILLIS =
+ "verifier_connection_timeout_millis";
// The maximum amount of time to wait from the moment when the session requires a verification,
// till when the request is delivered to the verifier, pending the connection to be established.
- private static final long CONNECTION_TIMEOUT_SECONDS = 10;
+ private static final long DEFAULT_VERIFIER_CONNECTION_TIMEOUT_MILLIS =
+ TimeUnit.SECONDS.toMillis(10);
+
// The maximum amount of time to wait before the system unbinds from the verifier.
private static final long UNBIND_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(6);
@@ -271,6 +285,7 @@ public class VerifierController {
int installationSessionId, String packageName,
Uri stagedPackageUri, SigningInfo signingInfo,
List<SharedLibraryInfo> declaredLibraries,
+ @PackageInstaller.VerificationPolicy int verificationPolicy,
PersistableBundle extensionParams, PackageInstallerSession.VerifierCallback callback,
boolean retry) {
// Try connecting to the verifier if not already connected
@@ -292,7 +307,7 @@ public class VerifierController {
/* id= */ verificationId,
/* installSessionId= */ installationSessionId,
packageName, stagedPackageUri, signingInfo, declaredLibraries, extensionParams,
- new VerificationSessionInterface(),
+ verificationPolicy, new VerificationSessionInterface(callback),
new VerificationSessionCallback(callback));
AndroidFuture<Void> unusedFuture = mRemoteService.post(service -> {
if (!retry) {
@@ -306,7 +321,8 @@ public class VerifierController {
}
service.onVerificationRetry(session);
}
- }).orTimeout(CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS).whenComplete((res, err) -> {
+ }).orTimeout(mInjector.getVerifierConnectionTimeoutMillis(), TimeUnit.MILLISECONDS)
+ .whenComplete((res, err) -> {
if (err != null) {
Slog.e(TAG, "Error notifying verification request for session " + verificationId,
err);
@@ -407,6 +423,12 @@ public class VerifierController {
// This class handles requests from the remote verifier
private class VerificationSessionInterface extends IVerificationSessionInterface.Stub {
+ private final PackageInstallerSession.VerifierCallback mCallback;
+
+ VerificationSessionInterface(PackageInstallerSession.VerifierCallback callback) {
+ mCallback = callback;
+ }
+
@Override
public long getTimeoutTime(int verificationId) {
checkCallerPermission();
@@ -432,6 +454,20 @@ public class VerifierController {
return tracker.extendTimeRemaining(additionalMs);
}
}
+
+ @Override
+ public boolean setVerificationPolicy(int verificationId,
+ @PackageInstaller.VerificationPolicy int policy) {
+ checkCallerPermission();
+ synchronized (mVerificationStatus) {
+ final VerificationStatusTracker tracker = mVerificationStatus.get(verificationId);
+ if (tracker == null) {
+ throw new IllegalStateException("Verification session " + verificationId
+ + " doesn't exist or has finished");
+ }
+ }
+ return mCallback.setVerificationPolicy(policy);
+ }
}
private class VerificationSessionCallback extends IVerificationSessionCallback.Stub {
@@ -451,8 +487,8 @@ public class VerifierController {
throw new IllegalStateException("Verification session " + id
+ " doesn't exist or has finished");
}
- mCallback.onVerificationIncompleteReceived(reason);
}
+ mCallback.onVerificationIncompleteReceived(reason);
// Remove status tracking and stop the timeout countdown
removeStatusTracker(id);
}
@@ -630,6 +666,14 @@ public class VerifierController {
return getMaxVerificationExtendedTimeoutMillisFromDeviceConfig();
}
+ /**
+ * This is added so that we can mock the maximum connection timeout duration without
+ * calling into DeviceConfig.
+ */
+ public long getVerifierConnectionTimeoutMillis() {
+ return getVerifierConnectionTimeoutMillisFromDeviceConfig();
+ }
+
private static long getVerificationRequestTimeoutMillisFromDeviceConfig() {
return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE,
PROPERTY_VERIFICATION_REQUEST_TIMEOUT_MILLIS,
@@ -641,5 +685,11 @@ public class VerifierController {
PROPERTY_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS,
DEFAULT_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS);
}
+
+ private static long getVerifierConnectionTimeoutMillisFromDeviceConfig() {
+ return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+ PROPERTY_VERIFIER_CONNECTION_TIMEOUT_MILLIS,
+ DEFAULT_VERIFIER_CONNECTION_TIMEOUT_MILLIS);
+ }
}
}
diff --git a/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java b/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java
index 8f603fc34b32..075a31f3b24c 100644
--- a/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java
+++ b/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java
@@ -191,8 +191,7 @@ public class RotationResolverManagerService extends
SensorPrivacyManager.Sensors.CAMERA);
if (mIsServiceEnabled && isCameraAvailable) {
final RotationResolverManagerPerUserService service =
- getServiceForUserLocked(
- UserHandle.getCallingUserId());
+ getServiceForUserLocked(UserHandle.USER_CURRENT);
final RotationResolutionRequest request;
if (packageName == null) {
request = new RotationResolutionRequest(/* packageName */ "",
diff --git a/services/core/java/com/android/server/security/forensic/ForensicService.java b/services/core/java/com/android/server/security/forensic/ForensicService.java
new file mode 100644
index 000000000000..07639d1a3945
--- /dev/null
+++ b/services/core/java/com/android/server/security/forensic/ForensicService.java
@@ -0,0 +1,294 @@
+/*
+ * 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.security.forensic;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.security.forensic.IForensicService;
+import android.security.forensic.IForensicServiceCommandCallback;
+import android.security.forensic.IForensicServiceStateCallback;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ServiceThread;
+import com.android.server.SystemService;
+
+import java.util.ArrayList;
+
+/**
+ * @hide
+ */
+public class ForensicService extends SystemService {
+ private static final String TAG = "ForensicService";
+
+ private static final int MSG_MONITOR_STATE = 0;
+ private static final int MSG_MAKE_VISIBLE = 1;
+ private static final int MSG_MAKE_INVISIBLE = 2;
+ private static final int MSG_ENABLE = 3;
+ private static final int MSG_DISABLE = 4;
+ private static final int MSG_BACKUP = 5;
+
+ private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
+ private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE;
+ private static final int STATE_VISIBLE = IForensicServiceStateCallback.State.VISIBLE;
+ private static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED;
+
+ private static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN;
+ private static final int ERROR_PERMISSION_DENIED =
+ IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
+ private static final int ERROR_INVALID_STATE_TRANSITION =
+ IForensicServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION;
+ private static final int ERROR_BACKUP_TRANSPORT_UNAVAILABLE =
+ IForensicServiceCommandCallback.ErrorCode.BACKUP_TRANSPORT_UNAVAILABLE;
+ private static final int ERROR_DATA_SOURCE_UNAVAILABLE =
+ IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final BinderService mBinderService;
+
+ private final ArrayList<IForensicServiceStateCallback> mStateMonitors = new ArrayList<>();
+ private volatile int mState = STATE_INVISIBLE;
+
+ public ForensicService(@NonNull Context context) {
+ this(new InjectorImpl(context));
+ }
+
+ @VisibleForTesting
+ ForensicService(@NonNull Injector injector) {
+ super(injector.getContext());
+ mContext = injector.getContext();
+ mHandler = new EventHandler(injector.getLooper(), this);
+ mBinderService = new BinderService(this);
+ }
+
+ @VisibleForTesting
+ protected void setState(int state) {
+ mState = state;
+ }
+
+ private static final class BinderService extends IForensicService.Stub {
+ final ForensicService mService;
+
+ BinderService(ForensicService service) {
+ mService = service;
+ }
+
+ @Override
+ public void monitorState(IForensicServiceStateCallback callback) {
+ mService.mHandler.obtainMessage(MSG_MONITOR_STATE, callback).sendToTarget();
+ }
+
+ @Override
+ public void makeVisible(IForensicServiceCommandCallback callback) {
+ mService.mHandler.obtainMessage(MSG_MAKE_VISIBLE, callback).sendToTarget();
+ }
+
+ @Override
+ public void makeInvisible(IForensicServiceCommandCallback callback) {
+ mService.mHandler.obtainMessage(MSG_MAKE_INVISIBLE, callback).sendToTarget();
+ }
+
+ @Override
+ public void enable(IForensicServiceCommandCallback callback) {
+ mService.mHandler.obtainMessage(MSG_ENABLE, callback).sendToTarget();
+ }
+
+ @Override
+ public void disable(IForensicServiceCommandCallback callback) {
+ mService.mHandler.obtainMessage(MSG_DISABLE, callback).sendToTarget();
+ }
+ }
+
+ private static class EventHandler extends Handler {
+ private final ForensicService mService;
+
+ EventHandler(Looper looper, ForensicService service) {
+ super(looper);
+ mService = service;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_MONITOR_STATE:
+ try {
+ mService.monitorState(
+ (IForensicServiceStateCallback) msg.obj);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "RemoteException", e);
+ }
+ break;
+ case MSG_MAKE_VISIBLE:
+ try {
+ mService.makeVisible((IForensicServiceCommandCallback) msg.obj);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "RemoteException", e);
+ }
+ break;
+ case MSG_MAKE_INVISIBLE:
+ try {
+ mService.makeInvisible((IForensicServiceCommandCallback) msg.obj);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "RemoteException", e);
+ }
+ break;
+ case MSG_ENABLE:
+ try {
+ mService.enable((IForensicServiceCommandCallback) msg.obj);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "RemoteException", e);
+ }
+ break;
+ case MSG_DISABLE:
+ try {
+ mService.disable((IForensicServiceCommandCallback) msg.obj);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "RemoteException", e);
+ }
+ break;
+ default:
+ Slog.w(TAG, "Unknown message: " + msg.what);
+ }
+ }
+ }
+
+ private void monitorState(IForensicServiceStateCallback callback) throws RemoteException {
+ for (int i = 0; i < mStateMonitors.size(); i++) {
+ if (mStateMonitors.get(i).asBinder() == callback.asBinder()) {
+ return;
+ }
+ }
+ mStateMonitors.add(callback);
+ callback.onStateChange(mState);
+ }
+
+ private void notifyStateMonitors() throws RemoteException {
+ for (int i = 0; i < mStateMonitors.size(); i++) {
+ mStateMonitors.get(i).onStateChange(mState);
+ }
+ }
+
+ private void makeVisible(IForensicServiceCommandCallback callback) throws RemoteException {
+ switch (mState) {
+ case STATE_INVISIBLE:
+ mState = STATE_VISIBLE;
+ notifyStateMonitors();
+ callback.onSuccess();
+ break;
+ case STATE_VISIBLE:
+ callback.onSuccess();
+ break;
+ default:
+ callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+ }
+ }
+
+ private void makeInvisible(IForensicServiceCommandCallback callback) throws RemoteException {
+ switch (mState) {
+ case STATE_VISIBLE:
+ case STATE_ENABLED:
+ mState = STATE_INVISIBLE;
+ notifyStateMonitors();
+ callback.onSuccess();
+ break;
+ case STATE_INVISIBLE:
+ callback.onSuccess();
+ break;
+ default:
+ callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+ }
+ }
+
+ private void enable(IForensicServiceCommandCallback callback) throws RemoteException {
+ switch (mState) {
+ case STATE_VISIBLE:
+ mState = STATE_ENABLED;
+ notifyStateMonitors();
+ callback.onSuccess();
+ break;
+ case STATE_ENABLED:
+ callback.onSuccess();
+ break;
+ default:
+ callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+ }
+ }
+
+ private void disable(IForensicServiceCommandCallback callback) throws RemoteException {
+ switch (mState) {
+ case STATE_ENABLED:
+ mState = STATE_VISIBLE;
+ notifyStateMonitors();
+ callback.onSuccess();
+ break;
+ case STATE_VISIBLE:
+ callback.onSuccess();
+ break;
+ default:
+ callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ try {
+ publishBinderService(Context.FORENSIC_SERVICE, mBinderService);
+ } catch (Throwable t) {
+ Slog.e(TAG, "Could not start the ForensicService.", t);
+ }
+ }
+
+ @VisibleForTesting
+ IForensicService getBinderService() {
+ return mBinderService;
+ }
+
+ interface Injector {
+ Context getContext();
+
+ Looper getLooper();
+ }
+
+ private static final class InjectorImpl implements Injector {
+ private final Context mContext;
+
+ InjectorImpl(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+
+ @Override
+ public Looper getLooper() {
+ ServiceThread serviceThread =
+ new ServiceThread(
+ TAG, android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
+ serviceThread.start();
+ return serviceThread.getLooper();
+ }
+ }
+}
+
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 91a17a9e1c31..4589d26261dc 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -381,10 +381,12 @@ public final class TvInputManagerService extends SystemService {
// service to populate the hardware list.
serviceState = new ServiceState(component, userId);
userState.serviceStateMap.put(component, serviceState);
- updateServiceConnectionLocked(component, userId);
} else {
inputList.addAll(serviceState.hardwareInputMap.values());
}
+ if (serviceState.needInit) {
+ updateServiceConnectionLocked(component, userId);
+ }
} else {
try {
TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build();
@@ -489,6 +491,27 @@ public final class TvInputManagerService extends SystemService {
}
}
+ @GuardedBy("mLock")
+ private void cleanUpHdmiDevices(int userId) {
+ if (DEBUG) {
+ Slog.d(TAG, "cleanUpHdmiDevices: user " + userId);
+ }
+ UserState userState = getOrCreateUserStateLocked(userId);
+ for (ServiceState serviceState : userState.serviceStateMap.values()) {
+ for (HdmiDeviceInfo device : mTvInputHardwareManager.getHdmiDeviceList()) {
+ try {
+ if (serviceState.service != null) {
+ serviceState.service.notifyHdmiDeviceRemoved(device);
+ } else {
+ serviceState.hdmiDeviceRemovedBuffer.add(device);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in notifyHdmiDeviceRemoved", e);
+ }
+ }
+ }
+ }
+
private void startUser(int userId) {
synchronized (mLock) {
if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) {
@@ -500,9 +523,13 @@ public final class TvInputManagerService extends SystemService {
if (userInfo.isProfile()
&& parentInfo != null
&& parentInfo.id == mCurrentUserId) {
- // only the children of the current user can be started in background
+ int prevUserId = mCurrentUserId;
mCurrentUserId = userId;
- startProfileLocked(userId);
+ // only the children of the current user can be started in background
+ releaseSessionOfUserLocked(prevUserId);
+ cleanUpHdmiDevices(prevUserId);
+ unbindServiceOfUserLocked(prevUserId);
+ startProfileLocked(mCurrentUserId);
}
}
}
@@ -515,6 +542,7 @@ public final class TvInputManagerService extends SystemService {
}
releaseSessionOfUserLocked(userId);
+ cleanUpHdmiDevices(userId);
unbindServiceOfUserLocked(userId);
mRunningProfiles.remove(userId);
}
@@ -543,15 +571,19 @@ public final class TvInputManagerService extends SystemService {
unbindServiceOfUserLocked(runningId);
}
mRunningProfiles.clear();
- releaseSessionOfUserLocked(mCurrentUserId);
- unbindServiceOfUserLocked(mCurrentUserId);
+ int prevUserId = mCurrentUserId;
mCurrentUserId = userId;
- buildTvInputListLocked(userId, null);
- buildTvContentRatingSystemListLocked(userId);
+
+ releaseSessionOfUserLocked(prevUserId);
+ cleanUpHdmiDevices(prevUserId);
+ unbindServiceOfUserLocked(prevUserId);
+
+ buildTvInputListLocked(mCurrentUserId, null);
+ buildTvContentRatingSystemListLocked(mCurrentUserId);
mMessageHandler
.obtainMessage(MessageHandler.MSG_SWITCH_CONTENT_RESOLVER,
- getContentResolverForUser(userId))
+ getContentResolverForUser(mCurrentUserId))
.sendToTarget();
}
}
@@ -590,6 +622,9 @@ public final class TvInputManagerService extends SystemService {
@GuardedBy("mLock")
private void unbindServiceOfUserLocked(int userId) {
+ if (DEBUG) {
+ Slog.d(TAG, "unbindServiceOfUserLocked: user " + userId);
+ }
UserState userState = getUserStateLocked(userId);
if (userState == null) {
return;
@@ -600,7 +635,12 @@ public final class TvInputManagerService extends SystemService {
ServiceState serviceState = userState.serviceStateMap.get(component);
if (serviceState != null && serviceState.sessionTokens.isEmpty()) {
unbindService(serviceState);
- it.remove();
+ if (!serviceState.isHardware) {
+ it.remove();
+ } else {
+ serviceState.hardwareInputMap.clear();
+ serviceState.needInit = true;
+ }
}
}
}
@@ -774,7 +814,7 @@ public final class TvInputManagerService extends SystemService {
boolean shouldBind;
if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) {
shouldBind = !serviceState.sessionTokens.isEmpty()
- || (serviceState.isHardware && serviceState.neverConnected);
+ || (serviceState.isHardware && serviceState.needInit);
} else {
// For a non-current user,
// if sessionTokens is not empty, it contains recording sessions only
@@ -3404,13 +3444,13 @@ public final class TvInputManagerService extends SystemService {
private ServiceCallback callback;
private boolean bound;
private boolean reconnecting;
- private boolean neverConnected;
+ private boolean needInit;
private ServiceState(ComponentName component, int userId) {
this.component = component;
this.connection = new InputServiceConnection(component, userId);
this.isHardware = hasHardwarePermission(mContext.getPackageManager(), component);
- this.neverConnected = true;
+ this.needInit = true;
}
}
@@ -3618,11 +3658,9 @@ public final class TvInputManagerService extends SystemService {
}
ComponentName component = mTvInputHardwareManager.getInputMap().get(inputId).getComponent();
ServiceState serviceState = getServiceStateLocked(component, userId);
- boolean removed = serviceState.hardwareInputMap.remove(inputId) != null;
- if (removed) {
- buildTvInputListLocked(userId, null);
- mTvInputHardwareManager.removeHardwareInput(inputId);
- }
+ serviceState.hardwareInputMap.remove(inputId);
+ buildTvInputListLocked(userId, null);
+ mTvInputHardwareManager.removeHardwareInput(inputId);
}
private final class InputServiceConnection implements ServiceConnection {
@@ -3648,7 +3686,7 @@ public final class TvInputManagerService extends SystemService {
}
ServiceState serviceState = userState.serviceStateMap.get(mComponent);
serviceState.service = ITvInputService.Stub.asInterface(service);
- serviceState.neverConnected = false;
+ serviceState.needInit = false;
// Register a callback, if we need to.
if (serviceState.isHardware && serviceState.callback == null) {
@@ -3841,9 +3879,12 @@ public final class TvInputManagerService extends SystemService {
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- Slog.d(TAG, "ServiceCallback: removeHardwareInput, inputId: " + inputId +
- " by " + mComponent + ", userId: " + mUserId);
- removeHardwareInputLocked(inputId, mUserId);
+ if (mUserId == mCurrentUserId) {
+ Slog.d(TAG,
+ "ServiceCallback: removeHardwareInput, inputId: " + inputId + " by "
+ + mComponent + ", userId: " + mUserId);
+ removeHardwareInputLocked(inputId, mUserId);
+ }
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -4578,6 +4619,11 @@ public final class TvInputManagerService extends SystemService {
private final class HardwareListener implements TvInputHardwareManager.Listener {
@Override
public void onStateChanged(String inputId, int state) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "onStateChanged: inputId " + (inputId != null ? inputId : "null")
+ + ", state " + state);
+ }
synchronized (mLock) {
setStateLocked(inputId, state, mCurrentUserId);
}
@@ -4585,6 +4631,11 @@ public final class TvInputManagerService extends SystemService {
@Override
public void onHardwareDeviceAdded(TvInputHardwareInfo info) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "onHardwareDeviceAdded: TvInputHardwareInfo "
+ + (info != null ? info.toString() : "null"));
+ }
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
@@ -4607,6 +4658,11 @@ public final class TvInputManagerService extends SystemService {
@Override
public void onHardwareDeviceRemoved(TvInputHardwareInfo info) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "onHardwareDeviceRemoved: TvInputHardwareInfo "
+ + (info != null ? info.toString() : "null"));
+ }
synchronized (mLock) {
String relatedInputId =
mTvInputHardwareManager.getHardwareInputIdMap().get(info.getDeviceId());
@@ -4634,6 +4690,11 @@ public final class TvInputManagerService extends SystemService {
@Override
public void onHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "onHdmiDeviceAdded: HdmiDeviceInfo "
+ + (deviceInfo != null ? deviceInfo.toString() : "null"));
+ }
synchronized (mLock) {
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
@@ -4656,6 +4717,11 @@ public final class TvInputManagerService extends SystemService {
@Override
public void onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "onHdmiDeviceRemoved: HdmiDeviceInfo "
+ + (deviceInfo != null ? deviceInfo.toString() : "null"));
+ }
synchronized (mLock) {
String relatedInputId =
mTvInputHardwareManager.getHdmiInputIdMap().get(deviceInfo.getId());
@@ -4683,6 +4749,12 @@ public final class TvInputManagerService extends SystemService {
@Override
public void onHdmiDeviceUpdated(String inputId, HdmiDeviceInfo deviceInfo) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "onHdmiDeviceUpdated: inputId " + (inputId != null ? inputId : "null")
+ + ", deviceInfo: "
+ + (deviceInfo != null ? deviceInfo.toString() : "null"));
+ }
synchronized (mLock) {
Integer state;
switch (deviceInfo.getDevicePowerStatus()) {
diff --git a/services/core/java/com/android/server/uri/NeededUriGrants.java b/services/core/java/com/android/server/uri/NeededUriGrants.java
index 8c8f55304fbb..2fe61e00c97e 100644
--- a/services/core/java/com/android/server/uri/NeededUriGrants.java
+++ b/services/core/java/com/android/server/uri/NeededUriGrants.java
@@ -17,10 +17,13 @@
package com.android.server.uri;
import android.util.ArraySet;
+import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import com.android.server.am.NeededUriGrantsProto;
+import java.util.Objects;
+
/** List of {@link GrantUri} a process needs. */
public class NeededUriGrants {
final String targetPkg;
@@ -35,6 +38,20 @@ public class NeededUriGrants {
this.uris = new ArraySet<>();
}
+ public void merge(NeededUriGrants other) {
+ if (other == null) return;
+ if (!Objects.equals(this.targetPkg, other.targetPkg)
+ || this.targetUid != other.targetUid || this.flags != other.flags) {
+ Slog.wtf("NeededUriGrants",
+ "The other NeededUriGrants does not share the same targetUid, targetPkg or "
+ + "flags. It cannot be merged into this NeededUriGrants. This "
+ + "NeededUriGrants: " + this.toStringWithoutUri()
+ + ". Other NeededUriGrants: " + other.toStringWithoutUri());
+ } else {
+ this.uris.addAll(other.uris);
+ }
+ }
+
public void dumpDebug(ProtoOutputStream proto, long fieldId) {
long token = proto.start(fieldId);
proto.write(NeededUriGrantsProto.TARGET_PACKAGE, targetPkg);
@@ -47,4 +64,12 @@ public class NeededUriGrants {
}
proto.end(token);
}
+
+ public String toStringWithoutUri() {
+ return "NeededUriGrants{" +
+ "targetPkg='" + targetPkg + '\'' +
+ ", targetUid=" + targetUid +
+ ", flags=" + flags +
+ '}';
+ }
}
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index ae30fcde39e1..d119a08b0c85 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -442,8 +442,6 @@ class ActivityClientController extends IActivityClientController.Stub {
throw new IllegalArgumentException("File descriptors passed in Intent");
}
- mService.mAmInternal.addCreatorToken(resultData);
-
final ActivityRecord r;
synchronized (mGlobalLock) {
r = ActivityRecord.isInRootTaskLocked(token);
@@ -502,6 +500,8 @@ class ActivityClientController extends IActivityClientController.Stub {
r.app.setLastActivityFinishTimeIfNeeded(SystemClock.uptimeMillis());
}
+ mService.mAmInternal.addCreatorToken(resultData, r.packageName);
+
final long origId = Binder.clearCallingIdentity();
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "finishActivity");
try {
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 0580d4a5a4a3..c1f5a27b81e7 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -25,6 +25,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.os.FactoryTest.FACTORY_TEST_LOW_LEVEL;
+import static com.android.server.wm.ActivityStarter.Request.DEFAULT_INTENT_CREATOR_UID;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP;
@@ -441,6 +442,17 @@ public class ActivityStartController {
0 /* startFlags */, null /* profilerInfo */, userId, filterCallingUid,
callingPid);
aInfo = mService.mAmInternal.getActivityInfoForUser(aInfo, userId);
+ int creatorUid = DEFAULT_INTENT_CREATOR_UID;
+ String creatorPackage = null;
+ if (ActivityManagerService.IntentCreatorToken.isValid(intent)) {
+ ActivityManagerService.IntentCreatorToken creatorToken =
+ (ActivityManagerService.IntentCreatorToken) intent.getCreatorToken();
+ if (creatorToken.getCreatorUid() != filterCallingUid) {
+ creatorUid = creatorToken.getCreatorUid();
+ creatorPackage = creatorToken.getCreatorPackage();
+ }
+ // leave creatorUid as -1 if the intent creator is the same as the launcher
+ }
if (aInfo != null) {
try {
@@ -454,6 +466,24 @@ public class ActivityStartController {
return START_CANCELED;
}
+ if (creatorUid != DEFAULT_INTENT_CREATOR_UID) {
+ try {
+ NeededUriGrants creatorIntentGrants = mSupervisor.mService.mUgmInternal
+ .checkGrantUriPermissionFromIntent(intent, creatorUid,
+ aInfo.applicationInfo.packageName,
+ UserHandle.getUserId(aInfo.applicationInfo.uid));
+ if (intentGrants == null) {
+ intentGrants = creatorIntentGrants;
+ } else {
+ intentGrants.merge(creatorIntentGrants);
+ }
+ } catch (SecurityException securityException) {
+ ActivityStarter.logForIntentRedirect(
+ "Creator URI Grant Caused Exception.", intent, creatorUid,
+ creatorPackage, filterCallingUid, callingPackage);
+ // TODO b/368559093 - rethrow the securityException.
+ }
+ }
if ((aInfo.applicationInfo.privateFlags
& ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0) {
throw new IllegalArgumentException(
@@ -477,6 +507,8 @@ public class ActivityStartController {
.setCallingUid(callingUid)
.setCallingPackage(callingPackage)
.setCallingFeatureId(callingFeatureId)
+ .setIntentCreatorUid(creatorUid)
+ .setIntentCreatorPackage(creatorPackage)
.setRealCallingPid(realCallingPid)
.setRealCallingUid(realCallingUid)
.setActivityOptions(checkedOptions)
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 5b5bb88cac98..5d3ae54f0934 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -132,6 +132,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.HeavyWeightSwitcherActivity;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.protolog.ProtoLog;
+import com.android.server.am.ActivityManagerService.IntentCreatorToken;
import com.android.server.am.PendingIntentRecord;
import com.android.server.pm.InstantAppResolver;
import com.android.server.pm.PackageArchiver;
@@ -384,6 +385,7 @@ class ActivityStarter {
private static final int DEFAULT_CALLING_PID = 0;
static final int DEFAULT_REAL_CALLING_UID = -1;
static final int DEFAULT_REAL_CALLING_PID = 0;
+ static final int DEFAULT_INTENT_CREATOR_UID = -1;
IApplicationThread caller;
Intent intent;
@@ -404,6 +406,8 @@ class ActivityStarter {
@Nullable String callingFeatureId;
int realCallingPid = DEFAULT_REAL_CALLING_PID;
int realCallingUid = DEFAULT_REAL_CALLING_UID;
+ int intentCreatorUid = DEFAULT_INTENT_CREATOR_UID;
+ String intentCreatorPackage;
int startFlags;
SafeActivityOptions activityOptions;
boolean ignoreTargetSecurity;
@@ -464,6 +468,8 @@ class ActivityStarter {
callingPid = DEFAULT_CALLING_PID;
callingUid = DEFAULT_CALLING_UID;
callingPackage = null;
+ intentCreatorUid = DEFAULT_INTENT_CREATOR_UID;
+ intentCreatorPackage = null;
callingFeatureId = null;
realCallingPid = DEFAULT_REAL_CALLING_PID;
realCallingUid = DEFAULT_REAL_CALLING_UID;
@@ -556,12 +562,14 @@ class ActivityStarter {
// "resolved" calling UID, where we try our best to identify the
// actual caller that is starting this activity
int resolvedCallingUid = callingUid;
+ String resolvedCallingPackage = callingPackage;
if (caller != null) {
synchronized (supervisor.mService.mGlobalLock) {
final WindowProcessController callerApp = supervisor.mService
.getProcessController(caller);
if (callerApp != null) {
resolvedCallingUid = callerApp.mInfo.uid;
+ resolvedCallingPackage = callerApp.mInfo.packageName;
}
}
}
@@ -597,7 +605,23 @@ class ActivityStarter {
// Collect information about the target of the Intent.
activityInfo = supervisor.resolveActivity(intent, resolveInfo, startFlags,
profilerInfo);
-
+ // Check if the Intent was redirected
+ if ((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN)
+ != 0) {
+ ActivityStarter.logForIntentRedirect(
+ "Unparceled intent does not have a creator token set.", intent,
+ intentCreatorUid,
+ intentCreatorPackage, resolvedCallingUid, resolvedCallingPackage);
+ // TODO b/368559093 - eventually ramp up to throw SecurityException
+ }
+ if (IntentCreatorToken.isValid(intent)) {
+ IntentCreatorToken creatorToken = (IntentCreatorToken) intent.getCreatorToken();
+ if (creatorToken.getCreatorUid() != resolvedCallingUid) {
+ intentCreatorUid = creatorToken.getCreatorUid();
+ intentCreatorPackage = creatorToken.getCreatorPackage();
+ }
+ // leave intentCreatorUid as -1 if the intent creator is the same as the launcher
+ }
// Carefully collect grants without holding lock
if (activityInfo != null) {
if (android.security.Flags.contentUriPermissionApis()) {
@@ -607,11 +631,52 @@ class ActivityStarter {
UserHandle.getUserId(activityInfo.applicationInfo.uid),
activityInfo.requireContentUriPermissionFromCaller,
/* requestHashCode */ this.hashCode());
+ if (intentCreatorUid != DEFAULT_INTENT_CREATOR_UID) {
+ try {
+ NeededUriGrants creatorIntentGrants = supervisor.mService.mUgmInternal
+ .checkGrantUriPermissionFromIntent(intent, intentCreatorUid,
+ activityInfo.applicationInfo.packageName,
+ UserHandle.getUserId(activityInfo.applicationInfo.uid),
+ activityInfo.requireContentUriPermissionFromCaller,
+ /* requestHashCode */ this.hashCode());
+ if (intentGrants == null) {
+ intentGrants = creatorIntentGrants;
+ } else {
+ intentGrants.merge(creatorIntentGrants);
+ }
+ } catch (SecurityException securityException) {
+ ActivityStarter.logForIntentRedirect(
+ "Creator URI Grant Caused Exception.", intent, intentCreatorUid,
+ intentCreatorPackage, resolvedCallingUid,
+ resolvedCallingPackage);
+ // TODO b/368559093 - rethrow the securityException.
+ }
+ }
} else {
intentGrants = supervisor.mService.mUgmInternal
.checkGrantUriPermissionFromIntent(intent, resolvedCallingUid,
activityInfo.applicationInfo.packageName,
UserHandle.getUserId(activityInfo.applicationInfo.uid));
+ if (intentCreatorUid != DEFAULT_INTENT_CREATOR_UID && intentGrants != null) {
+ try {
+ NeededUriGrants creatorIntentGrants = supervisor.mService.mUgmInternal
+ .checkGrantUriPermissionFromIntent(intent, intentCreatorUid,
+ activityInfo.applicationInfo.packageName,
+ UserHandle.getUserId(
+ activityInfo.applicationInfo.uid));
+ if (intentGrants == null) {
+ intentGrants = creatorIntentGrants;
+ } else {
+ intentGrants.merge(creatorIntentGrants);
+ }
+ } catch (SecurityException securityException) {
+ ActivityStarter.logForIntentRedirect(
+ "Creator URI Grant Caused Exception.", intent, intentCreatorUid,
+ intentCreatorPackage, resolvedCallingUid,
+ resolvedCallingPackage);
+ // TODO b/368559093 - rethrow the securityException.
+ }
+ }
}
}
}
@@ -978,7 +1043,9 @@ class ActivityStarter {
int requestCode = request.requestCode;
int callingPid = request.callingPid;
int callingUid = request.callingUid;
- String callingPackage = request.callingPackage;
+ int intentCreatorUid = request.intentCreatorUid;
+ String intentCreatorPackage = request.intentCreatorPackage;
+ String intentCallingPackage = request.callingPackage;
String callingFeatureId = request.callingFeatureId;
final int realCallingPid = request.realCallingPid;
final int realCallingUid = request.realCallingUid;
@@ -1063,7 +1130,7 @@ class ActivityStarter {
// launched in the app flow to redirect to an activity picked by the user, where
// we want the final activity to consider it to have been launched by the
// previous app activity.
- callingPackage = sourceRecord.launchedFromPackage;
+ intentCallingPackage = sourceRecord.launchedFromPackage;
callingFeatureId = sourceRecord.launchedFromFeatureId;
}
}
@@ -1085,7 +1152,7 @@ class ActivityStarter {
if (packageArchiver.isIntentResolvedToArchivedApp(intent, mRequest.userId)) {
err = packageArchiver
.requestUnarchiveOnActivityStart(
- intent, callingPackage, mRequest.userId, realCallingUid);
+ intent, intentCallingPackage, mRequest.userId, realCallingUid);
}
}
}
@@ -1144,7 +1211,7 @@ class ActivityStarter {
boolean abort;
try {
abort = !mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho,
- requestCode, callingPid, callingUid, callingPackage, callingFeatureId,
+ requestCode, callingPid, callingUid, intentCallingPackage, callingFeatureId,
request.ignoreTargetSecurity, inTask != null, callerApp, resultRecord,
resultRootTask);
} catch (SecurityException e) {
@@ -1172,7 +1239,47 @@ class ActivityStarter {
abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid,
callingPid, resolvedType, aInfo.applicationInfo);
abort |= !mService.getPermissionPolicyInternal().checkStartActivity(intent, callingUid,
- callingPackage);
+ intentCallingPackage);
+
+ if (intentCreatorUid != Request.DEFAULT_INTENT_CREATOR_UID) {
+ try {
+ if (!mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho,
+ requestCode, 0, intentCreatorUid, intentCreatorPackage, "",
+ request.ignoreTargetSecurity, inTask != null, null, resultRecord,
+ resultRootTask)) {
+ logForIntentRedirect("Creator checkStartAnyActivityPermission Caused abortion.",
+ intent, intentCreatorUid, intentCreatorPackage, callingUid,
+ intentCallingPackage);
+ // TODO b/368559093 - set abort to true.
+ // abort = true;
+ }
+ } catch (SecurityException e) {
+ logForIntentRedirect("Creator checkStartAnyActivityPermission Caused Exception.",
+ intent, intentCreatorUid, intentCreatorPackage, callingUid,
+ intentCallingPackage);
+ // TODO b/368559093 - rethrow the exception.
+ //throw e;
+ }
+ if (!mService.mIntentFirewall.checkStartActivity(intent, intentCreatorUid, 0,
+ resolvedType, aInfo.applicationInfo)) {
+ logForIntentRedirect("Creator IntentFirewall.checkStartActivity Caused abortion.",
+ intent, intentCreatorUid, intentCreatorPackage, callingUid,
+ intentCallingPackage);
+ // TODO b/368559093 - set abort to true.
+ // abort = true;
+ }
+
+ if (!mService.getPermissionPolicyInternal().checkStartActivity(intent, intentCreatorUid,
+ intentCreatorPackage)) {
+ logForIntentRedirect(
+ "Creator PermissionPolicyService.checkStartActivity Caused abortion.",
+ intent, intentCreatorUid, intentCreatorPackage, callingUid,
+ intentCallingPackage);
+ // TODO b/368559093 - set abort to true.
+ // abort = true;
+ }
+ intent.removeCreatorTokenInfo();
+ }
// Merge the two options bundles, while realCallerOptions takes precedence.
ActivityOptions checkedOptions = options != null
@@ -1189,7 +1296,7 @@ class ActivityStarter {
balController.checkBackgroundActivityStart(
callingUid,
callingPid,
- callingPackage,
+ intentCallingPackage,
realCallingUid,
realCallingPid,
callerApp,
@@ -1210,7 +1317,7 @@ class ActivityStarter {
if (request.allowPendingRemoteAnimationRegistryLookup) {
checkedOptions = mService.getActivityStartController()
.getPendingRemoteAnimationRegistry()
- .overrideOptionsIfNeeded(callingPackage, checkedOptions);
+ .overrideOptionsIfNeeded(intentCallingPackage, checkedOptions);
}
if (mService.mController != null) {
try {
@@ -1226,7 +1333,8 @@ class ActivityStarter {
final TaskDisplayArea suggestedLaunchDisplayArea =
computeSuggestedLaunchDisplayArea(inTask, sourceRecord, checkedOptions);
- mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, callingPackage,
+ mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags,
+ intentCallingPackage,
callingFeatureId);
if (mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, inTaskFragment,
callingPid, callingUid, checkedOptions, suggestedLaunchDisplayArea)) {
@@ -1264,7 +1372,8 @@ class ActivityStarter {
if (mService.getPackageManagerInternalLocked().isPermissionsReviewRequired(
aInfo.packageName, userId)) {
final IIntentSender target = mService.getIntentSenderLocked(
- ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage, callingFeatureId,
+ ActivityManager.INTENT_SENDER_ACTIVITY, intentCallingPackage,
+ callingFeatureId,
callingUid, userId, null, null, 0, new Intent[]{intent},
new String[]{resolvedType}, PendingIntent.FLAG_CANCEL_CURRENT
| PendingIntent.FLAG_ONE_SHOT, null);
@@ -1327,7 +1436,8 @@ class ActivityStarter {
// app [on install success].
if (rInfo != null && rInfo.auxiliaryInfo != null) {
intent = createLaunchIntent(rInfo.auxiliaryInfo, request.ephemeralIntent,
- callingPackage, callingFeatureId, verificationBundle, resolvedType, userId);
+ intentCallingPackage, callingFeatureId, verificationBundle, resolvedType,
+ userId);
resolvedType = null;
callingUid = realCallingUid;
callingPid = realCallingPid;
@@ -1350,7 +1460,7 @@ class ActivityStarter {
.setCaller(callerApp)
.setLaunchedFromPid(callingPid)
.setLaunchedFromUid(callingUid)
- .setLaunchedFromPackage(callingPackage)
+ .setLaunchedFromPackage(intentCallingPackage)
.setLaunchedFromFeature(callingFeatureId)
.setIntent(intent)
.setResolvedType(resolvedType)
@@ -3308,6 +3418,16 @@ class ActivityStarter {
return this;
}
+ ActivityStarter setIntentCreatorUid(int uid) {
+ mRequest.intentCreatorUid = uid;
+ return this;
+ }
+
+ ActivityStarter setIntentCreatorPackage(String intentCreatorPackage) {
+ mRequest.intentCreatorPackage = intentCreatorPackage;
+ return this;
+ }
+
/**
* Sets the pid of the caller who requested to launch the activity.
*
@@ -3467,4 +3587,19 @@ class ActivityStarter {
pw.print(" mInTaskFragment=");
pw.println(mInTaskFragment);
}
+
+ static void logForIntentRedirect(String message, Intent intent, int intentCreatorUid,
+ String intentCreatorPackage, int callingUid, String callingPackage) {
+ String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid,
+ intentCreatorPackage, callingUid, callingPackage);
+ Slog.wtf(TAG, msg);
+ }
+
+ private static String getIntentRedirectPreventedLogMessage(String message, Intent intent,
+ int intentCreatorUid, String intentCreatorPackage, int callingUid,
+ String callingPackage) {
+ return "[IntentRedirect]" + message + " intentCreatorUid: " + intentCreatorUid
+ + "; intentCreatorPackage: " + intentCreatorPackage + "; callingUid: " + callingUid
+ + "; callingPackage: " + callingPackage + "; intent: " + intent;
+ }
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 4db478a13c92..5339753624d8 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -1228,7 +1228,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo,
String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo,
Bundle bOptions) {
- mAmInternal.addCreatorToken(intent);
+ mAmInternal.addCreatorToken(intent, callingPackage);
return startActivityAsUser(caller, callingPackage, callingFeatureId, intent, resolvedType,
resultTo, resultWho, requestCode, startFlags, profilerInfo, bOptions,
UserHandle.getCallingUserId());
@@ -1243,7 +1243,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
enforceNotIsolatedCaller(reason);
if (intents != null) {
for (Intent intent : intents) {
- mAmInternal.addCreatorToken(intent);
+ mAmInternal.addCreatorToken(intent, callingPackage);
}
}
userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, reason);
@@ -1275,7 +1275,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
@Nullable String callingFeatureId, Intent intent, String resolvedType,
IBinder resultTo, String resultWho, int requestCode, int startFlags,
ProfilerInfo profilerInfo, Bundle bOptions, int userId, boolean validateIncomingUser) {
- mAmInternal.addCreatorToken(intent);
+ mAmInternal.addCreatorToken(intent, callingPackage);
final SafeActivityOptions opts = SafeActivityOptions.fromBundle(bOptions);
assertPackageMatchesCallingUid(callingPackage);
@@ -1330,7 +1330,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
}
// Remove existing mismatch flag so it can be properly updated later
fillInIntent.removeExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH);
- mAmInternal.addCreatorToken(fillInIntent);
}
if (!(target instanceof PendingIntentRecord)) {
@@ -1339,6 +1338,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
PendingIntentRecord pir = (PendingIntentRecord) target;
+ if (fillInIntent != null) {
+ mAmInternal.addCreatorToken(fillInIntent, pir.getPackageName());
+ }
+
synchronized (mGlobalLock) {
// If this is coming from the currently resumed activity, it is
// effectively saying that app switches are allowed at this point.
@@ -1349,6 +1352,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
mAppSwitchesState = APP_SWITCH_ALLOW;
}
}
+
return pir.sendInner(caller, 0, fillInIntent, resolvedType, allowlistToken, null, null,
resultTo, resultWho, requestCode, flagsMask, flagsValues, bOptions);
}
@@ -1361,8 +1365,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
throw new IllegalArgumentException("File descriptors passed in Intent");
}
- mAmInternal.addCreatorToken(intent);
-
SafeActivityOptions options = SafeActivityOptions.fromBundle(bOptions);
synchronized (mGlobalLock) {
@@ -1376,6 +1378,9 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
SafeActivityOptions.abort(options);
return false;
}
+
+ mAmInternal.addCreatorToken(intent, r.packageName);
+
intent = new Intent(intent);
// Remove existing mismatch flag so it can be properly updated later
intent.removeExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH);
diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java
index 4d17ed24e734..eee4c86bc483 100644
--- a/services/core/java/com/android/server/wm/AppTaskImpl.java
+++ b/services/core/java/com/android/server/wm/AppTaskImpl.java
@@ -160,7 +160,7 @@ class AppTaskImpl extends IAppTask.Stub {
Intent intent, String resolvedType, Bundle bOptions) {
checkCallerOrSystemOrRoot();
mService.assertPackageMatchesCallingUid(callingPackage);
- mService.mAmInternal.addCreatorToken(intent);
+ mService.mAmInternal.addCreatorToken(intent, callingPackage);
int callingUser = UserHandle.getCallingUserId();
Task task;
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 94cd2e64b057..70f9ebb0e61e 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -34,6 +34,7 @@ import static com.android.server.wm.BackNavigationProto.LAST_BACK_TYPE;
import static com.android.server.wm.BackNavigationProto.MAIN_OPEN_ACTIVITY;
import static com.android.server.wm.BackNavigationProto.SHOW_WALLPAPER;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
+import static com.android.server.wm.WindowContainer.SYNC_STATE_NONE;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -1237,8 +1238,9 @@ class BackNavigationController {
}
allWindowDrawn &= next.mAppWindowDrawn;
}
- // Do not remove until transition ready.
- if (!activity.isVisible()) {
+ // Do not remove windowless surfaces if the transaction has not been applied.
+ if (activity.getSyncTransactionCommitCallbackDepth() > 0
+ || activity.mSyncState != SYNC_STATE_NONE) {
return;
}
if (allWindowDrawn) {
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index a4fe0647ea79..9d21183c6c03 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -131,7 +131,9 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal
final boolean changed = !com.android.window.flags.Flags.filterIrrelevantInputDeviceChange()
|| updateLastInputConfigurationSources();
- if (changed) {
+ // Even if the input devices are not changed, there could be other pending changes
+ // during booting. It's fine to apply earlier.
+ if (changed || !mService.mDisplayEnabled) {
synchronized (mService.mGlobalLock) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "inputDeviceConfigChanged");
mService.mRoot.forAllDisplays(DisplayContent::sendNewConfiguration);
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 6067a9972bbe..1d4d6eb82c44 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -409,7 +409,7 @@ class InsetsSourceProvider {
}
final Point position = getWindowFrameSurfacePosition();
if (!mPosition.equals(position)) {
- mPosition.set(position.x, position.y);
+ mPosition.set(position);
if (windowState != null && windowState.getWindowFrames().didFrameSizeChange()
&& windowState.mWinAnimator.getShown() && mWindowContainer.okToDisplay()) {
mHasPendingPosition = true;
@@ -553,6 +553,7 @@ class InsetsSourceProvider {
}
boolean initiallyVisible = mClientVisible;
final Point surfacePosition = getWindowFrameSurfacePosition();
+ mPosition.set(surfacePosition);
mAdapter = new ControlAdapter(surfacePosition);
if (mSource.getType() == WindowInsets.Type.ime()) {
if (android.view.inputmethod.Flags.refactorInsetsController()) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index a4e4deb9ed7d..4861341f830a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5742,9 +5742,9 @@ class Task extends TaskFragment {
}
private boolean canMoveTaskToBack(Task task) {
- // Checks whether a task is a child of this task because it can be reparetned when
+ // Checks whether a task is a child of this task because it can be reparented when
// transition is deferred.
- if (task != this && task.getParent() != this) {
+ if (task != this && !task.isDescendantOf(this)) {
return false;
}
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 86adc1944371..1a107c24a16a 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -133,7 +133,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio
private int mRapidActivityLaunchCount;
// all about the first app in the process
- final ApplicationInfo mInfo;
+ volatile ApplicationInfo mInfo;
final String mName;
final int mUid;
@@ -1805,12 +1805,17 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio
Configuration overrideConfig = new Configuration(r.getRequestedOverrideConfiguration());
overrideConfig.assetsSeq = assetSeq;
r.onRequestedOverrideConfigurationChanged(overrideConfig);
+ r.updateApplicationInfo(mInfo);
if (r.isVisibleRequested()) {
r.ensureActivityConfiguration();
}
}
}
+ public void updateApplicationInfo(ApplicationInfo aInfo) {
+ mInfo = aInfo;
+ }
+
/**
* This is called for sending {@link android.app.servertransaction.LaunchActivityItem}.
* The caller must call {@link #setLastReportedConfiguration} if the delivered configuration
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index b9727f9f3970..4103c477a8ae 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -327,8 +327,6 @@ public final class SystemServer implements Dumpable {
* Implementation class names for services in the {@code SYSTEMSERVERCLASSPATH}
* from {@code PRODUCT_SYSTEM_SERVER_JARS} that are *not* in {@code services.jar}.
*/
- private static final String ARC_NETWORK_SERVICE_CLASS =
- "com.android.server.arc.net.ArcNetworkService";
private static final String ARC_PERSISTENT_DATA_BLOCK_SERVICE_CLASS =
"com.android.server.arc.persistent_data_block.ArcPersistentDataBlockService";
private static final String ARC_SYSTEM_HEALTH_SERVICE =
@@ -2101,24 +2099,13 @@ public final class SystemServer implements Dumpable {
if (context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_WIFI)) {
// Wifi Service must be started first for wifi-related services.
- if (!isArc) {
- t.traceBegin("StartWifi");
- mSystemServiceManager.startServiceFromJar(
- WIFI_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH);
- t.traceEnd();
- t.traceBegin("StartWifiScanning");
- mSystemServiceManager.startServiceFromJar(
- WIFI_SCANNING_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH);
- t.traceEnd();
- }
- }
-
- // ARC - ArcNetworkService registers the ARC network stack and replaces the
- // stock WiFi service in both ARC++ container and ARCVM. Always starts the ARC network
- // stack regardless of whether FEATURE_WIFI is enabled/disabled (b/254755875).
- if (isArc) {
- t.traceBegin("StartArcNetworking");
- mSystemServiceManager.startService(ARC_NETWORK_SERVICE_CLASS);
+ t.traceBegin("StartWifi");
+ mSystemServiceManager.startServiceFromJar(
+ WIFI_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH);
+ t.traceEnd();
+ t.traceBegin("StartWifiScanning");
+ mSystemServiceManager.startServiceFromJar(
+ WIFI_SCANNING_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH);
t.traceEnd();
}
diff --git a/services/people/java/com/android/server/people/data/CallLogQueryHelper.java b/services/people/java/com/android/server/people/data/CallLogQueryHelper.java
index ff901af3defa..30df4c821134 100644
--- a/services/people/java/com/android/server/people/data/CallLogQueryHelper.java
+++ b/services/people/java/com/android/server/people/data/CallLogQueryHelper.java
@@ -96,6 +96,8 @@ class CallLogQueryHelper {
} catch (SecurityException ex) {
Slog.e(TAG, "Query call log failed: " + ex);
return false;
+ } catch (Exception e) {
+ Slog.e(TAG, "Exception when querying call log.", e);
}
return hasResults;
}
diff --git a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java
index 2505abf2d160..2bd9d87b0124 100644
--- a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java
+++ b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java
@@ -151,9 +151,11 @@ class ContactsQueryHelper {
found = true;
}
} catch (SQLiteException exception) {
- Slog.w("SQLite exception when querying contacts.", exception);
+ Slog.w(TAG, "SQLite exception when querying contacts.", exception);
} catch (IllegalArgumentException exception) {
- Slog.w("Illegal Argument exception when querying contacts.", exception);
+ Slog.w(TAG, "Illegal Argument exception when querying contacts.", exception);
+ } catch (Exception exception) {
+ Slog.e(TAG, "Exception when querying contacts.", exception);
}
if (found && lookupKey != null && hasPhoneNumber) {
return queryPhoneNumber(lookupKey);
@@ -181,6 +183,8 @@ class ContactsQueryHelper {
mPhoneNumber = cursor.getString(phoneNumIdx);
}
}
+ } catch (Exception exception) {
+ Slog.e(TAG, "Exception when querying contact phone number.", exception);
}
return true;
}
diff --git a/services/people/java/com/android/server/people/data/MmsQueryHelper.java b/services/people/java/com/android/server/people/data/MmsQueryHelper.java
index 39dba9c73ba2..414a523fb186 100644
--- a/services/people/java/com/android/server/people/data/MmsQueryHelper.java
+++ b/services/people/java/com/android/server/people/data/MmsQueryHelper.java
@@ -100,6 +100,8 @@ class MmsQueryHelper {
}
}
}
+ } catch (Exception e) {
+ Slog.e(TAG, "Exception when querying MMS table.", e);
} finally {
Binder.defaultBlockingForCurrentThread();
}
@@ -133,6 +135,8 @@ class MmsQueryHelper {
address = cursor.getString(addrIndex);
}
}
+ } catch (Exception e) {
+ Slog.e(TAG, "Exception when querying MMS address table.", e);
}
if (!Mms.isPhoneNumber(address)) {
return null;
diff --git a/services/people/java/com/android/server/people/data/SmsQueryHelper.java b/services/people/java/com/android/server/people/data/SmsQueryHelper.java
index a5eb3a581616..f8ff3abc8e4c 100644
--- a/services/people/java/com/android/server/people/data/SmsQueryHelper.java
+++ b/services/people/java/com/android/server/people/data/SmsQueryHelper.java
@@ -98,6 +98,8 @@ class SmsQueryHelper {
}
}
}
+ } catch (Exception e) {
+ Slog.e(TAG, "Exception when querying SMS table.", e);
} finally {
Binder.defaultBlockingForCurrentThread();
}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
index cbca434a6bb6..8af5b2081b81 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
@@ -21,6 +21,7 @@ import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_DEFAULT
import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_DENIED
import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_GRANTED
+import android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED
import android.content.pm.PackageManager
import android.content.pm.verify.domain.DomainSet
import android.os.Parcel
@@ -33,6 +34,11 @@ import com.android.internal.os.BackgroundThread
import com.android.server.pm.verify.pkg.VerifierController
import com.android.server.testutils.whenever
import com.google.common.truth.Truth.assertThat
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
import libcore.io.IoUtils
import org.junit.Before
import org.junit.Rule
@@ -46,11 +52,6 @@ import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileNotFoundException
-import java.io.FileOutputStream
-import java.io.IOException
@Presubmit
class PackageInstallerSessionTest {
@@ -197,7 +198,8 @@ class PackageInstallerSessionTest {
/* stagedSessionErrorCode */ PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
/* stagedSessionErrorMessage */ "some error",
/* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar")),
- /* VerifierController */ mock(VerifierController::class.java)
+ /* VerifierController */ mock(VerifierController::class.java),
+ VERIFICATION_POLICY_BLOCK_FAIL_CLOSED
)
}
@@ -339,6 +341,7 @@ class PackageInstallerSessionTest {
assertThat(expected.childSessionIds).asList()
.containsExactlyElementsIn(actual.childSessionIds.toList())
assertThat(expected.preVerifiedDomains).isEqualTo(actual.preVerifiedDomains)
+ assertThat(expected.verificationPolicy).isEqualTo(actual.verificationPolicy)
}
private fun assertInstallSourcesEquivalent(expected: InstallSource, actual: InstallSource) {
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java
index be094b0152bc..37b23b107ecd 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java
@@ -16,6 +16,9 @@
package com.android.server.pm.verify.pkg;
+import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED;
+import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_OPEN;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -92,6 +95,9 @@ public class VerifierControllerTest {
private static final long TEST_TIMEOUT_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(1);
private static final long TEST_MAX_TIMEOUT_DURATION_MILLIS =
TimeUnit.MINUTES.toMillis(10);
+ private static final long TEST_VERIFIER_CONNECTION_TIMEOUT_DURATION_MILLIS =
+ TimeUnit.SECONDS.toMillis(10);
+ private static final int TEST_POLICY = VERIFICATION_POLICY_BLOCK_FAIL_CLOSED;
private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>();
private final PersistableBundle mTestExtensionParams = new PersistableBundle();
@@ -124,6 +130,9 @@ public class VerifierControllerTest {
TEST_TIMEOUT_DURATION_MILLIS);
when(mInjector.getMaxVerificationExtendedTimeoutMillis()).thenReturn(
TEST_MAX_TIMEOUT_DURATION_MILLIS);
+ when(mInjector.getVerifierConnectionTimeoutMillis()).thenReturn(
+ TEST_VERIFIER_CONNECTION_TIMEOUT_DURATION_MILLIS
+ );
// Mock time forward as the code continues to check for the current time
when(mInjector.getCurrentTimeMillis())
.thenReturn(TEST_REQUEST_START_TIME)
@@ -159,12 +168,12 @@ public class VerifierControllerTest {
.isFalse();
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isFalse();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isFalse();
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ true)).isFalse();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ true)).isFalse();
verifyZeroInteractions(mSessionCallback);
}
@@ -200,8 +209,8 @@ public class VerifierControllerTest {
ServiceConnector.ServiceLifecycleCallbacks<IVerifierService> callbacks = captor.getValue();
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
verify(mMockService, times(1)).onVerificationRequired(any(VerificationSession.class));
callbacks.onBinderDied();
// Test that nothing crashes if the service connection is lost
@@ -212,12 +221,12 @@ public class VerifierControllerTest {
verifyNoMoreInteractions(mMockService);
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ true)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ true)).isTrue();
mVerifierController.notifyVerificationTimeout(TEST_ID);
verify(mMockService, times(1)).onVerificationTimeout(eq(TEST_ID));
}
@@ -226,8 +235,8 @@ public class VerifierControllerTest {
public void testNotifyPackageNameAvailable() throws Exception {
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
mVerifierController.notifyPackageNameAvailable(TEST_PACKAGE_NAME);
verify(mMockService).onPackageNameAvailable(eq(TEST_PACKAGE_NAME));
}
@@ -236,8 +245,8 @@ public class VerifierControllerTest {
public void testNotifyVerificationCancelled() throws Exception {
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
mVerifierController.notifyVerificationCancelled(TEST_PACKAGE_NAME);
verify(mMockService).onVerificationCancelled(eq(TEST_PACKAGE_NAME));
}
@@ -248,8 +257,8 @@ public class VerifierControllerTest {
ArgumentCaptor.forClass(VerificationSession.class);
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
verify(mMockService).onVerificationRequired(captor.capture());
VerificationSession session = captor.getValue();
assertThat(session.getId()).isEqualTo(TEST_ID);
@@ -276,8 +285,8 @@ public class VerifierControllerTest {
ArgumentCaptor.forClass(VerificationSession.class);
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ true)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ true)).isTrue();
verify(mMockService).onVerificationRetry(captor.capture());
VerificationSession session = captor.getValue();
assertThat(session.getId()).isEqualTo(TEST_ID);
@@ -302,8 +311,8 @@ public class VerifierControllerTest {
public void testNotifyVerificationTimeout() throws Exception {
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ true)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ true)).isTrue();
mVerifierController.notifyVerificationTimeout(TEST_ID);
verify(mMockService).onVerificationTimeout(eq(TEST_ID));
}
@@ -320,8 +329,8 @@ public class VerifierControllerTest {
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
verify(mHandler, times(1)).sendMessageAtTime(any(Message.class), anyLong());
verify(mSessionCallback, times(1)).onTimeout();
verify(mInjector, times(2)).getCurrentTimeMillis();
@@ -339,8 +348,8 @@ public class VerifierControllerTest {
.thenAnswer(i -> true);
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
verify(mHandler, times(1)).sendMessageAtTime(any(Message.class), anyLong());
verify(mSessionCallback, times(1)).onTimeout();
verify(mInjector, times(2)).getCurrentTimeMillis();
@@ -350,8 +359,8 @@ public class VerifierControllerTest {
ArgumentCaptor.forClass(VerificationSession.class);
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ true)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ true)).isTrue();
verify(mMockService).onVerificationRetry(captor.capture());
VerificationSession session = captor.getValue();
VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build();
@@ -367,8 +376,8 @@ public class VerifierControllerTest {
ArgumentCaptor.forClass(VerificationSession.class);
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
verify(mMockService).onVerificationRequired(captor.capture());
VerificationSession session = captor.getValue();
session.reportVerificationIncomplete(VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN);
@@ -383,8 +392,8 @@ public class VerifierControllerTest {
ArgumentCaptor.forClass(VerificationSession.class);
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
verify(mMockService).onVerificationRequired(captor.capture());
VerificationSession session = captor.getValue();
VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build();
@@ -401,8 +410,8 @@ public class VerifierControllerTest {
ArgumentCaptor.forClass(VerificationSession.class);
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
verify(mMockService).onVerificationRequired(captor.capture());
VerificationSession session = captor.getValue();
VerificationStatus status = new VerificationStatus.Builder()
@@ -421,8 +430,8 @@ public class VerifierControllerTest {
ArgumentCaptor.forClass(VerificationSession.class);
assertThat(mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false)).isTrue();
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false)).isTrue();
verify(mMockService).onVerificationRequired(captor.capture());
VerificationSession session = captor.getValue();
VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build();
@@ -439,8 +448,8 @@ public class VerifierControllerTest {
ArgumentCaptor.forClass(VerificationSession.class);
mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false);
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false);
verify(mMockService).onVerificationRequired(captor.capture());
VerificationSession session = captor.getValue();
final long initialTimeoutTime = TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS;
@@ -456,8 +465,8 @@ public class VerifierControllerTest {
ArgumentCaptor.forClass(VerificationSession.class);
mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false);
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false);
verify(mMockService).onVerificationRequired(captor.capture());
VerificationSession session = captor.getValue();
final long initialTimeoutTime = TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS;
@@ -493,10 +502,27 @@ public class VerifierControllerTest {
.thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS + 1);
mVerifierController.startVerificationSession(
mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
- TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
- /* retry= */ false);
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false);
verify(mHandler, times(3)).sendMessageAtTime(any(Message.class), anyLong());
verify(mInjector, times(6)).getCurrentTimeMillis();
verify(mSessionCallback, times(1)).onTimeout();
}
+
+ @Test
+ public void testPolicyOverride() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams,
+ mSessionCallback, /* retry= */ false);
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ final int policy = VERIFICATION_POLICY_BLOCK_FAIL_OPEN;
+ when(mSessionCallback.setVerificationPolicy(eq(policy))).thenReturn(true);
+ assertThat(session.setVerificationPolicy(policy)).isTrue();
+ assertThat(session.getVerificationPolicy()).isEqualTo(policy);
+ verify(mSessionCallback, times(1)).setVerificationPolicy(eq(policy));
+ }
}
diff --git a/services/tests/appfunctions/Android.bp b/services/tests/appfunctions/Android.bp
index c841643c6654..836f90b992d6 100644
--- a/services/tests/appfunctions/Android.bp
+++ b/services/tests/appfunctions/Android.bp
@@ -36,7 +36,9 @@ android_test {
"androidx.test.core",
"androidx.test.runner",
"androidx.test.ext.truth",
+ "androidx.core_core-ktx",
"kotlin-test",
+ "kotlinx_coroutines_test",
"platform-test-annotations",
"services.appfunctions",
"servicestests-core-utils",
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/AppFunctionManagerServiceImplTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/AppFunctionManagerServiceImplTest.kt
new file mode 100644
index 000000000000..a69e9025bfa0
--- /dev/null
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/AppFunctionManagerServiceImplTest.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.app.appfunctions.flags.Flags
+import android.content.Context
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER)
+class AppFunctionManagerServiceImplTest {
+ @get:Rule
+ val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private val serviceImpl = AppFunctionManagerServiceImpl(context)
+
+ @Test
+ fun testGetLockForPackage_samePackage() {
+ val packageName = "com.example.app"
+ val lock1 = serviceImpl.getLockForPackage(packageName)
+ val lock2 = serviceImpl.getLockForPackage(packageName)
+
+ // Assert that the same lock object is returned for the same package name
+ assertThat(lock1).isEqualTo(lock2)
+ }
+
+ @Test
+ fun testGetLockForPackage_differentPackages() {
+ val packageName1 = "com.example.app1"
+ val packageName2 = "com.example.app2"
+ val lock1 = serviceImpl.getLockForPackage(packageName1)
+ val lock2 = serviceImpl.getLockForPackage(packageName2)
+
+ // Assert that different lock objects are returned for different package names
+ assertThat(lock1).isNotEqualTo(lock2)
+ }
+
+ @Ignore("Hard to deterministically trigger the garbage collector.")
+ @Test
+ fun testWeakReference_garbageCollected_differentLockAfterGC() = runTest {
+ // Create a large number of temporary objects to put pressure on the GC
+ val tempObjects = MutableList<Any?>(10000000) { Any() }
+ var callingPackage: String? = "com.example.app"
+ var lock1: Any? = serviceImpl.getLockForPackage(callingPackage)
+ callingPackage = null // Set the key to null
+ val lock1Hash = lock1.hashCode()
+ lock1 = null
+
+ // Create memory pressure
+ repeat(3) {
+ for (i in 1..100) {
+ "a".repeat(10000)
+ }
+ System.gc() // Suggest garbage collection
+ System.runFinalization()
+ }
+ // Get the lock again - it should be a different object now
+ val lock2 = serviceImpl.getLockForPackage("com.example.app")
+ // Assert that the lock objects are different
+ assertThat(lock1Hash).isNotEqualTo(lock2.hashCode())
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
index 6ccc03709b4f..2a825f35bf62 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -107,6 +107,7 @@ import android.os.IBinder;
import android.os.IProgressListener;
import android.os.Looper;
import android.os.Message;
+import android.os.Parcel;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -1309,12 +1310,13 @@ public class ActivityManagerServiceTest {
intent.putExtra("EXTRA_INTENT0", extraIntent);
intent.collectExtraIntentKeys();
- mAms.addCreatorToken(intent);
+ mAms.addCreatorToken(intent, TEST_PACKAGE);
ActivityManagerService.IntentCreatorToken token =
(ActivityManagerService.IntentCreatorToken) extraIntent.getCreatorToken();
assertThat(token).isNotNull();
assertThat(token.getCreatorUid()).isEqualTo(mInjector.getCallingUid());
+ assertThat(token.getCreatorPackage()).isEqualTo(TEST_PACKAGE);
}
@Test
@@ -1330,7 +1332,7 @@ public class ActivityManagerServiceTest {
fillinIntent.collectExtraIntentKeys();
intent.fillIn(fillinIntent, FILL_IN_ACTION);
- mAms.addCreatorToken(fillinIntent);
+ mAms.addCreatorToken(fillinIntent, TEST_PACKAGE);
fillinExtraIntent = intent.getParcelableExtra("FILLIN_EXTRA_INTENT0", Intent.class);
@@ -1338,6 +1340,49 @@ public class ActivityManagerServiceTest {
(ActivityManagerService.IntentCreatorToken) fillinExtraIntent.getCreatorToken();
assertThat(token).isNotNull();
assertThat(token.getCreatorUid()).isEqualTo(mInjector.getCallingUid());
+ assertThat(token.getCreatorPackage()).isEqualTo(TEST_PACKAGE);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.security.Flags.FLAG_PREVENT_INTENT_REDIRECT)
+ public void testCheckCreatorToken() {
+ Intent intent = new Intent();
+ Intent extraIntent = new Intent("EXTRA_INTENT_ACTION");
+ intent.putExtra("EXTRA_INTENT", extraIntent);
+
+ intent.collectExtraIntentKeys();
+
+ // mimic client hack and sneak in an extra intent without going thru collectExtraIntentKeys.
+ Intent extraIntent2 = new Intent("EXTRA_INTENT_ACTION2");
+ intent.putExtra("EXTRA_INTENT2", extraIntent2);
+
+ // mock parceling on the client side, unparcling on the system server side, then
+ // addCreatorToken on system server side.
+ final Parcel parcel = Parcel.obtain();
+ intent.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ Intent newIntent = new Intent();
+ newIntent.readFromParcel(parcel);
+ intent = newIntent;
+ mAms.addCreatorToken(intent, TEST_PACKAGE);
+ // entering the target app's process.
+ intent.checkCreatorToken();
+
+ Intent extraIntent3 = new Intent("EXTRA_INTENT_ACTION3");
+ intent.putExtra("EXTRA_INTENT3", extraIntent3);
+
+ extraIntent = intent.getParcelableExtra("EXTRA_INTENT", Intent.class);
+ extraIntent2 = intent.getParcelableExtra("EXTRA_INTENT2", Intent.class);
+ extraIntent3 = intent.getParcelableExtra("EXTRA_INTENT3", Intent.class);
+
+ assertThat(extraIntent.getExtendedFlags()
+ & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0);
+ // sneaked in intent should have EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN set.
+ assertThat(extraIntent2.getExtendedFlags()
+ & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isNotEqualTo(0);
+ // local created intent should not have EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN set.
+ assertThat(extraIntent3.getExtendedFlags()
+ & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0);
}
private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq,
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 9781851da7e6..5c718d982476 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -25,7 +25,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSess
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.job.Flags.FLAG_COUNT_QUOTA_FIX;
-import static com.android.server.job.Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS;
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
@@ -77,9 +76,12 @@ import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.DeviceConfig;
import android.util.ArraySet;
import android.util.SparseBooleanArray;
@@ -90,6 +92,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.PowerAllowlistInternal;
+import com.android.server.job.Flags;
import com.android.server.job.JobSchedulerInternal;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobStore;
@@ -131,6 +134,7 @@ public class QuotaControllerTest {
private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests";
private static final int SOURCE_USER_ID = 0;
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private QuotaController mQuotaController;
private QuotaController.QcConstants mQcConstants;
private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants();
@@ -930,7 +934,8 @@ public class QuotaControllerTest {
* Tests that getExecutionStatsLocked returns the correct stats.
*/
@Test
- public void testGetExecutionStatsLocked_Values() {
+ @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetExecutionStatsLocked_Values_LegacyDefaultBucketWindowSizes() {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(now - (mQcConstants.WINDOW_SIZE_RARE_MS - HOUR_IN_MILLIS),
@@ -1015,11 +1020,112 @@ public class QuotaControllerTest {
}
}
+ @Test
+ @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetExecutionStatsLocked_Values_NewDefaultBucketWindowSizes() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
+
+ ExecutionStats expectedStats = new ExecutionStats();
+
+ // Exempted
+ expectedStats.allowedTimePerPeriodMs = mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS;
+ expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_EXEMPTED_MS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_EXEMPTED;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_EXEMPTED;
+ expectedStats.expirationTimeElapsed = now + 14 * MINUTE_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 5;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.sessionCountInWindow = 1;
+ synchronized (mQuotaController.mLock) {
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ EXEMPTED_INDEX));
+ }
+
+ // Active
+ expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_ACTIVE_MS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE;
+ // There is only one session in the past active bucket window, the empty time for this
+ // window is the bucket window size - duration of the session.
+ expectedStats.expirationTimeElapsed = now + 24 * MINUTE_IN_MILLIS;
+ synchronized (mQuotaController.mLock) {
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ ACTIVE_INDEX));
+ }
+
+ // Working
+ expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_WORKING_MS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING;
+ expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 10;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.sessionCountInWindow = 2;
+ expectedStats.inQuotaTimeElapsed = now + 2 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS
+ + mQcConstants.IN_QUOTA_BUFFER_MS;
+ synchronized (mQuotaController.mLock) {
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ WORKING_INDEX));
+ }
+
+ // Frequent
+ expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_FREQUENT_MS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT;
+ expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 15;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.sessionCountInWindow = 3;
+ expectedStats.inQuotaTimeElapsed = now + 10 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS
+ + mQcConstants.IN_QUOTA_BUFFER_MS;
+ synchronized (mQuotaController.mLock) {
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", FREQUENT_INDEX));
+ }
+
+ // Rare
+ expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_RARE_MS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE;
+ expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 20;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.sessionCountInWindow = 4;
+ expectedStats.inQuotaTimeElapsed = now + 22 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS
+ + mQcConstants.IN_QUOTA_BUFFER_MS;
+ synchronized (mQuotaController.mLock) {
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ RARE_INDEX));
+ }
+ }
+
/**
* Tests that getExecutionStatsLocked returns the correct stats soon after device startup.
*/
@Test
- public void testGetExecutionStatsLocked_Values_BeginningOfTime() {
+ @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetExecutionStatsLocked_Values_BeginningOfTime_LegacyDefaultBucketWindowSizes() {
// Set time to 3 minutes after boot.
advanceElapsedClock(-JobSchedulerService.sElapsedRealtimeClock.millis());
advanceElapsedClock(3 * MINUTE_IN_MILLIS);
@@ -1042,7 +1148,8 @@ public class QuotaControllerTest {
expectedStats.sessionCountInWindow = 1;
synchronized (mQuotaController.mLock) {
assertEquals(expectedStats,
- mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX));
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ ACTIVE_INDEX));
}
// Working
@@ -1052,7 +1159,8 @@ public class QuotaControllerTest {
expectedStats.expirationTimeElapsed = 2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
synchronized (mQuotaController.mLock) {
assertEquals(expectedStats,
- mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX));
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ WORKING_INDEX));
}
// Frequent
@@ -1073,7 +1181,83 @@ public class QuotaControllerTest {
expectedStats.expirationTimeElapsed = 24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
synchronized (mQuotaController.mLock) {
assertEquals(expectedStats,
- mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ RARE_INDEX));
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetExecutionStatsLocked_Values_BeginningOfTime_NewDefaultBucketWindowSizes() {
+ // Set time to 3 minutes after boot.
+ advanceElapsedClock(-JobSchedulerService.sElapsedRealtimeClock.millis());
+ advanceElapsedClock(3 * MINUTE_IN_MILLIS);
+
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 2), false);
+
+ ExecutionStats expectedStats = new ExecutionStats();
+
+ // Exempted
+ expectedStats.allowedTimePerPeriodMs = mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS;
+ expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_EXEMPTED_MS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE;
+ expectedStats.expirationTimeElapsed = 10 * MINUTE_IN_MILLIS + 11 * MINUTE_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 2;
+ expectedStats.executionTimeInMaxPeriodMs = MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 2;
+ expectedStats.sessionCountInWindow = 1;
+ synchronized (mQuotaController.mLock) {
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ EXEMPTED_INDEX));
+ }
+
+ // Active
+ expectedStats.allowedTimePerPeriodMs = mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS;
+ expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_ACTIVE_MS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE;
+ expectedStats.expirationTimeElapsed = 20 * MINUTE_IN_MILLIS + 11 * MINUTE_IN_MILLIS;
+ synchronized (mQuotaController.mLock) {
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ ACTIVE_INDEX));
+ }
+
+ // Working
+ expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_WORKING_MS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING;
+ expectedStats.expirationTimeElapsed = 4 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
+ synchronized (mQuotaController.mLock) {
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ WORKING_INDEX));
+ }
+
+ // Frequent
+ expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_FREQUENT_MS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT;
+ expectedStats.expirationTimeElapsed = 12 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
+ synchronized (mQuotaController.mLock) {
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", FREQUENT_INDEX));
+ }
+
+ // Rare
+ expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_RARE_MS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE;
+ expectedStats.expirationTimeElapsed = 24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
+ synchronized (mQuotaController.mLock) {
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test",
+ RARE_INDEX));
}
}
@@ -1081,7 +1265,8 @@ public class QuotaControllerTest {
* Tests that getExecutionStatsLocked returns the correct timing session stats when coalescing.
*/
@Test
- public void testGetExecutionStatsLocked_CoalescingSessions() {
+ @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetExecutionStatsLocked_CoalescingSessions_LegacyDefaultBucketWindowSizes() {
for (int i = 0; i < 10; ++i) {
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(
@@ -1098,12 +1283,14 @@ public class QuotaControllerTest {
advanceElapsedClock(54 * SECOND_IN_MILLIS);
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(
- JobSchedulerService.sElapsedRealtimeClock.millis(), 500, 1), false);
+ JobSchedulerService.sElapsedRealtimeClock.millis(),
+ 500, 1), false);
advanceElapsedClock(500);
advanceElapsedClock(400);
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(
- JobSchedulerService.sElapsedRealtimeClock.millis(), 100, 1), false);
+ JobSchedulerService.sElapsedRealtimeClock.millis(),
+ 100, 1), false);
advanceElapsedClock(100);
advanceElapsedClock(5 * SECOND_IN_MILLIS);
}
@@ -1229,6 +1416,164 @@ public class QuotaControllerTest {
}
}
+ @Test
+ @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetExecutionStatsLocked_CoalescingSessions_NewDefaultBucketWindowSizes() {
+ for (int i = 0; i < 20; ++i) {
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(
+ JobSchedulerService.sElapsedRealtimeClock.millis(),
+ 5 * MINUTE_IN_MILLIS, 5), false);
+ advanceElapsedClock(5 * MINUTE_IN_MILLIS);
+ advanceElapsedClock(5 * MINUTE_IN_MILLIS);
+ for (int j = 0; j < 5; ++j) {
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(
+ JobSchedulerService.sElapsedRealtimeClock.millis(),
+ MINUTE_IN_MILLIS, 2), false);
+ advanceElapsedClock(MINUTE_IN_MILLIS);
+ advanceElapsedClock(54 * SECOND_IN_MILLIS);
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(
+ JobSchedulerService.sElapsedRealtimeClock.millis(), 500, 1), false);
+ advanceElapsedClock(500);
+ advanceElapsedClock(400);
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(
+ JobSchedulerService.sElapsedRealtimeClock.millis(), 100, 1), false);
+ advanceElapsedClock(100);
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ }
+ advanceElapsedClock(40 * MINUTE_IN_MILLIS);
+ }
+
+ setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, 0);
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.invalidateAllExecutionStatsLocked();
+ assertEquals(0, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow);
+ assertEquals(64, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", WORKING_INDEX).sessionCountInWindow);
+ assertEquals(192, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow);
+ assertEquals(320, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", RARE_INDEX).sessionCountInWindow);
+ }
+
+ setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, 500);
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.invalidateAllExecutionStatsLocked();
+ assertEquals(0, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow);
+ // WINDOW_SIZE_WORKING_MS * 5 TimingSessions are coalesced
+ assertEquals(44, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", WORKING_INDEX).sessionCountInWindow);
+ // WINDOW_SIZE_FREQUENT_MS * 5 TimingSessions are coalesced
+ assertEquals(132, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow);
+ // WINDOW_SIZE_RARE_MS * 5 TimingSessions are coalesced
+ assertEquals(220, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", RARE_INDEX).sessionCountInWindow);
+ }
+
+ setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, 1000);
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.invalidateAllExecutionStatsLocked();
+ assertEquals(0, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow);
+ assertEquals(44, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", WORKING_INDEX).sessionCountInWindow);
+ assertEquals(132, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow);
+ assertEquals(220, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", RARE_INDEX).sessionCountInWindow);
+ }
+
+ setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS,
+ 5 * SECOND_IN_MILLIS);
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.invalidateAllExecutionStatsLocked();
+ assertEquals(0, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow);
+ // WINDOW_SIZE_WORKING_MS * 9 TimingSessions are coalesced
+ assertEquals(28, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", WORKING_INDEX).sessionCountInWindow);
+ // WINDOW_SIZE_FREQUENT_MS * 9 TimingSessions are coalesced
+ assertEquals(84, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow);
+ // WINDOW_SIZE_RARE_MS * 9 TimingSessions are coalesced
+ assertEquals(140, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", RARE_INDEX).sessionCountInWindow);
+ }
+
+ setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS,
+ MINUTE_IN_MILLIS);
+
+ // Only two TimingSessions there for every hour.
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.invalidateAllExecutionStatsLocked();
+ assertEquals(0, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow);
+ assertEquals(8, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", WORKING_INDEX).sessionCountInWindow);
+ assertEquals(24, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow);
+ assertEquals(40, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", RARE_INDEX).sessionCountInWindow);
+ }
+
+ setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS,
+ 5 * MINUTE_IN_MILLIS);
+
+ // Only one TimingSessions there for every hour
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.invalidateAllExecutionStatsLocked();
+ assertEquals(0, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow);
+ assertEquals(4, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", WORKING_INDEX).sessionCountInWindow);
+ assertEquals(12, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow);
+ assertEquals(20, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", RARE_INDEX).sessionCountInWindow);
+ }
+
+ setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS,
+ 15 * MINUTE_IN_MILLIS);
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.invalidateAllExecutionStatsLocked();
+ assertEquals(0, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow);
+ assertEquals(4, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", WORKING_INDEX).sessionCountInWindow);
+ assertEquals(12, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow);
+ assertEquals(20, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", RARE_INDEX).sessionCountInWindow);
+ }
+
+ // QuotaController caps the duration at 15 minutes, so there shouldn't be any difference
+ // between an hour and 15 minutes.
+ setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, HOUR_IN_MILLIS);
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.invalidateAllExecutionStatsLocked();
+ assertEquals(0, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow);
+ assertEquals(4, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", WORKING_INDEX).sessionCountInWindow);
+ assertEquals(12, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow);
+ assertEquals(20, mQuotaController.getExecutionStatsLocked(
+ 0, "com.android.test", RARE_INDEX).sessionCountInWindow);
+ }
+ }
+
/**
* Tests that getExecutionStatsLocked properly caches the stats and returns the cached object.
*/
@@ -1454,7 +1799,8 @@ public class QuotaControllerTest {
}
@Test
- public void testGetMaxJobExecutionTimeLocked_EJ() {
+ @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetMaxJobExecutionTimeLocked_EJ_LegacyDefaultEJLimits() {
final long timeUsedMs = 3 * MINUTE_IN_MILLIS;
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS),
@@ -1530,11 +1876,91 @@ public class QuotaControllerTest {
}
}
+ @Test
+ @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetMaxJobExecutionTimeLocked_EJ_NewDefaultEJLimits() {
+ final long timeUsedMs = 3 * MINUTE_IN_MILLIS;
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS),
+ timeUsedMs, 5), true);
+ JobStatus job = createExpeditedJobStatus("testGetMaxJobExecutionTimeLocked_EJ", 0);
+ setStandbyBucket(RARE_INDEX, job);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(job, null);
+ }
+
+ setCharging();
+ synchronized (mQuotaController.mLock) {
+ assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+ }
+
+ setDischarging();
+ setProcessState(getProcessStateQuotaFreeThreshold());
+ synchronized (mQuotaController.mLock) {
+ assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+ }
+
+ // Top-started job
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(job);
+ }
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+ mQuotaController.maybeStopTrackingJobLocked(job, null);
+ }
+
+ setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(mQcConstants.EJ_LIMIT_RARE_MS - timeUsedMs,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+ }
+
+ // Test used quota rolling out of window.
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.clearAppStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE);
+ }
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(sElapsedRealtimeClock.millis() - mQcConstants.EJ_WINDOW_SIZE_MS,
+ timeUsedMs, 5), true);
+
+ setProcessState(getProcessStateQuotaFreeThreshold());
+ synchronized (mQuotaController.mLock) {
+ // max of 50% WORKING limit and remaining quota
+ assertEquals(10 * MINUTE_IN_MILLIS,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+ }
+
+ // Top-started job
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(job, null);
+ mQuotaController.prepareForExecutionLocked(job);
+ }
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+ mQuotaController.maybeStopTrackingJobLocked(job, null);
+ }
+
+ setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(mQcConstants.EJ_LIMIT_RARE_MS,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+ }
+ }
+
/**
* Test getTimeUntilQuotaConsumedLocked when allowed time equals the bucket window size.
*/
@Test
- public void testGetTimeUntilQuotaConsumedLocked_AllowedEqualsWindow() {
+ @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetTimeUntilQuotaConsumedLocked_AllowedEqualsWindow_LegacyDefaultBucketWindowSizes() {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(now - (8 * HOUR_IN_MILLIS), 20 * MINUTE_IN_MILLIS, 5), false);
@@ -1558,27 +1984,56 @@ public class QuotaControllerTest {
}
}
+ @Test
+ @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetTimeUntilQuotaConsumedLocked_AllowedEqualsWindow_NewDefaultBucketWindowSizes() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (8 * HOUR_IN_MILLIS), 20 * MINUTE_IN_MILLIS, 5), false);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (10 * MINUTE_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5),
+ false);
+
+ setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS,
+ 20 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_EXEMPTED_MS, 20 * MINUTE_IN_MILLIS);
+ // window size = allowed time, so jobs can essentially run non-stop until they reach the
+ // max execution time.
+ setStandbyBucket(EXEMPTED_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(10 * MINUTE_IN_MILLIS,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 30 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
/**
* Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket
* window.
*/
@Test
- public void testGetTimeUntilQuotaConsumedLocked_BucketWindow() {
+ @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetTimeUntilQuotaConsumedLocked_BucketWindow_LegacyDefaultBucketWindowSizes() {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
// Close to RARE boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - (24 * HOUR_IN_MILLIS - 30 * SECOND_IN_MILLIS),
+ createTimingSession(now - (mQcConstants.WINDOW_SIZE_RARE_MS - 30 * SECOND_IN_MILLIS),
30 * SECOND_IN_MILLIS, 5), false);
// Far away from FREQUENT boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - (7 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
+ createTimingSession(now - (mQcConstants.WINDOW_SIZE_FREQUENT_MS - HOUR_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS, 5), false);
// Overlap WORKING_SET boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS),
+ createTimingSession(now - (mQcConstants.WINDOW_SIZE_WORKING_MS + MINUTE_IN_MILLIS),
3 * MINUTE_IN_MILLIS, 5), false);
// Close to ACTIVE boundary.
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - (9 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
+ createTimingSession(now - (mQcConstants.WINDOW_SIZE_ACTIVE_MS - MINUTE_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS, 5), false);
setStandbyBucket(RARE_INDEX);
synchronized (mQuotaController.mLock) {
@@ -1623,6 +2078,69 @@ public class QuotaControllerTest {
}
}
+ @Test
+ @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetTimeUntilQuotaConsumedLocked_BucketWindow_NewDefaultBucketWindowSizes() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ // Close to RARE boundary.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (mQcConstants.WINDOW_SIZE_RARE_MS - 30 * SECOND_IN_MILLIS),
+ 30 * SECOND_IN_MILLIS, 5), false);
+ // Far away from FREQUENT boundary.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (mQcConstants.WINDOW_SIZE_FREQUENT_MS - HOUR_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS, 5), false);
+ // Overlap WORKING_SET boundary.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (mQcConstants.WINDOW_SIZE_WORKING_MS + MINUTE_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS, 5), false);
+ // Close to ACTIVE boundary.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (mQcConstants.WINDOW_SIZE_ACTIVE_MS - MINUTE_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS, 5), false);
+
+ setStandbyBucket(RARE_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(30 * SECOND_IN_MILLIS,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ setStandbyBucket(FREQUENT_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(MINUTE_IN_MILLIS,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ setStandbyBucket(WORKING_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(5 * MINUTE_IN_MILLIS,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(7 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ // ACTIVE window != allowed time.
+ setStandbyBucket(ACTIVE_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(7 * MINUTE_IN_MILLIS,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(10 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
/**
* Test getTimeUntilQuotaConsumedLocked when the app is close to the max execution limit.
*/
@@ -1747,7 +2265,8 @@ public class QuotaControllerTest {
* Test getTimeUntilQuotaConsumedLocked when allowed time equals the bucket window size.
*/
@Test
- public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_AllowedEqualsWindow() {
+ @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_AllowedEqualsWindow_LegacyDefaultBucketWindowSizes() {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
createTimingSession(now - (24 * HOUR_IN_MILLIS),
@@ -1773,55 +2292,84 @@ public class QuotaControllerTest {
}
}
+ @Test
+ @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_AllowedEqualsWindow_NewDefaultBucketWindowSizes() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (24 * HOUR_IN_MILLIS),
+ mQcConstants.MAX_EXECUTION_TIME_MS - 20 * MINUTE_IN_MILLIS, 5),
+ false);
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (20 * MINUTE_IN_MILLIS), 20 * MINUTE_IN_MILLIS, 5),
+ false);
+
+ setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS,
+ 20 * MINUTE_IN_MILLIS);
+ setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_EXEMPTED_MS, 20 * MINUTE_IN_MILLIS);
+ // window size != allowed time.
+ setStandbyBucket(EXEMPTED_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(0,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 20 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
/**
* Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket
* window and the session is rolling out of the window.
*/
@Test
- public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_BucketWindow() {
+ @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_BucketWindow_LegacyDefaultBucketWindowSizes() {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - (24 * HOUR_IN_MILLIS),
- 10 * MINUTE_IN_MILLIS, 5), false);
+ createTimingSession(now - mQcConstants.WINDOW_SIZE_RARE_MS,
+ mQcConstants.ALLOWED_TIME_PER_PERIOD_RARE_MS, 5), false);
setStandbyBucket(RARE_INDEX);
synchronized (mQuotaController.mLock) {
assertEquals(0,
mQuotaController.getRemainingExecutionTimeLocked(
SOURCE_USER_ID, SOURCE_PACKAGE));
- assertEquals(10 * MINUTE_IN_MILLIS,
+ assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_RARE_MS,
mQuotaController.getTimeUntilQuotaConsumedLocked(
SOURCE_USER_ID, SOURCE_PACKAGE));
}
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - (8 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false);
+ createTimingSession(now - mQcConstants.WINDOW_SIZE_FREQUENT_MS,
+ mQcConstants.ALLOWED_TIME_PER_PERIOD_FREQUENT_MS, 5), false);
setStandbyBucket(FREQUENT_INDEX);
synchronized (mQuotaController.mLock) {
assertEquals(0,
mQuotaController.getRemainingExecutionTimeLocked(
SOURCE_USER_ID, SOURCE_PACKAGE));
- assertEquals(10 * MINUTE_IN_MILLIS,
+ assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_FREQUENT_MS,
mQuotaController.getTimeUntilQuotaConsumedLocked(
SOURCE_USER_ID, SOURCE_PACKAGE));
}
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - (2 * HOUR_IN_MILLIS),
- 10 * MINUTE_IN_MILLIS, 5), false);
+ createTimingSession(now - mQcConstants.WINDOW_SIZE_WORKING_MS,
+ mQcConstants.ALLOWED_TIME_PER_PERIOD_WORKING_MS, 5), false);
setStandbyBucket(WORKING_INDEX);
synchronized (mQuotaController.mLock) {
assertEquals(0,
mQuotaController.getRemainingExecutionTimeLocked(
SOURCE_USER_ID, SOURCE_PACKAGE));
- assertEquals(10 * MINUTE_IN_MILLIS,
+ assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_WORKING_MS,
mQuotaController.getTimeUntilQuotaConsumedLocked(
SOURCE_USER_ID, SOURCE_PACKAGE));
}
mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
- createTimingSession(now - (10 * MINUTE_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5),
- false);
+ createTimingSession(now - mQcConstants.WINDOW_SIZE_ACTIVE_MS,
+ mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS, 5), false);
// ACTIVE window = allowed time, so jobs can essentially run non-stop until they reach the
// max execution time.
setStandbyBucket(ACTIVE_INDEX);
@@ -1829,7 +2377,85 @@ public class QuotaControllerTest {
assertEquals(0,
mQuotaController.getRemainingExecutionTimeLocked(
SOURCE_USER_ID, SOURCE_PACKAGE));
- assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 30 * MINUTE_IN_MILLIS,
+ assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS
+ - (mQcConstants.ALLOWED_TIME_PER_PERIOD_RARE_MS
+ + mQcConstants.ALLOWED_TIME_PER_PERIOD_FREQUENT_MS
+ + mQcConstants.ALLOWED_TIME_PER_PERIOD_WORKING_MS),
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS)
+ public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_BucketWindow_NewDefaultBucketWindowSizes() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - mQcConstants.WINDOW_SIZE_RARE_MS,
+ mQcConstants.ALLOWED_TIME_PER_PERIOD_RARE_MS, 5), false);
+ setStandbyBucket(RARE_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(0,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_RARE_MS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - mQcConstants.WINDOW_SIZE_FREQUENT_MS,
+ mQcConstants.ALLOWED_TIME_PER_PERIOD_FREQUENT_MS, 5), false);
+ setStandbyBucket(FREQUENT_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(0,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_FREQUENT_MS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - mQcConstants.WINDOW_SIZE_WORKING_MS,
+ mQcConstants.ALLOWED_TIME_PER_PERIOD_WORKING_MS, 5), false);
+ setStandbyBucket(WORKING_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(0,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_WORKING_MS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - mQcConstants.WINDOW_SIZE_ACTIVE_MS,
+ mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS, 5),
+ false);
+ // ACTIVE window != allowed time.
+ setStandbyBucket(ACTIVE_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(0,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - mQcConstants.WINDOW_SIZE_EXEMPTED_MS,
+ mQcConstants.ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS, 5),
+ false);
+ // EXEMPTED window != allowed time
+ setStandbyBucket(EXEMPTED_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(0,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS,
mQuotaController.getTimeUntilQuotaConsumedLocked(
SOURCE_USER_ID, SOURCE_PACKAGE));
}
@@ -3733,7 +4359,7 @@ public class QuotaControllerTest {
/** Tests that Timers count FOREGROUND_SERVICE jobs. */
@Test
- @RequiresFlagsEnabled(FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS)
+ @EnableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS)
public void testTimerTracking_Fgs() {
setDischarging();
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt
index e131a98b52d0..de029e0d770f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt
@@ -185,7 +185,8 @@ class DistractingPackageHelperTest : PackageHelperTestBase() {
verify(pms, never()).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
verify(broadcastHelper, never()).sendPackageBroadcast(eq(
Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), nullable(), anyInt(),
- nullable(), nullable(), any(), nullable(), nullable(), nullable(), nullable())
+ nullable(), nullable(), any(), nullable(), nullable(), nullable(), nullable(),
+ nullable())
distractingPackageHelper.removeDistractingPackageRestrictions(pms.snapshotComputer(),
arrayOfNulls(0), TEST_USER_ID)
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
index 43a8aa957fa5..124c41ed449f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
@@ -741,7 +741,8 @@ public class StagingManagerTest {
/* stagedSessionErrorCode */ PackageManager.INSTALL_UNKNOWN,
/* stagedSessionErrorMessage */ "no error",
/* preVerifiedDomains */ null,
- /* verifierController */ null);
+ /* verifierController */ null,
+ /* verificationPolicy */ PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED);
StagingManager.StagedSession stagedSession = spy(session.mStagedSession);
doReturn(packageName).when(stagedSession).getPackageName();
diff --git a/services/tests/security/forensic/Android.bp b/services/tests/security/forensic/Android.bp
new file mode 100644
index 000000000000..adc49040a004
--- /dev/null
+++ b/services/tests/security/forensic/Android.bp
@@ -0,0 +1,41 @@
+package {
+ default_team: "trendy_team_platform_security",
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "ForensicServiceTests",
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.rules",
+ "androidx.test.runner",
+ "compatibility-device-util-axt",
+ "frameworks-base-testutils",
+ "junit",
+ "platform-test-annotations",
+ "services.core",
+ "truth",
+ ],
+
+ platform_apis: true,
+
+ test_suites: [
+ "device-tests",
+ "automotive-tests",
+ ],
+
+ certificate: "platform",
+ dxflags: ["--multi-dex"],
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/services/tests/security/forensic/AndroidManifest.xml b/services/tests/security/forensic/AndroidManifest.xml
new file mode 100644
index 000000000000..40feb19aecba
--- /dev/null
+++ b/services/tests/security/forensic/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.security.forensic.tests">
+
+ <application android:testOnly="true">
+ <uses-library android:name="android.test.runner"/>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.server.security.forensic.tests"
+ android:label="Frameworks Forensic Services Tests"/>
+</manifest>
diff --git a/services/tests/security/forensic/AndroidTest.xml b/services/tests/security/forensic/AndroidTest.xml
new file mode 100644
index 000000000000..bbe2e9c303ce
--- /dev/null
+++ b/services/tests/security/forensic/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Runs Frameworks Forensic Service tests.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true"/>
+ <option name="test-file-name" value="ForensicServiceTests.apk"/>
+ <option name="install-arg" value="-t" />
+ </target_preparer>
+
+ <option name="test-tag" value="ForensicServiceTests" />
+ <test class="com.android.tradefed.testtype.InstrumentationTest" >
+ <option name="package" value="com.android.server.security.forensic.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/services/tests/security/forensic/TEST_MAPPING b/services/tests/security/forensic/TEST_MAPPING
new file mode 100644
index 000000000000..bd8b2ab7c41f
--- /dev/null
+++ b/services/tests/security/forensic/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "postsubmit": [
+ {
+ "name": "ForensicServiceTests"
+ }
+ ]
+}
diff --git a/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java
new file mode 100644
index 000000000000..7aa2e0f609a7
--- /dev/null
+++ b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java
@@ -0,0 +1,387 @@
+/*
+ * 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.security.forensic;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.security.forensic.IForensicServiceCommandCallback;
+import android.security.forensic.IForensicServiceStateCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ForensicServiceTest {
+ private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
+ private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE;
+ private static final int STATE_VISIBLE = IForensicServiceStateCallback.State.VISIBLE;
+ private static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED;
+
+ private static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN;
+ private static final int ERROR_PERMISSION_DENIED =
+ IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
+ private static final int ERROR_INVALID_STATE_TRANSITION =
+ IForensicServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION;
+ private static final int ERROR_BACKUP_TRANSPORT_UNAVAILABLE =
+ IForensicServiceCommandCallback.ErrorCode.BACKUP_TRANSPORT_UNAVAILABLE;
+ private static final int ERROR_DATA_SOURCE_UNAVAILABLE =
+ IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
+
+ @Mock
+ private Context mContextSpy;
+
+ private ForensicService mForensicService;
+ private TestLooper mTestLooper;
+ private Looper mLooper;
+
+ @SuppressLint("VisibleForTests")
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mTestLooper = new TestLooper();
+ mLooper = mTestLooper.getLooper();
+ mForensicService = new ForensicService(new MockInjector(mContextSpy));
+ mForensicService.onStart();
+ }
+
+ @Test
+ public void testMonitorState_Invisible() throws RemoteException {
+ StateCallback scb = new StateCallback();
+ assertEquals(STATE_UNKNOWN, scb.mState);
+ mForensicService.getBinderService().monitorState(scb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb.mState);
+ }
+
+ @Test
+ public void testMonitorState_Invisible_TwoMonitors() throws RemoteException {
+ StateCallback scb1 = new StateCallback();
+ assertEquals(STATE_UNKNOWN, scb1.mState);
+ mForensicService.getBinderService().monitorState(scb1);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb1.mState);
+
+ StateCallback scb2 = new StateCallback();
+ assertEquals(STATE_UNKNOWN, scb2.mState);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb2.mState);
+ }
+
+ @Test
+ public void testMakeVisible_FromInvisible() throws RemoteException {
+ StateCallback scb = new StateCallback();
+ assertEquals(STATE_UNKNOWN, scb.mState);
+ mForensicService.getBinderService().monitorState(scb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().makeVisible(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_VISIBLE, scb.mState);
+ assertNull(ccb.mErrorCode);
+ }
+
+ @Test
+ public void testMakeVisible_FromInvisible_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_INVISIBLE);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb1.mState);
+ assertEquals(STATE_INVISIBLE, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().makeVisible(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_VISIBLE, scb1.mState);
+ assertEquals(STATE_VISIBLE, scb2.mState);
+ assertNull(ccb.mErrorCode);
+ }
+
+ @Test
+ public void testMakeVisible_FromVisible_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_VISIBLE);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_VISIBLE, scb1.mState);
+ assertEquals(STATE_VISIBLE, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().makeVisible(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_VISIBLE, scb1.mState);
+ assertEquals(STATE_VISIBLE, scb2.mState);
+ assertNull(ccb.mErrorCode);
+ }
+
+ @Test
+ public void testMakeVisible_FromEnabled_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_ENABLED);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_ENABLED, scb1.mState);
+ assertEquals(STATE_ENABLED, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().makeVisible(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_ENABLED, scb1.mState);
+ assertEquals(STATE_ENABLED, scb2.mState);
+ assertNotNull(ccb.mErrorCode);
+ assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue());
+ }
+
+ @Test
+ public void testMakeInvisible_FromInvisible_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_INVISIBLE);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb1.mState);
+ assertEquals(STATE_INVISIBLE, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().makeInvisible(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb1.mState);
+ assertEquals(STATE_INVISIBLE, scb2.mState);
+ assertNull(ccb.mErrorCode);
+ }
+
+ @Test
+ public void testMakeInvisible_FromVisible_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_VISIBLE);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_VISIBLE, scb1.mState);
+ assertEquals(STATE_VISIBLE, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().makeInvisible(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb1.mState);
+ assertEquals(STATE_INVISIBLE, scb2.mState);
+ assertNull(ccb.mErrorCode);
+ }
+
+ @Test
+ public void testMakeInvisible_FromEnabled_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_ENABLED);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_ENABLED, scb1.mState);
+ assertEquals(STATE_ENABLED, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().makeInvisible(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb1.mState);
+ assertEquals(STATE_INVISIBLE, scb2.mState);
+ assertNull(ccb.mErrorCode);
+ }
+
+
+ @Test
+ public void testEnable_FromInvisible_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_INVISIBLE);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb1.mState);
+ assertEquals(STATE_INVISIBLE, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().enable(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb1.mState);
+ assertEquals(STATE_INVISIBLE, scb2.mState);
+ assertNotNull(ccb.mErrorCode);
+ assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue());
+ }
+
+ @Test
+ public void testEnable_FromVisible_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_VISIBLE);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_VISIBLE, scb1.mState);
+ assertEquals(STATE_VISIBLE, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().enable(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_ENABLED, scb1.mState);
+ assertEquals(STATE_ENABLED, scb2.mState);
+ assertNull(ccb.mErrorCode);
+ }
+
+ @Test
+ public void testEnable_FromEnabled_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_ENABLED);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_ENABLED, scb1.mState);
+ assertEquals(STATE_ENABLED, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().enable(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_ENABLED, scb1.mState);
+ assertEquals(STATE_ENABLED, scb2.mState);
+ assertNull(ccb.mErrorCode);
+ }
+
+ @Test
+ public void testDisable_FromInvisible_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_INVISIBLE);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb1.mState);
+ assertEquals(STATE_INVISIBLE, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().disable(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_INVISIBLE, scb1.mState);
+ assertEquals(STATE_INVISIBLE, scb2.mState);
+ assertNotNull(ccb.mErrorCode);
+ assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue());
+ }
+
+ @Test
+ public void testDisable_FromVisible_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_VISIBLE);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_VISIBLE, scb1.mState);
+ assertEquals(STATE_VISIBLE, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().disable(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_VISIBLE, scb1.mState);
+ assertEquals(STATE_VISIBLE, scb2.mState);
+ assertNull(ccb.mErrorCode);
+ }
+
+ @Test
+ public void testDisable_FromEnabled_TwoMonitors() throws RemoteException {
+ mForensicService.setState(STATE_ENABLED);
+ StateCallback scb1 = new StateCallback();
+ StateCallback scb2 = new StateCallback();
+ mForensicService.getBinderService().monitorState(scb1);
+ mForensicService.getBinderService().monitorState(scb2);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_ENABLED, scb1.mState);
+ assertEquals(STATE_ENABLED, scb2.mState);
+
+ CommandCallback ccb = new CommandCallback();
+ mForensicService.getBinderService().disable(ccb);
+ mTestLooper.dispatchAll();
+ assertEquals(STATE_VISIBLE, scb1.mState);
+ assertEquals(STATE_VISIBLE, scb2.mState);
+ assertNull(ccb.mErrorCode);
+ }
+
+ private class MockInjector implements ForensicService.Injector {
+ private final Context mContext;
+
+ MockInjector(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+
+ @Override
+ public Looper getLooper() {
+ return mLooper;
+ }
+
+ }
+
+ private static class StateCallback extends IForensicServiceStateCallback.Stub {
+ int mState = STATE_UNKNOWN;
+
+ @Override
+ public void onStateChange(int state) throws RemoteException {
+ mState = state;
+ }
+ }
+
+ private static class CommandCallback extends IForensicServiceCommandCallback.Stub {
+ Integer mErrorCode = null;
+
+ public void reset() {
+ mErrorCode = null;
+ }
+
+ @Override
+ public void onSuccess() throws RemoteException {
+
+ }
+
+ @Override
+ public void onFailure(int errorCode) throws RemoteException {
+ mErrorCode = errorCode;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index b745e6a7d4a5..e5831b326de5 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -1417,7 +1417,7 @@ public class FullScreenMagnificationGestureHandlerTest {
}
@Test
- @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void testMouseMoveEventsDoNotMoveMagnifierViewport() {
runMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE);
}
@@ -1471,55 +1471,55 @@ public class FullScreenMagnificationGestureHandlerTest {
}
@Test
- @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void testMouseHoverMoveEventsDoNotMoveMagnifierViewport() {
runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE);
}
@Test
- @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void testStylusHoverMoveEventsDoNotMoveMagnifierViewport() {
runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_STYLUS);
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void testMouseHoverMoveEventsMoveMagnifierViewport() {
runHoverMovesViewportTest(InputDevice.SOURCE_MOUSE);
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void testStylusHoverMoveEventsMoveMagnifierViewport() {
runHoverMovesViewportTest(InputDevice.SOURCE_STYLUS);
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void testMouseDownEventsDoNotMoveMagnifierViewport() {
runDownDoesNotMoveViewportTest(InputDevice.SOURCE_MOUSE);
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void testStylusDownEventsDoNotMoveMagnifierViewport() {
runDownDoesNotMoveViewportTest(InputDevice.SOURCE_STYLUS);
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void testMouseUpEventsDoNotMoveMagnifierViewport() {
runUpDoesNotMoveViewportTest(InputDevice.SOURCE_MOUSE);
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void testStylusUpEventsDoNotMoveMagnifierViewport() {
runUpDoesNotMoveViewportTest(InputDevice.SOURCE_STYLUS);
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void testMouseMoveEventsMoveMagnifierViewport() {
final EventCaptor eventCaptor = new EventCaptor();
mMgh.setNext(eventCaptor);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java
index d80a1f056e94..45c157d1c1a0 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java
@@ -93,7 +93,7 @@ public class MagnificationGestureHandlerTest {
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void onMotionEvent_isFromMouse_handleMouseOrStylusEvent() {
final MotionEvent mouseEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0);
mouseEvent.setSource(InputDevice.SOURCE_MOUSE);
@@ -108,7 +108,7 @@ public class MagnificationGestureHandlerTest {
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void onMotionEvent_isFromStylus_handleMouseOrStylusEvent() {
final MotionEvent stylusEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0);
stylusEvent.setSource(InputDevice.SOURCE_STYLUS);
@@ -123,7 +123,7 @@ public class MagnificationGestureHandlerTest {
}
@Test
- @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void onMotionEvent_isFromMouse_handleMouseOrStylusEventNotCalled() {
final MotionEvent mouseEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0);
mouseEvent.setSource(InputDevice.SOURCE_MOUSE);
@@ -138,7 +138,7 @@ public class MagnificationGestureHandlerTest {
}
@Test
- @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE)
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX)
public void onMotionEvent_isFromStylus_handleMouseOrStylusEventNotCalled() {
final MotionEvent stylusEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0);
stylusEvent.setSource(InputDevice.SOURCE_STYLUS);
diff --git a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java
index d1f6c2f9f1f0..9c6412b81b34 100644
--- a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java
@@ -67,10 +67,8 @@ import android.provider.Settings;
import androidx.test.InstrumentationRegistry;
import com.android.internal.R;
-import com.android.internal.pm.parsing.PackageParser2;
import com.android.server.compat.PlatformCompat;
import com.android.server.integrity.model.IntegrityCheckResult;
-import com.android.server.pm.parsing.TestPackageParser2;
import com.android.server.testutils.TestUtils;
import org.junit.After;
@@ -140,8 +138,6 @@ public class AppIntegrityManagerServiceImplTest {
@Mock IntegrityFileManager mIntegrityFileManager;
@Mock Handler mHandler;
- private Supplier<PackageParser2> mParserSupplier = TestPackageParser2::new;
-
private final Context mRealContext = InstrumentationRegistry.getTargetContext();
private PackageManager mSpyPackageManager;
@@ -173,7 +169,6 @@ public class AppIntegrityManagerServiceImplTest {
new AppIntegrityManagerServiceImpl(
mMockContext,
mPackageManagerInternal,
- mParserSupplier,
mIntegrityFileManager,
mHandler);
diff --git a/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java
index a54501029712..f45eddcf4480 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java
@@ -24,6 +24,7 @@ import static org.mockito.Mockito.when;
import android.database.Cursor;
import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.provider.CallLog.Calls;
import android.test.mock.MockContentProvider;
@@ -58,6 +59,7 @@ public final class CallLogQueryHelperTest {
private MatrixCursor mCursor;
private EventConsumer mEventConsumer;
private CallLogQueryHelper mHelper;
+ private CallLogContentProvider mCallLogContentProvider;
@Before
public void setUp() {
@@ -66,7 +68,8 @@ public final class CallLogQueryHelperTest {
mCursor = new MatrixCursor(CALL_LOG_COLUMNS);
MockContentResolver contentResolver = new MockContentResolver();
- contentResolver.addProvider(CALL_LOG_AUTHORITY, new CallLogContentProvider());
+ mCallLogContentProvider = new CallLogContentProvider();
+ contentResolver.addProvider(CALL_LOG_AUTHORITY, mCallLogContentProvider);
when(mContext.getContentResolver()).thenReturn(contentResolver);
mEventConsumer = new EventConsumer();
@@ -80,6 +83,12 @@ public final class CallLogQueryHelperTest {
}
@Test
+ public void testQueryWithSQLiteException() {
+ mCallLogContentProvider.setThrowSQLiteException(true);
+ assertFalse(mHelper.querySince(50L));
+ }
+
+ @Test
public void testQueryIncomingCall() {
mCursor.addRow(new Object[] {
NORMALIZED_PHONE_NUMBER, /* date= */ 100L, /* duration= */ 30L,
@@ -159,11 +168,20 @@ public final class CallLogQueryHelperTest {
}
private class CallLogContentProvider extends MockContentProvider {
+ private boolean mThrowSQLiteException = false;
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
+ if (mThrowSQLiteException) {
+ throw new SQLiteException();
+ }
+
return mCursor;
}
+
+ public void setThrowSQLiteException(boolean throwException) {
+ this.mThrowSQLiteException = throwException;
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java
index 16a02b678511..1daee39ce9de 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java
@@ -99,6 +99,14 @@ public final class ContactsQueryHelperTest {
}
@Test
+ public void testQueryOtherException_returnsFalse() {
+ contentProvider.setThrowOtherException(true);
+
+ Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY);
+ assertFalse(mHelper.query(contactUri.toString()));
+ }
+
+ @Test
public void testQueryIllegalArgumentException_returnsFalse() {
contentProvider.setThrowIllegalArgumentException(true);
@@ -152,6 +160,13 @@ public final class ContactsQueryHelperTest {
}
@Test
+ public void testQueryWithPhoneNumber_otherExceptionReturnsFalse() {
+ contentProvider.setThrowOtherException(true);
+ String contactUri = "tel:" + PHONE_NUMBER;
+ assertFalse(mHelper.query(contactUri));
+ }
+
+ @Test
public void testQueryWithEmail() {
mContactsLookupCursor.addRow(new Object[] {
/* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 0 });
@@ -188,6 +203,7 @@ public final class ContactsQueryHelperTest {
private Map<Uri, Cursor> mUriPrefixToCursorMap = new ArrayMap<>();
private boolean mThrowSQLiteException = false;
private boolean mThrowIllegalArgumentException = false;
+ private boolean mThrowOtherException = false;
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
@@ -198,6 +214,9 @@ public final class ContactsQueryHelperTest {
if (mThrowIllegalArgumentException) {
throw new IllegalArgumentException();
}
+ if (mThrowOtherException) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
for (Uri prefixUri : mUriPrefixToCursorMap.keySet()) {
if (uri.isPathPrefixMatch(prefixUri)) {
@@ -215,6 +234,10 @@ public final class ContactsQueryHelperTest {
this.mThrowIllegalArgumentException = throwException;
}
+ public void setThrowOtherException(boolean throwException) {
+ this.mThrowOtherException = throwException;
+ }
+
private void registerCursor(Uri uriPrefix, Cursor cursor) {
mUriPrefixToCursorMap.put(uriPrefix, cursor);
}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java
index 7730890e1486..9f4a43df6de8 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java
@@ -23,6 +23,7 @@ import static org.mockito.Mockito.when;
import android.database.Cursor;
import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.provider.Telephony.BaseMmsColumns;
import android.provider.Telephony.Mms;
@@ -63,6 +64,7 @@ public final class MmsQueryHelperTest {
private final List<MatrixCursor> mAddrCursors = new ArrayList<>();
private EventConsumer mEventConsumer;
private MmsQueryHelper mHelper;
+ private MmsContentProvider mMmsContentProvider;
@Before
public void setUp() {
@@ -73,7 +75,8 @@ public final class MmsQueryHelperTest {
mAddrCursors.add(new MatrixCursor(ADDR_COLUMNS));
MockContentResolver contentResolver = new MockContentResolver();
- contentResolver.addProvider(MMS_AUTHORITY, new MmsContentProvider());
+ mMmsContentProvider = new MmsContentProvider();
+ contentResolver.addProvider(MMS_AUTHORITY, mMmsContentProvider);
when(mContext.getContentResolver()).thenReturn(contentResolver);
mEventConsumer = new EventConsumer();
@@ -87,6 +90,12 @@ public final class MmsQueryHelperTest {
}
@Test
+ public void testQueryWithSQLiteException() {
+ mMmsContentProvider.setThrowSQLiteException(true);
+ assertFalse(mHelper.querySince(50_000L));
+ }
+
+ @Test
public void testQueryIncomingMessage() {
mMmsCursor.addRow(new Object[] {
/* id= */ 0, /* date= */ 100L, /* msgBox= */ BaseMmsColumns.MESSAGE_BOX_INBOX });
@@ -159,10 +168,15 @@ public final class MmsQueryHelperTest {
}
private class MmsContentProvider extends MockContentProvider {
+ private boolean mThrowSQLiteException = false;
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
+ if (mThrowSQLiteException) {
+ throw new SQLiteException();
+ }
+
List<String> segments = uri.getPathSegments();
if (segments.size() == 2 && "addr".equals(segments.get(1))) {
int messageId = Integer.valueOf(segments.get(0));
@@ -170,5 +184,9 @@ public final class MmsQueryHelperTest {
}
return mMmsCursor;
}
+
+ public void setThrowSQLiteException(boolean throwException) {
+ this.mThrowSQLiteException = throwException;
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java
index 5cb8cb4fe9f1..09a0dff77eb0 100644
--- a/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java
@@ -23,6 +23,7 @@ import static org.mockito.Mockito.when;
import android.database.Cursor;
import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.provider.Telephony.Sms;
import android.provider.Telephony.TextBasedSmsColumns;
@@ -59,6 +60,7 @@ public final class SmsQueryHelperTest {
private MatrixCursor mSmsCursor;
private EventConsumer mEventConsumer;
private SmsQueryHelper mHelper;
+ private SmsContentProvider mSmsContentProvider;
@Before
public void setUp() {
@@ -67,7 +69,8 @@ public final class SmsQueryHelperTest {
mSmsCursor = new MatrixCursor(SMS_COLUMNS);
MockContentResolver contentResolver = new MockContentResolver();
- contentResolver.addProvider(SMS_AUTHORITY, new SmsContentProvider());
+ mSmsContentProvider = new SmsContentProvider();
+ contentResolver.addProvider(SMS_AUTHORITY, mSmsContentProvider);
when(mContext.getContentResolver()).thenReturn(contentResolver);
mEventConsumer = new EventConsumer();
@@ -130,6 +133,12 @@ public final class SmsQueryHelperTest {
assertEquals(110L, events.get(1).getTimestamp());
}
+ @Test
+ public void testQueryWithSQLiteException() {
+ mSmsContentProvider.setThrowSQLiteException(true);
+ assertFalse(mHelper.querySince(50L));
+ }
+
private class EventConsumer implements BiConsumer<String, Event> {
private final Map<String, List<Event>> mEventMap = new ArrayMap<>();
@@ -141,11 +150,19 @@ public final class SmsQueryHelperTest {
}
private class SmsContentProvider extends MockContentProvider {
+ private boolean mThrowSQLiteException = false;
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
+ if (mThrowSQLiteException) {
+ throw new SQLiteException();
+ }
return mSmsCursor;
}
+
+ public void setThrowSQLiteException(boolean throwException) {
+ this.mThrowSQLiteException = throwException;
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java
index 457058849fca..79967b861ea5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java
@@ -30,6 +30,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.graphics.Insets;
+import android.graphics.Point;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.view.InsetsSource;
@@ -260,6 +261,27 @@ public class InsetsSourceProviderTest extends WindowTestsBase {
}
@Test
+ public void testUpdateInsetsControlPosition() {
+ final WindowState target = createWindow(null, TYPE_APPLICATION, "target");
+
+ final WindowState ime1 = createWindow(null, TYPE_INPUT_METHOD, "ime1");
+ ime1.getFrame().set(new Rect(0, 0, 0, 0));
+ mImeProvider.setWindowContainer(ime1, null, null);
+ mImeProvider.updateControlForTarget(target, false /* force */, null /* statsToken */);
+ ime1.getFrame().set(new Rect(0, 400, 500, 500));
+ mImeProvider.updateInsetsControlPosition(ime1);
+ assertEquals(new Point(0, 400), mImeProvider.getControl(target).getSurfacePosition());
+
+ final WindowState ime2 = createWindow(null, TYPE_INPUT_METHOD, "ime2");
+ ime2.getFrame().set(new Rect(0, 0, 0, 0));
+ mImeProvider.setWindowContainer(ime2, null, null);
+ mImeProvider.updateControlForTarget(target, false /* force */, null /* statsToken */);
+ ime2.getFrame().set(new Rect(0, 400, 500, 500));
+ mImeProvider.updateInsetsControlPosition(ime2);
+ assertEquals(new Point(0, 400), mImeProvider.getControl(target).getSurfacePosition());
+ }
+
+ @Test
public void testSetRequestedVisibleTypes() {
final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar");
final WindowState target = createWindow(null, TYPE_APPLICATION, "target");