summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp1
-rw-r--r--apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java278
-rw-r--r--api/OWNERS2
-rw-r--r--cmds/bootanimation/BootAnimation.cpp2
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig8
-rw-r--r--core/java/android/hardware/input/input_framework.aconfig9
-rw-r--r--core/java/android/view/RoundScrollbarRenderer.java43
-rw-r--r--core/java/android/view/ViewConfiguration.java231
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig7
-rw-r--r--core/java/com/android/internal/graphics/palette/OWNERS5
-rw-r--r--core/java/com/android/internal/policy/DecorView.java85
-rw-r--r--core/jni/android_view_DisplayEventReceiver.cpp4
-rw-r--r--core/res/res/values-w225dp/dimens.xml20
-rw-r--r--core/res/res/values/config.xml37
-rw-r--r--core/res/res/values/dimens.xml3
-rw-r--r--core/res/res/values/symbols.xml12
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt10
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt78
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java14
-rw-r--r--libs/WindowManager/Shell/tests/OWNERS1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt179
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt191
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java6
-rw-r--r--libs/input/PointerControllerContext.cpp3
-rw-r--r--packages/EasterEgg/AndroidManifest.xml2
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_large.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_medium.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_small.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_tiny.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_spacecraft.xml44
-rw-r--r--packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml45
-rw-r--r--packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml21
-rw-r--r--packages/EasterEgg/res/values/themes.xml23
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt20
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt46
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt10
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt208
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/Namer.kt26
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt187
-rw-r--r--packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java1
-rw-r--r--packages/SystemUI/Android.bp9
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig14
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt43
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt18
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt121
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt44
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt196
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt16
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt274
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt223
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt50
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt33
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt3
-rw-r--r--packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml4
-rw-r--r--packages/SystemUI/res/values/strings.xml4
-rw-r--r--packages/SystemUI/res/values/styles.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/ailabs/OWNERS1
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt89
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt55
-rw-r--r--packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt122
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt116
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt175
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/stylus/OWNERS3
-rw-r--r--packages/SystemUI/tests/res/layout/custom_view_flipper.xml14
-rw-r--r--packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java57
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java285
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt94
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt54
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt57
-rw-r--r--services/accessibility/accessibility.aconfig7
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java5
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java174
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java13
-rw-r--r--services/core/Android.bp11
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java17
-rw-r--r--services/core/java/com/android/server/backup/InputBackupHelper.java82
-rw-r--r--services/core/java/com/android/server/backup/SystemBackupAgent.java6
-rw-r--r--services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java13
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java12
-rw-r--r--services/core/java/com/android/server/input/InputDataStore.java59
-rw-r--r--services/core/java/com/android/server/input/InputManagerInternal.java32
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java23
-rw-r--r--services/core/java/com/android/server/input/KeyGestureController.java28
-rw-r--r--services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java36
-rw-r--r--services/core/java/com/android/server/wm/AppWarnings.java4
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java14
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java90
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java33
-rw-r--r--tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt99
128 files changed, 3317 insertions, 2008 deletions
diff --git a/Android.bp b/Android.bp
index 9d3b64d7335b..303fa2cd18da 100644
--- a/Android.bp
+++ b/Android.bp
@@ -583,6 +583,7 @@ java_library {
"documents-ui-compat-config",
"calendar-provider-compat-config",
"contacts-provider-platform-compat-config",
+ "SystemUI-core-compat-config",
] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), {
"true": [],
default: [
diff --git a/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java b/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java
index 8e3ed6d9931c..7a7250b9e910 100644
--- a/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java
+++ b/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java
@@ -19,27 +19,24 @@ package android.view;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import android.content.Context;
-import android.perftests.utils.BenchmarkState;
-import android.perftests.utils.PerfStatusReporter;
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.benchmark.BenchmarkState;
+import androidx.benchmark.junit4.BenchmarkRule;
+import androidx.test.filters.SmallTest;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
-@LargeTest
-@RunWith(AndroidJUnit4.class)
+@SmallTest
public class ViewConfigurationPerfTest {
@Rule
- public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+ public final BenchmarkRule mBenchmarkRule = new BenchmarkRule();
private final Context mContext = getInstrumentation().getTargetContext();
@Test
public void testGet_newViewConfiguration() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final BenchmarkState state = mBenchmarkRule.getState();
while (state.keepRunning()) {
state.pauseTiming();
@@ -53,7 +50,7 @@ public class ViewConfigurationPerfTest {
@Test
public void testGet_cachedViewConfiguration() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final BenchmarkState state = mBenchmarkRule.getState();
// Do `get` once to make sure there's something cached.
ViewConfiguration.get(mContext);
@@ -61,265 +58,4 @@ public class ViewConfigurationPerfTest {
ViewConfiguration.get(mContext);
}
}
-
- @Test
- public void testGetPressedStateDuration_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getPressedStateDuration();
- }
- }
-
- @Test
- public void testGetPressedStateDuration_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getPressedStateDuration();
-
- while (state.keepRunning()) {
- ViewConfiguration.getPressedStateDuration();
- }
- }
-
- @Test
- public void testGetTapTimeout_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getTapTimeout();
- }
- }
-
- @Test
- public void testGetTapTimeout_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getTapTimeout();
-
- while (state.keepRunning()) {
- ViewConfiguration.getTapTimeout();
- }
- }
-
- @Test
- public void testGetJumpTapTimeout_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getJumpTapTimeout();
- }
- }
-
- @Test
- public void testGetJumpTapTimeout_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getJumpTapTimeout();
-
- while (state.keepRunning()) {
- ViewConfiguration.getJumpTapTimeout();
- }
- }
-
- @Test
- public void testGetDoubleTapTimeout_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getDoubleTapTimeout();
- }
- }
-
- @Test
- public void testGetDoubleTapTimeout_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getDoubleTapTimeout();
-
- while (state.keepRunning()) {
- ViewConfiguration.getDoubleTapTimeout();
- }
- }
-
- @Test
- public void testGetDoubleTapMinTime_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getDoubleTapMinTime();
- }
- }
-
- @Test
- public void testGetDoubleTapMinTime_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getDoubleTapMinTime();
-
- while (state.keepRunning()) {
- ViewConfiguration.getDoubleTapMinTime();
- }
- }
-
- @Test
- public void testGetZoomControlsTimeout_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getZoomControlsTimeout();
- }
- }
-
- @Test
- public void testGetZoomControlsTimeout_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getZoomControlsTimeout();
-
- while (state.keepRunning()) {
- ViewConfiguration.getZoomControlsTimeout();
- }
- }
-
- @Test
- public void testGetLongPressTimeout() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- ViewConfiguration.getLongPressTimeout();
- }
- }
-
- @Test
- public void testGetMultiPressTimeout() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- ViewConfiguration.getMultiPressTimeout();
- }
- }
-
- @Test
- public void testGetKeyRepeatTimeout() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- ViewConfiguration.getKeyRepeatTimeout();
- }
- }
-
- @Test
- public void testGetKeyRepeatDelay() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- ViewConfiguration.getKeyRepeatDelay();
- }
- }
-
- @Test
- public void testGetHoverTapSlop_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getHoverTapSlop();
- }
- }
-
- @Test
- public void testGetHoverTapSlop_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getHoverTapSlop();
-
- while (state.keepRunning()) {
- ViewConfiguration.getHoverTapSlop();
- }
- }
-
- @Test
- public void testGetScrollFriction_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getScrollFriction();
- }
- }
-
- @Test
- public void testGetScrollFriction_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getScrollFriction();
-
- while (state.keepRunning()) {
- ViewConfiguration.getScrollFriction();
- }
- }
-
- @Test
- public void testGetDefaultActionModeHideDuration_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getDefaultActionModeHideDuration();
- }
- }
-
- @Test
- public void testGetDefaultActionModeHideDuration_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getDefaultActionModeHideDuration();
-
- while (state.keepRunning()) {
- ViewConfiguration.getDefaultActionModeHideDuration();
- }
- }
}
diff --git a/api/OWNERS b/api/OWNERS
index 965093c9ab38..f2bcf13d2d2e 100644
--- a/api/OWNERS
+++ b/api/OWNERS
@@ -9,4 +9,4 @@ per-file *.go,go.mod,go.work,go.work.sum = file:platform/build/soong:/OWNERS
per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
# For metalava team to disable lint checks in platform
-per-file Android.bp = aurimas@google.com,emberrose@google.com
+per-file Android.bp = aurimas@google.com
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index b43905b19239..844e52c3ecf2 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -441,7 +441,7 @@ public:
numEvents = mBootAnimation->mDisplayEventReceiver->getEvents(buffer, kBufferSize);
for (size_t i = 0; i < static_cast<size_t>(numEvents); i++) {
const auto& event = buffer[i];
- if (event.header.type == DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG) {
+ if (event.header.type == DisplayEventType::DISPLAY_EVENT_HOTPLUG) {
SLOGV("Hotplug received");
if (!event.hotplug.connected) {
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index c3dc257e6535..fcdb02ab5da2 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -125,11 +125,3 @@ flag {
description: "Show virtual devices in Settings"
bug: "338974320"
}
-
-flag {
- name: "migrate_viewconfiguration_constants_to_resources"
- namespace: "virtual_devices"
- description: "Use resources instead of constants in ViewConfiguration"
- is_fixed_read_only: true
- bug: "370928384"
-}
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 23722ed5bb0d..8d58296e5581 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -233,3 +233,12 @@ flag {
description: "Key Event Activity Detection"
bug: "356412905"
}
+
+flag {
+ name: "enable_backup_and_restore_for_input_gestures"
+ namespace: "input"
+ description: "Adds backup and restore support for custom input gestures"
+ bug: "382184249"
+ is_fixed_read_only: true
+}
+
diff --git a/core/java/android/view/RoundScrollbarRenderer.java b/core/java/android/view/RoundScrollbarRenderer.java
index 5e1eadae0953..331e34526ae8 100644
--- a/core/java/android/view/RoundScrollbarRenderer.java
+++ b/core/java/android/view/RoundScrollbarRenderer.java
@@ -20,6 +20,7 @@ import static android.util.MathUtils.acos;
import static java.lang.Math.sin;
+import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
@@ -40,9 +41,9 @@ public class RoundScrollbarRenderer {
// The range of the scrollbar position represented as an angle in degrees.
private static final float SCROLLBAR_ANGLE_RANGE = 28.8f;
- private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 26.3f; // 90%
- private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 3.1f; // 10%
- private static final float THUMB_WIDTH_DP = 4f;
+ private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 0.7f * SCROLLBAR_ANGLE_RANGE;
+ private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 0.3f * SCROLLBAR_ANGLE_RANGE;
+ private static final float GAP_BETWEEN_TRACK_AND_THUMB_DP = 3f;
private static final float OUTER_PADDING_DP = 2f;
private static final int DEFAULT_THUMB_COLOR = 0xFFFFFFFF;
private static final int DEFAULT_TRACK_COLOR = 0x4CFFFFFF;
@@ -57,14 +58,16 @@ public class RoundScrollbarRenderer {
private final RectF mRect = new RectF();
private final View mParent;
private final float mInset;
+ private final float mGapBetweenThumbAndTrackPx;
+ private final boolean mUseRefactoredRoundScrollbar;
private float mPreviousMaxScroll = 0;
private float mMaxScrollDiff = 0;
private float mPreviousCurrentScroll = 0;
private float mCurrentScrollDiff = 0;
private float mThumbStrokeWidthAsDegrees = 0;
+ private float mGapBetweenTrackAndThumbAsDegrees = 0;
private boolean mDrawToLeft;
- private boolean mUseRefactoredRoundScrollbar;
public RoundScrollbarRenderer(View parent) {
// Paints for the round scrollbar.
@@ -80,16 +83,17 @@ public class RoundScrollbarRenderer {
mParent = parent;
+ Resources resources = parent.getContext().getResources();
// Fetch the resource indicating the thickness of CircularDisplayMask, rounding in the same
// way WindowManagerService.showCircularMask does. The scroll bar is inset by this amount so
// that it doesn't get clipped.
int maskThickness =
- parent.getContext()
- .getResources()
- .getDimensionPixelSize(
- com.android.internal.R.dimen.circular_display_mask_thickness);
+ resources.getDimensionPixelSize(
+ com.android.internal.R.dimen.circular_display_mask_thickness);
- float thumbWidth = dpToPx(THUMB_WIDTH_DP);
+ float thumbWidth =
+ resources.getDimensionPixelSize(com.android.internal.R.dimen.round_scrollbar_width);
+ mGapBetweenThumbAndTrackPx = dpToPx(GAP_BETWEEN_TRACK_AND_THUMB_DP);
mThumbPaint.setStrokeWidth(thumbWidth);
mTrackPaint.setStrokeWidth(thumbWidth);
mInset = thumbWidth / 2 + maskThickness;
@@ -175,7 +179,6 @@ public class RoundScrollbarRenderer {
}
}
- /** Returns true if horizontal bounds are updated */
private void updateBounds(Rect bounds) {
mRect.set(
bounds.left + mInset,
@@ -184,6 +187,8 @@ public class RoundScrollbarRenderer {
bounds.bottom - mInset);
mThumbStrokeWidthAsDegrees =
getVertexAngle((mRect.right - mRect.left) / 2f, mThumbPaint.getStrokeWidth() / 2f);
+ mGapBetweenTrackAndThumbAsDegrees =
+ getVertexAngle((mRect.right - mRect.left) / 2f, mGapBetweenThumbAndTrackPx);
}
private float computeSweepAngle(float scrollExtent, float maxScroll) {
@@ -262,20 +267,22 @@ public class RoundScrollbarRenderer {
// The highest point of the top track on a vertical scale. Here the thumb width is
// reduced to account for the arc formed by ROUND stroke style
-SCROLLBAR_ANGLE_RANGE / 2f - mThumbStrokeWidthAsDegrees,
- // The lowest point of the top track on a vertical scale. Here the thumb width is
- // reduced twice to (a) account for the arc formed by ROUND stroke style (b) gap
- // between thumb and top track
- thumbStartAngle - mThumbStrokeWidthAsDegrees * 2,
+ // The lowest point of the top track on a vertical scale. It's reduced by
+ // (a) angular distance for the arc formed by ROUND stroke style
+ // (b) gap between thumb and top track
+ thumbStartAngle - mThumbStrokeWidthAsDegrees - mGapBetweenTrackAndThumbAsDegrees,
alpha);
// Draws the thumb
drawArc(canvas, thumbStartAngle, thumbSweepAngle, mThumbPaint);
// Draws the bottom arc
drawTrack(
canvas,
- // The highest point of the bottom track on a vertical scale. Here the thumb width
- // is added twice to (a) account for the arc formed by ROUND stroke style (b) gap
- // between thumb and bottom track
- (thumbStartAngle + thumbSweepAngle) + mThumbStrokeWidthAsDegrees * 2,
+ // The highest point of the bottom track on a vertical scale. Following added to it
+ // (a) angular distance for the arc formed by ROUND stroke style
+ // (b) gap between thumb and top track
+ (thumbStartAngle + thumbSweepAngle)
+ + mThumbStrokeWidthAsDegrees
+ + mGapBetweenTrackAndThumbAsDegrees,
// The lowest point of the top track on a vertical scale. Here the thumb width is
// added to account for the arc formed by ROUND stroke style
SCROLLBAR_ANGLE_RANGE / 2f + mThumbStrokeWidthAsDegrees,
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index 2895bf3f846a..9e97a8eb58aa 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -21,9 +21,7 @@ import android.annotation.NonNull;
import android.annotation.TestApi;
import android.annotation.UiContext;
import android.app.Activity;
-import android.app.ActivityThread;
import android.app.AppGlobals;
-import android.app.Application;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.res.Configuration;
@@ -41,13 +39,14 @@ import android.util.SparseArray;
import android.util.TypedValue;
import android.view.flags.Flags;
-import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
/**
* Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
*/
public class ViewConfiguration {
+ private static final String TAG = "ViewConfiguration";
+
/**
* Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in
* dips
@@ -350,8 +349,6 @@ public class ViewConfiguration {
*/
private static final int SMART_SELECTION_INITIALIZING_TIMEOUT_IN_MILLISECOND = 500;
- private static ResourceCache sResourceCache = new ResourceCache();
-
private final boolean mConstructedWithContext;
private final int mEdgeSlop;
private final int mFadingEdgeLength;
@@ -377,6 +374,7 @@ public class ViewConfiguration {
private final int mOverscrollDistance;
private final int mOverflingDistance;
private final boolean mViewTouchScreenHapticScrollFeedbackEnabled;
+ @UnsupportedAppUsage
private final boolean mFadingMarqueeEnabled;
private final long mGlobalActionsKeyTimeout;
private final float mVerticalScrollFactor;
@@ -470,12 +468,14 @@ public class ViewConfiguration {
mEdgeSlop = (int) (sizeAndDensity * EDGE_SLOP + 0.5f);
mFadingEdgeLength = (int) (sizeAndDensity * FADING_EDGE_LENGTH + 0.5f);
- mScrollbarSize = res.getDimensionPixelSize(R.dimen.config_scrollbarSize);
+ mScrollbarSize = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_scrollbarSize);
mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f);
mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f);
final TypedValue multiplierValue = new TypedValue();
- res.getValue(R.dimen.config_ambiguousGestureMultiplier,
+ res.getValue(
+ com.android.internal.R.dimen.config_ambiguousGestureMultiplier,
multiplierValue,
true /*resolveRefs*/);
mAmbiguousGestureMultiplier = Math.max(1.0f, multiplierValue.getFloat());
@@ -488,7 +488,8 @@ public class ViewConfiguration {
mOverflingDistance = (int) (sizeAndDensity * OVERFLING_DISTANCE + 0.5f);
if (!sHasPermanentMenuKeySet) {
- final int configVal = res.getInteger(R.integer.config_overrideHasPermanentMenuKey);
+ final int configVal = res.getInteger(
+ com.android.internal.R.integer.config_overrideHasPermanentMenuKey);
switch (configVal) {
default:
@@ -515,27 +516,32 @@ public class ViewConfiguration {
}
}
- mFadingMarqueeEnabled = res.getBoolean(R.bool.config_ui_enableFadingMarquee);
- mTouchSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationTouchSlop);
+ mFadingMarqueeEnabled = res.getBoolean(
+ com.android.internal.R.bool.config_ui_enableFadingMarquee);
+ mTouchSlop = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
mHandwritingSlop = res.getDimensionPixelSize(
- R.dimen.config_viewConfigurationHandwritingSlop);
- mHoverSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationHoverSlop);
+ com.android.internal.R.dimen.config_viewConfigurationHandwritingSlop);
+ mHoverSlop = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewConfigurationHoverSlop);
mMinScrollbarTouchTarget = res.getDimensionPixelSize(
- R.dimen.config_minScrollbarTouchTarget);
+ com.android.internal.R.dimen.config_minScrollbarTouchTarget);
mPagingTouchSlop = mTouchSlop * 2;
mDoubleTapTouchSlop = mTouchSlop;
mHandwritingGestureLineMargin = res.getDimensionPixelSize(
- R.dimen.config_viewConfigurationHandwritingGestureLineMargin);
+ com.android.internal.R.dimen.config_viewConfigurationHandwritingGestureLineMargin);
- mMinimumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMinFlingVelocity);
- mMaximumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMaxFlingVelocity);
+ mMinimumFlingVelocity = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewMinFlingVelocity);
+ mMaximumFlingVelocity = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewMaxFlingVelocity);
int configMinRotaryEncoderFlingVelocity = res.getDimensionPixelSize(
- R.dimen.config_viewMinRotaryEncoderFlingVelocity);
+ com.android.internal.R.dimen.config_viewMinRotaryEncoderFlingVelocity);
int configMaxRotaryEncoderFlingVelocity = res.getDimensionPixelSize(
- R.dimen.config_viewMaxRotaryEncoderFlingVelocity);
+ com.android.internal.R.dimen.config_viewMaxRotaryEncoderFlingVelocity);
if (configMinRotaryEncoderFlingVelocity < 0 || configMaxRotaryEncoderFlingVelocity < 0) {
mMinimumRotaryEncoderFlingVelocity = NO_FLING_MIN_VELOCITY;
mMaximumRotaryEncoderFlingVelocity = NO_FLING_MAX_VELOCITY;
@@ -545,7 +551,8 @@ public class ViewConfiguration {
}
int configRotaryEncoderHapticScrollFeedbackTickIntervalPixels =
- res.getDimensionPixelSize(R.dimen
+ res.getDimensionPixelSize(
+ com.android.internal.R.dimen
.config_rotaryEncoderAxisScrollTickInterval);
mRotaryEncoderHapticScrollFeedbackTickIntervalPixels =
configRotaryEncoderHapticScrollFeedbackTickIntervalPixels > 0
@@ -553,31 +560,41 @@ public class ViewConfiguration {
: NO_HAPTIC_SCROLL_TICK_INTERVAL;
mRotaryEncoderHapticScrollFeedbackEnabled =
- res.getBoolean(R.bool
+ res.getBoolean(
+ com.android.internal.R.bool
.config_viewRotaryEncoderHapticScrollFedbackEnabled);
- mGlobalActionsKeyTimeout = res.getInteger(R.integer.config_globalActionsKeyTimeout);
+ mGlobalActionsKeyTimeout = res.getInteger(
+ com.android.internal.R.integer.config_globalActionsKeyTimeout);
- mHorizontalScrollFactor = res.getDimensionPixelSize(R.dimen.config_horizontalScrollFactor);
- mVerticalScrollFactor = res.getDimensionPixelSize(R.dimen.config_verticalScrollFactor);
+ mHorizontalScrollFactor = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_horizontalScrollFactor);
+ mVerticalScrollFactor = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_verticalScrollFactor);
mShowMenuShortcutsWhenKeyboardPresent = res.getBoolean(
- R.bool.config_showMenuShortcutsWhenKeyboardPresent);
+ com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent);
- mMinScalingSpan = res.getDimensionPixelSize(R.dimen.config_minScalingSpan);
+ mMinScalingSpan = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_minScalingSpan);
- mScreenshotChordKeyTimeout = res.getInteger(R.integer.config_screenshotChordKeyTimeout);
+ mScreenshotChordKeyTimeout = res.getInteger(
+ com.android.internal.R.integer.config_screenshotChordKeyTimeout);
mSmartSelectionInitializedTimeout = res.getInteger(
- R.integer.config_smartSelectionInitializedTimeoutMillis);
+ com.android.internal.R.integer.config_smartSelectionInitializedTimeoutMillis);
mSmartSelectionInitializingTimeout = res.getInteger(
- R.integer.config_smartSelectionInitializingTimeoutMillis);
- mPreferKeepClearForFocusEnabled = res.getBoolean(R.bool.config_preferKeepClearForFocus);
+ com.android.internal.R.integer.config_smartSelectionInitializingTimeoutMillis);
+ mPreferKeepClearForFocusEnabled = res.getBoolean(
+ com.android.internal.R.bool.config_preferKeepClearForFocus);
mViewBasedRotaryEncoderScrollHapticsEnabledConfig =
- res.getBoolean(R.bool.config_viewBasedRotaryEncoderHapticsEnabled);
+ res.getBoolean(
+ com.android.internal.R.bool.config_viewBasedRotaryEncoderHapticsEnabled);
mViewTouchScreenHapticScrollFeedbackEnabled =
Flags.enableScrollFeedbackForTouch()
- ? res.getBoolean(R.bool.config_viewTouchScreenHapticScrollFeedbackEnabled)
+ ? res.getBoolean(
+ com.android.internal.R.bool
+ .config_viewTouchScreenHapticScrollFeedbackEnabled)
: false;
}
@@ -615,7 +632,6 @@ public class ViewConfiguration {
@VisibleForTesting
public static void resetCacheForTesting() {
sConfigurations.clear();
- sResourceCache = new ResourceCache();
}
/**
@@ -691,7 +707,7 @@ public class ViewConfiguration {
* components.
*/
public static int getPressedStateDuration() {
- return sResourceCache.getPressedStateDuration();
+ return PRESSED_STATE_DURATION;
}
/**
@@ -736,7 +752,7 @@ public class ViewConfiguration {
* considered to be a tap.
*/
public static int getTapTimeout() {
- return sResourceCache.getTapTimeout();
+ return TAP_TIMEOUT;
}
/**
@@ -745,7 +761,7 @@ public class ViewConfiguration {
* considered to be a tap.
*/
public static int getJumpTapTimeout() {
- return sResourceCache.getJumpTapTimeout();
+ return JUMP_TAP_TIMEOUT;
}
/**
@@ -754,7 +770,7 @@ public class ViewConfiguration {
* double-tap.
*/
public static int getDoubleTapTimeout() {
- return sResourceCache.getDoubleTapTimeout();
+ return DOUBLE_TAP_TIMEOUT;
}
/**
@@ -766,7 +782,7 @@ public class ViewConfiguration {
*/
@UnsupportedAppUsage
public static int getDoubleTapMinTime() {
- return sResourceCache.getDoubleTapMinTime();
+ return DOUBLE_TAP_MIN_TIME;
}
/**
@@ -776,7 +792,7 @@ public class ViewConfiguration {
* @hide
*/
public static int getHoverTapTimeout() {
- return sResourceCache.getHoverTapTimeout();
+ return HOVER_TAP_TIMEOUT;
}
/**
@@ -787,7 +803,7 @@ public class ViewConfiguration {
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static int getHoverTapSlop() {
- return sResourceCache.getHoverTapSlop();
+ return HOVER_TAP_SLOP;
}
/**
@@ -1028,7 +1044,7 @@ public class ViewConfiguration {
* in milliseconds.
*/
public static long getZoomControlsTimeout() {
- return sResourceCache.getZoomControlsTimeout();
+ return ZOOM_CONTROLS_TIMEOUT;
}
/**
@@ -1097,14 +1113,14 @@ public class ViewConfiguration {
* friction.
*/
public static float getScrollFriction() {
- return sResourceCache.getScrollFriction();
+ return SCROLL_FRICTION;
}
/**
* @return the default duration in milliseconds for {@link ActionMode#hide(long)}.
*/
public static long getDefaultActionModeHideDuration() {
- return sResourceCache.getDefaultActionModeHideDuration();
+ return ACTION_MODE_HIDE_DURATION_DEFAULT;
}
/**
@@ -1455,137 +1471,8 @@ public class ViewConfiguration {
return HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT;
}
- private static int getDisplayDensity(Context context) {
+ private static final int getDisplayDensity(Context context) {
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
return (int) (100.0f * metrics.density);
}
-
- /**
- * Fetches resource values statically and caches them locally for fast lookup. Note that these
- * values will not be updated during the lifetime of a process, even if resource overlays are
- * applied.
- */
- private static final class ResourceCache {
-
- private int mPressedStateDuration = -1;
- private int mTapTimeout = -1;
- private int mJumpTapTimeout = -1;
- private int mDoubleTapTimeout = -1;
- private int mDoubleTapMinTime = -1;
- private int mHoverTapTimeout = -1;
- private int mHoverTapSlop = -1;
- private long mZoomControlsTimeout = -1L;
- private float mScrollFriction = -1f;
- private long mDefaultActionModeHideDuration = -1L;
-
- public int getPressedStateDuration() {
- if (mPressedStateDuration < 0) {
- Resources resources = getCurrentResources();
- mPressedStateDuration = resources != null
- ? resources.getInteger(R.integer.config_pressedStateDurationMillis)
- : PRESSED_STATE_DURATION;
- }
- return mPressedStateDuration;
- }
-
- public int getTapTimeout() {
- if (mTapTimeout < 0) {
- Resources resources = getCurrentResources();
- mTapTimeout = resources != null
- ? resources.getInteger(R.integer.config_tapTimeoutMillis)
- : TAP_TIMEOUT;
- }
- return mTapTimeout;
- }
-
- public int getJumpTapTimeout() {
- if (mJumpTapTimeout < 0) {
- Resources resources = getCurrentResources();
- mJumpTapTimeout = resources != null
- ? resources.getInteger(R.integer.config_jumpTapTimeoutMillis)
- : JUMP_TAP_TIMEOUT;
- }
- return mJumpTapTimeout;
- }
-
- public int getDoubleTapTimeout() {
- if (mDoubleTapTimeout < 0) {
- Resources resources = getCurrentResources();
- mDoubleTapTimeout = resources != null
- ? resources.getInteger(R.integer.config_doubleTapTimeoutMillis)
- : DOUBLE_TAP_TIMEOUT;
- }
- return mDoubleTapTimeout;
- }
-
- public int getDoubleTapMinTime() {
- if (mDoubleTapMinTime < 0) {
- Resources resources = getCurrentResources();
- mDoubleTapMinTime = resources != null
- ? resources.getInteger(R.integer.config_doubleTapMinTimeMillis)
- : DOUBLE_TAP_MIN_TIME;
- }
- return mDoubleTapMinTime;
- }
-
- public int getHoverTapTimeout() {
- if (mHoverTapTimeout < 0) {
- Resources resources = getCurrentResources();
- mHoverTapTimeout = resources != null
- ? resources.getInteger(R.integer.config_hoverTapTimeoutMillis)
- : HOVER_TAP_TIMEOUT;
- }
- return mHoverTapTimeout;
- }
-
- public int getHoverTapSlop() {
- if (mHoverTapSlop < 0) {
- Resources resources = getCurrentResources();
- mHoverTapSlop = resources != null
- ? resources.getDimensionPixelSize(R.dimen.config_hoverTapSlop)
- : HOVER_TAP_SLOP;
- }
- return mHoverTapSlop;
- }
-
- public long getZoomControlsTimeout() {
- if (mZoomControlsTimeout < 0) {
- Resources resources = getCurrentResources();
- mZoomControlsTimeout = resources != null
- ? resources.getInteger(R.integer.config_zoomControlsTimeoutMillis)
- : ZOOM_CONTROLS_TIMEOUT;
- }
- return mZoomControlsTimeout;
- }
-
- public float getScrollFriction() {
- if (mScrollFriction < 0) {
- Resources resources = getCurrentResources();
- mScrollFriction = resources != null
- ? resources.getFloat(R.dimen.config_scrollFriction)
- : SCROLL_FRICTION;
- }
- return mScrollFriction;
- }
-
- public long getDefaultActionModeHideDuration() {
- if (mDefaultActionModeHideDuration < 0) {
- Resources resources = getCurrentResources();
- mDefaultActionModeHideDuration = resources != null
- ? resources.getInteger(R.integer.config_defaultActionModeHideDurationMillis)
- : ACTION_MODE_HIDE_DURATION_DEFAULT;
- }
- return mDefaultActionModeHideDuration;
- }
-
- private static Resources getCurrentResources() {
- if (!android.companion.virtualdevice.flags.Flags
- .migrateViewconfigurationConstantsToResources()) {
- return null;
- }
- Application application = ActivityThread.currentApplication();
- Context context = application != null ? application.getApplicationContext() : null;
- return context != null ? context.getResources() : null;
- }
- }
}
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index d413ba0b042c..09c6dc0e2b20 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -572,6 +572,13 @@ flag {
}
flag {
+ name: "enable_display_reconnect_interaction"
+ namespace: "lse_desktop_experience"
+ description: "Enables new interaction that occurs when a display is reconnected."
+ bug: "365873835"
+}
+
+flag {
name: "show_desktop_experience_dev_option"
namespace: "lse_desktop_experience"
description: "Replace the freeform windowing dev options with a desktop experience one."
diff --git a/core/java/com/android/internal/graphics/palette/OWNERS b/core/java/com/android/internal/graphics/palette/OWNERS
index 731dca9b128f..df867252c01c 100644
--- a/core/java/com/android/internal/graphics/palette/OWNERS
+++ b/core/java/com/android/internal/graphics/palette/OWNERS
@@ -1,3 +1,2 @@
-# Bug component: 484670
-dupin@google.com
-jamesoleary@google.com \ No newline at end of file
+# Bug component: 484670
+dupin@google.com
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 270cf085b06f..e20a52b24485 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -231,6 +231,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
private int mLastRightInset = 0;
@UnsupportedAppUsage
private int mLastLeftInset = 0;
+ private WindowInsets mLastInsets = null;
private boolean mLastHasTopStableInset = false;
private boolean mLastHasBottomStableInset = false;
private boolean mLastHasRightStableInset = false;
@@ -1100,6 +1101,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
mLastWindowFlags = attrs.flags;
if (insets != null) {
+ mLastInsets = insets;
mLastForceConsumingTypes = insets.getForceConsumingTypes();
mLastForceConsumingOpaqueCaptionBar = insets.isForceConsumingOpaqueCaptionBar();
@@ -1176,6 +1178,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
mForceWindowDrawsBarBackgrounds, requestedVisibleTypes);
}
+ int consumingTypes = 0;
// When we expand the window with FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or
// mForceWindowDrawsBarBackgrounds, we still need to ensure that the rest of the view
// hierarchy doesn't notice it, unless they've explicitly asked for it.
@@ -1186,43 +1189,47 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
//
// Note: Once the app uses the R+ Window.setDecorFitsSystemWindows(false) API we no longer
// consume insets because they might no longer set SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION.
- boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
+ final boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
|| (requestedVisibleTypes & WindowInsets.Type.navigationBars()) == 0;
- boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows;
- boolean forceConsumingNavBar =
+ final boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows;
+
+ final boolean fitsNavBar =
+ (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
+ && decorFitsSystemWindows
+ && !hideNavigation;
+ final boolean forceConsumingNavBar =
((mForceWindowDrawsBarBackgrounds || mDrawLegacyNavigationBarBackgroundHandled)
&& (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0
- && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
- && decorFitsSystemWindows
- && !hideNavigation)
+ && fitsNavBar)
|| ((mLastForceConsumingTypes & WindowInsets.Type.navigationBars()) != 0
&& hideNavigation);
-
- boolean consumingNavBar =
- ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
- && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
- && decorFitsSystemWindows
- && !hideNavigation)
+ final boolean consumingNavBar =
+ ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 && fitsNavBar)
|| forceConsumingNavBar;
+ if (consumingNavBar) {
+ consumingTypes |= WindowInsets.Type.navigationBars();
+ }
- // If we didn't request fullscreen layout, but we still got it because of the
- // mForceWindowDrawsBarBackgrounds flag, also consume top inset.
+ // If the fullscreen layout was not requested, but still received because of the
+ // mForceWindowDrawsBarBackgrounds flag, also consume status bar.
// If we should always consume system bars, only consume that if the app wanted to go to
// fullscreen, as otherwise we can expect the app to handle it.
- boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0
+ final boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0
|| (attrs.flags & FLAG_FULLSCREEN) != 0;
final boolean hideStatusBar = fullscreen
|| (requestedVisibleTypes & WindowInsets.Type.statusBars()) == 0;
- boolean consumingStatusBar =
- ((sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
- && decorFitsSystemWindows
- && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
- && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
- && mForceWindowDrawsBarBackgrounds
- && mLastTopInset != 0)
+ if (((sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
+ && decorFitsSystemWindows
+ && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
+ && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
+ && mForceWindowDrawsBarBackgrounds
+ && mLastTopInset != 0)
|| ((mLastForceConsumingTypes & WindowInsets.Type.statusBars()) != 0
- && hideStatusBar);
+ && hideStatusBar)) {
+ consumingTypes |= WindowInsets.Type.statusBars();
+ }
+ // Decide if caption bar need to be consumed
final boolean hideCaptionBar = fullscreen
|| (requestedVisibleTypes & WindowInsets.Type.captionBar()) == 0;
final boolean consumingCaptionBar =
@@ -1237,22 +1244,23 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
&& mLastForceConsumingOpaqueCaptionBar
&& isOpaqueCaptionBar;
- final int consumedTop =
- (consumingStatusBar || consumingCaptionBar || consumingOpaqueCaptionBar)
- ? mLastTopInset : 0;
- int consumedRight = consumingNavBar ? mLastRightInset : 0;
- int consumedBottom = consumingNavBar ? mLastBottomInset : 0;
- int consumedLeft = consumingNavBar ? mLastLeftInset : 0;
+ if (consumingCaptionBar || consumingOpaqueCaptionBar) {
+ consumingTypes |= WindowInsets.Type.captionBar();
+ }
+
+ final Insets consumedInsets = mLastInsets != null
+ ? mLastInsets.getInsets(consumingTypes) : Insets.NONE;
if (mContentRoot != null
&& mContentRoot.getLayoutParams() instanceof MarginLayoutParams) {
MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams();
- if (lp.topMargin != consumedTop || lp.rightMargin != consumedRight
- || lp.bottomMargin != consumedBottom || lp.leftMargin != consumedLeft) {
- lp.topMargin = consumedTop;
- lp.rightMargin = consumedRight;
- lp.bottomMargin = consumedBottom;
- lp.leftMargin = consumedLeft;
+ if (lp.topMargin != consumedInsets.top || lp.rightMargin != consumedInsets.right
+ || lp.bottomMargin != consumedInsets.bottom || lp.leftMargin !=
+ consumedInsets.left) {
+ lp.topMargin = consumedInsets.top;
+ lp.rightMargin = consumedInsets.right;
+ lp.bottomMargin = consumedInsets.bottom;
+ lp.leftMargin = consumedInsets.left;
mContentRoot.setLayoutParams(lp);
if (insets == null) {
@@ -1261,11 +1269,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
requestApplyInsets();
}
}
- if (insets != null && (consumedLeft > 0
- || consumedTop > 0
- || consumedRight > 0
- || consumedBottom > 0)) {
- insets = insets.inset(consumedLeft, consumedTop, consumedRight, consumedBottom);
+ if (insets != null && !Insets.NONE.equals(consumedInsets)) {
+ insets = insets.inset(consumedInsets);
}
}
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index d8f1b626abf2..31b9fd1ad170 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -284,6 +284,8 @@ void NativeDisplayEventReceiver::dispatchModeRejected(PhysicalDisplayId displayI
displayId.value, modeId);
ALOGV("receiver %p ~ Returned from Mode Rejected handler.", this);
}
+
+ mMessageQueue->raiseAndClearException(env, "dispatchModeRejected");
}
void NativeDisplayEventReceiver::dispatchFrameRateOverrides(
@@ -314,7 +316,7 @@ void NativeDisplayEventReceiver::dispatchFrameRateOverrides(
ALOGV("receiver %p ~ Returned from FrameRateOverride handler.", this);
}
- mMessageQueue->raiseAndClearException(env, "dispatchModeChanged");
+ mMessageQueue->raiseAndClearException(env, "dispatchFrameRateOverrides");
}
void NativeDisplayEventReceiver::dispatchHdcpLevelsChanged(PhysicalDisplayId displayId,
diff --git a/core/res/res/values-w225dp/dimens.xml b/core/res/res/values-w225dp/dimens.xml
new file mode 100644
index 000000000000..0cd3293f0894
--- /dev/null
+++ b/core/res/res/values-w225dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!-- The width of the round scrollbar -->
+ <dimen name="round_scrollbar_width">6dp</dimen>
+</resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 6d57427ce221..b3581d98face 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3061,43 +3061,6 @@
{@link MotionEvent#ACTION_SCROLL} event. -->
<dimen name="config_scrollFactor">64dp</dimen>
- <!-- Duration in milliseconds of the pressed state in child components. -->
- <integer name="config_pressedStateDurationMillis">64</integer>
-
- <!-- Duration in milliseconds we will wait to see if a touch event is a tap or a scroll.
- If the user does not move within this interval, it is considered to be a tap. -->
- <integer name="config_tapTimeoutMillis">100</integer>
-
- <!-- Duration in milliseconds we will wait to see if a touch event is a jump tap.
- If the user does not move within this interval, it is considered to be a tap. -->
- <integer name="config_jumpTapTimeoutMillis">500</integer>
-
- <!-- Duration in milliseconds between the first tap's up event and the second tap's down
- event for an interaction to be considered a double-tap. -->
- <integer name="config_doubleTapTimeoutMillis">300</integer>
-
- <!-- Minimum duration in milliseconds between the first tap's up event and the second tap's
- down event for an interaction to be considered a double-tap. -->
- <integer name="config_doubleTapMinTimeMillis">40</integer>
-
- <!-- Maximum duration in milliseconds between a touch pad touch and release for a given touch
- to be considered a tap (click) as opposed to a hover movement gesture. -->
- <integer name="config_hoverTapTimeoutMillis">150</integer>
-
- <!-- The amount of time in milliseconds that the zoom controls should be displayed on the
- screen. -->
- <integer name="config_zoomControlsTimeoutMillis">3000</integer>
-
- <!-- Default duration in milliseconds for {@link ActionMode#hide(long)}. -->
- <integer name="config_defaultActionModeHideDurationMillis">2000</integer>
-
- <!-- Maximum distance in pixels that a touch pad touch can move before being released
- for it to be considered a tap (click) as opposed to a hover movement gesture. -->
- <dimen name="config_hoverTapSlop">20px</dimen>
-
- <!-- The amount of friction applied to scrolls and flings. -->
- <item name="config_scrollFriction" format="float" type="dimen">0.015</item>
-
<!-- Maximum number of grid columns permitted in the ResolverActivity
used for picking activities to handle an intent. -->
<integer name="config_maxResolverActivityColumns">3</integer>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 484e8ef1e049..595160ec9f66 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -782,6 +782,9 @@
aliasing effects). This is only used on circular displays. -->
<dimen name="circular_display_mask_thickness">1px</dimen>
+ <!-- The width of the round scrollbar -->
+ <dimen name="round_scrollbar_width">5dp</dimen>
+
<dimen name="lock_pattern_dot_line_width">22dp</dimen>
<dimen name="lock_pattern_dot_size">14dp</dimen>
<dimen name="lock_pattern_dot_size_activated">30dp</dimen>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c92743827008..9393aa4b6086 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -586,6 +586,7 @@
<java-symbol type="dimen" name="accessibility_magnification_indicator_width" />
<java-symbol type="dimen" name="circular_display_mask_thickness" />
<java-symbol type="dimen" name="user_icon_size" />
+ <java-symbol type="dimen" name="round_scrollbar_width" />
<java-symbol type="string" name="add_account_button_label" />
<java-symbol type="string" name="addToDictionary" />
@@ -4154,17 +4155,6 @@
<java-symbol type="string" name="config_headlineFontFamily" />
<java-symbol type="string" name="config_headlineFontFamilyMedium" />
- <java-symbol type="integer" name="config_pressedStateDurationMillis" />
- <java-symbol type="integer" name="config_tapTimeoutMillis" />
- <java-symbol type="integer" name="config_jumpTapTimeoutMillis" />
- <java-symbol type="integer" name="config_doubleTapTimeoutMillis" />
- <java-symbol type="integer" name="config_doubleTapMinTimeMillis" />
- <java-symbol type="integer" name="config_hoverTapTimeoutMillis" />
- <java-symbol type="integer" name="config_zoomControlsTimeoutMillis" />
- <java-symbol type="integer" name="config_defaultActionModeHideDurationMillis" />
- <java-symbol type="dimen" name="config_hoverTapSlop" />
- <java-symbol type="dimen" name="config_scrollFriction" />
-
<java-symbol type="drawable" name="stat_sys_vitals" />
<java-symbol type="color" name="text_color_primary" />
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
index aa523f57c469..35802c936361 100644
--- 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
@@ -58,11 +58,11 @@ class DragZoneFactory(
when (draggedObject) {
is DraggedObject.BubbleBar -> {
dragZones.add(createDismissDragZone())
- dragZones.addAll(createBubbleDragZones())
+ dragZones.addAll(createBubbleHalfScreenDragZones())
}
is DraggedObject.Bubble -> {
dragZones.add(createDismissDragZone())
- dragZones.addAll(createBubbleDragZones())
+ dragZones.addAll(createBubbleCornerDragZones())
dragZones.add(createFullScreenDragZone())
if (shouldShowDesktopWindowDragZones()) {
dragZones.add(createDesktopWindowDragZoneForBubble())
@@ -80,7 +80,7 @@ class DragZoneFactory(
} else {
dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet())
}
- createBubbleDragZonesForExpandedView()
+ dragZones.addAll(createBubbleHalfScreenDragZones())
}
}
return dragZones
@@ -98,7 +98,7 @@ class DragZoneFactory(
)
}
- private fun createBubbleDragZones(): List<DragZone> {
+ private fun createBubbleCornerDragZones(): List<DragZone> {
val dragZoneSize =
if (deviceConfig.isSmallTablet) {
bubbleDragZoneFoldableSize
@@ -124,7 +124,7 @@ class DragZoneFactory(
)
}
- private fun createBubbleDragZonesForExpandedView(): List<DragZone> {
+ private fun createBubbleHalfScreenDragZones(): List<DragZone> {
return listOf(
DragZone.Bubble.Left(
bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
new file mode 100644
index 000000000000..29ce8d90e66f
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
@@ -0,0 +1,78 @@
+/*
+ * 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
+
+/**
+ * Manages animating drop targets in response to dragging bubble icons or bubble expanded views
+ * across different drag zones.
+ */
+class DropTargetManager(
+ private val isLayoutRtl: Boolean,
+ private val dragZoneChangedListener: DragZoneChangedListener
+) {
+
+ private var state: DragState? = null
+
+ /** Must be called when a drag gesture is starting. */
+ fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) {
+ val state = DragState(dragZones, draggedObject)
+ dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone)
+ this.state = state
+ }
+
+ /** Called when the user drags to a new location. */
+ fun onDragUpdated(x: Int, y: Int) {
+ val state = state ?: return
+ val oldDragZone = state.currentDragZone
+ val newDragZone = state.getMatchingDragZone(x = x, y = y)
+ state.currentDragZone = newDragZone
+ if (oldDragZone != newDragZone) {
+ dragZoneChangedListener.onDragZoneChanged(from = oldDragZone, to = newDragZone)
+ }
+ }
+
+ /** Called when the drag ended. */
+ fun onDragEnded() {
+ state = null
+ }
+
+ /** Stores the current drag state. */
+ private inner class DragState(
+ private val dragZones: List<DragZone>,
+ draggedObject: DraggedObject
+ ) {
+ val initialDragZone =
+ if (draggedObject.initialLocation.isOnLeft(isLayoutRtl)) {
+ dragZones.filterIsInstance<DragZone.Bubble.Left>().first()
+ } else {
+ dragZones.filterIsInstance<DragZone.Bubble.Right>().first()
+ }
+ var currentDragZone: DragZone = initialDragZone
+
+ fun getMatchingDragZone(x: Int, y: Int): DragZone {
+ return dragZones.firstOrNull { it.contains(x, y) } ?: currentDragZone
+ }
+ }
+
+ /** An interface to be notified when drag zones change. */
+ interface DragZoneChangedListener {
+ /** An initial drag zone was set. Called when a drag starts. */
+ fun onInitialDragZoneSet(dragZone: DragZone)
+ /** Called when the object was dragged to a different drag zone. */
+ fun onDragZoneChanged(from: DragZone, to: DragZone)
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index ae8f8c4eff79..b43ea3161dec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -1450,6 +1450,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
wct.clear();
if (Flags.enableRecentsBookendTransition()) {
+ // Notify the mixers of the pending finish
+ for (int i = 0; i < mMixers.size(); ++i) {
+ mMixers.get(i).handleFinishRecents(returningToApp, wct, t);
+ }
+
// In this case, we've already started the PIP transition, so we can
// clean up immediately
mPendingRunnerFinishCb = runnerFinishCb;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index aff21cbe0ae6..15ac03ccaf30 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -1675,8 +1675,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
void prepareExitSplitScreen(@StageType int stageToTop,
@NonNull WindowContainerTransaction wct, @ExitReason int exitReason) {
if (!isSplitActive()) return;
- ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s",
- stageTypeToString(stageToTop));
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s reason=%s",
+ stageTypeToString(stageToTop), exitReasonToString(exitReason));
if (enableFlexibleSplit()) {
mStageOrderOperator.getActiveStages().stream()
.filter(stage -> stage.getId() != stageToTop)
@@ -3395,12 +3395,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
TransitionInfo.Change sideChild = null;
StageTaskListener firstAppStage = null;
StageTaskListener secondAppStage = null;
+ boolean foundPausingTask = false;
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
for (int iC = 0; iC < info.getChanges().size(); ++iC) {
final TransitionInfo.Change change = info.getChanges().get(iC);
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
if (taskInfo == null || !taskInfo.hasParentTask()) continue;
if (mPausingTasks.contains(taskInfo.taskId)) {
+ foundPausingTask = true;
continue;
}
StageTaskListener stage = getStageOfTask(taskInfo);
@@ -3443,9 +3445,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
prepareExitSplitScreen(dismissTop, cancelWct, EXIT_REASON_UNKNOWN);
logExit(EXIT_REASON_UNKNOWN);
});
- Log.w(TAG, splitFailureMessage("startPendingEnterAnimation",
- "launched 2 tasks in split, but didn't receive "
- + "2 tasks in transition. Possibly one of them failed to launch"));
+ Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", "launched 2 tasks in "
+ + "split, but didn't receive 2 tasks in transition. Possibly one of them "
+ + "failed to launch (foundPausingTask=" + foundPausingTask + ")"));
if (mRecentTasks.isPresent() && mainChild != null) {
mRecentTasks.get().removeSplitPair(mainChild.getTaskInfo().taskId);
}
@@ -3800,6 +3802,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
/** Call this when the recents animation canceled during split-screen. */
public void onRecentsInSplitAnimationCanceled() {
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationCanceled");
mPausingTasks.clear();
setSplitsVisible(false);
@@ -3809,31 +3812,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mTaskOrganizer.applyTransaction(wct);
}
- public void onRecentsInSplitAnimationFinishing(boolean returnToApp,
- @NonNull WindowContainerTransaction finishWct,
- @NonNull SurfaceControl.Transaction finishT) {
- if (!Flags.enableRecentsBookendTransition()) {
- // The non-bookend recents transition case will be handled by
- // RecentsMixedTransition wrapping the finish callback and calling
- // onRecentsInSplitAnimationFinish()
- return;
- }
-
- onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT);
- }
-
- /** Call this when the recents animation during split-screen finishes. */
- public void onRecentsInSplitAnimationFinish(@NonNull WindowContainerTransaction finishWct,
- @NonNull SurfaceControl.Transaction finishT) {
- if (Flags.enableRecentsBookendTransition()) {
- // The bookend recents transition case will be handled by
- // onRecentsInSplitAnimationFinishing above
- return;
- }
-
- // Check if the recent transition is finished by returning to the current
- // split, so we can restore the divider bar.
- boolean returnToApp = false;
+ /**
+ * Returns whether the given WCT is reordering any of the split tasks to top.
+ */
+ public boolean wctIsReorderingSplitToTop(@NonNull WindowContainerTransaction finishWct) {
for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
final WindowContainerTransaction.HierarchyOp op =
finishWct.getHierarchyOps().get(i);
@@ -3848,14 +3830,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
&& anyStageContainsContainer) {
- returnToApp = true;
+ return true;
}
}
- onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT);
+ return false;
}
- /** Call this when the recents animation during split-screen finishes. */
- public void onRecentsInSplitAnimationFinishInner(boolean returnToApp,
+ /** Called when the recents animation during split-screen finishes. */
+ public void onRecentsInSplitAnimationFinishing(boolean returnToApp,
@NonNull WindowContainerTransaction finishWct,
@NonNull SurfaceControl.Transaction finishT) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish: returnToApp=%b",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
index f40dc8ad93b5..1e926c57ca61 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -159,9 +159,17 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
// If pair-to-pair switching, the post-recents clean-up isn't needed.
wct = wct != null ? wct : new WindowContainerTransaction();
if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
- // TODO(b/346588978): Only called if !enableRecentsBookendTransition(), can remove
- // once that rolls out
- mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
+ // We've dispatched to the mLeftoversHandler to handle the rest of the transition
+ // and called onRecentsInSplitAnimationStart(), but if the recents handler is not
+ // actually handling the transition, then onRecentsInSplitAnimationFinishing()
+ // won't actually get called by the recents handler. In such cases, we still need
+ // to clean up after the changes from the start call.
+ boolean splitNotifiedByRecents = mRecentsHandler == mLeftoversHandler;
+ if (!splitNotifiedByRecents) {
+ mSplitHandler.onRecentsInSplitAnimationFinishing(
+ mSplitHandler.wctIsReorderingSplitToTop(wct),
+ wct, finishTransaction);
+ }
} else {
// notify pair-to-pair recents animation finish
mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index 19829e7e5677..bac8e5062128 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -12,7 +12,6 @@ atsjenk@google.com
jorgegil@google.com
vaniadesmonda@google.com
pbdr@google.com
-tkachenkoi@google.com
mpodolian@google.com
jeremysim@google.com
peanutbutter@google.com
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
index e28d6ff8bf7f..7cd46af9402b 100644
--- 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
@@ -27,6 +27,8 @@ import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
+private typealias DragZoneVerifier = (dragZone: DragZone) -> Unit
+
@SmallTest
@RunWith(AndroidJUnit4::class)
/** Unit tests for [DragZoneFactory]. */
@@ -58,15 +60,14 @@ class DragZoneFactoryTest {
DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.BubbleBar(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
listOf(
- DragZone.Dismiss::class.java,
- DragZone.Bubble::class.java,
- DragZone.Bubble::class.java,
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
)
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
@@ -75,19 +76,18 @@ class DragZoneFactoryTest {
DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
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,
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.DesktopWindow>(),
+ verifyInstance<DragZone.Split.Top>(),
+ verifyInstance<DragZone.Split.Bottom>(),
)
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
@@ -95,19 +95,18 @@ class DragZoneFactoryTest {
dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker)
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
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,
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.DesktopWindow>(),
+ verifyInstance<DragZone.Split.Left>(),
+ verifyInstance<DragZone.Split.Right>(),
)
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
@@ -115,18 +114,17 @@ class DragZoneFactoryTest {
dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker)
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
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,
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.Split.Left>(),
+ verifyInstance<DragZone.Split.Right>(),
)
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
@@ -134,18 +132,17 @@ class DragZoneFactoryTest {
dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
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,
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.Split.Top>(),
+ verifyInstance<DragZone.Split.Bottom>(),
)
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
@@ -156,19 +153,18 @@ class DragZoneFactoryTest {
dragZoneFactory.createSortedDragZones(
DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
)
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
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,
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.DesktopWindow>(),
+ verifyInstance<DragZone.Split.Top>(),
+ verifyInstance<DragZone.Split.Bottom>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
)
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
@@ -176,19 +172,18 @@ class DragZoneFactoryTest {
dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker)
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
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,
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.DesktopWindow>(),
+ verifyInstance<DragZone.Split.Left>(),
+ verifyInstance<DragZone.Split.Right>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
)
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
@@ -196,18 +191,17 @@ class DragZoneFactoryTest {
dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker)
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
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,
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.Split.Left>(),
+ verifyInstance<DragZone.Split.Right>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
)
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
@@ -215,18 +209,17 @@ class DragZoneFactoryTest {
dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
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,
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.Split.Top>(),
+ verifyInstance<DragZone.Split.Bottom>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
)
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
@@ -246,4 +239,8 @@ class DragZoneFactoryTest {
dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty()
}
+
+ private inline fun <reified T> verifyInstance(): DragZoneVerifier = { dragZone ->
+ assertThat(dragZone).isInstanceOf(T::class.java)
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
new file mode 100644
index 000000000000..efb91c5fbfda
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertFails
+
+/** Unit tests for [DropTargetManager]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DropTargetManagerTest {
+
+ private lateinit var dropTargetManager: DropTargetManager
+ private lateinit var dragZoneChangedListener: FakeDragZoneChangedListener
+ private val dropTarget = Rect(0, 0, 0, 0)
+
+ // create 3 drop zones that are horizontally next to each other
+ // -------------------------------------------------
+ // | | | |
+ // | bubble | | bubble |
+ // | | dismiss | |
+ // | left | | right |
+ // | | | |
+ // -------------------------------------------------
+ private val bubbleLeftDragZone =
+ DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = dropTarget)
+ private val dismissDragZone = DragZone.Dismiss(bounds = Rect(100, 0, 200, 100))
+ private val bubbleRightDragZone =
+ DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = dropTarget)
+
+ @Before
+ fun setUp() {
+ dragZoneChangedListener = FakeDragZoneChangedListener()
+ dropTargetManager = DropTargetManager(isLayoutRtl = false, dragZoneChangedListener)
+ }
+
+ @Test
+ fun onDragStarted_notifiesInitialDragZone() {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone)
+ )
+ assertThat(dragZoneChangedListener.initialDragZone).isEqualTo(bubbleLeftDragZone)
+ }
+
+ @Test
+ fun onDragStarted_missingExpectedDragZone_fails() {
+ assertFails {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.RIGHT),
+ listOf(bubbleLeftDragZone)
+ )
+ }
+ }
+
+ @Test
+ fun onDragUpdated_notifiesDragZoneChanged() {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
+ )
+ dropTargetManager.onDragUpdated(
+ bubbleRightDragZone.bounds.centerX(),
+ bubbleRightDragZone.bounds.centerY()
+ )
+ assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
+ assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone)
+
+ dropTargetManager.onDragUpdated(
+ dismissDragZone.bounds.centerX(),
+ dismissDragZone.bounds.centerY()
+ )
+ assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone)
+ assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone)
+ }
+
+ @Test
+ fun onDragUpdated_withinSameZone_doesNotNotify() {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
+ )
+ dropTargetManager.onDragUpdated(
+ bubbleLeftDragZone.bounds.centerX(),
+ bubbleLeftDragZone.bounds.centerY()
+ )
+ assertThat(dragZoneChangedListener.fromDragZone).isNull()
+ assertThat(dragZoneChangedListener.toDragZone).isNull()
+ }
+
+ @Test
+ fun onDragUpdated_outsideAllZones_doesNotNotify() {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone)
+ )
+ val pointX = 200
+ val pointY = 200
+ assertThat(bubbleLeftDragZone.contains(pointX, pointY)).isFalse()
+ assertThat(bubbleRightDragZone.contains(pointX, pointY)).isFalse()
+ dropTargetManager.onDragUpdated(pointX, pointY)
+ assertThat(dragZoneChangedListener.fromDragZone).isNull()
+ assertThat(dragZoneChangedListener.toDragZone).isNull()
+ }
+
+ @Test
+ fun onDragUpdated_hasOverlappingZones_notifiesFirstDragZoneChanged() {
+ // create a drag zone that spans across the width of all 3 drag zones, but extends below
+ // them
+ val splitDragZone = DragZone.Split.Left(bounds = Rect(0, 0, 300, 200))
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone, splitDragZone)
+ )
+
+ // drag to a point that is within both the bubble right zone and split zone
+ val (pointX, pointY) =
+ Pair(
+ bubbleRightDragZone.bounds.centerX(),
+ bubbleRightDragZone.bounds.centerY()
+ )
+ assertThat(splitDragZone.contains(pointX, pointY)).isTrue()
+ dropTargetManager.onDragUpdated(pointX, pointY)
+ // verify we dragged to the bubble right zone because that has higher priority than split
+ assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
+ assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone)
+
+ dropTargetManager.onDragUpdated(
+ bubbleRightDragZone.bounds.centerX(),
+ 150 // below the bubble and dismiss drag zones but within split
+ )
+ assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone)
+ assertThat(dragZoneChangedListener.toDragZone).isEqualTo(splitDragZone)
+
+ val (dismissPointX, dismissPointY) =
+ Pair(dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY())
+ assertThat(splitDragZone.contains(dismissPointX, dismissPointY)).isTrue()
+ dropTargetManager.onDragUpdated(dismissPointX, dismissPointY)
+ assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(splitDragZone)
+ assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone)
+ }
+
+ @Test
+ fun onDragUpdated_afterDragEnded_doesNotNotify() {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
+ )
+ dropTargetManager.onDragEnded()
+ dropTargetManager.onDragUpdated(
+ bubbleRightDragZone.bounds.centerX(),
+ bubbleRightDragZone.bounds.centerY()
+ )
+ assertThat(dragZoneChangedListener.fromDragZone).isNull()
+ assertThat(dragZoneChangedListener.toDragZone).isNull()
+ }
+
+ private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener {
+ var initialDragZone: DragZone? = null
+ var fromDragZone: DragZone? = null
+ var toDragZone: DragZone? = null
+
+ override fun onInitialDragZoneSet(dragZone: DragZone) {
+ initialDragZone = dragZone
+ }
+ override fun onDragZoneChanged(from: DragZone, to: DragZone) {
+ fromDragZone = from
+ toDragZone = to
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index b9d6a454694d..e5a6a6d258dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -360,7 +360,8 @@ public class SplitTransitionTests extends ShellTestCase {
mStageCoordinator.onRecentsInSplitAnimationFinishing(false /* returnToApp */, commitWCT,
mock(SurfaceControl.Transaction.class));
} else {
- mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT,
+ mStageCoordinator.onRecentsInSplitAnimationFinishing(
+ mStageCoordinator.wctIsReorderingSplitToTop(commitWCT), commitWCT,
mock(SurfaceControl.Transaction.class));
}
assertFalse(mStageCoordinator.isSplitScreenVisible());
@@ -430,7 +431,8 @@ public class SplitTransitionTests extends ShellTestCase {
mStageCoordinator.onRecentsInSplitAnimationFinishing(true /* returnToApp */, restoreWCT,
mock(SurfaceControl.Transaction.class));
} else {
- mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT,
+ mStageCoordinator.onRecentsInSplitAnimationFinishing(
+ mStageCoordinator.wctIsReorderingSplitToTop(restoreWCT), restoreWCT,
mock(SurfaceControl.Transaction.class));
}
assertTrue(mStageCoordinator.isSplitScreenVisible());
diff --git a/libs/input/PointerControllerContext.cpp b/libs/input/PointerControllerContext.cpp
index 747eb8e5ad1b..5406de8602d6 100644
--- a/libs/input/PointerControllerContext.cpp
+++ b/libs/input/PointerControllerContext.cpp
@@ -15,6 +15,7 @@
*/
#include "PointerControllerContext.h"
+
#include "PointerController.h"
namespace {
@@ -184,7 +185,7 @@ void PointerControllerContext::PointerAnimator::handleVsyncEvents() {
DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE];
while ((n = mDisplayEventReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) {
for (size_t i = 0; i < static_cast<size_t>(n); ++i) {
- if (buf[i].header.type == DisplayEventReceiver::DISPLAY_EVENT_VSYNC) {
+ if (buf[i].header.type == DisplayEventType::DISPLAY_EVENT_VSYNC) {
timestamp = buf[i].header.timestamp;
gotVsync = true;
}
diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml
index 96e5892f4d1d..bcc10ddde228 100644
--- a/packages/EasterEgg/AndroidManifest.xml
+++ b/packages/EasterEgg/AndroidManifest.xml
@@ -64,7 +64,7 @@
android:label="@string/u_egg_name"
android:icon="@drawable/android16_patch_adaptive"
android:configChanges="orientation|screenLayout|screenSize|density"
- android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen">
+ android:theme="@style/Theme.Landroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
diff --git a/packages/EasterEgg/res/drawable/ic_planet_large.xml b/packages/EasterEgg/res/drawable/ic_planet_large.xml
new file mode 100644
index 000000000000..7ac7c38153f2
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_large.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,12m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0"
+ android:strokeWidth="2"
+ android:fillColor="#16161D"
+ android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_planet_medium.xml b/packages/EasterEgg/res/drawable/ic_planet_medium.xml
new file mode 100644
index 000000000000..e997b45eb6e5
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_medium.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,12m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0"
+ android:strokeWidth="2"
+ android:fillColor="#16161D"
+ android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_planet_small.xml b/packages/EasterEgg/res/drawable/ic_planet_small.xml
new file mode 100644
index 000000000000..43339573207b
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_small.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,12m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
+ android:strokeWidth="2"
+ android:fillColor="#16161D"
+ android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_planet_tiny.xml b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml
new file mode 100644
index 000000000000..c666765113da
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
+ android:strokeWidth="2"
+ android:fillColor="#16161D"
+ android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft.xml b/packages/EasterEgg/res/drawable/ic_spacecraft.xml
new file mode 100644
index 000000000000..3cef4ab29192
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_spacecraft.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportHeight="24" android:viewportWidth="24"
+ >
+ <group android:translateX="10" android:translateY="12">
+ <path
+ android:strokeColor="#FFFFFF"
+ android:strokeWidth="2"
+ android:pathData="
+M11.853 0
+C11.853 -4.418 8.374 -8 4.083 -8
+L-5.5 -8
+C-6.328 -8 -7 -7.328 -7 -6.5
+C-7 -5.672 -6.328 -5 -5.5 -5
+L-2.917 -5
+C-1.26 -5 0.083 -3.657 0.083 -2
+L0.083 2
+C0.083 3.657 -1.26 5 -2.917 5
+L-5.5 5
+C-6.328 5 -7 5.672 -7 6.5
+C-7 7.328 -6.328 8 -5.5 8
+L4.083 8
+C8.374 8 11.853 4.418 11.853 0
+Z
+ "/>
+ </group>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml
new file mode 100644
index 000000000000..7a0c70379f20
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportHeight="24" android:viewportWidth="24"
+ >
+ <group android:translateX="10" android:translateY="12">
+ <path
+ android:strokeColor="#FFFFFF"
+ android:fillColor="#000000"
+ android:strokeWidth="2"
+ android:pathData="
+M11.853 0
+C11.853 -4.418 8.374 -8 4.083 -8
+L-5.5 -8
+C-6.328 -8 -7 -7.328 -7 -6.5
+C-7 -5.672 -6.328 -5 -5.5 -5
+L-2.917 -5
+C-1.26 -5 0.083 -3.657 0.083 -2
+L0.083 2
+C0.083 3.657 -1.26 5 -2.917 5
+L-5.5 5
+C-6.328 5 -7 5.672 -7 6.5
+C-7 7.328 -6.328 8 -5.5 8
+L4.083 8
+C8.374 8 11.853 4.418 11.853 0
+Z
+ "/>
+ </group>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml
new file mode 100644
index 000000000000..2d4ce106ef38
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/ic_spacecraft"
+ android:fromDegrees="0"
+ android:toDegrees="360"
+ /> \ No newline at end of file
diff --git a/packages/EasterEgg/res/values/themes.xml b/packages/EasterEgg/res/values/themes.xml
index 5b163043a356..3a87e456fc3b 100644
--- a/packages/EasterEgg/res/values/themes.xml
+++ b/packages/EasterEgg/res/values/themes.xml
@@ -1,7 +1,26 @@
-<resources>
+<?xml version="1.0" encoding="utf-8"?><!--
+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.
+-->
+<resources>
<style name="ThemeOverlay.EasterEgg.AppWidgetContainer" parent="">
<item name="appWidgetBackgroundColor">@color/light_blue_600</item>
<item name="appWidgetTextColor">@color/light_blue_50</item>
</style>
-</resources> \ No newline at end of file
+
+ <style name="Theme.Landroid" parent="android:Theme.Material.NoActionBar">
+ <item name="android:windowLightStatusBar">false</item>
+ <item name="android:windowLightNavigationBar">false</item>
+ </style>
+</resources>
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
index fb5954ec9736..8214c540304e 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
@@ -41,14 +41,16 @@ class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity {
val telemetry: String
get() =
- listOf(
- "---- AUTOPILOT ENGAGED ----",
- "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."),
- "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "",
- )
- .joinToString("\n")
-
- private var strategy: String = "NONE"
+ if (enabled)
+ listOf(
+ "---- AUTOPILOT ENGAGED ----",
+ "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."),
+ "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "",
+ )
+ .joinToString("\n")
+ else ""
+
+ var strategy: String = "NONE"
private var debug: String = ""
override fun update(sim: Simulator, dt: Float) {
@@ -119,7 +121,7 @@ class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity {
target.pos +
Vec2.makeWithAngleMag(
target.velocity.angle(),
- min(altitude / 2, target.velocity.mag())
+ min(altitude / 2, target.velocity.mag()),
)
leadingVector = leadingPos - ship.pos
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
index d040fba49fdf..e74863849efa 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
@@ -20,9 +20,19 @@ import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import kotlin.random.Random
@Composable fun Dp.toLocalPx() = with(LocalDensity.current) { this@toLocalPx.toPx() }
@@ -36,6 +46,40 @@ val flickerFadeIn =
animationSpec =
tween(
durationMillis = 1000,
- easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random)
+ easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random),
)
)
+
+fun flickerFadeInAfterDelay(delay: Int = 0) =
+ fadeIn(
+ animationSpec =
+ tween(
+ durationMillis = 1000,
+ delayMillis = delay,
+ easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random),
+ )
+ )
+
+@Composable
+fun ConsoleButton(
+ modifier: Modifier = Modifier,
+ textStyle: TextStyle = TextStyle.Default,
+ color: Color,
+ bgColor: Color,
+ borderColor: Color,
+ text: String,
+ onClick: () -> Unit,
+) {
+ Text(
+ style = textStyle,
+ color = color,
+ modifier =
+ modifier
+ .clickable { onClick() }
+ .background(color = bgColor)
+ .border(width = 1.dp, color = borderColor)
+ .padding(6.dp)
+ .minimumInteractiveComponentSize(),
+ text = text,
+ )
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
index d56e8b9e8d0e..8d4adf638bb3 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
@@ -56,6 +56,8 @@ class DreamUniverse : DreamService() {
}
}
+ private var notifier: UniverseProgressNotifier? = null
+
override fun onAttachedToWindow() {
super.onAttachedToWindow()
@@ -76,8 +78,8 @@ class DreamUniverse : DreamService() {
Random.nextFloat() * PI2f,
Random.nextFloatInRange(
PLANET_ORBIT_RANGE.start,
- PLANET_ORBIT_RANGE.endInclusive
- )
+ PLANET_ORBIT_RANGE.endInclusive,
+ ),
)
}
@@ -94,9 +96,11 @@ class DreamUniverse : DreamService() {
composeView.setContent {
Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
DebugText(DEBUG_TEXT)
- Telemetry(universe)
+ Telemetry(universe, showControls = false)
}
+ notifier = UniverseProgressNotifier(this, universe)
+
composeView.setViewTreeLifecycleOwner(lifecycleOwner)
composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
index 4f77b00b7570..95a60c7a5292 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
@@ -21,6 +21,7 @@ import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
@@ -34,6 +35,7 @@ import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -46,6 +48,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -59,6 +62,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
@@ -74,9 +78,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
import java.lang.Float.max
import java.lang.Float.min
import java.util.Calendar
@@ -85,11 +86,14 @@ import kotlin.math.absoluteValue
import kotlin.math.floor
import kotlin.math.sqrt
import kotlin.random.Random
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
enum class RandomSeedType {
Fixed,
Daily,
- Evergreen
+ Evergreen,
}
const val TEST_UNIVERSE = false
@@ -138,6 +142,10 @@ fun getDessertCode(): String =
else -> Build.VERSION.RELEASE_OR_CODENAME.replace(Regex("[a-z]*"), "")
}
+fun getSystemDesignation(universe: Universe): String {
+ return "${getDessertCode()}-${universe.randomSeed % 100_000}"
+}
+
val DEBUG_TEXT = mutableStateOf("Hello Universe")
const val SHOW_DEBUG_TEXT = false
@@ -150,13 +158,13 @@ fun DebugText(text: MutableState<String>) {
fontWeight = FontWeight.Medium,
fontSize = 9.sp,
color = Color.Yellow,
- text = text.value
+ text = text.value,
)
}
}
@Composable
-fun Telemetry(universe: Universe) {
+fun Telemetry(universe: Universe, showControls: Boolean) {
var topVisible by remember { mutableStateOf(false) }
var bottomVisible by remember { mutableStateOf(false) }
@@ -174,7 +182,6 @@ fun Telemetry(universe: Universe) {
LaunchedEffect("blah") {
delay(1000)
bottomVisible = true
- delay(1000)
topVisible = true
}
@@ -183,13 +190,11 @@ fun Telemetry(universe: Universe) {
// TODO: Narrow the scope of invalidation here to the specific data needed;
// the behavior below mimics the previous implementation of a snapshot ticker value
val recomposeScope = currentRecomposeScope
- Telescope(universe) {
- recomposeScope.invalidate()
- }
+ Telescope(universe) { recomposeScope.invalidate() }
BoxWithConstraints(
modifier =
- Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent),
+ Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent)
) {
val wide = maxWidth > maxHeight
Column(
@@ -197,57 +202,82 @@ fun Telemetry(universe: Universe) {
Modifier.align(if (wide) Alignment.BottomEnd else Alignment.BottomStart)
.fillMaxWidth(if (wide) 0.45f else 1.0f)
) {
- universe.ship.autopilot?.let { autopilot ->
- if (autopilot.enabled) {
+ val autopilotEnabled = universe.ship.autopilot?.enabled == true
+ if (autopilotEnabled) {
+ universe.ship.autopilot?.let { autopilot ->
AnimatedVisibility(
modifier = Modifier,
visible = bottomVisible,
- enter = flickerFadeIn
+ enter = flickerFadeIn,
) {
Text(
style = textStyle,
color = Colors.Autopilot,
modifier = Modifier.align(Left),
- text = autopilot.telemetry
+ text = autopilot.telemetry,
)
}
}
}
- AnimatedVisibility(
- modifier = Modifier,
- visible = bottomVisible,
- enter = flickerFadeIn
- ) {
- Text(
- style = textStyle,
- color = Colors.Console,
- modifier = Modifier.align(Left),
- text =
- with(universe.ship) {
- val closest = universe.closestPlanet()
- val distToClosest = ((closest.pos - pos).mag() - closest.radius).toInt()
- listOfNotNull(
- landing?.let {
- "LND: ${it.planet.name.toUpperCase()}\nJOB: ${it.text}"
- }
- ?: if (distToClosest < 10_000) {
- "ALT: $distToClosest"
- } else null,
- "THR: %.0f%%".format(thrust.mag() * 100f),
- "POS: %s".format(pos.str("%+7.0f")),
- "VEL: %.0f".format(velocity.mag())
- )
- .joinToString("\n")
+ Row(modifier = Modifier.padding(top = 6.dp)) {
+ AnimatedVisibility(
+ modifier = Modifier.weight(1f),
+ visible = bottomVisible,
+ enter = flickerFadeIn,
+ ) {
+ Text(
+ style = textStyle,
+ color = Colors.Console,
+ text =
+ with(universe.ship) {
+ val closest = universe.closestPlanet()
+ val distToClosest =
+ ((closest.pos - pos).mag() - closest.radius).toInt()
+ listOfNotNull(
+ landing?.let {
+ "LND: ${it.planet.name.toUpperCase()}\n" +
+ "JOB: ${it.text.toUpperCase()}"
+ }
+ ?: if (distToClosest < 10_000) {
+ "ALT: $distToClosest"
+ } else null,
+ "THR: %.0f%%".format(thrust.mag() * 100f),
+ "POS: %s".format(pos.str("%+7.0f")),
+ "VEL: %.0f".format(velocity.mag()),
+ )
+ .joinToString("\n")
+ },
+ )
+ }
+
+ if (showControls) {
+ AnimatedVisibility(
+ visible = bottomVisible,
+ enter = flickerFadeInAfterDelay(500),
+ ) {
+ ConsoleButton(
+ textStyle = textStyle,
+ color = Colors.Console,
+ bgColor = if (autopilotEnabled) Colors.Autopilot else Color.Transparent,
+ borderColor = Colors.Console,
+ text = "AUTO",
+ ) {
+ universe.ship.autopilot?.let {
+ it.enabled = !it.enabled
+ DYNAMIC_ZOOM = it.enabled
+ if (!it.enabled) universe.ship.thrust = Vec2.Zero
+ }
}
- )
+ }
+ }
}
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.TopStart),
visible = topVisible,
- enter = flickerFadeIn
+ enter = flickerFadeInAfterDelay(1000),
) {
Text(
style = textStyle,
@@ -263,13 +293,12 @@ fun Telemetry(universe: Universe) {
text =
(with(universe.star) {
listOf(
- " STAR: $name (${getDessertCode()}-" +
- "${universe.randomSeed % 100_000})",
+ " STAR: $name (${getSystemDesignation(universe)})",
" CLASS: ${cls.name}",
"RADIUS: ${radius.toInt()}",
" MASS: %.3g".format(mass),
"BODIES: ${explored.size} / ${universe.planets.size}",
- ""
+ "",
)
} +
explored
@@ -280,11 +309,11 @@ fun Telemetry(universe: Universe) {
" ATMO: ${it.atmosphere.capitalize()}",
" FAUNA: ${it.fauna.capitalize()}",
" FLORA: ${it.flora.capitalize()}",
- ""
+ "",
)
}
.flatten())
- .joinToString("\n")
+ .joinToString("\n"),
// TODO: different colors, highlight latest discovery
)
@@ -293,6 +322,7 @@ fun Telemetry(universe: Universe) {
}
class MainActivity : ComponentActivity() {
+ private var notifier: UniverseProgressNotifier? = null
private var foldState = mutableStateOf<FoldingFeature?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -300,7 +330,7 @@ class MainActivity : ComponentActivity() {
onWindowLayoutInfoChange()
- enableEdgeToEdge()
+ enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.Red.toArgb()))
val universe = Universe(namer = Namer(resources), randomSeed = randomSeed())
@@ -312,12 +342,13 @@ class MainActivity : ComponentActivity() {
com.android.egg.ComponentActivationActivity.lockUnlockComponents(applicationContext)
- // for autopilot testing in the activity
- // val autopilot = Autopilot(universe.ship, universe)
- // universe.ship.autopilot = autopilot
- // universe.add(autopilot)
- // autopilot.enabled = true
- // DYNAMIC_ZOOM = autopilot.enabled
+ // set up the autopilot in case we need it
+ val autopilot = Autopilot(universe.ship, universe)
+ universe.ship.autopilot = autopilot
+ universe.add(autopilot)
+ autopilot.enabled = false
+
+ notifier = UniverseProgressNotifier(this, universe)
setContent {
Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
@@ -329,7 +360,7 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(),
minRadius = minRadius,
maxRadius = maxRadius,
- color = Color.Green
+ color = Color.Green,
) { vec ->
(universe.follow as? Spacecraft)?.let { ship ->
if (vec == Vec2.Zero) {
@@ -346,13 +377,13 @@ class MainActivity : ComponentActivity() {
ship.thrust =
Vec2.makeWithAngleMag(
a,
- lexp(minRadius, maxRadius, m).coerceIn(0f, 1f)
+ lexp(minRadius, maxRadius, m).coerceIn(0f, 1f),
)
}
}
}
}
- Telemetry(universe)
+ Telemetry(universe, true)
}
}
@@ -382,7 +413,7 @@ fun MainActivityPreview() {
Spaaaace(modifier = Modifier.fillMaxSize(), universe)
DebugText(DEBUG_TEXT)
- Telemetry(universe)
+ Telemetry(universe, true)
}
@Composable
@@ -391,7 +422,7 @@ fun FlightStick(
minRadius: Float = 0f,
maxRadius: Float = 1000f,
color: Color = Color.Green,
- onStickChanged: (vector: Vec2) -> Unit
+ onStickChanged: (vector: Vec2) -> Unit,
) {
val origin = remember { mutableStateOf(Vec2.Zero) }
val target = remember { mutableStateOf(Vec2.Zero) }
@@ -444,14 +475,14 @@ fun FlightStick(
PathEffect.dashPathEffect(
floatArrayOf(this.density * 1f, this.density * 2f)
)
- else null
- )
+ else null,
+ ),
)
drawLine(
color = color,
start = origin.value,
end = origin.value + Vec2.makeWithAngleMag(a, mag),
- strokeWidth = 2f
+ strokeWidth = 2f,
)
}
}
@@ -462,15 +493,13 @@ fun FlightStick(
fun Spaaaace(
modifier: Modifier,
u: Universe,
- foldState: MutableState<FoldingFeature?> = mutableStateOf(null)
+ foldState: MutableState<FoldingFeature?> = mutableStateOf(null),
) {
LaunchedEffect(u) {
- while (true) withInfiniteAnimationFrameNanos { frameTimeNanos ->
- u.step(frameTimeNanos)
- }
+ while (true) withInfiniteAnimationFrameNanos { frameTimeNanos -> u.step(frameTimeNanos) }
}
- var cameraZoom by remember { mutableStateOf(1f) }
+ var cameraZoom by remember { mutableFloatStateOf(DEFAULT_CAMERA_ZOOM) }
var cameraOffset by remember { mutableStateOf(Offset.Zero) }
val transformableState =
@@ -501,15 +530,16 @@ fun Spaaaace(
val closest = u.closestPlanet()
val distToNearestSurf = max(0f, (u.ship.pos - closest.pos).mag() - closest.radius * 1.2f)
// val normalizedDist = clamp(distToNearestSurf, 50f, 50_000f) / 50_000f
- if (DYNAMIC_ZOOM) {
- cameraZoom =
- expSmooth(
- cameraZoom,
- clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM),
- dt = u.dt,
- speed = 1.5f
- )
- } else if (!TOUCH_CAMERA_ZOOM) cameraZoom = DEFAULT_CAMERA_ZOOM
+ val targetZoom =
+ if (DYNAMIC_ZOOM) {
+ clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM)
+ } else {
+ DEFAULT_CAMERA_ZOOM
+ }
+ if (!TOUCH_CAMERA_ZOOM) {
+ cameraZoom = expSmooth(cameraZoom, targetZoom, dt = u.dt, speed = 1.5f)
+ }
+
if (!TOUCH_CAMERA_PAN) cameraOffset = (u.follow?.pos ?: Vec2.Zero) * -1f
// cameraZoom: metersToPixels
@@ -521,9 +551,9 @@ fun Spaaaace(
-cameraOffset -
Offset(
visibleSpaceSizeMeters.width * centerFracX,
- visibleSpaceSizeMeters.height * centerFracY
+ visibleSpaceSizeMeters.height * centerFracY,
),
- visibleSpaceSizeMeters
+ visibleSpaceSizeMeters,
)
var gridStep = 1000f
@@ -537,14 +567,14 @@ fun Spaaaace(
"fps: ${"%3.0f".format(1f / u.dt)} " +
"dt: ${u.dt}\n" +
((u.follow as? Spacecraft)?.let {
- "ship: p=%s v=%7.2f a=%6.3f t=%s\n".format(
- it.pos.str("%+7.1f"),
- it.velocity.mag(),
- it.angle,
- it.thrust.str("%+5.2f")
- )
- }
- ?: "") +
+ "ship: p=%s v=%7.2f a=%6.3f t=%s\n"
+ .format(
+ it.pos.str("%+7.1f"),
+ it.velocity.mag(),
+ it.angle,
+ it.thrust.str("%+5.2f"),
+ )
+ } ?: "") +
"star: '${u.star.name}' designation=UDC-${u.randomSeed % 100_000} " +
"class=${u.star.cls.name} r=${u.star.radius.toInt()} m=${u.star.mass}\n" +
"planets: ${u.planets.size}\n" +
@@ -574,7 +604,7 @@ fun Spaaaace(
translate(
-visibleSpaceRectMeters.center.x + size.width * 0.5f,
- -visibleSpaceRectMeters.center.y + size.height * 0.5f
+ -visibleSpaceRectMeters.center.y + size.height * 0.5f,
) {
// debug outer frame
// drawRect(
@@ -590,7 +620,7 @@ fun Spaaaace(
color = Colors.Eigengrau2,
start = Offset(x, visibleSpaceRectMeters.top),
end = Offset(x, visibleSpaceRectMeters.bottom),
- strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom
+ strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom,
)
x += gridStep
}
@@ -601,7 +631,7 @@ fun Spaaaace(
color = Colors.Eigengrau2,
start = Offset(visibleSpaceRectMeters.left, y),
end = Offset(visibleSpaceRectMeters.right, y),
- strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom
+ strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom,
)
y += gridStep
}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
index 73318077f47a..babf1328c7d4 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
@@ -16,8 +16,8 @@
package com.android.egg.landroid
-import android.content.res.Resources
import com.android.egg.R
+import android.content.res.Resources
import kotlin.random.Random
const val SUFFIX_PROB = 0.75f
@@ -58,7 +58,7 @@ class Namer(resources: Resources) {
1f to "*",
1f to "^",
1f to "#",
- 0.1f to "(^*!%@##!!"
+ 0.1f to "(^*!%@##!!",
)
private var activities = Bag(resources.getStringArray(R.array.activities))
@@ -101,26 +101,26 @@ class Namer(resources: Resources) {
fun floraPlural(rng: Random): String {
return floraGenericPlurals.pull(rng)
}
+
fun faunaPlural(rng: Random): String {
return faunaGenericPlurals.pull(rng)
}
+
fun atmoPlural(rng: Random): String {
return atmoGenericPlurals.pull(rng)
}
val TEMPLATE_REGEX = Regex("""\{(flora|fauna|planet|atmo)\}""")
+
fun describeActivity(rng: Random, target: Planet?): String {
- return activities
- .pull(rng)
- .replace(TEMPLATE_REGEX) {
- when (it.groupValues[1]) {
- "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng)
- "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng)
- "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng)
- "planet" -> (target?.description ?: "SOME BODY") // once told me
- else -> "unknown template tag: ${it.groupValues[0]}"
- }
+ return activities.pull(rng).replace(TEMPLATE_REGEX) {
+ when (it.groupValues[1]) {
+ "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng)
+ "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng)
+ "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng)
+ "planet" -> (target?.description ?: "SOME BODY") // once told me
+ else -> "unknown template tag: ${it.groupValues[0]}"
}
- .toUpperCase()
+ }
}
}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt
new file mode 100644
index 000000000000..bb3a04df6f36
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt
@@ -0,0 +1,187 @@
+/*
+ * 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.egg.landroid
+
+import com.android.egg.R
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.util.lerp
+import kotlinx.coroutines.DisposableHandle
+
+const val CHANNEL_ID = "progress"
+const val CHANNEL_NAME = "Spacecraft progress"
+const val UPDATE_FREQUENCY_SEC = 1f
+
+fun lerpRange(range: ClosedFloatingPointRange<Float>, x: Float): Float =
+ lerp(range.start, range.endInclusive, x)
+
+class UniverseProgressNotifier(val context: Context, val universe: Universe) {
+ private val notificationId = universe.randomSeed.toInt()
+ private val chan =
+ NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
+ .apply { lockscreenVisibility = Notification.VISIBILITY_PUBLIC }
+ private val noman =
+ context.getSystemService(NotificationManager::class.java)?.apply {
+ createNotificationChannel(chan)
+ }
+
+ private val registration: DisposableHandle =
+ universe.addSimulationStepListener(this::onSimulationStep)
+
+ private val spacecraftIcon = Icon.createWithResource(context, R.drawable.ic_spacecraft_filled)
+ private val planetIcons =
+ listOf(
+ (lerpRange(PLANET_RADIUS_RANGE, 0.75f)) to
+ Icon.createWithResource(context, R.drawable.ic_planet_large),
+ (lerpRange(PLANET_RADIUS_RANGE, 0.5f)) to
+ Icon.createWithResource(context, R.drawable.ic_planet_medium),
+ (lerpRange(PLANET_RADIUS_RANGE, 0.25f)) to
+ Icon.createWithResource(context, R.drawable.ic_planet_small),
+ (PLANET_RADIUS_RANGE.start to
+ Icon.createWithResource(context, R.drawable.ic_planet_tiny)),
+ )
+
+ private fun getPlanetIcon(planet: Planet): Icon {
+ for ((radius, icon) in planetIcons) {
+ if (planet.radius > radius) return icon
+ }
+ return planetIcons.last().second
+ }
+
+ private val progress = Notification.ProgressStyle().setProgressTrackerIcon(spacecraftIcon)
+
+ private val builder =
+ Notification.Builder(context, CHANNEL_ID)
+ .setContentIntent(
+ PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
+ },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ )
+ )
+ .setPriority(Notification.PRIORITY_DEFAULT)
+ .setColorized(true)
+ .setOngoing(true)
+ .setColor(Colors.Eigengrau2.toArgb())
+ .setStyle(progress)
+
+ private var lastUpdate = 0f
+ private var initialDistToTarget = 0
+
+ private fun onSimulationStep() {
+ if (universe.now - lastUpdate >= UPDATE_FREQUENCY_SEC) {
+ lastUpdate = universe.now
+ // android.util.Log.v("Landroid", "posting notification at time ${universe.now}")
+
+ var distToTarget = 0
+ val autopilot = universe.ship.autopilot
+ val autopilotEnabled: Boolean = autopilot?.enabled == true
+ val target = autopilot?.target
+ val landing = universe.ship.landing
+ val speed = universe.ship.velocity.mag()
+
+ if (landing != null) {
+ // landed
+ builder.setContentTitle("landed: ${landing.planet.name}")
+ builder.setContentText("currently: ${landing.text}")
+ builder.setShortCriticalText("landed")
+
+ progress.setProgress(progress.progressMax)
+ progress.setProgressIndeterminate(false)
+
+ builder.setStyle(progress)
+ } else if (autopilotEnabled) {
+ if (target != null) {
+ // autopilot en route
+ distToTarget = ((target.pos - universe.ship.pos).mag() - target.radius).toInt()
+ if (initialDistToTarget == 0) {
+ // we have a new target!
+ initialDistToTarget = distToTarget
+ progress.progressEndIcon = getPlanetIcon(target)
+ }
+
+ val eta = if (speed > 0) "%1.0fs".format(distToTarget / speed) else "???"
+ builder.setContentTitle("headed to: ${target.name}")
+ builder.setContentText(
+ "autopilot is ${autopilot.strategy.toLowerCase()}" +
+ "\ndist: ${distToTarget}u // eta: $eta"
+ )
+ // fun fact: ProgressStyle was originally EnRouteStyle
+ builder.setShortCriticalText("en route")
+
+ progress
+ .setProgressSegments(
+ listOf(
+ Notification.ProgressStyle.Segment(initialDistToTarget)
+ .setColor(Colors.Track.toArgb())
+ )
+ )
+ .setProgress(initialDistToTarget - distToTarget)
+ .setProgressIndeterminate(false)
+ builder.setStyle(progress)
+ } else {
+ // no target
+ if (initialDistToTarget != 0) {
+ // just launched
+ initialDistToTarget = 0
+ progress.progressStartIcon = progress.progressEndIcon
+ progress.progressEndIcon = null
+ }
+
+ builder.setContentTitle("in space")
+ builder.setContentText("selecting new target...")
+ builder.setShortCriticalText("launched")
+
+ progress.setProgressIndeterminate(true)
+
+ builder.setStyle(progress)
+ }
+ } else {
+ // under user control
+
+ initialDistToTarget = 0
+
+ builder.setContentTitle("in space")
+ builder.setContentText("under manual control")
+ builder.setShortCriticalText("adrift")
+
+ builder.setStyle(null)
+ }
+
+ builder
+ .setSubText(getSystemDesignation(universe))
+ .setSmallIcon(R.drawable.ic_spacecraft_rotated)
+
+ val notification = builder.build()
+
+ // one of the silliest things about Android is that icon levels go from 0 to 10000
+ notification.iconLevel = (((universe.ship.angle + PI2f) / PI2f) * 10_000f).toInt()
+
+ noman?.notify(notificationId, notification)
+ }
+ }
+}
diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
index e173c5e996df..0f6a2a082e0c 100644
--- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
+++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
@@ -118,6 +118,7 @@ public class SettingsSpinnerPreference extends Preference
spinner.setAdapter(mAdapter);
spinner.setSelection(mPosition);
spinner.setOnItemSelectedListener(mOnSelectedListener);
+ spinner.setLongClickable(false);
if (mShouldPerformClick) {
mShouldPerformClick = false;
// To show dropdown view.
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 1a6365433be5..19806e7cdf64 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -207,6 +207,8 @@ filegroup {
"tests/src/**/systemui/statusbar/notification/row/NotificationConversationInfoTest.java",
"tests/src/**/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt",
"tests/src/**/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt",
+ "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java",
+ "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierDisabledTest.java",
"tests/src/**/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java",
"tests/src/**/systemui/statusbar/phone/CentralSurfacesImplTest.java",
"tests/src/**/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java",
@@ -553,6 +555,11 @@ android_library {
},
}
+platform_compat_config {
+ name: "SystemUI-core-compat-config",
+ src: ":SystemUI-core",
+}
+
filegroup {
name: "AAA-src",
srcs: ["tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java"],
@@ -755,6 +762,7 @@ android_library {
"kosmos",
"testables",
"androidx.test.rules",
+ "platform-compat-test-rules",
],
libs: [
"android.test.runner.stubs.system",
@@ -889,6 +897,7 @@ android_robolectric_test {
static_libs: [
"RoboTestLibraries",
"androidx.compose.runtime_runtime",
+ "platform-compat-test-rules",
],
libs: [
"android.test.runner.impl",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 5b989cb6abc4..028a0c6e978b 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1869,20 +1869,6 @@ flag {
bug: "385194612"
}
-flag{
- name: "gsf_bouncer"
- namespace: "systemui"
- description: "Applies GSF font styles to Bouncer surfaces."
- bug: "379364381"
-}
-
-flag {
- name: "gsf_quick_settings"
- namespace: "systemui"
- description: "Applies GSF font styles to Quick Settings surfaces."
- bug: "379364381"
-}
-
flag {
name: "spatial_model_launcher_pushback"
namespace: "systemui"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 910328dfa140..9c57efc24a22 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -1705,38 +1705,15 @@ private fun Umo(
contentScope: ContentScope?,
modifier: Modifier = Modifier,
) {
- val showNextActionLabel = stringResource(R.string.accessibility_action_label_umo_show_next)
- val showPreviousActionLabel =
- stringResource(R.string.accessibility_action_label_umo_show_previous)
-
- Box(
- modifier =
- modifier.thenIf(!viewModel.isEditMode) {
- Modifier.semantics {
- customActions =
- listOf(
- CustomAccessibilityAction(showNextActionLabel) {
- viewModel.onShowNextMedia()
- true
- },
- CustomAccessibilityAction(showPreviousActionLabel) {
- viewModel.onShowPreviousMedia()
- true
- },
- )
- }
- }
- ) {
- if (SceneContainerFlag.isEnabled && contentScope != null) {
- contentScope.MediaCarousel(
- modifier = modifier.fillMaxSize(),
- isVisible = true,
- mediaHost = viewModel.mediaHost,
- carouselController = viewModel.mediaCarouselController,
- )
- } else {
- UmoLegacy(viewModel, modifier)
- }
+ if (SceneContainerFlag.isEnabled && contentScope != null) {
+ contentScope.MediaCarousel(
+ modifier = modifier.fillMaxSize(),
+ isVisible = true,
+ mediaHost = viewModel.mediaHost,
+ carouselController = viewModel.mediaCarouselController,
+ )
+ } else {
+ UmoLegacy(viewModel, modifier)
}
}
@@ -1747,7 +1724,7 @@ private fun UmoLegacy(viewModel: BaseCommunalViewModel, modifier: Modifier = Mod
modifier
.clip(
shape =
- RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+ RoundedCornerShape(dimensionResource(R.dimen.notification_corner_radius))
)
.background(MaterialTheme.colorScheme.primary)
.pointerInput(Unit) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
index db1358a5a28a..64f3cb13662a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
@@ -86,7 +86,7 @@ constructor(
OverlayShade(
panelElement = NotificationsShade.Elements.Panel,
- panelAlignment = Alignment.TopStart,
+ alignmentOnWideScreens = Alignment.TopStart,
modifier = modifier,
onScrimClicked = viewModel::onScrimClicked,
header = {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
index cc58b8e13744..afdb3cbba60e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
@@ -128,7 +128,7 @@ constructor(
)
OverlayShade(
panelElement = QuickSettingsShade.Elements.Panel,
- panelAlignment = Alignment.TopEnd,
+ alignmentOnWideScreens = Alignment.TopEnd,
onScrimClicked = contentViewModel::onScrimClicked,
header = {
OverlayShadeHeader(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 619b4280d954..aa0d474ba41c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -204,7 +204,7 @@ fun SceneContainer(
SceneTransitionLayout(
state = state,
modifier = modifier.fillMaxSize(),
- swipeSourceDetector = viewModel.edgeDetector,
+ swipeSourceDetector = viewModel.swipeSourceDetector,
) {
sceneByKey.forEach { (sceneKey, scene) ->
scene(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index 5dcec5b8836d..cdb1e2e53b09 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -59,7 +59,7 @@ import com.android.systemui.res.R
@Composable
fun ContentScope.OverlayShade(
panelElement: ElementKey,
- panelAlignment: Alignment,
+ alignmentOnWideScreens: Alignment,
onScrimClicked: () -> Unit,
modifier: Modifier = Modifier,
header: @Composable () -> Unit,
@@ -71,7 +71,7 @@ fun ContentScope.OverlayShade(
Box(
modifier = Modifier.fillMaxSize().panelContainerPadding(isFullWidth),
- contentAlignment = panelAlignment,
+ contentAlignment = if (isFullWidth) Alignment.TopCenter else alignmentOnWideScreens,
) {
Panel(
modifier =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 433894b58350..85155157eda2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -78,7 +78,6 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
import com.android.systemui.media.controls.ui.controller.mediaCarouselController
-import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
@@ -121,7 +120,6 @@ import platform.test.runner.parameterized.Parameters
@RunWith(ParameterizedAndroidJunit4::class)
class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
- @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
@Mock private lateinit var metricsLogger: CommunalMetricsLogger
private val kosmos = testKosmos()
@@ -163,8 +161,6 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0)
whenever(mediaHost.visible).thenReturn(true)
- whenever(kosmos.mediaCarouselController.mediaCarouselScrollHandler)
- .thenReturn(mediaCarouselScrollHandler)
kosmos.powerInteractor.setAwakeForTest()
@@ -907,20 +903,6 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- fun onShowPreviousMedia_scrollHandler_isCalled() =
- testScope.runTest {
- underTest.onShowPreviousMedia()
- verify(mediaCarouselScrollHandler).scrollByStep(-1)
- }
-
- @Test
- fun onShowNextMedia_scrollHandler_isCalled() =
- testScope.runTest {
- underTest.onShowNextMedia()
- verify(mediaCarouselScrollHandler).scrollByStep(1)
- }
-
- @Test
@EnableFlags(FLAG_BOUNCER_UI_REVAMP)
fun uiIsBlurred_whenPrimaryBouncerIsShowing() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
index b66e2fe13e8a..47ca4b14a26f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
@@ -41,7 +41,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.disableDualShade
import com.android.systemui.shade.domain.interactor.enableDualShade
@@ -275,20 +275,20 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
assertThat(downDestination?.transitionKey).isNull()
}
- val downFromTopRightDestination =
+ val downFromEndHalfDestination =
userActions?.get(
Swipe.Down(
- fromSource = SceneContainerEdge.TopRight,
+ fromSource = SceneContainerArea.EndHalf,
pointerCount = if (downWithTwoPointers) 2 else 1,
)
)
when {
- !isShadeTouchable -> assertThat(downFromTopRightDestination).isNull()
- downWithTwoPointers -> assertThat(downFromTopRightDestination).isNull()
+ !isShadeTouchable -> assertThat(downFromEndHalfDestination).isNull()
+ downWithTwoPointers -> assertThat(downFromEndHalfDestination).isNull()
else -> {
- assertThat(downFromTopRightDestination)
+ assertThat(downFromEndHalfDestination)
.isEqualTo(ShowOverlay(Overlays.QuickSettingsShade))
- assertThat(downFromTopRightDestination?.transitionKey).isNull()
+ assertThat(downFromEndHalfDestination?.transitionKey).isNull()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
index 46940297e673..d073cf1ac9db 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
@@ -16,11 +16,8 @@
package com.android.systemui.media.controls.ui.view
-import android.content.res.Resources
import android.testing.TestableLooper
import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -28,19 +25,16 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.PageIndicator
import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.Mock
import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -48,7 +42,6 @@ import org.mockito.kotlin.whenever
class MediaCarouselScrollHandlerTest : SysuiTestCase() {
private val carouselWidth = 1038
- private val settingsButtonWidth = 200
private val motionEventUp = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0)
@Mock lateinit var mediaCarousel: MediaScrollView
@@ -60,9 +53,6 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() {
@Mock lateinit var falsingManager: FalsingManager
@Mock lateinit var logSmartspaceImpression: (Boolean) -> Unit
@Mock lateinit var logger: MediaUiEventLogger
- @Mock lateinit var contentContainer: ViewGroup
- @Mock lateinit var settingsButton: View
- @Mock lateinit var resources: Resources
lateinit var executor: FakeExecutor
private val clock = FakeSystemClock()
@@ -73,7 +63,6 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() {
fun setup() {
MockitoAnnotations.initMocks(this)
executor = FakeExecutor(clock)
- whenever(mediaCarousel.contentContainer).thenReturn(contentContainer)
mediaCarouselScrollHandler =
MediaCarouselScrollHandler(
mediaCarousel,
@@ -85,9 +74,10 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() {
closeGuts,
falsingManager,
logSmartspaceImpression,
- logger,
+ logger
)
mediaCarouselScrollHandler.playerWidthPlusPadding = carouselWidth
+
whenever(mediaCarousel.touchListener).thenReturn(mediaCarouselScrollHandler.touchListener)
}
@@ -138,107 +128,4 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() {
verify(mediaCarousel).smoothScrollTo(eq(0), anyInt())
}
-
- @Test
- fun testCarouselScrollByStep_scrollRight() {
- setupMediaContainer(visibleIndex = 0)
-
- mediaCarouselScrollHandler.scrollByStep(1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel).smoothScrollTo(eq(carouselWidth), anyInt())
- }
-
- @Test
- fun testCarouselScrollByStep_scrollLeft() {
- setupMediaContainer(visibleIndex = 1)
-
- mediaCarouselScrollHandler.scrollByStep(-1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel).smoothScrollTo(eq(0), anyInt())
- }
-
- @Test
- fun testCarouselScrollByStep_scrollRight_alreadyAtEnd() {
- setupMediaContainer(visibleIndex = 1)
-
- mediaCarouselScrollHandler.scrollByStep(1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
- verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat())
- }
-
- @Test
- fun testCarouselScrollByStep_scrollLeft_alreadyAtStart() {
- setupMediaContainer(visibleIndex = 0)
-
- mediaCarouselScrollHandler.scrollByStep(-1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
- verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat())
- }
-
- @Test
- fun testCarouselScrollByStep_scrollLeft_alreadyAtStart_isRTL() {
- setupMediaContainer(visibleIndex = 0)
- whenever(mediaCarousel.isLayoutRtl).thenReturn(true)
-
- mediaCarouselScrollHandler.scrollByStep(-1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
- verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat())
- }
-
- @Test
- fun testCarouselScrollByStep_scrollRight_alreadyAtEnd_isRTL() {
- setupMediaContainer(visibleIndex = 1)
- whenever(mediaCarousel.isLayoutRtl).thenReturn(true)
-
- mediaCarouselScrollHandler.scrollByStep(1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
- verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat())
- }
-
- @Test
- fun testScrollByStep_noScroll_notDismissible() {
- setupMediaContainer(visibleIndex = 1, showsSettingsButton = false)
-
- mediaCarouselScrollHandler.scrollByStep(1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
- verify(mediaCarousel, never()).animationTargetX = anyFloat()
- }
-
- private fun setupMediaContainer(visibleIndex: Int, showsSettingsButton: Boolean = true) {
- whenever(contentContainer.childCount).thenReturn(2)
- val child1: View = mock()
- val child2: View = mock()
- whenever(child1.left).thenReturn(0)
- whenever(child2.left).thenReturn(carouselWidth)
- whenever(contentContainer.getChildAt(0)).thenReturn(child1)
- whenever(contentContainer.getChildAt(1)).thenReturn(child2)
-
- whenever(settingsButton.width).thenReturn(settingsButtonWidth)
- whenever(settingsButton.context).thenReturn(context)
- whenever(settingsButton.resources).thenReturn(resources)
- whenever(settingsButton.resources.getDimensionPixelSize(anyInt())).thenReturn(20)
- mediaCarouselScrollHandler.onSettingsButtonUpdated(settingsButton)
-
- mediaCarouselScrollHandler.visibleMediaIndex = visibleIndex
- mediaCarouselScrollHandler.showsSettingsButton = showsSettingsButton
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt
index 52b9e47e6d3d..52a0a5445002 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt
@@ -30,7 +30,7 @@ import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayActionsViewModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -71,13 +71,13 @@ class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() {
}
@Test
- fun downFromTopRight_switchesToQuickSettingsShade() =
+ fun downFromTopEnd_switchesToQuickSettingsShade() =
testScope.runTest {
val actions by collectLastValue(underTest.actions)
underTest.activateIn(this)
val action =
- (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopRight)) as? ShowOverlay)
+ (actions?.get(Swipe.Down(fromSource = SceneContainerArea.EndHalf)) as? ShowOverlay)
assertThat(action?.overlay).isEqualTo(Overlays.QuickSettingsShade)
val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some
assertThat(overlaysToHide).isNotNull()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt
index df2dd99c779e..b98059a1fe90 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt
@@ -31,7 +31,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel
import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -84,13 +84,14 @@ class QuickSettingsShadeOverlayActionsViewModelTest : SysuiTestCase() {
}
@Test
- fun downFromTopLeft_switchesToNotificationsShade() =
+ fun downFromTopStart_switchesToNotificationsShade() =
testScope.runTest {
val actions by collectLastValue(underTest.actions)
underTest.activateIn(this)
val action =
- (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopLeft)) as? ShowOverlay)
+ (actions?.get(Swipe.Down(fromSource = SceneContainerArea.StartHalf))
+ as? ShowOverlay)
assertThat(action?.overlay).isEqualTo(Overlays.NotificationsShade)
val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some
assertThat(overlaysToHide).isNotNull()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index a0d86f27b9b8..80c7026b0cea 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -28,6 +28,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteract
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runCurrent
@@ -60,6 +61,10 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -713,4 +718,43 @@ class SceneInteractorTest : SysuiTestCase() {
assertThat(currentScene).isEqualTo(originalScene)
assertThat(currentOverlays).isEmpty()
}
+
+ @Test
+ fun changeScene_notifiesAboutToChangeListener() =
+ kosmos.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ // Unlock so transitioning to the Gone scene becomes possible.
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ underTest.changeScene(toScene = Scenes.Gone, loggingReason = "")
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+
+ val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>()
+ underTest.registerSceneStateProcessor(processor)
+
+ underTest.changeScene(
+ toScene = Scenes.Lockscreen,
+ sceneState = KeyguardState.AOD,
+ loggingReason = "",
+ )
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+ verify(processor).onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD)
+ }
+
+ @Test
+ fun changeScene_noOp_whenFromAndToAreTheSame() =
+ kosmos.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>()
+ underTest.registerSceneStateProcessor(processor)
+
+ underTest.changeScene(toScene = checkNotNull(currentScene), loggingReason = "")
+
+ verify(processor, never()).onSceneAboutToChange(any(), any())
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt
new file mode 100644
index 000000000000..a09e5cd9de9b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt
@@ -0,0 +1,196 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndHalf
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.BottomEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftHalf
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightHalf
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartHalf
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SceneContainerSwipeDetectorTest : SysuiTestCase() {
+
+ private val edgeSize = 40
+ private val screenWidth = 800
+ private val screenHeight = 600
+
+ private val underTest = SceneContainerSwipeDetector(edgeSize = edgeSize.dp)
+
+ @Test
+ fun source_noEdge_detectsLeftHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = screenWidth / 2 - 1, y = screenHeight / 2)
+ assertThat(detectedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnTopLeft_detectsLeftHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnTopLeft_detectsLeftEdge() {
+ val detectedEdge = swipeHorizontallyFrom(x = 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(LeftEdge)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnTopRight_detectsRightHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = screenWidth - 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(RightHalf)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnTopRight_detectsRightEdge() {
+ val detectedEdge = swipeHorizontallyFrom(x = screenWidth - 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(RightEdge)
+ }
+
+ @Test
+ fun source_swipeVerticallyToLeftOfSplit_detectsLeftHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) - 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun source_swipeVerticallyToRightOfSplit_detectsRightHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) + 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(RightHalf)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnBottom_detectsBottomEdge() {
+ val detectedEdge =
+ swipeVerticallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize / 2))
+ assertThat(detectedEdge).isEqualTo(BottomEdge)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnBottom_detectsLeftHalf() {
+ val detectedEdge =
+ swipeHorizontallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize - 1))
+ assertThat(detectedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnLeft_detectsLeftEdge() {
+ val detectedEdge = swipeHorizontallyFrom(x = edgeSize - 1, y = screenHeight / 2)
+ assertThat(detectedEdge).isEqualTo(LeftEdge)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnLeft_detectsLeftHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = edgeSize - 1, y = screenHeight / 2)
+ assertThat(detectedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnRight_detectsRightEdge() {
+ val detectedEdge =
+ swipeHorizontallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2)
+ assertThat(detectedEdge).isEqualTo(RightEdge)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnRight_detectsRightHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2)
+ assertThat(detectedEdge).isEqualTo(RightHalf)
+ }
+
+ @Test
+ fun resolve_startEdgeInLtr_resolvesLeftEdge() {
+ val resolvedEdge = StartEdge.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(LeftEdge)
+ }
+
+ @Test
+ fun resolve_startEdgeInRtl_resolvesRightEdge() {
+ val resolvedEdge = StartEdge.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(RightEdge)
+ }
+
+ @Test
+ fun resolve_endEdgeInLtr_resolvesRightEdge() {
+ val resolvedEdge = EndEdge.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(RightEdge)
+ }
+
+ @Test
+ fun resolve_endEdgeInRtl_resolvesLeftEdge() {
+ val resolvedEdge = EndEdge.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(LeftEdge)
+ }
+
+ @Test
+ fun resolve_startHalfInLtr_resolvesLeftHalf() {
+ val resolvedEdge = StartHalf.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun resolve_startHalfInRtl_resolvesRightHalf() {
+ val resolvedEdge = StartHalf.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(RightHalf)
+ }
+
+ @Test
+ fun resolve_endHalfInLtr_resolvesRightHalf() {
+ val resolvedEdge = EndHalf.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(RightHalf)
+ }
+
+ @Test
+ fun resolve_endHalfInRtl_resolvesLeftHalf() {
+ val resolvedEdge = EndHalf.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(LeftHalf)
+ }
+
+ private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? {
+ return swipeFrom(x, y, Orientation.Vertical)
+ }
+
+ private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? {
+ return swipeFrom(x, y, Orientation.Horizontal)
+ }
+
+ private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerArea.Resolved? {
+ return underTest.source(
+ layoutSize = IntSize(width = screenWidth, height = screenHeight),
+ position = IntOffset(x, y),
+ density = Density(1f),
+ orientation = orientation,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 30d9f73d7441..adaebbd27986 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -48,6 +48,7 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -55,6 +56,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
@@ -324,7 +326,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {
kosmos.enableSingleShade()
assertThat(shadeMode).isEqualTo(ShadeMode.Single)
- assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+ assertThat(underTest.swipeSourceDetector).isEqualTo(DefaultEdgeDetector)
}
@Test
@@ -334,26 +336,28 @@ class SceneContainerViewModelTest : SysuiTestCase() {
kosmos.enableSplitShade()
assertThat(shadeMode).isEqualTo(ShadeMode.Split)
- assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+ assertThat(underTest.swipeSourceDetector).isEqualTo(DefaultEdgeDetector)
}
@Test
- fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() =
+ fun edgeDetector_dualShade_narrowScreen_usesSceneContainerSwipeDetector() =
testScope.runTest {
val shadeMode by collectLastValue(kosmos.shadeMode)
kosmos.enableDualShade(wideLayout = false)
assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
- assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+ assertThat(underTest.swipeSourceDetector)
+ .isInstanceOf(SceneContainerSwipeDetector::class.java)
}
@Test
- fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() =
+ fun edgeDetector_dualShade_wideScreen_usesSceneContainerSwipeDetector() =
testScope.runTest {
val shadeMode by collectLastValue(kosmos.shadeMode)
kosmos.enableDualShade(wideLayout = true)
assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
- assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+ assertThat(underTest.swipeSourceDetector)
+ .isInstanceOf(SceneContainerSwipeDetector::class.java)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
deleted file mode 100644
index 3d76d280b2cc..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
+++ /dev/null
@@ -1,274 +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.scene.ui.viewmodel
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertFailsWith
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class SplitEdgeDetectorTest : SysuiTestCase() {
-
- private val edgeSize = 40
- private val screenWidth = 800
- private val screenHeight = 600
-
- private var edgeSplitFraction = 0.7f
-
- private val underTest =
- SplitEdgeDetector(
- topEdgeSplitFraction = { edgeSplitFraction },
- edgeSize = edgeSize.dp,
- )
-
- @Test
- fun source_noEdge_detectsNothing() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = screenWidth / 2,
- y = screenHeight / 2,
- )
- assertThat(detectedEdge).isNull()
- }
-
- @Test
- fun source_swipeVerticallyOnTopLeft_detectsTopLeft() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(TopLeft)
- }
-
- @Test
- fun source_swipeHorizontallyOnTopLeft_detectsLeft() {
- val detectedEdge =
- swipeHorizontallyFrom(
- x = 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(Left)
- }
-
- @Test
- fun source_swipeVerticallyOnTopRight_detectsTopRight() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = screenWidth - 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(TopRight)
- }
-
- @Test
- fun source_swipeHorizontallyOnTopRight_detectsRight() {
- val detectedEdge =
- swipeHorizontallyFrom(
- x = screenWidth - 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(Right)
- }
-
- @Test
- fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = (screenWidth * edgeSplitFraction).toInt() - 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(TopLeft)
- }
-
- @Test
- fun source_swipeVerticallyToRightOfSplit_detectsTopRight() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = (screenWidth * edgeSplitFraction).toInt() + 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(TopRight)
- }
-
- @Test
- fun source_edgeSplitFractionUpdatesDynamically() {
- val middleX = (screenWidth * 0.5f).toInt()
- val topY = 0
-
- // Split closer to the right; middle of screen is considered "left".
- edgeSplitFraction = 0.6f
- assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft)
-
- // Split closer to the left; middle of screen is considered "right".
- edgeSplitFraction = 0.4f
- assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight)
-
- // Illegal fraction.
- edgeSplitFraction = 1.2f
- assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
-
- // Illegal fraction.
- edgeSplitFraction = -0.3f
- assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
- }
-
- @Test
- fun source_swipeVerticallyOnBottom_detectsBottom() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = screenWidth / 3,
- y = screenHeight - (edgeSize / 2),
- )
- assertThat(detectedEdge).isEqualTo(Bottom)
- }
-
- @Test
- fun source_swipeHorizontallyOnBottom_detectsNothing() {
- val detectedEdge =
- swipeHorizontallyFrom(
- x = screenWidth / 3,
- y = screenHeight - (edgeSize - 1),
- )
- assertThat(detectedEdge).isNull()
- }
-
- @Test
- fun source_swipeHorizontallyOnLeft_detectsLeft() {
- val detectedEdge =
- swipeHorizontallyFrom(
- x = edgeSize - 1,
- y = screenHeight / 2,
- )
- assertThat(detectedEdge).isEqualTo(Left)
- }
-
- @Test
- fun source_swipeVerticallyOnLeft_detectsNothing() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = edgeSize - 1,
- y = screenHeight / 2,
- )
- assertThat(detectedEdge).isNull()
- }
-
- @Test
- fun source_swipeHorizontallyOnRight_detectsRight() {
- val detectedEdge =
- swipeHorizontallyFrom(
- x = screenWidth - edgeSize + 1,
- y = screenHeight / 2,
- )
- assertThat(detectedEdge).isEqualTo(Right)
- }
-
- @Test
- fun source_swipeVerticallyOnRight_detectsNothing() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = screenWidth - edgeSize + 1,
- y = screenHeight / 2,
- )
- assertThat(detectedEdge).isNull()
- }
-
- @Test
- fun resolve_startInLtr_resolvesLeft() {
- val resolvedEdge = Start.resolve(LayoutDirection.Ltr)
- assertThat(resolvedEdge).isEqualTo(Left)
- }
-
- @Test
- fun resolve_startInRtl_resolvesRight() {
- val resolvedEdge = Start.resolve(LayoutDirection.Rtl)
- assertThat(resolvedEdge).isEqualTo(Right)
- }
-
- @Test
- fun resolve_endInLtr_resolvesRight() {
- val resolvedEdge = End.resolve(LayoutDirection.Ltr)
- assertThat(resolvedEdge).isEqualTo(Right)
- }
-
- @Test
- fun resolve_endInRtl_resolvesLeft() {
- val resolvedEdge = End.resolve(LayoutDirection.Rtl)
- assertThat(resolvedEdge).isEqualTo(Left)
- }
-
- @Test
- fun resolve_topStartInLtr_resolvesTopLeft() {
- val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr)
- assertThat(resolvedEdge).isEqualTo(TopLeft)
- }
-
- @Test
- fun resolve_topStartInRtl_resolvesTopRight() {
- val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl)
- assertThat(resolvedEdge).isEqualTo(TopRight)
- }
-
- @Test
- fun resolve_topEndInLtr_resolvesTopRight() {
- val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr)
- assertThat(resolvedEdge).isEqualTo(TopRight)
- }
-
- @Test
- fun resolve_topEndInRtl_resolvesTopLeft() {
- val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl)
- assertThat(resolvedEdge).isEqualTo(TopLeft)
- }
-
- private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
- return swipeFrom(x, y, Orientation.Vertical)
- }
-
- private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
- return swipeFrom(x, y, Orientation.Horizontal)
- }
-
- private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? {
- return underTest.source(
- layoutSize = IntSize(width = screenWidth, height = screenHeight),
- position = IntOffset(x, y),
- density = Density(1f),
- orientation = orientation,
- )
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 816df0102940..403ac3288128 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -61,14 +61,14 @@ import com.android.systemui.statusbar.core.StatusBarRootModernization
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
+import com.android.systemui.statusbar.notification.data.repository.addNotif
+import com.android.systemui.statusbar.notification.data.repository.addNotifs
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
-import com.android.systemui.statusbar.notification.shared.CallType
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
-import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
-import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
-import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState
import com.android.systemui.testKosmos
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -93,7 +93,6 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
- private val callRepo = kosmos.ongoingCallRepository
private val activeNotificationListRepository = kosmos.activeNotificationListRepository
private val mockSystemUIDialog = mock<SystemUIDialog>()
@@ -132,7 +131,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
@@ -145,7 +144,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -178,7 +177,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
@@ -191,7 +190,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -224,7 +223,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun primaryChip_screenRecordShowAndCallShow_screenRecordShown() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ addOngoingCallState("call")
val latest by collectLastValue(underTest.primaryChip)
@@ -237,9 +236,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -255,16 +252,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.Recording
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "call",
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- )
- )
- )
+ addOngoingCallState(callNotificationKey)
val latest by collectLastValue(underTest.chips)
val unused by collectLastValue(underTest.chipsLegacy)
@@ -281,7 +269,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@Test
fun chipsLegacy_oneChip_notSquished() =
kosmos.runTest {
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState()
val latest by collectLastValue(underTest.chipsLegacy)
@@ -294,17 +282,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@Test
fun chips_oneChip_notSquished() =
kosmos.runTest {
- val callNotificationKey = "call"
- setNotifs(
- listOf(
- activeNotificationModel(
- key = callNotificationKey,
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- )
- )
- )
+ addOngoingCallState()
val latest by collectLastValue(underTest.chips)
@@ -318,7 +296,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_twoTimerChips_isSmallPortrait_andChipsModernizationDisabled_bothSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
@@ -334,7 +312,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
@@ -354,7 +332,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// WHEN there's only one chip
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
// The screen record isn't squished because it's the only one
assertThat(latest!!.primary)
@@ -363,7 +341,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
// WHEN there's 2 chips
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState(key = "call")
// THEN they both become squished
assertThat(latest!!.primary)
@@ -387,7 +365,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_twoChips_isLandscape_notSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState(key = "call")
// WHEN we're in landscape
val config =
@@ -410,7 +388,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_twoChips_isLargeScreen_notSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState(key = "call")
// WHEN we're on a large screen
kosmos.displayStateRepository.setIsLargeScreen(true)
@@ -429,16 +407,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chips_twoChips_chipsModernizationEnabled_notSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "call",
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- )
- )
- )
+ addOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chips)
@@ -455,7 +424,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
@@ -469,7 +438,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -510,7 +479,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ addOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
@@ -525,9 +494,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -545,16 +512,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- setNotifs(
- listOf(
- activeNotificationModel(
- key = callNotificationKey,
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- )
- )
- )
+ addOngoingCallState(key = callNotificationKey)
val latest by collectLastValue(underTest.chips)
val unused by collectLastValue(underTest.chipsLegacy)
@@ -575,9 +533,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
mediaProjectionState.value = MediaProjectionState.NotProjecting
val callNotificationKey = "call"
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(key = callNotificationKey)
val latest by collectLastValue(underTest.primaryChip)
@@ -593,9 +549,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(key = callNotificationKey)
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -614,16 +568,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- setNotifs(
- listOf(
- activeNotificationModel(
- key = callNotificationKey,
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- )
- )
- )
+ addOngoingCallState(key = callNotificationKey)
val latest by collectLastValue(underTest.chips)
val unused by collectLastValue(underTest.chipsLegacy)
@@ -837,12 +782,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val unused by collectLastValue(underTest.chips)
val callNotificationKey = "call"
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
val firstIcon = createStatusBarIconViewOrNull()
- setNotifs(
+ activeNotificationListRepository.addNotifs(
listOf(
activeNotificationModel(
key = "firstNotif",
@@ -874,14 +817,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val callNotificationKey = "call"
val firstIcon = createStatusBarIconViewOrNull()
val secondIcon = createStatusBarIconViewOrNull()
- setNotifs(
+ addOngoingCallState(key = callNotificationKey)
+ activeNotificationListRepository.addNotifs(
listOf(
activeNotificationModel(
- key = callNotificationKey,
- whenTime = 499,
- callType = CallType.Ongoing,
- ),
- activeNotificationModel(
key = "firstNotif",
statusBarChipIcon = firstIcon,
promotedContent =
@@ -913,17 +852,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
screenRecordState.value = ScreenRecordModel.Recording
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- )
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
)
)
@@ -942,20 +877,14 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val callNotificationKey = "call"
val notifIcon = createStatusBarIconViewOrNull()
screenRecordState.value = ScreenRecordModel.Recording
- setNotifs(
- listOf(
- activeNotificationModel(
- key = callNotificationKey,
- whenTime = 499,
- callType = CallType.Ongoing,
- ),
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- ),
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = notifIcon,
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
)
)
+ addOngoingCallState(key = callNotificationKey)
assertThat(latest!!.active.size).isEqualTo(2)
assertIsScreenRecordChip(latest!!.active[0])
@@ -982,7 +911,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
)
)
// And everything else hidden
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = callNotificationKey)
mediaProjectionState.value = MediaProjectionState.NotProjecting
screenRecordState.value = ScreenRecordModel.DoingNothing
@@ -991,9 +920,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertIsNotifChip(latest, context, notifIcon, "notif")
// WHEN the higher priority call chip is added
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
// THEN the higher priority call chip is used
assertIsCallChip(latest, callNotificationKey)
@@ -1024,17 +951,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
val notifIcon = createStatusBarIconViewOrNull()
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- )
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = notifIcon,
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
)
)
@@ -1056,7 +979,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertIsCallChip(latest, callNotificationKey)
// WHEN the higher priority call is removed
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = callNotificationKey)
// THEN the lower priority notif is used
assertIsNotifChip(latest, context, notifIcon, "notif")
@@ -1069,17 +992,15 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val callNotificationKey = "call"
// Start with just the lowest priority chip shown
val notifIcon = createStatusBarIconViewOrNull()
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- )
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = notifIcon,
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
)
)
// And everything else hidden
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = callNotificationKey)
mediaProjectionState.value = MediaProjectionState.NotProjecting
screenRecordState.value = ScreenRecordModel.DoingNothing
@@ -1092,9 +1013,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel())
// WHEN the higher priority call chip is added
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
// THEN the higher priority call chip is used as primary and notif is demoted to
// secondary
@@ -1125,7 +1044,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// WHEN screen record and call is dropped
screenRecordState.value = ScreenRecordModel.DoingNothing
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = callNotificationKey)
// THEN media projection and notif remain
assertIsShareToAppChip(latest!!.primary)
@@ -1172,21 +1091,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
// WHEN the higher priority call chip is added
- setNotifs(
- listOf(
- activeNotificationModel(
- key = callNotificationKey,
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- ),
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- ),
- )
- )
+ addOngoingCallState(key = callNotificationKey)
// THEN the higher priority call chip and notif are active in that order
assertThat(latest!!.active.size).isEqualTo(2)
@@ -1372,7 +1277,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
@@ -1399,7 +1304,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
screenRecordState.value = ScreenRecordModel.DoingNothing
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
index 96c9dc83a6bd..d570f18e35d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
@@ -20,6 +20,8 @@ import android.os.PowerManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.Kosmos
@@ -28,9 +30,13 @@ import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testCase
import com.android.systemui.plugins.statusbar.statusBarStateController
import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.shade.domain.interactor.enableDualShade
+import com.android.systemui.shade.domain.interactor.enableSingleShade
+import com.android.systemui.shade.domain.interactor.enableSplitShade
import com.android.systemui.statusbar.lockscreenShadeTransitionController
import com.android.systemui.statusbar.phone.screenOffAnimationController
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
@@ -120,4 +126,48 @@ class NotificationShelfViewModelTest : SysuiTestCase() {
assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true))
}
+
+ @Test
+ @EnableSceneContainer
+ fun isAlignedToEnd_splitShade_true() =
+ kosmos.runTest {
+ val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+ kosmos.enableSplitShade()
+
+ assertThat(isShelfAlignedToEnd).isTrue()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun isAlignedToEnd_singleShade_false() =
+ kosmos.runTest {
+ val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+ kosmos.enableSingleShade()
+
+ assertThat(isShelfAlignedToEnd).isFalse()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun isAlignedToEnd_dualShade_wideScreen_false() =
+ kosmos.runTest {
+ val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+ kosmos.enableDualShade(wideLayout = true)
+
+ assertThat(isShelfAlignedToEnd).isFalse()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun isAlignedToEnd_dualShade_narrowScreen_false() =
+ kosmos.runTest {
+ val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+ kosmos.enableDualShade(wideLayout = false)
+
+ assertThat(isShelfAlignedToEnd).isFalse()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index f0823e2f645e..c48287c32120 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
@@ -35,8 +35,8 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
-import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.setNoCallState
-import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.setOngoingCallState
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState
import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -78,8 +78,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
val testIntent: PendingIntent = mock()
val testPromotedContent =
PromotedNotificationContentModel.Builder("promotedCall").build()
- setOngoingCallState(
- kosmos = this,
+ addOngoingCallState(
key = "promotedCall",
startTimeMs = 1000L,
statusBarChipIconView = testIconView,
@@ -100,8 +99,8 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.runTest {
val latest by collectLastValue(underTest.ongoingCallState)
- setOngoingCallState(kosmos = this)
- setNoCallState(kosmos = this)
+ addOngoingCallState(key = "testKey")
+ removeOngoingCallState(key = "testKey")
assertThat(latest).isInstanceOf(OngoingCallModel.NoCall::class.java)
}
@@ -112,7 +111,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true
val latest by collectLastValue(underTest.ongoingCallState)
- setOngoingCallState(kosmos = this, uid = UID)
+ addOngoingCallState(uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java)
}
@@ -123,7 +122,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
val latest by collectLastValue(underTest.ongoingCallState)
- setOngoingCallState(kosmos = this, uid = UID)
+ addOngoingCallState(uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
}
@@ -135,7 +134,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Start with notification and app not visible
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
- setOngoingCallState(kosmos = this, uid = UID)
+ addOngoingCallState(uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
// App becomes visible
@@ -161,7 +160,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.fakeStatusBarWindowControllerStore.defaultDisplay
.ongoingProcessRequiresStatusBarVisible
)
- setOngoingCallState(kosmos = this)
+ addOngoingCallState()
assertThat(isStatusBarRequired).isTrue()
assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -183,9 +182,9 @@ class OngoingCallInteractorTest : SysuiTestCase() {
.ongoingProcessRequiresStatusBarVisible
)
- setOngoingCallState(kosmos = this)
+ addOngoingCallState(key = "testKey")
- setNoCallState(kosmos = this)
+ removeOngoingCallState(key = "testKey")
assertThat(isStatusBarRequired).isFalse()
assertThat(requiresStatusBarVisibleInRepository).isFalse()
@@ -210,7 +209,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
- setOngoingCallState(kosmos = this, uid = UID)
+ addOngoingCallState(uid = UID)
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -232,7 +231,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
// Set up notification but not in fullscreen
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
- setOngoingCallState(kosmos = this)
+ addOngoingCallState()
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
verify(kosmos.swipeStatusBarAwayGestureHandler, never())
@@ -246,7 +245,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Set up notification and fullscreen mode
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- setOngoingCallState(kosmos = this)
+ addOngoingCallState()
assertThat(isGestureListeningEnabled).isTrue()
verify(kosmos.swipeStatusBarAwayGestureHandler)
@@ -260,7 +259,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Set up notification and fullscreen mode
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- setOngoingCallState(kosmos = this)
+ addOngoingCallState()
clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
@@ -287,7 +286,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
)
// Start with an ongoing call (which should set status bar required)
- setOngoingCallState(kosmos = this)
+ addOngoingCallState()
assertThat(isStatusBarRequiredForOngoingCall).isTrue()
assertThat(requiresStatusBarVisibleInRepository).isTrue()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
index 61ee5e04afd9..390518f3e2e5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
@@ -16,8 +16,10 @@
package com.android.systemui.window.ui.viewmodel
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
@@ -32,6 +34,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(Flags.FLAG_BOUNCER_UI_REVAMP)
class WindowRootViewModelTest : SysuiTestCase() {
val kosmos = testKosmos()
val testScope = kosmos.testScope
diff --git a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
index 91cd019c85d1..43808f215a81 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
@@ -149,9 +149,9 @@
style="@style/TextAppearance.AuthCredential.Indicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginTop="24dp"
android:layout_marginHorizontal="24dp"
- android:accessibilityLiveRegion="assertive"
+ android:layout_marginTop="24dp"
+ android:accessibilityLiveRegion="polite"
android:fadingEdge="horizontal"
android:gravity="center_horizontal"
android:scrollHorizontally="true"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 86292039d93d..d18a90a17abe 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1351,10 +1351,6 @@
<string name="accessibility_action_label_shrink_widget">Decrease height</string>
<!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] -->
<string name="accessibility_action_label_expand_widget">Increase height</string>
- <!-- Label for accessibility action to show the next media player. [CHAR LIMIT=NONE] -->
- <string name="accessibility_action_label_umo_show_next">Show next</string>
- <!-- Label for accessibility action to show the previous media player. [CHAR LIMIT=NONE] -->
- <string name="accessibility_action_label_umo_show_previous">Show previous</string>
<!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] -->
<string name="communal_widgets_disclaimer_title">Lock screen widgets</string>
<!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 5ef4d4014ba6..7f2c89346423 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -258,7 +258,7 @@
<style name="TextAppearance.AuthNonBioCredential.Title">
<item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">24dp</item>
- <item name="android:textSize">36dp</item>
+ <item name="android:textSize">36sp</item>
<item name="android:focusable">true</item>
<item name="android:textColor">@androidprv:color/materialColorOnSurface</item>
</style>
diff --git a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS
index b65d29c6a0bb..429b4b0fccab 100644
--- a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS
@@ -5,5 +5,4 @@ linyuh@google.com
pauldpong@google.com
praveenj@google.com
vicliang@google.com
-mfolkerts@google.com
yuklimko@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
index 6cd763a9d3d0..bbf9a19012a4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -31,6 +31,7 @@ import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieProperty
import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.keyguard.KeyguardPINView
import com.android.systemui.CoreStartable
import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
@@ -50,7 +51,6 @@ import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */
@SysUISingleton
@@ -65,51 +65,53 @@ constructor(
private val layoutInflater: Lazy<LayoutInflater>,
private val sideFpsProgressBarViewModel: Lazy<SideFpsProgressBarViewModel>,
private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>,
- private val windowManager: Lazy<WindowManager>
+ private val windowManager: Lazy<WindowManager>,
) : CoreStartable {
override fun start() {
- applicationScope
- .launch {
- sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable ->
- if (isSfpsAvailable) {
- combine(
- biometricStatusInteractor.get().sfpsAuthenticationReason,
- deviceEntrySideFpsOverlayInteractor
- .get()
- .showIndicatorForDeviceEntry,
- sideFpsProgressBarViewModel.get().isVisible,
- ::Triple
+ applicationScope.launch {
+ sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable ->
+ if (isSfpsAvailable) {
+ combine(
+ biometricStatusInteractor.get().sfpsAuthenticationReason,
+ deviceEntrySideFpsOverlayInteractor.get().showIndicatorForDeviceEntry,
+ sideFpsProgressBarViewModel.get().isVisible,
+ ::Triple,
+ )
+ .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair)
+ .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
+ val (
+ systemServerAuthReason,
+ showIndicatorForDeviceEntry,
+ progressBarIsVisible) =
+ combinedFlows
+ Log.d(
+ TAG,
+ "systemServerAuthReason = $systemServerAuthReason, " +
+ "showIndicatorForDeviceEntry = " +
+ "$showIndicatorForDeviceEntry, " +
+ "progressBarIsVisible = $progressBarIsVisible",
)
- .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair)
- .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
- val (
- systemServerAuthReason,
- showIndicatorForDeviceEntry,
- progressBarIsVisible) =
- combinedFlows
- Log.d(
- TAG,
- "systemServerAuthReason = $systemServerAuthReason, " +
- "showIndicatorForDeviceEntry = " +
- "$showIndicatorForDeviceEntry, " +
- "progressBarIsVisible = $progressBarIsVisible"
- )
- if (!isInRearDisplayMode) {
- if (progressBarIsVisible) {
- hide()
- } else if (systemServerAuthReason != NotRunning) {
- show()
- } else if (showIndicatorForDeviceEntry) {
- show()
- } else {
- hide()
- }
+ if (!isInRearDisplayMode) {
+ if (progressBarIsVisible) {
+ hide()
+ } else if (systemServerAuthReason != NotRunning) {
+ show()
+ } else if (showIndicatorForDeviceEntry) {
+ show()
+ overlayView?.announceForAccessibility(
+ applicationContext.resources.getString(
+ R.string.accessibility_side_fingerprint_indicator_label
+ )
+ )
+ } else {
+ hide()
}
}
- }
+ }
}
}
+ }
}
private var overlayView: View? = null
@@ -119,7 +121,7 @@ constructor(
if (overlayView?.isAttachedToWindow == true) {
Log.d(
TAG,
- "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request"
+ "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request",
)
return
}
@@ -137,11 +139,6 @@ constructor(
overlayView!!.visibility = View.INVISIBLE
Log.d(TAG, "show(): adding overlayView $overlayView")
windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
- overlayView!!.announceForAccessibility(
- applicationContext.resources.getString(
- R.string.accessibility_side_fingerprint_indicator_label
- )
- )
}
/** Hide the side fingerprint sensor indicator */
@@ -163,7 +160,7 @@ constructor(
fun bind(
overlayView: View,
viewModel: SideFpsOverlayViewModel,
- windowManager: WindowManager
+ windowManager: WindowManager,
) {
overlayView.repeatWhenAttached {
val lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation)
@@ -186,7 +183,7 @@ constructor(
object : View.AccessibilityDelegate() {
override fun dispatchPopulateAccessibilityEvent(
host: View,
- event: AccessibilityEvent
+ event: AccessibilityEvent,
): Boolean {
return if (
event.getEventType() ===
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index a4860dfc47ce..49003a735fbd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -202,12 +202,6 @@ abstract class BaseCommunalViewModel(
/** Called as the user request to show the customize widget button. */
open fun onLongClick() {}
- /** Called as the user requests to switch to the previous player in UMO. */
- open fun onShowPreviousMedia() {}
-
- /** Called as the user requests to switch to the next player in UMO. */
- open fun onShowNextMedia() {}
-
/** Called as the UI determines that a new widget has been added to the grid. */
open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index dd4018a9d7b9..4bc44005d2fc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -254,14 +254,6 @@ constructor(
}
}
- override fun onShowPreviousMedia() {
- mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(-1)
- }
-
- override fun onShowNextMedia() {
- mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(1)
- }
-
override fun onTapWidget(componentName: ComponentName, rank: Int) {
metricsLogger.logTapWidget(componentName.flattenToString(), rank)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
index 382436cf9397..5f821022d580 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
@@ -215,6 +215,7 @@ constructor(
animator = null,
modeOnCanceled = TransitionModeOnCanceled.RESET,
)
+ repository.nextLockscreenTargetState.value = DEFAULT_STATE
startTransition(newTransition)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
index 0107a5278e3e..d63c2e07b94f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
@@ -23,11 +23,11 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
-import androidx.annotation.VisibleForTesting
import androidx.core.view.GestureDetectorCompat
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringForce
import com.android.app.tracing.TraceStateLogger
+import com.android.internal.annotations.VisibleForTesting
import com.android.settingslib.Utils
import com.android.systemui.Gefingerpoken
import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS
@@ -38,10 +38,9 @@ import com.android.systemui.res.R
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.wm.shell.shared.animation.PhysicsAnimator
-import kotlin.math.sign
private const val FLING_SLOP = 1000000
-@VisibleForTesting const val DISMISS_DELAY = 100L
+private const val DISMISS_DELAY = 100L
private const val SCROLL_DELAY = 100L
private const val RUBBERBAND_FACTOR = 0.2f
private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f
@@ -65,7 +64,7 @@ class MediaCarouselScrollHandler(
private val closeGuts: (immediate: Boolean) -> Unit,
private val falsingManager: FalsingManager,
private val logSmartspaceImpression: (Boolean) -> Unit,
- private val logger: MediaUiEventLogger,
+ private val logger: MediaUiEventLogger
) {
/** Trace state logger for media carousel visibility */
private val visibleStateLogger = TraceStateLogger("$TAG#visibleToUser")
@@ -97,7 +96,7 @@ class MediaCarouselScrollHandler(
/** What's the currently visible player index? */
var visibleMediaIndex: Int = 0
- @VisibleForTesting set
+ private set
/** How much are we scrolled into the current media? */
private var scrollIntoCurrentMedia: Int = 0
@@ -138,14 +137,14 @@ class MediaCarouselScrollHandler(
eStart: MotionEvent?,
eCurrent: MotionEvent,
vX: Float,
- vY: Float,
+ vY: Float
) = onFling(vX, vY)
override fun onScroll(
down: MotionEvent?,
lastMotion: MotionEvent,
distanceX: Float,
- distanceY: Float,
+ distanceY: Float
) = onScroll(down!!, lastMotion, distanceX)
override fun onDown(e: MotionEvent): Boolean {
@@ -158,7 +157,6 @@ class MediaCarouselScrollHandler(
val touchListener =
object : Gefingerpoken {
override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!)
-
override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!)
}
@@ -170,7 +168,7 @@ class MediaCarouselScrollHandler(
scrollX: Int,
scrollY: Int,
oldScrollX: Int,
- oldScrollY: Int,
+ oldScrollY: Int
) {
if (playerWidthPlusPadding == 0) {
return
@@ -179,7 +177,7 @@ class MediaCarouselScrollHandler(
val relativeScrollX = scrollView.relativeScrollX
onMediaScrollingChanged(
relativeScrollX / playerWidthPlusPadding,
- relativeScrollX % playerWidthPlusPadding,
+ relativeScrollX % playerWidthPlusPadding
)
}
}
@@ -211,7 +209,7 @@ class MediaCarouselScrollHandler(
0,
carouselWidth,
carouselHeight,
- cornerRadius.toFloat(),
+ cornerRadius.toFloat()
)
}
}
@@ -237,7 +235,7 @@ class MediaCarouselScrollHandler(
getMaxTranslation().toFloat(),
0.0f,
1.0f,
- Math.abs(contentTranslation),
+ Math.abs(contentTranslation)
)
val settingsTranslation =
(1.0f - settingsOffset) *
@@ -325,7 +323,7 @@ class MediaCarouselScrollHandler(
CONTENT_TRANSLATION,
newTranslation,
startVelocity = 0.0f,
- config = translationConfig,
+ config = translationConfig
)
.start()
scrollView.animationTargetX = newTranslation
@@ -393,7 +391,7 @@ class MediaCarouselScrollHandler(
CONTENT_TRANSLATION,
newTranslation,
startVelocity = 0.0f,
- config = translationConfig,
+ config = translationConfig
)
.start()
} else {
@@ -432,7 +430,7 @@ class MediaCarouselScrollHandler(
CONTENT_TRANSLATION,
newTranslation,
startVelocity = vX,
- config = translationConfig,
+ config = translationConfig
)
.start()
scrollView.animationTargetX = newTranslation
@@ -585,35 +583,10 @@ class MediaCarouselScrollHandler(
// We need to post this to wait for the active player becomes visible.
mainExecutor.executeDelayed(
{ scrollView.smoothScrollTo(view.left, scrollView.scrollY) },
- SCROLL_DELAY,
+ SCROLL_DELAY
)
}
- /**
- * Scrolls the media carousel by the number of players specified by [step]. If scrolling beyond
- * the carousel's bounds:
- * - If the carousel is not dismissible, the settings button is displayed.
- * - If the carousel is dismissible, no action taken.
- *
- * @param step A positive number means next, and negative means previous.
- */
- fun scrollByStep(step: Int) {
- val destIndex = visibleMediaIndex + step
- if (destIndex >= mediaContent.childCount || destIndex < 0) {
- if (!showsSettingsButton) return
- var translation = getMaxTranslation() * sign(-step.toFloat())
- translation = if (isRtl) -translation else translation
- PhysicsAnimator.getInstance(this)
- .spring(CONTENT_TRANSLATION, translation, config = translationConfig)
- .start()
- scrollView.animationTargetX = translation
- } else if (scrollView.getContentTranslation() != 0.0f) {
- resetTranslation(true)
- } else {
- scrollToPlayer(destIndex = destIndex)
- }
- }
-
companion object {
private val CONTENT_TRANSLATION =
object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") {
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt
index 1b9251061f3d..9319961f5b68 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt
@@ -24,7 +24,7 @@ import com.android.compose.animation.scene.UserActionResult.HideOverlay
import com.android.compose.animation.scene.UserActionResult.ShowOverlay
import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays
import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -38,7 +38,7 @@ class NotificationsShadeOverlayActionsViewModel @AssistedInject constructor() :
mapOf(
Swipe.Up to HideOverlay(Overlays.NotificationsShade),
Back to HideOverlay(Overlays.NotificationsShade),
- Swipe.Down(fromSource = SceneContainerEdge.TopRight) to
+ Swipe.Down(fromSource = SceneContainerArea.EndHalf) to
ShowOverlay(
Overlays.QuickSettingsShade,
hideCurrentOverlays = HideCurrentOverlays.Some(Overlays.NotificationsShade),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt
index 5bc26f50f70f..52c4e2fac6d5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt
@@ -25,7 +25,7 @@ import com.android.compose.animation.scene.UserActionResult.ShowOverlay
import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays
import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -47,7 +47,7 @@ constructor(private val editModeViewModel: EditModeViewModel) : UserActionsViewM
put(Back, HideOverlay(Overlays.QuickSettingsShade))
}
put(
- Swipe.Down(fromSource = SceneContainerEdge.TopLeft),
+ Swipe.Down(fromSource = SceneContainerArea.StartHalf),
ShowOverlay(
Overlays.NotificationsShade,
hideCurrentOverlays =
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index a4949ad66109..caa61617505f 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -16,7 +16,6 @@
package com.android.systemui.scene
-import androidx.compose.ui.unit.dp
import com.android.systemui.CoreStartable
import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,8 +29,6 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.SceneContainerTransitions
-import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -99,15 +96,5 @@ interface KeyguardlessSceneContainerFrameworkModule {
transitionsBuilder = SceneContainerTransitions(),
)
}
-
- @Provides
- fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
- return SplitEdgeDetector(
- topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
- // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
- // replace this constant with dynamic window insets.
- edgeSize = 40.dp,
- )
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index a018283c3953..ea11d202b119 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,7 +16,6 @@
package com.android.systemui.scene
-import androidx.compose.ui.unit.dp
import com.android.systemui.CoreStartable
import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,8 +29,6 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.SceneContainerTransitions
-import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -121,15 +118,5 @@ interface SceneContainerFrameworkModule {
transitionsBuilder = SceneContainerTransitions(),
)
}
-
- @Provides
- fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
- return SplitEdgeDetector(
- topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
- // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
- // replace this constant with dynamic window insets.
- edgeSize = 40.dp,
- )
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 7a32491c0b67..475c0794861f 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -240,7 +240,13 @@ constructor(
) {
val currentSceneKey = currentScene.value
val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene
- if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) {
+ if (
+ !validateSceneChange(
+ from = currentSceneKey,
+ to = resolvedScene,
+ loggingReason = loggingReason,
+ )
+ ) {
return
}
@@ -249,6 +255,7 @@ constructor(
logger.logSceneChanged(
from = currentSceneKey,
to = resolvedScene,
+ sceneState = sceneState,
reason = loggingReason,
isInstant = false,
)
@@ -272,13 +279,20 @@ constructor(
familyResolver.resolvedScene.value
}
} ?: toScene
- if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) {
+ if (
+ !validateSceneChange(
+ from = currentSceneKey,
+ to = resolvedScene,
+ loggingReason = loggingReason,
+ )
+ ) {
return
}
logger.logSceneChanged(
from = currentSceneKey,
to = resolvedScene,
+ sceneState = null,
reason = loggingReason,
isInstant = true,
)
@@ -489,11 +503,12 @@ constructor(
* Will throw a runtime exception for illegal states (for example, attempting to change to a
* scene that's not part of the current scene framework configuration).
*
+ * @param from The current scene being transitioned away from
* @param to The desired destination scene to transition to
* @param loggingReason The reason why the transition is requested, for logging purposes
* @return `true` if the scene change is valid; `false` if it shouldn't happen
*/
- private fun validateSceneChange(to: SceneKey, loggingReason: String): Boolean {
+ private fun validateSceneChange(from: SceneKey, to: SceneKey, loggingReason: String): Boolean {
check(
!shadeModeInteractor.isDualShade || (to != Scenes.Shade && to != Scenes.QuickSettings)
) {
@@ -503,6 +518,10 @@ constructor(
"Can't change scene to ${to.debugName} in split shade mode!"
}
+ if (from == to) {
+ return false
+ }
+
if (to !in repository.allContentKeys) {
return false
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
index 16c2ef556de8..d00585858ccb 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
@@ -45,23 +45,30 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer:
)
}
- fun logSceneChanged(from: SceneKey, to: SceneKey, reason: String, isInstant: Boolean) {
+ fun logSceneChanged(
+ from: SceneKey,
+ to: SceneKey,
+ sceneState: Any?,
+ reason: String,
+ isInstant: Boolean,
+ ) {
logBuffer.log(
tag = TAG,
level = LogLevel.INFO,
messageInitializer = {
- str1 = from.toString()
- str2 = to.toString()
- str3 = reason
+ str1 = "${from.debugName} → ${to.debugName}"
+ str2 = reason
+ str3 = sceneState?.toString()
bool1 = isInstant
},
messagePrinter = {
buildString {
- append("Scene changed: $str1 → $str2")
+ append("Scene changed: $str1")
+ str3?.let { append(" (sceneState=$it)") }
if (isInstant) {
append(" (instant)")
}
- append(", reason: $str3")
+ append(", reason: $str2")
}
},
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt
new file mode 100644
index 000000000000..ede453dbe6b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.FixedSizeEdgeDetector
+import com.android.compose.animation.scene.SwipeSource
+import com.android.compose.animation.scene.SwipeSourceDetector
+
+/** Identifies an area of the [SceneContainer] to detect swipe gestures on. */
+sealed class SceneContainerArea(private val resolveArea: (LayoutDirection) -> Resolved) :
+ SwipeSource {
+ data object StartEdge :
+ SceneContainerArea(
+ resolveArea = {
+ if (it == LayoutDirection.Ltr) Resolved.LeftEdge else Resolved.RightEdge
+ }
+ )
+
+ data object StartHalf :
+ SceneContainerArea(
+ resolveArea = {
+ if (it == LayoutDirection.Ltr) Resolved.LeftHalf else Resolved.RightHalf
+ }
+ )
+
+ data object EndEdge :
+ SceneContainerArea(
+ resolveArea = {
+ if (it == LayoutDirection.Ltr) Resolved.RightEdge else Resolved.LeftEdge
+ }
+ )
+
+ data object EndHalf :
+ SceneContainerArea(
+ resolveArea = {
+ if (it == LayoutDirection.Ltr) Resolved.RightHalf else Resolved.LeftHalf
+ }
+ )
+
+ override fun resolve(layoutDirection: LayoutDirection): Resolved {
+ return resolveArea(layoutDirection)
+ }
+
+ sealed interface Resolved : SwipeSource.Resolved {
+ data object LeftEdge : Resolved
+
+ data object LeftHalf : Resolved
+
+ data object BottomEdge : Resolved
+
+ data object RightEdge : Resolved
+
+ data object RightHalf : Resolved
+ }
+}
+
+/**
+ * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], but additionally
+ * detects the left and right halves of the screen (besides the edges).
+ *
+ * Corner cases (literally): A vertical swipe on the top-left corner of the screen will be resolved
+ * to [SceneContainerArea.Resolved.LeftHalf], whereas a horizontal swipe in the same position will
+ * be resolved to [SceneContainerArea.Resolved.LeftEdge]. The behavior is similar on the top-right
+ * corner of the screen.
+ *
+ * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL)
+ * should subscribe to [SceneContainerArea.StartEdge] and [SceneContainerArea.EndEdge] instead.
+ * These will be resolved at runtime to [SceneContainerArea.Resolved.LeftEdge] and
+ * [SceneContainerArea.Resolved.RightEdge] appropriately. Similarly, [SceneContainerArea.StartHalf]
+ * and [SceneContainerArea.EndHalf] will be resolved appropriately to
+ * [SceneContainerArea.Resolved.LeftHalf] and [SceneContainerArea.Resolved.RightHalf].
+ *
+ * @param edgeSize The fixed size of each edge.
+ */
+class SceneContainerSwipeDetector(val edgeSize: Dp) : SwipeSourceDetector {
+
+ private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize)
+
+ override fun source(
+ layoutSize: IntSize,
+ position: IntOffset,
+ density: Density,
+ orientation: Orientation,
+ ): SceneContainerArea.Resolved {
+ val fixedEdge = fixedEdgeDetector.source(layoutSize, position, density, orientation)
+ return when (fixedEdge) {
+ Edge.Resolved.Left -> SceneContainerArea.Resolved.LeftEdge
+ Edge.Resolved.Bottom -> SceneContainerArea.Resolved.BottomEdge
+ Edge.Resolved.Right -> SceneContainerArea.Resolved.RightEdge
+ else -> {
+ // Note: This intentionally includes Edge.Resolved.Top. At the moment, we don't need
+ // to detect swipes on the top edge, and consider them part of the right/left half.
+ if (position.x < layoutSize.width * 0.5f) {
+ SceneContainerArea.Resolved.LeftHalf
+ } else {
+ SceneContainerArea.Resolved.RightHalf
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 233e15846450..01bcc2400933 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -19,6 +19,7 @@ package com.android.systemui.scene.ui.viewmodel
import android.view.MotionEvent
import android.view.View
import androidx.compose.runtime.getValue
+import androidx.compose.ui.unit.dp
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.DefaultEdgeDetector
@@ -64,7 +65,6 @@ constructor(
private val powerInteractor: PowerInteractor,
shadeModeInteractor: ShadeModeInteractor,
private val remoteInputInteractor: RemoteInputInteractor,
- private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory,
val lightRevealScrim: LightRevealScrimViewModel,
@@ -89,16 +89,20 @@ constructor(
val hapticsViewModel: SceneContainerHapticsViewModel = hapticsViewModelFactory.create(view)
/**
- * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the
+ * The [SwipeSourceDetector] to use for defining which areas of the screen can be defined in the
* [UserAction]s for this container.
*/
- val edgeDetector: SwipeSourceDetector by
+ val swipeSourceDetector: SwipeSourceDetector by
hydrator.hydratedStateOf(
- traceName = "edgeDetector",
+ traceName = "swipeSourceDetector",
initialValue = DefaultEdgeDetector,
source =
shadeModeInteractor.shadeMode.map {
- if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector
+ if (it is ShadeMode.Dual) {
+ SceneContainerSwipeDetector(edgeSize = 40.dp)
+ } else {
+ DefaultEdgeDetector
+ }
},
)
@@ -241,6 +245,7 @@ constructor(
logger.logSceneChanged(
from = fromScene,
to = toScene,
+ sceneState = null,
reason = "user interaction",
isInstant = false,
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
deleted file mode 100644
index f88bcb57a27d..000000000000
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
+++ /dev/null
@@ -1,116 +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.scene.ui.viewmodel
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import com.android.compose.animation.scene.Edge
-import com.android.compose.animation.scene.FixedSizeEdgeDetector
-import com.android.compose.animation.scene.SwipeSource
-import com.android.compose.animation.scene.SwipeSourceDetector
-
-/**
- * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into
- * top-left and top-right.
- */
-enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) :
- SwipeSource {
- TopLeft(resolveEdge = { Resolved.TopLeft }),
- TopRight(resolveEdge = { Resolved.TopRight }),
- TopStart(
- resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight }
- ),
- TopEnd(
- resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft }
- ),
- Bottom(resolveEdge = { Resolved.Bottom }),
- Left(resolveEdge = { Resolved.Left }),
- Right(resolveEdge = { Resolved.Right }),
- Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }),
- End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left });
-
- override fun resolve(layoutDirection: LayoutDirection): Resolved {
- return resolveEdge(layoutDirection)
- }
-
- enum class Resolved : SwipeSource.Resolved {
- TopLeft,
- TopRight,
- Bottom,
- Left,
- Right,
- }
-}
-
-/**
- * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the
- * top edge is split in two: top-left and top-right. The split point between the two is dynamic and
- * may change during runtime.
- *
- * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL)
- * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These
- * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and
- * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and
- * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and
- * [SceneContainerEdge.Resolved.Right].
- *
- * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e.,
- * percentage) of screen width to consider the split point between "top-left" and "top-right"
- * edges. It is called on each source detection event.
- * @param edgeSize The fixed size of each edge.
- */
-class SplitEdgeDetector(
- val topEdgeSplitFraction: () -> Float,
- val edgeSize: Dp,
-) : SwipeSourceDetector {
-
- private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize)
-
- override fun source(
- layoutSize: IntSize,
- position: IntOffset,
- density: Density,
- orientation: Orientation,
- ): SceneContainerEdge.Resolved? {
- val fixedEdge =
- fixedEdgeDetector.source(
- layoutSize,
- position,
- density,
- orientation,
- )
- return when (fixedEdge) {
- Edge.Resolved.Top -> {
- val topEdgeSplitFraction = topEdgeSplitFraction()
- require(topEdgeSplitFraction in 0f..1f) {
- "topEdgeSplitFraction must return a value between 0.0 and 1.0"
- }
- val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction
- if (isLeftSide) SceneContainerEdge.Resolved.TopLeft
- else SceneContainerEdge.Resolved.TopRight
- }
- Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left
- Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom
- Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right
- null -> null
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
index b155ada87efd..1f534a5c191a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
@@ -111,10 +111,7 @@ constructor(
statusbarWidth: Int,
): ShadeElement {
val xPercentage = motionEvent.x / statusbarWidth
- val threshold = shadeInteractor.get().getTopEdgeSplitFraction()
- return if (xPercentage < threshold) {
- notificationElement.get()
- } else qsShadeElement.get()
+ return if (xPercentage < 0.5f) notificationElement.get() else qsShadeElement.get()
}
private fun monitorDisplayRemovals(): Job {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
index 6eaedd73ea76..2b3e4b5db453 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
@@ -34,7 +34,11 @@ constructor(
override fun animateCollapseQs(fullyCollapse: Boolean) {
if (shadeInteractor.isQsExpanded.value) {
val key =
- if (fullyCollapse || shadeModeInteractor.isDualShade) {
+ if (
+ fullyCollapse ||
+ shadeModeInteractor.isDualShade ||
+ shadeModeInteractor.isSplitShade
+ ) {
SceneFamilies.Home
} else {
Scenes.Shade
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index c8ce316c41dd..6d68796454eb 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -16,7 +16,6 @@
package com.android.systemui.shade.domain.interactor
-import androidx.annotation.FloatRange
import com.android.compose.animation.scene.TransitionKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -66,16 +65,6 @@ interface ShadeInteractor : BaseShadeInteractor {
* wide as the entire screen.
*/
val isShadeLayoutWide: StateFlow<Boolean>
-
- /**
- * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold
- * between "top-left" and "top-right" for the purposes of dual-shade invocation.
- *
- * Note that this fraction only determines the *split* between the absolute left and right
- * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end"
- * will resolve to "top-left".
- */
- @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float
}
/** ShadeInteractor methods with implementations that differ between non-empty impls. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index b1129a94d833..77e6a833c153 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -48,8 +48,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor {
override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean
override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean
- override fun getTopEdgeSplitFraction(): Float = 0.5f
-
override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {}
override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
index c6752f867183..cf3b08c041be 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
@@ -20,10 +20,11 @@ import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.UserActionResult.ShowOverlay
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the single shade. */
fun singleShadeActions(
@@ -66,11 +67,10 @@ fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> {
/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the dual shade. */
fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> {
- val notifShadeUserActionResult = UserActionResult.ShowOverlay(Overlays.NotificationsShade)
- val qsShadeuserActionResult = UserActionResult.ShowOverlay(Overlays.QuickSettingsShade)
return arrayOf(
- Swipe.Down to notifShadeUserActionResult,
- Swipe.Down(fromSource = SceneContainerEdge.TopRight) to qsShadeuserActionResult,
+ Swipe.Down to ShowOverlay(Overlays.NotificationsShade),
+ Swipe.Down(fromSource = SceneContainerArea.EndHalf) to
+ ShowOverlay(Overlays.QuickSettingsShade),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 155049f512d8..31fdec6147f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -93,6 +93,7 @@ public class NotificationShelf extends ActivatableNotificationView {
private int mPaddingBetweenElements;
private int mNotGoneIndex;
private boolean mHasItemsInStableShelf;
+ private boolean mAlignedToEnd;
private int mScrollFastThreshold;
private boolean mInteractive;
private boolean mAnimationsEnabled = true;
@@ -412,8 +413,22 @@ public class NotificationShelf extends ActivatableNotificationView {
public boolean isAlignedToEnd() {
if (!NotificationMinimalism.isEnabled()) {
return false;
+ } else if (SceneContainerFlag.isEnabled()) {
+ return mAlignedToEnd;
+ } else {
+ return mAmbientState.getUseSplitShade();
+ }
+ }
+
+ /** @see #isAlignedToEnd() */
+ public void setAlignedToEnd(boolean alignedToEnd) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+ return;
+ }
+ if (mAlignedToEnd != alignedToEnd) {
+ mAlignedToEnd = alignedToEnd;
+ requestLayout();
}
- return mAmbientState.getUseSplitShade();
}
@Override
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 09cc3f23032e..9dc651ed507a 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
@@ -643,6 +643,10 @@ public final class NotificationEntry extends ListEntry {
return row.isMediaRow();
}
+ public boolean containsCustomViews() {
+ return getSbn().getNotification().containsCustomViews();
+ }
+
public void resetUserExpansion() {
if (row != null) row.resetUserExpansion();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
index 6491223e6e10..f9e9bee4d809 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -12,7 +12,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.util.children
/** Walks view hiearchy of a given notification to estimate its memory use. */
-internal object NotificationMemoryViewWalker {
+object NotificationMemoryViewWalker {
private const val TAG = "NotificationMemory"
@@ -26,9 +26,13 @@ internal object NotificationMemoryViewWalker {
private var softwareBitmaps = 0
fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse }
+
fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse }
+
fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse }
+
fun addStyle(styleUse: Int) = apply { style += styleUse }
+
fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply {
softwareBitmaps += softwareBitmapUse
}
@@ -67,14 +71,14 @@ internal object NotificationMemoryViewWalker {
getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild),
getViewUsage(
ViewType.PRIVATE_CONTRACTED_VIEW,
- row.privateLayout?.contractedChild
+ row.privateLayout?.contractedChild,
),
getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild),
getViewUsage(
ViewType.PUBLIC_VIEW,
row.publicLayout?.expandedChild,
row.publicLayout?.contractedChild,
- row.publicLayout?.headsUpChild
+ row.publicLayout?.headsUpChild,
),
)
.filterNotNull()
@@ -107,14 +111,14 @@ internal object NotificationMemoryViewWalker {
row.publicLayout?.expandedChild,
row.publicLayout?.contractedChild,
row.publicLayout?.headsUpChild,
- seenObjects = seenObjects
+ seenObjects = seenObjects,
)
}
private fun getViewUsage(
type: ViewType,
vararg rootViews: View?,
- seenObjects: HashSet<Int> = hashSetOf()
+ seenObjects: HashSet<Int> = hashSetOf(),
): NotificationViewUsage? {
val usageBuilder = lazy { UsageBuilder() }
rootViews.forEach { rootView ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index c7e15fdb98c7..73e8246907aa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -901,6 +901,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder
if (!satisfiesMinHeightRequirement(view, entry, resources)) {
return "inflated notification does not meet minimum height requirement";
}
+
+ if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) {
+ if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) {
+ return "inflated notification does not meet maximum memory size requirement";
+ }
+ }
+
return null;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java
new file mode 100644
index 000000000000..c55cb6725e45
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java
@@ -0,0 +1,36 @@
+/*
+ * 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.statusbar.notification.row;
+
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
+import android.os.Build;
+
+/**
+ * Holds compat {@link ChangeId} for {@link NotificationCustomContentMemoryVerifier}.
+ */
+final class NotificationCustomContentCompat {
+ /**
+ * Enables memory size checking of custom views included in notifications to ensure that
+ * they conform to the size limit set in `config_notificationStripRemoteViewSizeBytes`
+ * config.xml parameter.
+ * Notifications exceeding the size will be rejected.
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ public static final long CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS = 270553691L;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt
new file mode 100644
index 000000000000..a3e6a5cddc94
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt
@@ -0,0 +1,175 @@
+/*
+ * 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.row
+
+import android.app.compat.CompatChanges
+import android.content.Context
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.traceSection
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+
+/** Checks whether Notifications with Custom content views conform to configured memory limits. */
+object NotificationCustomContentMemoryVerifier {
+
+ private const val NOTIFICATION_SERVICE_TAG = "NotificationService"
+
+ /** Notifications with custom views need to conform to maximum memory consumption. */
+ @JvmStatic
+ fun requiresImageViewMemorySizeCheck(entry: NotificationEntry): Boolean {
+ if (!com.android.server.notification.Flags.notificationCustomViewUriRestriction()) {
+ return false
+ }
+
+ return entry.containsCustomViews()
+ }
+
+ /**
+ * This walks the custom view hierarchy contained in the passed Notification view and determines
+ * if the total memory consumption of all image views satisfies the limit set by
+ * [getStripViewSizeLimit]. It will also log to logcat if the limit exceeds
+ * [getWarnViewSizeLimit].
+ *
+ * @return true if the Notification conforms to the view size limits.
+ */
+ @JvmStatic
+ fun satisfiesMemoryLimits(view: View, entry: NotificationEntry): Boolean {
+ val mainColumnView =
+ view.findViewById<View>(com.android.internal.R.id.notification_main_column)
+ if (mainColumnView == null) {
+ Log.wtf(
+ NOTIFICATION_SERVICE_TAG,
+ "R.id.notification_main_column view should not be null!",
+ )
+ return true
+ }
+
+ val memorySize =
+ traceSection("computeViewHiearchyImageViewSize") {
+ computeViewHierarchyImageViewSize(view)
+ }
+
+ if (memorySize > getStripViewSizeLimit(view.context)) {
+ val stripOversizedView = isCompatChangeEnabledForUid(entry.sbn.uid)
+ if (stripOversizedView) {
+ Log.w(
+ NOTIFICATION_SERVICE_TAG,
+ "Dropped notification due to too large RemoteViews ($memorySize bytes) on " +
+ "pkg: ${entry.sbn.packageName} tag: ${entry.sbn.tag} id: ${entry.sbn.id}",
+ )
+ } else {
+ Log.w(
+ NOTIFICATION_SERVICE_TAG,
+ "RemoteViews too large on pkg: ${entry.sbn.packageName} " +
+ "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " +
+ "this WILL notification WILL be dropped when targetSdk " +
+ "is set to ${Build.VERSION_CODES.BAKLAVA}!",
+ )
+ }
+
+ // We still warn for size, but return "satisfies = ok" if the target SDK
+ // is too low.
+ return !stripOversizedView
+ }
+
+ if (memorySize > getWarnViewSizeLimit(view.context)) {
+ // We emit the same warning as NotificationManagerService does to keep some consistency
+ // for developers.
+ Log.w(
+ NOTIFICATION_SERVICE_TAG,
+ "RemoteViews too large on pkg: ${entry.sbn.packageName} " +
+ "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " +
+ "this notifications might be dropped in a future release",
+ )
+ }
+ return true
+ }
+
+ private fun isCompatChangeEnabledForUid(uid: Int): Boolean =
+ try {
+ CompatChanges.isChangeEnabled(
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS,
+ uid,
+ )
+ } catch (e: RuntimeException) {
+ Log.wtf(NOTIFICATION_SERVICE_TAG, "Failed to contact system_server for compat change.")
+ false
+ }
+
+ @VisibleForTesting
+ @JvmStatic
+ fun computeViewHierarchyImageViewSize(view: View): Int =
+ when (view) {
+ is ViewGroup -> {
+ var use = 0
+ for (i in 0 until view.childCount) {
+ use += computeViewHierarchyImageViewSize(view.getChildAt(i))
+ }
+ use
+ }
+ is ImageView -> computeImageViewSize(view)
+ else -> 0
+ }
+
+ /**
+ * Returns the memory size of a Bitmap contained in a passed [ImageView] in bytes. If the view
+ * contains any other kind of drawable, the memory size is estimated from its intrinsic
+ * dimensions.
+ *
+ * @return Bitmap size in bytes or 0 if no drawable is set.
+ */
+ private fun computeImageViewSize(view: ImageView): Int {
+ val drawable = view.drawable
+ return computeDrawableSize(drawable)
+ }
+
+ private fun computeDrawableSize(drawable: Drawable?): Int {
+ return when (drawable) {
+ null -> 0
+ is AdaptiveIconDrawable ->
+ computeDrawableSize(drawable.foreground) +
+ computeDrawableSize(drawable.background) +
+ computeDrawableSize(drawable.monochrome)
+ is BitmapDrawable -> drawable.bitmap.allocationByteCount
+ // People can sneak large drawables into those custom memory views via resources -
+ // we use the intrisic size as a proxy for how much memory rendering those will
+ // take.
+ else -> drawable.intrinsicWidth * drawable.intrinsicHeight * 4
+ }
+ }
+
+ /** @return Size of remote views after which a size warning is logged. */
+ @VisibleForTesting
+ fun getWarnViewSizeLimit(context: Context): Int =
+ context.resources.getInteger(
+ com.android.internal.R.integer.config_notificationWarnRemoteViewSizeBytes
+ )
+
+ /** @return Size of remote views after which the notification is dropped. */
+ @VisibleForTesting
+ fun getStripViewSizeLimit(context: Context): Int =
+ context.resources.getInteger(
+ com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index 20c3464536e9..589e5b8be240 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -1396,9 +1396,17 @@ constructor(
*/
@VisibleForTesting
fun isValidView(view: View, entry: NotificationEntry, resources: Resources): String? {
- return if (!satisfiesMinHeightRequirement(view, entry, resources)) {
- "inflated notification does not meet minimum height requirement"
- } else null
+ if (!satisfiesMinHeightRequirement(view, entry, resources)) {
+ return "inflated notification does not meet minimum height requirement"
+ }
+
+ if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) {
+ if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) {
+ return "inflated notification does not meet maximum memory size requirement"
+ }
+ }
+
+ return null
}
private fun satisfiesMinHeightRequirement(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
index 9fdd0bcc4ee9..0703f2de250d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
@@ -21,11 +21,14 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.NotificationShelf
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
/** Interactor for the [NotificationShelf] */
@SysUISingleton
@@ -35,6 +38,7 @@ constructor(
private val keyguardRepository: KeyguardRepository,
private val deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository,
private val powerInteractor: PowerInteractor,
+ private val shadeModeInteractor: ShadeModeInteractor,
private val keyguardTransitionController: LockscreenShadeTransitionController,
) {
/** Is the shelf showing on the keyguard? */
@@ -51,6 +55,16 @@ constructor(
isKeyguardShowing && isBypassEnabled
}
+ /** Should the shelf be aligned to the end in the current configuration? */
+ val isAlignedToEnd: Flow<Boolean>
+ get() =
+ shadeModeInteractor.shadeMode.map { shadeMode ->
+ when (shadeMode) {
+ ShadeMode.Split -> true
+ else -> false
+ }
+ }
+
/** Transition keyguard to the locked shade, triggered by the shelf. */
fun goToLockedShadeFromShelf() {
powerInteractor.wakeUpIfDozing("SHADE_CLICK", PowerManager.WAKE_REASON_GESTURE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index 0352a304a5c1..f663ea019319 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -16,15 +16,16 @@
package com.android.systemui.statusbar.notification.shelf.ui.viewbinder
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.traceSection
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.NotificationShelf
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
object NotificationShelfViewBinder {
@@ -41,6 +42,11 @@ object NotificationShelfViewBinder {
viewModel.canModifyColorOfNotifications.collect(::setCanModifyColorOfNotifications)
}
launch { viewModel.isClickable.collect(::setCanInteract) }
+
+ if (SceneContainerFlag.isEnabled) {
+ launch { viewModel.isAlignedToEnd.collect(::setAlignedToEnd) }
+ }
+
registerViewListenersWhileAttached(shelf, viewModel)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
index 5ca8b53d0704..96cdda6d4a23 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
@@ -17,11 +17,13 @@
package com.android.systemui.statusbar.notification.shelf.ui.viewmodel
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.NotificationShelf
import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel
import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
/** ViewModel for [NotificationShelf]. */
@@ -40,6 +42,15 @@ constructor(
val canModifyColorOfNotifications: Flow<Boolean>
get() = interactor.isShelfStatic.map { static -> !static }
+ /** Is the shelf aligned to the end in the current configuration? */
+ val isAlignedToEnd: Flow<Boolean> by lazy {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ interactor.isAlignedToEnd
+ }
+ }
+
/** Notifies that the user has clicked the shelf. */
fun onShelfClicked() {
interactor.goToLockedShadeFromShelf()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 1bcc5adea6e8..54efa4a2bcf2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -478,7 +478,7 @@ constructor(
/**
* Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out
- * notifications unless in splitshade.
+ * notifications unless it's a large screen.
*/
private val alphaForShadeAndQsExpansion: Flow<Float> =
if (SceneContainerFlag.isEnabled) {
@@ -501,16 +501,26 @@ constructor(
Split -> isAnyExpanded.filter { it }.map { 1f }
Dual ->
combineTransform(
+ shadeModeInteractor.isShadeLayoutWide,
headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway,
shadeInteractor.shadeExpansion,
shadeInteractor.qsExpansion,
- ) { isHeadsUpOrAnimatingAway, shadeExpansion, qsExpansion ->
- if (isHeadsUpOrAnimatingAway) {
+ ) {
+ isShadeLayoutWide,
+ isHeadsUpOrAnimatingAway,
+ shadeExpansion,
+ qsExpansion ->
+ if (isShadeLayoutWide) {
+ if (shadeExpansion > 0f) {
+ emit(1f)
+ }
+ } else if (isHeadsUpOrAnimatingAway) {
// Ensure HUNs will be visible in QS shade (at least while
// unlocked)
emit(1f)
} else if (shadeExpansion > 0f || qsExpansion > 0f) {
- // Fade out as QS shade expands
+ // On a narrow screen, the QS shade overlaps with lockscreen
+ // notifications. Fade them out as the QS shade expands.
emit(1f - qsExpansion)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt
index 72d093c65a91..9f05850f3405 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt
@@ -22,11 +22,11 @@ interface SplitShadeStateController {
/** Returns true if the device should use the split notification shade. */
@Deprecated(
- message = "This is deprecated, please use ShadeInteractor#shadeMode instead",
+ message = "This is deprecated, please use ShadeModeInteractor#shadeMode instead",
replaceWith =
ReplaceWith(
- "shadeInteractor.shadeMode",
- "com.android.systemui.shade.domain.interactor.ShadeInteractor",
+ "shadeModeInteractor.shadeMode",
+ "com.android.systemui.shade.domain.interactor.ShadeModeInteractor",
),
)
fun shouldUseSplitNotificationShade(resources: Resources): Boolean
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
index 0ec996be72de..9b4902a9e7b2 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
@@ -6,5 +6,4 @@ madym@google.com
mgalhardo@google.com
petrcermak@google.com
stevenckng@google.com
-tkachenkoi@google.com
-vanjan@google.com \ No newline at end of file
+vanjan@google.com
diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml
new file mode 100644
index 000000000000..eb3ba82b043b
--- /dev/null
+++ b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ViewFlipper
+ android:id="@+id/flipper"
+ android:layout_width="match_parent"
+ android:layout_height="400dp"
+ android:flipInterval="1000"
+ />
+
+</FrameLayout> \ No newline at end of file
diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml
new file mode 100644
index 000000000000..e2a00bd845cd
--- /dev/null
+++ b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/imageview"
+ android:layout_width="match_parent"
+ android:layout_height="400dp" /> \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java
new file mode 100644
index 000000000000..09fa3871f6e3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.statusbar.notification.row;
+
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.platform.test.annotations.DisableFlags;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.notification.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NotificationCustomContentMemoryVerifierFlagDisabledTest extends SysuiTestCase {
+
+ @Rule
+ public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule();
+
+ @Test
+ @DisableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION)
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS
+ })
+ public void requiresImageViewMemorySizeCheck_flagDisabled_returnsFalse() {
+ NotificationEntry entry = buildAcceptableNotificationEntry(mContext);
+ assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+ .isFalse();
+ }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java
new file mode 100644
index 000000000000..1cadb3c0a909
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java
@@ -0,0 +1,285 @@
+/*
+ * 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.statusbar.notification.row;
+
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotification;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildOversizedNotification;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildWarningSizedNotification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification;
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.platform.test.annotations.EnableFlags;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.RemoteViews;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.notification.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@EnableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION)
+public class NotificationCustomContentMemoryVerifierTest extends SysuiTestCase {
+
+ private static final String AUTHORITY = "notification.memory.test.authority";
+ private static final Uri TEST_URI = new Uri.Builder()
+ .scheme("content")
+ .authority(AUTHORITY)
+ .path("path")
+ .build();
+
+ @Rule
+ public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule();
+
+ @Before
+ public void setUp() {
+ TestImageContentProvider provider = new TestImageContentProvider(mContext);
+ mContext.getContentResolver().addProvider(AUTHORITY, provider);
+ provider.onCreate();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void requiresImageViewMemorySizeCheck_customViewNotification_returnsTrue() {
+ NotificationEntry entry =
+ buildAcceptableNotificationEntry(
+ mContext);
+ assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+ .isTrue();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void requiresImageViewMemorySizeCheck_plainNotification_returnsFalse() {
+ Notification notification =
+ new Notification.Builder(mContext, "ChannelId")
+ .setContentTitle("Just a notification")
+ .setContentText("Yep")
+ .build();
+ NotificationEntry entry = new NotificationEntryBuilder().setNotification(
+ notification).build();
+ assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+ .isFalse();
+ }
+
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void satisfiesMemoryLimits_smallNotification_returnsTrue() {
+ Notification.Builder notification =
+ buildAcceptableNotification(mContext,
+ TEST_URI);
+ NotificationEntry entry = toEntry(notification);
+ View inflatedView = inflateNotification(notification);
+ assertThat(
+ NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+ )
+ .isTrue();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void satisfiesMemoryLimits_oversizedNotification_returnsFalse() {
+ Notification.Builder notification =
+ buildOversizedNotification(mContext,
+ TEST_URI);
+ NotificationEntry entry = toEntry(notification);
+ View inflatedView = inflateNotification(notification);
+ assertThat(
+ NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+ ).isFalse();
+ }
+
+ @Test
+ @DisableCompatChanges(
+ {NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}
+ )
+ public void satisfiesMemoryLimits_oversizedNotification_compatDisabled_returnsTrue() {
+ Notification.Builder notification =
+ buildOversizedNotification(mContext,
+ TEST_URI);
+ NotificationEntry entry = toEntry(notification);
+ View inflatedView = inflateNotification(notification);
+ assertThat(
+ NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+ ).isTrue();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void satisfiesMemoryLimits_warningSizedNotification_returnsTrue() {
+ Notification.Builder notification =
+ buildWarningSizedNotification(mContext,
+ TEST_URI);
+ NotificationEntry entry = toEntry(notification);
+ View inflatedView = inflateNotification(notification);
+ assertThat(
+ NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+ )
+ .isTrue();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void satisfiesMemoryLimits_viewWithoutCustomNotificationRoot_returnsTrue() {
+ NotificationEntry entry = new NotificationEntryBuilder().build();
+ View view = new FrameLayout(mContext);
+ assertThat(NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry))
+ .isTrue();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void computeViewHierarchyImageViewSize_smallNotification_returnsSensibleValue() {
+ Notification.Builder notification =
+ buildAcceptableNotification(mContext,
+ TEST_URI);
+ // This should have a size of a single image
+ View inflatedView = inflateNotification(notification);
+ assertThat(
+ NotificationCustomContentMemoryVerifier.computeViewHierarchyImageViewSize(
+ inflatedView))
+ .isGreaterThan(170000);
+ }
+
+ private View inflateNotification(Notification.Builder builder) {
+ RemoteViews remoteViews = builder.createBigContentView();
+ return remoteViews.apply(mContext, new FrameLayout(mContext));
+ }
+
+ private NotificationEntry toEntry(Notification.Builder builder) {
+ return new NotificationEntryBuilder().setNotification(builder.build())
+ .setUid(Process.myUid()).build();
+ }
+
+
+ /** This provider serves the images for inflation. */
+ class TestImageContentProvider extends ContentProvider {
+
+ TestImageContentProvider(Context context) {
+ ProviderInfo info = new ProviderInfo();
+ info.authority = AUTHORITY;
+ info.exported = true;
+ attachInfoForTesting(context, info);
+ setAuthorities(AUTHORITY);
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) {
+ return getContext().getResources().openRawResourceFd(
+ NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE())
+ .getParcelFileDescriptor();
+ }
+
+ @Override
+ public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) {
+ return getContext().getResources().openRawResourceFd(
+ NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE());
+ }
+
+ @Override
+ public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
+ CancellationSignal signal) throws FileNotFoundException {
+ return openTypedAssetFile(uri, mimeTypeFilter, opts);
+ }
+
+ @Override
+ public int delete(Uri uri, Bundle extras) {
+ return 0;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "image/png";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values, Bundle extras) {
+ return super.insert(uri, values, extras);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, Bundle queryArgs,
+ CancellationSignal cancellationSignal) {
+ return super.query(uri, projection, queryArgs, cancellationSignal);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+ }
+
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt
new file mode 100644
index 000000000000..ca4f24da3c08
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+@file:JvmName("NotificationCustomContentNotificationBuilder")
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.Notification.DecoratedCustomViewStyle
+import android.content.Context
+import android.graphics.drawable.BitmapDrawable
+import android.net.Uri
+import android.os.Process
+import android.widget.RemoteViews
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.tests.R
+import org.hamcrest.Matchers.lessThan
+import org.junit.Assume.assumeThat
+
+public val DRAWABLE_IMAGE_RESOURCE = R.drawable.romainguy_rockaway
+
+fun buildAcceptableNotificationEntry(context: Context): NotificationEntry {
+ return NotificationEntryBuilder()
+ .setNotification(buildAcceptableNotification(context, null).build())
+ .setUid(Process.myUid())
+ .build()
+}
+
+fun buildAcceptableNotification(context: Context, uri: Uri?): Notification.Builder =
+ buildNotification(context, uri, 1)
+
+fun buildOversizedNotification(context: Context, uri: Uri): Notification.Builder {
+ val numImagesForOversize =
+ (NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context) /
+ drawableSizeOnDevice(context)) + 2
+ return buildNotification(context, uri, numImagesForOversize)
+}
+
+fun buildWarningSizedNotification(context: Context, uri: Uri): Notification.Builder {
+ val numImagesForOversize =
+ (NotificationCustomContentMemoryVerifier.getWarnViewSizeLimit(context) /
+ drawableSizeOnDevice(context)) + 1
+ // The size needs to be smaller than outright stripping size.
+ assumeThat(
+ numImagesForOversize * drawableSizeOnDevice(context),
+ lessThan(NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context)),
+ )
+ return buildNotification(context, uri, numImagesForOversize)
+}
+
+fun buildNotification(context: Context, uri: Uri?, numImages: Int): Notification.Builder {
+ val remoteViews = RemoteViews(context.packageName, R.layout.custom_view_flipper)
+ repeat(numImages) { i ->
+ val remoteViewFlipperImageView =
+ RemoteViews(context.packageName, R.layout.custom_view_flipper_image)
+
+ if (uri == null) {
+ remoteViewFlipperImageView.setImageViewResource(
+ R.id.imageview,
+ R.drawable.romainguy_rockaway,
+ )
+ } else {
+ val imageUri = uri.buildUpon().appendPath(i.toString()).build()
+ remoteViewFlipperImageView.setImageViewUri(R.id.imageview, imageUri)
+ }
+ remoteViews.addView(R.id.flipper, remoteViewFlipperImageView)
+ }
+
+ return Notification.Builder(context, "ChannelId")
+ .setSmallIcon(android.R.drawable.ic_info)
+ .setStyle(DecoratedCustomViewStyle())
+ .setCustomContentView(remoteViews)
+ .setCustomBigContentView(remoteViews)
+ .setContentTitle("This is a remote view!")
+}
+
+fun drawableSizeOnDevice(context: Context): Int {
+ val drawable = context.resources.getDrawable(DRAWABLE_IMAGE_RESOURCE)
+ return (drawable as BitmapDrawable).bitmap.allocationByteCount
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 825e0143800b..f0350acd83ca 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -20,7 +20,6 @@ import com.android.systemui.scene.ui.FakeOverlay
import com.android.systemui.scene.ui.composable.ConstantSceneContainerTransitionsBuilder
import com.android.systemui.scene.ui.viewmodel.SceneContainerHapticsViewModel
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
@@ -99,7 +98,6 @@ val Kosmos.sceneContainerViewModelFactory by Fixture {
powerInteractor = powerInteractor,
shadeModeInteractor = shadeModeInteractor,
remoteInputInteractor = remoteInputInteractor,
- splitEdgeDetector = splitEdgeDetector,
logger = sceneLogger,
hapticsViewModelFactory = sceneContainerHapticsViewModelFactory,
view = view,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
deleted file mode 100644
index e0b529261c4d..000000000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
+++ /dev/null
@@ -1,29 +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.scene.ui.viewmodel
-
-import androidx.compose.ui.unit.dp
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.shade.domain.interactor.shadeInteractor
-
-var Kosmos.splitEdgeDetector: SplitEdgeDetector by
- Kosmos.Fixture {
- SplitEdgeDetector(
- topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
- edgeSize = 40.dp,
- )
- }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
index b40e1e7ab33b..6b641934bc44 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.data.repository
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
/**
* Make the repository hold [count] active notifications for testing. The keys of the notifications
@@ -37,3 +38,56 @@ fun ActiveNotificationListRepository.setActiveNotifs(count: Int) {
}
.build()
}
+
+/**
+ * Adds the given notification to the repository while *maintaining any notifications already
+ * present*. [notif] will be ranked highest.
+ */
+fun ActiveNotificationListRepository.addNotif(notif: ActiveNotificationModel) {
+ val currentNotifications = this.activeNotifications.value.individuals
+ this.activeNotifications.value =
+ ActiveNotificationsStore.Builder()
+ .apply {
+ addIndividualNotif(notif)
+ currentNotifications.forEach {
+ if (it.key != notif.key) {
+ addIndividualNotif(it.value)
+ }
+ }
+ }
+ .build()
+}
+
+/**
+ * Adds the given notification to the repository while *maintaining any notifications already
+ * present*. [notifs] will be ranked higher than existing notifs.
+ */
+fun ActiveNotificationListRepository.addNotifs(notifs: List<ActiveNotificationModel>) {
+ val currentNotifications = this.activeNotifications.value.individuals
+ val newKeys = notifs.map { it.key }
+ this.activeNotifications.value =
+ ActiveNotificationsStore.Builder()
+ .apply {
+ notifs.forEach { addIndividualNotif(it) }
+ currentNotifications.forEach {
+ if (!newKeys.contains(it.key)) {
+ addIndividualNotif(it.value)
+ }
+ }
+ }
+ .build()
+}
+
+fun ActiveNotificationListRepository.removeNotif(keyToRemove: String) {
+ val currentNotifications = this.activeNotifications.value.individuals
+ this.activeNotifications.value =
+ ActiveNotificationsStore.Builder()
+ .apply {
+ currentNotifications.forEach {
+ if (it.key != keyToRemove) {
+ addIndividualNotif(it.value)
+ }
+ }
+ }
+ .build()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt
index 2057b849c069..c7380c91f703 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt
@@ -21,6 +21,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.statusbar.lockscreenShadeTransitionController
val Kosmos.notificationShelfInteractor by Fixture {
@@ -28,6 +29,7 @@ val Kosmos.notificationShelfInteractor by Fixture {
keyguardRepository = keyguardRepository,
deviceEntryFaceAuthRepository = deviceEntryFaceAuthRepository,
powerInteractor = powerInteractor,
+ shadeModeInteractor = shadeModeInteractor,
keyguardTransitionController = lockscreenShadeTransitionController,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
index 7bcedcaa99d1..d09d010cba2e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
@@ -21,8 +21,9 @@ import com.android.systemui.kosmos.Kosmos
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
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
+import com.android.systemui.statusbar.notification.data.repository.addNotif
+import com.android.systemui.statusbar.notification.data.repository.removeNotif
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.shared.CallType
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
@@ -49,51 +50,47 @@ fun inCallModel(
object OngoingCallTestHelper {
/**
- * Sets the call state to be no call, and does it correctly based on whether
- * [StatusBarChipsModernization] is enabled or not.
+ * Removes any ongoing call state and removes any call notification associated with [key]. Does
+ * it correctly based on whether [StatusBarChipsModernization] is enabled or not.
+ *
+ * @param key the notification key associated with the call notification.
*/
- fun setNoCallState(kosmos: Kosmos) {
+ fun Kosmos.removeOngoingCallState(key: String) {
if (StatusBarChipsModernization.isEnabled) {
- // TODO(b/372657935): Maybe don't clear *all* notifications
- kosmos.activeNotificationListRepository.activeNotifications.value =
- ActiveNotificationsStore()
+ activeNotificationListRepository.removeNotif(key)
} else {
- kosmos.ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall)
+ ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall)
}
}
/**
- * Sets the ongoing call state correctly based on whether [StatusBarChipsModernization] is
- * enabled or not.
+ * Sets SysUI to have an ongoing call state. Does it correctly based on whether
+ * [StatusBarChipsModernization] is enabled or not.
+ *
+ * @param key the notification key to be associated with the call notification
*/
- fun setOngoingCallState(
- kosmos: Kosmos,
- startTimeMs: Long = 1000L,
+ fun Kosmos.addOngoingCallState(
key: String = "notif",
+ startTimeMs: Long = 1000L,
statusBarChipIconView: StatusBarIconView? = createStatusBarIconViewOrNull(),
promotedContent: PromotedNotificationContentModel? = null,
contentIntent: PendingIntent? = null,
uid: Int = DEFAULT_UID,
) {
if (StatusBarChipsModernization.isEnabled) {
- kosmos.activeNotificationListRepository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = key,
- whenTime = startTimeMs,
- callType = CallType.Ongoing,
- statusBarChipIcon = statusBarChipIconView,
- contentIntent = contentIntent,
- promotedContent = promotedContent,
- uid = uid,
- )
- )
- }
- .build()
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = key,
+ whenTime = startTimeMs,
+ callType = CallType.Ongoing,
+ statusBarChipIcon = statusBarChipIconView,
+ contentIntent = contentIntent,
+ promotedContent = promotedContent,
+ uid = uid,
+ )
+ )
} else {
- kosmos.ongoingCallRepository.setOngoingCallState(
+ ongoingCallRepository.setOngoingCallState(
inCallModel(
startTimeMs = startTimeMs,
notificationIcon = statusBarChipIconView,
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index e8dddcb537cd..529a564ea607 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -100,6 +100,13 @@ flag {
}
flag {
+ name: "enable_low_vision_generic_feedback"
+ namespace: "accessibility"
+ description: "Use generic feedback for low vision."
+ bug: "393981463"
+}
+
+flag {
name: "enable_low_vision_hats"
namespace: "accessibility"
description: "Use HaTS for low vision feedback."
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index 8e448676c214..db8441d2424b 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -510,6 +510,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
return mMetaState;
}
+ @VisibleForTesting
+ boolean getIsActiveForTesting() {
+ return mActive;
+ }
+
/**
* Updates delay that should be used when scheduling clicks. The delay will be used only for
* clicks scheduled after this point (pending click tasks are not affected).
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index b75728e9f97c..f03e8c713228 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -110,6 +110,7 @@ import android.util.SparseIntArray;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;
+import android.window.DisplayWindowPolicyController;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -1411,8 +1412,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
return mirroredDisplayId == Display.INVALID_DISPLAY ? displayId : mirroredDisplayId;
}
- @GuardedBy("mVirtualDeviceLock")
- private GenericWindowPolicyController createWindowPolicyControllerLocked(
+ private GenericWindowPolicyController createWindowPolicyController(
@NonNull Set<String> displayCategories) {
final boolean activityLaunchAllowedByDefault =
getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT;
@@ -1421,28 +1421,28 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
final boolean showTasksInHostDeviceRecents =
getDevicePolicy(POLICY_TYPE_RECENTS) == DEVICE_POLICY_DEFAULT;
- if (mActivityListenerAdapter == null) {
- mActivityListenerAdapter = new GwpcActivityListener();
- }
-
- final GenericWindowPolicyController gwpc = new GenericWindowPolicyController(
- WindowManager.LayoutParams.FLAG_SECURE,
- WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
- mAttributionSource,
- getAllowedUserHandles(),
- activityLaunchAllowedByDefault,
- mActivityPolicyExemptions,
- mActivityPolicyPackageExemptions,
- crossTaskNavigationAllowedByDefault,
- /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault
- ? mParams.getBlockedCrossTaskNavigations()
- : mParams.getAllowedCrossTaskNavigations(),
- mActivityListenerAdapter,
- displayCategories,
- showTasksInHostDeviceRecents,
- mParams.getHomeComponent());
- gwpc.registerRunningAppsChangedListener(/* listener= */ this);
- return gwpc;
+ synchronized (mVirtualDeviceLock) {
+ if (mActivityListenerAdapter == null) {
+ mActivityListenerAdapter = new GwpcActivityListener();
+ }
+
+ return new GenericWindowPolicyController(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
+ mAttributionSource,
+ getAllowedUserHandles(),
+ activityLaunchAllowedByDefault,
+ mActivityPolicyExemptions,
+ mActivityPolicyPackageExemptions,
+ crossTaskNavigationAllowedByDefault,
+ /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault
+ ? mParams.getBlockedCrossTaskNavigations()
+ : mParams.getAllowedCrossTaskNavigations(),
+ mActivityListenerAdapter,
+ displayCategories,
+ showTasksInHostDeviceRecents,
+ mParams.getHomeComponent());
+ }
}
@Override // Binder call
@@ -1450,55 +1450,54 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
@NonNull IVirtualDisplayCallback callback) {
checkCallerIsDeviceOwner();
- int displayId;
- boolean showPointer;
- boolean isTrustedDisplay;
- GenericWindowPolicyController gwpc;
- synchronized (mVirtualDeviceLock) {
- gwpc = createWindowPolicyControllerLocked(virtualDisplayConfig.getDisplayCategories());
- displayId = mDisplayManagerInternal.createVirtualDisplay(virtualDisplayConfig,
+ final boolean isTrustedDisplay =
+ (virtualDisplayConfig.getFlags() & DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED)
+ == DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
+ if (!isTrustedDisplay && getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) {
+ throw new SecurityException(
+ "All displays must be trusted for devices with custom clipboard policy.");
+ }
+
+ GenericWindowPolicyController gwpc =
+ createWindowPolicyController(virtualDisplayConfig.getDisplayCategories());
+
+ // Create the display outside of the lock to avoid deadlock. DisplayManagerService will
+ // acquire the global WM lock while creating the display. At the same time, WM may query
+ // VDM and this virtual device to get policies, display ownership, etc.
+ int displayId = mDisplayManagerInternal.createVirtualDisplay(virtualDisplayConfig,
callback, this, gwpc, mOwnerPackageName);
- boolean isMirrorDisplay =
- mDisplayManagerInternal.getDisplayIdToMirror(displayId)
- != Display.INVALID_DISPLAY;
- gwpc.setDisplayId(displayId, isMirrorDisplay);
- isTrustedDisplay =
- (mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
- == Display.FLAG_TRUSTED;
- if (!isTrustedDisplay
- && getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) {
- throw new SecurityException("All displays must be trusted for devices with "
- + "custom clipboard policy.");
- }
+ if (displayId == Display.INVALID_DISPLAY) {
+ return displayId;
+ }
- if (mVirtualDisplays.contains(displayId)) {
- gwpc.unregisterRunningAppsChangedListener(this);
- throw new IllegalStateException(
- "Virtual device already has a virtual display with ID " + displayId);
+ // DisplayManagerService will call onVirtualDisplayCreated() after the display is created,
+ // while holding its own lock to ensure that this device knows about the display before any
+ // other display listeners are notified about the display creation.
+ VirtualDisplayWrapper displayWrapper;
+ boolean showPointer;
+ synchronized (mVirtualDeviceLock) {
+ if (!mVirtualDisplays.contains(displayId)) {
+ throw new IllegalStateException("Virtual device was not notified about the "
+ + "creation of display with ID " + displayId);
}
-
- PowerManager.WakeLock wakeLock =
- isTrustedDisplay ? createAndAcquireWakeLockForDisplay(displayId) : null;
- mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock,
- isTrustedDisplay, isMirrorDisplay));
+ displayWrapper = mVirtualDisplays.get(displayId);
showPointer = mDefaultShowPointerIcon;
}
+ displayWrapper.acquireWakeLock();
+ gwpc.registerRunningAppsChangedListener(/* listener= */ this);
- final long token = Binder.clearCallingIdentity();
- try {
+ Binder.withCleanCallingIdentity(() -> {
mInputController.setMouseScalingEnabled(false, displayId);
mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false,
displayId);
- if (isTrustedDisplay) {
+ if (displayWrapper.isTrusted()) {
mInputController.setShowPointerIcon(showPointer, displayId);
mInputController.setDisplayImePolicy(displayId,
WindowManager.DISPLAY_IME_POLICY_LOCAL);
} else {
gwpc.setShowInHostDeviceRecents(true);
}
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ });
Counter.logIncrementWithUid(
"virtual_devices.value_virtual_display_created_count",
@@ -1506,7 +1505,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
return displayId;
}
- private PowerManager.WakeLock createAndAcquireWakeLockForDisplay(int displayId) {
+ private PowerManager.WakeLock createWakeLockForDisplay(int displayId) {
if (Flags.deviceAwareDisplayPower()) {
return null;
}
@@ -1516,7 +1515,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
PowerManager.WakeLock wakeLock = powerManager.newWakeLock(
PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
TAG + ":" + displayId, displayId);
- wakeLock.acquire();
return wakeLock;
} finally {
Binder.restoreCallingIdentity(token);
@@ -1561,17 +1559,47 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
return result;
}
+ /**
+ * DisplayManagerService is notifying this virtual device about the display creation. This
+ * should happen before the DisplayManagerInternal#createVirtualDisplay() call above
+ * returns.
+ * This is called while holding the DisplayManagerService lock, so no heavy-weight work must
+ * be done here and especially *** no calls to WindowManager! ***
+ */
+ public void onVirtualDisplayCreated(int displayId, IVirtualDisplayCallback callback,
+ DisplayWindowPolicyController dwpc) {
+ final boolean isMirrorDisplay =
+ mDisplayManagerInternal.getDisplayIdToMirror(displayId) != Display.INVALID_DISPLAY;
+ final boolean isTrustedDisplay =
+ (mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
+ == Display.FLAG_TRUSTED;
+
+ GenericWindowPolicyController gwpc = (GenericWindowPolicyController) dwpc;
+ gwpc.setDisplayId(displayId, isMirrorDisplay);
+ PowerManager.WakeLock wakeLock =
+ isTrustedDisplay ? createWakeLockForDisplay(displayId) : null;
+ synchronized (mVirtualDeviceLock) {
+ if (mVirtualDisplays.contains(displayId)) {
+ Slog.wtf(TAG, "Virtual device already has a virtual display with ID " + displayId);
+ return;
+ }
+ mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock,
+ isTrustedDisplay, isMirrorDisplay));
+ }
+ }
+
+ /**
+ * This is callback invoked by VirtualDeviceManagerService when VirtualDisplay was released
+ * by DisplayManager (most probably caused by someone calling VirtualDisplay.close()).
+ * At this point, the display is already released, but we still need to release the
+ * corresponding wakeLock and unregister the RunningAppsChangedListener from corresponding
+ * WindowPolicyController.
+ *
+ * Note that when the display is destroyed during VirtualDeviceImpl.close() call,
+ * this callback won't be invoked because the display is removed from
+ * VirtualDeviceManagerService before any resources are released.
+ */
void onVirtualDisplayRemoved(int displayId) {
- /* This is callback invoked by VirtualDeviceManagerService when VirtualDisplay was released
- * by DisplayManager (most probably caused by someone calling VirtualDisplay.close()).
- * At this point, the display is already released, but we still need to release the
- * corresponding wakeLock and unregister the RunningAppsChangedListener from corresponding
- * WindowPolicyController.
- *
- * Note that when the display is destroyed during VirtualDeviceImpl.close() call,
- * this callback won't be invoked because the display is removed from
- * VirtualDeviceManagerService before any resources are released.
- */
VirtualDisplayWrapper virtualDisplayWrapper;
synchronized (mVirtualDeviceLock) {
virtualDisplayWrapper = mVirtualDisplays.removeReturnOld(displayId);
@@ -1847,6 +1875,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
return mWindowPolicyController;
}
+ void acquireWakeLock() {
+ if (mWakeLock != null && !mWakeLock.isHeld()) {
+ mWakeLock.acquire();
+ }
+ }
+
void releaseWakeLock() {
if (mWakeLock != null && mWakeLock.isHeld()) {
mWakeLock.release();
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 8a0b85859b66..ff82ca00b840 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -40,7 +40,6 @@ import android.companion.virtual.IVirtualDeviceSoundEffectListener;
import android.companion.virtual.VirtualDevice;
import android.companion.virtual.VirtualDeviceManager;
import android.companion.virtual.VirtualDeviceParams;
-import android.companion.virtual.flags.Flags;
import android.companion.virtual.sensor.VirtualSensor;
import android.companion.virtualnative.IVirtualDeviceManagerNative;
import android.compat.annotation.ChangeId;
@@ -49,6 +48,7 @@ import android.content.AttributionSource;
import android.content.Context;
import android.content.Intent;
import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.IVirtualDisplayCallback;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
@@ -67,6 +67,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.widget.Toast;
+import android.window.DisplayWindowPolicyController;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
@@ -751,6 +752,16 @@ public class VirtualDeviceManagerService extends SystemService {
}
@Override
+ public void onVirtualDisplayCreated(IVirtualDevice virtualDevice, int displayId,
+ IVirtualDisplayCallback callback, DisplayWindowPolicyController dwpc) {
+ VirtualDeviceImpl virtualDeviceImpl = getVirtualDeviceForId(
+ ((VirtualDeviceImpl) virtualDevice).getDeviceId());
+ if (virtualDeviceImpl != null) {
+ virtualDeviceImpl.onVirtualDisplayCreated(displayId, callback, dwpc);
+ }
+ }
+
+ @Override
public void onVirtualDisplayRemoved(IVirtualDevice virtualDevice, int displayId) {
VirtualDeviceImpl virtualDeviceImpl = getVirtualDeviceForId(
((VirtualDeviceImpl) virtualDevice).getDeviceId());
diff --git a/services/core/Android.bp b/services/core/Android.bp
index f98076ab41e4..00db11e72dd9 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -292,9 +292,18 @@ java_genrule {
out: ["services.core.priorityboosted.jar"],
}
+java_genrule_combiner {
+ name: "services.core.combined",
+ static_libs: ["services.core.priorityboosted"],
+ headers: ["services.core.unboosted"],
+}
+
java_library {
name: "services.core",
- static_libs: ["services.core.priorityboosted"],
+ static_libs: select(release_flag("RELEASE_SERVICES_JAVA_GENRULE_COMBINER"), {
+ true: ["services.core.combined"],
+ default: ["services.core.priorityboosted"],
+ }),
}
java_library_host {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 2219ecc77167..6f79f7073b89 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -66,7 +66,6 @@ import static com.android.media.audio.Flags.equalScoLeaVcIndexRange;
import static com.android.media.audio.Flags.replaceStreamBtSco;
import static com.android.media.audio.Flags.ringMyCar;
import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
-import static com.android.media.audio.Flags.vgsVssSyncMuteOrder;
import static com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl;
import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
import static com.android.server.utils.EventLogger.Event.ALOGE;
@@ -4977,9 +4976,8 @@ public class AudioService extends IAudioService.Stub
+ roForegroundAudioControl());
pw.println("\tandroid.media.audio.scoManagedByAudio:"
+ scoManagedByAudio());
- pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:"
- + vgsVssSyncMuteOrder());
pw.println("\tcom.android.media.audio.absVolumeIndexFix - EOL");
+ pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder - EOL");
pw.println("\tcom.android.media.audio.replaceStreamBtSco:"
+ replaceStreamBtSco());
pw.println("\tcom.android.media.audio.equalScoLeaVcIndexRange:"
@@ -9010,22 +9008,13 @@ public class AudioService extends IAudioService.Stub
synced = true;
continue;
}
- if (vgsVssSyncMuteOrder()) {
- if ((isMuted() != streamMuted) && isVssMuteBijective(
- stream)) {
- vss.mute(isMuted(), "VGS.applyAllVolumes#1");
- }
+ if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) {
+ vss.mute(isMuted(), "VGS.applyAllVolumes#1");
}
if (indexForStream != index) {
vss.setIndex(index * 10, device,
caller, true /*hasModifyAudioSettings*/);
}
- if (!vgsVssSyncMuteOrder()) {
- if ((isMuted() != streamMuted) && isVssMuteBijective(
- stream)) {
- vss.mute(isMuted(), "VGS.applyAllVolumes#1");
- }
- }
}
}
}
diff --git a/services/core/java/com/android/server/backup/InputBackupHelper.java b/services/core/java/com/android/server/backup/InputBackupHelper.java
new file mode 100644
index 000000000000..af9606c6e70f
--- /dev/null
+++ b/services/core/java/com/android/server/backup/InputBackupHelper.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup;
+
+import static com.android.server.input.InputManagerInternal.BACKUP_CATEGORY_INPUT_GESTURES;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.backup.BlobBackupHelper;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+import com.android.server.input.InputManagerInternal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class InputBackupHelper extends BlobBackupHelper {
+ private static final String TAG = "InputBackupHelper"; // must be < 23 chars
+
+ // Current version of the blob schema
+ private static final int BLOB_VERSION = 1;
+
+ // Key under which the payload blob is stored
+ private static final String KEY_INPUT_GESTURES = "input_gestures";
+
+ private final @UserIdInt int mUserId;
+
+ private final @NonNull InputManagerInternal mInputManagerInternal;
+
+ public InputBackupHelper(int userId) {
+ super(BLOB_VERSION, KEY_INPUT_GESTURES);
+ mUserId = userId;
+ mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
+ }
+
+ @Override
+ protected byte[] getBackupPayload(String key) {
+ Map<Integer, byte[]> payloads;
+ try {
+ payloads = mInputManagerInternal.getBackupPayload(mUserId);
+ } catch (Exception exception) {
+ Slog.e(TAG, "Failed to get backup payload for input gestures", exception);
+ return null;
+ }
+
+ if (KEY_INPUT_GESTURES.equals(key)) {
+ return payloads.getOrDefault(BACKUP_CATEGORY_INPUT_GESTURES, null);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void applyRestoredPayload(String key, byte[] payload) {
+ Map<Integer, byte[]> payloads = new HashMap<>();
+ if (KEY_INPUT_GESTURES.equals(key)) {
+ payloads.put(BACKUP_CATEGORY_INPUT_GESTURES, payload);
+ }
+
+ try {
+ mInputManagerInternal.applyBackupPayload(payloads, mUserId);
+ } catch (Exception exception) {
+ Slog.e(TAG, "Failed to apply input backup payload", exception);
+ }
+ }
+
+}
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index 677e0c055455..b11267ef8634 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -68,6 +68,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
private static final String COMPANION_HELPER = "companion";
private static final String SYSTEM_GENDER_HELPER = "system_gender";
private static final String DISPLAY_HELPER = "display";
+ private static final String INPUT_HELPER = "input";
// These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME
// are also used in the full-backup file format, so must not change unless steps are
@@ -112,7 +113,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
private static final Set<String> sEligibleHelpersForNonSystemUser =
SetUtils.union(sEligibleHelpersForProfileUser,
Sets.newArraySet(ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER, PREFERRED_HELPER,
- SHORTCUT_MANAGER_HELPER));
+ SHORTCUT_MANAGER_HELPER, INPUT_HELPER));
private int mUserId = UserHandle.USER_SYSTEM;
private boolean mIsProfileUser = false;
@@ -149,6 +150,9 @@ public class SystemBackupAgent extends BackupAgentHelper {
addHelperIfEligibleForUser(SYSTEM_GENDER_HELPER,
new SystemGrammaticalGenderBackupHelper(mUserId));
addHelperIfEligibleForUser(DISPLAY_HELPER, new DisplayBackupHelper(mUserId));
+ if (com.android.hardware.input.Flags.enableBackupAndRestoreForInputGestures()) {
+ addHelperIfEligibleForUser(INPUT_HELPER, new InputBackupHelper(mUserId));
+ }
}
@Override
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index 471b7b4ddfc8..d412277d2605 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -24,8 +24,10 @@ import android.companion.virtual.VirtualDeviceManager;
import android.companion.virtual.VirtualDeviceParams;
import android.companion.virtual.sensor.VirtualSensor;
import android.content.Context;
+import android.hardware.display.IVirtualDisplayCallback;
import android.os.LocaleList;
import android.util.ArraySet;
+import android.window.DisplayWindowPolicyController;
import java.util.Set;
import java.util.function.Consumer;
@@ -104,6 +106,17 @@ public abstract class VirtualDeviceManagerInternal {
public abstract @NonNull ArraySet<Integer> getDeviceIdsForUid(int uid);
/**
+ * Notifies that a virtual display was created.
+ *
+ * @param virtualDevice The virtual device that owns the virtual display.
+ * @param displayId The display id of the created virtual display.
+ * @param callback The callback of the virtual display.
+ * @param dwpc The DisplayWindowPolicyController of the created virtual display.
+ */
+ public abstract void onVirtualDisplayCreated(IVirtualDevice virtualDevice, int displayId,
+ IVirtualDisplayCallback callback, DisplayWindowPolicyController dwpc);
+
+ /**
* Notifies that a virtual display is removed.
*
* @param virtualDevice The virtual device where the virtual display located.
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index e83efc573ea8..854b0dd7676b 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -2041,6 +2041,7 @@ public final class DisplayManagerService extends SystemService {
packageName,
displayUniqueId,
virtualDevice,
+ dwpc,
surface,
flags,
virtualDisplayConfig);
@@ -2135,6 +2136,7 @@ public final class DisplayManagerService extends SystemService {
String packageName,
String uniqueId,
IVirtualDevice virtualDevice,
+ DisplayWindowPolicyController dwpc,
Surface surface,
int flags,
VirtualDisplayConfig virtualDisplayConfig) {
@@ -2188,6 +2190,16 @@ public final class DisplayManagerService extends SystemService {
final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
if (display != null) {
+ // Notify the virtual device that the display has been created. This needs to be called
+ // in this locked section before the repository had the chance to notify any listeners
+ // to ensure that the device is aware of the new display before others know about it.
+ if (virtualDevice != null) {
+ final VirtualDeviceManagerInternal vdm =
+ getLocalService(VirtualDeviceManagerInternal.class);
+ vdm.onVirtualDisplayCreated(
+ virtualDevice, display.getDisplayIdLocked(), callback, dwpc);
+ }
+
return display.getDisplayIdLocked();
}
diff --git a/services/core/java/com/android/server/input/InputDataStore.java b/services/core/java/com/android/server/input/InputDataStore.java
index e8f21fe8fb74..834f8154240e 100644
--- a/services/core/java/com/android/server/input/InputDataStore.java
+++ b/services/core/java/com/android/server/input/InputDataStore.java
@@ -125,8 +125,20 @@ public final class InputDataStore {
}
}
- @VisibleForTesting
- List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded)
+ /**
+ * Parses the given input stream and returns the list of {@link InputGestureData} objects.
+ * This parsing happens on a best effort basis. If invalid data exists in the given payload
+ * it will be skipped. An example of this would be a keycode that does not exist in the
+ * present version of Android. If the payload is malformed, instead this will throw an
+ * exception and require the caller to handel this appropriately for its situation.
+ *
+ * @param stream stream of the input payload of XML data
+ * @param utf8Encoded whether or not the input data is UTF-8 encoded
+ * @return list of {@link InputGestureData} objects pulled from the payload
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ public List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded)
throws XmlPullParserException, IOException {
List<InputGestureData> inputGestureDataList = new ArrayList<>();
TypedXmlPullParser parser;
@@ -153,6 +165,31 @@ public final class InputDataStore {
return inputGestureDataList;
}
+ /**
+ * Serializes the given list of {@link InputGestureData} objects to XML in the provided output
+ * stream.
+ *
+ * @param stream output stream to put serialized data.
+ * @param utf8Encoded whether or not to encode the serialized data in UTF-8 format.
+ * @param inputGestureDataList the list of {@link InputGestureData} objects to serialize.
+ */
+ public void writeInputGestureXml(OutputStream stream, boolean utf8Encoded,
+ List<InputGestureData> inputGestureDataList) throws IOException {
+ final TypedXmlSerializer serializer;
+ if (utf8Encoded) {
+ serializer = Xml.newFastSerializer();
+ serializer.setOutput(stream, StandardCharsets.UTF_8.name());
+ } else {
+ serializer = Xml.resolveSerializer(stream);
+ }
+
+ serializer.startDocument(null, true);
+ serializer.startTag(null, TAG_ROOT);
+ writeInputGestureListToXml(serializer, inputGestureDataList);
+ serializer.endTag(null, TAG_ROOT);
+ serializer.endDocument();
+ }
+
private InputGestureData readInputGestureFromXml(TypedXmlPullParser parser)
throws XmlPullParserException, IOException, IllegalArgumentException {
InputGestureData.Builder builder = new InputGestureData.Builder();
@@ -239,24 +276,6 @@ public final class InputDataStore {
return inputGestureDataList;
}
- @VisibleForTesting
- void writeInputGestureXml(OutputStream stream, boolean utf8Encoded,
- List<InputGestureData> inputGestureDataList) throws IOException {
- final TypedXmlSerializer serializer;
- if (utf8Encoded) {
- serializer = Xml.newFastSerializer();
- serializer.setOutput(stream, StandardCharsets.UTF_8.name());
- } else {
- serializer = Xml.resolveSerializer(stream);
- }
-
- serializer.startDocument(null, true);
- serializer.startTag(null, TAG_ROOT);
- writeInputGestureListToXml(serializer, inputGestureDataList);
- serializer.endTag(null, TAG_ROOT);
- serializer.endDocument();
- }
-
private void writeInputGestureToXml(TypedXmlSerializer serializer,
InputGestureData inputGestureData) throws IOException {
serializer.startTag(null, TAG_INPUT_GESTURE);
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index d2486fe8bd66..87f693cc7291 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -16,6 +16,7 @@
package com.android.server.input;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -32,7 +33,11 @@ import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.policy.IShortcutService;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
import java.util.List;
+import java.util.Map;
/**
* Input manager local system service interface.
@@ -41,6 +46,15 @@ import java.util.List;
*/
public abstract class InputManagerInternal {
+ // Backup and restore information for custom input gestures.
+ public static final int BACKUP_CATEGORY_INPUT_GESTURES = 0;
+
+ // Backup and Restore categories for sending map of data back and forth to backup and restore
+ // infrastructure.
+ @IntDef({BACKUP_CATEGORY_INPUT_GESTURES})
+ public @interface BackupCategory {
+ }
+
/**
* Called by the display manager to set information about the displays as needed
* by the input system. The input system must copy this information to retain it.
@@ -312,4 +326,22 @@ public abstract class InputManagerInternal {
* @return true if setting power wakeup was successful.
*/
public abstract boolean setKernelWakeEnabled(int deviceId, boolean enabled);
+
+ /**
+ * Retrieves the input gestures backup payload data.
+ *
+ * @param userId the user ID of the backup data.
+ * @return byte array of UTF-8 encoded backup data.
+ */
+ public abstract Map<Integer, byte[]> getBackupPayload(int userId) throws IOException;
+
+ /**
+ * Applies the given UTF-8 encoded byte array payload to the given user's input data
+ * on a best effort basis.
+ *
+ * @param payload UTF-8 encoded map of byte arrays of restored data
+ * @param userId the user ID for which to apply the payload data
+ */
+ public abstract void applyBackupPayload(Map<Integer, byte[]> payload, int userId)
+ throws XmlPullParserException, IOException;
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 2ad5a1538da9..4a5f4a19893a 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -24,6 +24,7 @@ import static android.provider.DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT;
import static android.view.KeyEvent.KEYCODE_UNKNOWN;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static com.android.hardware.input.Flags.enableCustomizableInputGestures;
import static com.android.hardware.input.Flags.touchpadVisualizer;
import static com.android.hardware.input.Flags.keyEventActivityDetection;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
@@ -153,6 +154,8 @@ import com.android.server.wm.WindowManagerInternal;
import libcore.io.IoUtils;
+import org.xmlpull.v1.XmlPullParserException;
+
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
@@ -3805,6 +3808,26 @@ public class InputManagerService extends IInputManager.Stub
public boolean setKernelWakeEnabled(int deviceId, boolean enabled) {
return mNative.setKernelWakeEnabled(deviceId, enabled);
}
+
+ @Override
+ public Map<Integer, byte[]> getBackupPayload(int userId) throws IOException {
+ final Map<Integer, byte[]> payload = new HashMap<>();
+ if (enableCustomizableInputGestures()) {
+ payload.put(BACKUP_CATEGORY_INPUT_GESTURES,
+ mKeyGestureController.getInputGestureBackupPayload(userId));
+ }
+ return payload;
+ }
+
+ @Override
+ public void applyBackupPayload(Map<Integer, byte[]> payload, int userId)
+ throws XmlPullParserException, IOException {
+ if (enableCustomizableInputGestures() && payload.containsKey(
+ BACKUP_CATEGORY_INPUT_GESTURES)) {
+ mKeyGestureController.applyInputGesturesBackupPayload(
+ payload.get(BACKUP_CATEGORY_INPUT_GESTURES), userId);
+ }
+ }
}
@Override
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 41f58ae76a4d..5770a09e3b92 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -69,6 +69,11 @@ import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.policy.KeyCombinationManager;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.List;
@@ -1191,6 +1196,29 @@ final class KeyGestureController {
}
}
+ byte[] getInputGestureBackupPayload(int userId) throws IOException {
+ final List<InputGestureData> inputGestureDataList =
+ mInputGestureManager.getCustomInputGestures(userId, null);
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ synchronized (mInputDataStore) {
+ mInputDataStore.writeInputGestureXml(byteArrayOutputStream, true, inputGestureDataList);
+ }
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ void applyInputGesturesBackupPayload(byte[] payload, int userId)
+ throws XmlPullParserException, IOException {
+ final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
+ List<InputGestureData> inputGestureDataList;
+ synchronized (mInputDataStore) {
+ inputGestureDataList = mInputDataStore.readInputGesturesXml(byteArrayInputStream, true);
+ }
+ for (final InputGestureData inputGestureData : inputGestureDataList) {
+ mInputGestureManager.addCustomInputGesture(userId, inputGestureData);
+ }
+ mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget();
+ }
+
// A record of a registered key gesture event listener from one process.
private class KeyGestureEventListenerRecord implements IBinder.DeathRecipient {
public final int mPid;
diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
index 12495bb4f2cc..d7d0eb40af70 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
@@ -612,25 +612,23 @@ class GnssNetworkConnectivityHandler {
networkRequestBuilder.addCapability(getNetworkCapability(mAGpsType));
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
- if (com.android.internal.telephony.flags.Flags.satelliteInternet()) {
- // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network.
- TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
- if (telephonyManager != null) {
- ServiceState state = telephonyManager.getServiceState();
- if (state != null && state.isUsingNonTerrestrialNetwork()) {
- networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
- try {
- networkRequestBuilder.addTransportType(NetworkCapabilities
- .TRANSPORT_SATELLITE);
- networkRequestBuilder.removeCapability(NetworkCapabilities
- .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
- } catch (IllegalArgumentException ignored) {
- // In case TRANSPORT_SATELLITE or NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED
- // are not recognized, meaning an old connectivity module runs on new
- // android in which case no network with such capabilities will be brought
- // up, so it's safe to ignore the exception.
- // TODO: Can remove the try-catch in next quarter release.
- }
+ // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network.
+ TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
+ if (telephonyManager != null) {
+ ServiceState state = telephonyManager.getServiceState();
+ if (state != null && state.isUsingNonTerrestrialNetwork()) {
+ networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+ try {
+ networkRequestBuilder.addTransportType(NetworkCapabilities
+ .TRANSPORT_SATELLITE);
+ networkRequestBuilder.removeCapability(NetworkCapabilities
+ .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
+ } catch (IllegalArgumentException ignored) {
+ // In case TRANSPORT_SATELLITE or NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED
+ // are not recognized, meaning an old connectivity module runs on new
+ // android in which case no network with such capabilities will be brought
+ // up, so it's safe to ignore the exception.
+ // TODO: Can remove the try-catch in next quarter release.
}
}
}
diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java
index 576e5d5d0cd2..439b503c0c57 100644
--- a/services/core/java/com/android/server/wm/AppWarnings.java
+++ b/services/core/java/com/android/server/wm/AppWarnings.java
@@ -506,6 +506,10 @@ class AppWarnings {
context = new ContextThemeWrapper(context, context.getThemeResId()) {
@Override
public void startActivity(Intent intent) {
+ // PageSizeMismatch dialog stays on top of the browser even after opening link
+ // set broadcast to close the dialog when link has been clicked.
+ sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
super.startActivity(intent);
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
index 86bf203771ba..409b114100e7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
@@ -27,6 +27,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArraySet;
@@ -73,6 +74,7 @@ public class SystemBackupAgentTest {
}
@Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
public void onCreate_systemUser_addsAllHelpers() {
UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
when(mUserManagerMock.isProfile()).thenReturn(false);
@@ -94,10 +96,12 @@ public class SystemBackupAgentTest {
"app_gender",
"companion",
"system_gender",
- "display");
+ "display",
+ "input");
}
@Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
public void onCreate_systemUser_slicesDisabled_addsAllNonSlicesHelpers() {
UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
when(mUserManagerMock.isProfile()).thenReturn(false);
@@ -120,10 +124,12 @@ public class SystemBackupAgentTest {
"app_gender",
"companion",
"system_gender",
- "display");
+ "display",
+ "input");
}
@Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
public void onCreate_profileUser_addsProfileEligibleHelpers() {
UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID);
when(mUserManagerMock.isProfile()).thenReturn(true);
@@ -143,6 +149,7 @@ public class SystemBackupAgentTest {
}
@Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
public void onCreate_nonSystemUser_addsNonSystemEligibleHelpers() {
UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID);
when(mUserManagerMock.isProfile()).thenReturn(false);
@@ -162,7 +169,8 @@ public class SystemBackupAgentTest {
"companion",
"app_gender",
"system_gender",
- "display");
+ "display",
+ "input");
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index 457fde8d74d0..0227ef1d2dc0 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -85,7 +85,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_lazyInitClickScheduler() {
assertThat(mController.mClickScheduler).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mClickScheduler).isNotNull();
}
@@ -94,7 +94,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_nonMouseSource_notInitClickScheduler() {
assertThat(mController.mClickScheduler).isNull();
- injectFakeNonMouseActionDownEvent();
+ injectFakeNonMouseActionHoverMoveEvent();
assertThat(mController.mClickScheduler).isNull();
}
@@ -103,7 +103,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_lazyInitAutoclickSettingsObserver() {
assertThat(mController.mAutoclickSettingsObserver).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickSettingsObserver).isNotNull();
}
@@ -113,7 +113,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorScheduler() {
assertThat(mController.mAutoclickIndicatorScheduler).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickIndicatorScheduler).isNotNull();
}
@@ -123,7 +123,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOff_notInitAutoclickIndicatorScheduler() {
assertThat(mController.mAutoclickIndicatorScheduler).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickIndicatorScheduler).isNull();
}
@@ -133,7 +133,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorView() {
assertThat(mController.mAutoclickIndicatorView).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickIndicatorView).isNotNull();
}
@@ -143,7 +143,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOff_notInitAutoclickIndicatorView() {
assertThat(mController.mAutoclickIndicatorView).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickIndicatorView).isNull();
}
@@ -153,7 +153,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOn_lazyInitAutoclickTypePanelView() {
assertThat(mController.mAutoclickTypePanel).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickTypePanel).isNotNull();
}
@@ -163,7 +163,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOff_notInitAutoclickTypePanelView() {
assertThat(mController.mAutoclickTypePanel).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickTypePanel).isNull();
}
@@ -171,7 +171,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onMotionEvent_flagOn_addAutoclickIndicatorViewToWindowManager() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
verify(mMockWindowManager).addView(eq(mController.mAutoclickIndicatorView), any());
}
@@ -179,7 +179,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onDestroy_flagOn_removeAutoclickIndicatorViewToWindowManager() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
mController.onDestroy();
@@ -189,7 +189,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onDestroy_flagOn_removeAutoclickTypePanelViewToWindowManager() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
mController.mAutoclickTypePanel = mockAutoclickTypePanel;
@@ -200,7 +200,7 @@ public class AutoclickControllerTest {
@Test
public void onMotionEvent_initClickSchedulerDelayFromSetting() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
int delay =
Settings.Secure.getIntForUser(
@@ -214,7 +214,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onMotionEvent_flagOn_initCursorAreaSizeFromSetting() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
int size =
Settings.Secure.getIntForUser(
@@ -238,7 +238,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onKeyEvent_modifierKey_updateMetaStateWhenControllerNotNull() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
int metaState = KeyEvent.META_ALT_ON | KeyEvent.META_META_ON;
injectFakeKeyEvent(KeyEvent.KEYCODE_ALT_LEFT, metaState);
@@ -250,7 +250,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onKeyEvent_modifierKey_cancelAutoClickWhenAdditionalRegularKeyPresssed() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
injectFakeKeyEvent(KeyEvent.KEYCODE_J, KeyEvent.META_ALT_ON);
@@ -260,7 +260,7 @@ public class AutoclickControllerTest {
@Test
public void onDestroy_clearClickScheduler() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
mController.onDestroy();
@@ -269,7 +269,7 @@ public class AutoclickControllerTest {
@Test
public void onDestroy_clearAutoclickSettingsObserver() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
mController.onDestroy();
@@ -279,21 +279,61 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onDestroy_flagOn_clearAutoclickIndicatorScheduler() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
mController.onDestroy();
assertThat(mController.mAutoclickIndicatorScheduler).isNull();
}
- private void injectFakeMouseActionDownEvent() {
- MotionEvent event = getFakeMotionDownEvent();
+ @Test
+ @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onMotionEvent_hoverEnter_doesNotScheduleClick() {
+ injectFakeMouseActionHoverMoveEvent();
+
+ // Send hover enter event.
+ MotionEvent hoverEnter = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_ENTER,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverEnter.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverEnter, hoverEnter, /* policyFlags= */ 0);
+
+ // Verify there is no pending click.
+ assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse();
+ }
+
+ @Test
+ @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onMotionEvent_hoverMove_scheduleClick() {
+ injectFakeMouseActionHoverMoveEvent();
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+
+ // Verify there is a pending click.
+ assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
+ }
+
+ private void injectFakeMouseActionHoverMoveEvent() {
+ MotionEvent event = getFakeMotionHoverMoveEvent();
event.setSource(InputDevice.SOURCE_MOUSE);
mController.onMotionEvent(event, event, /* policyFlags= */ 0);
}
- private void injectFakeNonMouseActionDownEvent() {
- MotionEvent event = getFakeMotionDownEvent();
+ private void injectFakeNonMouseActionHoverMoveEvent() {
+ MotionEvent event = getFakeMotionHoverMoveEvent();
event.setSource(InputDevice.SOURCE_KEYBOARD);
mController.onMotionEvent(event, event, /* policyFlags= */ 0);
}
@@ -309,11 +349,11 @@ public class AutoclickControllerTest {
mController.onKeyEvent(keyEvent, /* policyFlags= */ 0);
}
- private MotionEvent getFakeMotionDownEvent() {
+ private MotionEvent getFakeMotionHoverMoveEvent() {
return MotionEvent.obtain(
/* downTime= */ 0,
/* eventTime= */ 0,
- /* action= */ MotionEvent.ACTION_DOWN,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
/* x= */ 0,
/* y= */ 0,
/* metaState= */ 0);
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index ffcb96120b19..ab7b4da269db 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -73,6 +73,7 @@ import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.hardware.Sensor;
+import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.display.IDisplayManager;
@@ -173,8 +174,7 @@ public class VirtualDeviceManagerServiceTest {
private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000;
private static final int VIRTUAL_DEVICE_ID_1 = 42;
private static final int VIRTUAL_DEVICE_ID_2 = 43;
- private static final VirtualDisplayConfig VIRTUAL_DISPLAY_CONFIG =
- new VirtualDisplayConfig.Builder("virtual_display", 640, 480, 400).build();
+
private static final VirtualDpadConfig DPAD_CONFIG =
new VirtualDpadConfig.Builder()
.setVendorId(VENDOR_ID)
@@ -284,7 +284,12 @@ public class VirtualDeviceManagerServiceTest {
private Intent createRestrictedActivityBlockedIntent(Set<String> displayCategories,
String targetDisplayCategory) {
when(mDisplayManagerInternalMock.createVirtualDisplay(any(), any(), any(), any(),
- eq(VIRTUAL_DEVICE_OWNER_PACKAGE))).thenReturn(DISPLAY_ID_1);
+ eq(VIRTUAL_DEVICE_OWNER_PACKAGE)))
+ .thenAnswer(inv -> {
+ mLocalService.onVirtualDisplayCreated(
+ mDeviceImpl, DISPLAY_ID_1, inv.getArgument(1), inv.getArgument(3));
+ return DISPLAY_ID_1;
+ });
VirtualDisplayConfig config = new VirtualDisplayConfig.Builder("display", 640, 480,
420).setDisplayCategories(displayCategories).build();
mDeviceImpl.createVirtualDisplay(config, mVirtualDisplayCallback);
@@ -997,8 +1002,7 @@ public class VirtualDeviceManagerServiceTest {
public void onVirtualDisplayCreatedLocked_duplicateCalls_onlyOneWakeLockIsAcquired()
throws RemoteException {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
- assertThrows(IllegalStateException.class,
- () -> addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1));
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
TestableLooper.get(this).processAllMessages();
verify(mIPowerManagerMock).acquireWakeLock(any(Binder.class), anyInt(),
nullable(String.class), nullable(String.class), nullable(WorkSource.class),
@@ -1871,8 +1875,6 @@ public class VirtualDeviceManagerServiceTest {
}
private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId, int flags) {
- when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback),
- eq(virtualDevice), any(), any())).thenReturn(displayId);
final String uniqueId = UNIQUE_ID + displayId;
doAnswer(inv -> {
final DisplayInfo displayInfo = new DisplayInfo();
@@ -1880,7 +1882,22 @@ public class VirtualDeviceManagerServiceTest {
displayInfo.flags = flags;
return displayInfo;
}).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId));
- virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback);
+
+ when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback),
+ eq(virtualDevice), any(), any())).thenAnswer(inv -> {
+ mLocalService.onVirtualDisplayCreated(
+ virtualDevice, displayId, mVirtualDisplayCallback, inv.getArgument(3));
+ return displayId;
+ });
+
+ final int virtualDisplayFlags = (flags & Display.FLAG_TRUSTED) == 0
+ ? 0
+ : DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
+ VirtualDisplayConfig virtualDisplayConfig =
+ new VirtualDisplayConfig.Builder("virtual_display", 640, 480, 400)
+ .setFlags(virtualDisplayFlags)
+ .build();
+ virtualDevice.createVirtualDisplay(virtualDisplayConfig, mVirtualDisplayCallback);
mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId);
}
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 37bdf6b8614d..de47f013271a 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -1438,6 +1438,58 @@ class KeyGestureControllerTests {
)
}
+ @Test
+ @Parameters(method = "customInputGesturesTestArguments")
+ fun testCustomKeyGestureRestoredFromBackup(test: TestData) {
+ val userId = 10
+ setupKeyGestureController()
+ val builder = InputGestureData.Builder()
+ .setKeyGestureType(test.expectedKeyGestureType)
+ .setTrigger(
+ InputGestureData.createKeyTrigger(
+ test.expectedKeys[0],
+ test.expectedModifierState
+ )
+ )
+ if (test.expectedAppLaunchData != null) {
+ builder.setAppLaunchData(test.expectedAppLaunchData)
+ }
+ val inputGestureData = builder.build()
+
+ keyGestureController.setCurrentUserId(userId)
+ testLooper.dispatchAll()
+ keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData)
+ testLooper.dispatchAll()
+ val backupData = keyGestureController.getInputGestureBackupPayload(userId)
+
+ // Delete the old data and reinitialize the controller simulating a "fresh" install.
+ tempFile.delete()
+ setupKeyGestureController()
+ keyGestureController.setCurrentUserId(userId)
+ testLooper.dispatchAll()
+
+ // Initially there should be no gestures registered.
+ var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+ assertEquals(
+ "Test: $test doesn't produce correct number of saved input gestures",
+ 0,
+ savedInputGestures.size
+ )
+
+ // After the restore, there should be the original gesture re-registered.
+ keyGestureController.applyInputGesturesBackupPayload(backupData, userId)
+ savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+ assertEquals(
+ "Test: $test doesn't produce correct number of saved input gestures",
+ 1,
+ savedInputGestures.size
+ )
+ assertEquals(
+ "Test: $test doesn't produce correct input gesture data", inputGestureData,
+ InputGestureData(savedInputGestures[0])
+ )
+ }
+
class TouchpadTestData(
val name: String,
val touchpadGestureType: Int,
@@ -1549,6 +1601,53 @@ class KeyGestureControllerTests {
)
}
+
+ @Test
+ @Parameters(method = "customTouchpadGesturesTestArguments")
+ fun testCustomTouchpadGesturesRestoredFromBackup(test: TouchpadTestData) {
+ val userId = 10
+ setupKeyGestureController()
+ val builder = InputGestureData.Builder()
+ .setKeyGestureType(test.expectedKeyGestureType)
+ .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType))
+ if (test.expectedAppLaunchData != null) {
+ builder.setAppLaunchData(test.expectedAppLaunchData)
+ }
+ val inputGestureData = builder.build()
+ keyGestureController.setCurrentUserId(userId)
+ testLooper.dispatchAll()
+ keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData)
+ testLooper.dispatchAll()
+ val backupData = keyGestureController.getInputGestureBackupPayload(userId)
+
+ // Delete the old data and reinitialize the controller simulating a "fresh" install.
+ tempFile.delete()
+ setupKeyGestureController()
+ keyGestureController.setCurrentUserId(userId)
+ testLooper.dispatchAll()
+
+ // Initially there should be no gestures registered.
+ var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+ assertEquals(
+ "Test: $test doesn't produce correct number of saved input gestures",
+ 0,
+ savedInputGestures.size
+ )
+
+ // After the restore, there should be the original gesture re-registered.
+ keyGestureController.applyInputGesturesBackupPayload(backupData, userId)
+ savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+ assertEquals(
+ "Test: $test doesn't produce correct number of saved input gestures",
+ 1,
+ savedInputGestures.size
+ )
+ assertEquals(
+ "Test: $test doesn't produce correct input gesture data", inputGestureData,
+ InputGestureData(savedInputGestures[0])
+ )
+ }
+
private fun testKeyGestureInternal(test: TestData) {
val handledEvents = mutableListOf<KeyGestureEvent>()
val handler = KeyGestureHandler { event, _ ->