summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apct-tests/perftests/autofill/src/android/view/autofill/AutofillTestWatcher.java4
-rw-r--r--cmds/screencap/Android.bp65
-rw-r--r--cmds/screencap/TEST_MAPPING12
-rw-r--r--cmds/screencap/screencap.cpp36
-rw-r--r--cmds/screencap/screencap_utils.cpp43
-rw-r--r--cmds/screencap/screencap_utils.h28
-rw-r--r--cmds/screencap/tests/screencap_test.cpp68
-rw-r--r--core/api/system-current.txt4
-rw-r--r--core/api/test-current.txt8
-rw-r--r--core/java/android/content/pm/multiuser.aconfig7
-rw-r--r--core/java/android/hardware/display/DisplayManager.java2
-rw-r--r--core/java/android/hardware/display/DisplayTopology.java37
-rw-r--r--core/java/android/os/IUserManager.aidl2
-rw-r--r--core/java/android/os/UserManager.java68
-rw-r--r--core/java/android/view/ViewRootImpl.java10
-rw-r--r--core/java/android/window/WindowContainerTransaction.java58
-rw-r--r--core/java/android/window/flags/windowing_frontend.aconfig11
-rw-r--r--core/java/com/android/internal/os/BatteryStatsHistory.java779
-rw-r--r--core/java/com/android/internal/policy/PhoneWindow.java3
-rw-r--r--core/jni/android_view_SurfaceControlActivePictureListener.cpp6
-rw-r--r--core/res/res/values/attrs_manifest.xml35
-rw-r--r--libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_16.xml28
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml4
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml4
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt29
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt59
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt450
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt27
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java92
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/InputChannelSupplier.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowSessionSupplier.kt35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplier.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/TransactionSupplier.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureListener.kt65
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputController.kt113
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputDetector.kt230
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputSurfaceBuilder.kt58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java103
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java55
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/InputChannelSupplierTest.kt42
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/SuppliersUtilsTest.kt34
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt42
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplierTest.kt43
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransactionSupplierTest.kt43
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureDelegateTest.kt75
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxInputControllerTest.kt203
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt19
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt101
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt249
-rw-r--r--media/jni/android_media_MediaCodec.cpp106
-rw-r--r--packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java11
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt65
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt22
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt4
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt17
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt23
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt39
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt22
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt20
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt28
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt13
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt59
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt70
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt40
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt39
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt141
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt82
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java23
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt76
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt7
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt11
-rw-r--r--packages/SystemUI/res/layout/media_output_list_group_divider.xml2
-rw-r--r--packages/SystemUI/res/layout/media_output_list_item_advanced.xml27
-rw-r--r--packages/SystemUI/res/layout/window_magnification_settings_view.xml1
-rw-r--r--packages/SystemUI/res/values-ldrtl/dimens.xml2
-rw-r--r--packages/SystemUI/res/values-sw600dp/dimens.xml2
-rw-r--r--packages/SystemUI/res/values/dimens.xml10
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/SessionTracker.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java32
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java25
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java28
-rw-r--r--packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt58
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java62
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java53
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt63
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt104
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteria.kt43
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessKosmos.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModuleKosmos.kt11
-rw-r--r--services/core/Android.bp1
-rw-r--r--services/core/java/com/android/server/VpnManagerService.java9
-rw-r--r--services/core/java/com/android/server/Watchdog.java3
-rw-r--r--services/core/java/com/android/server/am/AppBatteryTracker.java12
-rw-r--r--services/core/java/com/android/server/am/BatteryStatsService.java2
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java19
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityService.java7
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityUtils.java6
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java52
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java573
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryStatsImpl.java30
-rw-r--r--services/core/java/com/android/server/power/stats/flags.aconfig10
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java6
-rw-r--r--services/core/java/com/android/server/wm/AppCompatConfiguration.java4
-rw-r--r--services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java15
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java6
-rw-r--r--services/core/java/com/android/server/wm/LaunchParamsController.java16
-rw-r--r--services/core/java/com/android/server/wm/Task.java29
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java27
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java112
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java89
-rw-r--r--services/tests/powerstatstests/res/raw/history_01bin0 -> 131116 bytes
-rw-r--r--services/tests/powerstatstests/res/raw/history_02bin0 -> 131432 bytes
-rw-r--r--services/tests/powerstatstests/res/raw/history_03bin0 -> 132864 bytes
-rw-r--r--services/tests/powerstatstests/res/raw/history_04bin0 -> 131204 bytes
-rw-r--r--services/tests/powerstatstests/res/raw/history_05bin0 -> 131112 bytes
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java280
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java147
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java12
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java21
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java2
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java5
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java2
-rw-r--r--services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java124
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java3
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java14
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java35
-rw-r--r--telephony/java/android/telephony/SubscriptionInfo.java8
-rw-r--r--telephony/java/android/telephony/TelephonyManager.java2
168 files changed, 5451 insertions, 1601 deletions
diff --git a/apct-tests/perftests/autofill/src/android/view/autofill/AutofillTestWatcher.java b/apct-tests/perftests/autofill/src/android/view/autofill/AutofillTestWatcher.java
index f65067fe2d92..afdc36190066 100644
--- a/apct-tests/perftests/autofill/src/android/view/autofill/AutofillTestWatcher.java
+++ b/apct-tests/perftests/autofill/src/android/view/autofill/AutofillTestWatcher.java
@@ -174,7 +174,9 @@ final class AutofillTestWatcher extends TestWatcher {
public static void onConnected() {
Log.i(TAG, "onConnected: sServiceWatcher=" + sServiceWatcher);
-
+ if (sServiceWatcher == null) {
+ sServiceWatcher = new ServiceWatcher();
+ }
sServiceWatcher.mConnected.countDown();
}
diff --git a/cmds/screencap/Android.bp b/cmds/screencap/Android.bp
index 16026eca2980..9f350b1d6054 100644
--- a/cmds/screencap/Android.bp
+++ b/cmds/screencap/Android.bp
@@ -7,25 +7,66 @@ package {
default_applicable_licenses: ["frameworks_base_license"],
}
-cc_binary {
- name: "screencap",
+cc_defaults {
+ name: "screencap_defaults",
- srcs: ["screencap.cpp"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wunreachable-code",
+ "-Wunused",
+ ],
shared_libs: [
- "libcutils",
- "libutils",
"libbinder",
- "libjnigraphics",
+ "libcutils",
+ "libgui",
"libhwui",
+ "libjnigraphics",
"libui",
- "libgui",
+ "libutils",
],
+}
- cflags: [
- "-Wall",
- "-Werror",
- "-Wunused",
- "-Wunreachable-code",
+cc_library {
+ name: "libscreencap",
+
+ defaults: [
+ "screencap_defaults",
+ ],
+
+ srcs: ["screencap_utils.cpp"],
+}
+
+cc_binary {
+ name: "screencap",
+
+ defaults: [
+ "screencap_defaults",
+ ],
+
+ srcs: ["screencap.cpp"],
+
+ static_libs: [
+ "libscreencap",
+ ],
+}
+
+cc_test {
+ name: "libscreencap_test",
+
+ defaults: [
+ "screencap_defaults",
+ ],
+
+ test_suites: ["device-tests"],
+
+ srcs: [
+ "tests/screencap_test.cpp",
+ ],
+
+ static_libs: [
+ "libgmock",
+ "libscreencap",
],
}
diff --git a/cmds/screencap/TEST_MAPPING b/cmds/screencap/TEST_MAPPING
new file mode 100644
index 000000000000..05c598e1e9cc
--- /dev/null
+++ b/cmds/screencap/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+ "presubmit": [
+ {
+ "name": "libscreencap_test"
+ }
+ ],
+ "hwasan-presubmit": [
+ {
+ "name": "libscreencap_test"
+ }
+ ]
+} \ No newline at end of file
diff --git a/cmds/screencap/screencap.cpp b/cmds/screencap/screencap.cpp
index d563ad3fd3db..9ff1161081fc 100644
--- a/cmds/screencap/screencap.cpp
+++ b/cmds/screencap/screencap.cpp
@@ -37,6 +37,9 @@
#include <ui/GraphicTypes.h>
#include <ui/PixelFormat.h>
+#include "utils/Errors.h"
+#include "screencap_utils.h"
+
using namespace android;
#define COLORSPACE_UNKNOWN 0
@@ -145,24 +148,6 @@ static status_t notifyMediaScanner(const char* fileName) {
return NO_ERROR;
}
-status_t capture(const DisplayId displayId,
- const gui::CaptureArgs& captureArgs,
- ScreenCaptureResults& outResult) {
- sp<SyncScreenCaptureListener> captureListener = new SyncScreenCaptureListener();
- ScreenshotClient::captureDisplay(displayId, captureArgs, captureListener);
-
- ScreenCaptureResults captureResults = captureListener->waitForResults();
- if (!captureResults.fenceResult.ok()) {
- fprintf(stderr, "Failed to take screenshot. Status: %d\n",
- fenceStatus(captureResults.fenceResult));
- return 1;
- }
-
- outResult = captureResults;
-
- return 0;
-}
-
status_t saveImage(const char* fn, std::optional<AndroidBitmapCompressFormat> format,
const ScreenCaptureResults& captureResults) {
void* base = nullptr;
@@ -427,15 +412,12 @@ int main(int argc, char** argv)
std::vector<ScreenCaptureResults> results;
const size_t numDisplays = displaysToCapture.size();
- for (int i=0; i<numDisplays; i++) {
- ScreenCaptureResults result;
-
+ for (int i = 0; i < numDisplays; i++) {
// 1. Capture the screen
- if (const status_t captureStatus =
- capture(displaysToCapture[i], captureArgs, result) != 0) {
-
- fprintf(stderr, "Capturing failed.\n");
- return captureStatus;
+ auto captureResult = screencap::capture(displaysToCapture[i], captureArgs);
+ if (!captureResult.ok()) {
+ fprintf(stderr, "%sCapturing failed.\n", captureResult.error().message().c_str());
+ return 1;
}
// 2. Save the capture result as an image.
@@ -453,7 +435,7 @@ int main(int argc, char** argv)
if (!filename.empty()) {
fn = filename.c_str();
}
- if (const status_t saveImageStatus = saveImage(fn, format, result) != 0) {
+ if (const status_t saveImageStatus = saveImage(fn, format, captureResult.value()) != 0) {
fprintf(stderr, "Saving image failed.\n");
return saveImageStatus;
}
diff --git a/cmds/screencap/screencap_utils.cpp b/cmds/screencap/screencap_utils.cpp
new file mode 100644
index 000000000000..03ade73d0e30
--- /dev/null
+++ b/cmds/screencap/screencap_utils.cpp
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "screencap_utils.h"
+
+#include "gui/SyncScreenCaptureListener.h"
+
+namespace android::screencap {
+
+base::Result<gui::ScreenCaptureResults> capture(const DisplayId displayId,
+ const gui::CaptureArgs& captureArgs) {
+ sp<SyncScreenCaptureListener> captureListener = new SyncScreenCaptureListener();
+ auto captureDisplayStatus =
+ ScreenshotClient::captureDisplay(displayId, captureArgs, captureListener);
+
+ gui::ScreenCaptureResults captureResults = captureListener->waitForResults();
+ if (!captureResults.fenceResult.ok()) {
+ status_t captureStatus = fenceStatus(captureResults.fenceResult);
+ std::stringstream errorMsg;
+ errorMsg << "Failed to take take screenshot. ";
+ if (captureStatus == NAME_NOT_FOUND) {
+ errorMsg << "Display Id '" << displayId.value << "' is not valid.\n";
+ }
+ return base::ResultError(errorMsg.str(), captureStatus);
+ }
+
+ return captureResults;
+}
+
+} // namespace android::screencap \ No newline at end of file
diff --git a/cmds/screencap/screencap_utils.h b/cmds/screencap/screencap_utils.h
new file mode 100644
index 000000000000..6580e3fa5ff1
--- /dev/null
+++ b/cmds/screencap/screencap_utils.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <android-base/result.h>
+#include <android/gui/DisplayCaptureArgs.h>
+
+#include "gui/ScreenCaptureResults.h"
+#include "ui/DisplayId.h"
+
+#pragma once
+
+namespace android::screencap {
+base::Result<gui::ScreenCaptureResults> capture(const DisplayId displayId,
+ const gui::CaptureArgs& captureArgs);
+} // namespace android::screencap
diff --git a/cmds/screencap/tests/screencap_test.cpp b/cmds/screencap/tests/screencap_test.cpp
new file mode 100644
index 000000000000..b7bfca9ada65
--- /dev/null
+++ b/cmds/screencap/tests/screencap_test.cpp
@@ -0,0 +1,68 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <binder/ProcessState.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <gui/SurfaceComposerClient.h>
+
+#include "android/gui/CaptureArgs.h"
+#include "gmock/gmock.h"
+#include "gui/ScreenCaptureResults.h"
+#include "screencap_utils.h"
+#include "ui/DisplayId.h"
+
+using ::android::DisplayId;
+using ::android::OK;
+using ::android::PhysicalDisplayId;
+using ::android::ProcessState;
+using ::android::SurfaceComposerClient;
+using ::android::gui::CaptureArgs;
+using ::android::gui::ScreenCaptureResults;
+using ::testing::AllOf;
+using ::testing::HasSubstr;
+
+class ScreenCapTest : public ::testing::Test {
+protected:
+ static void SetUpTestSuite() {
+ // These lines are copied from screencap.cpp. They are necessary to call binder.
+ ProcessState::self()->setThreadPoolMaxThreadCount(0);
+ ProcessState::self()->startThreadPool();
+ }
+};
+
+TEST_F(ScreenCapTest, Capture_InvalidDisplayNumber) {
+ DisplayId display;
+ display.value = -1;
+
+ CaptureArgs args;
+ auto result = ::android::screencap::capture(display, args);
+ EXPECT_FALSE(result.ok());
+ EXPECT_THAT(result.error().message(),
+ AllOf(HasSubstr("Display Id"), HasSubstr("is not valid.")));
+}
+
+TEST_F(ScreenCapTest, Capture_SuccessWithPhysicalDisplay) {
+ const std::vector<PhysicalDisplayId> physicalDisplays =
+ SurfaceComposerClient::getPhysicalDisplayIds();
+
+ ASSERT_FALSE(physicalDisplays.empty());
+ DisplayId display;
+ display.value = physicalDisplays.front().value;
+
+ CaptureArgs args;
+ auto result = ::android::screencap::capture(display, args);
+ EXPECT_TRUE(result.ok());
+ // TODO consider verifying actual captured image.
+} \ No newline at end of file
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ab824119d643..9a848d423c9a 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -16052,7 +16052,7 @@ package android.telephony {
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isLteCdmaEvdoGsmWcdmaEnabled();
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isMobileDataPolicyEnabled(int);
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNrDualConnectivityEnabled();
- method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNullCipherNotificationsEnabled();
+ method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNullCipherNotificationsEnabled();
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public boolean isOffhook();
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isOpportunisticNetworkEnabled();
method @Deprecated @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isPotentialEmergencyNumber(@NonNull String);
@@ -16097,7 +16097,7 @@ package android.telephony {
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMobileDataPolicyEnabled(int, boolean);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMultiSimCarrierRestriction(boolean);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public int setNrDualConnectivityState(int);
- method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setNullCipherNotificationsEnabled(boolean);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setNullCipherNotificationsEnabled(boolean);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setOpportunisticNetworkState(boolean);
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setPreferredNetworkTypeBitmask(long);
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setRadio(boolean);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 0b0738ee14dc..c60a2d451ab3 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1722,6 +1722,7 @@ package android.hardware.display {
public final class DisplayManager {
method public boolean areUserDisabledHdrTypesAllowed();
method @RequiresPermission(android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE) public void clearGlobalUserPreferredDisplayMode();
+ method @FlaggedApi("com.android.server.display.feature.flags.display_topology") @Nullable @RequiresPermission("android.permission.MANAGE_DISPLAYS") public android.hardware.display.DisplayTopology getDisplayTopology();
method @Nullable public android.view.Display.Mode getGlobalUserPreferredDisplayMode();
method @NonNull public android.hardware.display.HdrConversionMode getHdrConversionModeSetting();
method @NonNull public int[] getSupportedHdrOutputTypes();
@@ -1747,6 +1748,13 @@ package android.hardware.display {
field public static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 64; // 0x40
}
+ @FlaggedApi("com.android.server.display.feature.flags.display_topology") public final class DisplayTopology implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.util.SparseArray<android.graphics.RectF> getAbsoluteBounds();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.display.DisplayTopology> CREATOR;
+ }
+
}
package android.hardware.fingerprint {
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 4e6fb8d3a8e7..e6082d0df1f8 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -615,3 +615,10 @@ flag {
bug: "346553745"
is_exported: true
}
+
+flag {
+ namespace: "multi_user"
+ name: "logout_user_api"
+ description: "Add API to logout user"
+ bug: "350045389"
+}
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index a96de4b050a3..fded88212127 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -1873,6 +1873,8 @@ public final class DisplayManager {
*/
@RequiresPermission(MANAGE_DISPLAYS)
@Nullable
+ @TestApi
+ @FlaggedApi(Flags.FLAG_DISPLAY_TOPOLOGY)
public DisplayTopology getDisplayTopology() {
return mGlobal.getDisplayTopology();
}
diff --git a/core/java/android/hardware/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java
index 785a0e0adc48..4ed0fc056e7d 100644
--- a/core/java/android/hardware/display/DisplayTopology.java
+++ b/core/java/android/hardware/display/DisplayTopology.java
@@ -21,8 +21,10 @@ import static android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT;
import static android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT;
import static android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.Nullable;
+import android.annotation.TestApi;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Parcel;
@@ -39,6 +41,7 @@ import android.view.Display;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.feature.flags.Flags;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -59,6 +62,8 @@ import java.util.Queue;
*
* @hide
*/
+@TestApi
+@FlaggedApi(Flags.FLAG_DISPLAY_TOPOLOGY)
public final class DisplayTopology implements Parcelable {
private static final String TAG = "DisplayTopology";
private static final float EPSILON = 0.0001f;
@@ -82,6 +87,7 @@ public final class DisplayTopology implements Parcelable {
* @param px The value in logical pixels
* @param dpi The logical density of the display
* @return The value in density-independent pixels
+ * @hide
*/
public static float pxToDp(float px, int dpi) {
return px * DisplayMetrics.DENSITY_DEFAULT / dpi;
@@ -91,6 +97,7 @@ public final class DisplayTopology implements Parcelable {
* @param dp The value in density-independent pixels
* @param dpi The logical density of the display
* @return The value in logical pixels
+ * @hide
*/
public static float dpToPx(float dp, int dpi) {
return dp * dpi / DisplayMetrics.DENSITY_DEFAULT;
@@ -108,8 +115,14 @@ public final class DisplayTopology implements Parcelable {
*/
private int mPrimaryDisplayId = Display.INVALID_DISPLAY;
+ /**
+ * @hide
+ */
public DisplayTopology() {}
+ /**
+ * @hide
+ */
public DisplayTopology(@Nullable TreeNode root, int primaryDisplayId) {
mRoot = root;
if (mRoot != null) {
@@ -124,15 +137,24 @@ public final class DisplayTopology implements Parcelable {
mPrimaryDisplayId = primaryDisplayId;
}
+ /**
+ * @hide
+ */
public DisplayTopology(Parcel source) {
this(source.readTypedObject(TreeNode.CREATOR), source.readInt());
}
+ /**
+ * @hide
+ */
@Nullable
public TreeNode getRoot() {
return mRoot;
}
+ /**
+ * @hide
+ */
public int getPrimaryDisplayId() {
return mPrimaryDisplayId;
}
@@ -144,6 +166,7 @@ public final class DisplayTopology implements Parcelable {
* @param displayId The logical display ID
* @param width The width of the display
* @param height The height of the display
+ * @hide
*/
public void addDisplay(int displayId, float width, float height) {
addDisplay(displayId, width, height, /* shouldLog= */ true);
@@ -155,6 +178,7 @@ public final class DisplayTopology implements Parcelable {
* @param width The new width
* @param height The new height
* @return True if the topology has changed.
+ * @hide
*/
public boolean updateDisplay(int displayId, float width, float height) {
TreeNode display = findDisplay(displayId, mRoot);
@@ -178,6 +202,7 @@ public final class DisplayTopology implements Parcelable {
* one by one.
* @param displayId The logical display ID
* @return True if the display was present in the topology and removed.
+ * @hide
*/
public boolean removeDisplay(int displayId) {
if (findDisplay(displayId, mRoot) == null) {
@@ -221,6 +246,7 @@ public final class DisplayTopology implements Parcelable {
* are the display IDs.
* @throws IllegalArgumentException if the keys in {@code positions} are not the exact display
* IDs in this topology, no more, no less
+ * @hide
*/
public void rearrange(Map<Integer, PointF> newPos) {
if (mRoot == null) {
@@ -346,6 +372,7 @@ public final class DisplayTopology implements Parcelable {
/**
* Clamp offsets and remove any overlaps between displays.
+ * @hide
*/
public void normalize() {
if (mRoot == null) {
@@ -494,6 +521,7 @@ public final class DisplayTopology implements Parcelable {
/**
* @return A deep copy of the topology that will not be modified by the system.
+ * @hide
*/
public DisplayTopology copy() {
TreeNode rootCopy = mRoot == null ? null : mRoot.copy();
@@ -505,6 +533,7 @@ public final class DisplayTopology implements Parcelable {
* (0, 0).
* @return Map from logical display ID to the display's absolute bounds
*/
+ @NonNull
public SparseArray<RectF> getAbsoluteBounds() {
Map<TreeNode, RectF> bounds = new HashMap<>();
getInfo(bounds, /* depths= */ null, /* parents= */ null, mRoot, /* x= */ 0, /* y= */ 0,
@@ -529,6 +558,7 @@ public final class DisplayTopology implements Parcelable {
/**
* Print the object's state and debug information into the given stream.
+ * @hide
* @param pw The stream to dump information to.
*/
public void dump(PrintWriter pw) {
@@ -629,6 +659,9 @@ public final class DisplayTopology implements Parcelable {
return result;
}
+ /**
+ * @hide
+ */
@Nullable
public static TreeNode findDisplay(int displayId, @Nullable TreeNode startingNode) {
if (startingNode == null) {
@@ -725,6 +758,7 @@ public final class DisplayTopology implements Parcelable {
* @param densityPerDisplay The logical display densities, indexed by logical display ID
* @return The graph representation of the topology. If there is a corner adjacency, the same
* display will appear twice in the list of adjacent displays with both possible placements.
+ * @hide
*/
@Nullable
public DisplayTopologyGraph getGraph(SparseIntArray densityPerDisplay) {
@@ -839,6 +873,9 @@ public final class DisplayTopology implements Parcelable {
}
}
+ /**
+ * @hide
+ */
public static final class TreeNode implements Parcelable {
public static final int POSITION_LEFT = 0;
public static final int POSITION_TOP = 1;
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index 18f9b2b9d74f..59bd9822c157 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -83,6 +83,8 @@ interface IUserManager {
long getUserCreationTime(int userId);
int getUserSwitchability(int userId);
boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable, int mUserId);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_USERS)")
+ int getUserLogoutability(int userId);
boolean isRestricted(int userId);
boolean canHaveRestrictedProfile(int userId);
boolean canAddPrivateProfile(int userId);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index ce93c71ac776..c00f31db1a38 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2261,6 +2261,45 @@ public class UserManager {
public @interface UserSwitchabilityResult {}
/**
+ * Indicates that user can logout.
+ * @hide
+ */
+ public static final int LOGOUTABILITY_STATUS_OK = 0;
+
+ /**
+ * Indicates that user cannot logout because it is the system user.
+ * @hide
+ */
+ public static final int LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER = 1;
+
+ /**
+ * Indicates that user cannot logout because there is no suitable user to logout to. This is
+ * generally applicable to Headless System User Mode devices that do not have an interactive
+ * system user.
+ * @hide
+ */
+ public static final int LOGOUTABILITY_STATUS_NO_SUITABLE_USER_TO_LOGOUT_TO = 2;
+
+ /**
+ * Indicates that user cannot logout because user switch cannot happen.
+ * @hide
+ */
+ public static final int LOGOUTABILITY_STATUS_CANNOT_SWITCH = 3;
+
+ /**
+ * Result returned in {@link #getUserLogoutability()} indicating user logoutability.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = { "LOGOUTABILITY_STATUS_" }, value = {
+ LOGOUTABILITY_STATUS_OK,
+ LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER,
+ LOGOUTABILITY_STATUS_NO_SUITABLE_USER_TO_LOGOUT_TO,
+ LOGOUTABILITY_STATUS_CANNOT_SWITCH
+ })
+ public @interface UserLogoutability {}
+
+ /**
* A response code from {@link #removeUserWhenPossible(UserHandle, boolean)} indicating that
* the specified user has been successfully removed.
*
@@ -2737,6 +2776,35 @@ public class UserManager {
}
/**
+ * Returns whether logging out is currently allowed for the context user.
+ *
+ * <p>Logging out is not allowed in the following cases:
+ * <ol>
+ * <li>the user is system user
+ * <li>there is no suitable user to logout to (if no interactive system user)
+ * <li>the user is in a phone call
+ * <li>{@link #DISALLOW_USER_SWITCH} is set
+ * <li>system user hasn't been unlocked yet
+ * </ol>
+ *
+ * @return A {@link UserLogoutability} flag indicating if the user can logout,
+ * one of {@link #LOGOUTABILITY_STATUS_OK},
+ * {@link #LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER},
+ * {@link #LOGOUTABILITY_STATUS_NO_SUITABLE_USER_TO_LOGOUT_TO},
+ * {@link #LOGOUTABILITY_STATUS_CANNOT_SWITCH}.
+ * @hide
+ */
+ @UserHandleAware
+ @RequiresPermission(Manifest.permission.MANAGE_USERS)
+ public @UserLogoutability int getUserLogoutability() {
+ try {
+ return mService.getUserLogoutability(mUserId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns the userId for the context user.
*
* @return the userId of the context user.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 350906818cb4..900f22d2b37b 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -9349,6 +9349,16 @@ public final class ViewRootImpl implements ViewParent,
return mVibrator;
}
+ /**
+ * Clears the system vibrator.
+ *
+ * <p>This method releases the reference to the system vibrator. It's crucial to call this
+ * method when the vibrator is no longer needed to prevent any potential memory leaks.
+ */
+ public void clearSystemVibrator() {
+ mVibrator = null;
+ }
+
private @Nullable AutofillManager getAutofillManager() {
if (mView instanceof ViewGroup) {
ViewGroup decorView = (ViewGroup) mView;
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 68b5a261f507..1156503cf8e8 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -1130,6 +1130,19 @@ public final class WindowContainerTransaction implements Parcelable {
}
/**
+ * Adds a hierarchy op for app compat reachability.
+ *
+ * @param container The token for the container Task
+ * @param taskId The id of the current task
+ * @hide
+ */
+ public WindowContainerTransaction setReachabilityOffset(
+ @NonNull WindowContainerToken container, int taskId, int x, int y) {
+ mHierarchyOps.add(HierarchyOp.createForReachability(container.asBinder(), taskId, x, y));
+ return this;
+ }
+
+ /**
* Merges another WCT into this one.
* @param transfer When true, this will transfer everything from other potentially leaving
* other in an unusable state. When false, other is left alone, but
@@ -1590,6 +1603,7 @@ public final class WindowContainerTransaction implements Parcelable {
public static final int HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE = 22;
public static final int HIERARCHY_OP_TYPE_SET_DISABLE_LAUNCH_ADJACENT = 23;
public static final int HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK = 24;
+ public static final int HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY = 25;
@IntDef(prefix = {"HIERARCHY_OP_TYPE_"}, value = {
HIERARCHY_OP_TYPE_REPARENT,
@@ -1617,6 +1631,7 @@ public final class WindowContainerTransaction implements Parcelable {
HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE,
HIERARCHY_OP_TYPE_SET_DISABLE_LAUNCH_ADJACENT,
HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK,
+ HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY,
})
@Retention(RetentionPolicy.SOURCE)
public @interface HierarchyOpType {
@@ -1630,6 +1645,10 @@ public final class WindowContainerTransaction implements Parcelable {
public static final String LAUNCH_KEY_SHORTCUT_CALLING_PACKAGE =
"android:transaction.hop.shortcut_calling_package";
+ // The following keys are used to define the reachability direction after a double tap.
+ public static final String REACHABILITY_EVENT_X = "android:transaction.reachability_x";
+ public static final String REACHABILITY_EVENT_Y = "android:transaction.reachability_y";
+
@HierarchyOpType
private final int mType;
@@ -1665,6 +1684,9 @@ public final class WindowContainerTransaction implements Parcelable {
private Bundle mLaunchOptions;
@Nullable
+ private Bundle mAppCompatOptions;
+
+ @Nullable
private Intent mActivityIntent;
/** Used as options for {@link #addTaskFragmentOperation}. */
@@ -1833,7 +1855,21 @@ public final class WindowContainerTransaction implements Parcelable {
.build();
}
- /** Creates a hierarchy op for setting a task non-trimmable by recents. */
+ /** Create a hierarchy op for app compat reachability. */
+ @NonNull
+ public static HierarchyOp createForReachability(IBinder container, int taskId, int x,
+ int y) {
+ final Bundle appCompatOptions = new Bundle();
+ appCompatOptions.putInt(LAUNCH_KEY_TASK_ID, taskId);
+ appCompatOptions.putInt(REACHABILITY_EVENT_X, x);
+ appCompatOptions.putInt(REACHABILITY_EVENT_Y, y);
+ return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY)
+ .setAppCompatOptions(appCompatOptions)
+ .setContainer(container)
+ .build();
+ }
+
+ /** Create a hierarchy op for setting a task non-trimmable by recents. */
@NonNull
@FlaggedApi(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
public static HierarchyOp createForSetTaskTrimmableFromRecents(@NonNull IBinder container,
@@ -1863,6 +1899,7 @@ public final class WindowContainerTransaction implements Parcelable {
mWindowingModes = copy.mWindowingModes;
mActivityTypes = copy.mActivityTypes;
mLaunchOptions = copy.mLaunchOptions;
+ mAppCompatOptions = copy.mAppCompatOptions;
mActivityIntent = copy.mActivityIntent;
mTaskFragmentOperation = copy.mTaskFragmentOperation;
mKeyguardState = copy.mKeyguardState;
@@ -1889,6 +1926,7 @@ public final class WindowContainerTransaction implements Parcelable {
mWindowingModes = in.createIntArray();
mActivityTypes = in.createIntArray();
mLaunchOptions = in.readBundle();
+ mAppCompatOptions = in.readBundle(getClass().getClassLoader());
mActivityIntent = in.readTypedObject(Intent.CREATOR);
mTaskFragmentOperation = in.readTypedObject(TaskFragmentOperation.CREATOR);
mKeyguardState = in.readTypedObject(KeyguardState.CREATOR);
@@ -1966,6 +2004,11 @@ public final class WindowContainerTransaction implements Parcelable {
}
@Nullable
+ public Bundle getAppCompatOptions() {
+ return mAppCompatOptions;
+ }
+
+ @Nullable
public Intent getActivityIntent() {
return mActivityIntent;
}
@@ -2100,6 +2143,9 @@ public final class WindowContainerTransaction implements Parcelable {
case HIERARCHY_OP_TYPE_LAUNCH_TASK:
sb.append(mLaunchOptions);
break;
+ case HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY:
+ sb.append(mAppCompatOptions);
+ break;
case HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT:
sb.append("container=").append(mContainer).append(" clearRoot=").append(mToTop);
break;
@@ -2182,6 +2228,7 @@ public final class WindowContainerTransaction implements Parcelable {
dest.writeIntArray(mWindowingModes);
dest.writeIntArray(mActivityTypes);
dest.writeBundle(mLaunchOptions);
+ dest.writeBundle(mAppCompatOptions);
dest.writeTypedObject(mActivityIntent, flags);
dest.writeTypedObject(mTaskFragmentOperation, flags);
dest.writeTypedObject(mKeyguardState, flags);
@@ -2245,6 +2292,9 @@ public final class WindowContainerTransaction implements Parcelable {
private Bundle mLaunchOptions;
@Nullable
+ private Bundle mAppCompatOptions;
+
+ @Nullable
private Intent mActivityIntent;
@Nullable
@@ -2328,6 +2378,11 @@ public final class WindowContainerTransaction implements Parcelable {
return this;
}
+ Builder setAppCompatOptions(@Nullable Bundle appCompatOptions) {
+ mAppCompatOptions = appCompatOptions;
+ return this;
+ }
+
Builder setActivityIntent(@Nullable Intent activityIntent) {
mActivityIntent = activityIntent;
return this;
@@ -2407,6 +2462,7 @@ public final class WindowContainerTransaction implements Parcelable {
hierarchyOp.mToTop = mToTop;
hierarchyOp.mReparentTopOnly = mReparentTopOnly;
hierarchyOp.mLaunchOptions = mLaunchOptions;
+ hierarchyOp.mAppCompatOptions = mAppCompatOptions;
hierarchyOp.mActivityIntent = mActivityIntent;
hierarchyOp.mPendingIntent = mPendingIntent;
hierarchyOp.mAlwaysOnTop = mAlwaysOnTop;
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 9f6ea42c6fc4..25dc6723aa78 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -460,4 +460,15 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
+}
+
+flag {
+ name: "clear_system_vibrator"
+ namespace: "windowing_frontend"
+ description: "Clears the system vibrator before attaching new window, to avoid leaks."
+ bug: "393190314"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
} \ No newline at end of file
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index f49c5f1c2b0f..036faef7aa65 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -34,45 +34,38 @@ import android.os.Build;
import android.os.Parcel;
import android.os.ParcelFormatException;
import android.os.Process;
-import android.os.StatFs;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.util.ArraySet;
-import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
/**
* BatteryStatsHistory encapsulates battery history files.
* Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into
- * {@link #mActiveFile}.
- * When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
+ * {@link #mActiveFragment}.
+ * When {@link #mHistoryBuffer} size reaches {@link #mMaxHistoryBufferSize},
* current mActiveFile is closed and a new mActiveFile is open.
* History files are under directory /data/system/battery-history/.
- * History files have name battery-history-<num>.bin. The file number <num> starts from zero and
- * grows sequentially.
+ * History files have name &lt;num&gt;.bf. The file number &lt;num&gt; corresponds to the
+ * monotonic time when the file was started.
* The mActiveFile is always the highest numbered history file.
* The lowest number file is always the oldest file.
* The highest number file is always the newest file.
- * The file number grows sequentially and we never skip number.
- * When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES},
+ * The file number grows monotonically and we never skip number.
+ * When the total size of history files exceeds the maximum allowed value,
* the lowest numbered file is deleted and a new file is open.
*
* All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by
@@ -86,10 +79,6 @@ public class BatteryStatsHistory {
// Current on-disk Parcel version. Must be updated when the format of the parcelable changes
private static final int VERSION = 212;
- private static final String HISTORY_DIR = "battery-history";
- private static final String FILE_SUFFIX = ".bh";
- private static final int MIN_FREE_SPACE = 100 * 1024 * 1024;
-
// Part of initial delta int that specifies the time delta.
static final int DELTA_TIME_MASK = 0x7ffff;
static final int DELTA_TIME_LONG = 0x7ffff; // The delta is a following long
@@ -135,7 +124,7 @@ public class BatteryStatsHistory {
// For state1, trace everything except the wakelock bit (which can race with
// suspend) and the running bit (which isn't meaningful in traces).
static final int STATE1_TRACE_MASK = ~(HistoryItem.STATE_WAKE_LOCK_FLAG
- | HistoryItem.STATE_CPU_RUNNING_FLAG);
+ | HistoryItem.STATE_CPU_RUNNING_FLAG);
// For state2, trace all bit changes.
static final int STATE2_TRACE_MASK = ~0;
@@ -146,22 +135,132 @@ public class BatteryStatsHistory {
*/
private static final int EXTRA_BUFFER_SIZE_WHEN_DIR_LOCKED = 100_000;
+ public abstract static class BatteryHistoryFragment
+ implements Comparable<BatteryHistoryFragment> {
+ public final long monotonicTimeMs;
+
+ public BatteryHistoryFragment(long monotonicTimeMs) {
+ this.monotonicTimeMs = monotonicTimeMs;
+ }
+
+ @Override
+ public int compareTo(BatteryHistoryFragment o) {
+ return Long.compare(monotonicTimeMs, o.monotonicTimeMs);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return monotonicTimeMs == ((BatteryHistoryFragment) o).monotonicTimeMs;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(monotonicTimeMs);
+ }
+ }
+
+ /**
+ * Persistent storage for battery history fragments
+ */
+ public interface BatteryHistoryStore {
+ /**
+ * Returns the table of contents, in the chronological order.
+ */
+ List<BatteryHistoryFragment> getFragments();
+
+ /**
+ * Returns the earliest available fragment
+ */
+ @Nullable
+ BatteryHistoryFragment getEarliestFragment();
+
+ /**
+ * Returns the latest available fragment
+ */
+ @Nullable
+ BatteryHistoryFragment getLatestFragment();
+
+ /**
+ * Given a fragment, returns the earliest fragment that follows it whose monotonic
+ * start time falls within the specified range. `startTimeMs` is inclusive, `endTimeMs`
+ * is exclusive.
+ */
+ @Nullable
+ BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs,
+ long endTimeMs);
+
+ /**
+ * Acquires a lock on the entire store.
+ */
+ void lock();
+
+ /**
+ * Acquires a lock unless the store is already locked by a different thread. Returns true
+ * if the lock has been successfully acquired.
+ */
+ boolean tryLock();
+
+ /**
+ * Unlocks the store.
+ */
+ void unlock();
+
+ /**
+ * Returns true if the store is currently locked.
+ */
+ boolean isLocked();
+
+ /**
+ * Returns the total amount of storage occupied by history fragments, in bytes.
+ */
+ int getSize();
+
+ /**
+ * Returns true if the store contains any history fragments, excluding the currently
+ * active partial fragment.
+ */
+ boolean hasCompletedFragments();
+
+ /**
+ * Creates a new empty history fragment starting at the specified time.
+ */
+ BatteryHistoryFragment createFragment(long monotonicStartTime);
+
+ /**
+ * Writes a fragment to disk as raw bytes.
+ *
+ * @param fragmentComplete indicates if this fragment is done or still partial.
+ */
+ void writeFragment(BatteryHistoryFragment fragment, @NonNull byte[] bytes,
+ boolean fragmentComplete);
+
+ /**
+ * Reads a fragment as raw bytes.
+ */
+ @Nullable
+ byte[] readFragment(BatteryHistoryFragment fragment);
+
+ /**
+ * Removes all persistent fragments
+ */
+ void reset();
+ }
+
private final Parcel mHistoryBuffer;
- private final File mSystemDir;
private final HistoryStepDetailsCalculator mStepDetailsCalculator;
private final Clock mClock;
private int mMaxHistoryBufferSize;
/**
- * The active history file that the history buffer is backed up into.
+ * The active history fragment that the history buffer is backed up into.
*/
- private AtomicFile mActiveFile;
+ private BatteryHistoryFragment mActiveFragment;
/**
- * A list of history files with increasing timestamps.
+ * Persistent storage of history files.
*/
- private final BatteryHistoryDirectory mHistoryDir;
+ private final BatteryHistoryStore mStore;
/**
* A list of small history parcels, used when BatteryStatsImpl object is created from
@@ -172,7 +271,7 @@ public class BatteryStatsHistory {
/**
* When iterating history files, the current file index.
*/
- private BatteryHistoryFile mCurrentFile;
+ private BatteryHistoryFragment mCurrentFragment;
/**
* When iterating history files, the current file parcel.
@@ -221,326 +320,6 @@ public class BatteryStatsHistory {
private int mIteratorCookie;
private final BatteryStatsHistory mWritableHistory;
- private static class BatteryHistoryFile implements Comparable<BatteryHistoryFile> {
- public final long monotonicTimeMs;
- public final AtomicFile atomicFile;
-
- private BatteryHistoryFile(File directory, long monotonicTimeMs) {
- this.monotonicTimeMs = monotonicTimeMs;
- atomicFile = new AtomicFile(new File(directory, monotonicTimeMs + FILE_SUFFIX));
- }
-
- @Override
- public int compareTo(BatteryHistoryFile o) {
- return Long.compare(monotonicTimeMs, o.monotonicTimeMs);
- }
-
- @Override
- public boolean equals(Object o) {
- return monotonicTimeMs == ((BatteryHistoryFile) o).monotonicTimeMs;
- }
-
- @Override
- public int hashCode() {
- return Long.hashCode(monotonicTimeMs);
- }
-
- @Override
- public String toString() {
- return atomicFile.getBaseFile().toString();
- }
- }
-
- private static class BatteryHistoryDirectory {
- private final File mDirectory;
- private final MonotonicClock mMonotonicClock;
- private int mMaxHistorySize;
- private final List<BatteryHistoryFile> mHistoryFiles = new ArrayList<>();
- private final ReentrantLock mLock = new ReentrantLock();
- private boolean mCleanupNeeded;
-
- BatteryHistoryDirectory(File directory, MonotonicClock monotonicClock, int maxHistorySize) {
- mDirectory = directory;
- mMonotonicClock = monotonicClock;
- mMaxHistorySize = maxHistorySize;
- if (mMaxHistorySize == 0) {
- Slog.w(TAG, "mMaxHistorySize should not be zero when writing history");
- }
- }
-
- void setMaxHistorySize(int maxHistorySize) {
- mMaxHistorySize = maxHistorySize;
- cleanup();
- }
-
- void lock() {
- mLock.lock();
- }
-
- boolean tryLock() {
- return mLock.tryLock();
- }
-
- void unlock() {
- mLock.unlock();
- if (mCleanupNeeded) {
- cleanup();
- }
- }
-
- boolean isLocked() {
- return mLock.isLocked();
- }
-
- void load() {
- Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
- mDirectory.mkdirs();
- if (!mDirectory.exists()) {
- Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath());
- }
-
- final List<File> toRemove = new ArrayList<>();
- final Set<BatteryHistoryFile> dedup = new ArraySet<>();
- mDirectory.listFiles((dir, name) -> {
- final int b = name.lastIndexOf(FILE_SUFFIX);
- if (b <= 0) {
- toRemove.add(new File(dir, name));
- return false;
- }
- try {
- long monotonicTime = Long.parseLong(name.substring(0, b));
- dedup.add(new BatteryHistoryFile(mDirectory, monotonicTime));
- } catch (NumberFormatException e) {
- toRemove.add(new File(dir, name));
- return false;
- }
- return true;
- });
- if (!dedup.isEmpty()) {
- mHistoryFiles.addAll(dedup);
- Collections.sort(mHistoryFiles);
- }
- if (!toRemove.isEmpty()) {
- // Clear out legacy history files, which did not follow the X-Y.bin naming format.
- BackgroundThread.getHandler().post(() -> {
- lock();
- try {
- for (File file : toRemove) {
- file.delete();
- }
- } finally {
- unlock();
- Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
- }
- });
- } else {
- Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
- }
- }
-
- List<String> getFileNames() {
- lock();
- try {
- List<String> names = new ArrayList<>();
- for (BatteryHistoryFile historyFile : mHistoryFiles) {
- names.add(historyFile.atomicFile.getBaseFile().getName());
- }
- return names;
- } finally {
- unlock();
- }
- }
-
- @Nullable
- BatteryHistoryFile getFirstFile() {
- lock();
- try {
- if (!mHistoryFiles.isEmpty()) {
- return mHistoryFiles.get(0);
- }
- return null;
- } finally {
- unlock();
- }
- }
-
- @Nullable
- BatteryHistoryFile getLastFile() {
- lock();
- try {
- if (!mHistoryFiles.isEmpty()) {
- return mHistoryFiles.get(mHistoryFiles.size() - 1);
- }
- return null;
- } finally {
- unlock();
- }
- }
-
- @Nullable
- BatteryHistoryFile getNextFile(BatteryHistoryFile current, long startTimeMs,
- long endTimeMs) {
- if (!mLock.isHeldByCurrentThread()) {
- throw new IllegalStateException("Iterating battery history without a lock");
- }
-
- int nextFileIndex = 0;
- int firstFileIndex = 0;
- // skip the last file because its data is in history buffer.
- int lastFileIndex = mHistoryFiles.size() - 2;
- for (int i = lastFileIndex; i >= 0; i--) {
- BatteryHistoryFile file = mHistoryFiles.get(i);
- if (current != null && file.monotonicTimeMs == current.monotonicTimeMs) {
- nextFileIndex = i + 1;
- }
- if (file.monotonicTimeMs > endTimeMs) {
- lastFileIndex = i - 1;
- }
- if (file.monotonicTimeMs <= startTimeMs) {
- firstFileIndex = i;
- break;
- }
- }
-
- if (nextFileIndex < firstFileIndex) {
- nextFileIndex = firstFileIndex;
- }
-
- if (nextFileIndex <= lastFileIndex) {
- return mHistoryFiles.get(nextFileIndex);
- }
-
- return null;
- }
-
- BatteryHistoryFile makeBatteryHistoryFile() {
- BatteryHistoryFile file = new BatteryHistoryFile(mDirectory,
- mMonotonicClock.monotonicTime());
- lock();
- try {
- mHistoryFiles.add(file);
- } finally {
- unlock();
- }
- return file;
- }
-
- void writeToParcel(Parcel out, boolean useBlobs,
- long preferredEarliestIncludedTimestampMs) {
- Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel");
- lock();
- try {
- final long start = SystemClock.uptimeMillis();
- for (int i = 0; i < mHistoryFiles.size() - 1; i++) {
- long monotonicEndTime = Long.MAX_VALUE;
- if (i < mHistoryFiles.size() - 1) {
- monotonicEndTime = mHistoryFiles.get(i + 1).monotonicTimeMs;
- }
-
- if (monotonicEndTime < preferredEarliestIncludedTimestampMs) {
- continue;
- }
-
- AtomicFile file = mHistoryFiles.get(i).atomicFile;
- byte[] raw = new byte[0];
- try {
- raw = file.readFully();
- } catch (Exception e) {
- Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
- }
-
- out.writeBoolean(true);
- if (useBlobs) {
- out.writeBlob(raw);
- } else {
- // Avoiding blobs in the check-in file for compatibility
- out.writeByteArray(raw);
- }
- }
- out.writeBoolean(false);
- if (DEBUG) {
- Slog.d(TAG,
- "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start));
- }
- } finally {
- unlock();
- Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
- }
- }
-
- int getFileCount() {
- lock();
- try {
- return mHistoryFiles.size();
- } finally {
- unlock();
- }
- }
-
- int getSize() {
- lock();
- try {
- int ret = 0;
- for (int i = 0; i < mHistoryFiles.size() - 1; i++) {
- ret += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length();
- }
- return ret;
- } finally {
- unlock();
- }
- }
-
- void reset() {
- lock();
- try {
- if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!");
- for (BatteryHistoryFile file : mHistoryFiles) {
- file.atomicFile.delete();
- }
- mHistoryFiles.clear();
- } finally {
- unlock();
- }
- }
-
- private void cleanup() {
- Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.cleanup");
- try {
- if (mDirectory == null) {
- return;
- }
-
- if (!tryLock()) {
- mCleanupNeeded = true;
- return;
- }
-
- mCleanupNeeded = false;
- try {
- // if free disk space is less than 100MB, delete oldest history file.
- if (!hasFreeDiskSpace(mDirectory)) {
- BatteryHistoryFile oldest = mHistoryFiles.remove(0);
- oldest.atomicFile.delete();
- }
-
- // if there is more history stored than allowed, delete oldest history files.
- int size = getSize();
- while (size > mMaxHistorySize) {
- BatteryHistoryFile oldest = mHistoryFiles.get(0);
- int length = (int) oldest.atomicFile.getBaseFile().length();
- oldest.atomicFile.delete();
- mHistoryFiles.remove(0);
- size -= length;
- }
- } finally {
- unlock();
- }
- } finally {
- Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
- }
- }
- }
-
/**
* A delegate responsible for computing additional details for a step in battery history.
*/
@@ -621,24 +400,22 @@ public class BatteryStatsHistory {
/**
* Constructor
*
- * @param systemDir typically /data/system
- * @param maxHistorySize the largest amount of battery history to keep on disk
* @param maxHistoryBufferSize the most amount of RAM to used for buffering of history steps
*/
- public BatteryStatsHistory(Parcel historyBuffer, File systemDir,
- int maxHistorySize, int maxHistoryBufferSize,
- HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
- MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger) {
- this(historyBuffer, systemDir, maxHistorySize, maxHistoryBufferSize, stepDetailsCalculator,
+ public BatteryStatsHistory(Parcel historyBuffer, int maxHistoryBufferSize,
+ @Nullable BatteryHistoryStore store, HistoryStepDetailsCalculator stepDetailsCalculator,
+ Clock clock, MonotonicClock monotonicClock, TraceDelegate tracer,
+ EventLogger eventLogger) {
+ this(historyBuffer, maxHistoryBufferSize, store,
+ stepDetailsCalculator,
clock, monotonicClock, tracer, eventLogger, null);
}
- private BatteryStatsHistory(@Nullable Parcel historyBuffer, @Nullable File systemDir,
- int maxHistorySize, int maxHistoryBufferSize,
+ private BatteryStatsHistory(@Nullable Parcel historyBuffer, int maxHistoryBufferSize,
+ @Nullable BatteryHistoryStore store,
@NonNull HistoryStepDetailsCalculator stepDetailsCalculator, @NonNull Clock clock,
@NonNull MonotonicClock monotonicClock, @NonNull TraceDelegate tracer,
@NonNull EventLogger eventLogger, @Nullable BatteryStatsHistory writableHistory) {
- mSystemDir = systemDir;
mMaxHistoryBufferSize = maxHistoryBufferSize;
mStepDetailsCalculator = stepDetailsCalculator;
mTracer = tracer;
@@ -659,18 +436,16 @@ public class BatteryStatsHistory {
}
if (writableHistory != null) {
- mHistoryDir = writableHistory.mHistoryDir;
- } else if (systemDir != null) {
- mHistoryDir = new BatteryHistoryDirectory(new File(systemDir, HISTORY_DIR),
- monotonicClock, maxHistorySize);
- mHistoryDir.load();
- BatteryHistoryFile activeFile = mHistoryDir.getLastFile();
- if (activeFile == null) {
- activeFile = mHistoryDir.makeBatteryHistoryFile();
- }
- setActiveFile(activeFile);
+ mStore = writableHistory.mStore;
} else {
- mHistoryDir = null;
+ mStore = store;
+ if (mStore != null) {
+ BatteryHistoryFragment activeFile = mStore.getLatestFragment();
+ if (activeFile == null) {
+ activeFile = mStore.createFragment(mMonotonicClock.monotonicTime());
+ }
+ setActiveFragment(activeFile);
+ }
}
}
@@ -681,8 +456,7 @@ public class BatteryStatsHistory {
private BatteryStatsHistory(Parcel parcel) {
mClock = Clock.SYSTEM_CLOCK;
mTracer = null;
- mSystemDir = null;
- mHistoryDir = null;
+ mStore = null;
mStepDetailsCalculator = null;
mEventLogger = new EventLogger();
mWritableHistory = null;
@@ -718,15 +492,6 @@ public class BatteryStatsHistory {
}
/**
- * Changes the maximum amount of history to be kept on disk.
- */
- public void setMaxHistorySize(int maxHistorySize) {
- if (mHistoryDir != null) {
- mHistoryDir.setMaxHistorySize(maxHistorySize);
- }
- }
-
- /**
* Changes the maximum size of the history buffer, in bytes.
*/
public void setMaxHistoryBufferSize(int maxHistoryBufferSize) {
@@ -745,8 +510,8 @@ public class BatteryStatsHistory {
Parcel historyBufferCopy = Parcel.obtain();
historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
- return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null,
- null, null, mEventLogger, this);
+ return new BatteryStatsHistory(historyBufferCopy, 0, mStore, null,
+ null, null, null, mEventLogger, this);
}
} finally {
Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
@@ -757,45 +522,40 @@ public class BatteryStatsHistory {
* Returns true if this instance only supports reading history.
*/
public boolean isReadOnly() {
- return !mMutable || mActiveFile == null/* || mHistoryDir == null*/;
+ return !mMutable || mActiveFragment == null || mStore == null;
}
/**
* Set the active file that mHistoryBuffer is backed up into.
*/
- private void setActiveFile(BatteryHistoryFile file) {
- mActiveFile = file.atomicFile;
+ private void setActiveFragment(BatteryHistoryFragment file) {
+ mActiveFragment = file;
if (DEBUG) {
- Slog.d(TAG, "activeHistoryFile:" + mActiveFile.getBaseFile().getPath());
+ Slog.d(TAG, "activeHistoryFile:" + mActiveFragment);
}
}
/**
- * When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
- * create next history file.
+ * When {@link #mHistoryBuffer} reaches {@link #mMaxHistoryBufferSize},
+ * create next history fragment.
*/
- public void startNextFile(long elapsedRealtimeMs) {
+ public void startNextFragment(long elapsedRealtimeMs) {
synchronized (this) {
- startNextFileLocked(elapsedRealtimeMs);
+ startNextFragmentLocked(elapsedRealtimeMs);
}
}
@GuardedBy("this")
- private void startNextFileLocked(long elapsedRealtimeMs) {
+ private void startNextFragmentLocked(long elapsedRealtimeMs) {
final long start = SystemClock.uptimeMillis();
- writeHistory();
+ writeHistory(true /* fragmentComplete */);
if (DEBUG) {
Slog.d(TAG, "writeHistory took ms:" + (SystemClock.uptimeMillis() - start));
}
- setActiveFile(mHistoryDir.makeBatteryHistoryFile());
- try {
- mActiveFile.getBaseFile().createNewFile();
- } catch (IOException e) {
- Slog.e(TAG, "Could not create history file: " + mActiveFile.getBaseFile());
- }
-
- mHistoryBufferStartTime = mMonotonicClock.monotonicTime(elapsedRealtimeMs);
+ long monotonicStartTime = mMonotonicClock.monotonicTime(elapsedRealtimeMs);
+ setActiveFragment(mStore.createFragment(monotonicStartTime));
+ mHistoryBufferStartTime = monotonicStartTime;
mHistoryBuffer.setDataSize(0);
mHistoryBuffer.setDataPosition(0);
mHistoryBuffer.setDataCapacity(mMaxHistoryBufferSize / 2);
@@ -810,7 +570,6 @@ public class BatteryStatsHistory {
}
mWrittenPowerStatsDescriptors.clear();
- mHistoryDir.cleanup();
}
/**
@@ -818,7 +577,7 @@ public class BatteryStatsHistory {
* currently being read.
*/
public boolean isResetEnabled() {
- return mHistoryDir == null || !mHistoryDir.isLocked();
+ return mStore == null || !mStore.isLocked();
}
/**
@@ -827,11 +586,11 @@ public class BatteryStatsHistory {
*/
public void reset() {
synchronized (this) {
- if (mHistoryDir != null) {
- mHistoryDir.reset();
- setActiveFile(mHistoryDir.makeBatteryHistoryFile());
- }
initHistoryBuffer();
+ if (mStore != null) {
+ mStore.reset();
+ setActiveFragment(mStore.createFragment(mHistoryBufferStartTime));
+ }
}
}
@@ -840,9 +599,9 @@ public class BatteryStatsHistory {
*/
public long getStartTime() {
synchronized (this) {
- BatteryHistoryFile file = mHistoryDir.getFirstFile();
- if (file != null) {
- return file.monotonicTimeMs;
+ BatteryHistoryFragment firstFragment = mStore.getEarliestFragment();
+ if (firstFragment != null) {
+ return firstFragment.monotonicTimeMs;
} else {
return mHistoryBufferStartTime;
}
@@ -863,10 +622,10 @@ public class BatteryStatsHistory {
return copy().iterate(startTimeMs, endTimeMs);
}
- if (mHistoryDir != null) {
- mHistoryDir.lock();
+ if (mStore != null) {
+ mStore.lock();
}
- mCurrentFile = null;
+ mCurrentFragment = null;
mCurrentParcel = null;
mCurrentParcelEnd = 0;
mParcelIndex = 0;
@@ -883,8 +642,8 @@ public class BatteryStatsHistory {
*/
void iteratorFinished() {
mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
- if (mHistoryDir != null) {
- mHistoryDir.unlock();
+ if (mStore != null) {
+ mStore.unlock();
}
Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate",
mIteratorCookie);
@@ -918,27 +677,26 @@ public class BatteryStatsHistory {
}
}
- if (mHistoryDir != null) {
- BatteryHistoryFile nextFile = mHistoryDir.getNextFile(mCurrentFile, startTimeMs,
+ if (mStore != null) {
+ BatteryHistoryFragment next = mStore.getNextFragment(mCurrentFragment, startTimeMs,
endTimeMs);
- while (nextFile != null) {
+ while (next != null) {
mCurrentParcel = null;
mCurrentParcelEnd = 0;
final Parcel p = Parcel.obtain();
- AtomicFile file = nextFile.atomicFile;
- if (readFileToParcel(p, file)) {
+ if (readFragmentToParcel(p, next)) {
int bufSize = p.readInt();
int curPos = p.dataPosition();
mCurrentParcelEnd = curPos + bufSize;
mCurrentParcel = p;
if (curPos < mCurrentParcelEnd) {
- mCurrentFile = nextFile;
+ mCurrentFragment = next;
return mCurrentParcel;
}
} else {
p.recycle();
}
- nextFile = mHistoryDir.getNextFile(nextFile, startTimeMs, endTimeMs);
+ next = mStore.getNextFragment(next, startTimeMs, endTimeMs);
}
}
@@ -988,39 +746,26 @@ public class BatteryStatsHistory {
* Read history file into a parcel.
*
* @param out the Parcel read into.
- * @param file the File to read from.
+ * @param fragment the fragment to read from.
* @return true if success, false otherwise.
*/
- public boolean readFileToParcel(Parcel out, AtomicFile file) {
- Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read");
- try {
- byte[] raw = null;
- try {
- final long start = SystemClock.uptimeMillis();
- raw = file.readFully();
- if (DEBUG) {
- Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
- + " duration ms:" + (SystemClock.uptimeMillis() - start));
- }
- } catch (Exception e) {
- Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
- return false;
- }
- out.unmarshall(raw, 0, raw.length);
- out.setDataPosition(0);
- if (!verifyVersion(out)) {
- return false;
- }
- // skip monotonic time field.
- out.readLong();
- // skip monotonic end time field
- out.readLong();
- // skip monotonic size field
- out.readLong();
- return true;
- } finally {
- Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
+ public boolean readFragmentToParcel(Parcel out, BatteryHistoryFragment fragment) {
+ byte[] data = mStore.readFragment(fragment);
+ if (data == null) {
+ return false;
+ }
+ out.unmarshall(data, 0, data.length);
+ out.setDataPosition(0);
+ if (!verifyVersion(out)) {
+ return false;
}
+ // skip monotonic time field.
+ out.readLong();
+ // skip monotonic end time field
+ out.readLong();
+ // skip monotonic size field
+ out.readLong();
+ return true;
}
/**
@@ -1106,9 +851,8 @@ public class BatteryStatsHistory {
public void writeToParcel(Parcel out) {
synchronized (this) {
writeHistoryBuffer(out);
- /* useBlobs */
- if (mHistoryDir != null) {
- mHistoryDir.writeToParcel(out, false /* useBlobs */, 0);
+ if (mStore != null) {
+ writeToParcel(out, false /* useBlobs */, 0);
}
}
}
@@ -1122,13 +866,54 @@ public class BatteryStatsHistory {
public void writeToBatteryUsageStatsParcel(Parcel out, long preferredHistoryDurationMs) {
synchronized (this) {
out.writeBlob(mHistoryBuffer.marshall());
- if (mHistoryDir != null) {
- mHistoryDir.writeToParcel(out, true /* useBlobs */,
+ if (mStore != null) {
+ writeToParcel(out, true /* useBlobs */,
mHistoryMonotonicEndTime - preferredHistoryDurationMs);
}
}
}
+ private void writeToParcel(Parcel out, boolean useBlobs,
+ long preferredEarliestIncludedTimestampMs) {
+ Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel");
+ mStore.lock();
+ try {
+ final long start = SystemClock.uptimeMillis();
+ List<BatteryHistoryFragment> fragments = mStore.getFragments();
+ for (int i = 0; i < fragments.size() - 1; i++) {
+ long monotonicEndTime = Long.MAX_VALUE;
+ if (i < fragments.size() - 1) {
+ monotonicEndTime = fragments.get(i + 1).monotonicTimeMs;
+ }
+
+ if (monotonicEndTime < preferredEarliestIncludedTimestampMs) {
+ continue;
+ }
+
+ byte[] data = mStore.readFragment(fragments.get(i));
+ if (data == null) {
+ Slog.e(TAG, "Error reading history fragment " + fragments.get(i));
+ continue;
+ }
+
+ out.writeBoolean(true);
+ if (useBlobs) {
+ out.writeBlob(data, 0, data.length);
+ } else {
+ // Avoiding blobs in the check-in file for compatibility
+ out.writeByteArray(data, 0, data.length);
+ }
+ }
+ out.writeBoolean(false);
+ if (DEBUG) {
+ Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start));
+ }
+ } finally {
+ mStore.unlock();
+ Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
+ }
+ }
+
/**
* Reads a BatteryStatsHistory from a parcel written with
* the {@link #writeToBatteryUsageStatsParcel} method.
@@ -1141,28 +926,21 @@ public class BatteryStatsHistory {
* Read history from a check-in file.
*/
public boolean readSummary() {
- if (mActiveFile == null) {
+ if (mActiveFragment == null) {
Slog.w(TAG, "readSummary: no history file associated with this instance");
return false;
}
Parcel parcel = Parcel.obtain();
try {
- final long start = SystemClock.uptimeMillis();
- if (mActiveFile.exists()) {
- byte[] raw = mActiveFile.readFully();
- if (raw.length > 0) {
- parcel.unmarshall(raw, 0, raw.length);
- parcel.setDataPosition(0);
- readHistoryBuffer(parcel);
- }
- if (DEBUG) {
- Slog.d(TAG, "read history file::"
- + mActiveFile.getBaseFile().getPath()
- + " bytes:" + raw.length + " took ms:" + (SystemClock.uptimeMillis()
- - start));
- }
+ byte[] data = mStore.readFragment(mActiveFragment);
+ if (data == null) {
+ return false;
}
+
+ parcel.unmarshall(data, 0, data.length);
+ parcel.setDataPosition(0);
+ readHistoryBuffer(parcel);
} catch (Exception e) {
Slog.e(TAG, "Error reading battery history", e);
reset();
@@ -1201,41 +979,21 @@ public class BatteryStatsHistory {
}
}
- /**
- * @return true if there is more than 100MB free disk space left.
- */
- @android.ravenwood.annotation.RavenwoodReplace
- private static boolean hasFreeDiskSpace(File systemDir) {
- final StatFs stats = new StatFs(systemDir.getAbsolutePath());
- return stats.getAvailableBytes() > MIN_FREE_SPACE;
- }
-
- private static boolean hasFreeDiskSpace$ravenwood(File systemDir) {
- return true;
- }
-
@VisibleForTesting
- public List<String> getFilesNames() {
- return mHistoryDir.getFileNames();
+ public BatteryHistoryStore getBatteryHistoryStore() {
+ return mStore;
}
@VisibleForTesting
- public AtomicFile getActiveFile() {
- return mActiveFile;
- }
-
- /**
- * Returns the maximum storage size allocated to battery history.
- */
- public int getMaxHistorySize() {
- return mHistoryDir.mMaxHistorySize;
+ public BatteryHistoryFragment getActiveFragment() {
+ return mActiveFragment;
}
/**
* @return the total size of all history files and history buffer.
*/
public int getHistoryUsedSize() {
- int ret = mHistoryDir.getSize();
+ int ret = mStore.getSize();
ret += mHistoryBuffer.dataSize();
if (mHistoryParcels != null) {
for (int i = 0; i < mHistoryParcels.size(); i++) {
@@ -1293,7 +1051,7 @@ public class BatteryStatsHistory {
*/
public void continueRecordingHistory() {
synchronized (this) {
- if (mHistoryBuffer.dataPosition() <= 0 && mHistoryDir.getFileCount() <= 1) {
+ if (mHistoryBuffer.dataPosition() <= 0 && !mStore.hasCompletedFragments()) {
return;
}
@@ -1852,7 +1610,7 @@ public class BatteryStatsHistory {
}
final long timeDiffMs = mMonotonicClock.monotonicTime(elapsedRealtimeMs)
- - mHistoryLastWritten.time;
+ - mHistoryLastWritten.time;
final int diffStates = mHistoryLastWritten.states ^ cur.states;
final int diffStates2 = mHistoryLastWritten.states2 ^ cur.states2;
final int lastDiffStates = mHistoryLastWritten.states ^ mHistoryLastLastWritten.states;
@@ -1953,7 +1711,7 @@ public class BatteryStatsHistory {
mMaxHistoryBufferSize = 1024;
}
- boolean successfullyLocked = mHistoryDir.tryLock();
+ boolean successfullyLocked = mStore.tryLock();
if (!successfullyLocked) { // Already locked by another thread
// If the buffer size is below the allowed overflow limit, just keep going
if (dataSize < mMaxHistoryBufferSize + EXTRA_BUFFER_SIZE_WHEN_DIR_LOCKED) {
@@ -1971,10 +1729,10 @@ public class BatteryStatsHistory {
copy.setTo(cur);
try {
- startNextFile(elapsedRealtimeMs);
+ startNextFragment(elapsedRealtimeMs);
} finally {
if (successfullyLocked) {
- mHistoryDir.unlock();
+ mStore.unlock();
}
}
@@ -2095,6 +1853,7 @@ public class BatteryStatsHistory {
Battery charge int: if F in the first token is set, an int representing the battery charge
in coulombs follows.
*/
+
/**
* Writes the delta between the previous and current history items into history buffer.
*/
@@ -2376,9 +2135,13 @@ public class BatteryStatsHistory {
}
/**
- * Saves the accumulated history buffer in the active file, see {@link #getActiveFile()} .
+ * Saves the accumulated history buffer in the active file, see {@link #getActiveFragment()} .
*/
public void writeHistory() {
+ writeHistory(false /* fragmentComplete */);
+ }
+
+ private void writeHistory(boolean fragmentComplete) {
synchronized (this) {
if (isReadOnly()) {
Slog.w(TAG, "writeHistory: this instance instance is read-only");
@@ -2397,7 +2160,7 @@ public class BatteryStatsHistory {
Slog.d(TAG, "writeHistoryBuffer duration ms:"
+ (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize());
}
- writeParcelToFileLocked(p, mActiveFile);
+ writeParcelLocked(p, mActiveFragment, fragmentComplete);
} finally {
p.recycle();
}
@@ -2457,30 +2220,18 @@ public class BatteryStatsHistory {
}
@GuardedBy("this")
- private void writeParcelToFileLocked(Parcel p, AtomicFile file) {
- FileOutputStream fos = null;
+ private void writeParcelLocked(Parcel p, BatteryHistoryFragment fragment,
+ boolean fragmentComplete) {
mWriteLock.lock();
try {
final long startTimeMs = SystemClock.uptimeMillis();
- fos = file.startWrite();
- fos.write(p.marshall());
- fos.flush();
- file.finishWrite(fos);
- if (DEBUG) {
- Slog.d(TAG, "writeParcelToFileLocked file:" + file.getBaseFile().getPath()
- + " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs)
- + " bytes:" + p.dataSize());
- }
+ mStore.writeFragment(fragment, p.marshall(), fragmentComplete);
mEventLogger.writeCommitSysConfigFile(startTimeMs);
- } catch (IOException e) {
- Slog.w(TAG, "Error writing battery statistics", e);
- file.failWrite(fos);
} finally {
mWriteLock.unlock();
}
}
-
/**
* Returns the total number of history tags in the tag pool.
*/
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 90f8a0714a04..d73e2d47348b 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -436,6 +436,9 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
if (viewRoot != null) {
// Clear the old callbacks and attach to the new window.
viewRoot.getOnBackInvokedDispatcher().clear();
+ if (Flags.clearSystemVibrator()) {
+ viewRoot.clearSystemVibrator();
+ }
onViewRootImplSet(viewRoot);
}
}
diff --git a/core/jni/android_view_SurfaceControlActivePictureListener.cpp b/core/jni/android_view_SurfaceControlActivePictureListener.cpp
index 15132db2a569..ee8efe19e15e 100644
--- a/core/jni/android_view_SurfaceControlActivePictureListener.cpp
+++ b/core/jni/android_view_SurfaceControlActivePictureListener.cpp
@@ -106,11 +106,13 @@ struct SurfaceControlActivePictureListener : public gui::BnActivePictureListener
}
status_t startListening() {
- return SurfaceComposerClient::addActivePictureListener(this);
+ return SurfaceComposerClient::addActivePictureListener(
+ sp<SurfaceControlActivePictureListener>::fromExisting(this));
}
status_t stopListening() {
- return SurfaceComposerClient::removeActivePictureListener(this);
+ return SurfaceComposerClient::removeActivePictureListener(
+ sp<SurfaceControlActivePictureListener>::fromExisting(this));
}
protected:
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 3edc5c108083..aaf84201821f 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1862,9 +1862,38 @@
<enum name="sync" value="2" />
</attr>
- <!-- This attribute will be used to override app compatibility mode on 16 KB devices.
- If set to enabled, Natives lib will be extracted from APK if they are not page aligned on
- 16 KB device. 4 KB natives libs will be loaded app-compat mode if they are eligible.
+ <!-- This attribute overrides the user-set or platform-set 16 KB page size
+ compatibility mode, so that page agnostic compatibility is always enabled
+ or always disabled, rather than according to the user's preference.
+
+ <p>On 4 KB systems, this attribute is ignored and apps are installed
+ normally.
+
+ <p>On 16 KB systems, if an app is built for 16 KB page sizes, this
+ attribute is ignored and apps are installed normally.
+
+ <p>This attribute only affects 16 KB systems for apps that are built
+ with 4 KB page size (old) options.
+
+ <p>When page agnostic compatibility is enabled (either through this
+ flag or via the user's preference), the system specializes the app
+ installation process in ways known to improve compatibility of 4 KB
+ built apps on 16 KB systems. That is, apps which do not have aligned
+ libraries in APK files are extracted, requiring more space on the
+ device. An additional specialization when this option is enabled is
+ that the linker loads the application in a special mode intended
+ to allow 4 KB aligned program segments to load on a 16 KB page system.
+
+ <p>Here are the situations where this attribute should be most useful:
+ <ul>
+ <li>If an app works on 16 KB mode, but is not built for it, enabling this
+ attribute forces the app to be installed in 16 KB mode without
+ the user having to set these options themself.
+ <li>If an app is fully working in 16 KB mode, you can set this
+ attribute to disabled, so that any regression causes a clear failure
+ and this compatibility mode is not used.
+ </ul>
+
@FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) -->
<attr name="pageSizeCompat">
<!-- value for enabled must match with
diff --git a/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_16.xml b/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_16.xml
new file mode 100644
index 000000000000..c2a20b977b70
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_16.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16"
+ android:tint="?android:attr/textColorSecondary">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M 8 11.375 L 2 5.375 L 3.4 3.975 L 8 8.575 L 12.6 3.975 L 14 5.375 L 8 11.375 Z"
+ />
+</vector>
+
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
index 87c520ca1b51..b898e4b06c14 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
@@ -64,7 +64,7 @@
android:id="@+id/expand_menu_button"
android:layout_width="16dp"
android:layout_height="16dp"
- android:src="@drawable/ic_baseline_expand_more_24"
+ android:src="@drawable/ic_baseline_expand_more_16"
android:background="@null"
android:scaleType="fitCenter"
android:clickable="false"
@@ -101,7 +101,7 @@
android:layout_width="44dp"
android:layout_height="40dp"
android:layout_gravity="end"
- android:layout_marginHorizontal="8dp"
+ android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"/>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 404bbd1d0a33..e23d5725e9c3 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -295,6 +295,10 @@
<dimen name="bubble_bar_dismiss_zone_width">192dp</dimen>
<!-- Height of the box around bottom center of the screen where drag only leads to dismiss -->
<dimen name="bubble_bar_dismiss_zone_height">242dp</dimen>
+ <!-- Height of the box at the corner of the screen where drag leads to app moving to bubble -->
+ <dimen name="bubble_transform_area_width">140dp</dimen>
+ <!-- Width of the box at the corner of the screen where drag leads to app moving to bubble -->
+ <dimen name="bubble_transform_area_height">140dp</dimen>
<!-- Bottom and end margin for compat buttons. -->
<dimen name="compat_button_margin">24dp</dimen>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt
new file mode 100644
index 000000000000..9bee11a92430
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared.bubbles
+
+import android.graphics.Rect
+
+/**
+ * Provide bounds for Bubbles drop targets that are shown when dragging over drag zones
+ */
+interface BubbleDropTargetBoundsProvider {
+ /**
+ * Get bubble bar expanded view visual drop target bounds on screen
+ */
+ fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt
new file mode 100644
index 000000000000..5d346c047123
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared.bubbles
+
+import android.graphics.Rect
+
+/**
+ * Represents an invisible area on the screen that determines what happens to a dragged object if it
+ * is released in that area.
+ *
+ * [bounds] are the bounds of the drag zone. Drag zones have an associated drop target that serves
+ * as visual feedback hinting what would happen if the object is released. When a dragged object is
+ * dragged into a drag zone, the associated drop target will be displayed. Not all drag zones have
+ * drop targets; only those that are made visible by Bubbles do.
+ */
+sealed interface DragZone {
+
+ /** The bounds of this drag zone. */
+ val bounds: Rect
+
+ fun contains(x: Int, y: Int) = bounds.contains(x, y)
+
+ /** Represents the bubble drag area on the screen. */
+ sealed class Bubble(override val bounds: Rect) : DragZone {
+ data class Left(override val bounds: Rect, val dropTarget: Rect) : Bubble(bounds)
+ data class Right(override val bounds: Rect, val dropTarget: Rect) : Bubble(bounds)
+ }
+
+ /** Represents dragging to Desktop Window. */
+ data class DesktopWindow(override val bounds: Rect, val dropTarget: Rect) : DragZone
+
+ /** Represents dragging to Full Screen. */
+ data class FullScreen(override val bounds: Rect, val dropTarget: Rect) : DragZone
+
+ /** Represents dragging to dismiss. */
+ data class Dismiss(override val bounds: Rect) : DragZone
+
+ /** Represents dragging to enter Split or replace a Split app. */
+ sealed class Split(override val bounds: Rect) : DragZone {
+ data class Left(override val bounds: Rect) : Split(bounds)
+ data class Right(override val bounds: Rect) : Split(bounds)
+ data class Top(override val bounds: Rect) : Split(bounds)
+ data class Bottom(override val bounds: Rect) : Split(bounds)
+ }
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
new file mode 100644
index 000000000000..c2eef33881be
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared.bubbles
+
+import android.graphics.Rect
+import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
+
+/** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */
+class DragZoneFactory(
+ private val deviceConfig: DeviceConfig,
+ private val splitScreenModeChecker: SplitScreenModeChecker,
+ private val desktopWindowModeChecker: DesktopWindowModeChecker,
+) {
+
+ private val windowBounds: Rect
+ get() = deviceConfig.windowBounds
+
+ // TODO b/393172431: move these to xml
+ private val dismissDragZoneSize = if (deviceConfig.isSmallTablet) 140 else 200
+ private val bubbleDragZoneTabletSize = 200
+ private val bubbleDragZoneFoldableSize = 140
+ private val fullScreenDragZoneWidth = 512
+ private val fullScreenDragZoneHeight = 44
+ private val desktopWindowDragZoneWidth = 880
+ private val desktopWindowDragZoneHeight = 300
+ private val desktopWindowFromExpandedViewDragZoneWidth = 200
+ private val desktopWindowFromExpandedViewDragZoneHeight = 350
+ private val splitFromBubbleDragZoneHeight = 100
+ private val splitFromBubbleDragZoneWidth = 60
+ private val hSplitFromExpandedViewDragZoneWidth = 60
+ private val vSplitFromExpandedViewDragZoneWidth = 200
+ private val vSplitFromExpandedViewDragZoneHeightTablet = 285
+ private val vSplitFromExpandedViewDragZoneHeightFold = 150
+ private val vUnevenSplitFromExpandedViewDragZoneHeight = 96
+
+ /**
+ * Creates the list of drag zones for the dragged object.
+ *
+ * Drag zones may have overlap, but the list is sorted by priority where the first drag zone has
+ * the highest priority so it should be checked first.
+ */
+ fun createSortedDragZones(draggedObject: DraggedObject): List<DragZone> {
+ val dragZones = mutableListOf<DragZone>()
+ when (draggedObject) {
+ is DraggedObject.BubbleBar -> {
+ dragZones.add(createDismissDragZone())
+ dragZones.addAll(createBubbleDragZones())
+ }
+ is DraggedObject.Bubble -> {
+ dragZones.add(createDismissDragZone())
+ dragZones.addAll(createBubbleDragZones())
+ dragZones.add(createFullScreenDragZone())
+ if (shouldShowDesktopWindowDragZones()) {
+ dragZones.add(createDesktopWindowDragZoneForBubble())
+ }
+ dragZones.addAll(createSplitScreenDragZonesForBubble())
+ }
+ is DraggedObject.ExpandedView -> {
+ dragZones.add(createDismissDragZone())
+ dragZones.add(createFullScreenDragZone())
+ if (shouldShowDesktopWindowDragZones()) {
+ dragZones.add(createDesktopWindowDragZoneForExpandedView())
+ }
+ if (deviceConfig.isSmallTablet) {
+ dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnFoldable())
+ } else {
+ dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet())
+ }
+ createBubbleDragZonesForExpandedView()
+ }
+ }
+ return dragZones
+ }
+
+ private fun createDismissDragZone(): DragZone {
+ return DragZone.Dismiss(
+ bounds =
+ Rect(
+ windowBounds.right / 2 - dismissDragZoneSize / 2,
+ windowBounds.bottom - dismissDragZoneSize,
+ windowBounds.right / 2 + dismissDragZoneSize / 2,
+ windowBounds.bottom
+ )
+ )
+ }
+
+ private fun createBubbleDragZones(): List<DragZone> {
+ val dragZoneSize =
+ if (deviceConfig.isSmallTablet) {
+ bubbleDragZoneFoldableSize
+ } else {
+ bubbleDragZoneTabletSize
+ }
+ return listOf(
+ DragZone.Bubble.Left(
+ bounds =
+ Rect(0, windowBounds.bottom - dragZoneSize, dragZoneSize, windowBounds.bottom),
+ dropTarget = Rect(0, 0, 0, 0),
+ ),
+ DragZone.Bubble.Right(
+ bounds =
+ Rect(
+ windowBounds.right - dragZoneSize,
+ windowBounds.bottom - dragZoneSize,
+ windowBounds.right,
+ windowBounds.bottom,
+ ),
+ dropTarget = Rect(0, 0, 0, 0),
+ )
+ )
+ }
+
+ private fun createBubbleDragZonesForExpandedView(): List<DragZone> {
+ return listOf(
+ DragZone.Bubble.Left(
+ bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
+ dropTarget = Rect(0, 0, 0, 0),
+ ),
+ DragZone.Bubble.Right(
+ bounds =
+ Rect(
+ windowBounds.right / 2,
+ 0,
+ windowBounds.right,
+ windowBounds.bottom,
+ ),
+ dropTarget = Rect(0, 0, 0, 0),
+ )
+ )
+ }
+
+ private fun createFullScreenDragZone(): DragZone {
+ return DragZone.FullScreen(
+ bounds =
+ Rect(
+ windowBounds.right / 2 - fullScreenDragZoneWidth / 2,
+ 0,
+ windowBounds.right / 2 + fullScreenDragZoneWidth / 2,
+ fullScreenDragZoneHeight
+ ),
+ dropTarget = Rect(0, 0, 0, 0)
+ )
+ }
+
+ private fun shouldShowDesktopWindowDragZones() =
+ !deviceConfig.isSmallTablet && desktopWindowModeChecker.isSupported()
+
+ private fun createDesktopWindowDragZoneForBubble(): DragZone {
+ return DragZone.DesktopWindow(
+ bounds =
+ if (deviceConfig.isLandscape) {
+ Rect(
+ windowBounds.right / 2 - desktopWindowDragZoneWidth / 2,
+ windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2,
+ windowBounds.right / 2 + desktopWindowDragZoneWidth / 2,
+ windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
+ )
+ } else {
+ Rect(
+ 0,
+ windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2,
+ windowBounds.right,
+ windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2
+ )
+ },
+ dropTarget = Rect(0, 0, 0, 0)
+ )
+ }
+
+ private fun createDesktopWindowDragZoneForExpandedView(): DragZone {
+ return DragZone.DesktopWindow(
+ bounds =
+ Rect(
+ windowBounds.right / 2 - desktopWindowFromExpandedViewDragZoneWidth / 2,
+ windowBounds.bottom / 2 - desktopWindowFromExpandedViewDragZoneHeight / 2,
+ windowBounds.right / 2 + desktopWindowFromExpandedViewDragZoneWidth / 2,
+ windowBounds.bottom / 2 + desktopWindowFromExpandedViewDragZoneHeight / 2
+ ),
+ dropTarget = Rect(0, 0, 0, 0)
+ )
+ }
+
+ private fun createSplitScreenDragZonesForBubble(): List<DragZone> {
+ // for foldables in landscape mode or tables in portrait modes we have vertical split drag
+ // zones. otherwise we have horizontal split drag zones.
+ val isVerticalSplit = deviceConfig.isSmallTablet == deviceConfig.isLandscape
+ return if (isVerticalSplit) {
+ when (splitScreenModeChecker.getSplitScreenMode()) {
+ SplitScreenMode.SPLIT_50_50,
+ SplitScreenMode.NONE ->
+ listOf(
+ DragZone.Split.Top(
+ bounds = Rect(0, 0, windowBounds.right, windowBounds.bottom / 2),
+ ),
+ DragZone.Split.Bottom(
+ bounds =
+ Rect(
+ 0,
+ windowBounds.bottom / 2,
+ windowBounds.right,
+ windowBounds.bottom
+ ),
+ )
+ )
+ SplitScreenMode.SPLIT_90_10 -> {
+ listOf(
+ DragZone.Split.Top(
+ bounds =
+ Rect(
+ 0,
+ 0,
+ windowBounds.right,
+ windowBounds.bottom - splitFromBubbleDragZoneHeight
+ ),
+ ),
+ DragZone.Split.Bottom(
+ bounds =
+ Rect(
+ 0,
+ windowBounds.bottom - splitFromBubbleDragZoneHeight,
+ windowBounds.right,
+ windowBounds.bottom
+ ),
+ )
+ )
+ }
+ SplitScreenMode.SPLIT_10_90 -> {
+ listOf(
+ DragZone.Split.Top(
+ bounds = Rect(0, 0, windowBounds.right, splitFromBubbleDragZoneHeight),
+ ),
+ DragZone.Split.Bottom(
+ bounds =
+ Rect(
+ 0,
+ splitFromBubbleDragZoneHeight,
+ windowBounds.right,
+ windowBounds.bottom
+ ),
+ )
+ )
+ }
+ }
+ } else {
+ when (splitScreenModeChecker.getSplitScreenMode()) {
+ SplitScreenMode.SPLIT_50_50,
+ SplitScreenMode.NONE ->
+ listOf(
+ DragZone.Split.Left(
+ bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
+ ),
+ DragZone.Split.Right(
+ bounds =
+ Rect(
+ windowBounds.right / 2,
+ 0,
+ windowBounds.right,
+ windowBounds.bottom
+ ),
+ )
+ )
+ SplitScreenMode.SPLIT_90_10 ->
+ listOf(
+ DragZone.Split.Left(
+ bounds =
+ Rect(
+ 0,
+ 0,
+ windowBounds.right - splitFromBubbleDragZoneWidth,
+ windowBounds.bottom
+ ),
+ ),
+ DragZone.Split.Right(
+ bounds =
+ Rect(
+ windowBounds.right - splitFromBubbleDragZoneWidth,
+ 0,
+ windowBounds.right,
+ windowBounds.bottom
+ ),
+ )
+ )
+ SplitScreenMode.SPLIT_10_90 ->
+ listOf(
+ DragZone.Split.Left(
+ bounds = Rect(0, 0, splitFromBubbleDragZoneWidth, windowBounds.bottom),
+ ),
+ DragZone.Split.Right(
+ bounds =
+ Rect(
+ splitFromBubbleDragZoneWidth,
+ 0,
+ windowBounds.right,
+ windowBounds.bottom
+ ),
+ )
+ )
+ }
+ }
+ }
+
+ private fun createSplitScreenDragZonesForExpandedViewOnTablet(): List<DragZone> {
+ return if (deviceConfig.isLandscape) {
+ createHorizontalSplitDragZonesForExpandedView()
+ } else {
+ // for tablets in portrait mode, split drag zones appear below the full screen drag zone
+ // for the top split zone, and above the dismiss zone. Both are horizontally centered.
+ val splitZoneLeft = windowBounds.right / 2 - vSplitFromExpandedViewDragZoneWidth / 2
+ val splitZoneRight = splitZoneLeft + vSplitFromExpandedViewDragZoneWidth
+ val bottomSplitZoneBottom = windowBounds.bottom - dismissDragZoneSize
+ listOf(
+ DragZone.Split.Top(
+ bounds =
+ Rect(
+ splitZoneLeft,
+ fullScreenDragZoneHeight,
+ splitZoneRight,
+ fullScreenDragZoneHeight + vSplitFromExpandedViewDragZoneHeightTablet
+ ),
+ ),
+ DragZone.Split.Bottom(
+ bounds =
+ Rect(
+ splitZoneLeft,
+ bottomSplitZoneBottom - vSplitFromExpandedViewDragZoneHeightTablet,
+ splitZoneRight,
+ bottomSplitZoneBottom
+ ),
+ )
+ )
+ }
+ }
+
+ private fun createSplitScreenDragZonesForExpandedViewOnFoldable(): List<DragZone> {
+ return if (deviceConfig.isLandscape) {
+ // vertical split drag zones are aligned with the full screen drag zone width
+ val splitZoneLeft = windowBounds.right / 2 - fullScreenDragZoneWidth / 2
+ when (splitScreenModeChecker.getSplitScreenMode()) {
+ SplitScreenMode.SPLIT_50_50,
+ SplitScreenMode.NONE ->
+ listOf(
+ DragZone.Split.Top(
+ bounds =
+ Rect(
+ splitZoneLeft,
+ fullScreenDragZoneHeight,
+ splitZoneLeft + fullScreenDragZoneWidth,
+ fullScreenDragZoneHeight +
+ vSplitFromExpandedViewDragZoneHeightFold
+ ),
+ ),
+ DragZone.Split.Bottom(
+ bounds =
+ Rect(
+ splitZoneLeft,
+ windowBounds.bottom / 2,
+ splitZoneLeft + fullScreenDragZoneWidth,
+ windowBounds.bottom / 2 +
+ vSplitFromExpandedViewDragZoneHeightFold
+ ),
+ )
+ )
+ // TODO b/393172431: add this zone when it's defined
+ SplitScreenMode.SPLIT_10_90 -> listOf()
+ SplitScreenMode.SPLIT_90_10 ->
+ listOf(
+ DragZone.Split.Top(
+ bounds =
+ Rect(
+ splitZoneLeft,
+ fullScreenDragZoneHeight,
+ splitZoneLeft + fullScreenDragZoneWidth,
+ fullScreenDragZoneHeight +
+ vUnevenSplitFromExpandedViewDragZoneHeight
+ ),
+ ),
+ DragZone.Split.Bottom(
+ bounds =
+ Rect(
+ 0,
+ windowBounds.bottom -
+ vUnevenSplitFromExpandedViewDragZoneHeight,
+ windowBounds.right,
+ windowBounds.bottom
+ ),
+ )
+ )
+ }
+ } else {
+ // horizontal split drag zones
+ createHorizontalSplitDragZonesForExpandedView()
+ }
+ }
+
+ private fun createHorizontalSplitDragZonesForExpandedView(): List<DragZone> {
+ // horizontal split drag zones for expanded view appear on the edges of the screen from the
+ // top down until the dismiss drag zone height
+ return listOf(
+ DragZone.Split.Left(
+ bounds =
+ Rect(
+ 0,
+ 0,
+ hSplitFromExpandedViewDragZoneWidth,
+ windowBounds.bottom - dismissDragZoneSize
+ ),
+ ),
+ DragZone.Split.Right(
+ bounds =
+ Rect(
+ windowBounds.right - hSplitFromExpandedViewDragZoneWidth,
+ 0,
+ windowBounds.right,
+ windowBounds.bottom - dismissDragZoneSize
+ ),
+ )
+ )
+ }
+
+ /** Checks the current split screen mode. */
+ fun interface SplitScreenModeChecker {
+ enum class SplitScreenMode {
+ NONE,
+ SPLIT_50_50,
+ SPLIT_10_90,
+ SPLIT_90_10
+ }
+
+ fun getSplitScreenMode(): SplitScreenMode
+ }
+
+ /** Checks if desktop window mode is supported. */
+ fun interface DesktopWindowModeChecker {
+ fun isSupported(): Boolean
+ }
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt
new file mode 100644
index 000000000000..028622798f34
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared.bubbles
+
+/** A Bubble object being dragged. */
+sealed interface DraggedObject {
+ /** The initial location of the object at the start of the drag gesture. */
+ val initialLocation: BubbleBarLocation
+
+ data class Bubble(override val initialLocation: BubbleBarLocation) : DraggedObject
+ data class BubbleBar(override val initialLocation: BubbleBarLocation) : DraggedObject
+ data class ExpandedView(override val initialLocation: BubbleBarLocation) : DraggedObject
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
index 62ca5c687a2a..b1bc6e81e1bd 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
@@ -28,6 +28,7 @@ import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.hardware.HardwareBuffer;
import android.util.TypedValue;
import android.view.SurfaceControl;
import android.window.TaskSnapshot;
@@ -225,12 +226,17 @@ public abstract class PipContentOverlay {
@Override
public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
+ final HardwareBuffer buffer = mBitmap.getHardwareBuffer();
tx.show(mLeash);
tx.setLayer(mLeash, Integer.MAX_VALUE);
- tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
+ tx.setBuffer(mLeash, buffer);
tx.setAlpha(mLeash, 0f);
tx.reparent(mLeash, parentLeash);
tx.apply();
+ // Cleanup the bitmap and buffer after setting up the leash
+ mBitmap.recycle();
+ mBitmap = null;
+ buffer.close();
}
@Override
@@ -253,14 +259,6 @@ public abstract class PipContentOverlay {
.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
}
- @Override
- public void detach(SurfaceControl.Transaction tx) {
- super.detach(tx);
- if (mBitmap != null && !mBitmap.isRecycled()) {
- mBitmap.recycle();
- }
- }
-
private void prepareAppIconOverlay(Drawable appIcon) {
final Canvas canvas = new Canvas();
canvas.setBitmap(mBitmap);
@@ -282,7 +280,9 @@ public abstract class PipContentOverlay {
mOverlayHalfSize + mAppIconSizePx / 2);
appIcon.setBounds(appIconBounds);
appIcon.draw(canvas);
+ Bitmap oldBitmap = mBitmap;
mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
+ oldBitmap.recycle();
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 947dbd276d3a..d77c177437b8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -192,10 +192,10 @@ public class Bubble implements BubbleViewProvider {
* that bubble being added back to the stack anyways.
*/
@Nullable
- private PendingIntent mIntent;
- private boolean mIntentActive;
+ private PendingIntent mPendingIntent;
+ private boolean mPendingIntentActive;
@Nullable
- private PendingIntent.CancelListener mIntentCancelListener;
+ private PendingIntent.CancelListener mPendingIntentCancelListener;
/**
* Sent when the bubble & notification are no longer visible to the user (i.e. no
@@ -205,12 +205,10 @@ public class Bubble implements BubbleViewProvider {
private PendingIntent mDeleteIntent;
/**
- * Used only for a special bubble in the stack that has {@link #mIsAppBubble} set to true.
- * There can only be one of these bubbles in the stack and this intent will be populated for
- * that bubble.
+ * Used for app & note bubbles.
*/
@Nullable
- private Intent mAppIntent;
+ private Intent mIntent;
/**
* Set while preparing a transition for animation. Several steps are needed before animation
@@ -275,7 +273,7 @@ public class Bubble implements BubbleViewProvider {
mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
mTaskId = INVALID_TASK_ID;
- mAppIntent = intent;
+ mIntent = intent;
mDesiredHeight = Integer.MAX_VALUE;
mPackageName = intent.getPackage();
}
@@ -294,7 +292,7 @@ public class Bubble implements BubbleViewProvider {
mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
mTaskId = INVALID_TASK_ID;
- mAppIntent = null;
+ mIntent = null;
mDesiredHeight = Integer.MAX_VALUE;
mPackageName = info.getPackage();
mShortcutInfo = info;
@@ -319,7 +317,7 @@ public class Bubble implements BubbleViewProvider {
mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
mTaskId = task.taskId;
- mAppIntent = null;
+ mIntent = null;
mDesiredHeight = Integer.MAX_VALUE;
mPackageName = task.baseActivity.getPackageName();
}
@@ -413,9 +411,9 @@ public class Bubble implements BubbleViewProvider {
mGroupKey = entry.getGroupKey();
mLocusId = entry.getLocusId();
mBubbleMetadataFlagListener = listener;
- mIntentCancelListener = intent -> {
- if (mIntent != null) {
- mIntent.unregisterCancelListener(mIntentCancelListener);
+ mPendingIntentCancelListener = intent -> {
+ if (mPendingIntent != null) {
+ mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener);
}
mainExecutor.execute(() -> {
intentCancelListener.onPendingIntentCanceled(this);
@@ -601,10 +599,10 @@ public class Bubble implements BubbleViewProvider {
if (cleanupTaskView) {
cleanupTaskView();
}
- if (mIntent != null) {
- mIntent.unregisterCancelListener(mIntentCancelListener);
+ if (mPendingIntent != null) {
+ mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener);
}
- mIntentActive = false;
+ mPendingIntentActive = false;
}
/** Cleans-up the taskview associated with this bubble (possibly removing the task from wm) */
@@ -874,19 +872,19 @@ public class Bubble implements BubbleViewProvider {
mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId();
mIcon = entry.getBubbleMetadata().getIcon();
- if (!mIntentActive || mIntent == null) {
- if (mIntent != null) {
- mIntent.unregisterCancelListener(mIntentCancelListener);
+ if (!mPendingIntentActive || mPendingIntent == null) {
+ if (mPendingIntent != null) {
+ mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener);
}
- mIntent = entry.getBubbleMetadata().getIntent();
- if (mIntent != null) {
- mIntent.registerCancelListener(mIntentCancelListener);
+ mPendingIntent = entry.getBubbleMetadata().getIntent();
+ if (mPendingIntent != null) {
+ mPendingIntent.registerCancelListener(mPendingIntentCancelListener);
}
- } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) {
+ } else if (mPendingIntent != null && entry.getBubbleMetadata().getIntent() == null) {
// Was an intent bubble now it's a shortcut bubble... still unregister the listener
- mIntent.unregisterCancelListener(mIntentCancelListener);
- mIntentActive = false;
- mIntent = null;
+ mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener);
+ mPendingIntentActive = false;
+ mPendingIntent = null;
}
mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent();
}
@@ -926,12 +924,15 @@ public class Bubble implements BubbleViewProvider {
* Sets if the intent used for this bubble is currently active (i.e. populating an
* expanded view, expanded or not).
*/
- void setIntentActive() {
- mIntentActive = true;
+ void setPendingIntentActive() {
+ mPendingIntentActive = true;
}
- boolean isIntentActive() {
- return mIntentActive;
+ /**
+ * Whether the pending intent of this bubble is active (i.e. has been sent).
+ */
+ boolean isPendingIntentActive() {
+ return mPendingIntentActive;
}
public InstanceId getInstanceId() {
@@ -1118,9 +1119,12 @@ public class Bubble implements BubbleViewProvider {
}
}
+ /**
+ * Returns the pending intent used to populate the bubble.
+ */
@Nullable
- PendingIntent getBubbleIntent() {
- return mIntent;
+ PendingIntent getPendingIntent() {
+ return mPendingIntent;
}
/**
@@ -1128,31 +1132,33 @@ public class Bubble implements BubbleViewProvider {
* intent for an app. In this case we don't show a badge on the icon.
*/
public boolean isAppLaunchIntent() {
- if (BubbleAnythingFlagHelper.enableCreateAnyBubble() && mAppIntent != null) {
- return mAppIntent.hasCategory("android.intent.category.LAUNCHER");
+ if (BubbleAnythingFlagHelper.enableCreateAnyBubble() && mIntent != null) {
+ return mIntent.hasCategory("android.intent.category.LAUNCHER");
}
return false;
}
+ /**
+ * Returns the pending intent to send when a bubble is dismissed (set via the notification API).
+ */
@Nullable
PendingIntent getDeleteIntent() {
return mDeleteIntent;
}
+ /**
+ * Returns the intent used to populate the bubble.
+ */
@Nullable
- @VisibleForTesting
- public Intent getAppBubbleIntent() {
- return mAppIntent;
+ public Intent getIntent() {
+ return mIntent;
}
/**
- * Sets the intent for a bubble that is an app bubble (one for which {@link #mIsAppBubble} is
- * true).
- *
- * @param appIntent The intent to set for the app bubble.
+ * Sets the intent used to populate the bubble.
*/
- void setAppBubbleIntent(Intent appIntent) {
- mAppIntent = appIntent;
+ void setIntent(Intent intent) {
+ mIntent = intent;
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 9120e0894ccf..2c81945ffdbe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -116,6 +116,7 @@ import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
+import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider;
import com.android.wm.shell.shared.bubbles.DeviceConfig;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -419,10 +420,11 @@ public class BubbleController implements ConfigurationChangeListener,
mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
mBubbleData.setPendingIntentCancelledListener(bubble -> {
- if (bubble.getBubbleIntent() == null) {
+ if (bubble.getPendingIntent() == null) {
return;
}
- if (bubble.isIntentActive() || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
+ if (bubble.isPendingIntentActive()
+ || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
bubble.setPendingIntentCanceled();
return;
}
@@ -924,6 +926,11 @@ public class BubbleController implements ConfigurationChangeListener,
return mBubblePositioner;
}
+ /** Provides bounds for drag zone drop targets */
+ public BubbleDropTargetBoundsProvider getBubbleDropTargetBoundsProvider() {
+ return mBubblePositioner;
+ }
+
BubbleIconFactory getIconFactory() {
return mBubbleIconFactory;
}
@@ -1663,7 +1670,7 @@ public class BubbleController implements ConfigurationChangeListener,
// It's in the overflow, so remove it & reinflate
mBubbleData.dismissBubbleWithKey(noteBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL);
// Update the bubble entry in the overflow with the latest intent.
- b.setAppBubbleIntent(intent);
+ b.setIntent(intent);
} else {
// Notes bubble does not exist, lets add and expand it
b = Bubble.createNotesBubble(intent, user, icon, mMainExecutor,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index ac74a42d1359..ad9ab7a722ee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -237,8 +237,7 @@ public class BubbleExpandedView extends LinearLayout {
PendingIntent pi = PendingIntent.getActivity(
context,
/* requestCode= */ 0,
- mBubble.getAppBubbleIntent()
- .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
+ mBubble.getIntent().addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
/* options= */ null);
mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
@@ -252,7 +251,7 @@ public class BubbleExpandedView extends LinearLayout {
} else {
options.setLaunchedFromBubble(true);
if (mBubble != null) {
- mBubble.setIntentActive();
+ mBubble.setPendingIntentActive();
}
final Intent fillInIntent = new Intent();
// Apply flags to make behaviour match documentLaunchMode=always.
@@ -920,7 +919,7 @@ public class BubbleExpandedView extends LinearLayout {
});
if (isNew) {
- mPendingIntent = mBubble.getBubbleIntent();
+ mPendingIntent = mBubble.getPendingIntent();
if ((mPendingIntent != null || mBubble.hasMetadataShortcutId())
&& mTaskView != null) {
setContentVisibility(false);
@@ -947,7 +946,7 @@ public class BubbleExpandedView extends LinearLayout {
*/
private boolean didBackingContentChange(Bubble newBubble) {
boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
- boolean newIsIntentBased = newBubble.getBubbleIntent() != null;
+ boolean newIsIntentBased = newBubble.getPendingIntent() != null;
return prevWasIntentBased != newIsIntentBased;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 8cf3f7afd46a..5273a7cf2432 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -27,19 +27,21 @@ import android.graphics.RectF;
import android.view.Surface;
import android.view.WindowManager;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.R;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider;
import com.android.wm.shell.shared.bubbles.DeviceConfig;
/**
* Keeps track of display size, configuration, and specific bubble sizes. One place for all
* placement and positioning calculations to refer to.
*/
-public class BubblePositioner {
+public class BubblePositioner implements BubbleDropTargetBoundsProvider {
/** The screen edge the bubble stack is pinned to */
public enum StackPinnedEdge {
@@ -100,6 +102,7 @@ public class BubblePositioner {
private int mManageButtonHeight;
private int mOverflowHeight;
private int mMinimumFlyoutWidthLargeScreen;
+ private int mBubbleBarExpandedViewDropTargetPadding;
private PointF mRestingStackPosition;
@@ -164,6 +167,8 @@ public class BubblePositioner {
res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width),
mPositionRect.width() - 2 * mExpandedViewPadding
);
+ mBubbleBarExpandedViewDropTargetPadding = res.getDimensionPixelSize(
+ R.dimen.bubble_bar_expanded_view_drop_target_padding);
if (mShowingInBubbleBar) {
mExpandedViewLargeScreenWidth = mExpandedViewBubbleBarWidth;
@@ -965,4 +970,14 @@ public class BubblePositioner {
int top = getExpandedViewBottomForBubbleBar() - height;
out.offsetTo(left, top);
}
+
+ @NonNull
+ @Override
+ public Rect getBubbleBarExpandedViewDropTargetBounds(boolean onLeft) {
+ Rect bounds = new Rect();
+ getBubbleBarExpandedViewBounds(onLeft, false, bounds);
+ bounds.inset(mBubbleBarExpandedViewDropTargetPadding,
+ mBubbleBarExpandedViewDropTargetPadding);
+ return bounds;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 83d311ed6cd9..0d89bb260bf5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -119,7 +119,7 @@ public class BubbleTaskViewHelper {
PendingIntent pi = PendingIntent.getActivity(
context,
/* requestCode= */ 0,
- mBubble.getAppBubbleIntent()
+ mBubble.getIntent()
.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
/* options= */ null);
@@ -133,7 +133,7 @@ public class BubbleTaskViewHelper {
} else {
options.setLaunchedFromBubble(true);
if (mBubble != null) {
- mBubble.setIntentActive();
+ mBubble.setPendingIntentActive();
}
final Intent fillInIntent = new Intent();
// Apply flags to make behaviour match documentLaunchMode=always.
@@ -231,7 +231,7 @@ public class BubbleTaskViewHelper {
boolean isNew = mBubble == null || didBackingContentChange(bubble);
mBubble = bubble;
if (isNew) {
- mPendingIntent = mBubble.getBubbleIntent();
+ mPendingIntent = mBubble.getPendingIntent();
return true;
}
return false;
@@ -276,7 +276,7 @@ public class BubbleTaskViewHelper {
*/
private boolean didBackingContentChange(Bubble newBubble) {
boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
- boolean newIsIntentBased = newBubble.getBubbleIntent() != null;
+ boolean newIsIntentBased = newBubble.getPendingIntent() != null;
return prevWasIntentBased != newIsIntentBased;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/InputChannelSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InputChannelSupplier.kt
new file mode 100644
index 000000000000..41382047945b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InputChannelSupplier.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.view.InputChannel
+import com.android.wm.shell.dagger.WMSingleton
+import java.util.function.Supplier
+import javax.inject.Inject
+
+/**
+ * An Injectable [Supplier<InputChannel>]. This can be used in place of kotlin default
+ * parameters values [builder = ::InputChannel] which requires the [@JvmOverloads] annotation to
+ * make this available in Java.
+ * This can be used every time a component needs the dependency to the default [Supplier] for
+ * [InputChannel]s.
+ */
+@WMSingleton
+class InputChannelSupplier @Inject constructor() : Supplier<InputChannel> {
+ override fun get(): InputChannel = InputChannel()
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowSessionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowSessionSupplier.kt
new file mode 100644
index 000000000000..2c66e97f03e1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowSessionSupplier.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.view.IWindowSession
+import android.view.WindowManagerGlobal
+import com.android.wm.shell.dagger.WMSingleton
+import java.util.function.Supplier
+import javax.inject.Inject
+
+/**
+ * An Injectable [Supplier<IWindowSession>]. This can be used in place of kotlin default
+ * parameters values [builder = WindowManagerGlobal::getWindowSession] which requires the
+ * [@JvmOverloads] annotation to make this available in Java.
+ * This can be used every time a component needs the dependency to the default [Supplier] for
+ * [IWindowSession]s.
+ */
+@WMSingleton
+class WindowSessionSupplier @Inject constructor() : Supplier<IWindowSession> {
+ override fun get(): IWindowSession = WindowManagerGlobal.getWindowSession()
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplier.kt
new file mode 100644
index 000000000000..0b6c06ac5649
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplier.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.transition
+
+import android.view.SurfaceControl
+import com.android.wm.shell.dagger.WMSingleton
+import java.util.function.Supplier
+import javax.inject.Inject
+
+/**
+ * An Injectable [Supplier<SurfaceControl.Builder>]. This can be used in place of kotlin default
+ * parameters values [builder = ::SurfaceControl.Builder] which requires the [@JvmOverloads]
+ * annotation to make this available in Java.
+ * This can be used every time a component needs the dependency to the default builder for
+ * [SurfaceControl]s.
+ */
+@WMSingleton
+class SurfaceBuilderSupplier @Inject constructor() : Supplier<SurfaceControl.Builder> {
+ override fun get(): SurfaceControl.Builder = SurfaceControl.Builder()
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/TransactionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/TransactionSupplier.kt
new file mode 100644
index 000000000000..2d9899b4fccf
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/TransactionSupplier.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.transition
+
+import android.view.SurfaceControl
+import com.android.wm.shell.dagger.WMSingleton
+import java.util.function.Supplier
+import javax.inject.Inject
+
+/**
+ * An Injectable [Supplier<SurfaceControl.Transaction>]. This can be used in place of kotlin default
+ * parameters values [builder = ::SurfaceControl.Transaction] which requires the [@JvmOverloads]
+ * annotation to make this available in Java.
+ * This can be used every time a component needs the dependency to the default builder for
+ * [SurfaceControl.Transaction]s.
+ */
+@WMSingleton
+class TransactionSupplier @Inject constructor() : Supplier<SurfaceControl.Transaction> {
+ override fun get(): SurfaceControl.Transaction = SurfaceControl.Transaction()
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureListener.kt
new file mode 100644
index 000000000000..f7afbb5bdaef
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureListener.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.letterbox
+
+import android.view.GestureDetector.OnContextClickListener
+import android.view.GestureDetector.OnDoubleTapListener
+import android.view.GestureDetector.OnGestureListener
+import android.view.MotionEvent
+
+/**
+ * Interface which unions all the interfaces related to gestures.
+ */
+interface LetterboxGestureListener : OnGestureListener, OnDoubleTapListener, OnContextClickListener
+
+/**
+ * Convenience class which provide an overrideable implementation of
+ * {@link LetterboxGestureListener}.
+ */
+object LetterboxGestureDelegate : LetterboxGestureListener {
+ override fun onDown(e: MotionEvent): Boolean = false
+
+ override fun onShowPress(e: MotionEvent) {
+ }
+
+ override fun onSingleTapUp(e: MotionEvent): Boolean = false
+
+ override fun onScroll(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean = false
+
+ override fun onLongPress(e: MotionEvent) {
+ }
+
+ override fun onFling(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean = false
+
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean = false
+
+ override fun onDoubleTap(e: MotionEvent): Boolean = false
+
+ override fun onDoubleTapEvent(e: MotionEvent): Boolean = false
+
+ override fun onContextClick(e: MotionEvent): Boolean = false
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputController.kt
new file mode 100644
index 000000000000..afd8e1519d24
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputController.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.letterbox
+
+import android.content.Context
+import android.graphics.Rect
+import android.graphics.Region
+import android.os.Handler
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.common.InputChannelSupplier
+import com.android.wm.shell.common.WindowSessionSupplier
+import com.android.wm.shell.compatui.letterbox.LetterboxUtils.Maps.runOnItem
+import com.android.wm.shell.dagger.WMSingleton
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_APP_COMPAT
+import java.util.function.Supplier
+import javax.inject.Inject
+
+/**
+ * [LetterboxController] implementation responsible for handling the spy [SurfaceControl] we use
+ * to detect letterbox events.
+ */
+@WMSingleton
+class LetterboxInputController @Inject constructor(
+ private val context: Context,
+ private val handler: Handler,
+ private val inputSurfaceBuilder: LetterboxInputSurfaceBuilder,
+ private val listenerSupplier: Supplier<LetterboxGestureListener>,
+ private val windowSessionSupplier: WindowSessionSupplier,
+ private val inputChannelSupplier: InputChannelSupplier
+) : LetterboxController {
+
+ companion object {
+ @JvmStatic
+ private val TAG = "LetterboxInputController"
+ }
+
+ private val inputDetectorMap = mutableMapOf<LetterboxKey, LetterboxInputDetector>()
+
+ override fun createLetterboxSurface(
+ key: LetterboxKey,
+ transaction: Transaction,
+ parentLeash: SurfaceControl
+ ) {
+ inputDetectorMap.runOnItem(key, onMissed = { k, m ->
+ m[k] =
+ LetterboxInputDetector(
+ context,
+ handler,
+ listenerSupplier.get(),
+ inputSurfaceBuilder,
+ windowSessionSupplier,
+ inputChannelSupplier
+ ).apply {
+ start(transaction, parentLeash, key)
+ }
+ })
+ }
+
+ override fun destroyLetterboxSurface(
+ key: LetterboxKey,
+ transaction: Transaction
+ ) {
+ with(inputDetectorMap) {
+ runOnItem(key, onFound = { item ->
+ item.stop(transaction)
+ })
+ remove(key)
+ }
+ }
+
+ override fun updateLetterboxSurfaceVisibility(
+ key: LetterboxKey,
+ transaction: Transaction,
+ visible: Boolean
+ ) {
+ with(inputDetectorMap) {
+ runOnItem(key, onFound = { item ->
+ item.updateVisibility(transaction, visible)
+ })
+ }
+ }
+
+ override fun updateLetterboxSurfaceBounds(
+ key: LetterboxKey,
+ transaction: Transaction,
+ taskBounds: Rect,
+ activityBounds: Rect
+ ) {
+ inputDetectorMap.runOnItem(key, onFound = { item ->
+ item.updateTouchableRegion(transaction, Region(taskBounds))
+ })
+ }
+
+ override fun dump() {
+ ProtoLog.v(WM_SHELL_APP_COMPAT, "%s: %s", TAG, "${inputDetectorMap.keys}")
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputDetector.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputDetector.kt
new file mode 100644
index 000000000000..812cc0161aae
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputDetector.kt
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.letterbox
+
+import android.content.Context
+import android.graphics.Region
+import android.os.Binder
+import android.os.Handler
+import android.os.IBinder
+import android.os.RemoteException
+import android.view.GestureDetector
+import android.view.IWindowSession
+import android.view.InputChannel
+import android.view.InputEvent
+import android.view.InputEventReceiver
+import android.view.MotionEvent
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager
+import android.window.InputTransferToken
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.common.InputChannelSupplier
+import com.android.wm.shell.common.WindowSessionSupplier
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_APP_COMPAT
+
+/**
+ * This is responsible for detecting events on a given [SurfaceControl].
+ */
+class LetterboxInputDetector(
+ private val context: Context,
+ private val handler: Handler,
+ private val listener: LetterboxGestureListener,
+ private val inputSurfaceBuilder: LetterboxInputSurfaceBuilder,
+ private val windowSessionSupplier: WindowSessionSupplier,
+ private val inputChannelSupplier: InputChannelSupplier
+) {
+
+ companion object {
+ @JvmStatic
+ private val TAG = "LetterboxInputDetector"
+ }
+
+ private var state: InputDetectorState? = null
+
+ fun start(tx: Transaction, source: SurfaceControl, key: LetterboxKey) {
+ if (!isRunning()) {
+ val tmpState =
+ InputDetectorState(
+ context,
+ handler,
+ source,
+ key.displayId,
+ listener,
+ inputSurfaceBuilder,
+ windowSessionSupplier.get(),
+ inputChannelSupplier
+ )
+ if (tmpState.start(tx)) {
+ state = tmpState
+ } else {
+ ProtoLog.v(
+ WM_SHELL_APP_COMPAT,
+ "%s not started for %s on %s",
+ TAG,
+ "$source",
+ "$key"
+ )
+ }
+ }
+ }
+
+ fun updateTouchableRegion(tx: Transaction, region: Region) {
+ if (isRunning()) {
+ state?.setTouchableRegion(tx, region)
+ }
+ }
+
+ fun isRunning() = state != null
+
+ fun updateVisibility(tx: Transaction, visible: Boolean) {
+ if (isRunning()) {
+ state?.updateVisibility(tx, visible)
+ }
+ }
+
+ fun stop(tx: Transaction) {
+ if (isRunning()) {
+ state!!.stop(tx)
+ state = null
+ }
+ }
+
+ /**
+ * The state for a {@link SurfaceControl} for a given displayId.
+ */
+ private class InputDetectorState(
+ val context: Context,
+ val handler: Handler,
+ val source: SurfaceControl,
+ val displayId: Int,
+ val listener: LetterboxGestureListener,
+ val inputSurfaceBuilder: LetterboxInputSurfaceBuilder,
+ val windowSession: IWindowSession,
+ inputChannelSupplier: InputChannelSupplier
+ ) {
+
+ private val inputToken: IBinder
+ private val inputChannel: InputChannel
+ private var receiver: EventReceiver? = null
+ private var inputSurface: SurfaceControl? = null
+
+ init {
+ inputToken = Binder()
+ inputChannel = inputChannelSupplier.get()
+ }
+
+ fun start(tx: Transaction): Boolean {
+ val inputTransferToken = InputTransferToken()
+ try {
+ inputSurface =
+ inputSurfaceBuilder.createInputSurface(
+ tx,
+ source,
+ "Sink for $source",
+ "$TAG creation"
+ )
+ windowSession.grantInputChannel(
+ displayId,
+ inputSurface,
+ inputToken,
+ null,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY,
+ WindowManager.LayoutParams.INPUT_FEATURE_SPY,
+ WindowManager.LayoutParams.TYPE_INPUT_CONSUMER,
+ null,
+ inputTransferToken,
+ "$TAG of $source",
+ inputChannel
+ )
+
+ receiver = EventReceiver(context, inputChannel, handler, listener)
+ return true
+ } catch (e: RemoteException) {
+ e.rethrowFromSystemServer()
+ }
+ return false
+ }
+
+ fun setTouchableRegion(tx: Transaction, region: Region) {
+ try {
+ tx.setWindowCrop(inputSurface, region.bounds.width(), region.bounds.height())
+
+ windowSession.updateInputChannel(
+ inputChannel.token,
+ displayId,
+ inputSurface,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY,
+ WindowManager.LayoutParams.INPUT_FEATURE_SPY,
+ region
+ )
+ } catch (e: RemoteException) {
+ e.rethrowFromSystemServer()
+ }
+ }
+
+ fun updateVisibility(tx: Transaction, visible: Boolean) {
+ inputSurface?.let {
+ tx.setVisibility(it, visible)
+ }
+ }
+
+ fun stop(tx: Transaction) {
+ receiver?.dispose()
+ receiver = null
+ inputChannel.dispose()
+ windowSession.removeToken(inputToken)
+ inputSurface?.let { s ->
+ tx.remove(s)
+ }
+ }
+
+ // Removes the provided token
+ private fun IWindowSession.removeToken(token: IBinder) {
+ try {
+ remove(token)
+ } catch (e: RemoteException) {
+ e.rethrowFromSystemServer()
+ }
+ }
+ }
+
+ /**
+ * Reads from the provided {@link InputChannel} and identifies a specific event.
+ */
+ private class EventReceiver(
+ context: Context,
+ inputChannel: InputChannel,
+ uiHandler: Handler,
+ listener: LetterboxGestureListener
+ ) : InputEventReceiver(inputChannel, uiHandler.looper) {
+ private val eventDetector: GestureDetector
+
+ init {
+ eventDetector = GestureDetector(
+ context, listener,
+ uiHandler
+ )
+ }
+
+ override fun onInputEvent(event: InputEvent) {
+ finishInputEvent(event, eventDetector.onTouchEvent(event as MotionEvent))
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputSurfaceBuilder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputSurfaceBuilder.kt
new file mode 100644
index 000000000000..fd8d86576115
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputSurfaceBuilder.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.letterbox
+
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import com.android.wm.shell.common.transition.SurfaceBuilderSupplier
+import com.android.wm.shell.dagger.WMSingleton
+import javax.inject.Inject
+
+/**
+ * Component responsible for the actual creation of the Letterbox surfaces.
+ */
+@WMSingleton
+class LetterboxInputSurfaceBuilder @Inject constructor(
+ private val surfaceBuilderSupplier: SurfaceBuilderSupplier
+) {
+
+ companion object {
+ /*
+ * Letterbox spy surfaces need to stay above the activity layer which is 0.
+ */
+ // TODO(b/378673153): Consider adding this to [TaskConstants].
+ @JvmStatic
+ private val TASK_CHILD_LAYER_LETTERBOX_SPY = 1000
+ }
+
+ fun createInputSurface(
+ tx: Transaction,
+ parentLeash: SurfaceControl,
+ surfaceName: String,
+ callSite: String
+ ) = surfaceBuilderSupplier.get()
+ .setName(surfaceName)
+ .setContainerLayer()
+ .setParent(parentLeash)
+ .setCallsite(callSite)
+ .build().apply {
+ tx.setLayer(this, TASK_CHILD_LAYER_LETTERBOX_SPY)
+ .setTrustedOverlay(this, true)
+ .show(this)
+ .apply()
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index 621ccba40db2..27aed17762ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -60,6 +60,7 @@ import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
+import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
/**
@@ -76,7 +77,11 @@ public class DesktopModeVisualIndicator {
/** Indicates impending transition into split select on the left side */
TO_SPLIT_LEFT_INDICATOR,
/** Indicates impending transition into split select on the right side */
- TO_SPLIT_RIGHT_INDICATOR
+ TO_SPLIT_RIGHT_INDICATOR,
+ /** Indicates impending transition into bubble on the left side */
+ TO_BUBBLE_LEFT_INDICATOR,
+ /** Indicates impending transition into bubble on the right side */
+ TO_BUBBLE_RIGHT_INDICATOR
}
/**
@@ -115,6 +120,7 @@ public class DesktopModeVisualIndicator {
private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer;
private final ActivityManager.RunningTaskInfo mTaskInfo;
private final SurfaceControl mTaskSurface;
+ private final @Nullable BubbleDropTargetBoundsProvider mBubbleBoundsProvider;
private SurfaceControl mLeash;
private final SyncTransactionQueue mSyncQueue;
@@ -129,13 +135,15 @@ public class DesktopModeVisualIndicator {
ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController,
Context context, SurfaceControl taskSurface,
RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer,
- DragStartState dragStartState) {
+ DragStartState dragStartState,
+ @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) {
mSyncQueue = syncQueue;
mTaskInfo = taskInfo;
mDisplayController = displayController;
mContext = context;
mTaskSurface = taskSurface;
mRootTdaOrganizer = taskDisplayAreaOrganizer;
+ mBubbleBoundsProvider = bubbleBoundsProvider;
mCurrentType = NO_INDICATOR;
mDragStartState = dragStartState;
}
@@ -175,15 +183,24 @@ public class DesktopModeVisualIndicator {
captionHeight);
final Region splitRightRegion = calculateSplitRightRegion(layout, transitionAreaWidth,
captionHeight);
- if (fullscreenRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {
+ final int x = (int) inputCoordinates.x;
+ final int y = (int) inputCoordinates.y;
+ if (fullscreenRegion.contains(x, y)) {
result = TO_FULLSCREEN_INDICATOR;
}
- if (splitLeftRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {
+ if (splitLeftRegion.contains(x, y)) {
result = IndicatorType.TO_SPLIT_LEFT_INDICATOR;
}
- if (splitRightRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) {
+ if (splitRightRegion.contains(x, y)) {
result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
}
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
+ if (calculateBubbleLeftRegion(layout).contains(x, y)) {
+ result = IndicatorType.TO_BUBBLE_LEFT_INDICATOR;
+ } else if (calculateBubbleRightRegion(layout).contains(x, y)) {
+ result = IndicatorType.TO_BUBBLE_RIGHT_INDICATOR;
+ }
+ }
if (mDragStartState != DragStartState.DRAGGED_INTENT) {
transitionIndicator(result);
}
@@ -247,6 +264,25 @@ public class DesktopModeVisualIndicator {
return region;
}
+ @VisibleForTesting
+ Region calculateBubbleLeftRegion(DisplayLayout layout) {
+ int regionWidth = mContext.getResources().getDimensionPixelSize(
+ com.android.wm.shell.R.dimen.bubble_transform_area_width);
+ int regionHeight = mContext.getResources().getDimensionPixelSize(
+ com.android.wm.shell.R.dimen.bubble_transform_area_height);
+ return new Region(0, layout.height() - regionHeight, regionWidth, layout.height());
+ }
+
+ @VisibleForTesting
+ Region calculateBubbleRightRegion(DisplayLayout layout) {
+ int regionWidth = mContext.getResources().getDimensionPixelSize(
+ com.android.wm.shell.R.dimen.bubble_transform_area_width);
+ int regionHeight = mContext.getResources().getDimensionPixelSize(
+ com.android.wm.shell.R.dimen.bubble_transform_area_height);
+ return new Region(layout.width() - regionWidth, layout.height() - regionHeight,
+ layout.width(), layout.height());
+ }
+
/**
* Create a fullscreen indicator with no animation
*/
@@ -297,6 +333,11 @@ public class DesktopModeVisualIndicator {
});
}
+ @VisibleForTesting
+ Rect getIndicatorBounds() {
+ return mView.getBackground().getBounds();
+ }
+
/**
* Fade indicator in as provided type. Animator fades it in while expanding the bounds outwards.
*/
@@ -304,7 +345,8 @@ public class DesktopModeVisualIndicator {
mView.setBackgroundResource(R.drawable.desktop_windowing_transition_background);
final VisualIndicatorAnimator animator = VisualIndicatorAnimator
.fadeBoundsIn(mView, type,
- mDisplayController.getDisplayLayout(mTaskInfo.displayId));
+ mDisplayController.getDisplayLayout(mTaskInfo.displayId),
+ mBubbleBoundsProvider);
animator.start();
mCurrentType = type;
}
@@ -323,7 +365,8 @@ public class DesktopModeVisualIndicator {
}
final VisualIndicatorAnimator animator = VisualIndicatorAnimator
.fadeBoundsOut(mView, mCurrentType,
- mDisplayController.getDisplayLayout(mTaskInfo.displayId));
+ mDisplayController.getDisplayLayout(mTaskInfo.displayId),
+ mBubbleBoundsProvider);
animator.start();
if (finishCallback != null) {
animator.addListener(new AnimatorListenerAdapter() {
@@ -351,7 +394,7 @@ public class DesktopModeVisualIndicator {
} else {
final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType(
mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType,
- newType);
+ newType, mBubbleBoundsProvider);
mCurrentType = newType;
animator.start();
}
@@ -406,8 +449,9 @@ public class DesktopModeVisualIndicator {
}
private static VisualIndicatorAnimator fadeBoundsIn(
- @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout) {
- final Rect endBounds = getIndicatorBounds(displayLayout, type);
+ @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout,
+ @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) {
+ final Rect endBounds = getIndicatorBounds(displayLayout, type, bubbleBoundsProvider);
final Rect startBounds = getMinBounds(endBounds);
view.getBackground().setBounds(startBounds);
@@ -419,8 +463,9 @@ public class DesktopModeVisualIndicator {
}
private static VisualIndicatorAnimator fadeBoundsOut(
- @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout) {
- final Rect startBounds = getIndicatorBounds(displayLayout, type);
+ @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout,
+ @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) {
+ final Rect startBounds = getIndicatorBounds(displayLayout, type, bubbleBoundsProvider);
final Rect endBounds = getMinBounds(startBounds);
view.getBackground().setBounds(startBounds);
@@ -435,16 +480,19 @@ public class DesktopModeVisualIndicator {
* Create animator for visual indicator changing type (i.e., fullscreen to freeform,
* freeform to split, etc.)
*
- * @param view the view for this indicator
- * @param displayLayout information about the display the transitioning task is currently on
- * @param origType the original indicator type
- * @param newType the new indicator type
+ * @param view the view for this indicator
+ * @param displayLayout information about the display the transitioning task is
+ * currently on
+ * @param origType the original indicator type
+ * @param newType the new indicator type
+ * @param bubbleBoundsProvider provides bounds for bubbles indicators
*/
private static VisualIndicatorAnimator animateIndicatorType(@NonNull View view,
- @NonNull DisplayLayout displayLayout, IndicatorType origType,
- IndicatorType newType) {
- final Rect startBounds = getIndicatorBounds(displayLayout, origType);
- final Rect endBounds = getIndicatorBounds(displayLayout, newType);
+ @NonNull DisplayLayout displayLayout, IndicatorType origType, IndicatorType newType,
+ @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) {
+ final Rect startBounds = getIndicatorBounds(displayLayout, origType,
+ bubbleBoundsProvider);
+ final Rect endBounds = getIndicatorBounds(displayLayout, newType, bubbleBoundsProvider);
final VisualIndicatorAnimator animator = new VisualIndicatorAnimator(
view, startBounds, endBounds);
animator.setInterpolator(new DecelerateInterpolator());
@@ -453,7 +501,8 @@ public class DesktopModeVisualIndicator {
}
/** Calculates the bounds the indicator should have when fully faded in. */
- private static Rect getIndicatorBounds(DisplayLayout layout, IndicatorType type) {
+ private static Rect getIndicatorBounds(DisplayLayout layout, IndicatorType type,
+ @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) {
final Rect desktopStableBounds = new Rect();
layout.getStableBounds(desktopStableBounds);
final int padding = desktopStableBounds.top;
@@ -481,6 +530,18 @@ public class DesktopModeVisualIndicator {
return new Rect(desktopStableBounds.width() / 2 + padding, padding,
desktopStableBounds.width() - padding,
desktopStableBounds.height());
+ case TO_BUBBLE_LEFT_INDICATOR:
+ if (bubbleBoundsProvider == null) {
+ return new Rect();
+ }
+ return bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(
+ /* onLeft= */ true);
+ case TO_BUBBLE_RIGHT_INDICATOR:
+ if (bubbleBoundsProvider == null) {
+ return new Rect();
+ }
+ return bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(
+ /* onLeft= */ false);
default:
throw new IllegalArgumentException("Invalid indicator type provided.");
}
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 fb4016c4e7b6..0d32acd6b068 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
@@ -150,6 +150,7 @@ import java.util.Optional
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
+import kotlin.jvm.optionals.getOrNull
/** Handles moving tasks in and out of desktop */
class DesktopTasksController(
@@ -2706,6 +2707,7 @@ class DesktopTasksController(
taskSurface,
rootTaskDisplayAreaOrganizer,
dragStartState,
+ bubbleController.getOrNull()?.bubbleDropTargetBoundsProvider,
)
if (visualIndicator == null) visualIndicator = indicator
return indicator.updateIndicatorType(PointF(inputX, taskTop))
@@ -2788,7 +2790,11 @@ class DesktopTasksController(
desktopModeWindowDecoration,
)
}
- IndicatorType.NO_INDICATOR -> {
+ IndicatorType.NO_INDICATOR,
+ IndicatorType.TO_BUBBLE_LEFT_INDICATOR,
+ IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> {
+ // TODO(b/391928049): add support fof dragging desktop apps to a bubble
+
// Create a copy so that we can animate from the current bounds if we end up having
// to snap the surface back without a WCT change.
val destinationBounds = Rect(currentDragBounds)
@@ -2915,6 +2921,11 @@ class DesktopTasksController(
)
requestSplit(taskInfo, leftOrTop = false)
}
+ IndicatorType.TO_BUBBLE_LEFT_INDICATOR,
+ IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> {
+ // TODO(b/388851898): move to bubble
+ cancelDragToDesktop(taskInfo)
+ }
}
return indicatorType
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index e74870d4d139..5894ea8d0b5c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -32,6 +32,7 @@ import android.view.View;
import android.view.ViewRootImpl;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityManager;
import android.window.SurfaceSyncGroup;
import androidx.annotation.Nullable;
@@ -63,6 +64,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
private TvPipMenuView mPipMenuView;
private TvPipBackgroundView mPipBackgroundView;
+ private final AccessibilityManager mA11yManager;
+
private boolean mIsReloading;
private static final int PIP_MENU_FORCE_CLOSE_DELAY_MS = 10_000;
private final Runnable mClosePipMenuRunnable = this::closeMenu;
@@ -107,6 +110,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
mSystemWindows = systemWindows;
mMainHandler = mainHandler;
+ mA11yManager = context.getSystemService(AccessibilityManager.class);
+
// We need to "close" the menu the platform call for all the system dialogs to close (for
// example, on the Home button press).
final BroadcastReceiver closeSystemDialogsBroadcastReceiver = new BroadcastReceiver() {
@@ -499,7 +504,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
switchToMenuMode(menuMode);
} else {
if (isMenuOpen(menuMode)) {
- mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS);
+ if (!mA11yManager.isEnabled()) {
+ mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS);
+ }
mMenuModeOnFocus = menuMode;
}
// Send a request to gain window focus if the menu is open, or lose window focus
@@ -594,8 +601,10 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
public void onUserInteracting() {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onUserInteracting - mCurrentMenuMode=%s", TAG, getMenuModeString());
- mMainHandler.removeCallbacks(mClosePipMenuRunnable);
- mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS);
+ if (mMainHandler.hasCallbacks(mClosePipMenuRunnable)) {
+ mMainHandler.removeCallbacks(mClosePipMenuRunnable);
+ mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS);
+ }
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
index 9af23080351f..a6f872634ee9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
@@ -52,6 +52,7 @@ import com.android.wm.shell.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.shared.TransitionUtil;
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.transition.Transitions;
import java.util.ArrayList;
@@ -571,7 +572,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- PendingTransition pending = findPending(transition);
+ final PendingTransition pending = findPending(transition);
if (pending != null) {
mPending.remove(pending);
}
@@ -586,10 +587,11 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
WindowContainerTransaction wct = null;
for (int i = 0; i < info.getChanges().size(); ++i) {
final TransitionInfo.Change chg = info.getChanges().get(i);
- if (chg.getTaskInfo() == null) continue;
+ final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo();
+ if (taskInfo == null) continue;
if (TransitionUtil.isClosingType(chg.getMode())) {
final boolean isHide = chg.getMode() == TRANSIT_TO_BACK;
- TaskViewTaskController tv = findTaskView(chg.getTaskInfo());
+ TaskViewTaskController tv = findTaskView(taskInfo);
if (tv == null && !isHide) {
// TaskView can be null when closing
changesHandled++;
@@ -599,7 +601,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
if (pending != null) {
Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This "
+ "shouldn't happen, so there may be a visual artifact: "
- + chg.getTaskInfo().taskId);
+ + taskInfo.taskId);
}
continue;
}
@@ -615,40 +617,51 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV
}
changesHandled++;
} else if (TransitionUtil.isOpeningType(chg.getMode())) {
- final boolean taskIsNew = chg.getMode() == TRANSIT_OPEN;
- final TaskViewTaskController tv;
- if (taskIsNew) {
- if (pending == null
- || !chg.getTaskInfo().containsLaunchCookie(pending.mLaunchCookie)) {
+ boolean isNewInTaskView = false;
+ TaskViewTaskController tv;
+ if (chg.getMode() == TRANSIT_OPEN) {
+ isNewInTaskView = true;
+ if (pending == null || !taskInfo.containsLaunchCookie(pending.mLaunchCookie)) {
Slog.e(TAG, "Found a launching TaskView in the wrong transition. All "
+ "TaskView launches should be initiated by shell and in their "
- + "own transition: " + chg.getTaskInfo().taskId);
+ + "own transition: " + taskInfo.taskId);
continue;
}
stillNeedsMatchingLaunch = false;
tv = pending.mTaskView;
} else {
- tv = findTaskView(chg.getTaskInfo());
- if (tv == null) {
- if (pending != null) {
- Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This "
- + "shouldn't happen, so there may be a visual artifact: "
- + chg.getTaskInfo().taskId);
+ tv = findTaskView(taskInfo);
+ if (tv == null && pending != null) {
+ if (BubbleAnythingFlagHelper.enableCreateAnyBubble()
+ && chg.getMode() == TRANSIT_TO_FRONT
+ && pending.mTaskView.getPendingInfo() != null
+ && pending.mTaskView.getPendingInfo().taskId == taskInfo.taskId) {
+ // In this case an existing task, not currently in TaskView, is
+ // brought to the front to be moved into TaskView. This is still
+ // "new" from TaskView's perspective. (e.g. task being moved into a
+ // bubble)
+ isNewInTaskView = true;
+ stillNeedsMatchingLaunch = false;
+ tv = pending.mTaskView;
+ } else {
+ Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. "
+ + "This shouldn't happen, so there may be a visual "
+ + "artifact: " + taskInfo.taskId);
}
- continue;
}
+ if (tv == null) continue;
}
if (wct == null) wct = new WindowContainerTransaction();
- prepareOpenAnimation(tv, taskIsNew, startTransaction, finishTransaction,
- chg.getTaskInfo(), chg.getLeash(), wct);
+ prepareOpenAnimation(tv, isNewInTaskView, startTransaction, finishTransaction,
+ taskInfo, chg.getLeash(), wct);
changesHandled++;
} else if (chg.getMode() == TRANSIT_CHANGE) {
- TaskViewTaskController tv = findTaskView(chg.getTaskInfo());
+ TaskViewTaskController tv = findTaskView(taskInfo);
if (tv == null) {
if (pending != null) {
Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This "
+ "shouldn't happen, so there may be a visual artifact: "
- + chg.getTaskInfo().taskId);
+ + taskInfo.taskId);
}
continue;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 72cbc4702ac8..c90f6cf62b7e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -193,6 +193,9 @@ public class Transitions implements RemoteCallable<Transitions>,
/** Transition to end the recents transition */
public static final int TRANSIT_END_RECENTS_TRANSITION = TRANSIT_FIRST_CUSTOM + 22;
+ /** Transition type for app compat reachability. */
+ public static final int TRANSIT_MOVE_LETTERBOX_REACHABILITY = TRANSIT_FIRST_CUSTOM + 23;
+
/** Transition type for desktop mode transitions. */
public static final int TRANSIT_DESKTOP_MODE_TYPES =
WindowManager.TRANSIT_FIRST_CUSTOM + 100;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/InputChannelSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/InputChannelSupplierTest.kt
new file mode 100644
index 000000000000..09c2faaa2670
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/InputChannelSupplierTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.testing.AndroidTestingRunner
+import android.view.InputChannel
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for [InputChannelSupplier].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:InputChannelSupplierTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class InputChannelSupplierTest {
+
+ @Test
+ fun `InputChannelSupplier supplies an InputChannel`() {
+ val supplier = InputChannelSupplier()
+ SuppliersUtilsTest.assertSupplierProvidesValue(supplier) {
+ it is InputChannel
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/SuppliersUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/SuppliersUtilsTest.kt
new file mode 100644
index 000000000000..8468c636542e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/SuppliersUtilsTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import java.util.function.Supplier
+
+/**
+ * Utility class we can use to test a []Supplier<T>] of any parameters type [T].
+ */
+class SuppliersUtilsTest {
+
+ companion object {
+ /**
+ * Allows to check that the object supplied is asserts what in [assertion].
+ */
+ fun <T> assertSupplierProvidesValue(supplier: Supplier<T>, assertion: (Any?) -> Boolean) {
+ assert(assertion(supplier.get())) { "Supplier didn't provided what is expected" }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt
new file mode 100644
index 000000000000..33e8d78d6a15
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.testing.AndroidTestingRunner
+import android.view.IWindowSession
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for [WindowSessionSupplier].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:WindowSessionSupplierTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class WindowSessionSupplierTest {
+
+ @Test
+ fun `InputChannelSupplier supplies an InputChannel`() {
+ val supplier = WindowSessionSupplier()
+ SuppliersUtilsTest.assertSupplierProvidesValue(supplier) {
+ it is IWindowSession
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplierTest.kt
new file mode 100644
index 000000000000..f88f72356759
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplierTest.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.transition
+
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.common.SuppliersUtilsTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for [SurfaceBuilderSupplier].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:SurfaceBuilderSupplierTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class SurfaceBuilderSupplierTest {
+
+ @Test
+ fun `SurfaceBuilderSupplier supplies an SurfaceControl Builder`() {
+ val supplier = SurfaceBuilderSupplier()
+ SuppliersUtilsTest.assertSupplierProvidesValue(supplier) {
+ it is SurfaceControl.Builder
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransactionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransactionSupplierTest.kt
new file mode 100644
index 000000000000..12b4d8b5f96b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransactionSupplierTest.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.transition
+
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.common.SuppliersUtilsTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for [TransactionSupplier].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:TransactionSupplierTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class TransactionSupplierTest {
+
+ @Test
+ fun `SurfaceBuilderSupplier supplies a Transaction`() {
+ val supplier = TransactionSupplier()
+ SuppliersUtilsTest.assertSupplierProvidesValue(supplier) {
+ it is SurfaceControl.Transaction
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt
index 88cc981dd30c..e34884b103f6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt
@@ -33,10 +33,10 @@ abstract class LetterboxControllerRobotTest {
companion object {
@JvmStatic
- private val DISPLAY_ID = 1
+ val DISPLAY_ID = 1
@JvmStatic
- private val TASK_ID = 20
+ val TASK_ID = 20
}
lateinit var letterboxController: LetterboxController
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureDelegateTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureDelegateTest.kt
new file mode 100644
index 000000000000..bc3416a88918
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureDelegateTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.letterbox
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
+import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [LetterboxGestureDelegate].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:LetterboxGestureDelegateTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LetterboxGestureDelegateTest {
+
+ class DelegateTest : LetterboxGestureListener by LetterboxGestureDelegate
+
+ val delegate = DelegateTest()
+
+ @Before
+ fun setUp() {
+ spyOn(LetterboxGestureDelegate)
+ }
+
+ @Test
+ fun `When delegating all methods are invoked`() {
+ val event = motionEventAt(0f, 0f)
+ with(delegate) {
+ onDown(event)
+ onShowPress(event)
+ onSingleTapUp(event)
+ onScroll(event, event, 0f, 0f)
+ onFling(event, event, 0f, 0f)
+ onLongPress(event)
+ onSingleTapConfirmed(event)
+ onDoubleTap(event)
+ onDoubleTapEvent(event)
+ onContextClick(event)
+ }
+ with(LetterboxGestureDelegate) {
+ verify(this).onDown(event)
+ verify(this).onShowPress(event)
+ verify(this).onSingleTapUp(event)
+ verify(this).onScroll(event, event, 0f, 0f)
+ verify(this).onFling(event, event, 0f, 0f)
+ verify(this).onLongPress(event)
+ verify(this).onSingleTapConfirmed(event)
+ verify(this).onDoubleTap(event)
+ verify(this).onDoubleTapEvent(event)
+ verify(this).onContextClick(event)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxInputControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxInputControllerTest.kt
new file mode 100644
index 000000000000..fa95faee4b6e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxInputControllerTest.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.letterbox
+
+import android.content.Context
+import android.graphics.Rect
+import android.graphics.Region
+import android.os.Handler
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import android.view.IWindowSession
+import android.view.InputChannel
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.InputChannelSupplier
+import com.android.wm.shell.common.WindowSessionSupplier
+import com.android.wm.shell.compatui.letterbox.LetterboxMatchers.asAnyMode
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.TAG
+import java.util.function.Consumer
+import java.util.function.Supplier
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [LetterboxInputController].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:LetterboxInputControllerTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LetterboxInputControllerTest : ShellTestCase() {
+
+ @Test
+ fun `When creation is requested the surface is created if not present`() {
+ runTestScenario { r ->
+ r.sendCreateSurfaceRequest()
+
+ r.checkInputSurfaceBuilderInvoked()
+ }
+ }
+
+ @Test
+ fun `When creation is requested multiple times the input surface is created once`() {
+ runTestScenario { r ->
+ r.sendCreateSurfaceRequest()
+ r.sendCreateSurfaceRequest()
+ r.sendCreateSurfaceRequest()
+ r.sendCreateSurfaceRequest()
+
+ r.checkInputSurfaceBuilderInvoked(times = 1)
+ }
+ }
+
+ @Test
+ fun `A different input surface is created for every key`() {
+ runTestScenario { r ->
+ r.sendCreateSurfaceRequest()
+ r.sendCreateSurfaceRequest()
+ r.sendCreateSurfaceRequest(displayId = 2)
+ r.sendCreateSurfaceRequest(displayId = 2, taskId = 2)
+ r.sendCreateSurfaceRequest(displayId = 2)
+ r.sendCreateSurfaceRequest(displayId = 2, taskId = 2)
+
+ r.checkInputSurfaceBuilderInvoked(times = 3)
+ }
+ }
+
+ @Test
+ fun `Created spy surface is removed once`() {
+ runTestScenario { r ->
+ r.sendCreateSurfaceRequest()
+ r.checkInputSurfaceBuilderInvoked()
+
+ r.sendDestroySurfaceRequest()
+ r.sendDestroySurfaceRequest()
+ r.sendDestroySurfaceRequest()
+
+ r.checkTransactionRemovedInvoked()
+ }
+ }
+ @Test
+ fun `Only existing surfaces receive visibility update`() {
+ runTestScenario { r ->
+ r.sendCreateSurfaceRequest()
+ r.sendUpdateSurfaceVisibilityRequest(visible = true)
+ r.sendUpdateSurfaceVisibilityRequest(visible = true, displayId = 20)
+
+ r.checkVisibilityUpdated(expectedVisibility = true)
+ }
+ }
+
+ @Test
+ fun `Only existing surfaces receive taskBounds update`() {
+ runTestScenario { r ->
+ r.sendUpdateSurfaceBoundsRequest(
+ taskBounds = Rect(0, 0, 2000, 1000),
+ activityBounds = Rect(500, 0, 1500, 1000)
+ )
+
+ r.checkUpdateSessionRegion(times = 0, region = Region(0, 0, 2000, 1000))
+ r.checkSurfaceSizeUpdated(times = 0, expectedWidth = 2000, expectedHeight = 1000)
+
+ r.resetTransitionTest()
+
+ r.sendCreateSurfaceRequest()
+ r.sendUpdateSurfaceBoundsRequest(
+ taskBounds = Rect(0, 0, 2000, 1000),
+ activityBounds = Rect(500, 0, 1500, 1000)
+ )
+ r.checkUpdateSessionRegion(region = Region(0, 0, 2000, 1000))
+ r.checkSurfaceSizeUpdated(expectedWidth = 2000, expectedHeight = 1000)
+ }
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ fun runTestScenario(consumer: Consumer<InputLetterboxControllerRobotTest>) {
+ consumer.accept(InputLetterboxControllerRobotTest(mContext).apply { initController() })
+ }
+
+ class InputLetterboxControllerRobotTest(private val context: Context) :
+ LetterboxControllerRobotTest() {
+
+ private val inputSurfaceBuilder: LetterboxInputSurfaceBuilder
+ private val handler = Handler(Looper.getMainLooper())
+ private val listener: LetterboxGestureListener
+ private val listenerSupplier: Supplier<LetterboxGestureListener>
+ private val windowSessionSupplier: WindowSessionSupplier
+ private val windowSession: IWindowSession
+ private val inputChannelSupplier: InputChannelSupplier
+
+ init {
+ inputSurfaceBuilder = getLetterboxInputSurfaceBuilderMock()
+ listener = mock<LetterboxGestureListener>()
+ listenerSupplier = mock<Supplier<LetterboxGestureListener>>()
+ doReturn(LetterboxGestureDelegate).`when`(listenerSupplier).get()
+ windowSessionSupplier = mock<WindowSessionSupplier>()
+ windowSession = mock<IWindowSession>()
+ doReturn(windowSession).`when`(windowSessionSupplier).get()
+ inputChannelSupplier = mock<InputChannelSupplier>()
+ val inputChannels = InputChannel.openInputChannelPair(TAG)
+ inputChannels.first().dispose()
+ doReturn(inputChannels[1]).`when`(inputChannelSupplier).get()
+ }
+
+ override fun buildController(): LetterboxController =
+ LetterboxInputController(
+ context,
+ handler,
+ inputSurfaceBuilder,
+ listenerSupplier,
+ windowSessionSupplier,
+ inputChannelSupplier
+ )
+
+ fun checkInputSurfaceBuilderInvoked(
+ times: Int = 1,
+ name: String = "",
+ callSite: String = ""
+ ) {
+ verify(inputSurfaceBuilder, times(times)).createInputSurface(
+ eq(transaction),
+ eq(parentLeash),
+ name.asAnyMode(),
+ callSite.asAnyMode()
+ )
+ }
+
+ fun checkUpdateSessionRegion(times: Int = 1, displayId: Int = DISPLAY_ID, region: Region) {
+ verify(windowSession, times(times)).updateInputChannel(
+ any(),
+ eq(displayId),
+ any(),
+ any(),
+ any(),
+ any(),
+ eq(region)
+ )
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt
index 2c06dfda7917..3ce1fec32a16 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt
@@ -16,6 +16,8 @@
package com.android.wm.shell.compatui.letterbox
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.obtain
import android.view.SurfaceControl
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
@@ -37,6 +39,18 @@ fun getTransactionMock(): SurfaceControl.Transaction = mock<SurfaceControl.Trans
doReturn(this).`when`(this).setWindowCrop(anyOrNull(), any(), any())
}
+/**
+ * @return A [LetterboxInputSurfaceBuilder] mock to use in tests.
+ */
+fun getLetterboxInputSurfaceBuilderMock() = mock<LetterboxInputSurfaceBuilder>().apply {
+ doReturn(SurfaceControl()).`when`(this).createInputSurface(
+ any(),
+ any(),
+ any(),
+ any()
+ )
+}
+
// Utility to make verification mode depending on a [Boolean].
fun Boolean.asMode(): VerificationMode = if (this) times(1) else never()
@@ -47,5 +61,10 @@ object LetterboxMatchers {
fun String.asAnyMode() = asAnyMode { this.isEmpty() }
}
+object LetterboxEvents {
+ fun motionEventAt(x: Float, y: Float) =
+ obtain(0, 10, ACTION_DOWN, x, y, 0)
+}
+
private inline fun <reified T : Any> T.asAnyMode(condition: () -> Boolean) =
(if (condition()) any() else eq(this))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
index 13b44977e9c7..a6575535faee 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
@@ -16,11 +16,11 @@
package com.android.wm.shell.desktopmode
+import android.animation.AnimatorTestRule
import android.app.ActivityManager.RunningTaskInfo
import android.graphics.PointF
import android.graphics.Rect
import android.platform.test.annotations.EnableFlags
-import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.view.SurfaceControl
@@ -34,6 +34,7 @@ import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -56,7 +57,7 @@ import org.mockito.kotlin.whenever
@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
class DesktopModeVisualIndicatorTest : ShellTestCase() {
- @JvmField @Rule val setFlagsRule = SetFlagsRule()
+ @JvmField @Rule val animatorTestRule = AnimatorTestRule(this)
@JvmField
@Rule
@@ -69,6 +70,7 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
@Mock private lateinit var taskSurface: SurfaceControl
@Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock private lateinit var displayLayout: DisplayLayout
+ @Mock private lateinit var bubbleBoundsProvider: BubbleDropTargetBoundsProvider
private lateinit var visualIndicator: DesktopModeVisualIndicator
@@ -80,6 +82,8 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS)
whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
whenever(displayController.getDisplay(anyInt())).thenReturn(mContext.display)
+ whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(any()))
+ .thenReturn(Rect())
taskInfo = DesktopTestHelpers.createFullscreenTask()
}
@@ -194,6 +198,40 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
}
@Test
+ fun testBubbleLeftRegionCalculation() {
+ val bubbleRegionWidth =
+ context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_width)
+ val bubbleRegionHeight =
+ context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_height)
+ val expectedRect = Rect(0, 1600 - bubbleRegionHeight, bubbleRegionWidth, 1600)
+
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
+ var testRegion = visualIndicator.calculateBubbleLeftRegion(displayLayout)
+ assertThat(testRegion.bounds).isEqualTo(expectedRect)
+
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
+ testRegion = visualIndicator.calculateBubbleLeftRegion(displayLayout)
+ assertThat(testRegion.bounds).isEqualTo(expectedRect)
+ }
+
+ @Test
+ fun testBubbleRightRegionCalculation() {
+ val bubbleRegionWidth =
+ context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_width)
+ val bubbleRegionHeight =
+ context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_height)
+ val expectedRect = Rect(2400 - bubbleRegionWidth, 1600 - bubbleRegionHeight, 2400, 1600)
+
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
+ var testRegion = visualIndicator.calculateBubbleRightRegion(displayLayout)
+ assertThat(testRegion.bounds).isEqualTo(expectedRect)
+
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
+ testRegion = visualIndicator.calculateBubbleRightRegion(displayLayout)
+ assertThat(testRegion.bounds).isEqualTo(expectedRect)
+ }
+
+ @Test
fun testDefaultIndicators() {
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
var result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f))
@@ -219,31 +257,79 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
fun testDefaultIndicatorWithNoDesktop() {
whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false)
+ // Fullscreen to center, no desktop indicator
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
var result = visualIndicator.updateIndicatorType(PointF(500f, 500f))
assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
-
+ // Fullscreen to split
result = visualIndicator.updateIndicatorType(PointF(10000f, 500f))
assertThat(result)
.isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR)
-
result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f))
assertThat(result)
.isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR)
-
+ // Fullscreen to bubble
+ result = visualIndicator.updateIndicatorType(PointF(100f, 1500f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_LEFT_INDICATOR)
+ result = visualIndicator.updateIndicatorType(PointF(2300f, 1500f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR)
+ // Split to center, no desktop indicator
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
result = visualIndicator.updateIndicatorType(PointF(500f, 500f))
assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
-
+ // Split to fullscreen
result = visualIndicator.updateIndicatorType(PointF(500f, 0f))
assertThat(result)
.isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
-
+ // Split to bubble
+ result = visualIndicator.updateIndicatorType(PointF(100f, 1500f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_LEFT_INDICATOR)
+ result = visualIndicator.updateIndicatorType(PointF(2300f, 1500f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR)
+ // Drag app to center, no desktop indicator
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
result = visualIndicator.updateIndicatorType(PointF(500f, 500f))
assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
}
+ @Test
+ @EnableFlags(
+ com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN,
+ com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE,
+ )
+ fun testBubbleLeftVisualIndicatorSize() {
+ val dropTargetBounds = Rect(100, 100, 500, 1500)
+ whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(/* onLeft= */ true))
+ .thenReturn(dropTargetBounds)
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
+ visualIndicator.updateIndicatorType(PointF(100f, 1500f))
+
+ animatorTestRule.advanceTimeBy(200)
+
+ assertThat(visualIndicator.indicatorBounds).isEqualTo(dropTargetBounds)
+ }
+
+ @Test
+ @EnableFlags(
+ com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN,
+ com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE,
+ )
+ fun testBubbleRightVisualIndicatorSize() {
+ val dropTargetBounds = Rect(1900, 100, 2300, 1500)
+ whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(/* onLeft= */ false))
+ .thenReturn(dropTargetBounds)
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
+ visualIndicator.updateIndicatorType(PointF(2300f, 1500f))
+
+ animatorTestRule.advanceTimeBy(200)
+
+ assertThat(visualIndicator.indicatorBounds).isEqualTo(dropTargetBounds)
+ }
+
private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) {
visualIndicator =
DesktopModeVisualIndicator(
@@ -254,6 +340,7 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
taskSurface,
taskDisplayAreaOrganizer,
dragStartState,
+ bubbleBoundsProvider,
)
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
new file mode 100644
index 000000000000..e28d6ff8bf7f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared.bubbles
+
+import android.graphics.Insets
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker
+import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker
+import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+/** Unit tests for [DragZoneFactory]. */
+class DragZoneFactoryTest {
+
+ private lateinit var dragZoneFactory: DragZoneFactory
+ private val tabletPortrait =
+ DeviceConfig(
+ windowBounds = Rect(0, 0, 1000, 2000),
+ isLargeScreen = true,
+ isSmallTablet = false,
+ isLandscape = false,
+ isRtl = false,
+ insets = Insets.of(0, 0, 0, 0)
+ )
+ private val tabletLandscape =
+ tabletPortrait.copy(windowBounds = Rect(0, 0, 2000, 1000), isLandscape = true)
+ private val foldablePortrait =
+ tabletPortrait.copy(windowBounds = Rect(0, 0, 800, 900), isSmallTablet = true)
+ private val foldableLandscape =
+ foldablePortrait.copy(windowBounds = Rect(0, 0, 900, 800), isLandscape = true)
+ private val splitScreenModeChecker = SplitScreenModeChecker { SplitScreenMode.NONE }
+ private var isDesktopWindowModeSupported = true
+ private val desktopWindowModeChecker = DesktopWindowModeChecker { isDesktopWindowModeSupported }
+
+ @Test
+ fun dragZonesForBubbleBar_tablet() {
+ dragZoneFactory =
+ DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.BubbleBar(BubbleBarLocation.LEFT))
+ val expectedZones: List<Class<out DragZone>> =
+ listOf(
+ DragZone.Dismiss::class.java,
+ DragZone.Bubble::class.java,
+ DragZone.Bubble::class.java,
+ )
+ dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
+ assertThat(zone).isInstanceOf(expectedType)
+ }
+ }
+
+ @Test
+ fun dragZonesForBubble_tablet_portrait() {
+ dragZoneFactory =
+ DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
+ val expectedZones: List<Class<out DragZone>> =
+ listOf(
+ DragZone.Dismiss::class.java,
+ DragZone.Bubble.Left::class.java,
+ DragZone.Bubble.Right::class.java,
+ DragZone.FullScreen::class.java,
+ DragZone.DesktopWindow::class.java,
+ DragZone.Split.Top::class.java,
+ DragZone.Split.Bottom::class.java,
+ )
+ dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
+ assertThat(zone).isInstanceOf(expectedType)
+ }
+ }
+
+ @Test
+ fun dragZonesForBubble_tablet_landscape() {
+ dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
+ val expectedZones: List<Class<out DragZone>> =
+ listOf(
+ DragZone.Dismiss::class.java,
+ DragZone.Bubble.Left::class.java,
+ DragZone.Bubble.Right::class.java,
+ DragZone.FullScreen::class.java,
+ DragZone.DesktopWindow::class.java,
+ DragZone.Split.Left::class.java,
+ DragZone.Split.Right::class.java,
+ )
+ dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
+ assertThat(zone).isInstanceOf(expectedType)
+ }
+ }
+
+ @Test
+ fun dragZonesForBubble_foldable_portrait() {
+ dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
+ val expectedZones: List<Class<out DragZone>> =
+ listOf(
+ DragZone.Dismiss::class.java,
+ DragZone.Bubble.Left::class.java,
+ DragZone.Bubble.Right::class.java,
+ DragZone.FullScreen::class.java,
+ DragZone.Split.Left::class.java,
+ DragZone.Split.Right::class.java,
+ )
+ dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
+ assertThat(zone).isInstanceOf(expectedType)
+ }
+ }
+
+ @Test
+ fun dragZonesForBubble_foldable_landscape() {
+ dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
+ val expectedZones: List<Class<out DragZone>> =
+ listOf(
+ DragZone.Dismiss::class.java,
+ DragZone.Bubble.Left::class.java,
+ DragZone.Bubble.Right::class.java,
+ DragZone.FullScreen::class.java,
+ DragZone.Split.Top::class.java,
+ DragZone.Split.Bottom::class.java,
+ )
+ dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
+ assertThat(zone).isInstanceOf(expectedType)
+ }
+ }
+
+ @Test
+ fun dragZonesForExpandedView_tablet_portrait() {
+ dragZoneFactory =
+ DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(
+ DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
+ )
+ val expectedZones: List<Class<out DragZone>> =
+ listOf(
+ DragZone.Dismiss::class.java,
+ DragZone.FullScreen::class.java,
+ DragZone.DesktopWindow::class.java,
+ DragZone.Split.Top::class.java,
+ DragZone.Split.Bottom::class.java,
+ DragZone.Bubble.Left::class.java,
+ DragZone.Bubble.Right::class.java,
+ )
+ dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
+ assertThat(zone).isInstanceOf(expectedType)
+ }
+ }
+
+ @Test
+ fun dragZonesForExpandedView_tablet_landscape() {
+ dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
+ val expectedZones: List<Class<out DragZone>> =
+ listOf(
+ DragZone.Dismiss::class.java,
+ DragZone.FullScreen::class.java,
+ DragZone.DesktopWindow::class.java,
+ DragZone.Split.Left::class.java,
+ DragZone.Split.Right::class.java,
+ DragZone.Bubble.Left::class.java,
+ DragZone.Bubble.Right::class.java,
+ )
+ dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
+ assertThat(zone).isInstanceOf(expectedType)
+ }
+ }
+
+ @Test
+ fun dragZonesForExpandedView_foldable_portrait() {
+ dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
+ val expectedZones: List<Class<out DragZone>> =
+ listOf(
+ DragZone.Dismiss::class.java,
+ DragZone.FullScreen::class.java,
+ DragZone.Split.Left::class.java,
+ DragZone.Split.Right::class.java,
+ DragZone.Bubble.Left::class.java,
+ DragZone.Bubble.Right::class.java,
+ )
+ dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
+ assertThat(zone).isInstanceOf(expectedType)
+ }
+ }
+
+ @Test
+ fun dragZonesForExpandedView_foldable_landscape() {
+ dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
+ val expectedZones: List<Class<out DragZone>> =
+ listOf(
+ DragZone.Dismiss::class.java,
+ DragZone.FullScreen::class.java,
+ DragZone.Split.Top::class.java,
+ DragZone.Split.Bottom::class.java,
+ DragZone.Bubble.Left::class.java,
+ DragZone.Bubble.Right::class.java,
+ )
+ dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
+ assertThat(zone).isInstanceOf(expectedType)
+ }
+ }
+
+ @Test
+ fun dragZonesForBubble_tablet_desktopModeDisabled() {
+ isDesktopWindowModeSupported = false
+ dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
+ assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty()
+ }
+
+ @Test
+ fun dragZonesForExpandedView_tablet_desktopModeDisabled() {
+ isDesktopWindowModeSupported = false
+ dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ val dragZones =
+ dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
+ assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty()
+ }
+}
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 3bc238a812d9..61c287b9633c 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -1232,63 +1232,73 @@ static void AMessageToCryptoInfo(JNIEnv * env, const jobject & obj,
sp<ABuffer> ivBuffer;
CryptoPlugin::Mode mode;
CryptoPlugin::Pattern pattern;
- CHECK(msg->findInt32("mode", (int*)&mode));
- CHECK(msg->findSize("numSubSamples", &numSubSamples));
- CHECK(msg->findBuffer("subSamples", &subSamplesBuffer));
- CHECK(msg->findInt32("encryptBlocks", (int32_t *)&pattern.mEncryptBlocks));
- CHECK(msg->findInt32("skipBlocks", (int32_t *)&pattern.mSkipBlocks));
- CHECK(msg->findBuffer("iv", &ivBuffer));
- CHECK(msg->findBuffer("key", &keyBuffer));
-
- // subsamples
+ CryptoPlugin::SubSample *samplesArray = nullptr;
+ ScopedLocalRef<jbyteArray> keyArray(env, env->NewByteArray(16));
+ ScopedLocalRef<jbyteArray> ivArray(env, env->NewByteArray(16));
+ jboolean isCopy;
+ sp<RefBase> cryptoInfosObj;
+ if (msg->findObject("cryptoInfos", &cryptoInfosObj)) {
+ sp<CryptoInfosWrapper> cryptoInfos((CryptoInfosWrapper*)cryptoInfosObj.get());
+ CHECK(!cryptoInfos->value.empty() && (cryptoInfos->value[0] != nullptr));
+ std::unique_ptr<CodecCryptoInfo> &info = cryptoInfos->value[0];
+ mode = info->mMode;
+ numSubSamples = info->mNumSubSamples;
+ samplesArray = info->mSubSamples;
+ pattern = info->mPattern;
+ if (info->mKey != nullptr) {
+ jbyte * dstKey = env->GetByteArrayElements(keyArray.get(), &isCopy);
+ memcpy(dstKey, info->mKey, 16);
+ env->ReleaseByteArrayElements(keyArray.get(), dstKey, 0);
+ }
+ if (info->mIv != nullptr) {
+ jbyte * dstIv = env->GetByteArrayElements(ivArray.get(), &isCopy);
+ memcpy(dstIv, info->mIv, 16);
+ env->ReleaseByteArrayElements(ivArray.get(), dstIv, 0);
+ }
+ } else {
+ CHECK(msg->findInt32("mode", (int*)&mode));
+ CHECK(msg->findSize("numSubSamples", &numSubSamples));
+ CHECK(msg->findBuffer("subSamples", &subSamplesBuffer));
+ CHECK(msg->findInt32("encryptBlocks", (int32_t *)&pattern.mEncryptBlocks));
+ CHECK(msg->findInt32("skipBlocks", (int32_t *)&pattern.mSkipBlocks));
+ CHECK(msg->findBuffer("iv", &ivBuffer));
+ CHECK(msg->findBuffer("key", &keyBuffer));
+ samplesArray =
+ (CryptoPlugin::SubSample*)(subSamplesBuffer.get()->data());
+ if (keyBuffer.get() != nullptr && keyBuffer->size() > 0) {
+ jbyte * dstKey = env->GetByteArrayElements(keyArray.get(), &isCopy);
+ memcpy(dstKey, keyBuffer->data(), keyBuffer->size());
+ env->ReleaseByteArrayElements(keyArray.get(), dstKey, 0);
+ }
+ if (ivBuffer.get() != nullptr && ivBuffer->size() > 0) {
+ jbyte * dstIv = env->GetByteArrayElements(ivArray.get(), &isCopy);
+ memcpy(dstIv, ivBuffer->data(), ivBuffer->size());
+ env->ReleaseByteArrayElements(ivArray.get(), dstIv, 0);
+ }
+ }
ScopedLocalRef<jintArray> samplesOfEncryptedDataArr(env, env->NewIntArray(numSubSamples));
ScopedLocalRef<jintArray> samplesOfClearDataArr(env, env->NewIntArray(numSubSamples));
- jboolean isCopy;
- jint *dstEncryptedSamples =
- env->GetIntArrayElements(samplesOfEncryptedDataArr.get(), &isCopy);
- jint * dstClearSamples =
- env->GetIntArrayElements(samplesOfClearDataArr.get(), &isCopy);
-
- CryptoPlugin::SubSample * samplesArray =
- (CryptoPlugin::SubSample*)(subSamplesBuffer.get()->data());
-
- for(int i = 0 ; i < numSubSamples ; i++) {
- dstEncryptedSamples[i] = samplesArray[i].mNumBytesOfEncryptedData;
- dstClearSamples[i] = samplesArray[i].mNumBytesOfClearData;
- }
- env->ReleaseIntArrayElements(samplesOfEncryptedDataArr.get(), dstEncryptedSamples, 0);
- env->ReleaseIntArrayElements(samplesOfClearDataArr.get(), dstClearSamples, 0);
- // key and iv
- jbyteArray keyArray = NULL;
- jbyteArray ivArray = NULL;
- if (keyBuffer.get() != nullptr && keyBuffer->size() > 0) {
- keyArray = env->NewByteArray(keyBuffer->size());
- jbyte * dstKey = env->GetByteArrayElements(keyArray, &isCopy);
- memcpy(dstKey, keyBuffer->data(), keyBuffer->size());
- env->ReleaseByteArrayElements(keyArray,dstKey,0);
- }
- if (ivBuffer.get() != nullptr && ivBuffer->size() > 0) {
- ivArray = env->NewByteArray(ivBuffer->size());
- jbyte *dstIv = env->GetByteArrayElements(ivArray, &isCopy);
- memcpy(dstIv, ivBuffer->data(), ivBuffer->size());
- env->ReleaseByteArrayElements(ivArray, dstIv,0);
- }
- // set samples, key and iv
+ if (numSubSamples > 0) {
+ jint *dstEncryptedSamples =
+ env->GetIntArrayElements(samplesOfEncryptedDataArr.get(), &isCopy);
+ jint * dstClearSamples =
+ env->GetIntArrayElements(samplesOfClearDataArr.get(), &isCopy);
+ for(int i = 0 ; i < numSubSamples ; i++) {
+ dstEncryptedSamples[i] = samplesArray[i].mNumBytesOfEncryptedData;
+ dstClearSamples[i] = samplesArray[i].mNumBytesOfClearData;
+ }
+ env->ReleaseIntArrayElements(samplesOfEncryptedDataArr.get(), dstEncryptedSamples, 0);
+ env->ReleaseIntArrayElements(samplesOfClearDataArr.get(), dstClearSamples, 0);
+ }
env->CallVoidMethod(
obj,
gFields.cryptoInfoSetID,
(jint)numSubSamples,
samplesOfClearDataArr.get(),
samplesOfEncryptedDataArr.get(),
- keyArray,
- ivArray,
+ keyArray.get(),
+ ivArray.get(),
mode);
- if (keyArray != NULL) {
- env->DeleteLocalRef(keyArray);
- }
- if (ivArray != NULL) {
- env->DeleteLocalRef(ivArray);
- }
// set pattern
env->CallVoidMethod(
obj,
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index 4b407c50bbd5..af40c647e805 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -73,6 +73,7 @@ public class IllustrationPreference extends Preference implements GroupSectionDi
private boolean mLottieDynamicColor;
private CharSequence mContentDescription;
private boolean mIsTablet;
+ private boolean mIsAnimationPaused;
/**
* Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs.
@@ -143,6 +144,16 @@ public class IllustrationPreference extends Preference implements GroupSectionDi
(FrameLayout) holder.findViewById(R.id.middleground_layout);
final LottieAnimationView illustrationView =
(LottieAnimationView) holder.findViewById(R.id.lottie_view);
+ // Pause and resume animation
+ illustrationFrame.setOnClickListener(v -> {
+ mIsAnimationPaused = !mIsAnimationPaused;
+ if (mIsAnimationPaused) {
+ illustrationView.pauseAnimation();
+ } else {
+ illustrationView.resumeAnimation();
+ }
+ });
+
if (illustrationView != null && !TextUtils.isEmpty(mContentDescription)) {
illustrationView.setContentDescription(mContentDescription);
illustrationView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt
index 13d551aef4c2..a840a6f0476f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt
@@ -39,6 +39,7 @@ import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
@@ -71,6 +72,8 @@ constructor(
return
}
+ val buttonSize = dimensionResource(R.dimen.communal_to_dream_button_size)
+
if (viewModel.shouldShowTooltip) {
Column(
modifier =
@@ -96,7 +99,6 @@ constructor(
}
companion object {
- private val buttonSize = 64.dp
private val tooltipMaxWidth = 350.dp
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
index 34c0bcaca997..ae541dda6eeb 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
@@ -26,8 +26,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.contains
@@ -46,6 +46,28 @@ import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import javax.inject.Inject
+@Composable
+fun ClockView(view: View?, modifier: Modifier = Modifier) {
+ AndroidView(
+ factory = {
+ FrameLayout(it).apply {
+ // Clip nothing. The clock views at times render outside their bounds. Compose does
+ // not clip by default, so only this layer needs clipping to be explicitly disabled.
+ clipChildren = false
+ clipToPadding = false
+ }
+ },
+ update = { parent ->
+ view?.let {
+ parent.removeAllViews()
+ (view.parent as? ViewGroup)?.removeView(view)
+ parent.addView(view)
+ } ?: run { parent.removeAllViews() }
+ },
+ modifier = modifier,
+ )
+}
+
/** Provides small clock and large clock composables for the default clock face. */
class DefaultClockSection
@Inject
@@ -67,14 +89,9 @@ constructor(
if (currentClock?.smallClock?.view == null) {
return
}
- val context = LocalContext.current
- AndroidView(
- factory = { context ->
- FrameLayout(context).apply {
- ensureClockViewExists(checkNotNull(currentClock).smallClock.view)
- }
- },
- update = { it.ensureClockViewExists(checkNotNull(currentClock).smallClock.view) },
+
+ ClockView(
+ checkNotNull(currentClock).smallClock.view,
modifier =
modifier
.height(dimensionResource(R.dimen.small_clock_height))
@@ -116,25 +133,8 @@ constructor(
Element(key = largeClockElementKey, modifier = modifier) {
content {
- AndroidView(
- factory = { context ->
- FrameLayout(context).apply {
- // By default, ViewGroups like FrameLayout clip their children. Turning
- // off the clipping allows the child view to render outside of its
- // bounds - letting the step animation of the clock push the digits out
- // when needed.
- //
- // Note that, in Compose, clipping is actually disabled by default so
- // there's no need to propagate this up the composable hierarchy.
- clipChildren = false
- clipToPadding = false
-
- ensureClockViewExists(checkNotNull(currentClock).largeClock.view)
- }
- },
- update = {
- it.ensureClockViewExists(checkNotNull(currentClock).largeClock.view)
- },
+ ClockView(
+ checkNotNull(currentClock).largeClock.view,
modifier =
Modifier.fillMaxSize()
.burnInAware(
@@ -147,15 +147,6 @@ constructor(
}
}
- private fun FrameLayout.ensureClockViewExists(clockView: View) {
- if (contains(clockView)) {
- return
- }
- removeAllViews()
- (clockView.parent as? ViewGroup)?.removeView(clockView)
- addView(clockView)
- }
-
fun getClockCenteringDistance(): Float {
return Resources.getSystem().displayMetrics.widthPixels / 4f
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
index 6250da379402..4fcb5ca42df2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt
@@ -16,8 +16,6 @@
package com.android.systemui.keyguard.ui.composable.section
-import android.view.View
-import android.view.ViewGroup
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
@@ -26,10 +24,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.systemui.customization.R as customR
@@ -112,21 +110,11 @@ constructor(
) {
Element(key = elementKey, modifier) {
content {
- AndroidView(
- factory = {
- try {
- val view =
- clock.largeClock.layout.views.first {
- it.id == weatherClockElementViewId
- }
- (view.parent as? ViewGroup)?.removeView(view)
- view
- } catch (e: NoSuchElementException) {
- View(it)
- }
+ ClockView(
+ clock.largeClock.layout.views.firstOrNull {
+ it.id == weatherClockElementViewId
},
- update = {},
- modifier = modifier,
+ modifier,
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt
index c73656eb1ec5..f1cc71bc59af 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt
@@ -16,9 +16,9 @@
package com.android.systemui.volume.panel.component.mediaoutput
-import com.android.systemui.volume.panel.component.mediaoutput.domain.MediaOutputAvailabilityCriteria
import com.android.systemui.volume.panel.component.mediaoutput.ui.composable.MediaOutputComponent
import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents
+import com.android.systemui.volume.panel.domain.AlwaysAvailableCriteria
import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria
import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent
import dagger.Binds
@@ -39,6 +39,6 @@ interface MediaOutputModule {
@IntoMap
@StringKey(VolumePanelComponents.MEDIA_OUTPUT)
fun bindComponentAvailabilityCriteria(
- criteria: MediaOutputAvailabilityCriteria
+ criteria: AlwaysAvailableCriteria
): ComponentAvailabilityCriteria
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index ab31286b78b4..b59b4ab34c80 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -145,7 +145,6 @@ open class ClockRegistry(
var isCurrentClock = false
var isClockListChanged = false
for (metadata in knownClocks) {
- isCurrentClock = isCurrentClock || currentClockId == metadata.clockId
val id = metadata.clockId
val info =
availableClocks.concurrentGetOrPut(id, ClockInfo(metadata, null, manager)) {
@@ -156,15 +155,17 @@ open class ClockRegistry(
if (manager != info.manager) {
logger.e({
"Clock Id conflict on attach: " +
- "$str1 is double registered by $str2 and $str3"
+ "$str1 is double registered by $str2 and $str3. " +
+ "Using $str2 since it was attached first."
}) {
str1 = id
- str2 = info.manager.toString()
+ str2 = info.manager?.toString() ?: info.provider?.toString()
str3 = manager.toString()
}
continue
}
+ isCurrentClock = isCurrentClock || currentClockId == metadata.clockId
info.provider = null
}
@@ -197,10 +198,11 @@ open class ClockRegistry(
if (manager != info.manager) {
logger.e({
"Clock Id conflict on load: " +
- "$str1 is double registered by $str2 and $str3"
+ "$str1 is double registered by $str2 and $str3. " +
+ "Using $str2 since it was attached first."
}) {
str1 = id
- str2 = info.manager.toString()
+ str2 = info.manager?.toString() ?: info.provider?.toString()
str3 = manager.toString()
}
manager.unloadPlugin()
@@ -227,10 +229,11 @@ open class ClockRegistry(
if (info?.manager != manager) {
logger.e({
"Clock Id conflict on unload: " +
- "$str1 is double registered by $str2 and $str3"
+ "$str1 is double registered by $str2 and $str3. " +
+ "Using $str2 since it was attached first."
}) {
str1 = id
- str2 = info?.manager.toString()
+ str2 = info?.manager?.toString() ?: info?.provider?.toString()
str3 = manager.toString()
}
continue
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
index 9fb60c75b046..98cf68468151 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -47,7 +47,7 @@ class ComposedDigitalLayerController(private val clockCtx: ClockContext) :
init {
fun createController(cfg: LayerConfig) {
- val controller = SimpleDigitalHandLayerController(clockCtx, cfg)
+ val controller = SimpleDigitalHandLayerController(clockCtx, cfg, isLargeClock = true)
view.addView(controller.view)
layerControllers.add(controller)
}
@@ -55,31 +55,20 @@ class ComposedDigitalLayerController(private val clockCtx: ClockContext) :
val layerCfg =
LayerConfig(
style = FontTextStyle(lineHeight = 147.25f),
+ timespec = DigitalTimespec.DIGIT_PAIR,
+ alignment = DigitalAlignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER),
aodStyle =
FontTextStyle(
transitionInterpolator = Interpolators.EMPHASIZED,
transitionDuration = 750,
),
- alignment =
- DigitalAlignment(HorizontalAlignment.CENTER, VerticalAlignment.BASELINE),
- // Placeholders
- timespec = DigitalTimespec.TIME_FULL_FORMAT,
+ // Placeholder
dateTimeFormat = "hh:mm",
)
- createController(
- layerCfg.copy(timespec = DigitalTimespec.FIRST_DIGIT, dateTimeFormat = "hh")
- )
- createController(
- layerCfg.copy(timespec = DigitalTimespec.SECOND_DIGIT, dateTimeFormat = "hh")
- )
- createController(
- layerCfg.copy(timespec = DigitalTimespec.FIRST_DIGIT, dateTimeFormat = "mm")
- )
- createController(
- layerCfg.copy(timespec = DigitalTimespec.SECOND_DIGIT, dateTimeFormat = "mm")
- )
+ createController(layerCfg.copy(dateTimeFormat = "hh"))
+ createController(layerCfg.copy(dateTimeFormat = "mm"))
}
private fun refreshTime() {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 6cc281ace481..af9f2ce9d73f 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -17,21 +17,21 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Typeface
import android.view.LayoutInflater
-import com.android.systemui.animation.GSFAxes
import com.android.systemui.customization.R
import com.android.systemui.log.core.MessageBuffer
import com.android.systemui.plugins.clocks.ClockController
-import com.android.systemui.plugins.clocks.ClockFontAxis
-import com.android.systemui.plugins.clocks.ClockFontAxisSetting
+import com.android.systemui.plugins.clocks.ClockFontAxis.Companion.merge
import com.android.systemui.plugins.clocks.ClockLogger
import com.android.systemui.plugins.clocks.ClockMessageBuffers
import com.android.systemui.plugins.clocks.ClockMetadata
import com.android.systemui.plugins.clocks.ClockPickerConfig
import com.android.systemui.plugins.clocks.ClockProvider
import com.android.systemui.plugins.clocks.ClockSettings
+import com.android.systemui.shared.clocks.FlexClockController.Companion.getDefaultAxes
private val TAG = DefaultClockProvider::class.simpleName
const val DEFAULT_CLOCK_ID = "DEFAULT"
+const val FLEX_CLOCK_ID = "DIGITAL_CLOCK_FLEX"
data class ClockContext(
val context: Context,
@@ -55,16 +55,20 @@ class DefaultClockProvider(
messageBuffers = buffers
}
- override fun getClocks(): List<ClockMetadata> = listOf(ClockMetadata(DEFAULT_CLOCK_ID))
+ override fun getClocks(): List<ClockMetadata> {
+ var clocks = listOf(ClockMetadata(DEFAULT_CLOCK_ID))
+ if (isClockReactiveVariantsEnabled) clocks += ClockMetadata(FLEX_CLOCK_ID)
+ return clocks
+ }
override fun createClock(settings: ClockSettings): ClockController {
- if (settings.clockId != DEFAULT_CLOCK_ID) {
+ if (getClocks().all { it.clockId != settings.clockId }) {
throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
}
return if (isClockReactiveVariantsEnabled) {
val buffers = messageBuffers ?: ClockMessageBuffers(ClockLogger.DEFAULT_MESSAGE_BUFFER)
- val fontAxes = ClockFontAxis.merge(FlexClockController.FONT_AXES, settings.axes)
+ val fontAxes = getDefaultAxes(settings).merge(settings.axes)
val clockSettings = settings.copy(axes = fontAxes.map { it.toSetting() })
val typefaceCache =
TypefaceCache(buffers.infraMessageBuffer, NUM_CLOCK_FONT_ANIMATION_STEPS) {
@@ -86,15 +90,15 @@ class DefaultClockProvider(
}
override fun getClockPickerConfig(settings: ClockSettings): ClockPickerConfig {
- if (settings.clockId != DEFAULT_CLOCK_ID) {
+ if (getClocks().all { it.clockId != settings.clockId }) {
throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
}
val fontAxes =
if (!isClockReactiveVariantsEnabled) listOf()
- else ClockFontAxis.merge(FlexClockController.FONT_AXES, settings.axes)
+ else getDefaultAxes(settings).merge(settings.axes)
return ClockPickerConfig(
- DEFAULT_CLOCK_ID,
+ settings.clockId ?: DEFAULT_CLOCK_ID,
resources.getString(R.string.clock_default_name),
resources.getString(R.string.clock_default_description),
resources.getDrawable(R.drawable.clock_default_thumbnail, null),
@@ -106,23 +110,6 @@ class DefaultClockProvider(
companion object {
const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30
- // TODO(b/364681643): Variations for retargetted DIGITAL_CLOCK_FLEX
- val LEGACY_FLEX_LS_VARIATION =
- listOf(
- ClockFontAxisSetting(GSFAxes.WEIGHT, 600f),
- ClockFontAxisSetting(GSFAxes.WIDTH, 100f),
- ClockFontAxisSetting(GSFAxes.ROUND, 100f),
- ClockFontAxisSetting(GSFAxes.SLANT, 0f),
- )
-
- val LEGACY_FLEX_AOD_VARIATION =
- listOf(
- ClockFontAxisSetting(GSFAxes.WEIGHT, 74f),
- ClockFontAxisSetting(GSFAxes.WIDTH, 43f),
- ClockFontAxisSetting(GSFAxes.ROUND, 100f),
- ClockFontAxisSetting(GSFAxes.SLANT, 0f),
- )
-
val FLEX_TYPEFACE by lazy {
// TODO(b/364680873): Move constant to config_clockFontFamily when shipping
Typeface.create("google-sans-flex-clock", Typeface.NORMAL)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index cc3769e0a568..004d1aa1fe93 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -24,7 +24,9 @@ import com.android.systemui.plugins.clocks.ClockConfig
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockEvents
import com.android.systemui.plugins.clocks.ClockFontAxis
+import com.android.systemui.plugins.clocks.ClockFontAxis.Companion.merge
import com.android.systemui.plugins.clocks.ClockFontAxisSetting
+import com.android.systemui.plugins.clocks.ClockSettings
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.shared.clocks.view.FlexClockView
@@ -94,7 +96,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
}
override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
- val fontAxes = ClockFontAxis.merge(FONT_AXES, axes).map { it.toSetting() }
+ val fontAxes = getDefaultAxes(clockCtx.settings).merge(axes).map { it.toSetting() }
smallClock.events.onFontAxesChanged(fontAxes)
largeClock.events.onFontAxesChanged(fontAxes)
}
@@ -120,7 +122,13 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
override fun dump(pw: PrintWriter) {}
companion object {
- val FONT_AXES =
+ fun getDefaultAxes(settings: ClockSettings): List<ClockFontAxis> {
+ return if (settings.clockId == FLEX_CLOCK_ID) {
+ FONT_AXES.merge(LEGACY_FLEX_SETTINGS)
+ } else FONT_AXES
+ }
+
+ private val FONT_AXES =
listOf(
ClockFontAxis(
key = GSFAxes.WEIGHT,
@@ -135,7 +143,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
key = GSFAxes.WIDTH,
type = AxisType.Float,
minValue = 25f,
- currentValue = 100f,
+ currentValue = 85f,
maxValue = 151f,
name = "Width",
description = "Glyph Width",
@@ -159,5 +167,13 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
description = "Glyph Slant",
),
)
+
+ private val LEGACY_FLEX_SETTINGS =
+ listOf(
+ ClockFontAxisSetting(GSFAxes.WEIGHT, 600f),
+ ClockFontAxisSetting(GSFAxes.WIDTH, 100f),
+ ClockFontAxisSetting(GSFAxes.ROUND, 100f),
+ ClockFontAxisSetting(GSFAxes.SLANT, 0f),
+ )
}
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index d7d8d28a71e0..cfcf201796da 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -60,7 +60,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock:
init {
layerController =
if (isLargeClock) ComposedDigitalLayerController(clockCtx)
- else SimpleDigitalHandLayerController(clockCtx, SMALL_LAYER_CONFIG)
+ else SimpleDigitalHandLayerController(clockCtx, SMALL_LAYER_CONFIG, isLargeClock)
layerController.view.layoutParams =
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT).apply { gravity = Gravity.CENTER }
@@ -148,21 +148,6 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock:
* keyguard_large_clock_top_margin from default clock
*/
override fun onTargetRegionChanged(targetRegion: Rect?) {
- // When a clock needs to be aligned with screen, like weather clock
- // it needs to offset back the translation of keyguard_large_clock_top_margin
- if (isLargeClock && (view as FlexClockView).isAlignedWithScreen()) {
- val topMargin = keyguardLargeClockTopMargin
- targetRegion?.let {
- val (_, yDiff) = computeLayoutDiff(view, it, isLargeClock)
- // In LS, we use yDiff to counter translate
- // the translation of KeyguardLargeClockTopMargin
- // With the targetRegion passed from picker,
- // we will have yDiff = 0, no translation is needed for weather clock
- if (yDiff.toInt() != 0) view.translationY = yDiff - topMargin / 2
- }
- return
- }
-
var maxWidth = 0f
var maxHeight = 0f
@@ -231,7 +216,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock:
}
override fun onPickerCarouselSwiping(swipingFraction: Float) {
- if (isLargeClock && !(view as FlexClockView).isAlignedWithScreen()) {
+ if (isLargeClock) {
view.translationY = keyguardLargeClockTopMargin / 2F * swipingFraction
}
layerController.animations.onPickerCarouselSwiping(swipingFraction)
@@ -251,6 +236,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock:
companion object {
val SMALL_CLOCK_MAX_WDTH = 120f
+
val SMALL_LAYER_CONFIG =
LayerConfig(
timespec = DigitalTimespec.TIME_FULL_FORMAT,
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
index 82fc35012dbc..1659814b74eb 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -71,6 +71,7 @@ data class FontTextStyle(
enum class DigitalTimespec {
TIME_FULL_FORMAT,
+ DIGIT_PAIR,
FIRST_DIGIT,
SECOND_DIGIT,
}
@@ -78,8 +79,9 @@ enum class DigitalTimespec {
open class SimpleDigitalHandLayerController(
private val clockCtx: ClockContext,
private val layerCfg: LayerConfig,
+ isLargeClock: Boolean,
) : SimpleClockLayerController {
- override val view = SimpleDigitalClockTextView(clockCtx)
+ override val view = SimpleDigitalClockTextView(clockCtx, isLargeClock)
private val logger = Logger(clockCtx.messageBuffer, TAG)
val timespec = DigitalTimespecHandler(layerCfg.timespec, layerCfg.dateTimeFormat)
@@ -120,6 +122,28 @@ open class SimpleDigitalHandLayerController(
}
}
+ private fun applyLayout() {
+ // TODO: Remove NO-OP
+ if (view.layoutParams is RelativeLayout.LayoutParams) {
+ val lp = view.layoutParams as RelativeLayout.LayoutParams
+ lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER)
+ when (view.id) {
+ R.id.HOUR_DIGIT_PAIR -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.ALIGN_PARENT_START)
+ }
+ R.id.MINUTE_DIGIT_PAIR -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR)
+ }
+ else -> {
+ throw Exception("cannot apply two pairs layout to view ${view.id}")
+ }
+ }
+ view.layoutParams = lp
+ }
+ }
+
override val events =
object : ClockEvents {
override var isReactiveTouchInteractionEnabled = false
@@ -154,6 +178,7 @@ open class SimpleDigitalHandLayerController(
override val animations =
object : ClockAnimations {
override fun enter() {
+ applyLayout()
refreshTime()
}
@@ -169,6 +194,7 @@ open class SimpleDigitalHandLayerController(
}
override fun fold(fraction: Float) {
+ applyLayout()
refreshTime()
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
index 37db783aba53..8b3b92921ee0 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
@@ -106,19 +106,16 @@ class DigitalTimespecHandler(
)
}
- private fun getSingleDigit(): String {
- val isFirstDigit = timespec == DigitalTimespec.FIRST_DIGIT
+ private fun getSingleDigit(offset: Int): String {
val text = dateFormat.format(cal.time).toString()
- return text.substring(
- if (isFirstDigit) 0 else text.length - 1,
- if (isFirstDigit) text.length - 1 else text.length,
- )
+ return text.substring(offset, offset + 1)
}
fun getDigitString(): String {
return when (timespec) {
- DigitalTimespec.FIRST_DIGIT,
- DigitalTimespec.SECOND_DIGIT -> getSingleDigit()
+ DigitalTimespec.FIRST_DIGIT -> getSingleDigit(0)
+ DigitalTimespec.SECOND_DIGIT -> getSingleDigit(1)
+ DigitalTimespec.DIGIT_PAIR -> dateFormat.format(cal.time).toString()
DigitalTimespec.TIME_FULL_FORMAT -> dateFormat.format(cal.time).toString()
}
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index 55750b5e0925..f0f344a605a9 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -87,7 +87,7 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
protected fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? {
maxSingleDigitSize = Point(-1, -1)
- val bottomLocation: (textView: SimpleDigitalClockTextView) -> Int = { textView ->
+ val viewHeight: (textView: SimpleDigitalClockTextView) -> Int = { textView ->
if (isMonoVerticalNumericLineSpacing) {
maxSingleDigitSize.y
} else {
@@ -98,9 +98,15 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
digitalClockTextViewMap.forEach { (_, textView) ->
textView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
maxSingleDigitSize.x = max(maxSingleDigitSize.x, textView.measuredWidth)
- maxSingleDigitSize.y = max(bottomLocation(textView), textView.measuredHeight)
+ maxSingleDigitSize.y = max(viewHeight(textView), textView.measuredHeight)
}
aodTranslate = Point(0, 0)
+ // TODO(b/364680879): Cleanup
+ /*
+ aodTranslate = Point(
+ (maxSingleDigitSize.x * AOD_HORIZONTAL_TRANSLATE_RATIO).toInt(),
+ (maxSingleDigitSize.y * AOD_VERTICAL_TRANSLATE_RATIO).toInt())
+ */
return Point(
((maxSingleDigitSize.x + abs(aodTranslate.x)) * 2),
((maxSingleDigitSize.y + abs(aodTranslate.y)) * 2),
@@ -112,6 +118,10 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0)
digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y)
digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitSize)
+ digitLeftTopMap[R.id.HOUR_DIGIT_PAIR] = Point(maxSingleDigitSize.x / 2, 0)
+ // Add a small vertical buffer for the second digit pair
+ digitLeftTopMap[R.id.MINUTE_DIGIT_PAIR] =
+ Point(maxSingleDigitSize.x / 2, (maxSingleDigitSize.y * 1.05f).toInt())
digitLeftTopMap.forEach { (_, point) ->
point.x += abs(aodTranslate.x)
point.y += abs(aodTranslate.y)
@@ -179,9 +189,9 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
// save canvas location in anticipation of restoration later
canvas.save()
val xTranslateAmount =
- digitOffsets.getOrDefault(id, 0f) + digitLeftTopMap[id]!!.x.toFloat()
+ digitOffsets.getOrDefault(id, 0f) + (digitLeftTopMap[id]?.x?.toFloat() ?: 0f)
// move canvas to location that the textView would like
- canvas.translate(xTranslateAmount, digitLeftTopMap[id]!!.y.toFloat())
+ canvas.translate(xTranslateAmount, digitLeftTopMap[id]?.y?.toFloat() ?: 0f)
// draw the textView at the location of the canvas above
textView.draw(canvas)
// reset the canvas location back to 0 without drawing
@@ -189,8 +199,6 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
}
}
- fun isAlignedWithScreen(): Boolean = false
-
fun onLocaleChanged(locale: Locale) {
updateLocale(locale)
requestLayout()
@@ -302,23 +310,17 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
clockMoveDirection: Int,
moveFraction: Float,
) {
+ // TODO(b/393577936): The step animation isn't correct with the two pairs approach
val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0
// The sign of moveAmountDeltaForDigit is already set here
// we can interpret (left - clockStartLeft) as (destinationPosition - originPosition)
// so we no longer need to multiply direct sign to moveAmountDeltaForDigit
val currentMoveAmount = left - clockStartLeft
- for (i in 0 until NUM_DIGITS) {
- val mapIndexToId =
- when (i) {
- 0 -> R.id.HOUR_FIRST_DIGIT
- 1 -> R.id.HOUR_SECOND_DIGIT
- 2 -> R.id.MINUTE_FIRST_DIGIT
- 3 -> R.id.MINUTE_SECOND_DIGIT
- else -> -1
- }
+ var index = 0
+ digitalClockTextViewMap.forEach { id, _ ->
val digitFraction =
getDigitFraction(
- digit = i,
+ digit = index++,
isMovingToCenter = isMovingToCenter,
fraction = moveFraction,
)
@@ -326,7 +328,7 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
val moveAmountForDigit = currentMoveAmount * digitFraction
var moveAmountDeltaForDigit = moveAmountForDigit - currentMoveAmount
if (isMovingToCenter && moveAmountForDigit < 0) moveAmountDeltaForDigit *= -1
- digitOffsets[mapIndexToId] = moveAmountDeltaForDigit
+ digitOffsets[id] = moveAmountDeltaForDigit
invalidate()
}
}
@@ -347,7 +349,8 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
/* rangeMin= */ 0.0f,
/* rangeMax= */ 1.0f,
/* valueMin= */ digitInitialDelay,
- /* valueMax= */ digitInitialDelay + AVAILABLE_ANIMATION_TIME,
+ /* valueMax= */ digitInitialDelay +
+ availableAnimationTime(digitalClockTextViewMap.size),
/* value= */ fraction,
)
)
@@ -357,12 +360,8 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
val AOD_TRANSITION_DURATION = 750L
val CHARGING_TRANSITION_DURATION = 300L
- // Calculate the positions of all of the digits...
- // Offset each digit by, say, 0.1
- // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should
- // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3
- // from 0.3 - 1.0.
- private const val NUM_DIGITS = 4
+ val AOD_HORIZONTAL_TRANSLATE_RATIO = -0.15F
+ val AOD_VERTICAL_TRANSLATE_RATIO = 0.075F
// Delays. Each digit's animation should have a slight delay, so we get a nice
// "stepping" effect. When moving right, the second digit of the hour should move first.
@@ -387,7 +386,9 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
// Total available transition time for each digit, taking into account the step. If step is
// 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7.
- private const val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1)
+ private fun availableAnimationTime(numDigits: Int): Float {
+ return 1.0f - MOVE_DIGIT_STEP * (numDigits.toFloat() - 1)
+ }
// Add language tags below that do not have vertically mono spaced numerals
private val NON_MONO_VERTICAL_NUMERIC_LINE_SPACING_LANGUAGES =
@@ -415,6 +416,14 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
outPoint.x *= 1
outPoint.y *= 1
}
+ R.id.HOUR_DIGIT_PAIR -> {
+ outPoint.x *= -1
+ outPoint.y *= -1
+ }
+ R.id.MINUTE_DIGIT_PAIR -> {
+ outPoint.x *= -1
+ outPoint.y *= 1
+ }
}
return outPoint
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index db39162205b2..13f563389e19 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -38,10 +38,13 @@ import com.android.systemui.animation.GSFAxes
import com.android.systemui.animation.TextAnimator
import com.android.systemui.customization.R
import com.android.systemui.plugins.clocks.ClockFontAxisSetting
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.replace
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.toFVar
import com.android.systemui.plugins.clocks.ClockLogger
import com.android.systemui.shared.clocks.ClockContext
import com.android.systemui.shared.clocks.DigitTranslateAnimator
import com.android.systemui.shared.clocks.DimensionParser
+import com.android.systemui.shared.clocks.FLEX_CLOCK_ID
import com.android.systemui.shared.clocks.FontTextStyle
import java.lang.Thread
import kotlin.math.max
@@ -63,14 +66,32 @@ enum class HorizontalAlignment {
}
@SuppressLint("AppCompatCustomView")
-open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSet? = null) :
- TextView(clockCtx.context, attrs) {
+open class SimpleDigitalClockTextView(
+ clockCtx: ClockContext,
+ isLargeClock: Boolean,
+ attrs: AttributeSet? = null,
+) : TextView(clockCtx.context, attrs) {
val lockScreenPaint = TextPaint()
lateinit var textStyle: FontTextStyle
lateinit var aodStyle: FontTextStyle
- private var lsFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_LS_VARIATION)
- private var aodFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_AOD_VARIATION)
+ private val isLegacyFlex = clockCtx.settings.clockId == FLEX_CLOCK_ID
+ private val fixedAodAxes =
+ when {
+ !isLegacyFlex -> listOf(AOD_WEIGHT_AXIS, WIDTH_AXIS)
+ isLargeClock -> listOf(FLEX_AOD_LARGE_WEIGHT_AXIS, FLEX_AOD_WIDTH_AXIS)
+ else -> listOf(FLEX_AOD_SMALL_WEIGHT_AXIS, FLEX_AOD_WIDTH_AXIS)
+ }
+
+ private var lsFontVariation =
+ if (!isLegacyFlex) listOf(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS).toFVar()
+ else listOf(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS).toFVar()
+
+ private var aodFontVariation = run {
+ val roundAxis = if (!isLegacyFlex) ROUND_AXIS else FLEX_ROUND_AXIS
+ (fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar()
+ }
+
private val parser = DimensionParser(clockCtx.context)
var maxSingleDigitHeight = -1
var maxSingleDigitWidth = -1
@@ -129,8 +150,14 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
invalidate()
}
- fun updateAxes(axes: List<ClockFontAxisSetting>) {
- lsFontVariation = ClockFontAxisSetting.toFVar(axes + OPTICAL_SIZE_AXIS)
+ fun updateAxes(lsAxes: List<ClockFontAxisSetting>) {
+ lsFontVariation = lsAxes.toFVar()
+ aodFontVariation = lsAxes.replace(fixedAodAxes).toFVar()
+ logger.i({ "updateAxes(LS = $str1, AOD = $str2)" }) {
+ str1 = lsFontVariation
+ str2 = aodFontVariation
+ }
+
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
typeface = lockScreenPaint.typeface
@@ -287,6 +314,7 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
targetTextBounds,
)
}
+
if (layout == null) {
requestLayout()
} else {
@@ -501,22 +529,18 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
Paint().also { it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) }
val AOD_COLOR = Color.WHITE
- val OPTICAL_SIZE_AXIS = ClockFontAxisSetting(GSFAxes.OPTICAL_SIZE, 144f)
- val DEFAULT_LS_VARIATION =
- listOf(
- OPTICAL_SIZE_AXIS,
- ClockFontAxisSetting(GSFAxes.WEIGHT, 400f),
- ClockFontAxisSetting(GSFAxes.WIDTH, 100f),
- ClockFontAxisSetting(GSFAxes.ROUND, 0f),
- ClockFontAxisSetting(GSFAxes.SLANT, 0f),
- )
- val DEFAULT_AOD_VARIATION =
- listOf(
- OPTICAL_SIZE_AXIS,
- ClockFontAxisSetting(GSFAxes.WEIGHT, 200f),
- ClockFontAxisSetting(GSFAxes.WIDTH, 100f),
- ClockFontAxisSetting(GSFAxes.ROUND, 0f),
- ClockFontAxisSetting(GSFAxes.SLANT, 0f),
- )
+ val LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 400f)
+ val AOD_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 200f)
+ val WIDTH_AXIS = ClockFontAxisSetting(GSFAxes.WIDTH, 85f)
+ val ROUND_AXIS = ClockFontAxisSetting(GSFAxes.ROUND, 0f)
+ val SLANT_AXIS = ClockFontAxisSetting(GSFAxes.SLANT, 0f)
+
+ // Axes for Legacy version of the Flex Clock
+ val FLEX_LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 600f)
+ val FLEX_AOD_LARGE_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 74f)
+ val FLEX_AOD_SMALL_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 133f)
+ val FLEX_LS_WIDTH_AXIS = ClockFontAxisSetting(GSFAxes.WIDTH, 100f)
+ val FLEX_AOD_WIDTH_AXIS = ClockFontAxisSetting(GSFAxes.WIDTH, 43f)
+ val FLEX_ROUND_AXIS = ClockFontAxisSetting(GSFAxes.ROUND, 100f)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index 99146f257c12..bd0fb68a9c42 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -25,8 +25,10 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.andSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
import com.android.systemui.keyguard.shared.model.ClockSize
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
@@ -236,6 +238,10 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa
val isContentVisible by collectLastValue(underTest.isContentVisible)
keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null)
+ fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.OCCLUDED,
+ )
runCurrent()
assertThat(isContentVisible).isFalse()
}
@@ -246,12 +252,46 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa
with(kosmos) {
testScope.runTest {
val isContentVisible by collectLastValue(underTest.isContentVisible)
+
+ keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null)
+ fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.OCCLUDED,
+ )
+ runCurrent()
+
+ sceneInteractor.snapToScene(Scenes.Shade, "")
+ runCurrent()
+ assertThat(isContentVisible).isFalse()
+ }
+ }
+
+ @Test
+ fun isContentVisible_whenOccluded_notVisibleInOccluded_visibleInAod() =
+ with(kosmos) {
+ testScope.runTest {
+ val isContentVisible by collectLastValue(underTest.isContentVisible)
keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null)
+ fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.OCCLUDED,
+ )
runCurrent()
sceneInteractor.snapToScene(Scenes.Shade, "")
runCurrent()
assertThat(isContentVisible).isFalse()
+
+ fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.OCCLUDED,
+ KeyguardState.AOD,
+ )
+ runCurrent()
+
+ sceneInteractor.snapToScene(Scenes.Lockscreen, "")
+ runCurrent()
+
+ assertThat(isContentVisible).isTrue()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
index 2a4cba8cffe2..41d7e490ddc2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
@@ -41,7 +41,6 @@ import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.dialog.InternetDialogManager
import com.android.systemui.qs.tiles.dialog.WifiStateWorker
import com.android.systemui.res.R
-import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.connectivity.AccessPointController
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
@@ -145,7 +144,7 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() {
dialogManager,
wifiStateWorker,
accessPointController,
- internetDetailsViewModelFactory
+ internetDetailsViewModelFactory,
)
underTest.initialize()
@@ -295,7 +294,6 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() {
FLAG_SCENE_CONTAINER,
KeyguardWmStateRefactor.FLAG_NAME,
NotificationThrottleHun.FLAG_NAME,
- DualShade.FLAG_NAME,
]
)
fun click_withQsDetailedViewEnabled() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index a79f4085ec6d..d2ea62da0940 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -29,10 +29,12 @@ import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ShadeInterpolation
import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.res.R
import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shared.Flags as SharedFlags
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.ScrimController
@@ -80,6 +82,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
@Mock private lateinit var blurUtils: BlurUtils
@Mock private lateinit var biometricUnlockController: BiometricUnlockController
@Mock private lateinit var keyguardStateController: KeyguardStateController
+ @Mock private lateinit var keyguardInteractor: KeyguardInteractor
@Mock private lateinit var choreographer: Choreographer
@Mock private lateinit var wallpaperController: WallpaperController
@Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
@@ -123,6 +126,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
blurUtils,
biometricUnlockController,
keyguardStateController,
+ keyguardInteractor,
choreographer,
wallpaperController,
notificationShadeWindowController,
@@ -308,6 +312,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(SharedFlags.FLAG_AMBIENT_AOD)
fun onDozeAmountChanged_appliesBlur() {
statusBarStateListener.onDozeAmountChanged(1f, 1f)
notificationShadeDepthController.updateBlurCallback.doFrame(0)
@@ -315,6 +320,14 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(SharedFlags.FLAG_AMBIENT_AOD)
+ fun onDozeAmountChanged_doesNotApplyBlurWithAmbientAod() {
+ statusBarStateListener.onDozeAmountChanged(1f, 1f)
+ notificationShadeDepthController.updateBlurCallback.doFrame(0)
+ verify(blurUtils).applyBlur(any(), eq(0), eq(false))
+ }
+
+ @Test
fun setFullShadeTransition_appliesBlur_onlyIfSupported() {
reset(blurUtils)
`when`(blurUtils.blurRadiusOfRatio(anyFloat())).then { answer ->
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
index 04a8b5850f37..ee4d0990d38f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -15,6 +15,7 @@
package com.android.systemui.statusbar.notification.icon.domain.interactor
+import android.platform.test.annotations.DisableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -24,6 +25,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.data.repository.notificationListenerSettingsRepository
+import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -62,7 +64,7 @@ class NotificationIconsInteractorTest : SysuiTestCase() {
kosmos.activeNotificationsInteractor,
kosmos.bubblesOptional,
kosmos.headsUpNotificationIconInteractor,
- kosmos.notificationsKeyguardViewStateRepository
+ kosmos.notificationsKeyguardViewStateRepository,
)
@Before
@@ -306,6 +308,7 @@ class StatusBarNotificationIconsInteractorTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME)
fun filteredEntrySet_includesIsolatedIcon() =
testScope.runTest {
val filteredSet by collectLastValue(underTest.statusBarNotifs)
@@ -316,31 +319,11 @@ class StatusBarNotificationIconsInteractorTest : SysuiTestCase() {
private val testIcons =
listOf(
- activeNotificationModel(
- key = "notif1",
- ),
- activeNotificationModel(
- key = "notif2",
- isAmbient = true,
- ),
- activeNotificationModel(
- key = "notif3",
- isRowDismissed = true,
- ),
- activeNotificationModel(
- key = "notif4",
- isSilent = true,
- ),
- activeNotificationModel(
- key = "notif5",
- isLastMessageFromReply = true,
- ),
- activeNotificationModel(
- key = "notif6",
- isSuppressedFromStatusBar = true,
- ),
- activeNotificationModel(
- key = "notif7",
- isPulsing = true,
- ),
+ activeNotificationModel(key = "notif1"),
+ activeNotificationModel(key = "notif2", isAmbient = true),
+ activeNotificationModel(key = "notif3", isRowDismissed = true),
+ activeNotificationModel(key = "notif4", isSilent = true),
+ activeNotificationModel(key = "notif5", isLastMessageFromReply = true),
+ activeNotificationModel(key = "notif6", isSuppressedFromStatusBar = true),
+ activeNotificationModel(key = "notif7", isPulsing = true),
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
index 739636e01f0b..d14ff35f824a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.stack
import android.os.testableLooper
import android.testing.TestableLooper.RunWithLooper
+import androidx.dynamicanimation.animation.SpringForce
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -46,13 +47,14 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
private val childrenNumber = 5
private val stackScrollLayout = mock<NotificationStackScrollLayout>()
private val sectionsManager = mock<NotificationSectionsManager>()
- private val swipedMultiplier = 0.5f
private val msdlPlayer = kosmos.fakeMSDLPlayer
+ private var canRowBeDismissed = true
private val underTest = kosmos.magneticNotificationRowManagerImpl
private lateinit var notificationTestHelper: NotificationTestHelper
private lateinit var children: NotificationChildrenContainer
+ private lateinit var swipedRow: ExpandableNotificationRow
@Before
fun setUp() {
@@ -60,14 +62,15 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
notificationTestHelper =
NotificationTestHelper(mContext, mDependency, kosmos.testableLooper, featureFlags)
children = notificationTestHelper.createGroup(childrenNumber).childrenContainer
+ swipedRow = children.attachedChildren[childrenNumber / 2]
+ configureMagneticRowListener(swipedRow)
}
@Test
fun setMagneticAndRoundableTargets_onIdle_targetsGetSet() =
kosmos.testScope.runTest {
// WHEN the targets are set for a row
- val row = children.attachedChildren[childrenNumber / 2]
- setTargetsForRow(row)
+ setTargets()
// THEN the magnetic and roundable targets are defined and the state is TARGETS_SET
assertThat(underTest.currentState).isEqualTo(State.TARGETS_SET)
@@ -79,11 +82,10 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
fun setMagneticRowTranslation_whenTargetsAreSet_startsPulling() =
kosmos.testScope.runTest {
// GIVEN targets are set
- val row = children.attachedChildren[childrenNumber / 2]
- setTargetsForRow(row)
+ setTargets()
// WHEN setting a translation for the swiped row
- underTest.setMagneticRowTranslation(row, translation = 100f)
+ underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
// THEN the state moves to PULLING
assertThat(underTest.currentState).isEqualTo(State.PULLING)
@@ -107,8 +109,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
fun setMagneticRowTranslation_whenRowIsNotSwiped_doesNotSetMagneticTranslation() =
kosmos.testScope.runTest {
// GIVEN that targets are set
- val row = children.attachedChildren[childrenNumber / 2]
- setTargetsForRow(row)
+ setTargets()
// WHEN setting a translation for a row that is not being swiped
val differentRow = children.attachedChildren[childrenNumber / 2 - 1]
@@ -120,41 +121,61 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
}
@Test
- fun setMagneticRowTranslation_belowThreshold_whilePulling_setsMagneticTranslations() =
+ fun setMagneticRowTranslation_whenDismissible_belowThreshold_whenPulling_setsTranslations() =
kosmos.testScope.runTest {
// GIVEN a threshold of 100 px
val threshold = 100f
underTest.setSwipeThresholdPx(threshold)
// GIVEN that targets are set and the rows are being pulled
- val row = children.attachedChildren[childrenNumber / 2]
- setTargetsForRow(row)
- underTest.setMagneticRowTranslation(row, translation = 100f)
+ setTargets()
+ underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
// WHEN setting a translation that will fall below the threshold
- val translation = threshold / swipedMultiplier - 50f
- underTest.setMagneticRowTranslation(row, translation)
+ val translation = threshold / underTest.swipedRowMultiplier - 50f
+ underTest.setMagneticRowTranslation(swipedRow, translation)
// THEN the targets continue to be pulled and translations are set
assertThat(underTest.currentState).isEqualTo(State.PULLING)
- assertThat(row.translation).isEqualTo(swipedMultiplier * translation)
+ assertThat(swipedRow.translation).isEqualTo(underTest.swipedRowMultiplier * translation)
}
@Test
- fun setMagneticRowTranslation_aboveThreshold_whilePulling_detachesMagneticTargets() =
+ fun setMagneticRowTranslation_whenNotDismissible_belowThreshold_whenPulling_setsTranslations() =
kosmos.testScope.runTest {
// GIVEN a threshold of 100 px
val threshold = 100f
underTest.setSwipeThresholdPx(threshold)
// GIVEN that targets are set and the rows are being pulled
- val row = children.attachedChildren[childrenNumber / 2]
- setTargetsForRow(row)
- underTest.setMagneticRowTranslation(row, translation = 100f)
+ canRowBeDismissed = false
+ setTargets()
+ underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
+
+ // WHEN setting a translation that will fall below the threshold
+ val translation = threshold / underTest.swipedRowMultiplier - 50f
+ underTest.setMagneticRowTranslation(swipedRow, translation)
+
+ // THEN the targets continue to be pulled and reduced translations are set
+ val expectedTranslation = getReducedTranslation(translation)
+ assertThat(underTest.currentState).isEqualTo(State.PULLING)
+ assertThat(swipedRow.translation).isEqualTo(expectedTranslation)
+ }
+
+ @Test
+ fun setMagneticRowTranslation_whenDismissible_aboveThreshold_whilePulling_detaches() =
+ kosmos.testScope.runTest {
+ // GIVEN a threshold of 100 px
+ val threshold = 100f
+ underTest.setSwipeThresholdPx(threshold)
+
+ // GIVEN that targets are set and the rows are being pulled
+ setTargets()
+ underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
// WHEN setting a translation that will fall above the threshold
- val translation = threshold / swipedMultiplier + 50f
- underTest.setMagneticRowTranslation(row, translation)
+ val translation = threshold / underTest.swipedRowMultiplier + 50f
+ underTest.setMagneticRowTranslation(swipedRow, translation)
// THEN the swiped view detaches and the correct detach haptics play
assertThat(underTest.currentState).isEqualTo(State.DETACHED)
@@ -162,15 +183,36 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
}
@Test
+ fun setMagneticRowTranslation_whenNotDismissible_aboveThreshold_whilePulling_doesNotDetach() =
+ kosmos.testScope.runTest {
+ // GIVEN a threshold of 100 px
+ val threshold = 100f
+ underTest.setSwipeThresholdPx(threshold)
+
+ // GIVEN that targets are set and the rows are being pulled
+ canRowBeDismissed = false
+ setTargets()
+ underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
+
+ // WHEN setting a translation that will fall above the threshold
+ val translation = threshold / underTest.swipedRowMultiplier + 50f
+ underTest.setMagneticRowTranslation(swipedRow, translation)
+
+ // THEN the swiped view does not detach and the reduced translation is set
+ val expectedTranslation = getReducedTranslation(translation)
+ assertThat(underTest.currentState).isEqualTo(State.PULLING)
+ assertThat(swipedRow.translation).isEqualTo(expectedTranslation)
+ }
+
+ @Test
fun setMagneticRowTranslation_whileDetached_setsTranslationAndStaysDetached() =
kosmos.testScope.runTest {
// GIVEN that the swiped view has been detached
- val row = children.attachedChildren[childrenNumber / 2]
- setDetachedState(row)
+ setDetachedState()
// WHEN setting a new translation
val translation = 300f
- underTest.setMagneticRowTranslation(row, translation)
+ underTest.setMagneticRowTranslation(swipedRow, translation)
// THEN the swiped view continues to be detached
assertThat(underTest.currentState).isEqualTo(State.DETACHED)
@@ -180,14 +222,13 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
fun onMagneticInteractionEnd_whilePulling_goesToIdle() =
kosmos.testScope.runTest {
// GIVEN targets are set
- val row = children.attachedChildren[childrenNumber / 2]
- setTargetsForRow(row)
+ setTargets()
// WHEN setting a translation for the swiped row
- underTest.setMagneticRowTranslation(row, translation = 100f)
+ underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
// WHEN the interaction ends on the row
- underTest.onMagneticInteractionEnd(row, velocity = null)
+ underTest.onMagneticInteractionEnd(swipedRow, velocity = null)
// THEN the state resets
assertThat(underTest.currentState).isEqualTo(State.IDLE)
@@ -197,32 +238,56 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
fun onMagneticInteractionEnd_whileDetached_goesToIdle() =
kosmos.testScope.runTest {
// GIVEN the swiped row is detached
- val row = children.attachedChildren[childrenNumber / 2]
- setDetachedState(row)
+ setDetachedState()
// WHEN the interaction ends on the row
- underTest.onMagneticInteractionEnd(row, velocity = null)
+ underTest.onMagneticInteractionEnd(swipedRow, velocity = null)
// THEN the state resets
assertThat(underTest.currentState).isEqualTo(State.IDLE)
}
- private fun setDetachedState(row: ExpandableNotificationRow) {
+ private fun setDetachedState() {
val threshold = 100f
underTest.setSwipeThresholdPx(threshold)
// Set the pulling state
- setTargetsForRow(row)
- underTest.setMagneticRowTranslation(row, translation = 100f)
+ setTargets()
+ underTest.setMagneticRowTranslation(swipedRow, translation = 100f)
// Set a translation that will fall above the threshold
- val translation = threshold / swipedMultiplier + 50f
- underTest.setMagneticRowTranslation(row, translation)
+ val translation = threshold / underTest.swipedRowMultiplier + 50f
+ underTest.setMagneticRowTranslation(swipedRow, translation)
assertThat(underTest.currentState).isEqualTo(State.DETACHED)
}
- private fun setTargetsForRow(row: ExpandableNotificationRow) {
- underTest.setMagneticAndRoundableTargets(row, stackScrollLayout, sectionsManager)
+ private fun setTargets() {
+ underTest.setMagneticAndRoundableTargets(swipedRow, stackScrollLayout, sectionsManager)
+ }
+
+ private fun getReducedTranslation(originalTranslation: Float) =
+ underTest.swipedRowMultiplier *
+ originalTranslation *
+ MagneticNotificationRowManagerImpl.MAGNETIC_REDUCTION
+
+ private fun configureMagneticRowListener(row: ExpandableNotificationRow) {
+ val listener =
+ object : MagneticRowListener {
+ override fun setMagneticTranslation(translation: Float) {
+ row.translation = translation
+ }
+
+ override fun triggerMagneticForce(
+ endTranslation: Float,
+ springForce: SpringForce,
+ startVelocity: Float,
+ ) {}
+
+ override fun cancelMagneticAnimations() {}
+
+ override fun canRowBeDismissed(): Boolean = canRowBeDismissed
+ }
+ row.magneticRowListener = listener
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt
index 12d122b2fe1d..80e9e36862dd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt
@@ -170,6 +170,41 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
}
@Test
+ fun onNotificationVisibilityChanged_thenShadeNotInteractive_noDuplicateLogs() =
+ testScope.runTest {
+ // GIVEN a visible Notifications is reported
+ val (ranks, locations) = fakeNotificationMaps("key0")
+ val callable = Callable { locations }
+ underTest.onNotificationLocationsChanged(callable, ranks)
+ runCurrent()
+ clearInvocations(mockStatusBarService, mockNotificationListenerService)
+
+ // WHEN the same Notification becomins invisible
+ val emptyCallable = Callable { emptyMap<String, Int>() }
+ underTest.onNotificationLocationsChanged(emptyCallable, ranks)
+ // AND notifications become non interactible
+ underTest.onLockscreenOrShadeNotInteractive(emptyList())
+ runCurrent()
+
+ // THEN visibility changes are reported
+ verify(mockStatusBarService)
+ .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture())
+ val noLongerVisible = visibilityArrayCaptor.value
+ assertThat(noLongerVisible).hasLength(1)
+ assertThat(noLongerVisible[0]).apply {
+ isKeyEqualTo("key0")
+ isRankEqualTo(0)
+ notVisible()
+ isInMainArea()
+ isCountEqualTo(1)
+ }
+
+ // AND nothing else is logged
+ verifyNoMoreInteractions(mockStatusBarService)
+ verifyNoMoreInteractions(mockNotificationListenerService)
+ }
+
+ @Test
fun onNotificationListUpdated_itemsChangedPositions_nothingLogged() =
testScope.runTest {
// GIVEN some visible Notifications are reported
@@ -253,14 +288,14 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
activeNotificationModel(
key = "key0",
uid = 0,
- packageName = "com.android.first"
+ packageName = "com.android.first",
),
activeNotificationModel(
key = "key1",
uid = 1,
- packageName = "com.android.second"
+ packageName = "com.android.second",
),
- )
+ ),
)
runCurrent()
@@ -286,7 +321,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key",
isExpanded = true,
location = ExpandableViewState.LOCATION_MAIN_AREA,
- isUserAction = true
+ isUserAction = true,
)
runCurrent()
@@ -296,7 +331,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
/* key = */ "key",
/* userAction = */ true,
/* expanded = */ true,
- NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+ NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal,
)
}
@@ -308,7 +343,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key",
isExpanded = true,
location = ExpandableViewState.LOCATION_MAIN_AREA,
- isUserAction = true
+ isUserAction = true,
)
runCurrent()
clearInvocations(mockStatusBarService)
@@ -318,7 +353,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key",
isExpanded = true,
location = ExpandableViewState.LOCATION_MAIN_AREA,
- isUserAction = true
+ isUserAction = true,
)
runCurrent()
@@ -334,7 +369,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key",
isExpanded = true,
location = ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN,
- isUserAction = true
+ isUserAction = true,
)
runCurrent()
@@ -350,7 +385,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key",
isExpanded = true,
location = ExpandableViewState.LOCATION_GONE,
- isUserAction = false
+ isUserAction = false,
)
runCurrent()
@@ -366,7 +401,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
/* key = */ "key",
/* userAction = */ false,
/* expanded = */ true,
- NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+ NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal,
)
}
@@ -378,7 +413,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key",
isExpanded = true,
location = ExpandableViewState.LOCATION_GONE,
- isUserAction = false
+ isUserAction = false,
)
runCurrent()
// AND we open the shade, so we log its events
@@ -404,7 +439,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
/* key = */ "key",
/* userAction = */ false,
/* expanded = */ true,
- NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+ NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal,
)
}
@@ -416,7 +451,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key",
isExpanded = false,
location = ExpandableViewState.LOCATION_MAIN_AREA,
- isUserAction = false
+ isUserAction = false,
)
runCurrent()
@@ -433,7 +468,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key",
isExpanded = true,
location = ExpandableViewState.LOCATION_MAIN_AREA,
- isUserAction = true
+ isUserAction = true,
)
runCurrent()
@@ -443,7 +478,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
/* key = */ "key",
/* userAction = */ true,
/* expanded = */ true,
- NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+ NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal,
)
// AND the Notification is expanded again
@@ -451,7 +486,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key",
isExpanded = false,
location = ExpandableViewState.LOCATION_MAIN_AREA,
- isUserAction = true
+ isUserAction = true,
)
runCurrent()
@@ -461,7 +496,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
/* key = */ "key",
/* userAction = */ true,
/* expanded = */ false,
- NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal
+ NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal,
)
}
@@ -473,14 +508,14 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key1",
isExpanded = true,
location = ExpandableViewState.LOCATION_MAIN_AREA,
- isUserAction = true
+ isUserAction = true,
)
runCurrent()
underTest.onNotificationExpansionChanged(
key = "key2",
isExpanded = true,
location = ExpandableViewState.LOCATION_MAIN_AREA,
- isUserAction = true
+ isUserAction = true,
)
runCurrent()
clearInvocations(mockStatusBarService)
@@ -500,14 +535,14 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
key = "key1",
isExpanded = true,
location = ExpandableViewState.LOCATION_MAIN_AREA,
- isUserAction = true
+ isUserAction = true,
)
runCurrent()
underTest.onNotificationExpansionChanged(
key = "key2",
isExpanded = true,
location = ExpandableViewState.LOCATION_MAIN_AREA,
- isUserAction = true
+ isUserAction = true,
)
runCurrent()
clearInvocations(mockStatusBarService)
@@ -535,10 +570,15 @@ class NotificationStatsLoggerTest : SysuiTestCase() {
private class NotificationVisibilitySubject(private val visibility: NotificationVisibility) {
fun isKeyEqualTo(key: String) = assertThat(visibility.key).isEqualTo(key)
+
fun isRankEqualTo(rank: Int) = assertThat(visibility.rank).isEqualTo(rank)
+
fun isCountEqualTo(count: Int) = assertThat(visibility.count).isEqualTo(count)
+
fun isVisible() = assertThat(this.visibility.visible).isTrue()
+
fun notVisible() = assertThat(this.visibility.visible).isFalse()
+
fun isInMainArea() =
assertThat(this.visibility.location)
.isEqualTo(NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index f318c74e0584..7bc2bca3df02 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -43,6 +43,8 @@ import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.emergency.EmergencyGestureModule.EmergencyGestureIntentFactory;
+import com.android.systemui.flags.DisableSceneContainer;
+import com.android.systemui.flags.EnableSceneContainer;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.plugins.ActivityStarter;
@@ -55,10 +57,8 @@ import com.android.systemui.shade.CameraLauncher;
import com.android.systemui.shade.QuickSettingsController;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.shade.ShadeHeaderController;
-import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
-import com.android.systemui.shade.shared.flag.DualShade;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
@@ -67,6 +67,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
import com.android.systemui.wallet.controller.QuickAccessWalletController;
+import dagger.Lazy;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -76,8 +78,6 @@ import org.mockito.stubbing.Answer;
import java.util.Optional;
-import dagger.Lazy;
-
@SmallTest
@RunWith(AndroidJUnit4.class)
public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase {
@@ -87,7 +87,6 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase {
@Mock private ShadeController mShadeController;
@Mock private CommandQueue mCommandQueue;
@Mock private QuickSettingsController mQuickSettingsController;
- @Mock private ShadeViewController mShadeViewController;
@Mock private PanelExpansionInteractor mPanelExpansionInteractor;
@Mock private Lazy<ShadeInteractor> mShadeInteractorLazy;
@Mock private ShadeHeaderController mShadeHeaderController;
@@ -242,7 +241,8 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase {
}
@Test
- @DisableFlags(value = {QSComposeFragment.FLAG_NAME, DualShade.FLAG_NAME})
+ @DisableSceneContainer
+ @DisableFlags(QSComposeFragment.FLAG_NAME)
public void clickQsTile_flagsDisabled_callsQSPanelController() {
ComponentName c = new ComponentName("testpkg", "testcls");
@@ -251,7 +251,7 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase {
}
@Test
- @DisableFlags(DualShade.FLAG_NAME)
+ @DisableSceneContainer
@EnableFlags(QSComposeFragment.FLAG_NAME)
public void clickQsTile_onlyQSComposeFlag_callsQSHost() {
ComponentName c = new ComponentName("testpkg", "testcls");
@@ -262,9 +262,9 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase {
}
@Test
- @EnableFlags(DualShade.FLAG_NAME)
+ @EnableSceneContainer
@DisableFlags(QSComposeFragment.FLAG_NAME)
- public void clickQsTile_onlyDualShadeFlag_callsQSHost() {
+ public void clickQsTile_onlySceneContainerFlag_callsQSHost() {
ComponentName c = new ComponentName("testpkg", "testcls");
mSbcqCallbacks.clickTile(c);
@@ -273,8 +273,9 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase {
}
@Test
- @EnableFlags(value = {QSComposeFragment.FLAG_NAME, DualShade.FLAG_NAME})
- public void clickQsTile_qsComposeAndDualShadeFlags_callsQSHost() {
+ @EnableSceneContainer
+ @EnableFlags(QSComposeFragment.FLAG_NAME)
+ public void clickQsTile_qsComposeAndSceneContainerFlags_callsQSHost() {
ComponentName c = new ComponentName("testpkg", "testcls");
mSbcqCallbacks.clickTile(c);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index 1c40cd06c119..5d51c6d16c5a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -263,7 +263,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() {
guestUserInteractor = guestUserInteractor,
uiEventLogger = uiEventLogger,
userRestrictionChecker = mock(),
- processWrapper = ProcessWrapperFake()
+ processWrapper = ProcessWrapperFake(activityManager)
)
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index e1d1057ea249..8ff088f5d29b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -175,7 +175,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() {
guestUserInteractor = guestUserInteractor,
uiEventLogger = uiEventLogger,
userRestrictionChecker = mock(),
- processWrapper = ProcessWrapperFake()
+ processWrapper = ProcessWrapperFake(activityManager)
),
guestUserInteractor = guestUserInteractor,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt
deleted file mode 100644
index d0cc56860ce8..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.panel.component.mediaoutput.domain
-
-import android.content.mockedContext
-import android.content.packageManager
-import android.content.pm.PackageManager.FEATURE_PC
-import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.collectLastValue
-import com.android.systemui.kosmos.runTest
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-class MediaOutputAvailabilityCriteriaTest : SysuiTestCase() {
-
- private val kosmos = testKosmos()
- private val scope = kosmos.testScope
-
- private lateinit var underTest: MediaOutputAvailabilityCriteria
-
- @Before
- fun setup() {
- with(kosmos) {
- underTest = MediaOutputAvailabilityCriteria(kosmos.mockedContext, scope.backgroundScope)
- }
- }
-
- @Test
- fun isDesktop_unavailable() =
- kosmos.runTest {
- whenever(mockedContext.getPackageManager()).thenReturn(packageManager)
- whenever(packageManager.hasSystemFeature(FEATURE_PC)).thenReturn(true)
-
- val isAvailable by collectLastValue(underTest.isAvailable())
-
- assertThat(isAvailable).isFalse()
- }
-
- @Test
- fun notIsDesktop_available() =
- kosmos.runTest {
- whenever(mockedContext.getPackageManager()).thenReturn(packageManager)
- whenever(packageManager.hasSystemFeature(FEATURE_PC)).thenReturn(false)
-
- val isAvailable by collectLastValue(underTest.isAvailable())
-
- assertThat(isAvailable).isTrue()
- }
-}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
index 1bc9367ce3c5..6e4dc1485c7b 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
@@ -62,12 +62,11 @@ data class ClockFontAxis(
fun toSetting() = ClockFontAxisSetting(key, currentValue)
companion object {
- fun merge(
- fontAxes: List<ClockFontAxis>,
- axisSettings: List<ClockFontAxisSetting>,
+ fun List<ClockFontAxis>.merge(
+ axisSettings: List<ClockFontAxisSetting>
): List<ClockFontAxis> {
val result = mutableListOf<ClockFontAxis>()
- for (axis in fontAxes) {
+ for (axis in this) {
val setting = axisSettings.firstOrNull { axis.key == it.key }
val output = setting?.let { axis.copy(currentValue = it.value) } ?: axis
result.add(output)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt
index 6128c00f3843..e7b36626a810 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt
@@ -98,13 +98,20 @@ data class ClockFontAxisSetting(
return result
}
- fun toFVar(settings: List<ClockFontAxisSetting>): String {
+ fun List<ClockFontAxisSetting>.toFVar(): String {
val sb = StringBuilder()
- for (axis in settings) {
+ for (axis in this) {
if (sb.length > 0) sb.append(", ")
sb.append("'${axis.key}' ${axis.value.toInt()}")
}
return sb.toString()
}
+
+ fun List<ClockFontAxisSetting>.replace(
+ replacements: List<ClockFontAxisSetting>
+ ): List<ClockFontAxisSetting> {
+ var remaining = this.filterNot { lhs -> replacements.any { rhs -> lhs.key == rhs.key } }
+ return remaining + replacements
+ }
}
}
diff --git a/packages/SystemUI/res/layout/media_output_list_group_divider.xml b/packages/SystemUI/res/layout/media_output_list_group_divider.xml
index 5e96866c0a9a..c351912de295 100644
--- a/packages/SystemUI/res/layout/media_output_list_group_divider.xml
+++ b/packages/SystemUI/res/layout/media_output_list_group_divider.xml
@@ -26,7 +26,7 @@
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_gravity="center_vertical|start"
- android:layout_marginStart="16dp"
+ android:layout_marginStart="@dimen/media_output_dialog_margin_horizontal"
android:layout_marginEnd="56dp"
android:ellipsize="end"
android:maxLines="1"
diff --git a/packages/SystemUI/res/layout/media_output_list_item_advanced.xml b/packages/SystemUI/res/layout/media_output_list_item_advanced.xml
index 69117cf7cf5d..d297ec46e1e1 100644
--- a/packages/SystemUI/res/layout/media_output_list_item_advanced.xml
+++ b/packages/SystemUI/res/layout/media_output_list_item_advanced.xml
@@ -15,18 +15,19 @@
~ limitations under the License.
-->
-<FrameLayout
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/device_container"
android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/media_output_dialog_margin_horizontal"
+ android:baselineAligned="false">
<FrameLayout
- android:layout_width="match_parent"
- android:layout_height="64dp"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/media_output_dialog_item_height"
android:id="@+id/item_layout"
android:background="@drawable/media_output_item_background"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="80dp"
android:layout_marginBottom="12dp">
<FrameLayout
android:layout_width="match_parent"
@@ -36,7 +37,7 @@
android:id="@+id/volume_seekbar"
android:splitTrack="false"
android:visibility="gone"
- android:paddingStart="64dp"
+ android:paddingStart="@dimen/media_output_dialog_item_height"
android:paddingEnd="0dp"
android:background="@null"
android:contentDescription="@string/media_output_dialog_accessibility_seekbar"
@@ -48,8 +49,8 @@
<FrameLayout
android:id="@+id/icon_area"
- android:layout_width="64dp"
- android:layout_height="64dp"
+ android:layout_width="@dimen/media_output_dialog_item_height"
+ android:layout_height="@dimen/media_output_dialog_item_height"
android:focusable="false"
android:importantForAccessibility="no"
android:layout_gravity="center_vertical|start">
@@ -131,11 +132,11 @@
</FrameLayout>
<FrameLayout
android:id="@+id/end_action_area"
- android:layout_width="64dp"
- android:layout_height="64dp"
+ android:layout_width="@dimen/media_output_dialog_item_height"
+ android:layout_height="@dimen/media_output_dialog_item_height"
android:visibility="gone"
android:layout_marginBottom="6dp"
- android:layout_marginEnd="8dp"
+ android:layout_marginStart="7dp"
android:layout_gravity="end|center"
android:gravity="center"
android:background="@drawable/media_output_item_background_active">
@@ -160,4 +161,4 @@
android:indeterminateOnly="true"
android:visibility="gone"/>
</FrameLayout>
-</FrameLayout> \ No newline at end of file
+</LinearLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
index 7f7350472fa5..cb7bd1728077 100644
--- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml
+++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
@@ -23,6 +23,7 @@
android:orientation="vertical"
android:padding="@dimen/magnification_setting_background_padding"
android:focusable="true"
+ android:accessibilityPaneTitle="@string/accessibility_magnification_settings_panel_description"
android:contentDescription="@string/accessibility_magnification_settings_panel_description">
<LinearLayout
android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/values-ldrtl/dimens.xml b/packages/SystemUI/res/values-ldrtl/dimens.xml
index 0d99b617819b..345f0414e637 100644
--- a/packages/SystemUI/res/values-ldrtl/dimens.xml
+++ b/packages/SystemUI/res/values-ldrtl/dimens.xml
@@ -16,5 +16,5 @@
-->
<resources>
<dimen name="media_output_dialog_icon_left_radius">0dp</dimen>
- <dimen name="media_output_dialog_icon_right_radius">28dp</dimen>
+ <dimen name="media_output_dialog_icon_right_radius">@dimen/media_output_dialog_active_background_radius</dimen>
</resources> \ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 26f32ef60851..d22c8d910230 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -126,4 +126,6 @@
<dimen name="controls_content_padding">24dp</dimen>
<dimen name="control_list_vertical_spacing">8dp</dimen>
<dimen name="control_list_horizontal_spacing">16dp</dimen>
+
+ <dimen name="communal_to_dream_button_size">64dp</dimen>
</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a96ebe7b4fd6..d93716b03685 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -953,6 +953,8 @@
<dimen name="communal_widget_picker_desired_width">360dp</dimen>
<dimen name="communal_widget_picker_desired_height">240dp</dimen>
+ <dimen name="communal_to_dream_button_size">48dp</dimen>
+
<!-- The width/height of the unlock icon view on keyguard. -->
<dimen name="keyguard_lock_height">42dp</dimen>
<dimen name="keyguard_lock_padding">20dp</dimen>
@@ -1523,11 +1525,11 @@
<dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
<dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen>
<dimen name="media_output_dialog_background_radius">16dp</dimen>
- <dimen name="media_output_dialog_active_background_radius">30dp</dimen>
- <dimen name="media_output_dialog_default_margin_end">16dp</dimen>
- <dimen name="media_output_dialog_selectable_margin_end">80dp</dimen>
+ <dimen name="media_output_dialog_active_background_radius">32dp</dimen>
+ <dimen name="media_output_dialog_item_height">64dp</dimen>
+ <dimen name="media_output_dialog_margin_horizontal">16dp</dimen>
<dimen name="media_output_dialog_list_padding_top">8dp</dimen>
- <dimen name="media_output_dialog_icon_left_radius">28dp</dimen>
+ <dimen name="media_output_dialog_icon_left_radius">@dimen/media_output_dialog_active_background_radius</dimen>
<dimen name="media_output_dialog_icon_right_radius">0dp</dimen>
<!-- Distance that the full shade transition takes in order to complete by tapping on a button
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index 9d9f5691816e..c14d28d1c08d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -392,15 +392,6 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest
setSystemGestureExclusion();
mIsVisible = true;
mCallback.onSettingsPanelVisibilityChanged(/* shown= */ true);
-
- if (resetPosition) {
- // We could not put focus on the settings panel automatically
- // since it is an inactive window. Therefore, we announce the existence of
- // magnification settings for accessibility when it is opened.
- mSettingView.announceForAccessibility(
- mContext.getResources().getString(
- R.string.accessibility_magnification_settings_panel_description));
- }
}
mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index fd50485fc3a3..372fdca20ed9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -3728,12 +3728,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
// windows that appear on top, ever
int flags = StatusBarManager.DISABLE_NONE;
- // TODO (b/155663717) After restart, status bar will not properly hide home button
+ // TODO(b/155663717): After restart, status bar will not properly hide home button
// unless disable is called to show un-hide it once first
if (forceClearFlags) {
if (UserManager.isVisibleBackgroundUsersEnabled()
- && !mProcessWrapper.isSystemUser() && !mProcessWrapper.isForegroundUser()) {
- // TODO: b/341604160 - Support visible background users properly.
+ && !mProcessWrapper.isSystemUser()
+ && !mProcessWrapper.isForegroundUserOrProfile()) {
+ // TODO(b/341604160): Support visible background users properly.
if (DEBUG) {
Log.d(TAG, "Status bar manager is disabled for visible background users");
}
@@ -3769,8 +3770,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
if (!SceneContainerFlag.isEnabled()) {
if (UserManager.isVisibleBackgroundUsersEnabled()
- && !mProcessWrapper.isSystemUser() && !mProcessWrapper.isForegroundUser()) {
- // TODO: b/341604160 - Support visible background users properly.
+ && !mProcessWrapper.isSystemUser()
+ && !mProcessWrapper.isForegroundUserOrProfile()) {
+ // TODO(b/341604160): Support visible background users properly.
if (DEBUG) {
Log.d(TAG, "Status bar manager is disabled for visible background users");
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index aaad10140a92..4b36e7a43dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -79,6 +79,8 @@ interface KeyguardRepository {
val panelAlpha: MutableStateFlow<Float>
+ val zoomOut: StateFlow<Float>
+
/**
* Observable for whether the keyguard is showing.
*
@@ -278,6 +280,9 @@ interface KeyguardRepository {
/** Temporary shim for fading out content when the brightness slider is used */
fun setPanelAlpha(alpha: Float)
+ /** Sets the zoom out scale of spatial model pushback from e.g. pulling down the shade. */
+ fun setZoomOut(zoomOutFromShadeRadius: Float)
+
/** Whether the device is actively dreaming */
fun setDreaming(isDreaming: Boolean)
@@ -381,6 +386,7 @@ constructor(
override val onCameraLaunchDetected = MutableStateFlow(CameraLaunchSourceModel())
override val panelAlpha: MutableStateFlow<Float> = MutableStateFlow(1f)
+ override val zoomOut: MutableStateFlow<Float> = MutableStateFlow(0f)
override val topClippingBounds = MutableStateFlow<Int?>(null)
override val isKeyguardShowing: MutableStateFlow<Boolean> =
@@ -662,6 +668,10 @@ constructor(
panelAlpha.value = alpha
}
+ override fun setZoomOut(zoomOutFromShadeRadius: Float) {
+ zoomOut.value = zoomOutFromShadeRadius
+ }
+
override fun setDreaming(isDreaming: Boolean) {
this.isDreaming.value = isDreaming
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 3739d17da6c4..3652c17309f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -337,6 +337,9 @@ constructor(
@Deprecated("SceneContainer uses NotificationStackAppearanceInteractor")
val panelAlpha: StateFlow<Float> = repository.panelAlpha.asStateFlow()
+ /** Sets the zoom out scale of spatial model pushback from e.g. pulling down the shade. */
+ val zoomOut: StateFlow<Float> = repository.zoomOut
+
/**
* When the lockscreen can be dismissed, emit an alpha value as the user swipes up. This is
* useful just before the code commits to moving to GONE.
@@ -475,6 +478,10 @@ constructor(
repository.setPanelAlpha(alpha)
}
+ fun setZoomOut(zoomOutFromShadeRadius: Float) {
+ repository.setZoomOut(zoomOutFromShadeRadius)
+ }
+
fun setAnimateDozingTransitions(animate: Boolean) {
repository.setAnimateDozingTransitions(animate)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt
index d6a110a8fd55..cb602f1287f7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt
@@ -22,7 +22,10 @@ import android.content.Context
import android.os.Binder
import android.os.IBinder
import android.os.RemoteException
+import android.os.UserManager
import android.provider.DeviceConfig
+import android.util.Log
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
import com.android.internal.statusbar.IStatusBarService
import com.android.systemui.CoreStartable
@@ -39,6 +42,7 @@ import com.android.systemui.navigation.domain.interactor.NavigationInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.power.shared.model.WakefulnessModel
+import com.android.systemui.process.ProcessWrapper
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
@@ -49,9 +53,10 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
-import com.android.app.tracing.coroutines.launchTraced as launch
import kotlinx.coroutines.withContext
+private val TAG = StatusBarDisableFlagsInteractor::class.simpleName
+
/**
* Logic around StatusBarService#disableForUser, which is used to disable the home and recents
* button in certain device states.
@@ -67,6 +72,7 @@ constructor(
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
private val statusBarService: IStatusBarService,
+ private val processWrapper: ProcessWrapper,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
selectedUserInteractor: SelectedUserInteractor,
deviceConfigInteractor: DeviceConfigInteractor,
@@ -141,6 +147,24 @@ constructor(
return
}
+ // TODO(b/341604160): Remove this blocking logic once StatusBarManagerService supports
+ // visible background users properly.
+ if (
+ UserManager.isVisibleBackgroundUsersEnabled() &&
+ !processWrapper.isSystemUser() &&
+ !processWrapper.isForegroundUserOrProfile()
+ ) {
+ // Currently, only one SysUI process can register with IStatusBarService to listen
+ // for the CommandQueue events.
+ // In the Multi Display configuration with concurrent multi users (primarily used
+ // in Automotive), a visible background user (Automotive Multi Display passengers)
+ // could also access this code path. Given this limitation and we only allow the
+ // current user's SysUI process to register with IStatusBarService, we need to prevent
+ // calls into IStatusBarService from visible background users.
+ Log.d(TAG, "Status bar manager is disabled for visible background users")
+ return
+ }
+
scope.launch {
disableFlagsForUserId.collect { (selectedUserId, flags) ->
if (context.getSystemService(Context.STATUS_BAR_SERVICE) == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index e48af773497a..b8020b19ce86 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -172,6 +172,13 @@ object KeyguardRootViewBinder {
}
}
+ launch("$TAG#zoomOut") {
+ viewModel.scaleFromZoomOut.collect { scaleFromZoomOut ->
+ view.scaleX = scaleFromZoomOut
+ view.scaleY = scaleFromZoomOut
+ }
+ }
+
launch("$TAG#translationY") {
// When translation happens in burnInLayer, it won't be weather clock large
// clock isn't added to burnInLayer due to its scale transition so we also
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 11a509a4fa61..47a76a00fd4a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -287,6 +287,9 @@ constructor(
.distinctUntilChanged()
}
+ val scaleFromZoomOut: Flow<Float> =
+ keyguardInteractor.zoomOut.map { 1 - it * PUSHBACK_SCALE_FOR_LOCKSCREEN }
+
val translationY: Flow<Float> = aodBurnInViewModel.movement.map { it.translationY.toFloat() }
val translationX: Flow<StateToValue> =
@@ -418,5 +421,6 @@ constructor(
companion object {
private const val TAG = "KeyguardRootViewModel"
+ private const val PUSHBACK_SCALE_FOR_LOCKSCREEN = 0.05f
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index ef6ae0dd6427..b6a3b6aaba14 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -24,10 +24,11 @@ import com.android.systemui.customization.R as customR
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.ClockSize
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
import dagger.assisted.AssistedFactory
@@ -54,8 +55,8 @@ constructor(
val touchHandling: KeyguardTouchHandlingViewModel,
private val shadeInteractor: ShadeInteractor,
private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
- private val occlusionInteractor: SceneContainerOcclusionInteractor,
private val deviceEntryInteractor: DeviceEntryInteractor,
+ private val transitionInteractor: KeyguardTransitionInteractor,
) : ExclusiveActivatable() {
@VisibleForTesting val clockSize = clockInteractor.clockSize
@@ -89,9 +90,15 @@ constructor(
}
launch {
- occlusionInteractor.isOccludingActivityShown
- .map { !it }
- .collect { _isContentVisible.value = it }
+ transitionInteractor
+ .transitionValue(KeyguardState.OCCLUDED)
+ .map { it > 0f }
+ .collect { fullyOrPartiallyOccluded ->
+ // Content is visible unless we're OCCLUDED. Currently, we don't have nice
+ // animations into and out of OCCLUDED, so the lockscreen/AOD content is
+ // hidden immediately upon entering/exiting OCCLUDED.
+ _isContentVisible.value = !fullyOrPartiallyOccluded
+ }
}
awaitCancellation()
diff --git a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
index e8ded03e3b38..ad306694346e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
+++ b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java
@@ -116,8 +116,8 @@ public class SessionTracker implements CoreStartable {
mSessionToInstanceId.put(type, instanceId);
if (UserManager.isVisibleBackgroundUsersEnabled() && !mProcessWrapper.isSystemUser()
- && !mProcessWrapper.isForegroundUser()) {
- // TODO: b/341604160 - Support visible background users properly.
+ && !mProcessWrapper.isForegroundUserOrProfile()) {
+ // TODO(b/341604160): Support visible background users properly.
if (DEBUG) {
Log.d(TAG, "Status bar manager is disabled for visible background users");
}
@@ -155,8 +155,8 @@ public class SessionTracker implements CoreStartable {
mUiEventLogger.log(endSessionUiEvent, instanceId);
}
if (UserManager.isVisibleBackgroundUsersEnabled() && !mProcessWrapper.isSystemUser()
- && !mProcessWrapper.isForegroundUser()) {
- // TODO: b/341604160 - Support visible background users properly.
+ && !mProcessWrapper.isForegroundUserOrProfile()) {
+ // TODO(b/341604160): Support visible background users properly.
if (DEBUG) {
Log.d(TAG, "Status bar manager is disabled for visible background users");
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index b391cb079ec5..bc6b2beb2ddb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -298,8 +298,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
boolean showEndArea =
!Flags.enableOutputSwitcherSessionGrouping() || isDeselectable;
updateUnmutedVolumeIcon(device);
- updateGroupableCheckBox(true, isDeselectable, device);
- updateEndClickArea(device, isDeselectable);
+ updateEndAreaForGroupCheckbox(device, true /* isSelected */, isDeselectable);
disableFocusPropertyForView(mContainerLayout);
setUpContentDescriptionForView(mSeekBar, device);
setSingleLineLayout(device.getName(), true /* showSeekBar */,
@@ -331,8 +330,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
//If device is connected and there's other selectable devices, layout as
// one of selected devices.
updateUnmutedVolumeIcon(device);
- updateGroupableCheckBox(true, isDeselectable, device);
- updateEndClickArea(device, isDeselectable);
+ updateEndAreaForGroupCheckbox(device, true /* isSelected */,
+ isDeselectable);
disableFocusPropertyForView(mContainerLayout);
setUpContentDescriptionForView(mSeekBar, device);
setSingleLineLayout(device.getName(), true /* showSeekBar */,
@@ -352,8 +351,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
} else if (isSelectable) {
//groupable device
setUpDeviceIcon(device);
- updateGroupableCheckBox(false, true, device);
- updateEndClickArea(device, true);
+ updateEndAreaForGroupCheckbox(device, false /* isSelected */,
+ true /* isDeselectable */);
if (!Flags.disableTransferWhenAppsDoNotSupport()
|| isTransferable
|| hasRouteListingPreferenceItem) {
@@ -406,7 +405,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
private void updateEndClickAreaWithIcon(View.OnClickListener clickListener,
@DrawableRes int iconDrawableId,
@StringRes int accessibilityStringId) {
- updateEndClickAreaColor(mController.getColorSeekbarProgress());
+ updateEndAreaColor(mController.getColorSeekbarProgress());
mEndClickIcon.setImageTintList(
ColorStateList.valueOf(mController.getColorItemContent()));
mEndClickIcon.setOnClickListener(clickListener);
@@ -422,7 +421,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
}
}
- public void updateEndClickAreaColor(int color) {
+ public void updateEndAreaColor(int color) {
mEndTouchArea.setBackgroundTintList(
ColorStateList.valueOf(color));
}
@@ -455,25 +454,22 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
ColorStateList.valueOf(mController.getColorItemContent()));
}
- public void updateEndClickArea(MediaDevice device, boolean isDeviceDeselectable) {
+ public void updateEndAreaForGroupCheckbox(MediaDevice device, boolean isSelected,
+ boolean isDeselectable) {
mEndTouchArea.setOnClickListener(null);
mEndTouchArea.setOnClickListener(
- isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null);
+ isDeselectable ? (v) -> mCheckBox.performClick() : null);
mEndTouchArea.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_YES);
- mEndTouchArea.setBackgroundTintList(
- ColorStateList.valueOf(mController.getColorItemBackground()));
+ updateEndAreaColor(isSelected ? mController.getColorSeekbarProgress()
+ : mController.getColorItemBackground());
setUpContentDescriptionForView(mEndTouchArea, device);
- }
-
- private void updateGroupableCheckBox(boolean isSelected, boolean isGroupable,
- MediaDevice device) {
mCheckBox.setOnCheckedChangeListener(null);
mCheckBox.setChecked(isSelected);
mCheckBox.setOnCheckedChangeListener(
- isGroupable ? (buttonView, isChecked) -> onGroupActionTriggered(!isSelected,
+ isDeselectable ? (buttonView, isChecked) -> onGroupActionTriggered(!isSelected,
device) : null);
- mCheckBox.setEnabled(isGroupable);
+ mCheckBox.setEnabled(isDeselectable);
setCheckBoxColor(mCheckBox, mController.getColorItemContent());
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index ee2d8aa46264..a7786c8f0b57 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -141,6 +141,8 @@ public abstract class MediaOutputBaseAdapter extends
final ImageView mEndClickIcon;
@VisibleForTesting
MediaOutputSeekbar mSeekBar;
+ private final float mInactiveRadius;
+ private final float mActiveRadius;
private String mDeviceId;
private ValueAnimator mCornerAnimator;
private ValueAnimator mVolumeAnimator;
@@ -161,6 +163,10 @@ public abstract class MediaOutputBaseAdapter extends
mEndClickIcon = view.requireViewById(R.id.media_output_item_end_click_icon);
mVolumeValueText = view.requireViewById(R.id.volume_value);
mIconAreaLayout = view.requireViewById(R.id.icon_area);
+ mInactiveRadius = mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_background_radius);
+ mActiveRadius = mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_active_background_radius);
initAnimator();
}
@@ -216,10 +222,6 @@ public abstract class MediaOutputBaseAdapter extends
mEndClickIcon.setVisibility(
!showCheckBox && showEndTouchArea ? View.VISIBLE : View.GONE);
}
- ViewGroup.MarginLayoutParams params =
- (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams();
- params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable()
- : mController.getItemMarginEndDefault();
}
void setTwoLineLayout(CharSequence title, boolean showSeekBar,
@@ -247,10 +249,6 @@ public abstract class MediaOutputBaseAdapter extends
//update end click area by isActive
mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE);
mEndClickIcon.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE);
- ViewGroup.MarginLayoutParams params =
- (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams();
- params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable()
- : mController.getItemMarginEndDefault();
mItemLayout.setBackground(backgroundDrawable);
mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
@@ -264,10 +262,10 @@ public abstract class MediaOutputBaseAdapter extends
final GradientDrawable progressDrawable =
(GradientDrawable) clipDrawable.getDrawable();
progressDrawable.setCornerRadii(
- new float[]{0, 0, mController.getActiveRadius(),
- mController.getActiveRadius(),
- mController.getActiveRadius(),
- mController.getActiveRadius(), 0, 0});
+ new float[]{0, 0, mActiveRadius,
+ mActiveRadius,
+ mActiveRadius,
+ mActiveRadius, 0, 0});
}
private void initializeSeekbarVolume(
@@ -431,8 +429,7 @@ public abstract class MediaOutputBaseAdapter extends
}
private void initAnimator() {
- mCornerAnimator = ValueAnimator.ofFloat(mController.getInactiveRadius(),
- mController.getActiveRadius());
+ mCornerAnimator = ValueAnimator.ofFloat(mInactiveRadius, mActiveRadius);
mCornerAnimator.setDuration(ANIM_DURATION);
mCornerAnimator.setInterpolator(new LinearInterpolator());
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 02a2befe44e5..19409b32a2f6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -171,10 +171,6 @@ public class MediaSwitchingController
private int mColorConnectedItemBackground;
private int mColorPositiveButtonText;
private int mColorDialogBackground;
- private int mItemMarginEndDefault;
- private int mItemMarginEndSelectable;
- private float mInactiveRadius;
- private float mActiveRadius;
private FeatureFlags mFeatureFlags;
private UserTracker mUserTracker;
private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor;
@@ -246,16 +242,8 @@ public class MediaSwitchingController
R.color.media_dialog_connected_item_background);
mColorPositiveButtonText = Utils.getColorStateListDefaultColor(mContext,
R.color.media_dialog_solid_button_text);
- mInactiveRadius = mContext.getResources().getDimension(
- R.dimen.media_output_dialog_background_radius);
- mActiveRadius = mContext.getResources().getDimension(
- R.dimen.media_output_dialog_active_background_radius);
mColorDialogBackground = Utils.getColorStateListDefaultColor(mContext,
R.color.media_dialog_background);
- mItemMarginEndDefault = (int) mContext.getResources().getDimension(
- R.dimen.media_output_dialog_default_margin_end);
- mItemMarginEndSelectable = (int) mContext.getResources().getDimension(
- R.dimen.media_output_dialog_selectable_margin_end);
if (enableInputRouting()) {
mInputRouteManager = new InputRouteManager(mContext, audioManager);
@@ -638,22 +626,6 @@ public class MediaSwitchingController
return mColorItemBackground;
}
- public float getInactiveRadius() {
- return mInactiveRadius;
- }
-
- public float getActiveRadius() {
- return mActiveRadius;
- }
-
- public int getItemMarginEndDefault() {
- return mItemMarginEndDefault;
- }
-
- public int getItemMarginEndSelectable() {
- return mItemMarginEndSelectable;
- }
-
private void buildMediaItems(List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices);
diff --git a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
index 294d0c75167a..f3a3a3a2ac4c 100644
--- a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
@@ -27,8 +27,12 @@ import javax.inject.Inject;
* providing a mockable target around these details.
*/
public class ProcessWrapper {
+ private final ActivityManager mActivityManager;
+
@Inject
- public ProcessWrapper() {}
+ public ProcessWrapper(ActivityManager activityManager) {
+ mActivityManager = activityManager;
+ }
/**
* Returns {@code true} if System User is running the current process.
@@ -38,10 +42,10 @@ public class ProcessWrapper {
}
/**
- * Returns {@code true} if the foreground user is running the current process.
+ * Returns {@code true} if the foreground user or profile is running the current process.
*/
- public boolean isForegroundUser() {
- return ActivityManager.getCurrentUser() == myUserHandle().getIdentifier();
+ public boolean isForegroundUserOrProfile() {
+ return mActivityManager.isProfileForeground(myUserHandle());
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt
index c302cb21f77f..3afaef5ea6a1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt
@@ -20,7 +20,6 @@ import com.android.systemui.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.shade.shared.flag.DualShade
/** Helper for reading or using the QS Detailed View flag state. */
@Suppress("NOTHING_TO_INLINE")
@@ -37,7 +36,6 @@ object QsDetailedView {
inline val isEnabled
get() =
Flags.qsTileDetailedView() && // mainAconfigFlag
- DualShade.isEnabled &&
SceneContainerFlag.isEnabled
// NOTE: Changes should also be made in getSecondaryFlags
@@ -47,10 +45,8 @@ object QsDetailedView {
/** The set of secondary flags which must be enabled for qs detailed view to work properly */
inline fun getSecondaryFlags(): Sequence<FlagToken> =
- sequenceOf(
- DualShade.token
- // NOTE: Changes should also be made in isEnabled
- ) + SceneContainerFlag.getAllRequirements()
+ // NOTE: Changes should also be made in isEnabled
+ SceneContainerFlag.getAllRequirements()
/** The full set of requirements for QsDetailedView */
inline fun getAllRequirements(): Sequence<FlagToken> {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt
index 3067ccbb7cea..3140df8d947a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt
@@ -17,11 +17,11 @@
package com.android.systemui.qs.flags
import com.android.systemui.flags.RefactorFlagUtils
-import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
/**
* Object to help check if the new QS ui should be used. This is true if either [QSComposeFragment]
- * or [DualShade] are enabled.
+ * or [SceneContainerFlag] are enabled.
*/
object QsInCompose {
@@ -29,11 +29,12 @@ object QsInCompose {
* This is not a real flag name, but a representation of the allowed flag names. Should not be
* used with test annotations.
*/
- private val flagName = "${QSComposeFragment.FLAG_NAME}|${DualShade.FLAG_NAME}"
+ private val flagName =
+ "${QSComposeFragment.FLAG_NAME}|${SceneContainerFlag.getMainAconfigFlag().name}"
@JvmStatic
inline val isEnabled: Boolean
- get() = QSComposeFragment.isEnabled || DualShade.isEnabled
+ get() = QSComposeFragment.isEnabled || SceneContainerFlag.isEnabled
@JvmStatic
fun isUnexpectedlyInLegacyMode() =
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 28f5694c3332..e535019cd3d7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -527,6 +527,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private final ActivityStarter mActivityStarter;
private final BrightnessMirrorShowingInteractor mBrightnessMirrorShowingInteractor;
+ @Nullable
+ private RenderEffect mBlurRenderEffect = null;
+
@Inject
public NotificationPanelViewController(NotificationPanelView view,
NotificationWakeUpCoordinator coordinator,
@@ -912,13 +915,14 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private void handleBouncerShowingChanged(Boolean isBouncerShowing) {
if (!com.android.systemui.Flags.bouncerUiRevamp()) return;
-
if (isBouncerShowing && isExpanded()) {
- float shadeBlurEffect = mDepthController.getMaxBlurRadiusPx();
- mView.setRenderEffect(RenderEffect.createBlurEffect(
- shadeBlurEffect,
- shadeBlurEffect,
- Shader.TileMode.CLAMP));
+ if (mBlurRenderEffect == null) {
+ mBlurRenderEffect = RenderEffect.createBlurEffect(
+ mDepthController.getMaxBlurRadiusPx(),
+ mDepthController.getMaxBlurRadiusPx(),
+ Shader.TileMode.CLAMP);
+ }
+ mView.setRenderEffect(mBlurRenderEffect);
} else {
mView.setRenderEffect(null);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index a2a840942f3c..e3b36df9aed7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -40,6 +40,7 @@ import com.android.systemui.animation.ShadeInterpolation
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.ShadeExpansionChangeEvent
import com.android.systemui.shade.ShadeExpansionListener
@@ -74,6 +75,7 @@ constructor(
private val blurUtils: BlurUtils,
private val biometricUnlockController: BiometricUnlockController,
private val keyguardStateController: KeyguardStateController,
+ private val keyguardInteractor: KeyguardInteractor,
private val choreographer: Choreographer,
private val wallpaperController: WallpaperController,
private val notificationShadeWindowController: NotificationShadeWindowController,
@@ -281,6 +283,7 @@ constructor(
appZoomOutOptional.ifPresent { appZoomOut ->
appZoomOut.setProgress(zoomOutFromShadeRadius)
}
+ keyguardInteractor.setZoomOut(zoomOutFromShadeRadius)
}
listeners.forEach {
it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 5cc79df9130a..09cc3f23032e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -812,11 +812,6 @@ public final class NotificationEntry extends ListEntry {
return !mSbn.isOngoing() || !isLocked;
}
- public boolean canViewBeDismissed() {
- if (row == null) return true;
- return row.canViewBeDismissed();
- }
-
@VisibleForTesting
boolean isExemptFromDndVisualSuppression() {
if (isNotificationBlockedByPolicy(mSbn.getNotification())) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt
deleted file mode 100644
index 56057fb00e45..000000000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt
+++ /dev/null
@@ -1,58 +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.notification.promoted.ui.viewmodel
-
-import android.graphics.drawable.Icon
-import com.android.internal.widget.NotificationProgressModel
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-
-class PromotedNotificationViewModel(
- identity: PromotedNotificationContentModel.Identity,
- content: Flow<PromotedNotificationContentModel>,
-) {
- // for all styles:
-
- val key: String = identity.key
- val style: Style = identity.style
-
- val skeletonSmallIcon: Flow<Icon?> = content.map { it.skeletonSmallIcon }
- val appName: Flow<CharSequence?> = content.map { it.appName }
- val subText: Flow<CharSequence?> = content.map { it.subText }
-
- private val time: Flow<When?> = content.map { it.time }
- val whenTime: Flow<Long?> = time.map { it?.time }
- val whenMode: Flow<When.Mode?> = time.map { it?.mode }
-
- val lastAudiblyAlertedMs: Flow<Long> = content.map { it.lastAudiblyAlertedMs }
- val profileBadgeResId: Flow<Int?> = content.map { it.profileBadgeResId }
- val title: Flow<CharSequence?> = content.map { it.title }
- val text: Flow<CharSequence?> = content.map { it.text }
- val skeletonLargeIcon: Flow<Icon?> = content.map { it.skeletonLargeIcon }
-
- // for CallStyle:
- val personIcon: Flow<Icon?> = content.map { it.personIcon }
- val personName: Flow<CharSequence?> = content.map { it.personName }
- val verificationIcon: Flow<Icon?> = content.map { it.verificationIcon }
- val verificationText: Flow<CharSequence?> = content.map { it.verificationText }
-
- // for ProgressStyle:
- val progress: Flow<NotificationProgressModel?> = content.map { it.newProgress }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index aa7665a5b630..15f316800000 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -71,7 +71,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
-import androidx.dynamicanimation.animation.SpringForce;
import com.android.app.animation.Interpolators;
import com.android.internal.annotations.VisibleForTesting;
@@ -361,39 +360,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
};
- private final SpringAnimation mMagneticAnimator = new SpringAnimation(
- this, FloatPropertyCompat.createFloatPropertyCompat(TRANSLATE_CONTENT));
-
- private final MagneticRowListener mMagneticRowListener = new MagneticRowListener() {
-
- @Override
- public void setMagneticTranslation(float translation) {
- if (mMagneticAnimator.isRunning()) {
- mMagneticAnimator.animateToFinalPosition(translation);
- } else {
- setTranslation(translation);
- }
- }
-
- @Override
- public void triggerMagneticForce(float endTranslation, @NonNull SpringForce springForce,
- float startVelocity) {
- cancelMagneticAnimations();
- mMagneticAnimator.setSpring(springForce);
- mMagneticAnimator.setStartVelocity(startVelocity);
- mMagneticAnimator.animateToFinalPosition(endTranslation);
- }
-
- @Override
- public void cancelMagneticAnimations() {
- cancelSnapBackAnimation();
- cancelTranslateAnimation();
- mMagneticAnimator.cancel();
- }
- };
+ @Override
+ protected void cancelTranslationAnimations() {
+ cancelSnapBackAnimation();
+ cancelTranslateAnimation();
+ }
private void cancelSnapBackAnimation() {
- PhysicsAnimator<ExpandableNotificationRow> animator =
+ PhysicsAnimator<ExpandableView> animator =
PhysicsAnimator.getInstanceIfExists(this /* target */);
if (animator != null) {
animator.cancel();
@@ -2044,6 +2018,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
new NotificationInlineImageCache());
float radius = getResources().getDimension(R.dimen.notification_corner_radius_small);
mSmallRoundness = radius / getMaxRadius();
+ mMagneticAnimator = new SpringAnimation(
+ this, FloatPropertyCompat.createFloatPropertyCompat(TRANSLATE_CONTENT));
initDimens();
}
@@ -3300,6 +3276,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
/**
+ * For the case of an {@link ExpandableNotificationRow}, the dismissibility of the row considers
+ * the exposure of guts, the state of the notification entry, and if the view itself is allowed
+ * to be dismissed.
+ */
+ @Override
+ public boolean canExpandableViewBeDismissed() {
+ if (areGutsExposed() || !mEntry.hasFinishedInitialization()) {
+ return false;
+ }
+ return canViewBeDismissed();
+ }
+
+ /**
* @return Whether this view is allowed to be dismissed. Only valid for visible notifications as
* otherwise some state might not be updated.
*/
@@ -4067,6 +4056,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
}
+ @VisibleForTesting
+ public void setMagneticRowListener(MagneticRowListener listener) {
+ mMagneticRowListener = listener;
+ }
+
/**
* Equivalent to View.OnLongClickListener with coordinates
*/
@@ -4317,8 +4311,4 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
mLogger.logRemoveTransientRow(row.getEntry(), getEntry());
}
-
- public MagneticRowListener getMagneticRowListener() {
- return mMagneticRowListener;
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index f83a1d9b7833..76ba7f9ea901 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -33,6 +33,9 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
import com.android.app.animation.Interpolators;
import com.android.systemui.Dumpable;
@@ -42,6 +45,7 @@ import com.android.systemui.statusbar.notification.Roundable;
import com.android.systemui.statusbar.notification.RoundableState;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
+import com.android.systemui.statusbar.notification.stack.MagneticRowListener;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.util.Compile;
import com.android.systemui.util.DumpUtilsKt;
@@ -85,6 +89,55 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro
protected boolean mLastInSection;
protected boolean mFirstInSection;
+ protected SpringAnimation mMagneticAnimator = new SpringAnimation(
+ this /* object */, DynamicAnimation.TRANSLATION_X);
+
+ protected MagneticRowListener mMagneticRowListener = new MagneticRowListener() {
+
+ @Override
+ public void setMagneticTranslation(float translation) {
+ if (mMagneticAnimator.isRunning()) {
+ mMagneticAnimator.animateToFinalPosition(translation);
+ } else {
+ setTranslation(translation);
+ }
+ }
+
+ @Override
+ public void triggerMagneticForce(float endTranslation, @NonNull SpringForce springForce,
+ float startVelocity) {
+ cancelMagneticAnimations();
+ mMagneticAnimator.setSpring(springForce);
+ mMagneticAnimator.setStartVelocity(startVelocity);
+ mMagneticAnimator.animateToFinalPosition(endTranslation);
+ }
+
+ @Override
+ public void cancelMagneticAnimations() {
+ cancelTranslationAnimations();
+ mMagneticAnimator.cancel();
+ }
+
+ @Override
+ public boolean canRowBeDismissed() {
+ return canExpandableViewBeDismissed();
+ }
+ };
+
+ /**
+ * @return true if the ExpandableView can be dismissed. False otherwise.
+ */
+ public boolean canExpandableViewBeDismissed() {
+ return false;
+ }
+
+ /** Cancel any trailing animations on the translation of the view */
+ protected void cancelTranslationAnimations(){}
+
+ public MagneticRowListener getMagneticRowListener() {
+ return mMagneticRowListener;
+ }
+
public ExpandableView(Context context, AttributeSet attrs) {
super(context, attrs);
mViewState = createExpandableViewState();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
index 61aaf4998e87..3941700496f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
@@ -57,7 +57,7 @@ constructor(
SpringForce().setStiffness(SNAP_BACK_STIFFNESS).setDampingRatio(SNAP_BACK_DAMPING_RATIO)
// Multiplier applied to the translation of a row while swiped
- private val swipedRowMultiplier =
+ val swipedRowMultiplier =
MAGNETIC_TRANSLATION_MULTIPLIERS[MAGNETIC_TRANSLATION_MULTIPLIERS.size / 2]
override fun setSwipeThresholdPx(thresholdPx: Float) {
@@ -111,24 +111,22 @@ constructor(
): Boolean {
if (!row.isSwipedTarget()) return false
+ val canTargetBeDismissed =
+ currentMagneticListeners.swipedListener()?.canRowBeDismissed() ?: false
when (currentState) {
State.IDLE -> {
logger.logMagneticRowTranslationNotSet(currentState, row.entry)
return false
}
State.TARGETS_SET -> {
- pullTargets(translation)
+ pullTargets(translation, canTargetBeDismissed)
currentState = State.PULLING
}
State.PULLING -> {
- val targetTranslation = swipedRowMultiplier * translation
- val crossedThreshold = abs(targetTranslation) >= magneticDetachThreshold
- if (crossedThreshold) {
- snapNeighborsBack()
- currentMagneticListeners.swipedListener()?.let { detach(it, translation) }
- currentState = State.DETACHED
+ if (canTargetBeDismissed) {
+ pullDismissibleRow(translation)
} else {
- pullTargets(translation)
+ pullTargets(translation, canSwipedBeDismissed = false)
}
}
State.DETACHED -> {
@@ -139,23 +137,49 @@ constructor(
return true
}
- private fun pullTargets(translation: Float) {
+ private fun pullDismissibleRow(translation: Float) {
+ val targetTranslation = swipedRowMultiplier * translation
+ val crossedThreshold = abs(targetTranslation) >= magneticDetachThreshold
+ if (crossedThreshold) {
+ snapNeighborsBack()
+ currentMagneticListeners.swipedListener()?.let { detach(it, translation) }
+ currentState = State.DETACHED
+ } else {
+ pullTargets(translation, canSwipedBeDismissed = true)
+ }
+ }
+
+ private fun pullTargets(translation: Float, canSwipedBeDismissed: Boolean) {
var targetTranslation: Float
currentMagneticListeners.forEachIndexed { i, listener ->
- targetTranslation = MAGNETIC_TRANSLATION_MULTIPLIERS[i] * translation
- listener?.setMagneticTranslation(targetTranslation)
+ listener?.let {
+ if (!canSwipedBeDismissed || !it.canRowBeDismissed()) {
+ // Use a reduced translation if the target swiped can't be dismissed or if the
+ // target itself can't be dismissed
+ targetTranslation =
+ MAGNETIC_TRANSLATION_MULTIPLIERS[i] * translation * MAGNETIC_REDUCTION
+ } else {
+ targetTranslation = MAGNETIC_TRANSLATION_MULTIPLIERS[i] * translation
+ }
+ it.setMagneticTranslation(targetTranslation)
+ }
}
- playPullHaptics(mappedTranslation = swipedRowMultiplier * translation)
+ playPullHaptics(mappedTranslation = swipedRowMultiplier * translation, canSwipedBeDismissed)
}
- private fun playPullHaptics(mappedTranslation: Float) {
+ private fun playPullHaptics(mappedTranslation: Float, canSwipedBeDismissed: Boolean) {
val normalizedTranslation = abs(mappedTranslation) / magneticDetachThreshold
- val vibrationScale =
- (normalizedTranslation * MAX_VIBRATION_SCALE).pow(VIBRATION_PERCEPTION_EXPONENT)
+ val scaleFactor =
+ if (canSwipedBeDismissed) {
+ WEAK_VIBRATION_SCALE
+ } else {
+ STRONG_VIBRATION_SCALE
+ }
+ val vibrationScale = scaleFactor * normalizedTranslation
msdlPlayer.playToken(
MSDLToken.DRAG_INDICATOR_CONTINUOUS,
InteractionProperties.DynamicVibrationScale(
- scale = vibrationScale,
+ scale = vibrationScale.pow(VIBRATION_PERCEPTION_EXPONENT),
vibrationAttributes = VIBRATION_ATTRIBUTES_PIPELINING,
),
)
@@ -233,6 +257,8 @@ constructor(
*/
private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.18f, 0.28f, 0.5f, 0.28f, 0.18f)
+ const val MAGNETIC_REDUCTION = 0.65f
+
/** Spring parameters for physics animators */
private const val DETACH_STIFFNESS = 800f
private const val DETACH_DAMPING_RATIO = 0.95f
@@ -244,7 +270,8 @@ constructor(
.setUsage(VibrationAttributes.USAGE_TOUCH)
.setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
.build()
- private const val MAX_VIBRATION_SCALE = 0.2f
private const val VIBRATION_PERCEPTION_EXPONENT = 1 / 0.89f
+ private const val WEAK_VIBRATION_SCALE = 0.2f
+ private const val STRONG_VIBRATION_SCALE = 0.45f
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
index 8a1adfe95392..46036d4c1fad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
@@ -41,4 +41,7 @@ interface MagneticRowListener {
/** Cancel any animations related to the magnetic interactions of the row */
fun cancelMagneticAnimations()
+
+ /** Can the row be dismissed. */
+ fun canRowBeDismissed(): Boolean
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index ce1fc97cbffe..d6b34b068cc5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -6609,10 +6609,7 @@ public class NotificationStackScrollLayout
static boolean canChildBeDismissed(View v) {
if (v instanceof ExpandableNotificationRow row) {
- if (row.areGutsExposed() || !row.getEntry().hasFinishedInitialization()) {
- return false;
- }
- return row.canViewBeDismissed();
+ return row.canExpandableViewBeDismissed();
}
return false;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
index 74e8b8ef29c2..b69b936ea9f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
@@ -3,7 +3,9 @@ package com.android.systemui.statusbar.notification.stack
import androidx.core.view.children
import androidx.core.view.isVisible
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.NotificationShelf
import com.android.systemui.statusbar.notification.Roundable
+import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import javax.inject.Inject
@@ -129,6 +131,10 @@ class NotificationTargetsHelper @Inject constructor() {
magneticTargets[leftIndex] = leftElement.magneticRowListener
leftIndex--
} else {
+ if (leftElement.isValidMagneticBoundary()) {
+ // Add the boundary and then stop the iterating
+ magneticTargets[leftIndex] = leftElement?.magneticRowListener
+ }
canMoveLeft = false
}
}
@@ -138,12 +144,24 @@ class NotificationTargetsHelper @Inject constructor() {
magneticTargets[rightIndex] = rightElement.magneticRowListener
rightIndex++
} else {
+ if (rightElement.isValidMagneticBoundary()) {
+ // Add the boundary and then stop the iterating
+ magneticTargets[rightIndex] = rightElement?.magneticRowListener
+ }
canMoveRight = false
}
}
}
return magneticTargets
}
+
+ private fun ExpandableView?.isValidMagneticBoundary(): Boolean =
+ when (this) {
+ is FooterView,
+ is NotificationShelf,
+ is SectionHeaderView -> true
+ else -> false
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
index c8c798d00a06..5689230f6bed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.stack.ui.view
import android.service.notification.NotificationListenerService
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.coroutines.TrackTracer
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.statusbar.IStatusBarService
import com.android.internal.statusbar.NotificationVisibility
import com.android.systemui.dagger.SysUISingleton
@@ -33,8 +34,9 @@ import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import com.android.app.tracing.coroutines.launchTraced as launch
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.withContext
@VisibleForTesting const val UNKNOWN_RANK = -1
@@ -49,32 +51,56 @@ constructor(
private val notificationPanelLogger: NotificationPanelLogger,
private val statusBarService: IStatusBarService,
) : NotificationStatsLogger {
- private val lastLoggedVisibilities = mutableMapOf<String, VisibilityState>()
- private var logVisibilitiesJob: Job? = null
-
private val expansionStates: MutableMap<String, ExpansionState> =
ConcurrentHashMap<String, ExpansionState>()
@VisibleForTesting
val lastReportedExpansionValues: MutableMap<String, Boolean> =
ConcurrentHashMap<String, Boolean>()
+ private val visibilityLogger =
+ Channel<VisibilityAction>(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+
+ init {
+ applicationScope.launch { consumeVisibilityActions() }
+ }
+
+ private suspend fun consumeVisibilityActions() {
+ val lastLoggedVisibilities = mutableMapOf<String, VisibilityState>()
+
+ visibilityLogger.consumeEach { action ->
+ val newVisibilities =
+ when (action) {
+ is VisibilityAction.Change -> action.visibilities
+ is VisibilityAction.Clear -> emptyMap()
+ }
+
+ val newlyVisible = newVisibilities - lastLoggedVisibilities.keys
+ val noLongerVisible = lastLoggedVisibilities - newVisibilities.keys
+
+ maybeLogVisibilityChanges(newlyVisible, noLongerVisible, action.activeCount)
+ updateExpansionStates(newlyVisible, noLongerVisible)
+ TrackTracer.instantForGroup("Notifications", "Active", action.activeCount)
+ TrackTracer.instantForGroup("Notifications", "Visible", newVisibilities.size)
+
+ lastLoggedVisibilities.clear()
+ lastLoggedVisibilities.putAll(newVisibilities)
+ }
+ }
+
override fun onNotificationLocationsChanged(
locationsProvider: Callable<Map<String, Int>>,
notificationRanks: Map<String, Int>,
) {
- if (logVisibilitiesJob?.isActive == true) {
- return
- }
-
- logVisibilitiesJob =
- startLogVisibilitiesJob(
- newVisibilities =
+ visibilityLogger.trySend(
+ VisibilityAction.Change(
+ visibilities =
combine(
visibilities = locationsProvider.call(),
- rankingsMap = notificationRanks
+ rankingsMap = notificationRanks,
),
- activeNotifCount = notificationRanks.size,
+ activeCount = notificationRanks.size,
)
+ )
}
override fun onNotificationExpansionChanged(
@@ -125,7 +151,7 @@ constructor(
/* expanded = */ expansionState.isExpanded,
/* notificationLocation = */ expansionState.location
.toNotificationLocation()
- .ordinal
+ .ordinal,
)
}
}
@@ -138,7 +164,7 @@ constructor(
withContext(bgDispatcher) {
notificationPanelLogger.logPanelShown(
isOnLockScreen,
- activeNotifications.toNotificationProto()
+ activeNotifications.toNotificationProto(),
)
}
}
@@ -147,11 +173,7 @@ constructor(
override fun onLockscreenOrShadeNotInteractive(
activeNotifications: List<ActiveNotificationModel>
) {
- logVisibilitiesJob =
- startLogVisibilitiesJob(
- newVisibilities = emptyMap(),
- activeNotifCount = activeNotifications.size
- )
+ visibilityLogger.trySend(VisibilityAction.Clear(activeCount = activeNotifications.size))
}
override fun onNotificationRemoved(key: String) {
@@ -167,29 +189,12 @@ constructor(
private fun combine(
visibilities: Map<String, Int>,
- rankingsMap: Map<String, Int>
+ rankingsMap: Map<String, Int>,
): Map<String, VisibilityState> =
visibilities.mapValues { entry ->
VisibilityState(entry.key, entry.value, rankingsMap[entry.key] ?: UNKNOWN_RANK)
}
- private fun startLogVisibilitiesJob(
- newVisibilities: Map<String, VisibilityState>,
- activeNotifCount: Int,
- ) =
- applicationScope.launch {
- val newlyVisible = newVisibilities - lastLoggedVisibilities.keys
- val noLongerVisible = lastLoggedVisibilities - newVisibilities.keys
-
- maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount)
- updateExpansionStates(newlyVisible, noLongerVisible)
- TrackTracer.instantForGroup("Notifications", "Active", activeNotifCount)
- TrackTracer.instantForGroup("Notifications", "Visible", newVisibilities.size)
-
- lastLoggedVisibilities.clear()
- lastLoggedVisibilities.putAll(newVisibilities)
- }
-
private suspend fun maybeLogVisibilityChanges(
newlyVisible: Map<String, VisibilityState>,
noLongerVisible: Map<String, VisibilityState>,
@@ -205,7 +210,7 @@ constructor(
val noLongerVisibleAr =
noLongerVisible.mapToNotificationVisibilitiesAr(
visible = false,
- count = activeNotifCount
+ count = activeNotifCount,
)
withContext(bgDispatcher) {
@@ -218,7 +223,7 @@ constructor(
private fun updateExpansionStates(
newlyVisible: Map<String, VisibilityState>,
- noLongerVisible: Map<String, VisibilityState>
+ noLongerVisible: Map<String, VisibilityState>,
) {
expansionStates.forEach { (key, expansionState) ->
if (newlyVisible.contains(key)) {
@@ -241,11 +246,16 @@ constructor(
}
}
- private data class VisibilityState(
- val key: String,
- val location: Int,
- val rank: Int,
- )
+ private sealed class VisibilityAction(open val activeCount: Int) {
+ data class Change(
+ val visibilities: Map<String, VisibilityState>,
+ override val activeCount: Int,
+ ) : VisibilityAction(activeCount)
+
+ data class Clear(override val activeCount: Int) : VisibilityAction(activeCount)
+ }
+
+ private data class VisibilityState(val key: String, val location: Int, val rank: Int)
private data class ExpansionState(
val key: String,
@@ -278,7 +288,7 @@ constructor(
/* rank = */ state.rank,
/* count = */ count,
/* visible = */ visible,
- /* location = */ state.location.toNotificationLocation()
+ /* location = */ state.location.toNotificationLocation(),
)
}
.toTypedArray()
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteria.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteria.kt
deleted file mode 100644
index bee45645bfdb..000000000000
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteria.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.panel.component.mediaoutput.domain
-
-import android.content.Context
-import com.android.settingslib.media.PhoneMediaDevice.isDesktop
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
-import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.stateIn
-
-@VolumePanelScope
-class MediaOutputAvailabilityCriteria
-@Inject
-constructor(
- @Application private val context: Context,
- @VolumePanelScope private val scope: CoroutineScope,
-) : ComponentAvailabilityCriteria {
-
- private val availability =
- flow { emit(!isDesktop(context)) }.stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override fun isAvailable(): Flow<Boolean> = availability
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt
index 82e247714794..fbc6f84cb8fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt
@@ -29,10 +29,10 @@ import com.android.internal.logging.InstanceId
import com.android.internal.statusbar.IAddTileResultCallback
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.external.ui.dialog.tileRequestDialogComposeDelegateFactory
import com.android.systemui.qs.flags.QSComposeFragment
-import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.testKosmos
@@ -57,7 +57,8 @@ import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
-@DisableFlags(value = [QSComposeFragment.FLAG_NAME, DualShade.FLAG_NAME])
+@DisableFlags(QSComposeFragment.FLAG_NAME)
+@DisableSceneContainer
class TileServiceRequestControllerTestComposeOff : SysuiTestCase() {
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
index 50b8f37f8d25..c20a801cd5e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
@@ -63,7 +63,7 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@RunWithLooper(setAsMainLooper = true)
@EnableSceneContainer
-@EnableFlags(Flags.FLAG_QS_TILE_DETAILED_VIEW, Flags.FLAG_DUAL_SHADE)
+@EnableFlags(Flags.FLAG_QS_TILE_DETAILED_VIEW)
@UiThreadTest
class InternetDetailsContentManagerTest : SysuiTestCase() {
private val kosmos = Kosmos()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt
index 69b762b470b7..40547c2787ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt
@@ -90,7 +90,7 @@ class LauncherProxyServiceTest : SysuiTestCase() {
private val kosmos = testKosmos()
private lateinit var subject: LauncherProxyService
@Mock private val dumpManager = DumpManager()
- @Mock private val processWrapper = ProcessWrapper()
+ @Mock private lateinit var processWrapper: ProcessWrapper
private val displayTracker = FakeDisplayTracker(mContext)
private val fakeSystemClock = FakeSystemClock()
private val sysUiState = SysUiState(displayTracker, kosmos.sceneContainerPlugin)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
index 57c28580c063..b75dd0402175 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
@@ -58,7 +58,8 @@ class SimpleDigitalClockTextViewTest : SysuiTestCase() {
},
ClockMessageBuffers(messageBuffer),
messageBuffer,
- )
+ ),
+ isLargeClock = false,
)
underTest.textStyle = FontTextStyle()
underTest.aodStyle = FontTextStyle()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 097f3929db42..bf10dc6c4aef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -2110,7 +2110,7 @@ public class BubblesTest extends SysuiTestCase {
assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(noteBubbleKey);
assertThat(mBubbleController.isStackExpanded()).isTrue();
assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
- assertThat(mBubbleData.getBubbles().get(0).getAppBubbleIntent()
+ assertThat(mBubbleData.getBubbles().get(0).getIntent()
.getStringExtra("hello")).isEqualTo("world");
assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNull();
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 8ea80081a871..1952f26b4e6a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -114,6 +114,7 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository {
override val keyguardAlpha: StateFlow<Float> = _keyguardAlpha
override val panelAlpha: MutableStateFlow<Float> = MutableStateFlow(1f)
+ override val zoomOut: MutableStateFlow<Float> = MutableStateFlow(0f)
override val lastRootViewTapPosition: MutableStateFlow<Point?> = MutableStateFlow(null)
@@ -272,6 +273,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository {
panelAlpha.value = alpha
}
+ override fun setZoomOut(zoomOutFromShadeRadius: Float) {
+ zoomOut.value = zoomOutFromShadeRadius
+ }
+
fun setIsEncryptedOrLockdown(value: Boolean) {
_isEncryptedOrLockdown.value = value
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
index f47b2df607c1..78d44d4917fe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
@@ -20,8 +20,8 @@ import com.android.systemui.biometrics.authController
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
@@ -34,7 +34,7 @@ val Kosmos.lockscreenContentViewModel by
touchHandling = keyguardTouchHandlingViewModel,
shadeInteractor = shadeInteractor,
unfoldTransitionInteractor = unfoldTransitionInteractor,
- occlusionInteractor = sceneContainerOcclusionInteractor,
deviceEntryInteractor = deviceEntryInteractor,
+ transitionInteractor = keyguardTransitionInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessKosmos.kt
index 79167f840f60..4d1e0a8c025a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessKosmos.kt
@@ -16,6 +16,9 @@
package com.android.systemui.process
+import android.app.ActivityManager
+
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
-val Kosmos.processWrapper: ProcessWrapperFake by Kosmos.Fixture { ProcessWrapperFake() }
+val Kosmos.processWrapper: ProcessWrapperFake by Kosmos.Fixture { ProcessWrapperFake(mock<ActivityManager>()) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt
index dee3644e95bd..152cc3019d85 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt
@@ -16,9 +16,10 @@
package com.android.systemui.process
+import android.app.ActivityManager
import android.os.UserHandle
-class ProcessWrapperFake : ProcessWrapper() {
+class ProcessWrapperFake(activityManager: ActivityManager) : ProcessWrapper(activityManager) {
var systemUser: Boolean = false
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt
index de52155dce79..f91c2f620bb1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt
@@ -26,7 +26,7 @@ import com.android.systemui.statusbar.notification.logging.notificationPanelLogg
val Kosmos.notificationStatsLogger by Fixture {
NotificationStatsLoggerImpl(
- applicationScope = testScope,
+ applicationScope = testScope.backgroundScope,
bgDispatcher = testDispatcher,
statusBarService = statusBarService,
notificationListenerService = notificationListenerService,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModuleKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModuleKosmos.kt
index 556c34d85f8e..0c814c566d63 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModuleKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModuleKosmos.kt
@@ -16,16 +16,13 @@
package com.android.systemui.volume.panel.component.mediaoutput
-import android.content.applicationContext
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.volume.panel.component.mediaoutput.domain.MediaOutputAvailabilityCriteria
import com.android.systemui.volume.panel.component.mediaoutput.ui.composable.MediaOutputComponent
import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.mediaOutputViewModel
+import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria
+import com.android.systemui.volume.panel.domain.availableCriteria
var Kosmos.mediaOutputComponent: MediaOutputComponent by
Kosmos.Fixture { MediaOutputComponent(mediaOutputViewModel) }
-var Kosmos.mediaOutputAvailabilityCriteria by
- Kosmos.Fixture {
- MediaOutputAvailabilityCriteria(applicationContext, testScope.backgroundScope)
- }
+var Kosmos.mediaOutputAvailabilityCriteria: ComponentAvailabilityCriteria by
+ Kosmos.Fixture { availableCriteria }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 420dcfe9cea6..9b0caf561544 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -227,6 +227,7 @@ java_library_static {
"com.android.sysprop.watchdog",
"securebox",
"apache-commons-math",
+ "apache-commons-compress",
"battery_saver_flag_lib",
"notification_flags_lib",
"power_hint_flags_lib",
diff --git a/services/core/java/com/android/server/VpnManagerService.java b/services/core/java/com/android/server/VpnManagerService.java
index 626fa708b4e7..7e68239e0c3b 100644
--- a/services/core/java/com/android/server/VpnManagerService.java
+++ b/services/core/java/com/android/server/VpnManagerService.java
@@ -19,6 +19,7 @@ package com.android.server;
import static android.Manifest.permission.NETWORK_STACK;
import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
+import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -1020,6 +1021,8 @@ public class VpnManagerService extends IVpnManager.Stub {
@Override
@Nullable
public byte[] getFromVpnProfileStore(@NonNull String name) {
+ // TODO(b/307903113): Replace NETWORK_STACK permission and adopt proper permission
+ enforceNetworkStackPermission(mContext);
return mVpnProfileStore.get(name);
}
@@ -1037,6 +1040,8 @@ public class VpnManagerService extends IVpnManager.Stub {
*/
@Override
public boolean putIntoVpnProfileStore(@NonNull String name, @NonNull byte[] blob) {
+ // TODO(b/307903113): Replace NETWORK_STACK permission and adopt proper permission
+ enforceNetworkStackPermission(mContext);
return mVpnProfileStore.put(name, blob);
}
@@ -1052,6 +1057,8 @@ public class VpnManagerService extends IVpnManager.Stub {
*/
@Override
public boolean removeFromVpnProfileStore(@NonNull String name) {
+ // TODO(b/307903113): Replace NETWORK_STACK permission and adopt proper permission
+ enforceNetworkStackPermission(mContext);
return mVpnProfileStore.remove(name);
}
@@ -1069,6 +1076,8 @@ public class VpnManagerService extends IVpnManager.Stub {
@Override
@NonNull
public String[] listFromVpnProfileStore(@NonNull String prefix) {
+ // TODO(b/307903113): Replace NETWORK_STACK permission and adopt proper permission
+ enforceNetworkStackPermission(mContext);
return mVpnProfileStore.list(prefix);
}
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index 8e520dc632c3..96b30d4e1285 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -191,6 +191,9 @@ public class Watchdog implements Dumpable {
"android.hardware.sensors.ISensors/",
"android.hardware.vibrator.IVibrator/",
"android.hardware.vibrator.IVibratorManager/",
+ "android.hardware.wifi.hostapd.IHostapd/",
+ "android.hardware.wifi.IWifi/",
+ "android.hardware.wifi.supplicant.ISupplicant/",
"android.system.suspend.ISystemSuspend/",
};
diff --git a/services/core/java/com/android/server/am/AppBatteryTracker.java b/services/core/java/com/android/server/am/AppBatteryTracker.java
index 374abe0256c1..0bc816e78e7b 100644
--- a/services/core/java/com/android/server/am/AppBatteryTracker.java
+++ b/services/core/java/com/android/server/am/AppBatteryTracker.java
@@ -818,8 +818,10 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy>
void dump(PrintWriter pw, String prefix) {
pw.print(prefix);
pw.println("APP BATTERY STATE TRACKER:");
- // Force an update.
- updateBatteryUsageStatsIfNecessary(mInjector.currentTimeMillis(), true);
+ if (mInjector.getActivityManagerInternal().isBooted()) {
+ // Force an update.
+ updateBatteryUsageStatsIfNecessary(mInjector.currentTimeMillis(), true);
+ }
// Force a check.
scheduleBgBatteryUsageStatsCheck();
// Wait for its completion (as it runs in handler thread for the sake of thread safe)
@@ -878,8 +880,10 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy>
@Override
void dumpAsProto(ProtoOutputStream proto, int uid) {
- // Force an update.
- updateBatteryUsageStatsIfNecessary(mInjector.currentTimeMillis(), true);
+ if (mInjector.getActivityManagerInternal().isBooted()) {
+ // Force an update.
+ updateBatteryUsageStatsIfNecessary(mInjector.currentTimeMillis(), true);
+ }
synchronized (mLock) {
final SparseArray<ImmutableBatteryUsage> uidConsumers = mUidBatteryUsageInWindow;
if (uid != android.os.Process.INVALID_UID) {
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 644077a7e6bb..c8b0a57fe9f0 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -526,6 +526,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
public void systemServicesReady() {
+ mStats.setBatteryHistoryCompressionEnabled(
+ Flags.extendedBatteryHistoryCompressionEnabled());
mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore,
isBatteryUsageStatsAccumulationSupported());
mStats.resetBatteryHistoryOnNewSession(
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
index 740c4f195852..4389dd09ba73 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
@@ -30,6 +30,7 @@ import android.os.ServiceSpecificException;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.Collections;
import java.util.HashMap;
@@ -45,14 +46,14 @@ import java.util.function.Consumer;
*/
/* package */ class ContextHubEndpointManager
implements ContextHubHalEndpointCallback.IEndpointSessionCallback {
+ /** The range of session IDs to use for endpoints */
+ public static final int SERVICE_SESSION_RANGE = 1024;
+
private static final String TAG = "ContextHubEndpointManager";
/** The hub ID of the Context Hub Service. */
private static final long SERVICE_HUB_ID = 0x416e64726f696400L;
- /** The range of session IDs to use for endpoints */
- private static final int SERVICE_SESSION_RANGE = 1024;
-
/** The length of the array that should be returned by HAL requestSessionIdRange */
private static final int SERVICE_SESSION_RANGE_LENGTH = 2;
@@ -400,4 +401,16 @@ import java.util.function.Consumer;
private boolean isSessionIdRangeValid(int minId, int maxId) {
return (minId <= maxId) && (minId >= 0) && (maxId >= 0);
}
+
+ @VisibleForTesting
+ /* package */ int getNumAvailableSessions() {
+ synchronized (mSessionIdLock) {
+ return (mMaxSessionId - mMinSessionId + 1) - mReservedSessionIds.size();
+ }
+ }
+
+ @VisibleForTesting
+ /* package */ int getNumRegisteredClients() {
+ return mEndpointMap.size();
+ }
}
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index d00ac4d9cd11..c93f107d6640 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -1160,6 +1160,9 @@ public class MediaQualityService extends SystemService {
private Bundle convertToCaps(ParameterRange range) {
Bundle bundle = new Bundle();
+ if (range == null || range.numRange == null) {
+ return bundle;
+ }
bundle.putObject("INT_MIN_MAX", range.numRange.getIntMinMax());
bundle.putObject("INT_VALUES_SUPPORTED", range.numRange.getIntValuesSupported());
bundle.putObject("DOUBLE_MIN_MAX", range.numRange.getDoubleMinMax());
@@ -1351,7 +1354,7 @@ public class MediaQualityService extends SystemService {
RemoteCallbackList<IPictureProfileCallback> {
@Override
public void onCallbackDied(IPictureProfileCallback callback) {
- synchronized ("mPictureProfileLock") { //TODO: Change to lock
+ synchronized (mPictureProfileLock) {
for (int i = 0; i < mUserStates.size(); i++) {
int userId = mUserStates.keyAt(i);
UserState userState = getOrCreateUserStateLocked(userId);
@@ -1365,7 +1368,7 @@ public class MediaQualityService extends SystemService {
RemoteCallbackList<ISoundProfileCallback> {
@Override
public void onCallbackDied(ISoundProfileCallback callback) {
- synchronized ("mSoundProfileLock") { //TODO: Change to lock
+ synchronized (mSoundProfileLock) {
for (int i = 0; i < mUserStates.size(); i++) {
int userId = mUserStates.keyAt(i);
UserState userState = getOrCreateUserStateLocked(userId);
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
index 5bd4420e9944..ba61e508bef6 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
@@ -357,6 +357,9 @@ public final class MediaQualityUtils {
*/
public static PictureParameter[] convertPersistableBundleToPictureParameterList(
PersistableBundle params) {
+ if (params == null) {
+ return null;
+ }
List<PictureParameter> pictureParams = new ArrayList<>();
if (params.containsKey(PictureQuality.PARAMETER_BRIGHTNESS)) {
pictureParams.add(PictureParameter.brightness(params.getLong(
@@ -784,6 +787,9 @@ public final class MediaQualityUtils {
*/
public static SoundParameter[] convertPersistableBundleToSoundParameterList(
PersistableBundle params) {
+ if (params == null) {
+ return null;
+ }
//TODO: set EqualizerDetail
List<SoundParameter> soundParams = new ArrayList<>();
if (params.containsKey(SoundQuality.PARAMETER_BALANCE)) {
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 7de7dde8c260..c7737e9f8bbd 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -129,6 +129,7 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.os.UserManager.EnforcingUser;
import android.os.UserManager.QuietModeFlag;
+import android.os.UserManager.UserLogoutability;
import android.os.storage.StorageManager;
import android.os.storage.StorageManagerInternal;
import android.provider.Settings;
@@ -1472,10 +1473,13 @@ public class UserManagerService extends IUserManager.Stub {
return UserHandle.USER_NULL;
}
-
@Override
public @CanBeNULL @UserIdInt int getPreviousFullUserToEnterForeground() {
checkQueryOrCreateUsersPermission("get previous user");
+ return getPreviousFullUserToEnterForegroundUnchecked();
+ }
+
+ private int getPreviousFullUserToEnterForegroundUnchecked() {
int previousUser = UserHandle.USER_NULL;
long latestEnteredTime = 0;
final int currentUser = getCurrentUserId();
@@ -2915,7 +2919,8 @@ public class UserManagerService extends IUserManager.Stub {
* @return A {@link UserManager.UserSwitchabilityResult} flag indicating if the user is
* switchable.
*/
- public @UserManager.UserSwitchabilityResult int getUserSwitchability(int userId) {
+ @Override
+ public @UserManager.UserSwitchabilityResult int getUserSwitchability(@UserIdInt int userId) {
if (Flags.getUserSwitchabilityPermission()) {
if (!hasManageUsersOrPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)) {
throw new SecurityException(
@@ -2994,6 +2999,49 @@ public class UserManagerService extends IUserManager.Stub {
}
@Override
+ public @UserLogoutability int getUserLogoutability(@UserIdInt int userId) {
+ if (!android.multiuser.Flags.logoutUserApi()) {
+ throw new UnsupportedOperationException(
+ "aconfig flag android.multiuser.logout_user_api not enabled");
+ }
+
+ checkManageUsersPermission("getUserLogoutability");
+
+ if (userId == UserHandle.USER_SYSTEM) {
+ return UserManager.LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER;
+ }
+
+ if (userId != getCurrentUserId()) {
+ // TODO(b/393656514): Decide what to do with non-current/background users.
+ // As of now, we are not going to logout a background user. A background user should
+ // simply be stopped instead.
+ return UserManager.LOGOUTABILITY_STATUS_CANNOT_SWITCH;
+ }
+
+ if (getUserSwitchability(userId) != UserManager.SWITCHABILITY_STATUS_OK) {
+ return UserManager.LOGOUTABILITY_STATUS_CANNOT_SWITCH;
+ }
+
+ if (getUserToLogoutCurrentUserTo() == UserHandle.USER_NULL) {
+ return UserManager.LOGOUTABILITY_STATUS_NO_SUITABLE_USER_TO_LOGOUT_TO;
+ }
+
+ return UserManager.LOGOUTABILITY_STATUS_OK;
+ }
+
+ /**
+ * Returns the user to switch to, when logging out current user. If in HSUM and has interactive
+ * system user, then logout would switch to the system user. Otherwise, logout would switch to
+ * the previous foreground user.
+ */
+ private @UserIdInt int getUserToLogoutCurrentUserTo() {
+ if (isHeadlessSystemUserMode() && canSwitchToHeadlessSystemUser()) {
+ return USER_SYSTEM;
+ }
+ return getPreviousFullUserToEnterForegroundUnchecked();
+ }
+
+ @Override
public boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable,
@UserIdInt int userId) {
if (!isUserSwitcherEnabled(userId)) {
diff --git a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java
new file mode 100644
index 000000000000..adf308a522ed
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.power.stats;
+
+import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER;
+
+import android.annotation.NonNull;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.os.BatteryStatsHistory;
+import com.android.internal.os.BatteryStatsHistory.BatteryHistoryFragment;
+
+import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
+import org.apache.commons.compress.compressors.gzip.GzipParameters;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.zip.Deflater;
+import java.util.zip.GZIPInputStream;
+
+public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHistoryStore {
+ public static final String TAG = "BatteryHistoryDirectory";
+ private static final boolean DEBUG = false;
+
+ private static final String FILE_SUFFIX = ".bh";
+
+ // Size of the magic number written at the start of each history file
+ private static final int FILE_FORMAT_BYTES = 4;
+ private static final byte[] FILE_FORMAT_PARCEL = {0x50, 0x52, 0x43, 0x4c}; // PRCL
+ private static final byte[] FILE_FORMAT_COMPRESSED_PARCEL = {0x47, 0x5a, 0x49, 0x50}; // GZIP
+
+ static class BatteryHistoryFile extends BatteryHistoryFragment {
+ public final AtomicFile atomicFile;
+
+ BatteryHistoryFile(File directory, long monotonicTimeMs) {
+ super(monotonicTimeMs);
+ atomicFile = new AtomicFile(new File(directory, monotonicTimeMs + FILE_SUFFIX));
+ }
+
+ @Override
+ public String toString() {
+ return atomicFile.getBaseFile().toString();
+ }
+ }
+
+ interface Compressor {
+ void compress(OutputStream stream, byte[] data) throws IOException;
+ void uncompress(byte[] data, InputStream stream) throws IOException;
+
+ default void readFully(byte[] data, InputStream stream) throws IOException {
+ int pos = 0;
+ while (pos < data.length) {
+ int count = stream.read(data, pos, data.length - pos);
+ if (count == -1) {
+ throw new IOException("Invalid battery history file format");
+ }
+ pos += count;
+ }
+ }
+ }
+
+ static final Compressor DEFAULT_COMPRESSOR = new Compressor() {
+ @Override
+ public void compress(OutputStream stream, byte[] data) throws IOException {
+ // With the BEST_SPEED hint, we see ~4x improvement in write latency over
+ // GZIPOutputStream.
+ GzipParameters parameters = new GzipParameters();
+ parameters.setCompressionLevel(Deflater.BEST_SPEED);
+ GzipCompressorOutputStream os = new GzipCompressorOutputStream(stream, parameters);
+ os.write(data);
+ os.finish();
+ os.flush();
+ }
+
+ @Override
+ public void uncompress(byte[] data, InputStream stream) throws IOException {
+ readFully(data, new GZIPInputStream(stream));
+ }
+ };
+
+ private final File mDirectory;
+ private int mMaxHistorySize;
+ private boolean mInitialized;
+ private final List<BatteryHistoryFile> mHistoryFiles = new ArrayList<>();
+ private final ReentrantLock mLock = new ReentrantLock();
+ private final Compressor mCompressor;
+ private boolean mWaitForDirectoryLock = false;
+ private boolean mFileCompressionEnabled;
+
+ public BatteryHistoryDirectory(@NonNull File directory, int maxHistorySize) {
+ this(directory, maxHistorySize, DEFAULT_COMPRESSOR);
+ }
+
+ public BatteryHistoryDirectory(@NonNull File directory, int maxHistorySize,
+ Compressor compressor) {
+ mDirectory = directory;
+ mMaxHistorySize = maxHistorySize;
+ if (mMaxHistorySize == 0) {
+ Slog.w(TAG, "maxHistorySize should not be zero");
+ }
+ mCompressor = compressor;
+ }
+
+ public void setFileCompressionEnabled(boolean enabled) {
+ mFileCompressionEnabled = enabled;
+ }
+
+ void setMaxHistorySize(int maxHistorySize) {
+ mMaxHistorySize = maxHistorySize;
+ trim();
+ }
+
+ /**
+ * Returns the maximum storage size allocated to battery history.
+ */
+ public int getMaxHistorySize() {
+ return mMaxHistorySize;
+ }
+
+ @Override
+ public void lock() {
+ mLock.lock();
+ }
+
+ /**
+ * Turns "tryLock" into "lock" to prevent flaky unit tests.
+ * Should only be called from unit tests.
+ */
+ @VisibleForTesting
+ void makeDirectoryLockUnconditional() {
+ mWaitForDirectoryLock = true;
+ }
+
+ @Override
+ public boolean tryLock() {
+ if (mWaitForDirectoryLock) {
+ mLock.lock();
+ return true;
+ }
+ return mLock.tryLock();
+ }
+
+ @Override
+ public void writeFragment(BatteryHistoryFragment fragment,
+ @NonNull byte[] data, boolean fragmentComplete) {
+ AtomicFile file = ((BatteryHistoryFile) fragment).atomicFile;
+ FileOutputStream fos = null;
+ try {
+ final long startTimeMs = SystemClock.uptimeMillis();
+ fos = file.startWrite();
+ fos.write(FILE_FORMAT_PARCEL);
+ writeInt(fos, data.length);
+ fos.write(data);
+ fos.flush();
+ file.finishWrite(fos);
+ if (DEBUG) {
+ Slog.d(TAG, "writeHistoryFragment file:" + file.getBaseFile().getPath()
+ + " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs)
+ + " bytes:" + data.length);
+ }
+ if (fragmentComplete) {
+ if (mFileCompressionEnabled) {
+ BackgroundThread.getHandler().post(
+ () -> writeHistoryFragmentCompressed(file, data));
+ }
+ BackgroundThread.getHandler().post(()-> trim());
+ }
+ } catch (IOException e) {
+ Slog.w(TAG, "Error writing battery history fragment", e);
+ file.failWrite(fos);
+ }
+ }
+
+ private void writeHistoryFragmentCompressed(AtomicFile file, byte[] data) {
+ long uncompressedSize = data.length;
+ if (uncompressedSize == 0) {
+ return;
+ }
+
+ Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.compressHistoryFile");
+ lock();
+ FileOutputStream fos = null;
+ try {
+ long startTimeNs = System.nanoTime();
+ fos = file.startWrite();
+ fos.write(FILE_FORMAT_COMPRESSED_PARCEL);
+ writeInt(fos, data.length);
+
+ mCompressor.compress(fos, data);
+ file.finishWrite(fos);
+
+ if (DEBUG) {
+ long endTimeNs = System.nanoTime();
+ long compressedSize = file.getBaseFile().length();
+ Slog.i(TAG, String.format(Locale.ENGLISH,
+ "Compressed battery history file %s original size: %d compressed: %d "
+ + "(%.1f%%) elapsed: %.2f ms",
+ file.getBaseFile(), uncompressedSize, compressedSize,
+ (uncompressedSize - compressedSize) * 100.0 / uncompressedSize,
+ (endTimeNs - startTimeNs) / 1000000.0));
+ }
+ } catch (Exception e) {
+ Slog.w(TAG, "Error compressing battery history chunk " + file, e);
+ file.failWrite(fos);
+ } finally {
+ unlock();
+ Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
+ }
+ }
+
+ @Override
+ public byte[] readFragment(BatteryHistoryFragment fragment) {
+ AtomicFile file = ((BatteryHistoryFile) fragment).atomicFile;
+ if (!file.exists()) {
+ deleteFragment(fragment);
+ return null;
+ }
+ final long start = SystemClock.uptimeMillis();
+ Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read");
+ try (FileInputStream stream = file.openRead()) {
+ byte[] header = new byte[FILE_FORMAT_BYTES];
+ if (stream.read(header, 0, FILE_FORMAT_BYTES) == -1) {
+ Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile());
+ deleteFragment(fragment);
+ return null;
+ }
+
+ boolean isCompressed;
+ if (Arrays.equals(header, FILE_FORMAT_COMPRESSED_PARCEL)) {
+ isCompressed = true;
+ } else if (Arrays.equals(header, FILE_FORMAT_PARCEL)) {
+ isCompressed = false;
+ } else {
+ Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile());
+ deleteFragment(fragment);
+ return null;
+ }
+
+ int size = readInt(stream);
+ if (size < 0 || size > 10000000) { // Validity check to avoid a crash
+ Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile());
+ deleteFragment(fragment);
+ return null;
+ }
+
+ byte[] data = new byte[size];
+ if (isCompressed) {
+ mCompressor.uncompress(data, stream);
+ } else {
+ int pos = 0;
+ while (pos < data.length) {
+ int count = stream.read(data, pos, data.length - pos);
+ if (count == -1) {
+ throw new IOException("Invalid battery history file format");
+ }
+ pos += count;
+ }
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "readHistoryFragment:" + file.getBaseFile().getPath()
+ + " duration ms:" + (SystemClock.uptimeMillis() - start));
+ }
+ return data;
+ } catch (Exception e) {
+ Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
+ deleteFragment(fragment);
+ return null;
+ } finally {
+ Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
+ }
+ }
+
+ private void deleteFragment(BatteryHistoryFragment fragment) {
+ mHistoryFiles.remove(fragment);
+ ((BatteryHistoryFile) fragment).atomicFile.delete();
+ }
+
+ @Override
+ public void unlock() {
+ mLock.unlock();
+ }
+
+ @Override
+ public boolean isLocked() {
+ return mLock.isLocked();
+ }
+
+ private void ensureInitialized() {
+ if (mInitialized) {
+ return;
+ }
+
+ Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
+ mDirectory.mkdirs();
+ if (!mDirectory.exists()) {
+ Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath());
+ }
+
+ final List<File> toRemove = new ArrayList<>();
+ final Set<BatteryHistoryFile> dedup = new ArraySet<>();
+ mDirectory.listFiles((dir, name) -> {
+ final int b = name.lastIndexOf(FILE_SUFFIX);
+ if (b <= 0) {
+ toRemove.add(new File(dir, name));
+ return false;
+ }
+ try {
+ long monotonicTime = Long.parseLong(name.substring(0, b));
+ dedup.add(new BatteryHistoryFile(mDirectory, monotonicTime));
+ } catch (NumberFormatException e) {
+ toRemove.add(new File(dir, name));
+ return false;
+ }
+ return true;
+ });
+ if (!dedup.isEmpty()) {
+ mHistoryFiles.addAll(dedup);
+ Collections.sort(mHistoryFiles);
+ }
+ mInitialized = true;
+ if (!toRemove.isEmpty()) {
+ // Clear out legacy history files, which did not follow the X-Y.bin naming format.
+ BackgroundThread.getHandler().post(() -> {
+ lock();
+ try {
+ for (File file : toRemove) {
+ file.delete();
+ }
+ } finally {
+ unlock();
+ Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
+ }
+ });
+ } else {
+ Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public List<BatteryHistoryFragment> getFragments() {
+ ensureInitialized();
+ return (List<BatteryHistoryFragment>)
+ (List<? extends BatteryHistoryFragment>) mHistoryFiles;
+ }
+
+ @VisibleForTesting
+ List<String> getFileNames() {
+ ensureInitialized();
+ lock();
+ try {
+ List<String> names = new ArrayList<>();
+ for (BatteryHistoryFile historyFile : mHistoryFiles) {
+ names.add(historyFile.atomicFile.getBaseFile().getName());
+ }
+ return names;
+ } finally {
+ unlock();
+ }
+ }
+
+ @Override
+ public BatteryHistoryFragment getEarliestFragment() {
+ ensureInitialized();
+ lock();
+ try {
+ if (!mHistoryFiles.isEmpty()) {
+ return mHistoryFiles.get(0);
+ }
+ return null;
+ } finally {
+ unlock();
+ }
+ }
+
+ @Override
+ public BatteryHistoryFragment getLatestFragment() {
+ ensureInitialized();
+ lock();
+ try {
+ if (!mHistoryFiles.isEmpty()) {
+ return mHistoryFiles.get(mHistoryFiles.size() - 1);
+ }
+ return null;
+ } finally {
+ unlock();
+ }
+ }
+
+ @Override
+ public BatteryHistoryFragment createFragment(long monotonicStartTime) {
+ ensureInitialized();
+
+ BatteryHistoryFile file = new BatteryHistoryFile(mDirectory, monotonicStartTime);
+ lock();
+ try {
+ try {
+ file.atomicFile.getBaseFile().createNewFile();
+ } catch (IOException e) {
+ Slog.e(TAG, "Could not create history file: " + file);
+ }
+ mHistoryFiles.add(file);
+ } finally {
+ unlock();
+ }
+
+ return file;
+ }
+
+ @Override
+ public BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs,
+ long endTimeMs) {
+ ensureInitialized();
+
+ if (!mLock.isHeldByCurrentThread()) {
+ throw new IllegalStateException("Iterating battery history without a lock");
+ }
+
+ int nextFileIndex = 0;
+ int firstFileIndex = 0;
+ // skip the last file because its data is in history buffer.
+ int lastFileIndex = mHistoryFiles.size() - 2;
+ for (int i = lastFileIndex; i >= 0; i--) {
+ BatteryHistoryFragment fragment = mHistoryFiles.get(i);
+ if (current != null && fragment.monotonicTimeMs == current.monotonicTimeMs) {
+ nextFileIndex = i + 1;
+ }
+ if (fragment.monotonicTimeMs > endTimeMs) {
+ lastFileIndex = i - 1;
+ }
+ if (fragment.monotonicTimeMs <= startTimeMs) {
+ firstFileIndex = i;
+ break;
+ }
+ }
+
+ if (nextFileIndex < firstFileIndex) {
+ nextFileIndex = firstFileIndex;
+ }
+
+ if (nextFileIndex <= lastFileIndex) {
+ return mHistoryFiles.get(nextFileIndex);
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean hasCompletedFragments() {
+ ensureInitialized();
+
+ lock();
+ try {
+ // Active file is partial and does not count as "competed"
+ return mHistoryFiles.size() > 1;
+ } finally {
+ unlock();
+ }
+ }
+
+ @Override
+ public int getSize() {
+ ensureInitialized();
+
+ lock();
+ try {
+ int ret = 0;
+ for (int i = 0; i < mHistoryFiles.size() - 1; i++) {
+ ret += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length();
+ }
+ return ret;
+ } finally {
+ unlock();
+ }
+ }
+
+ @Override
+ public void reset() {
+ ensureInitialized();
+
+ lock();
+ try {
+ if (DEBUG) {
+ Slog.i(TAG, "********** CLEARING HISTORY!");
+ }
+ for (BatteryHistoryFile file : mHistoryFiles) {
+ file.atomicFile.delete();
+ }
+ mHistoryFiles.clear();
+ } finally {
+ unlock();
+ }
+ }
+
+ private void trim() {
+ ensureInitialized();
+
+ Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.trim");
+ try {
+ lock();
+ try {
+ // if there is more history stored than allowed, delete oldest history files.
+ int size = 0;
+ for (int i = 0; i < mHistoryFiles.size(); i++) {
+ size += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length();
+ }
+ while (size > mMaxHistorySize) {
+ BatteryHistoryFile oldest = mHistoryFiles.get(0);
+ int length = (int) oldest.atomicFile.getBaseFile().length();
+ oldest.atomicFile.delete();
+ mHistoryFiles.remove(0);
+ size -= length;
+ }
+ } finally {
+ unlock();
+ }
+ } finally {
+ Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
+ }
+ }
+
+ private static void writeInt(OutputStream stream, int value) throws IOException {
+ stream.write(value >> 24);
+ stream.write(value >> 16);
+ stream.write(value >> 8);
+ stream.write(value >> 0);
+ }
+
+ private static int readInt(InputStream stream) throws IOException {
+ return (readByte(stream) << 24)
+ | (readByte(stream) << 16)
+ | (readByte(stream) << 8)
+ | (readByte(stream) << 0);
+ }
+
+ private static int readByte(InputStream stream) throws IOException {
+ int out = stream.read();
+ if (out == -1) {
+ throw new IOException();
+ }
+ return out;
+ }
+}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 68768b8fa223..90bc54b06c0a 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -195,6 +195,8 @@ public class BatteryStatsImpl extends BatteryStats {
private static final boolean DEBUG_BINDER_STATS = false;
private static final boolean DEBUG_MEMORY = false;
+ private static final String HISTORY_DIR = "battery-history";
+
// TODO: remove "tcp" from network methods, since we measure total stats.
// Current on-disk Parcel version. Must be updated when the format of the parcelable changes
@@ -1143,6 +1145,8 @@ public class BatteryStatsImpl extends BatteryStats {
private int mBatteryTemperature;
private int mBatteryVoltageMv;
+ @Nullable
+ private final BatteryHistoryDirectory mBatteryHistoryDirectory;
@NonNull
private final BatteryStatsHistory mHistory;
@@ -11476,7 +11480,10 @@ public class BatteryStatsImpl extends BatteryStats {
@NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile,
@NonNull CpuScalingPolicies cpuScalingPolicies,
@NonNull PowerStatsUidResolver powerStatsUidResolver) {
- this(config, clock, monotonicClock, systemDir, handler, platformIdleStateCallback,
+ this(config, clock, monotonicClock, systemDir,
+ systemDir != null ? new BatteryHistoryDirectory(new File(systemDir, HISTORY_DIR),
+ config.getMaxHistorySizeBytes()) : null,
+ handler, platformIdleStateCallback,
energyStatsRetriever, userInfoProvider, powerProfile, cpuScalingPolicies,
powerStatsUidResolver, new FrameworkStatsLogger(),
new BatteryStatsHistory.TraceDelegate(), new BatteryStatsHistory.EventLogger());
@@ -11484,6 +11491,7 @@ public class BatteryStatsImpl extends BatteryStats {
public BatteryStatsImpl(@NonNull BatteryStatsConfig config, @NonNull Clock clock,
@NonNull MonotonicClock monotonicClock, @Nullable File systemDir,
+ @Nullable BatteryHistoryDirectory batteryHistoryDirectory,
@NonNull Handler handler, @Nullable PlatformIdleStateCallback platformIdleStateCallback,
@Nullable EnergyStatsRetriever energyStatsRetriever,
@NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile,
@@ -11517,9 +11525,10 @@ public class BatteryStatsImpl extends BatteryStats {
mDailyFile = null;
}
- mHistory = new BatteryStatsHistory(null /* historyBuffer */, systemDir,
- mConstants.MAX_HISTORY_SIZE, mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator,
- mClock, mMonotonicClock, traceDelegate, eventLogger);
+ mBatteryHistoryDirectory = batteryHistoryDirectory;
+ mHistory = new BatteryStatsHistory(null /* historyBuffer */, mConstants.MAX_HISTORY_BUFFER,
+ mBatteryHistoryDirectory, mStepDetailsCalculator, mClock, mMonotonicClock,
+ traceDelegate, eventLogger);
mCpuPowerStatsCollector = new CpuPowerStatsCollector(mPowerStatsCollectorInjector);
mCpuPowerStatsCollector.addConsumer(this::recordPowerStats);
@@ -12060,7 +12069,7 @@ public class BatteryStatsImpl extends BatteryStats {
}
public int getHistoryTotalSize() {
- return mHistory.getMaxHistorySize();
+ return mBatteryHistoryDirectory.getMaxHistorySize();
}
public int getHistoryUsedSize() {
@@ -12160,6 +12169,13 @@ public class BatteryStatsImpl extends BatteryStats {
mResetBatteryHistoryOnNewSession = enabled;
}
+ /**
+ * Enables or disables battery history file compression.
+ */
+ public void setBatteryHistoryCompressionEnabled(boolean enabled) {
+ mBatteryHistoryDirectory.setFileCompressionEnabled(enabled);
+ }
+
@GuardedBy("this")
public void resetAllStatsAndHistoryLocked(int reason) {
final long mSecUptime = mClock.uptimeMillis();
@@ -16354,7 +16370,9 @@ public class BatteryStatsImpl extends BatteryStats {
*/
@VisibleForTesting
public void onChange() {
- mHistory.setMaxHistorySize(MAX_HISTORY_SIZE);
+ if (mBatteryHistoryDirectory != null) {
+ mBatteryHistoryDirectory.setMaxHistorySize(MAX_HISTORY_SIZE);
+ }
mHistory.setMaxHistoryBufferSize(MAX_HISTORY_BUFFER);
}
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index c8dbbd29823c..521ee58decea 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -97,3 +97,13 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "extended_battery_history_compression_enabled"
+ namespace: "backstage_power"
+ description: "Compress each battery history chunk on disk"
+ bug: "381937912"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 6f76618b0029..247264f049d6 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -3035,6 +3035,12 @@ class ActivityStarter {
}
}
+ if (com.android.window.flags.Flags.fixLayoutExistingTask()) {
+ // Layout the task to ensure the Task is in correct bounds.
+ mSupervisor.getLaunchParamsController().layoutTask(intentTask,
+ mStartActivity.info.windowLayout, mStartActivity, mSourceRecord, mOptions);
+ }
+
// If the target task is not in the front, then we need to bring it to the front.
final boolean differentTopTask;
if (mTargetRootTask.getDisplayArea() == mPreferredTaskDisplayArea) {
diff --git a/services/core/java/com/android/server/wm/AppCompatConfiguration.java b/services/core/java/com/android/server/wm/AppCompatConfiguration.java
index 9a15c4a8bff2..0d8950b6cc45 100644
--- a/services/core/java/com/android/server/wm/AppCompatConfiguration.java
+++ b/services/core/java/com/android/server/wm/AppCompatConfiguration.java
@@ -311,7 +311,7 @@ final class AppCompatConfiguration {
// Whether should ignore app requested orientation in response to an app
// calling Activity#setRequestedOrientation. See
- // LetterboxUiController#shouldIgnoreRequestedOrientation for details.
+ // AppCompatOrientationPolicy#shouldIgnoreRequestedOrientation for details.
private final boolean mIsPolicyForIgnoringRequestedOrientationEnabled;
// Flags dynamically updated with {@link android.provider.DeviceConfig}.
@@ -1259,7 +1259,7 @@ final class AppCompatConfiguration {
/**
* Whether should ignore app requested orientation in response to an app calling
* {@link android.app.Activity#setRequestedOrientation}. See {@link
- * LetterboxUiController#shouldIgnoreRequestedOrientation} for details.
+ * AppCompatOrientationPolicy#shouldIgnoreRequestedOrientation} for details.
*/
boolean isPolicyForIgnoringRequestedOrientationEnabled() {
return mIsPolicyForIgnoringRequestedOrientationEnabled;
diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java
index a7c52bd1fc38..b47786675fc9 100644
--- a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java
@@ -32,6 +32,7 @@ import android.annotation.Nullable;
import android.graphics.Rect;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
import java.io.PrintWriter;
import java.util.function.Supplier;
@@ -97,8 +98,11 @@ class AppCompatReachabilityPolicy {
private void handleHorizontalDoubleTap(int x) {
final AppCompatReachabilityOverrides reachabilityOverrides =
mActivityRecord.mAppCompatController.getReachabilityOverrides();
- if (!reachabilityOverrides.isHorizontalReachabilityEnabled()
- || mActivityRecord.isInTransition()) {
+ // We don't return early when the Shell letterbox implementation is enabled because
+ // double tap is always sent via transitions.
+ final boolean isInTransition = !Flags.appCompatRefactoring()
+ && mActivityRecord.isInTransition();
+ if (!reachabilityOverrides.isHorizontalReachabilityEnabled() || isInTransition) {
return;
}
final Rect letterboxInnerFrame = getLetterboxInnerFrame();
@@ -143,8 +147,11 @@ class AppCompatReachabilityPolicy {
private void handleVerticalDoubleTap(int y) {
final AppCompatReachabilityOverrides reachabilityOverrides =
mActivityRecord.mAppCompatController.getReachabilityOverrides();
- if (!reachabilityOverrides.isVerticalReachabilityEnabled()
- || mActivityRecord.isInTransition()) {
+ // We don't return early when the Shell letterbox implementation is enabled because
+ // double tap is always sent via transitions.
+ final boolean isInTransition = !Flags.appCompatRefactoring()
+ && mActivityRecord.isInTransition();
+ if (!reachabilityOverrides.isVerticalReachabilityEnabled() || isInTransition) {
return;
}
final Rect letterboxInnerFrame = getLetterboxInnerFrame();
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index 548addbef39d..ac987929a142 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -74,6 +74,12 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
appendLog("task null, skipping");
return RESULT_SKIP;
}
+ if (com.android.window.flags.Flags.fixLayoutExistingTask()
+ && task.getOrganizedTask() != null) {
+ appendLog("task is organized, skipping");
+ return RESULT_SKIP;
+ }
+
if (!task.isActivityTypeStandardOrUndefined()) {
appendLog("not standard or undefined activity type, skipping");
return RESULT_SKIP;
diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java
index 4f5c0c8ecf6e..fa65bda7104d 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsController.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsController.java
@@ -124,31 +124,19 @@ class LaunchParamsController {
}
}
- /**
- * A convenience method for laying out a task.
- * @return {@code true} if bounds were set on the task. {@code false} otherwise.
- */
- boolean layoutTask(Task task, WindowLayout layout) {
- return layoutTask(task, layout, null /*activity*/, null /*source*/, null /*options*/);
- }
-
+ /** @return {@code true} if bounds were set on the task. {@code false} otherwise. */
boolean layoutTask(Task task, WindowLayout layout, ActivityRecord activity,
ActivityRecord source, ActivityOptions options) {
calculate(task, layout, activity, source, options, null /* request */, PHASE_BOUNDS,
mTmpParams);
// No changes, return.
- if (mTmpParams.isEmpty()) {
+ if (mTmpParams.isEmpty() || mTmpParams.mBounds.isEmpty()) {
return false;
}
mService.deferWindowLayout();
-
try {
- if (mTmpParams.mBounds.isEmpty()) {
- return false;
- }
-
if (task.getRootTask().inMultiWindowMode()) {
task.setBounds(mTmpParams.mBounds);
return true;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index a8686d3e4ea7..4b07e9e232be 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5232,7 +5232,6 @@ class Task extends TaskFragment {
final boolean[] resumed = new boolean[1];
final TaskFragment topFragment = topActivity.getTaskFragment();
- resumed[0] = topFragment.resumeTopActivity(prev, options, deferPause);
forAllLeafTaskFragments(f -> {
if (topFragment == f) {
return;
@@ -5242,6 +5241,7 @@ class Task extends TaskFragment {
}
resumed[0] |= f.resumeTopActivity(prev, options, deferPause);
}, true);
+ resumed[0] |= topFragment.resumeTopActivity(prev, options, deferPause);
return resumed[0];
}
@@ -6033,7 +6033,7 @@ class Task extends TaskFragment {
IVoiceInteractor voiceInteractor, boolean toTop, ActivityRecord activity,
ActivityRecord source, ActivityOptions options) {
- Task task;
+ final Task task;
if (canReuseAsLeafTask()) {
// This root task will only contain one task, so just return itself since all root
// tasks ara now tasks and all tasks are now root tasks.
@@ -6043,7 +6043,6 @@ class Task extends TaskFragment {
final int taskId = activity != null
? mTaskSupervisor.getNextTaskIdForUser(activity.mUserId)
: mTaskSupervisor.getNextTaskIdForUser();
- final int activityType = getActivityType();
task = new Task.Builder(mAtmService)
.setTaskId(taskId)
.setActivityInfo(info)
@@ -6056,17 +6055,21 @@ class Task extends TaskFragment {
.build();
}
- int displayId = getDisplayId();
- if (displayId == INVALID_DISPLAY) displayId = DEFAULT_DISPLAY;
- final boolean isLockscreenShown = mAtmService.mTaskSupervisor.getKeyguardController()
- .isKeyguardOrAodShowing(displayId);
- if (!mTaskSupervisor.getLaunchParamsController()
- .layoutTask(task, info.windowLayout, activity, source, options)
- && !getRequestedOverrideBounds().isEmpty()
- && task.isResizeable() && !isLockscreenShown) {
- task.setBounds(getRequestedOverrideBounds());
+ if (com.android.window.flags.Flags.fixLayoutExistingTask()) {
+ mTaskSupervisor.getLaunchParamsController()
+ .layoutTask(task, info.windowLayout, activity, source, options);
+ } else {
+ int displayId = getDisplayId();
+ if (displayId == INVALID_DISPLAY) displayId = DEFAULT_DISPLAY;
+ final boolean isLockscreenShown =
+ mAtmService.mKeyguardController.isKeyguardOrAodShowing(displayId);
+ if (!mTaskSupervisor.getLaunchParamsController()
+ .layoutTask(task, info.windowLayout, activity, source, options)
+ && !getRequestedOverrideBounds().isEmpty()
+ && task.isResizeable() && !isLockscreenShown) {
+ task.setBounds(getRequestedOverrideBounds());
+ }
}
-
return task;
}
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index b4c2c0173767..a11f4b1f3fc3 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -52,6 +52,7 @@ import static android.window.WindowContainerTransaction.Change.CHANGE_FOCUSABLE;
import static android.window.WindowContainerTransaction.Change.CHANGE_FORCE_TRANSLUCENT;
import static android.window.WindowContainerTransaction.Change.CHANGE_HIDDEN;
import static android.window.WindowContainerTransaction.Change.CHANGE_RELATIVE_BOUNDS;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER;
@@ -77,6 +78,8 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_SHORTCUT;
+import static android.window.WindowContainerTransaction.HierarchyOp.REACHABILITY_EVENT_X;
+import static android.window.WindowContainerTransaction.HierarchyOp.REACHABILITY_EVENT_Y;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_ORGANIZER;
import static com.android.server.wm.ActivityRecord.State.PAUSING;
@@ -1196,6 +1199,30 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
caller.mPid, caller.mUid, taskId, safeOptions));
break;
}
+ case HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY: {
+ int doubleTapX = hop.getAppCompatOptions().getInt(REACHABILITY_EVENT_X);
+ int doubleTapY = hop.getAppCompatOptions().getInt(REACHABILITY_EVENT_Y);
+ final WindowContainer<?> wc = WindowContainer.fromBinder(hop.getContainer());
+ if (wc == null) {
+ break;
+ }
+ final Task currentTask = wc.asTask();
+ if (chain.mTransition != null) {
+ chain.mTransition.collect(wc);
+ }
+ if (currentTask != null) {
+ final ActivityRecord top = currentTask.topRunningActivity();
+ if (top != null) {
+ if (chain.mTransition != null) {
+ chain.mTransition.collect(top);
+ }
+ top.mAppCompatController.getReachabilityPolicy().handleDoubleTap(doubleTapX,
+ doubleTapY);
+ }
+ }
+ effects |= TRANSACT_EFFECTS_CLIENT_CONFIG;
+ break;
+ }
case HIERARCHY_OP_TYPE_REORDER:
case HIERARCHY_OP_TYPE_REPARENT: {
final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer());
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index d08715586580..54d1b7c030f4 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -211,7 +211,6 @@ public class InputMethodServiceTest {
* lose flags like HIDE_IMPLICIT_ONLY.
*/
@Test
- @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
public void testShowHideSelf() throws Exception {
setShowImeWithHardKeyboard(true /* enabled */);
@@ -223,13 +222,16 @@ public class InputMethodServiceTest {
true /* inputViewStarted */);
assertThat(mInputMethodService.isInputViewShown()).isTrue();
- // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown).
- Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
- verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
- false /* expected */,
- true /* inputViewStarted */);
- assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+ // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown).
+ Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestHideSelf(
+ InputMethodManager.HIDE_IMPLICIT_ONLY),
+ false /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ }
// IME request to hide itself without any flags, expect hidden.
Log.i(TAG, "Call IMS#requestHideSelf(0)");
@@ -237,23 +239,32 @@ public class InputMethodServiceTest {
() -> mInputMethodService.requestHideSelf(0 /* flags */),
true /* expected */,
false /* inputViewStarted */);
- assertThat(mInputMethodService.isInputViewShown()).isFalse();
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+ // The IME visibility is only sent at the end of the animation. Therefore, we have to
+ // wait until the visibility was sent to the server and the IME window hidden.
+ eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
+ } else {
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
+ }
- // IME request to show itself with flag SHOW_IMPLICIT, expect shown.
- Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
- verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
- true /* expected */,
- true /* inputViewStarted */);
- assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+ // IME request to show itself with flag SHOW_IMPLICIT, expect shown.
+ Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
- // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden.
- Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
- verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
- true /* expected */,
- false /* inputViewStarted */);
- assertThat(mInputMethodService.isInputViewShown()).isFalse();
+ // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden.
+ Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
+ verifyInputViewStatusOnMainSync(
+ () -> mInputMethodService.requestHideSelf(
+ InputMethodManager.HIDE_IMPLICIT_ONLY),
+ true /* expected */,
+ false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
+ }
}
/**
@@ -992,35 +1003,26 @@ public class InputMethodServiceTest {
* @param expected whether the runnable is expected to trigger the signal.
* @param orientationPortrait whether the orientation is expected to be portrait.
*/
- private void verifyFullscreenMode(
- Runnable runnable, boolean expected, boolean orientationPortrait)
- throws InterruptedException {
- CountDownLatch signal = new CountDownLatch(1);
- mInputMethodService.setCountDownLatchForTesting(signal);
-
- // Runnable to trigger onConfigurationChanged()
- try {
- runnable.run();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- // Waits for onConfigurationChanged() to finish.
- mInstrumentation.waitForIdleSync();
- boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
- if (expected && !completed) {
- fail("Timed out waiting for onConfigurationChanged()");
- } else if (!expected && completed) {
- fail("Unexpected call onConfigurationChanged()");
+ private void verifyFullscreenMode(@NonNull Runnable runnable, boolean expected,
+ boolean orientationPortrait) throws InterruptedException {
+ verifyInputViewStatus(runnable, expected, false /* inputViewStarted */);
+ if (expected) {
+ // Wait for the TestActivity to be recreated.
+ eventually(() ->
+ assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity));
+ // Get the new TestActivity.
+ mActivity = TestActivity.getLastCreatedInstance();
}
- clickOnEditorText();
- eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isTrue());
+ verifyInputViewStatusOnMainSync(
+ () -> mActivity.showImeWithWindowInsetsController(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
assertThat(mInputMethodService.getResources().getConfiguration().orientation)
- .isEqualTo(
- orientationPortrait
- ? Configuration.ORIENTATION_PORTRAIT
- : Configuration.ORIENTATION_LANDSCAPE);
+ .isEqualTo(orientationPortrait
+ ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE);
EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN).isEqualTo(0);
assertThat(editorInfo.internalImeOptions & EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT)
@@ -1029,7 +1031,19 @@ public class InputMethodServiceTest {
assertThat(mInputMethodService.onEvaluateFullscreenMode()).isEqualTo(!orientationPortrait);
assertThat(mInputMethodService.isFullscreenMode()).isEqualTo(!orientationPortrait);
- mUiDevice.pressBack();
+ // Hide IME before finishing the run.
+ verifyInputViewStatusOnMainSync(
+ () -> mActivity.hideImeWithWindowInsetsController(),
+ true /* expected */,
+ false /* inputViewStarted */);
+
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
+ // The IME visibility is only sent at the end of the animation. Therefore, we have to
+ // wait until the visibility was sent to the server and the IME window hidden.
+ eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
+ } else {
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
+ }
}
private void prepareIme() throws Exception {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index b4e885fe5661..f79cb1105611 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -61,9 +61,12 @@ import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
+import android.telecom.TelecomManager;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
@@ -166,6 +169,7 @@ public final class UserManagerServiceTest {
private @Mock PackageManagerInternal mPackageManagerInternal;
private @Mock KeyguardManager mKeyguardManager;
private @Mock PowerManager mPowerManager;
+ private @Mock TelecomManager mTelecomManager;
/**
* Reference to the {@link UserManagerService} being tested.
@@ -192,6 +196,7 @@ public final class UserManagerServiceTest {
when(mSpiedContext.getSystemService(StorageManager.class)).thenReturn(mStorageManager);
doReturn(mKeyguardManager).when(mSpiedContext).getSystemService(KeyguardManager.class);
when(mSpiedContext.getSystemService(PowerManager.class)).thenReturn(mPowerManager);
+ when(mSpiedContext.getSystemService(TelecomManager.class)).thenReturn(mTelecomManager);
mockGetLocalService(LockSettingsInternal.class, mLockSettingsInternal);
mockGetLocalService(PackageManagerInternal.class, mPackageManagerInternal);
doNothing().when(mSpiedContext).sendBroadcastAsUser(any(), any(), any());
@@ -885,9 +890,7 @@ public final class UserManagerServiceTest {
.getInteger(com.android.internal.R.integer.config_hsumBootStrategy);
// Even if the headless system user switchable flag is true, the boot user should be the
// first switchable full user.
- doReturn(true)
- .when(mSpyResources)
- .getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser);
+ mockCanSwitchToHeadlessSystemUser(true);
assertThat(mUms.getBootUser()).isEqualTo(USER_ID);
}
@@ -906,6 +909,75 @@ public final class UserManagerServiceTest {
() -> mUms.getBootUser());
}
+ @Test
+ @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+ public void testGetUserLogoutability_HsumAndInteractiveHeadlessSystem_UserCanLogout()
+ throws Exception {
+ setSystemUserHeadless(true);
+ addUser(USER_ID);
+ setLastForegroundTime(USER_ID, 1_000_000L);
+ mockCurrentUser(USER_ID);
+
+ mockCanSwitchToHeadlessSystemUser(true);
+ mockUserIsInCall(false);
+
+ assertThat(mUms.getUserLogoutability(USER_ID))
+ .isEqualTo(UserManager.LOGOUTABILITY_STATUS_OK);
+ }
+
+ @Test
+ @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+ public void testGetUserLogoutability_HsumAndNonInteractiveHeadlessSystem_UserCannotLogout()
+ throws Exception {
+ setSystemUserHeadless(true);
+ mockCanSwitchToHeadlessSystemUser(false);
+ addUser(USER_ID);
+ setLastForegroundTime(USER_ID, 1_000_000L);
+ mockCurrentUser(USER_ID);
+ mockUserIsInCall(false);
+
+ assertThat(mUms.getUserLogoutability(USER_ID))
+ .isEqualTo(UserManager.LOGOUTABILITY_STATUS_NO_SUITABLE_USER_TO_LOGOUT_TO);
+ }
+
+ @Test
+ @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+ public void testGetUserLogoutability_Hsum_SystemUserCannotLogout() throws Exception {
+ setSystemUserHeadless(true);
+ mockCurrentUser(UserHandle.USER_SYSTEM);
+ assertThat(mUms.getUserLogoutability(UserHandle.USER_SYSTEM))
+ .isEqualTo(UserManager.LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER);
+ }
+
+ @Test
+ @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+ public void testGetUserLogoutability_NonHsum_SystemUserCannotLogout() throws Exception {
+ setSystemUserHeadless(false);
+ mockCurrentUser(UserHandle.USER_SYSTEM);
+ assertThat(
+ mUms.getUserLogoutability(UserHandle.USER_SYSTEM)).isEqualTo(
+ UserManager.LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER);
+ }
+
+ @Test
+ @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+ public void testGetUserLogoutability_CannotSwitch_CannotLogout() throws Exception {
+ setSystemUserHeadless(true);
+ addUser(USER_ID);
+ addUser(OTHER_USER_ID);
+ setLastForegroundTime(OTHER_USER_ID, 1_000_000L);
+ mockCurrentUser(USER_ID);
+ mUms.setUserRestriction(DISALLOW_USER_SWITCH, true, USER_ID);
+ assertThat(mUms.getUserLogoutability(USER_ID))
+ .isEqualTo(UserManager.LOGOUTABILITY_STATUS_CANNOT_SWITCH);
+ }
+
+ @Test
+ @DisableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API)
+ public void testGetUserLogoutability_LogoutDisabled() throws Exception {
+ assertThrows(UnsupportedOperationException.class, () -> mUms.getUserLogoutability(USER_ID));
+ }
+
/**
* Returns true if the user's XML file has Default restrictions
* @param userId Id of the user.
@@ -1021,6 +1093,16 @@ public final class UserManagerServiceTest {
doReturn(service).when(() -> LocalServices.getService(serviceClass));
}
+ private void mockCanSwitchToHeadlessSystemUser(boolean canSwitch) {
+ doReturn(canSwitch)
+ .when(mSpyResources)
+ .getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser);
+ }
+
+ private void mockUserIsInCall(boolean isInCall) {
+ when(mTelecomManager.isInCall()).thenReturn(isInCall);
+ }
+
private void addDefaultProfileAndParent() {
addUser(PARENT_USER_ID);
addProfile(PROFILE_USER_ID, PARENT_USER_ID);
@@ -1063,6 +1145,7 @@ public final class UserManagerServiceTest {
private void addUserData(TestUserData userData) {
Log.d(TAG, "Adding " + userData);
mUsers.put(userData.info.id, userData);
+ mUms.putUserInfo(userData.info);
}
private void setSystemUserHeadless(boolean headless) {
diff --git a/services/tests/powerstatstests/res/raw/history_01 b/services/tests/powerstatstests/res/raw/history_01
new file mode 100644
index 000000000000..f69eb275f2c6
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/history_01
Binary files differ
diff --git a/services/tests/powerstatstests/res/raw/history_02 b/services/tests/powerstatstests/res/raw/history_02
new file mode 100644
index 000000000000..1a536ab920db
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/history_02
Binary files differ
diff --git a/services/tests/powerstatstests/res/raw/history_03 b/services/tests/powerstatstests/res/raw/history_03
new file mode 100644
index 000000000000..76a3c7b69f01
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/history_03
Binary files differ
diff --git a/services/tests/powerstatstests/res/raw/history_04 b/services/tests/powerstatstests/res/raw/history_04
new file mode 100644
index 000000000000..7e43ac6281cc
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/history_04
Binary files differ
diff --git a/services/tests/powerstatstests/res/raw/history_05 b/services/tests/powerstatstests/res/raw/history_05
new file mode 100644
index 000000000000..b587723b7d1b
--- /dev/null
+++ b/services/tests/powerstatstests/res/raw/history_05
Binary files differ
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java
new file mode 100644
index 000000000000..48e0daa9dba0
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.power.stats;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.platform.test.annotations.LargeTest;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import libcore.io.Streams;
+
+import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
+import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
+import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream;
+import org.apache.commons.compress.compressors.deflate.DeflateCompressorOutputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
+import org.apache.commons.compress.compressors.gzip.GzipParameters;
+import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorInputStream;
+import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorOutputStream;
+import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream;
+import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorOutputStream;
+import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
+import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.zip.Deflater;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+@android.platform.test.annotations.DisabledOnRavenwood(reason = "Performance test")
+@Ignore("Performance experiment. Comment out @Ignore to run")
+public class BatteryStatsHistoryCompressionPerfTest {
+
+ @Rule
+ public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Rule
+ public final TestName mTestName = new TestName();
+
+ private final List<byte[]> mHistorySamples = new ArrayList<>();
+
+ @Before
+ public void loadHistorySamples() throws IOException {
+ Context context = InstrumentationRegistry.getContext();
+ Resources resources = context.getResources();
+
+ for (String sampleResource
+ : List.of("history_01", "history_02", "history_03", "history_04", "history_05")) {
+ int resId = resources.getIdentifier(sampleResource, "raw", context.getPackageName());
+ try (InputStream stream = resources.openRawResource(resId)) {
+ byte[] data = Streams.readFully(stream);
+ mHistorySamples.add(data);
+ }
+ }
+ }
+
+ private interface StreamWrapper<T> {
+ T wrap(T stream) throws IOException;
+ }
+
+ private static class CompressorTester implements BatteryHistoryDirectory.Compressor {
+ private final StreamWrapper<OutputStream> mCompressorSupplier;
+ private final StreamWrapper<InputStream> mUncompressorSupplier;
+ private final ByteArrayOutputStream mOutputStream = new ByteArrayOutputStream(200000);
+ private final Random mRandom = new Random();
+
+ private static class Sample {
+ public byte[] uncompressed;
+ public byte[] compressed;
+ }
+
+ private final List<Sample> mSamples;
+
+ CompressorTester(StreamWrapper<OutputStream> compressorSupplier,
+ StreamWrapper<InputStream> uncompressorSupplier,
+ List<byte[]> uncompressedSamples) throws IOException {
+ mCompressorSupplier = compressorSupplier;
+ mUncompressorSupplier = uncompressorSupplier;
+ mSamples = new ArrayList<>();
+ for (byte[] uncompressed : uncompressedSamples) {
+ Sample s = new Sample();
+ s.uncompressed = Arrays.copyOf(uncompressed, uncompressed.length);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ compress(baos, s.uncompressed);
+ s.compressed = baos.toByteArray();
+ mSamples.add(s);
+ }
+ }
+
+ float getCompressionRatio() {
+ long totalUncompressed = 0;
+ long totalCompressed = 0;
+ for (Sample sample : mSamples) {
+ totalUncompressed += sample.uncompressed.length;
+ totalCompressed += sample.compressed.length;
+ }
+ return (float) totalUncompressed / totalCompressed;
+ }
+
+ void compressSample() throws IOException {
+ Sample sample = mSamples.get(mRandom.nextInt(mSamples.size()));
+ mOutputStream.reset();
+ compress(mOutputStream, sample.uncompressed);
+ // Absence of an exception indicates success
+ }
+
+ void uncompressSample() throws IOException {
+ Sample sample = mSamples.get(mRandom.nextInt(mSamples.size()));
+ uncompress(sample.uncompressed, new ByteArrayInputStream(sample.compressed));
+ // Absence of an exception indicates success
+ }
+
+ @Override
+ public void compress(OutputStream stream, byte[] data) throws IOException {
+ OutputStream cos = mCompressorSupplier.wrap(stream);
+ cos.write(data);
+ cos.close();
+ }
+
+ @Override
+ public void uncompress(byte[] data, InputStream stream) throws IOException {
+ InputStream cos = mUncompressorSupplier.wrap(stream);
+ readFully(data, cos);
+ }
+ }
+
+ private void benchmarkCompress(StreamWrapper<OutputStream> compressorSupplier)
+ throws IOException {
+ CompressorTester tester = new CompressorTester(compressorSupplier, null, mHistorySamples);
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ tester.compressSample();
+ }
+ Bundle status = new Bundle();
+ status.putFloat(mTestName.getMethodName() + "_compressionRatio",
+ tester.getCompressionRatio());
+ InstrumentationRegistry.getInstrumentation().sendStatus(Activity.RESULT_OK, status);
+ }
+
+ private void benchmarkUncompress(StreamWrapper<OutputStream> compressorSupplier,
+ StreamWrapper<InputStream> uncompressorSupplier) throws IOException {
+ CompressorTester tester = new CompressorTester(compressorSupplier, uncompressorSupplier,
+ mHistorySamples);
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ tester.uncompressSample();
+ }
+ }
+
+ @Test
+ public void block_lz4_compress() throws IOException {
+ benchmarkCompress(BlockLZ4CompressorOutputStream::new);
+ }
+
+ @Test
+ public void block_lz4_uncompress() throws IOException {
+ benchmarkUncompress(BlockLZ4CompressorOutputStream::new,
+ BlockLZ4CompressorInputStream::new);
+ }
+
+ @Test
+ public void framed_lz4_compress() throws IOException {
+ benchmarkCompress(FramedLZ4CompressorOutputStream::new);
+ }
+
+ @Test
+ public void framed_lz4_uncompress() throws IOException {
+ benchmarkUncompress(FramedLZ4CompressorOutputStream::new,
+ FramedLZ4CompressorInputStream::new);
+ }
+
+ @Test
+ public void gzip_compress() throws IOException {
+ benchmarkCompress(GzipCompressorOutputStream::new);
+ }
+
+ @Test
+ public void gzip_uncompress() throws IOException {
+ benchmarkUncompress(GzipCompressorOutputStream::new,
+ GzipCompressorInputStream::new);
+ }
+
+ @Test
+ public void best_speed_gzip_compress() throws IOException {
+ benchmarkCompress(stream -> {
+ GzipParameters parameters = new GzipParameters();
+ parameters.setCompressionLevel(Deflater.BEST_SPEED);
+ return new GzipCompressorOutputStream(stream, parameters);
+ });
+ }
+
+ @Test
+ public void best_speed_gzip_uncompress() throws IOException {
+ benchmarkUncompress(stream -> {
+ GzipParameters parameters = new GzipParameters();
+ parameters.setCompressionLevel(Deflater.BEST_SPEED);
+ return new GzipCompressorOutputStream(stream, parameters);
+ }, GzipCompressorInputStream::new);
+ }
+
+ @Test
+ public void java_util_gzip_compress() throws IOException {
+ benchmarkCompress(GZIPOutputStream::new);
+ }
+
+ @Test
+ public void java_util_gzip_uncompress() throws IOException {
+ benchmarkUncompress(GZIPOutputStream::new,
+ GZIPInputStream::new);
+ }
+
+ @Test
+ public void bzip2_compress() throws IOException {
+ benchmarkCompress(BZip2CompressorOutputStream::new);
+ }
+
+ @Test
+ public void bzip2_uncompress() throws IOException {
+ benchmarkUncompress(BZip2CompressorOutputStream::new,
+ BZip2CompressorInputStream::new);
+ }
+
+ @Test
+ public void xz_compress() throws IOException {
+ benchmarkCompress(XZCompressorOutputStream::new);
+ }
+
+ @Test
+ public void xz_uncompress() throws IOException {
+ benchmarkUncompress(XZCompressorOutputStream::new,
+ XZCompressorInputStream::new);
+ }
+
+ @Test
+ public void deflate_compress() throws IOException {
+ benchmarkCompress(DeflateCompressorOutputStream::new);
+ }
+
+ @Test
+ public void deflate_uncompress() throws IOException {
+ benchmarkUncompress(DeflateCompressorOutputStream::new,
+ DeflateCompressorInputStream::new);
+ }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index 164eec6fbc49..8fad93184732 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -17,6 +17,7 @@
package com.android.server.power.stats;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -30,18 +31,20 @@ import android.os.BatteryConsumer;
import android.os.BatteryManager;
import android.os.BatteryStats;
import android.os.BatteryStats.HistoryItem;
+import android.os.ConditionVariable;
import android.os.Parcel;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.UserHandle;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.telephony.NetworkRegistrationInfo;
-import android.util.AtomicFile;
import android.util.Log;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.os.BackgroundThread;
import com.android.internal.os.BatteryStatsHistory;
import com.android.internal.os.BatteryStatsHistoryIterator;
import com.android.internal.os.MonotonicClock;
@@ -58,6 +61,8 @@ import org.mockito.MockitoAnnotations;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
@@ -85,6 +90,7 @@ public class BatteryStatsHistoryTest {
private File mHistoryDir;
private final MockClock mClock = new MockClock();
private final MonotonicClock mMonotonicClock = new MonotonicClock(0, mClock);
+ private BatteryHistoryDirectory mDirectory;
private BatteryStatsHistory mHistory;
private BatteryStats.HistoryPrinter mHistoryPrinter;
@Mock
@@ -108,11 +114,30 @@ public class BatteryStatsHistoryTest {
}
mHistoryDir.delete();
+
+ BatteryHistoryDirectory.Compressor compressor;
+ if (RavenwoodRule.isOnRavenwood()) {
+ compressor = new BatteryHistoryDirectory.Compressor() {
+ @Override
+ public void compress(OutputStream stream, byte[] data) throws IOException {
+ stream.write(data);
+ }
+
+ @Override
+ public void uncompress(byte[] data, InputStream stream) throws IOException {
+ readFully(data, stream);
+ }
+ };
+ } else {
+ compressor = BatteryHistoryDirectory.DEFAULT_COMPRESSOR;
+ }
+ mDirectory = new BatteryHistoryDirectory(mHistoryDir, 32768, compressor);
+
mClock.realtime = 123;
mClock.currentTime = 1743645660000L; // 2025-04-03, 2:01:00 AM
- mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32768,
- MAX_HISTORY_BUFFER_SIZE, mStepDetailsCalculator, mClock, mMonotonicClock, mTracer,
+ mHistory = new BatteryStatsHistory(mHistoryBuffer, MAX_HISTORY_BUFFER_SIZE, mDirectory,
+ mStepDetailsCalculator, mClock, mMonotonicClock, mTracer,
mEventLogger);
mHistory.forceRecordAllHistory();
mHistory.startRecordingHistory(mClock.realtime, mClock.uptime, false);
@@ -210,8 +235,9 @@ public class BatteryStatsHistoryTest {
}
@Test
- public void testStartNextFile() throws Exception {
+ public void testStartNextFile() {
mHistory.forceRecordAllHistory();
+ mDirectory.setFileCompressionEnabled(false);
mClock.realtime = 123;
@@ -225,7 +251,7 @@ public class BatteryStatsHistoryTest {
mClock.realtime = 1000 * i;
fileList.add(mClock.realtime + ".bh");
- mHistory.startNextFile(mClock.realtime);
+ mHistory.startNextFragment(mClock.realtime);
createActiveFile(mHistory);
fillActiveFile(mHistory);
@@ -235,8 +261,9 @@ public class BatteryStatsHistoryTest {
// create file 32
mClock.realtime = 1000 * 32;
- mHistory.startNextFile(mClock.realtime);
+ mHistory.startNextFragment(mClock.realtime);
createActiveFile(mHistory);
+ fillActiveFile(mHistory);
fileList.add("32000.bh");
fileList.remove(0);
// verify file 0 is deleted.
@@ -244,21 +271,22 @@ public class BatteryStatsHistoryTest {
verifyFileNames(mHistory, fileList);
verifyActiveFile(mHistory, "32000.bh");
- fillActiveFile(mHistory);
-
// create file 33
mClock.realtime = 1000 * 33;
- mHistory.startNextFile(mClock.realtime);
+ mHistory.startNextFragment(mClock.realtime);
createActiveFile(mHistory);
- // verify file 1 is deleted
+ fillActiveFile(mHistory);
fileList.add("33000.bh");
fileList.remove(0);
+ mHistory.writeHistory();
+
+ // verify file 1 is deleted
verifyFileDeleted("1000.bh");
verifyFileNames(mHistory, fileList);
verifyActiveFile(mHistory, "33000.bh");
// create a new BatteryStatsHistory object, it will pick up existing history files.
- BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
+ BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, 1024, mDirectory,
null, mClock, mMonotonicClock, mTracer, mEventLogger);
// verify constructor can pick up all files from file system.
verifyFileNames(history2, fileList);
@@ -281,7 +309,7 @@ public class BatteryStatsHistoryTest {
// create file 1.
mClock.realtime = 2345678;
- history2.startNextFile(mClock.realtime);
+ history2.startNextFragment(mClock.realtime);
createActiveFile(history2);
verifyFileNames(history2, Arrays.asList("1234567.bh", "2345678.bh"));
verifyActiveFile(history2, "2345678.bh");
@@ -297,10 +325,10 @@ public class BatteryStatsHistoryTest {
mHistory = spy(mHistory.copy());
doAnswer(invocation -> {
- AtomicFile file = invocation.getArgument(1);
- mReadFiles.add(file.getBaseFile().getName());
+ BatteryHistoryDirectory.BatteryHistoryFile file = invocation.getArgument(1);
+ mReadFiles.add(file.atomicFile.getBaseFile().getName());
return invocation.callRealMethod();
- }).when(mHistory).readFileToParcel(any(), any());
+ }).when(mHistory).readFragmentToParcel(any(), any());
// Prepare history for iteration
mHistory.iterate(0, MonotonicClock.UNDEFINED);
@@ -339,10 +367,10 @@ public class BatteryStatsHistoryTest {
mHistory = spy(mHistory.copy());
doAnswer(invocation -> {
- AtomicFile file = invocation.getArgument(1);
- mReadFiles.add(file.getBaseFile().getName());
+ BatteryHistoryDirectory.BatteryHistoryFile file = invocation.getArgument(1);
+ mReadFiles.add(file.atomicFile.getBaseFile().getName());
return invocation.callRealMethod();
- }).when(mHistory).readFileToParcel(any(), any());
+ }).when(mHistory).readFragmentToParcel(any(), any());
// Prepare history for iteration
mHistory.iterate(1000, 3000);
@@ -371,14 +399,14 @@ public class BatteryStatsHistoryTest {
mHistory.recordEvent(mClock.realtime, mClock.uptime,
BatteryStats.HistoryItem.EVENT_JOB_START, "job", 42);
- mHistory.startNextFile(mClock.realtime); // 1000.bh
+ mHistory.startNextFragment(mClock.realtime); // 1000.bh
mClock.realtime = 2000;
mClock.uptime = 2000;
mHistory.recordEvent(mClock.realtime, mClock.uptime,
BatteryStats.HistoryItem.EVENT_JOB_FINISH, "job", 42);
- mHistory.startNextFile(mClock.realtime); // 2000.bh
+ mHistory.startNextFragment(mClock.realtime); // 2000.bh
mClock.realtime = 3000;
mClock.uptime = 3000;
@@ -386,30 +414,37 @@ public class BatteryStatsHistoryTest {
HistoryItem.EVENT_ALARM, "alarm", 42);
// Flush accumulated history to disk
- mHistory.startNextFile(mClock.realtime);
+ mHistory.startNextFragment(mClock.realtime);
}
private void verifyActiveFile(BatteryStatsHistory history, String file) {
final File expectedFile = new File(mHistoryDir, file);
- assertEquals(expectedFile.getPath(), history.getActiveFile().getBaseFile().getPath());
+ assertEquals(expectedFile.getPath(),
+ ((BatteryHistoryDirectory.BatteryHistoryFile) history.getActiveFragment())
+ .atomicFile.getBaseFile().getPath());
assertTrue(expectedFile.exists());
}
private void verifyFileNames(BatteryStatsHistory history, List<String> fileList) {
- assertEquals(fileList.size(), history.getFilesNames().size());
+ awaitCompletion();
+ List<String> fileNames =
+ ((BatteryHistoryDirectory) history.getBatteryHistoryStore()).getFileNames();
+ assertThat(fileNames).isEqualTo(fileList);
for (int i = 0; i < fileList.size(); i++) {
- assertEquals(fileList.get(i), history.getFilesNames().get(i));
final File expectedFile = new File(mHistoryDir, fileList.get(i));
- assertTrue(expectedFile.exists());
+ assertWithMessage("File does not exist " + expectedFile)
+ .that(expectedFile.exists()).isTrue();
}
}
private void verifyFileDeleted(String file) {
+ awaitCompletion();
assertFalse(new File(mHistoryDir, file).exists());
}
private void createActiveFile(BatteryStatsHistory history) {
- final File file = history.getActiveFile().getBaseFile();
+ File file = ((BatteryHistoryDirectory.BatteryHistoryFile) history.getActiveFragment())
+ .atomicFile.getBaseFile();
if (file.exists()) {
return;
}
@@ -561,7 +596,7 @@ public class BatteryStatsHistoryTest {
public void largeTagPool() {
// Keep the preserved part of history short - we only need to capture the very tail of
// history.
- mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 1, 6000,
+ mHistory = new BatteryStatsHistory(mHistoryBuffer, 6000, mDirectory,
mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger);
mHistory.forceRecordAllHistory();
@@ -699,7 +734,7 @@ public class BatteryStatsHistoryTest {
assertThat(size).isGreaterThan(lastHistorySize);
lastHistorySize = size;
- mHistory.startNextFile(mClock.realtime);
+ mHistory.startNextFragment(mClock.realtime);
size = mHistory.getMonotonicHistorySize();
assertThat(size).isEqualTo(lastHistorySize);
@@ -713,7 +748,7 @@ public class BatteryStatsHistoryTest {
assertThat(size).isGreaterThan(lastHistorySize);
lastHistorySize = size;
- mHistory.startNextFile(mClock.realtime);
+ mHistory.startNextFragment(mClock.realtime);
mClock.realtime = 3000;
mClock.uptime = 3000;
@@ -788,4 +823,58 @@ public class BatteryStatsHistoryTest {
parcel.recycle();
}
+
+ @Test
+ public void compressHistoryFiles() {
+ // The first history file will be uncompressed
+ mDirectory.setFileCompressionEnabled(false);
+
+ mClock.realtime = 1000;
+ mClock.uptime = 1000;
+ mHistory.recordEvent(mClock.realtime, mClock.uptime,
+ BatteryStats.HistoryItem.EVENT_JOB_START, "job", 42);
+
+ mHistory.startNextFragment(mClock.realtime);
+
+ // The second file will be compressed
+ mDirectory.setFileCompressionEnabled(true);
+
+ mClock.realtime = 2000;
+ mClock.uptime = 2000;
+ mHistory.recordEvent(mClock.realtime, mClock.uptime,
+ BatteryStats.HistoryItem.EVENT_JOB_FINISH, "job", 42);
+
+ mHistory.startNextFragment(mClock.realtime);
+
+ awaitCompletion();
+
+ assertThat(historySummary(mHistory)).isEqualTo(List.of("+42:job", "-42:job"));
+
+ Parcel parcel = Parcel.obtain();
+ mHistory.writeToBatteryUsageStatsParcel(parcel, Long.MAX_VALUE);
+ parcel.setDataPosition(0);
+
+ BatteryStatsHistory actual = BatteryStatsHistory.createFromBatteryUsageStatsParcel(parcel);
+ assertThat(historySummary(actual)).isEqualTo(List.of("+42:job", "-42:job"));
+ }
+
+ private List<String> historySummary(BatteryStatsHistory history) {
+ List<String> events = new ArrayList<>();
+ try (BatteryStatsHistoryIterator it = history.iterate(0, Long.MAX_VALUE)) {
+ HistoryItem item;
+ while ((item = it.next()) != null) {
+ if ((item.eventCode & HistoryItem.EVENT_TYPE_MASK) == HistoryItem.EVENT_JOB) {
+ events.add(((item.eventCode & HistoryItem.EVENT_FLAG_START) != 0 ? "+" : "-")
+ + item.eventTag.uid + ":" + item.eventTag.string);
+ }
+ }
+ }
+ return events;
+ }
+
+ private static void awaitCompletion() {
+ ConditionVariable done = new ConditionVariable();
+ BackgroundThread.getHandler().post(done::open);
+ done.block();
+ }
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index e94ef5bb4871..31ff50f8ca58 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -877,9 +877,19 @@ public class BatteryUsageStatsProviderTest {
}
@Test
- @EnableFlags(Flags.FLAG_EXTENDED_BATTERY_HISTORY_CONTINUOUS_COLLECTION_ENABLED)
+ @EnableFlags({
+ Flags.FLAG_EXTENDED_BATTERY_HISTORY_CONTINUOUS_COLLECTION_ENABLED,
+ Flags.FLAG_EXTENDED_BATTERY_HISTORY_COMPRESSION_ENABLED
+ })
public void testIncludeSubsetOfHistory() throws IOException {
MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+ BatteryHistoryDirectory store =
+ (BatteryHistoryDirectory) batteryStats.getHistory().getBatteryHistoryStore();
+ store.setFileCompressionEnabled(true);
+ // Make history fragment size predictable. Without this protection, holding the history
+ // directory lock in the background would prevent new fragments from being created.
+ store.makeDirectoryLockUnconditional();
+
batteryStats.getHistory().setMaxHistoryBufferSize(100);
synchronized (batteryStats) {
batteryStats.setRecordAllHistoryLocked(true);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index a69e2fdb0b03..c7a19ce7b233 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -41,6 +41,9 @@ import com.android.internal.os.PowerProfile;
import com.android.internal.power.EnergyConsumerStats;
import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Queue;
@@ -49,6 +52,18 @@ import java.util.Queue;
* Mocks a BatteryStatsImpl object.
*/
public class MockBatteryStatsImpl extends BatteryStatsImpl {
+ public static final BatteryHistoryDirectory.Compressor PASS_THROUGH_COMPRESSOR =
+ new BatteryHistoryDirectory.Compressor() {
+ @Override
+ public void compress(OutputStream stream, byte[] data) throws IOException {
+ stream.write(data);
+ }
+
+ @Override
+ public void uncompress(byte[] data, InputStream stream) throws IOException {
+ readFully(data, stream);
+ }
+ };
public boolean mForceOnBattery;
// The mNetworkStats will be used for both wifi and mobile categories
private NetworkStats mNetworkStats;
@@ -83,7 +98,11 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl {
MockBatteryStatsImpl(BatteryStatsConfig config, Clock clock, MonotonicClock monotonicClock,
File historyDirectory, Handler handler, PowerProfile powerProfile,
PowerStatsUidResolver powerStatsUidResolver) {
- super(config, clock, monotonicClock, historyDirectory, handler,
+ super(config, clock, monotonicClock, historyDirectory,
+ historyDirectory != null ? new BatteryHistoryDirectory(
+ new File(historyDirectory, "battery-history"),
+ config.getMaxHistorySizeBytes(), PASS_THROUGH_COMPRESSOR) : null,
+ handler,
mock(PlatformIdleStateCallback.class), mock(EnergyStatsRetriever.class),
mock(UserInfoProvider.class), powerProfile,
new CpuScalingPolicies(new SparseArray<>(), new SparseArray<>()),
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java
index 3bdbcb50e601..73d491c93bb5 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java
@@ -59,7 +59,7 @@ public class PowerStatsAggregatorTest {
@Before
public void setup() throws ParseException {
- mHistory = new BatteryStatsHistory(null, null, 0, 1024,
+ mHistory = new BatteryStatsHistory(null, 1024, null,
mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock,
mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class), null);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java
index d243f92a139f..9ef58cc28a69 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java
@@ -42,6 +42,7 @@ import com.android.internal.os.CpuScalingPolicies;
import com.android.internal.os.MonotonicClock;
import com.android.internal.os.PowerProfile;
import com.android.internal.os.PowerStats;
+import com.android.server.power.stats.BatteryHistoryDirectory;
import com.android.server.power.stats.BatteryUsageStatsRule;
import com.android.server.power.stats.MockClock;
import com.android.server.power.stats.PowerStatsStore;
@@ -84,6 +85,7 @@ public class PowerStatsExporterTest {
private PowerStatsStore mPowerStatsStore;
private PowerStatsAggregator mPowerStatsAggregator;
private MultiStatePowerAttributor mPowerAttributor;
+ private BatteryHistoryDirectory mDirectory;
private BatteryStatsHistory mHistory;
private CpuPowerStatsLayout mCpuStatsArrayLayout;
private PowerStats.Descriptor mPowerStatsDescriptor;
@@ -117,7 +119,8 @@ public class PowerStatsExporterTest {
AggregatedPowerStatsConfig.STATE_PROCESS_STATE);
mPowerStatsStore = new PowerStatsStore(storeDirectory, new TestHandler());
- mHistory = new BatteryStatsHistory(Parcel.obtain(), storeDirectory, 0, 10000,
+ mDirectory = new BatteryHistoryDirectory(storeDirectory, 0);
+ mHistory = new BatteryStatsHistory(Parcel.obtain(), 10000, mDirectory,
mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock,
mMonotonicClock, null, null);
mPowerStatsAggregator = new PowerStatsAggregator(config);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java
index ed3cda0f76ef..8257d714a5d5 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java
@@ -87,7 +87,7 @@ public class WakelockPowerStatsProcessorTest {
PowerStats ps = new PowerStats(descriptor);
long[] uidStats = new long[descriptor.uidStatsArrayLength];
- BatteryStatsHistory history = new BatteryStatsHistory(null, null, 0, 10000,
+ BatteryStatsHistory history = new BatteryStatsHistory(null, 10000, null,
mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class),
mStatsRule.getMockClock(),
new MonotonicClock(START_TIME, mStatsRule.getMockClock()), null, null);
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
new file mode 100644
index 000000000000..2e489a81c43a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.contexthub;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.hardware.contexthub.HubEndpointInfo;
+import android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier;
+import android.hardware.contexthub.IContextHubEndpoint;
+import android.hardware.contexthub.IContextHubEndpointCallback;
+import android.hardware.contexthub.IEndpointCommunication;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.platform.test.annotations.Postsubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Collections;
+
+@RunWith(AndroidJUnit4.class)
+@Postsubmit
+// TODO(b/378944402): Enable test in presubmit
+public class ContextHubEndpointTest {
+ private static final int SESSION_ID_RANGE = ContextHubEndpointManager.SERVICE_SESSION_RANGE;
+ private static final int MIN_SESSION_ID = 0;
+ private static final int MAX_SESSION_ID = MIN_SESSION_ID + SESSION_ID_RANGE - 1;
+
+ private static final String ENDPOINT_NAME = "Example test endpoint";
+ private static final int ENDPOINT_ID = 1;
+ private static final String ENDPOINT_PACKAGE_NAME = "com.android.server.location.contexthub";
+
+ private ContextHubClientManager mClientManager;
+ private ContextHubEndpointManager mEndpointManager;
+ private HubInfoRegistry mHubInfoRegistry;
+ private ContextHubTransactionManager mTransactionManager;
+ private Context mContext;
+ @Mock private IEndpointCommunication mMockEndpointCommunications;
+ @Mock private IContextHubWrapper mMockContextHubWrapper;
+ @Mock private IContextHubEndpointCallback mMockCallback;
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Before
+ public void setUp() throws RemoteException, InstantiationException {
+ when(mMockContextHubWrapper.getHubs()).thenReturn(Collections.emptyList());
+ when(mMockContextHubWrapper.getEndpoints()).thenReturn(Collections.emptyList());
+ when(mMockContextHubWrapper.registerEndpointHub(any(), any()))
+ .thenReturn(mMockEndpointCommunications);
+ when(mMockEndpointCommunications.requestSessionIdRange(SESSION_ID_RANGE))
+ .thenReturn(new int[] {MIN_SESSION_ID, MAX_SESSION_ID});
+ when(mMockCallback.asBinder()).thenReturn(new Binder());
+
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ mHubInfoRegistry = new HubInfoRegistry(mContext, mMockContextHubWrapper);
+ mClientManager = new ContextHubClientManager(mContext, mMockContextHubWrapper);
+ mTransactionManager =
+ new ContextHubTransactionManager(
+ mMockContextHubWrapper, mClientManager, new NanoAppStateManager());
+ mEndpointManager =
+ new ContextHubEndpointManager(
+ mContext, mMockContextHubWrapper, mHubInfoRegistry, mTransactionManager);
+ mEndpointManager.init();
+ }
+
+ @Test
+ public void testRegisterEndpoint() throws RemoteException {
+ // Register an endpoint and confirm we can get a valid IContextHubEndoint reference
+ HubEndpointInfo info =
+ new HubEndpointInfo(
+ ENDPOINT_NAME, ENDPOINT_ID, ENDPOINT_PACKAGE_NAME, Collections.emptyList());
+ IContextHubEndpoint endpoint =
+ mEndpointManager.registerEndpoint(
+ info, mMockCallback, ENDPOINT_PACKAGE_NAME, /* attributionTag= */ null);
+ assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(1);
+ assertThat(endpoint).isNotNull();
+ HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo();
+ assertThat(assignedInfo).isNotNull();
+ HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier();
+ assertThat(assignedIdentifier).isNotNull();
+
+ // Unregister the endpoint and confirm proper clean-up
+ mEndpointManager.unregisterEndpoint(assignedIdentifier.getEndpoint());
+ assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(0);
+ }
+
+ @Test
+ public void testReserveSessionId() {
+ assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+
+ int sessionId = mEndpointManager.reserveSessionId();
+ assertThat(sessionId).isAtLeast(MIN_SESSION_ID);
+ assertThat(sessionId).isAtMost(MAX_SESSION_ID);
+ assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE - 1);
+
+ mEndpointManager.returnSessionId(sessionId);
+ assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 01d34b697def..3aa95449cc98 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -606,8 +606,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
- return FlagsParameterization.allCombinationsOf(
- FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NM_BINDER_PERF_CACHE_CHANNELS);
+ return FlagsParameterization.allCombinationsOf();
}
public NotificationManagerServiceTest(FlagsParameterization flags) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java
index 5265b442c968..67a95de8a5c1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java
@@ -39,6 +39,7 @@ import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import android.annotation.NonNull;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.pm.ActivityInfo.WindowLayout;
@@ -293,7 +294,7 @@ public class LaunchParamsControllerTests extends WindowTestsBase {
final int beforeWindowMode = task.getWindowingMode();
assertNotEquals(windowingMode, beforeWindowMode);
- mController.layoutTask(task, null /* windowLayout */);
+ layoutTask(task);
final int afterWindowMode = task.getWindowingMode();
assertEquals(afterWindowMode, beforeWindowMode);
@@ -317,7 +318,7 @@ public class LaunchParamsControllerTests extends WindowTestsBase {
assertNotEquals(expected, task.getBounds());
- mController.layoutTask(task, null /* windowLayout */);
+ layoutTask(task);
// Task will make adjustments to requested bounds. We only need to guarantee that the
// reuqested bounds are expected.
@@ -342,7 +343,7 @@ public class LaunchParamsControllerTests extends WindowTestsBase {
assertNotEquals(expected, task.getBounds());
- mController.layoutTask(task, null /* windowLayout */);
+ layoutTask(task);
assertEquals(expected, task.getRequestedOverrideBounds());
}
@@ -365,7 +366,7 @@ public class LaunchParamsControllerTests extends WindowTestsBase {
assertNotEquals(expected, task.getBounds());
- mController.layoutTask(task, null /* windowLayout */);
+ layoutTask(task);
assertNotEquals(expected, task.getBounds());
assertEquals(expected, task.mLastNonFullscreenBounds);
@@ -467,4 +468,9 @@ public class LaunchParamsControllerTests extends WindowTestsBase {
private TestDisplayContent createNewDisplayContent() {
return addNewDisplayContentAt(DisplayContent.POSITION_TOP);
}
+
+ private void layoutTask(@NonNull Task task) {
+ mController.layoutTask(task, null /* layout */, null /* activity */, null /* source */,
+ null /* options */);
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java
index 369600c3f8d7..dcb68620e361 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java
@@ -18,20 +18,30 @@ package com.android.server.wm;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY;
+import static android.window.WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID;
+import static android.window.WindowContainerTransaction.HierarchyOp.REACHABILITY_EVENT_X;
+import static android.window.WindowContainerTransaction.HierarchyOp.REACHABILITY_EVENT_Y;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.times;
import android.content.Intent;
+import android.os.Binder;
+import android.os.Bundle;
import android.platform.test.annotations.Presubmit;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransaction.HierarchyOp;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
@@ -223,6 +233,31 @@ public class WindowContainerTransactionTests extends WindowTestsBase {
< tda.mChildren.indexOf(desktopOrganizer.mTasks.get(2).getRootTask()));
}
+ @Test
+ public void testAppCompat_setReachabilityOffsets() {
+ final Task task = createTask(/* taskId */ 37);
+ final WindowContainerToken containerToken = task.getTaskInfo().token;
+ spyOn(containerToken);
+ final Binder asBinder = new Binder();
+ doReturn(asBinder).when(containerToken).asBinder();
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setReachabilityOffset(containerToken, /* taskId */ task.mTaskId, 10, 20);
+
+ final List<HierarchyOp> hierarchyOps = wct.getHierarchyOps().stream()
+ .filter(op -> op.getType() == HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY)
+ .toList();
+
+ assertEquals(1, hierarchyOps.size());
+ final HierarchyOp appCompatOp = hierarchyOps.getFirst();
+ assertNotNull(appCompatOp);
+ final Bundle appCompatOptions = appCompatOp.getAppCompatOptions();
+
+ assertEquals(task.mTaskId, appCompatOptions.getInt(LAUNCH_KEY_TASK_ID));
+ assertEquals(10, appCompatOptions.getInt(REACHABILITY_EVENT_X));
+ assertEquals(20, appCompatOptions.getInt(REACHABILITY_EVENT_Y));
+ assertSame(asBinder, appCompatOp.getContainer());
+ }
+
private Task createTask(int taskId) {
return new Task.Builder(mAtm)
.setTaskId(taskId)
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index d164c8851f5b..4b175c134d84 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -618,9 +618,9 @@ public class SubscriptionInfo implements Parcelable {
@Deprecated
public int getMcc() {
try {
- return mMcc == null ? 0 : Integer.parseInt(mMcc);
+ return TextUtils.isEmpty(mMcc) ? 0 : Integer.parseInt(mMcc);
} catch (NumberFormatException e) {
- Log.w(SubscriptionInfo.class.getSimpleName(), "MCC string is not a number");
+ Log.w(SubscriptionInfo.class.getSimpleName(), "MCC string is not a number: " + mMcc);
return 0;
}
}
@@ -633,9 +633,9 @@ public class SubscriptionInfo implements Parcelable {
@Deprecated
public int getMnc() {
try {
- return mMnc == null ? 0 : Integer.parseInt(mMnc);
+ return TextUtils.isEmpty(mMnc) ? 0 : Integer.parseInt(mMnc);
} catch (NumberFormatException e) {
- Log.w(SubscriptionInfo.class.getSimpleName(), "MNC string is not a number");
+ Log.w(SubscriptionInfo.class.getSimpleName(), "MNC string is not a number: " + mMnc);
return 0;
}
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 73ea68bc3547..504605d0a1a2 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -19432,7 +19432,6 @@ public class TelephonyManager {
* and integrity algorithms in use
* @hide
*/
- @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY)
@RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
@SystemApi
public void setNullCipherNotificationsEnabled(boolean enable) {
@@ -19459,7 +19458,6 @@ public class TelephonyManager {
* and integrity algorithms in use
* @hide
*/
- @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY)
@RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
@SystemApi
public boolean isNullCipherNotificationsEnabled() {