diff options
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, _ -> |