diff options
79 files changed, 1921 insertions, 497 deletions
diff --git a/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java b/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java index 7a7250b9e910..8e3ed6d9931c 100644 --- a/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java +++ b/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java @@ -19,24 +19,27 @@ 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.benchmark.BenchmarkState; -import androidx.benchmark.junit4.BenchmarkRule; -import androidx.test.filters.SmallTest; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; -@SmallTest +@LargeTest +@RunWith(AndroidJUnit4.class) public class ViewConfigurationPerfTest { @Rule - public final BenchmarkRule mBenchmarkRule = new BenchmarkRule(); + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); private final Context mContext = getInstrumentation().getTargetContext(); @Test public void testGet_newViewConfiguration() { - final BenchmarkState state = mBenchmarkRule.getState(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { state.pauseTiming(); @@ -50,7 +53,7 @@ public class ViewConfigurationPerfTest { @Test public void testGet_cachedViewConfiguration() { - final BenchmarkState state = mBenchmarkRule.getState(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); // Do `get` once to make sure there's something cached. ViewConfiguration.get(mContext); @@ -58,4 +61,265 @@ 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/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index dc5974fde0b0..7e5c0fbe1ee1 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2944,7 +2944,11 @@ class ContextImpl extends Context { private void updateResourceOverlayConstraints() { if (mResources != null) { - mResources.getAssets().setOverlayConstraints(getDisplayId(), getDeviceId()); + // Avoid calling getDisplay() here, as it makes a binder call into + // DisplayManagerService if the relevant DisplayInfo is not cached in + // DisplayManagerGlobal. + int displayId = mDisplay != null ? mDisplay.getDisplayId() : Display.DEFAULT_DISPLAY; + mResources.getAssets().setOverlayConstraints(displayId, getDeviceId()); } } diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index 75c7e267d477..e43a5fce6cb7 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -138,7 +138,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private static native long nativeOpen(String path, int openFlags, String label, boolean enableTrace, boolean enableProfile, int lookasideSlotSize, int lookasideSlotCount); - private static native void nativeClose(long connectionPtr); + private static native void nativeClose(long connectionPtr, boolean fast); private static native void nativeRegisterCustomScalarFunction(long connectionPtr, String name, UnaryOperator<String> function); private static native void nativeRegisterCustomAggregateFunction(long connectionPtr, @@ -183,6 +183,11 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private static native long nativeChanges(long connectionPtr); private static native long nativeTotalChanges(long connectionPtr); + // This method is deprecated and should be removed when it is no longer needed by the + // robolectric tests. It should not be called from any frameworks java code. + @Deprecated + private static native void nativeClose(long connectionPtr); + private SQLiteConnection(SQLiteConnectionPool pool, SQLiteDatabaseConfiguration configuration, int connectionId, boolean primaryConnection) { @@ -300,7 +305,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen final int cookie = mRecentOperations.beginOperation("close", null, null); try { mPreparedStatementCache.evictAll(); - nativeClose(mConnectionPtr); + nativeClose(mConnectionPtr, finalized && Flags.noCheckpointOnFinalize()); mConnectionPtr = 0; } finally { mRecentOperations.endOperation(cookie); diff --git a/core/java/android/database/sqlite/flags.aconfig b/core/java/android/database/sqlite/flags.aconfig index 1d17a51f3653..9f4f1a16178b 100644 --- a/core/java/android/database/sqlite/flags.aconfig +++ b/core/java/android/database/sqlite/flags.aconfig @@ -5,7 +5,7 @@ flag { name: "oneway_finalizer_close_fixed" namespace: "system_performance" is_fixed_read_only: true - description: "Make BuildCursorNative.close oneway if in the the finalizer" + description: "Make BuildCursorNative.close oneway if in the finalizer" bug: "368221351" } @@ -26,3 +26,10 @@ flag { description: "Make SQLiteOpenHelper thread-safe" bug: "335904370" } + +flag { + name: "no_checkpoint_on_finalize" + namespace: "system_performance" + description: "Do not checkpoint WAL if closing in the finalizer" + bug: "397982577" +} diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index ca24c0c6c376..0476f62ec263 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -358,7 +358,16 @@ flag { is_fixed_read_only: true is_exported: true namespace: "permissions" - description: "Enables SQlite for recording discrete and historical AppOp accesses" + description: "Enables SQlite for recording individual/discrete AppOp accesses" + bug: "377584611" +} + +flag { + name: "enable_all_sqlite_appops_accesses" + is_fixed_read_only: true + is_exported: true + namespace: "permissions" + description: "Enables SQlite for storing aggregated & individual/discrete AppOp accesses" bug: "377584611" } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 9e97a8eb58aa..2895bf3f846a 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -21,7 +21,9 @@ 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; @@ -39,14 +41,13 @@ 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 @@ -349,6 +350,8 @@ 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; @@ -374,7 +377,6 @@ 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; @@ -468,14 +470,12 @@ public class ViewConfiguration { mEdgeSlop = (int) (sizeAndDensity * EDGE_SLOP + 0.5f); mFadingEdgeLength = (int) (sizeAndDensity * FADING_EDGE_LENGTH + 0.5f); - mScrollbarSize = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_scrollbarSize); + mScrollbarSize = res.getDimensionPixelSize(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( - com.android.internal.R.dimen.config_ambiguousGestureMultiplier, + res.getValue(R.dimen.config_ambiguousGestureMultiplier, multiplierValue, true /*resolveRefs*/); mAmbiguousGestureMultiplier = Math.max(1.0f, multiplierValue.getFloat()); @@ -488,8 +488,7 @@ public class ViewConfiguration { mOverflingDistance = (int) (sizeAndDensity * OVERFLING_DISTANCE + 0.5f); if (!sHasPermanentMenuKeySet) { - final int configVal = res.getInteger( - com.android.internal.R.integer.config_overrideHasPermanentMenuKey); + final int configVal = res.getInteger(R.integer.config_overrideHasPermanentMenuKey); switch (configVal) { default: @@ -516,32 +515,27 @@ public class ViewConfiguration { } } - mFadingMarqueeEnabled = res.getBoolean( - com.android.internal.R.bool.config_ui_enableFadingMarquee); - mTouchSlop = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_viewConfigurationTouchSlop); + mFadingMarqueeEnabled = res.getBoolean(R.bool.config_ui_enableFadingMarquee); + mTouchSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationTouchSlop); mHandwritingSlop = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_viewConfigurationHandwritingSlop); - mHoverSlop = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_viewConfigurationHoverSlop); + R.dimen.config_viewConfigurationHandwritingSlop); + mHoverSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationHoverSlop); mMinScrollbarTouchTarget = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_minScrollbarTouchTarget); + R.dimen.config_minScrollbarTouchTarget); mPagingTouchSlop = mTouchSlop * 2; mDoubleTapTouchSlop = mTouchSlop; mHandwritingGestureLineMargin = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_viewConfigurationHandwritingGestureLineMargin); + R.dimen.config_viewConfigurationHandwritingGestureLineMargin); - mMinimumFlingVelocity = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_viewMinFlingVelocity); - mMaximumFlingVelocity = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_viewMaxFlingVelocity); + mMinimumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMinFlingVelocity); + mMaximumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMaxFlingVelocity); int configMinRotaryEncoderFlingVelocity = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_viewMinRotaryEncoderFlingVelocity); + R.dimen.config_viewMinRotaryEncoderFlingVelocity); int configMaxRotaryEncoderFlingVelocity = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_viewMaxRotaryEncoderFlingVelocity); + R.dimen.config_viewMaxRotaryEncoderFlingVelocity); if (configMinRotaryEncoderFlingVelocity < 0 || configMaxRotaryEncoderFlingVelocity < 0) { mMinimumRotaryEncoderFlingVelocity = NO_FLING_MIN_VELOCITY; mMaximumRotaryEncoderFlingVelocity = NO_FLING_MAX_VELOCITY; @@ -551,8 +545,7 @@ public class ViewConfiguration { } int configRotaryEncoderHapticScrollFeedbackTickIntervalPixels = - res.getDimensionPixelSize( - com.android.internal.R.dimen + res.getDimensionPixelSize(R.dimen .config_rotaryEncoderAxisScrollTickInterval); mRotaryEncoderHapticScrollFeedbackTickIntervalPixels = configRotaryEncoderHapticScrollFeedbackTickIntervalPixels > 0 @@ -560,41 +553,31 @@ public class ViewConfiguration { : NO_HAPTIC_SCROLL_TICK_INTERVAL; mRotaryEncoderHapticScrollFeedbackEnabled = - res.getBoolean( - com.android.internal.R.bool + res.getBoolean(R.bool .config_viewRotaryEncoderHapticScrollFedbackEnabled); - mGlobalActionsKeyTimeout = res.getInteger( - com.android.internal.R.integer.config_globalActionsKeyTimeout); + mGlobalActionsKeyTimeout = res.getInteger(R.integer.config_globalActionsKeyTimeout); - mHorizontalScrollFactor = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_horizontalScrollFactor); - mVerticalScrollFactor = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_verticalScrollFactor); + mHorizontalScrollFactor = res.getDimensionPixelSize(R.dimen.config_horizontalScrollFactor); + mVerticalScrollFactor = res.getDimensionPixelSize(R.dimen.config_verticalScrollFactor); mShowMenuShortcutsWhenKeyboardPresent = res.getBoolean( - com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent); + R.bool.config_showMenuShortcutsWhenKeyboardPresent); - mMinScalingSpan = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_minScalingSpan); + mMinScalingSpan = res.getDimensionPixelSize(R.dimen.config_minScalingSpan); - mScreenshotChordKeyTimeout = res.getInteger( - com.android.internal.R.integer.config_screenshotChordKeyTimeout); + mScreenshotChordKeyTimeout = res.getInteger(R.integer.config_screenshotChordKeyTimeout); mSmartSelectionInitializedTimeout = res.getInteger( - com.android.internal.R.integer.config_smartSelectionInitializedTimeoutMillis); + R.integer.config_smartSelectionInitializedTimeoutMillis); mSmartSelectionInitializingTimeout = res.getInteger( - com.android.internal.R.integer.config_smartSelectionInitializingTimeoutMillis); - mPreferKeepClearForFocusEnabled = res.getBoolean( - com.android.internal.R.bool.config_preferKeepClearForFocus); + R.integer.config_smartSelectionInitializingTimeoutMillis); + mPreferKeepClearForFocusEnabled = res.getBoolean(R.bool.config_preferKeepClearForFocus); mViewBasedRotaryEncoderScrollHapticsEnabledConfig = - res.getBoolean( - com.android.internal.R.bool.config_viewBasedRotaryEncoderHapticsEnabled); + res.getBoolean(R.bool.config_viewBasedRotaryEncoderHapticsEnabled); mViewTouchScreenHapticScrollFeedbackEnabled = Flags.enableScrollFeedbackForTouch() - ? res.getBoolean( - com.android.internal.R.bool - .config_viewTouchScreenHapticScrollFeedbackEnabled) + ? res.getBoolean(R.bool.config_viewTouchScreenHapticScrollFeedbackEnabled) : false; } @@ -632,6 +615,7 @@ public class ViewConfiguration { @VisibleForTesting public static void resetCacheForTesting() { sConfigurations.clear(); + sResourceCache = new ResourceCache(); } /** @@ -707,7 +691,7 @@ public class ViewConfiguration { * components. */ public static int getPressedStateDuration() { - return PRESSED_STATE_DURATION; + return sResourceCache.getPressedStateDuration(); } /** @@ -752,7 +736,7 @@ public class ViewConfiguration { * considered to be a tap. */ public static int getTapTimeout() { - return TAP_TIMEOUT; + return sResourceCache.getTapTimeout(); } /** @@ -761,7 +745,7 @@ public class ViewConfiguration { * considered to be a tap. */ public static int getJumpTapTimeout() { - return JUMP_TAP_TIMEOUT; + return sResourceCache.getJumpTapTimeout(); } /** @@ -770,7 +754,7 @@ public class ViewConfiguration { * double-tap. */ public static int getDoubleTapTimeout() { - return DOUBLE_TAP_TIMEOUT; + return sResourceCache.getDoubleTapTimeout(); } /** @@ -782,7 +766,7 @@ public class ViewConfiguration { */ @UnsupportedAppUsage public static int getDoubleTapMinTime() { - return DOUBLE_TAP_MIN_TIME; + return sResourceCache.getDoubleTapMinTime(); } /** @@ -792,7 +776,7 @@ public class ViewConfiguration { * @hide */ public static int getHoverTapTimeout() { - return HOVER_TAP_TIMEOUT; + return sResourceCache.getHoverTapTimeout(); } /** @@ -803,7 +787,7 @@ public class ViewConfiguration { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static int getHoverTapSlop() { - return HOVER_TAP_SLOP; + return sResourceCache.getHoverTapSlop(); } /** @@ -1044,7 +1028,7 @@ public class ViewConfiguration { * in milliseconds. */ public static long getZoomControlsTimeout() { - return ZOOM_CONTROLS_TIMEOUT; + return sResourceCache.getZoomControlsTimeout(); } /** @@ -1113,14 +1097,14 @@ public class ViewConfiguration { * friction. */ public static float getScrollFriction() { - return SCROLL_FRICTION; + return sResourceCache.getScrollFriction(); } /** * @return the default duration in milliseconds for {@link ActionMode#hide(long)}. */ public static long getDefaultActionModeHideDuration() { - return ACTION_MODE_HIDE_DURATION_DEFAULT; + return sResourceCache.getDefaultActionModeHideDuration(); } /** @@ -1471,8 +1455,137 @@ public class ViewConfiguration { return HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT; } - private static final int getDisplayDensity(Context context) { + private static 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/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp index ba7e70564143..36c08a51c66a 100644 --- a/core/jni/android_database_SQLiteConnection.cpp +++ b/core/jni/android_database_SQLiteConnection.cpp @@ -204,7 +204,7 @@ static jlong nativeOpen(JNIEnv* env, jclass clazz, jstring pathStr, jint openFla return reinterpret_cast<jlong>(connection); } -static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) { +static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr, jboolean fast) { SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr); if (connection) { @@ -212,6 +212,13 @@ static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) { if (connection->tableQuery != nullptr) { sqlite3_finalize(connection->tableQuery); } + if (fast) { + // The caller requested a fast close, so do not checkpoint even if this is the last + // connection to the database. Note that the change is only to this connection. + // Any other connections to the same database are unaffected. + int _unused = 0; + sqlite3_db_config(connection->db, SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE, 1, &_unused); + } int err = sqlite3_close(connection->db); if (err != SQLITE_OK) { // This can happen if sub-objects aren't closed first. Make sure the caller knows. @@ -224,6 +231,12 @@ static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) { } } +// This method is deprecated and should be removed when it is no longer needed by the +// robolectric tests. +static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) { + nativeClose(env, clazz, connectionPtr, false); +} + static void sqliteCustomScalarFunctionCallback(sqlite3_context *context, int argc, sqlite3_value **argv) { JNIEnv* env = AndroidRuntime::getJNIEnv(); @@ -959,8 +972,10 @@ static const JNINativeMethod sMethods[] = /* name, signature, funcPtr */ { "nativeOpen", "(Ljava/lang/String;ILjava/lang/String;ZZII)J", (void*)nativeOpen }, + { "nativeClose", "(JZ)V", + (void*) static_cast<void(*)(JNIEnv*,jclass,jlong,jboolean)>(nativeClose) }, { "nativeClose", "(J)V", - (void*)nativeClose }, + (void*) static_cast<void(*)(JNIEnv*,jclass,jlong)>(nativeClose) }, { "nativeRegisterCustomScalarFunction", "(JLjava/lang/String;Ljava/util/function/UnaryOperator;)V", (void*)nativeRegisterCustomScalarFunction }, { "nativeRegisterCustomAggregateFunction", "(JLjava/lang/String;Ljava/util/function/BinaryOperator;)V", diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1cb38bed1388..fbb8e25eeced 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3072,6 +3072,43 @@ {@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/symbols.xml b/core/res/res/values/symbols.xml index a18c1d4df98b..f5424dbed21a 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4159,6 +4159,17 @@ <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/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java index 5b2dd97a338f..bc0a8573f977 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java @@ -39,6 +39,7 @@ import com.android.wm.shell.Flags; import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; import java.util.ArrayList; +import java.util.stream.IntStream; /** * Calculates the snap targets and the snap position given a position and a velocity. All positions @@ -354,10 +355,20 @@ public class DividerSnapAlgorithm { float ratio = areOffscreenRatiosSupported() ? SplitSpec.OFFSCREEN_ASYMMETRIC_RATIO : SplitSpec.ONSCREEN_ONLY_ASYMMETRIC_RATIO; + + // The intended size of the smaller app, in pixels int size = (int) (ratio * (end - start)) - mDividerSize / 2; - int leftTopPosition = start + pinnedTaskbarShiftStart + size; - int rightBottomPosition = end - pinnedTaskbarShiftEnd - size - mDividerSize; + // If there are insets that interfere with the smaller app (visually or blocking touch + // targets), make the smaller app bigger by that amount to compensate. This applies to + // pinned taskbar, 3-button nav (both create an opaque bar at bottom) and status bar (blocks + // touch targets at top). + int extraSpace = IntStream.of( + getStartInset(), getEndInset(), pinnedTaskbarShiftStart, pinnedTaskbarShiftEnd + ).max().getAsInt(); + + int leftTopPosition = start + extraSpace + size; + int rightBottomPosition = end - extraSpace - size - mDividerSize; addNonDismissingTargets(isLeftRightSplit, leftTopPosition, rightBottomPosition, dividerMax); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 720e8e53b218..b7867d0b81b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -1543,11 +1543,28 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } } + /** + * When IME is triggered on the bottom app in split screen, we want to translate the bottom + * app up by a certain amount so that it's not covered too much by the IME. But there's also + * an upper limit to the amount we want to translate (since we still need some of the top + * app to be visible too). So this function essentially says "try to translate the bottom + * app up, but stop before you make the top app too small." + */ private int getTargetYOffset() { - final int desireOffset = Math.abs(mEndImeTop - mStartImeTop); - // Make sure to keep at least 30% visible for the top split. - final int maxOffset = (int) (getTopLeftBounds().height() * ADJUSTED_SPLIT_FRACTION_MAX); - return -Math.min(desireOffset, maxOffset); + // We want to translate up the bottom app by this amount. + final int desiredOffset = Math.abs(mEndImeTop - mStartImeTop); + + // But we also want to keep this much of the top app visible. + final float amountOfTopAppToKeepVisible = + getTopLeftBounds().height() * (1 - ADJUSTED_SPLIT_FRACTION_MAX); + + // So the current onscreen size of the top app, minus the minimum size, is the max + // translation we will allow. + final float currentOnScreenSizeOfTopApp = getTopLeftBounds().bottom; + final int maxOffset = + (int) Math.max(currentOnScreenSizeOfTopApp - amountOfTopAppToKeepVisible, 0); + + return -Math.min(desiredOffset, maxOffset); } @SplitPosition diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 2bd5c72eb34b..6f0919e1d045 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.content.Context; import android.os.Handler; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; @@ -45,6 +46,7 @@ import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; import com.android.wm.shell.pip2.phone.PhonePipMenuController; import com.android.wm.shell.pip2.phone.PipController; +import com.android.wm.shell.pip2.phone.PipInteractionHandler; import com.android.wm.shell.pip2.phone.PipMotionHelper; import com.android.wm.shell.pip2.phone.PipScheduler; import com.android.wm.shell.pip2.phone.PipTaskListener; @@ -88,12 +90,13 @@ public abstract class Pip2Module { @NonNull PipUiStateChangeController pipUiStateChangeController, DisplayController displayController, Optional<SplitScreenController> splitScreenControllerOptional, - PipDesktopState pipDesktopState) { + PipDesktopState pipDesktopState, + PipInteractionHandler pipInteractionHandler) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener, pipScheduler, pipStackListenerController, pipDisplayLayoutState, pipUiStateChangeController, displayController, splitScreenControllerOptional, - pipDesktopState); + pipDesktopState, pipInteractionHandler); } @WMSingleton @@ -249,4 +252,14 @@ public abstract class Pip2Module { @BindsOptionalOf abstract DragToDesktopTransitionHandler optionalDragToDesktopTransitionHandler(); + + @WMSingleton + @Provides + static PipInteractionHandler providePipInteractionHandler( + Context context, + @ShellMainThread Handler mainHandler + ) { + return new PipInteractionHandler(context, mainHandler, + InteractionJankMonitor.getInstance()); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInteractionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInteractionHandler.java new file mode 100644 index 000000000000..321952480094 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInteractionHandler.java @@ -0,0 +1,88 @@ +/* + * 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.pip2.phone; + +import static com.android.internal.jank.Cuj.CUJ_PIP_TRANSITION; + +import android.annotation.IntDef; +import android.content.Context; +import android.os.Handler; +import android.view.SurfaceControl; + +import com.android.internal.jank.InteractionJankMonitor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Helps track PIP CUJ interactions + */ +public class PipInteractionHandler { + @IntDef(prefix = {"INTERACTION_"}, value = { + INTERACTION_EXIT_PIP, + INTERACTION_EXIT_PIP_TO_SPLIT + }) + + @Retention(RetentionPolicy.SOURCE) + public @interface Interaction {} + + public static final int INTERACTION_EXIT_PIP = 0; + public static final int INTERACTION_EXIT_PIP_TO_SPLIT = 1; + + private final Context mContext; + private final Handler mHandler; + private final InteractionJankMonitor mInteractionJankMonitor; + + public PipInteractionHandler(Context context, Handler handler, + InteractionJankMonitor interactionJankMonitor) { + mContext = context; + mHandler = handler; + mInteractionJankMonitor = interactionJankMonitor; + } + + /** + * Begin tracking PIP CUJ. + * + * @param leash PIP leash. + * @param interaction Tag for interaction. + */ + public void begin(SurfaceControl leash, @Interaction int interaction) { + mInteractionJankMonitor.begin(leash, mContext, mHandler, CUJ_PIP_TRANSITION, + pipInteractionToString(interaction)); + } + + /** + * End tracking CUJ. + */ + public void end() { + mInteractionJankMonitor.end(CUJ_PIP_TRANSITION); + } + + /** + * Converts an interaction to a string representation used for tagging. + * + * @param interaction Interaction to track. + * @return String representation of the interaction. + */ + public static String pipInteractionToString(@Interaction int interaction) { + return switch (interaction) { + case INTERACTION_EXIT_PIP -> "EXIT_PIP"; + case INTERACTION_EXIT_PIP_TO_SPLIT -> "EXIT_PIP_TO_SPLIT"; + default -> ""; + }; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index b51a58e604bf..9bb2e38e1526 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -113,6 +113,7 @@ public class PipTransition extends PipTransitionController implements private final DisplayController mDisplayController; private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; private final PipDesktopState mPipDesktopState; + private final PipInteractionHandler mPipInteractionHandler; // // Transition caches @@ -154,7 +155,8 @@ public class PipTransition extends PipTransitionController implements PipUiStateChangeController pipUiStateChangeController, DisplayController displayController, Optional<SplitScreenController> splitScreenControllerOptional, - PipDesktopState pipDesktopState) { + PipDesktopState pipDesktopState, + PipInteractionHandler pipInteractionHandler) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); @@ -168,9 +170,11 @@ public class PipTransition extends PipTransitionController implements mDisplayController = displayController; mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext); mPipDesktopState = pipDesktopState; + mPipInteractionHandler = pipInteractionHandler; mExpandHandler = new PipExpandHandler(mContext, pipBoundsState, pipBoundsAlgorithm, - pipTransitionState, pipDisplayLayoutState, splitScreenControllerOptional); + pipTransitionState, pipDisplayLayoutState, pipInteractionHandler, + splitScreenControllerOptional); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java index db4942b2fb95..3274f4ae354a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java @@ -45,6 +45,7 @@ import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.PipInteractionHandler; import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -58,6 +59,7 @@ public class PipExpandHandler implements Transitions.TransitionHandler { private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final PipTransitionState mPipTransitionState; private final PipDisplayLayoutState mPipDisplayLayoutState; + private final PipInteractionHandler mPipInteractionHandler; private final Optional<SplitScreenController> mSplitScreenControllerOptional; @Nullable @@ -72,12 +74,14 @@ public class PipExpandHandler implements Transitions.TransitionHandler { PipBoundsAlgorithm pipBoundsAlgorithm, PipTransitionState pipTransitionState, PipDisplayLayoutState pipDisplayLayoutState, + PipInteractionHandler pipInteractionHandler, Optional<SplitScreenController> splitScreenControllerOptional) { mContext = context; mPipBoundsState = pipBoundsState; mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipTransitionState = pipTransitionState; mPipDisplayLayoutState = pipDisplayLayoutState; + mPipInteractionHandler = pipInteractionHandler; mSplitScreenControllerOptional = splitScreenControllerOptional; mPipExpandAnimatorSupplier = PipExpandAnimator::new; @@ -183,6 +187,8 @@ public class PipExpandHandler implements Transitions.TransitionHandler { PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash, startTransaction, finishTransaction, endBounds, startBounds, endBounds, sourceRectHint, delta); + animator.setAnimationStartCallback(() -> mPipInteractionHandler.begin(pipLeash, + PipInteractionHandler.INTERACTION_EXIT_PIP)); animator.setAnimationEndCallback(() -> { if (parentBeforePip != null) { // TODO b/377362511: Animate local leash instead to also handle letterbox case. @@ -190,6 +196,7 @@ public class PipExpandHandler implements Transitions.TransitionHandler { finishTransaction.setCrop(pipLeash, null); } finishTransition(); + mPipInteractionHandler.end(); }); cacheAndStartTransitionAnimator(animator); saveReentryState(); @@ -248,6 +255,8 @@ public class PipExpandHandler implements Transitions.TransitionHandler { splitController.finishEnterSplitScreen(finishTransaction); }); + animator.setAnimationStartCallback(() -> mPipInteractionHandler.begin(pipLeash, + PipInteractionHandler.INTERACTION_EXIT_PIP_TO_SPLIT)); animator.setAnimationEndCallback(() -> { if (parentBeforePip == null) { // After PipExpandAnimator is done modifying finishTransaction, we need to make @@ -256,6 +265,7 @@ public class PipExpandHandler implements Transitions.TransitionHandler { finishTransaction.setPosition(pipLeash, 0, 0); } finishTransition(); + mPipInteractionHandler.end(); }); cacheAndStartTransitionAnimator(animator); saveReentryState(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipInteractionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipInteractionHandlerTest.java new file mode 100644 index 000000000000..9c0127ea2414 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipInteractionHandlerTest.java @@ -0,0 +1,95 @@ +/* + * 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.pip2.phone; + +import static com.android.internal.jank.Cuj.CUJ_PIP_TRANSITION; +import static com.android.wm.shell.pip2.phone.PipInteractionHandler.INTERACTION_EXIT_PIP; +import static com.android.wm.shell.pip2.phone.PipInteractionHandler.INTERACTION_EXIT_PIP_TO_SPLIT; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.kotlin.VerificationKt.times; + +import android.content.Context; +import android.os.Handler; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; + +import com.android.internal.jank.InteractionJankMonitor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit test against {@link PipInteractionHandler}. + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipInteractionHandlerTest { + @Mock private Context mMockContext; + @Mock private Handler mMockHandler; + @Mock private InteractionJankMonitor mMockInteractionJankMonitor; + + private SurfaceControl mTestLeash; + + private PipInteractionHandler mPipInteractionHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mPipInteractionHandler = new PipInteractionHandler(mMockContext, mMockHandler, + mMockInteractionJankMonitor); + mTestLeash = new SurfaceControl.Builder() + .setContainerLayer() + .setName("PipInteractionHandlerTest") + .setCallsite("PipInteractionHandlerTest") + .build(); + } + + @Test + public void begin_expand_startsTracking() { + mPipInteractionHandler.begin(mTestLeash, INTERACTION_EXIT_PIP); + + verify(mMockInteractionJankMonitor, times(1)).begin(eq(mTestLeash), + eq(mMockContext), eq(mMockHandler), eq(CUJ_PIP_TRANSITION), + eq(PipInteractionHandler.pipInteractionToString(INTERACTION_EXIT_PIP))); + } + + @Test + public void begin_expandToSplit_startsTracking() { + mPipInteractionHandler.begin(mTestLeash, INTERACTION_EXIT_PIP_TO_SPLIT); + + verify(mMockInteractionJankMonitor, times(1)).begin(eq(mTestLeash), + eq(mMockContext), eq(mMockHandler), eq(CUJ_PIP_TRANSITION), + eq(PipInteractionHandler.pipInteractionToString(INTERACTION_EXIT_PIP_TO_SPLIT))); + } + + @Test + public void end_stopsTracking() { + mPipInteractionHandler.end(); + + verify(mMockInteractionJankMonitor, times(1)).end(CUJ_PIP_TRANSITION); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java index 2a22842eda1a..cc66f00525b5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java @@ -23,6 +23,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -48,11 +49,13 @@ import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.PipInteractionHandler; import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.TransitionInfoBuilder; @@ -61,6 +64,8 @@ import com.android.wm.shell.util.StubTransaction; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -79,6 +84,7 @@ public class PipExpandHandlerTest { @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; @Mock private PipTransitionState mMockPipTransitionState; @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState; + @Mock private PipInteractionHandler mMockPipInteractionHandler; @Mock private SplitScreenController mMockSplitScreenController; @Mock private IBinder mMockTransitionToken; @@ -89,6 +95,8 @@ public class PipExpandHandlerTest { @Mock private PipExpandAnimator mMockPipExpandAnimator; + @Captor private ArgumentCaptor<Runnable> mAnimatorCallbackArgumentCaptor; + @Surface.Rotation private static final int DISPLAY_ROTATION = Surface.ROTATION_0; @@ -108,7 +116,7 @@ public class PipExpandHandlerTest { mPipExpandHandler = new PipExpandHandler(mMockContext, mMockPipBoundsState, mMockPipBoundsAlgorithm, mMockPipTransitionState, mMockPipDisplayLayoutState, - Optional.of(mMockSplitScreenController)); + mMockPipInteractionHandler, Optional.of(mMockSplitScreenController)); mPipExpandHandler.setPipExpandAnimatorSupplier((context, leash, startTransaction, finishTransaction, baseBounds, startBounds, endBounds, sourceRectHint, rotation) -> mMockPipExpandAnimator); @@ -138,6 +146,13 @@ public class PipExpandHandlerTest { verify(mMockPipExpandAnimator, times(1)).start(); verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION); + + verify(mMockPipExpandAnimator, times(1)) + .setAnimationStartCallback(mAnimatorCallbackArgumentCaptor.capture()); + InstrumentationRegistry.getInstrumentation() + .runOnMainSync(mAnimatorCallbackArgumentCaptor.getValue()); + verify(mMockPipInteractionHandler, times(1)).begin(any(), + eq(PipInteractionHandler.INTERACTION_EXIT_PIP)); } @Test @@ -158,6 +173,13 @@ public class PipExpandHandlerTest { verify(mMockSplitScreenController, times(1)).finishEnterSplitScreen(eq(mFinishT)); verify(mMockPipExpandAnimator, times(1)).start(); verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION); + + verify(mMockPipExpandAnimator, times(1)) + .setAnimationStartCallback(mAnimatorCallbackArgumentCaptor.capture()); + InstrumentationRegistry.getInstrumentation() + .runOnMainSync(mAnimatorCallbackArgumentCaptor.getValue()); + verify(mMockPipInteractionHandler, times(1)).begin(any(), + eq(PipInteractionHandler.INTERACTION_EXIT_PIP_TO_SPLIT)); } private TransitionInfo getExpandFromPipTransitionInfo(@WindowManager.TransitionType int type, diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_expressive_preference_selector_with_widget.xml index a79d69dbff8c..a79d69dbff8c 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/preference_selector_with_widget.xml +++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_expressive_preference_selector_with_widget.xml diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java index cde8b332f2e7..465b6ccf4d9c 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java +++ b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java @@ -238,7 +238,10 @@ public class SelectorWithWidgetPreference extends CheckBoxPreference { } else { setWidgetLayoutResource(R.layout.settingslib_preference_widget_radiobutton); } - setLayoutResource(R.layout.preference_selector_with_widget); + int resID = SettingsThemeHelper.isExpressiveTheme(context) + ? R.layout.settingslib_expressive_preference_selector_with_widget + : R.layout.preference_selector_with_widget; + setLayoutResource(resID); setIconSpaceReserved(false); final TypedArray a = diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index cb1f1af8b5a6..a90081738062 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -2093,3 +2093,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "always_compose_qs_ui_fragment" + namespace: "systemui" + description: "Have QQS and QS scenes in the Compose fragment always composed, not just when it should be visible." + bug: "389985793" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 404f1b217026..22688d310b44 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -102,6 +102,7 @@ interface SceneTransitionLayoutScope<out CS : ContentScope> { key: SceneKey, userActions: Map<UserAction, UserActionResult> = emptyMap(), effectFactory: OverscrollFactory? = null, + alwaysCompose: Boolean = false, content: @Composable CS.() -> Unit, ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index e3c4eb0f8bea..4da83c3a6fc9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -207,6 +207,9 @@ internal class SceneTransitionLayoutImpl( private val nestedScrollDispatcher = NestedScrollDispatcher() private val nestedScrollConnection = object : NestedScrollConnection {} + // TODO(b/399825091): Remove this. + private var scenesToAlwaysCompose: MutableList<Scene>? = null + init { updateContents(builder, layoutDirection, defaultEffectFactory) @@ -312,6 +315,7 @@ internal class SceneTransitionLayoutImpl( key: SceneKey, userActions: Map<UserAction, UserActionResult>, effectFactory: OverscrollFactory?, + alwaysCompose: Boolean, content: @Composable InternalContentScope.() -> Unit, ) { require(!overlaysDefined) { "all scenes must be defined before overlays" } @@ -324,6 +328,10 @@ internal class SceneTransitionLayoutImpl( Content.calculateGlobalZIndex(parentZIndex, ++zIndex, ancestors.size) val factory = effectFactory ?: defaultEffectFactory if (scene != null) { + check(alwaysCompose == scene.alwaysCompose) { + "scene.alwaysCompose can not change" + } + // Update an existing scene. scene.content = content scene.userActions = resolvedUserActions @@ -332,7 +340,7 @@ internal class SceneTransitionLayoutImpl( scene.maybeUpdateEffects(factory) } else { // New scene. - scenes[key] = + val scene = Scene( key, this@SceneTransitionLayoutImpl, @@ -341,7 +349,16 @@ internal class SceneTransitionLayoutImpl( zIndex.toFloat(), globalZIndex, factory, + alwaysCompose, ) + + scenes[key] = scene + + if (alwaysCompose) { + (scenesToAlwaysCompose + ?: mutableListOf<Scene>().also { scenesToAlwaysCompose = it }) + .add(scene) + } } } @@ -470,22 +487,24 @@ internal class SceneTransitionLayoutImpl( @Composable private fun Scenes() { - scenesToCompose().fastForEach { scene -> key(scene.key) { scene.Content() } } + scenesToCompose().fastForEach { (scene, isInvisible) -> + key(scene.key) { scene.Content(isInvisible = isInvisible) } + } } - private fun scenesToCompose(): List<Scene> { + private fun scenesToCompose(): List<SceneToCompose> { val transitions = state.currentTransitions - return if (transitions.isEmpty()) { - listOf(scene(state.transitionState.currentScene)) - } else { - buildList { - val visited = mutableSetOf<SceneKey>() - fun maybeAdd(sceneKey: SceneKey) { - if (visited.add(sceneKey)) { - add(scene(sceneKey)) - } + return buildList { + val visited = mutableSetOf<SceneKey>() + fun maybeAdd(sceneKey: SceneKey, isInvisible: Boolean = false) { + if (visited.add(sceneKey)) { + add(SceneToCompose(scene(sceneKey), isInvisible)) } + } + if (transitions.isEmpty()) { + maybeAdd(state.transitionState.currentScene) + } else { // Compose the new scene we are going to first. transitions.fastForEachReversed { transition -> when (transition) { @@ -504,9 +523,13 @@ internal class SceneTransitionLayoutImpl( // Make sure that the current scene is always composed. maybeAdd(transitions.last().currentScene) } + + scenesToAlwaysCompose?.fastForEach { maybeAdd(it.key, isInvisible = true) } } } + private data class SceneToCompose(val scene: Scene, val isInvisible: Boolean) + @Composable private fun BoxScope.Overlays() { val overlaysOrderedByZIndex = overlaysToComposeOrderedByZIndex() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt index 149a9e7c4705..72ee75ad2d47 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.LookaheadScope import androidx.compose.ui.layout.approachLayout +import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.IntSize import androidx.compose.ui.zIndex @@ -154,11 +155,12 @@ internal sealed class Content( @SuppressLint("NotConstructor") @Composable - fun Content(modifier: Modifier = Modifier) { + fun Content(modifier: Modifier = Modifier, isInvisible: Boolean = false) { // If this content has a custom factory, provide it to the content so that the factory is // automatically used when calling rememberOverscrollEffect(). Box( modifier + .thenIf(isInvisible) { InvisibleModifier } .zIndex(zIndex) .approachLayout( isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() } @@ -305,3 +307,8 @@ internal class ContentScopeImpl( ) } } + +private val InvisibleModifier = + Modifier.layout { measurable, constraints -> + measurable.measure(constraints).run { layout(width, height) {} } + } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt index 7f57798fb1b3..38acd4be80ae 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt @@ -35,6 +35,7 @@ internal class Scene( zIndex: Float, globalZIndex: Long, effectFactory: OverscrollFactory, + val alwaysCompose: Boolean, ) : Content(key, layoutImpl, content, actions, zIndex, globalZIndex, effectFactory) { override fun toString(): String { return "Scene(key=$key)" diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index fa7661b6d102..6538d4340cf3 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertHeightIsEqualTo import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertPositionInRootIsEqualTo import androidx.compose.ui.test.assertWidthIsEqualTo import androidx.compose.ui.test.junit4.createComposeRule @@ -64,10 +65,13 @@ import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.test.assertSizeIsEqualTo +import com.android.compose.test.setContentAndCreateMainScope import com.android.compose.test.subjects.DpOffsetSubject import com.android.compose.test.subjects.assertThat +import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test @@ -582,4 +586,34 @@ class SceneTransitionLayoutTest { assertThat((state2 as MutableSceneTransitionLayoutStateImpl).motionScheme) .isEqualTo(motionScheme2) } + + @Test + fun alwaysCompose() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateForTests(SceneA) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayoutForTesting(state) { + scene(SceneA) { Box(Modifier.element(TestElements.Foo).size(20.dp)) } + scene(SceneB, alwaysCompose = true) { + Box(Modifier.element(TestElements.Bar).size(40.dp)) + } + } + } + + // Idle(A): Foo is displayed and Bar exists given that SceneB is always composed but it is + // not displayed. + rule.onNode(isElement(TestElements.Foo)).assertIsDisplayed().assertSizeIsEqualTo(20.dp) + rule.onNode(isElement(TestElements.Bar)).assertExists().assertIsNotDisplayed() + + // Transition(A => B): Foo and Bar are both displayed + val aToB = transition(SceneA, SceneB) + scope.launch { state.startTransition(aToB) } + rule.onNode(isElement(TestElements.Foo)).assertIsDisplayed().assertSizeIsEqualTo(20.dp) + rule.onNode(isElement(TestElements.Bar)).assertIsDisplayed().assertSizeIsEqualTo(40.dp) + + // Idle(B): Foo does not exist and Bar is displayed. + aToB.finish() + rule.onNode(isElement(TestElements.Foo)).assertDoesNotExist() + rule.onNode(isElement(TestElements.Bar)).assertIsDisplayed().assertSizeIsEqualTo(40.dp) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/domain/interactor/SysUIStatePerDisplayInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/domain/interactor/SysUIStatePerDisplayInteractorTest.kt new file mode 100644 index 000000000000..ed9cd98a825a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/domain/interactor/SysUIStatePerDisplayInteractorTest.kt @@ -0,0 +1,113 @@ +/* + * 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.common.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.model.StateChange +import com.android.systemui.model.fakeSysUIStatePerDisplayRepository +import com.android.systemui.model.sysUiStateFactory +import com.android.systemui.model.sysuiStateInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.Before +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SysUIStatePerDisplayInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + val stateRepository = kosmos.fakeSysUIStatePerDisplayRepository + val state0 = kosmos.sysUiStateFactory.create(0) + val state1 = kosmos.sysUiStateFactory.create(1) + val state2 = kosmos.sysUiStateFactory.create(2) + + val underTest = kosmos.sysuiStateInteractor + + @Before + fun setup() { + stateRepository.apply { + add(0, state0) + add(1, state1) + add(2, state2) + } + } + + @Test + fun setFlagsExclusivelyToDisplay_setsFlagsOnTargetStateAndClearsTheOthers() { + val targetDisplayId = 0 + val stateChange = StateChange().setFlag(1L, true) + + underTest.setFlagsExclusivelyToDisplay(targetDisplayId, stateChange) + + assertThat(state0.isFlagEnabled(1)).isTrue() + assertThat(state1.isFlagEnabled(1)).isFalse() + assertThat(state2.isFlagEnabled(1)).isFalse() + + underTest.setFlagsExclusivelyToDisplay(1, stateChange) + + assertThat(state0.isFlagEnabled(1)).isFalse() + assertThat(state1.isFlagEnabled(1)).isTrue() + assertThat(state2.isFlagEnabled(1)).isFalse() + + underTest.setFlagsExclusivelyToDisplay(2, stateChange) + + assertThat(state0.isFlagEnabled(1)).isFalse() + assertThat(state1.isFlagEnabled(1)).isFalse() + assertThat(state2.isFlagEnabled(1)).isTrue() + + underTest.setFlagsExclusivelyToDisplay(3, stateChange) + + assertThat(state0.isFlagEnabled(1)).isFalse() + assertThat(state1.isFlagEnabled(1)).isFalse() + assertThat(state2.isFlagEnabled(1)).isFalse() + } + + @Test + fun setFlagsExclusivelyToDisplay_multipleFlags_setsFlagsOnTargetStateAndClearsTheOthers() { + val stateChange = StateChange().setFlag(1L, true).setFlag(2L, true) + + underTest.setFlagsExclusivelyToDisplay(1, stateChange) + + assertThat(state0.isFlagEnabled(1)).isFalse() + assertThat(state0.isFlagEnabled(2)).isFalse() + assertThat(state1.isFlagEnabled(1)).isTrue() + assertThat(state1.isFlagEnabled(2)).isTrue() + assertThat(state2.isFlagEnabled(1)).isFalse() + assertThat(state2.isFlagEnabled(1)).isFalse() + } + + @Test + fun setFlagsExclusivelyToDisplay_clearsFlags() { + state0.setFlag(1, true).setFlag(2, true).commitUpdate() + state1.setFlag(1, true).setFlag(2, true).commitUpdate() + state2.setFlag(1, true).setFlag(2, true).commitUpdate() + + val stateChange = StateChange().setFlag(1L, false) + + underTest.setFlagsExclusivelyToDisplay(1, stateChange) + + // Sets it as false in display 1, but also the others. + assertThat(state0.isFlagEnabled(1)).isFalse() + assertThat(state1.isFlagEnabled(1)).isFalse() + assertThat(state2.isFlagEnabled(1)).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt new file mode 100644 index 000000000000..b82f5fce9e14 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt @@ -0,0 +1,87 @@ +/* + * 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.model + +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.view.Display +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SysUIStateDispatcherTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private val stateFactory = kosmos.sysUiStateFactory + private val state0 = stateFactory.create(Display.DEFAULT_DISPLAY) + private val state1 = stateFactory.create(DISPLAY_1) + private val state2 = stateFactory.create(DISPLAY_2) + private val underTest = kosmos.sysUIStateDispatcher + + private val flagsChanges = mutableMapOf<Int, Long>() // display id -> flag value + private val callback = + SysUiState.SysUiStateCallback { sysUiFlags, displayId -> + flagsChanges[displayId] = sysUiFlags + } + + @Test + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun registerUnregisterListener_notifiedOfChanges_receivedForAllDisplayIdsWithOneCallback() { + underTest.registerListener(callback) + + state1.setFlag(FLAG_1, true).commitUpdate() + state2.setFlag(FLAG_2, true).commitUpdate() + + assertThat(flagsChanges).containsExactly(DISPLAY_1, FLAG_1, DISPLAY_2, FLAG_2) + + underTest.unregisterListener(callback) + + state1.setFlag(0, true).commitUpdate() + + // Didn't change + assertThat(flagsChanges).containsExactly(DISPLAY_1, FLAG_1, DISPLAY_2, FLAG_2) + } + + @Test + @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun registerUnregisterListener_notifiedOfChangesForNonDefaultDisplay_NotPropagated() { + underTest.registerListener(callback) + + state1.setFlag(FLAG_1, true).commitUpdate() + + assertThat(flagsChanges).isEmpty() + + state0.setFlag(FLAG_1, true).commitUpdate() + + assertThat(flagsChanges).containsExactly(Display.DEFAULT_DISPLAY, FLAG_1) + } + + private companion object { + const val DISPLAY_1 = 1 + const val DISPLAY_2 = 2 + const val FLAG_1 = 10L + const val FLAG_2 = 20L + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java index 779e10a9682a..f6de6295212b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java @@ -28,6 +28,8 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.view.Display; + import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -53,9 +55,11 @@ public class SysUiStateTest extends SysuiTestCase { private SysUiState mFlagsContainer; private SceneContainerPlugin mSceneContainerPlugin; private DumpManager mDumpManager; + private SysUIStateDispatcher mSysUIStateDispatcher; private SysUiState createInstance(int displayId) { - var sysuiState = new SysUiStateImpl(displayId, mSceneContainerPlugin, mDumpManager); + var sysuiState = new SysUiStateImpl(displayId, mSceneContainerPlugin, mDumpManager, + mSysUIStateDispatcher); sysuiState.addCallback(mCallback); return sysuiState; } @@ -67,6 +71,7 @@ public class SysUiStateTest extends SysuiTestCase { mSceneContainerPlugin = mKosmos.getSceneContainerPlugin(); mCallback = mock(SysUiState.SysUiStateCallback.class); mDumpManager = mock(DumpManager.class); + mSysUIStateDispatcher = mKosmos.getSysUIStateDispatcher(); mFlagsContainer = createInstance(DEFAULT_DISPLAY); } @@ -74,7 +79,7 @@ public class SysUiStateTest extends SysuiTestCase { public void addSingle_setFlag() { setFlags(FLAG_1); - verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1); + verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1, DEFAULT_DISPLAY); } @Test @@ -82,8 +87,8 @@ public class SysUiStateTest extends SysuiTestCase { setFlags(FLAG_1); setFlags(FLAG_2); - verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1); - verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1 | FLAG_2); + verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1, DEFAULT_DISPLAY); + verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1 | FLAG_2, DEFAULT_DISPLAY); } @Test @@ -92,9 +97,9 @@ public class SysUiStateTest extends SysuiTestCase { setFlags(FLAG_2); mFlagsContainer.setFlag(FLAG_1, false).commitUpdate(DISPLAY_ID); - verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1); - verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1 | FLAG_2); - verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_2); + verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1, DEFAULT_DISPLAY); + verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1 | FLAG_2, DEFAULT_DISPLAY); + verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_2, DEFAULT_DISPLAY); } @Test @@ -102,7 +107,7 @@ public class SysUiStateTest extends SysuiTestCase { setFlags(FLAG_1, FLAG_2, FLAG_3, FLAG_4); int expected = FLAG_1 | FLAG_2 | FLAG_3 | FLAG_4; - verify(mCallback, times(1)).onSystemUiStateChanged(expected); + verify(mCallback, times(1)).onSystemUiStateChanged(expected, DEFAULT_DISPLAY); } @Test @@ -111,9 +116,9 @@ public class SysUiStateTest extends SysuiTestCase { mFlagsContainer.setFlag(FLAG_2, false).commitUpdate(DISPLAY_ID); int expected1 = FLAG_1 | FLAG_2 | FLAG_3 | FLAG_4; - verify(mCallback, times(1)).onSystemUiStateChanged(expected1); + verify(mCallback, times(1)).onSystemUiStateChanged(expected1, DEFAULT_DISPLAY); int expected2 = FLAG_1 | FLAG_3 | FLAG_4; - verify(mCallback, times(1)).onSystemUiStateChanged(expected2); + verify(mCallback, times(1)).onSystemUiStateChanged(expected2, DEFAULT_DISPLAY); } @Test @@ -122,25 +127,16 @@ public class SysUiStateTest extends SysuiTestCase { setFlags(FLAG_1, FLAG_2, FLAG_3, FLAG_4); int expected = FLAG_1 | FLAG_2 | FLAG_3 | FLAG_4; - verify(mCallback, times(0)).onSystemUiStateChanged(expected); + verify(mCallback, times(0)).onSystemUiStateChanged(expected, DEFAULT_DISPLAY); } @Test public void setFlag_receivedForDefaultDisplay() { setFlags(FLAG_1); - verify(mCallback, times(1)).onSystemUiStateChangedForDisplay(FLAG_1, DEFAULT_DISPLAY); + verify(mCallback, times(1)).onSystemUiStateChanged(FLAG_1, DEFAULT_DISPLAY); } - @Test - public void setFlag_externalDisplayInstance_defaultDisplayCallbackNotPropagated() { - var instance = createInstance(/* displayId = */ 2); - reset(mCallback); - setFlags(instance, FLAG_1); - - verify(mCallback, times(1)).onSystemUiStateChangedForDisplay(FLAG_1, /* displayId= */ 2); - verify(mCallback, never()).onSystemUiStateChanged(FLAG_1); - } @Test public void init_registersWithDumpManager() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index d852a9dc0bb9..2c852c3f6185 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -17,7 +17,9 @@ package com.android.systemui.shade; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.Display.TYPE_INTERNAL; +import static com.android.systemui.display.data.repository.FakeDisplayRepositoryKt.display; import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static com.google.common.truth.Truth.assertThat; @@ -47,6 +49,7 @@ import android.os.Looper; import android.os.PowerManager; import android.os.UserManager; import android.util.DisplayMetrics; +import android.view.Display; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -70,6 +73,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; +import com.android.systemui.common.domain.interactor.SysUIStateDisplaysInteractor; import com.android.systemui.common.ui.view.TouchHandlingView; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; @@ -291,6 +295,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock private NaturalScrollingSettingObserver mNaturalScrollingSettingObserver; @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper; @Mock private StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector; + @Mock protected SysUIStateDisplaysInteractor mSysUIStateDisplaysInteractor; protected final int mMaxUdfpsBurnInOffsetY = 5; protected FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic(); protected KeyguardClockInteractor mKeyguardClockInteractor; @@ -435,6 +440,9 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { return null; }).when(mView).setOnTouchListener(any(NotificationPanelViewController.TouchHandler.class)); + var displayMock = display(TYPE_INTERNAL, /* flags= */ 0, /* id= */Display.DEFAULT_DISPLAY, + /* state= */ null); + when(mView.getDisplay()).thenReturn(displayMock); // Any edge transition when(mKeyguardTransitionInteractor.transition(any())) .thenReturn(emptyFlow()); @@ -565,6 +573,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mShadeRepository, mSysUIUnfoldComponent, mSysUiState, + mSysUIStateDisplaysInteractor, mKeyguardUnlockAnimationController, mKeyguardIndicationController, mNotificationListContainer, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 354c23d48916..f54c36754d31 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -16,10 +16,16 @@ package com.android.systemui.shade; +import static android.view.Display.TYPE_INTERNAL; + +import static com.android.systemui.display.data.repository.FakeDisplayRepositoryKt.display; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -28,6 +34,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.os.Build; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; import android.view.HapticFeedbackConstants; @@ -38,6 +45,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.DejankUtils; +import com.android.systemui.Flags; import com.android.systemui.flags.DisableSceneContainer; import com.google.android.msdl.data.model.MSDLToken; @@ -182,4 +190,27 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo assertThat(mMSDLPlayer.getLatestTokenPlayed()).isEqualTo(MSDLToken.FAILURE); } + + @Test + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + public void updateSystemUiStateFlags_updatesSysuiStateInteractor() { + var DISPLAY_ID = 10; + var displayMock = display(TYPE_INTERNAL, /* flags= */ 0, /* id= */DISPLAY_ID, + /* state= */ null); + when(mView.getDisplay()).thenReturn(displayMock); + + mNotificationPanelViewController.updateSystemUiStateFlags(); + + verify(mSysUIStateDisplaysInteractor).setFlagsExclusivelyToDisplay(eq(DISPLAY_ID), any()); + } + + @Test + @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + public void updateSystemUiStateFlags_flagOff_doesNotUpdateSysuiStateInteractor() { + mNotificationPanelViewController.updateSystemUiStateFlags(); + + verify(mSysUIStateDisplaysInteractor, never()).setFlagsExclusivelyToDisplay(anyInt(), + any()); + } + } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarOrchestratorStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarOrchestratorStoreTest.kt index 06650f2afe58..b2b28a28ac38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarOrchestratorStoreTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarOrchestratorStoreTest.kt @@ -50,11 +50,11 @@ class MultiDisplayStatusBarOrchestratorStoreTest : SysuiTestCase() { @Before fun addDisplays() = runBlocking { fakeDisplayRepository.addDisplay(DEFAULT_DISPLAY) } @Test - fun displayRemoved_stopsInstance() = + fun systemDecorationRemovedEvent_stopsInstance() = testScope.runTest { val instance = underTest.forDisplay(DEFAULT_DISPLAY)!! - fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY) + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(DEFAULT_DISPLAY) verify(instance).stop() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarterTest.kt index fee939df2cbb..18a2d0794f91 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarterTest.kt @@ -17,8 +17,7 @@ package com.android.systemui.statusbar.core import android.platform.test.annotations.EnableFlags -import android.view.Display -import android.view.mockIWindowManager +import android.view.Display.DEFAULT_DISPLAY import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -29,6 +28,8 @@ import com.android.systemui.statusbar.data.repository.fakePrivacyDotWindowContro import com.android.systemui.testKosmos import com.google.common.truth.Expect import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -37,11 +38,11 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.never import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) +@OptIn(ExperimentalCoroutinesApi::class) class MultiDisplayStatusBarStarterTest : SysuiTestCase() { @get:Rule val expect: Expect = Expect.create() @@ -52,293 +53,116 @@ class MultiDisplayStatusBarStarterTest : SysuiTestCase() { private val fakeInitializerStore = kosmos.fakeStatusBarInitializerStore private val fakePrivacyDotStore = kosmos.fakePrivacyDotWindowControllerStore private val fakeLightBarStore = kosmos.fakeLightBarControllerStore - private val windowManager = kosmos.mockIWindowManager // Lazy, so that @EnableFlags is set before initializer is instantiated. private val underTest by lazy { kosmos.multiDisplayStatusBarStarter } @Before - fun setup() { - whenever(windowManager.shouldShowSystemDecors(Display.DEFAULT_DISPLAY)).thenReturn(true) - whenever(windowManager.shouldShowSystemDecors(DISPLAY_1)).thenReturn(true) - whenever(windowManager.shouldShowSystemDecors(DISPLAY_2)).thenReturn(true) - whenever(windowManager.shouldShowSystemDecors(DISPLAY_3)).thenReturn(true) - whenever(windowManager.shouldShowSystemDecors(DISPLAY_4_NO_SYSTEM_DECOR)).thenReturn(false) + fun setUp() = runBlocking { + fakeDisplayRepository.addDisplay(DEFAULT_DISPLAY) + fakeDisplayRepository.addDisplay(DISPLAY_2) } @Test - fun start_startsInitializersForCurrentDisplays() = + fun start_triggerAddDisplaySystemDecoration_startsInitializersForDisplay() = testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = DISPLAY_1) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_2) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - underTest.start() runCurrent() - expect - .that(fakeInitializerStore.forDisplay(displayId = DISPLAY_1).startedByCoreStartable) - .isTrue() - expect - .that(fakeInitializerStore.forDisplay(displayId = DISPLAY_2).startedByCoreStartable) - .isTrue() + fakeDisplayRepository.triggerAddDisplaySystemDecorationEvent( + displayId = DEFAULT_DISPLAY + ) + fakeDisplayRepository.triggerAddDisplaySystemDecorationEvent(displayId = DISPLAY_2) + expect .that( fakeInitializerStore - .forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) + .forDisplay(displayId = DEFAULT_DISPLAY) .startedByCoreStartable ) - .isFalse() - } - - @Test - fun start_startsOrchestratorForCurrentDisplays() = - testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = DISPLAY_1) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_2) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - - underTest.start() - runCurrent() - - verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = DISPLAY_1)!!) - .start() - verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = DISPLAY_2)!!) - .start() - assertThat( - fakeOrchestratorFactory.createdOrchestratorForDisplay( - displayId = DISPLAY_4_NO_SYSTEM_DECOR - ) - ) - .isNull() - } - - @Test - fun start_startsPrivacyDotForCurrentDisplays() = - testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = DISPLAY_1) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_2) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - - underTest.start() - runCurrent() - - verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_1)).start() - verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_2)).start() - verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) - .start() - } - - @Test - fun start_doesNotStartLightBarControllerForCurrentDisplays() = - testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = DISPLAY_1) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_2) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - - underTest.start() - runCurrent() - - verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_1), never()).start() - verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_2), never()).start() - verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) - .start() - } - - @Test - fun start_createsLightBarControllerForCurrentDisplays() = - testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = DISPLAY_1) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_2) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - - underTest.start() - runCurrent() - - assertThat(fakeLightBarStore.perDisplayMocks.keys).containsExactly(1, DISPLAY_2) - } - - @Test - fun start_doesNotStartPrivacyDotForDefaultDisplay() = - testScope.runTest { - fakeDisplayRepository.addDisplay(displayId = Display.DEFAULT_DISPLAY) - - underTest.start() - runCurrent() - - verify(fakePrivacyDotStore.forDisplay(displayId = Display.DEFAULT_DISPLAY), never()) - .start() - } - - @Test - fun displayAdded_orchestratorForNewDisplay() = - testScope.runTest { - underTest.start() - runCurrent() - - fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - runCurrent() - - verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = DISPLAY_3)!!) - .start() - assertThat( - fakeOrchestratorFactory.createdOrchestratorForDisplay( - displayId = DISPLAY_4_NO_SYSTEM_DECOR - ) - ) - .isNull() - } - - @Test - fun displayAdded_initializerForNewDisplay() = - testScope.runTest { - underTest.start() - runCurrent() - - fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - runCurrent() - - expect - .that(fakeInitializerStore.forDisplay(displayId = DISPLAY_3).startedByCoreStartable) .isTrue() expect - .that( - fakeInitializerStore - .forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - .startedByCoreStartable - ) - .isFalse() + .that(fakeInitializerStore.forDisplay(displayId = DISPLAY_2).startedByCoreStartable) + .isTrue() } @Test - fun displayAdded_privacyDotForNewDisplay() = + fun start_triggerAddDisplaySystemDecoration_startsOrchestratorForDisplay() = testScope.runTest { underTest.start() runCurrent() - fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) + fakeDisplayRepository.triggerAddDisplaySystemDecorationEvent( + displayId = DEFAULT_DISPLAY + ) + fakeDisplayRepository.triggerAddDisplaySystemDecorationEvent(displayId = DISPLAY_2) runCurrent() - verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_3)).start() - verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) + verify( + fakeOrchestratorFactory.createdOrchestratorForDisplay( + displayId = DEFAULT_DISPLAY + )!! + ) + .start() + verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = DISPLAY_2)!!) .start() } @Test - fun displayAdded_lightBarForNewDisplayCreate() = + fun start_triggerAddDisplaySystemDecoration_startsPrivacyDotForNonDefaultDisplay() = testScope.runTest { underTest.start() runCurrent() - fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - runCurrent() + fakeDisplayRepository.triggerAddDisplaySystemDecorationEvent(displayId = DISPLAY_2) - assertThat(fakeLightBarStore.perDisplayMocks.keys).containsExactly(DISPLAY_3) + verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_2)).start() } @Test - fun displayAdded_lightBarForNewDisplayStart() = + fun start_triggerAddDisplaySystemDecoration_doesNotStartPrivacyDotForDefaultDisplay() = testScope.runTest { underTest.start() runCurrent() - fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - runCurrent() + fakeDisplayRepository.triggerAddDisplaySystemDecorationEvent( + displayId = DEFAULT_DISPLAY + ) - verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_3), never()).start() - verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) - .start() + verify(fakePrivacyDotStore.forDisplay(displayId = DEFAULT_DISPLAY), never()).start() } @Test - fun displayAddedDuringStart_initializerForNewDisplay() = + fun start_triggerAddDisplaySystemDecoration_doesNotStartLightBarControllerForDisplays() = testScope.runTest { underTest.start() - - fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() - expect - .that(fakeInitializerStore.forDisplay(displayId = DISPLAY_3).startedByCoreStartable) - .isTrue() - expect - .that( - fakeInitializerStore - .forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - .startedByCoreStartable - ) - .isFalse() - } - - @Test - fun displayAddedDuringStart_orchestratorForNewDisplay() = - testScope.runTest { - underTest.start() - - fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - runCurrent() - - verify(fakeOrchestratorFactory.createdOrchestratorForDisplay(displayId = DISPLAY_3)!!) - .start() - assertThat( - fakeOrchestratorFactory.createdOrchestratorForDisplay( - displayId = DISPLAY_4_NO_SYSTEM_DECOR - ) - ) - .isNull() - } - - @Test - fun displayAddedDuringStart_privacyDotForNewDisplay() = - testScope.runTest { - underTest.start() - - fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - runCurrent() + fakeDisplayRepository.triggerAddDisplaySystemDecorationEvent( + displayId = DEFAULT_DISPLAY + ) + fakeDisplayRepository.triggerAddDisplaySystemDecorationEvent(displayId = DISPLAY_2) - verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_3)).start() - verify(fakePrivacyDotStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) - .start() + verify(fakeLightBarStore.forDisplay(displayId = DEFAULT_DISPLAY), never()).start() + verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_2), never()).start() } @Test - fun displayAddedDuringStart_lightBarForNewDisplayCreate() = + fun start_triggerAddDisplaySystemDecoration_createsLightBarControllerForDisplay() = testScope.runTest { underTest.start() - - fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) runCurrent() - assertThat(fakeLightBarStore.perDisplayMocks.keys).containsExactly(DISPLAY_3) - } - - @Test - fun displayAddedDuringStart_lightBarForNewDisplayStart() = - testScope.runTest { - underTest.start() - - fakeDisplayRepository.addDisplay(displayId = DISPLAY_3) - fakeDisplayRepository.addDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR) - runCurrent() + fakeDisplayRepository.triggerAddDisplaySystemDecorationEvent( + displayId = DEFAULT_DISPLAY + ) + fakeDisplayRepository.triggerAddDisplaySystemDecorationEvent(displayId = DISPLAY_2) - verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_3), never()).start() - verify(fakeLightBarStore.forDisplay(displayId = DISPLAY_4_NO_SYSTEM_DECOR), never()) - .start() + assertThat(fakeLightBarStore.perDisplayMocks.keys) + .containsExactly(DEFAULT_DISPLAY, DISPLAY_2) } companion object { - const val DISPLAY_1 = 1 const val DISPLAY_2 = 2 - const val DISPLAY_3 = 3 - const val DISPLAY_4_NO_SYSTEM_DECOR = 4 } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/LightBarControllerStoreImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/LightBarControllerStoreImplTest.kt index 884c35c3457d..500332fa4a26 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/LightBarControllerStoreImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/LightBarControllerStoreImplTest.kt @@ -65,11 +65,11 @@ class LightBarControllerStoreImplTest : SysuiTestCase() { } @Test - fun displayRemoved_stopsInstance() = + fun systemDecorationRemovedEvent_stopsInstance() = testScope.runTest { val instance = underTest.forDisplay(DEFAULT_DISPLAY)!! - fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY) + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(DEFAULT_DISPLAY) verify(instance).stop() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayDarkIconDispatcherStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayDarkIconDispatcherStoreTest.kt index f37648a639df..62ead9b45adc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayDarkIconDispatcherStoreTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayDarkIconDispatcherStoreTest.kt @@ -62,11 +62,11 @@ class MultiDisplayDarkIconDispatcherStoreTest : SysuiTestCase() { } @Test - fun displayRemoved_stopsInstance() = + fun systemDecorationRemovedEvent_stopsInstance() = testScope.runTest { val instance = underTest.forDisplay(DEFAULT_DISPLAY)!! - fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY) + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(DEFAULT_DISPLAY) verify(instance).stop() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt index e0a1f273aa44..486a84598410 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt @@ -65,11 +65,11 @@ class MultiDisplayStatusBarContentInsetsProviderStoreTest : SysuiTestCase() { } @Test - fun displayRemoved_stopsInstance() = + fun systemDecorationRemovedEvent_stopsInstance() = testScope.runTest { val instance = underTest.forDisplay(DEFAULT_DISPLAY)!! - fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY) + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(DEFAULT_DISPLAY) verify(instance).stop() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarModeRepositoryStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarModeRepositoryStoreTest.kt index 11fd902fc50c..2c474da082bf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarModeRepositoryStoreTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarModeRepositoryStoreTest.kt @@ -59,11 +59,11 @@ class MultiDisplayStatusBarModeRepositoryStoreTest : SysuiTestCase() { } @Test - fun displayRemoved_stopsInstance() = + fun systemDecorationRemovedEvent_stopsInstance() = testScope.runTest { val instance = underTest.forDisplay(DEFAULT_DISPLAY)!! - fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY) + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(DEFAULT_DISPLAY) verify(instance).stop() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStoreImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStoreImplTest.kt index a5b7fc283976..bc7d47c52531 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStoreImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStoreImplTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.testKosmos import kotlinx.coroutines.runBlocking @@ -36,7 +37,7 @@ import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) class PrivacyDotWindowControllerStoreImplTest : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val underTest by lazy { kosmos.privacyDotWindowControllerStoreImpl } @@ -58,11 +59,11 @@ class PrivacyDotWindowControllerStoreImplTest : SysuiTestCase() { } @Test - fun displayRemoved_stopsInstance() = + fun systemDecorationRemovedEvent_stopsInstance() = testScope.runTest { val instance = underTest.forDisplay(DISPLAY_2)!! - kosmos.displayRepository.removeDisplay(DISPLAY_2) + kosmos.displayRepository.triggerRemoveSystemDecorationEvent(DISPLAY_2) verify(instance).stop() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayStoreImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayStoreImplTest.kt new file mode 100644 index 000000000000..41ae377a13f7 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayStoreImplTest.kt @@ -0,0 +1,88 @@ +/* + * 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.data.repository + +import android.view.Display +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class StatusBarPerDisplayStoreImplTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val testScope = kosmos.testScope + private val fakeDisplayRepository = kosmos.displayRepository + + private val store = kosmos.fakeStatusBarPerDisplayStore + + @Before + fun start() { + store.start() + } + + @Before + fun addDisplays() = runBlocking { + fakeDisplayRepository.addDisplay(DEFAULT_DISPLAY_ID) + fakeDisplayRepository.addDisplay(NON_DEFAULT_DISPLAY_ID) + } + + @Test + fun removeSystemDecoration_onDisplayRemovalActionInvoked() = + testScope.runTest { + val instance = store.forDisplay(NON_DEFAULT_DISPLAY_ID) + + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(NON_DEFAULT_DISPLAY_ID) + + assertThat(store.removalActions).containsExactly(instance) + } + + @Test + fun removeSystemDecoration_twice_onDisplayRemovalActionInvokedOnce() = + testScope.runTest { + val instance = store.forDisplay(NON_DEFAULT_DISPLAY_ID) + + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(NON_DEFAULT_DISPLAY_ID) + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(NON_DEFAULT_DISPLAY_ID) + + assertThat(store.removalActions).containsExactly(instance) + } + + @Test + fun forDisplay_withoutDisplayRemoval_onDisplayRemovalActionIsNotInvoked() = + testScope.runTest { + store.forDisplay(NON_DEFAULT_DISPLAY_ID) + + assertThat(store.removalActions).isEmpty() + } + + companion object { + private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY + private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/SystemEventChipAnimationControllerStoreImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/SystemEventChipAnimationControllerStoreImplTest.kt index 3cc592c94678..94394ede819e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/SystemEventChipAnimationControllerStoreImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/SystemEventChipAnimationControllerStoreImplTest.kt @@ -62,11 +62,11 @@ class SystemEventChipAnimationControllerStoreImplTest : SysuiTestCase() { } @Test - fun displayRemoved_stopsInstance() = + fun systemDecorationRemovedEvent_stopsInstance() = testScope.runTest { val instance = underTest.forDisplay(DEFAULT_DISPLAY)!! - fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY) + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(DEFAULT_DISPLAY) verify(instance).stop() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/MultiDisplayAutoHideControllerStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/MultiDisplayAutoHideControllerStoreTest.kt index d16372611e88..b9d9a53fd319 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/MultiDisplayAutoHideControllerStoreTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/MultiDisplayAutoHideControllerStoreTest.kt @@ -62,11 +62,11 @@ class MultiDisplayAutoHideControllerStoreTest : SysuiTestCase() { } @Test - fun displayRemoved_stopsInstance() = + fun systemDecorationRemovedEvent_stopsInstance() = testScope.runTest { val instance = underTest.forDisplay(DEFAULT_DISPLAY)!! - fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY) + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(DEFAULT_DISPLAY) verify(instance).stop() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt index 769f012bfdf7..8722a484417d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.testKosmos import kotlinx.coroutines.runBlocking @@ -37,7 +38,7 @@ import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) class MultiDisplayStatusBarWindowControllerStoreTest : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val fakeDisplayRepository = kosmos.displayRepository private val testScope = kosmos.testScope @@ -59,11 +60,11 @@ class MultiDisplayStatusBarWindowControllerStoreTest : SysuiTestCase() { } @Test - fun displayRemoved_stopsInstance() = + fun systemDecorationRemovedEvent_stopsInstance() = testScope.runTest { val instance = underTest.forDisplay(DEFAULT_DISPLAY)!! - fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY) + fakeDisplayRepository.triggerRemoveSystemDecorationEvent(DEFAULT_DISPLAY) verify(instance).stop() } diff --git a/packages/SystemUI/src/com/android/systemui/common/domain/interactor/SysUIStateDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/SysUIStateDisplaysInteractor.kt new file mode 100644 index 000000000000..097d50bb8f9d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/SysUIStateDisplaysInteractor.kt @@ -0,0 +1,45 @@ +/* + * 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.common.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.display.data.repository.PerDisplayRepository +import com.android.systemui.model.StateChange +import com.android.systemui.model.SysUiState +import javax.inject.Inject + +/** Handles [SysUiState] changes between displays. */ +@SysUISingleton +class SysUIStateDisplaysInteractor +@Inject +constructor(private val sysUIStateRepository: PerDisplayRepository<SysUiState>) { + + /** + * Sets the flags on the given [targetDisplayId] based on the [stateChanges], while making sure + * that those flags are not set in any other display. + */ + fun setFlagsExclusivelyToDisplay(targetDisplayId: Int, stateChanges: StateChange) { + sysUIStateRepository.forEachInstance { displayId, instance -> + if (displayId == targetDisplayId) { + stateChanges.applyTo(instance) + } else { + stateChanges.clearAllChangedFlagsIn(instance) + } + } + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt new file mode 100644 index 000000000000..083191c8ecde --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt @@ -0,0 +1,40 @@ +/* + * 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.display.data.repository + +class FakePerDisplayRepository<T> : PerDisplayRepository<T> { + + private val instances = mutableMapOf<Int, T>() + + fun add(displayId: Int, instance: T) { + instances[displayId] = instance + } + + fun remove(displayId: Int) { + instances.remove(displayId) + } + + override fun get(displayId: Int): T? { + return instances[displayId] + } + + override val displayIds: Set<Int> + get() = instances.keys + + override val debugName: String + get() = "FakePerDisplayRepository" +} diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt index 63c46bb30a07..04f245e91914 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt @@ -86,8 +86,23 @@ interface PerDisplayRepository<T> { /** Gets the cached instance or create a new one for a given display. */ operator fun get(displayId: Int): T? + /** List of display ids for which this repository has an instance. */ + val displayIds: Set<Int> + /** Debug name for this repository, mainly for tracing and logging. */ val debugName: String + + /** + * Invokes the specified action on each instance held by this repository. + * + * The action will receive the displayId and the instance associated with that display. + * If there is no instance for the display, the action is not called. + */ + fun forEachInstance(action: (Int, T) -> Unit) { + displayIds.forEach { displayId -> + get(displayId)?.let { instance -> action(displayId, instance) } + } + } } /** @@ -119,6 +134,9 @@ constructor( backgroundApplicationScope.launch("$debugName#start") { start() } } + override val displayIds: Set<Int> + get() = perDisplayInstances.keys + private suspend fun start() { dumpManager.registerDumpable(this) displayRepository.displayIds.collectLatest { displayIds -> @@ -186,6 +204,7 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>( private val lazyDefaultDisplayInstance by lazy { instanceProvider.createInstance(Display.DEFAULT_DISPLAY) } + override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY) override fun get(displayId: Int): T? = lazyDefaultDisplayInstance } diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt index 81aca27b9c28..46048868f503 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt @@ -50,7 +50,7 @@ abstract class PerDisplayStoreImpl<T>( private val displayRepository: DisplayRepository, ) : PerDisplayStore<T>, CoreStartable { - private val perDisplayInstances = ConcurrentHashMap<Int, T>() + protected val perDisplayInstances = ConcurrentHashMap<Int, T>() /** * The instance for the default/main display of the device. For example, on a phone or a tablet, diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt b/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt new file mode 100644 index 000000000000..f29b15730986 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt @@ -0,0 +1,82 @@ +/* + * 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.model + +/** + * Represents a set of state changes. A bit can either be set to `true` or `false`. + * + * This is used in [SysUIStateDisplaysInteractor] to selectively change bits. + */ +class StateChange { + private var flagsToSet: Long = 0 + private var flagsToClear: Long = 0 + + /** + * Sets the [state] of the given [bit]. + * + * @return `this` for chaining purposes + */ + fun setFlag(bit: Long, state: Boolean): StateChange { + if (state) { + flagsToSet = flagsToSet or bit + flagsToClear = flagsToClear and bit.inv() + } else { + flagsToClear = flagsToClear or bit + flagsToSet = flagsToSet and bit.inv() + } + return this + } + + fun hasChanges() = flagsToSet != 0L || flagsToClear != 0L + + /** Applies all changed flags to [sysUiState]. */ + fun applyTo(sysUiState: SysUiState) { + iterateBits(flagsToSet or flagsToClear) { bit -> + val isBitSetInNewState = flagsToSet and bit != 0L + sysUiState.setFlag(bit, isBitSetInNewState) + } + sysUiState.commitUpdate() + } + + fun applyTo(sysUiState: Long): Long { + var newState = sysUiState + newState = newState or flagsToSet + newState = newState and flagsToClear.inv() + return newState + } + + private inline fun iterateBits(flags: Long, action: (bit: Long) -> Unit) { + var remaining = flags + while (remaining != 0L) { + val lowestBit = remaining and -remaining + action(lowestBit) + + remaining -= lowestBit + } + } + + /** Clears all the flags changed in a [sysUiState] */ + fun clearAllChangedFlagsIn(sysUiState: SysUiState) { + iterateBits(flagsToSet or flagsToClear) { bit -> sysUiState.setFlag(bit, false) } + sysUiState.commitUpdate() + } + + fun clear() { + flagsToSet = 0 + flagsToClear = 0 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUIStateDispatcher.kt b/packages/SystemUI/src/com/android/systemui/model/SysUIStateDispatcher.kt new file mode 100644 index 000000000000..f95ae2501acb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/model/SysUIStateDispatcher.kt @@ -0,0 +1,77 @@ +/* + * 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.model + +import android.view.Display +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject + +/** + * Channels changes from several [SysUiState]s to a single callback. + * + * There are several [SysUiState]s (one per display). This class allows for listeners to listen to + * sysui state updates from any of those [SysUiState] instances. + * + * ┌────────────────────┐ + * │ SysUIStateOverride │ + * │ displayId=2 │ + * └┬───────────────────┘ + * │ ▲ + * ┌───────────────┐ │ │ ┌────────────────────┐ + * │ SysUIState │ │ │ │ SysUIStateOverride │ + * │ displayId=0 │ │ │ │ displayId=1 │ + * └────────────┬──┘ │ │ └┬───────────────────┘ + * │ │ │ │ ▲ + * ▼ ▼ │ ▼ │ + * ┌─────────────┴─────┴─┐ + * │SysUiStateDispatcher │ + * └────────┬────────────┘ + * │ + * ▼ + * ┌──────────────────┐ + * │ listeners for │ + * │ all displays │ + * └──────────────────┘ + */ +@SysUISingleton +class SysUIStateDispatcher @Inject constructor() { + + private val listeners = CopyOnWriteArrayList<SysUiState.SysUiStateCallback>() + + /** Called from each [SysUiState] to propagate new state changes. */ + fun dispatchSysUIStateChange(sysUiFlags: Long, displayId: Int) { + if (displayId != Display.DEFAULT_DISPLAY && !ShadeWindowGoesAround.isEnabled) return; + listeners.forEach { listener -> + listener.onSystemUiStateChanged(sysUiFlags = sysUiFlags, displayId = displayId) + } + } + + /** + * Registers a listener to listen for system UI state changes. + * + * Listeners will have [SysUiState.SysUiStateCallback.onSystemUiStateChanged] called whenever a + * system UI state changes. + */ + fun registerListener(listener: SysUiState.SysUiStateCallback) { + listeners += listener + } + + fun unregisterListener(listener: SysUiState.SysUiStateCallback) { + listeners -= listener + } +} diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt index 9e9a1df76b54..53105b2c0f6a 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt @@ -16,7 +16,6 @@ package com.android.systemui.model import android.util.Log -import android.view.Display import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.display.data.repository.PerDisplayInstanceProviderWithTeardown @@ -44,7 +43,9 @@ interface SysUiState : Dumpable { fun removeCallback(callback: SysUiStateCallback) /** Returns whether a flag is enabled in this state. */ - fun isFlagEnabled(@SystemUiStateFlags flag: Long): Boolean + fun isFlagEnabled(@SystemUiStateFlags flag: Long): Boolean { + return (flags and flag) != 0L + } /** Returns the current sysui state flags. */ val flags: Long @@ -59,19 +60,11 @@ interface SysUiState : Dumpable { /** Call to save all the flags updated from [setFlag]. */ fun commitUpdate() - /** Notify all those who are registered that the state has changed. */ - fun notifyAndSetSystemUiStateChanged(newFlags: Long, oldFlags: Long) - /** Callback to be notified whenever system UI state flags are changed. */ - interface SysUiStateCallback { - /** To be called when any SysUiStateFlag gets updated **for the default display** */ - fun onSystemUiStateChanged(@SystemUiStateFlags sysUiFlags: Long) + fun interface SysUiStateCallback { /** To be called when any SysUiStateFlag gets updated for a specific [displayId]. */ - fun onSystemUiStateChangedForDisplay( - @SystemUiStateFlags sysUiFlags: Long, - displayId: Int, - ) {} + fun onSystemUiStateChanged(@SystemUiStateFlags sysUiFlags: Long, displayId: Int) } /** @@ -86,12 +79,15 @@ interface SysUiState : Dumpable { } } +private const val TAG = "SysUIState" + class SysUiStateImpl @AssistedInject constructor( @Assisted private val displayId: Int, private val sceneContainerPlugin: SceneContainerPlugin?, private val dumpManager: DumpManager, + private val stateDispatcher: SysUIStateDispatcher, ) : SysUiState { private val debugName = "SysUiStateImpl-ForDisplay=$displayId" @@ -107,45 +103,30 @@ constructor( get() = _flags private var _flags: Long = 0 - private val callbacks: MutableList<SysUiStateCallback> = ArrayList() private var flagsToSet: Long = 0 private var flagsToClear: Long = 0 /** * Add listener to be notified of changes made to SysUI state. The callback will also be called * as part of this function. + * + * Note that the listener would receive updates for all displays. */ override fun addCallback(callback: SysUiStateCallback) { - callbacks.add(callback) - callback.onSystemUiStateChanged(flags) + stateDispatcher.registerListener(callback) + callback.onSystemUiStateChanged(flags, displayId) } /** Callback will no longer receive events on state change */ override fun removeCallback(callback: SysUiStateCallback) { - callbacks.remove(callback) - } - - override fun isFlagEnabled(@SystemUiStateFlags flag: Long): Boolean { - return (flags and flag) != 0L + stateDispatcher.unregisterListener(callback) } /** Methods to this call can be chained together before calling [.commitUpdate]. */ override fun setFlag(@SystemUiStateFlags flag: Long, enabled: Boolean): SysUiState { - var enabled = enabled - val overrideOrNull = - sceneContainerPlugin?.flagValueOverride(flag = flag, displayId = displayId) - if (overrideOrNull != null && enabled != overrideOrNull) { - if (SysUiState.DEBUG) { - Log.d( - TAG, - "setFlag for flag $flag and value $enabled overridden to $overrideOrNull by scene container plugin", - ) - } - - enabled = overrideOrNull - } + val toSet = flagWithOptionalOverrides(flag, enabled, displayId, sceneContainerPlugin) - if (enabled) { + if (toSet) { flagsToSet = flagsToSet or flag } else { flagsToClear = flagsToClear or flag @@ -176,22 +157,13 @@ constructor( } /** Notify all those who are registered that the state has changed. */ - override fun notifyAndSetSystemUiStateChanged(newFlags: Long, oldFlags: Long) { + private fun notifyAndSetSystemUiStateChanged(newFlags: Long, oldFlags: Long) { if (SysUiState.DEBUG) { Log.d(TAG, "SysUiState changed: old=$oldFlags new=$newFlags") } if (newFlags != oldFlags) { - callbacks.forEach { callback: SysUiStateCallback -> - if (displayId == Display.DEFAULT_DISPLAY) { - callback.onSystemUiStateChanged(newFlags) - } - callback.onSystemUiStateChangedForDisplay( - sysUiFlags = newFlags, - displayId = displayId, - ) - } - _flags = newFlags + stateDispatcher.dispatchSysUIStateChange(newFlags, displayId) } } @@ -222,6 +194,29 @@ constructor( } } +/** Returns the flag value taking into account [SceneContainerPlugin] potential overrides. */ +fun flagWithOptionalOverrides( + flag: Long, + enabled: Boolean, + displayId: Int, + sceneContainerPlugin: SceneContainerPlugin?, +): Boolean { + var toSet = enabled + val overrideOrNull = sceneContainerPlugin?.flagValueOverride(flag = flag, displayId = displayId) + if (overrideOrNull != null && toSet != overrideOrNull) { + if (SysUiState.DEBUG) { + Log.d( + TAG, + "setFlag for flag $flag and value $toSet overridden to " + + "$overrideOrNull by scene container plugin", + ) + } + + toSet = overrideOrNull + } + return toSet +} + /** Creates and destroy instances of [SysUiState] */ @SysUISingleton class SysUIStateInstanceProvider @Inject constructor(private val factory: SysUiStateImpl.Factory) : diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index f44c2c01951c..7af538105cae 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -355,11 +355,12 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final SysUiState.SysUiStateCallback mSysUiStateCallback = new SysUiState.SysUiStateCallback() { - @Override - public void onSystemUiStateChanged(@SystemUiStateFlags long sysUiFlags) { - mSysUiFlags = sysUiFlags; - } - }; + @Override + public void onSystemUiStateChanged(@SystemUiStateFlags long sysUiFlags, + int displayId) { + mSysUiFlags = sysUiFlags; + } + }; private final Consumer<Boolean> mOnIsInPipStateChangedListener = (isInPip) -> mIsInPip = isInPip; diff --git a/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java index 8253a071dea2..7a6426c741a6 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java @@ -656,11 +656,7 @@ public class LauncherProxyService implements CallbackController<LauncherProxyLis private final SysUiStateCallback mSysUiStateCallback = new SysUiStateCallback() { @Override - public void onSystemUiStateChanged(long sysUiFlags) { - } - - @Override - public void onSystemUiStateChangedForDisplay(long sysUiFlags, int displayId) { + public void onSystemUiStateChanged(long sysUiFlags, int displayId) { notifySystemUiStateFlags(sysUiFlags, displayId); } }; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 446935bf6594..17fb50aa6890 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -65,6 +65,7 @@ import android.os.Trace; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; +import android.view.Display; import android.view.HapticFeedbackConstants; import android.view.InputDevice; import android.view.MotionEvent; @@ -96,6 +97,7 @@ import com.android.systemui.Gefingerpoken; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.common.domain.interactor.SysUIStateDisplaysInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; @@ -119,6 +121,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.controller.KeyguardMediaController; import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager; +import com.android.systemui.model.StateChange; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; @@ -450,7 +453,9 @@ public final class NotificationPanelViewController implements private final MediaDataManager mMediaDataManager; @PanelState private int mCurrentPanelState = STATE_CLOSED; + @Deprecated // Use SysUIStateInteractor instead private final SysUiState mSysUiState; + private final SysUIStateDisplaysInteractor mSysUIStateDisplaysInteractor; private final NotificationShadeDepthController mDepthController; private final NavigationBarController mNavigationBarController; private final int mDisplayId; @@ -607,6 +612,7 @@ public final class NotificationPanelViewController implements ShadeRepository shadeRepository, Optional<SysUIUnfoldComponent> unfoldComponent, SysUiState sysUiState, + SysUIStateDisplaysInteractor sysUIStateDisplaysInteractor, KeyguardUnlockAnimationController keyguardUnlockAnimationController, KeyguardIndicationController keyguardIndicationController, NotificationListContainer notificationListContainer, @@ -738,6 +744,7 @@ public final class NotificationPanelViewController implements mMediaDataManager = mediaDataManager; mTapAgainViewController = tapAgainViewController; mSysUiState = sysUiState; + mSysUIStateDisplaysInteractor = sysUIStateDisplaysInteractor; mKeyguardBypassController = bypassController; mUpdateMonitor = keyguardUpdateMonitor; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; @@ -2701,13 +2708,41 @@ public final class NotificationPanelViewController implements Log.d(TAG, "Updating panel sysui state flags: fullyExpanded=" + isFullyExpanded() + " inQs=" + mQsController.getExpanded()); } + if (ShadeWindowGoesAround.isEnabled()) { + setPerDisplaySysUIStateFlags(); + } else { + setDefaultDisplayFlags(); + } + } + + private int getShadeDisplayId() { + if (mView != null && mView.getDisplay() != null) return mView.getDisplay().getDisplayId(); + return Display.DEFAULT_DISPLAY; + } + + private void setPerDisplaySysUIStateFlags() { + mSysUIStateDisplaysInteractor.setFlagsExclusivelyToDisplay( + getShadeDisplayId(), + new StateChange() + .setFlag(SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE, + isPanelExpanded() && !isCollapsing()) + .setFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED, + isFullyExpanded() && !mQsController.getExpanded()) + .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, + isFullyExpanded() && mQsController.getExpanded()) + ); + } + + @Deprecated + private void setDefaultDisplayFlags() { mSysUiState .setFlag(SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE, isPanelExpanded() && !isCollapsing()) .setFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED, isFullyExpanded() && !mQsController.getExpanded()) .setFlag(SYSUI_STATE_QUICK_SETTINGS_EXPANDED, - isFullyExpanded() && mQsController.getExpanded()).commitUpdate(mDisplayId); + isFullyExpanded() && mQsController.getExpanded()).commitUpdate( + mDisplayId); } private void debugLog(String fmt, Object... args) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarOrchestratorStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarOrchestratorStore.kt index 90c6a8a9f51c..7964950a2917 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarOrchestratorStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarOrchestratorStore.kt @@ -16,25 +16,19 @@ package com.android.systemui.statusbar.core -import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayScopeRepository -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore +import com.android.systemui.statusbar.data.repository.StatusBarPerDisplayStoreImpl import com.android.systemui.statusbar.phone.AutoHideControllerStore import com.android.systemui.statusbar.window.StatusBarWindowControllerStore import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore -import dagger.Lazy -import dagger.Module -import dagger.Provides -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -/** [PerDisplayStoreImpl] for providing display specific [StatusBarOrchestrator]. */ +/** [StatusBarPerDisplayStoreImpl] for providing display specific [StatusBarOrchestrator]. */ @SysUISingleton class MultiDisplayStatusBarOrchestratorStore @Inject @@ -48,7 +42,11 @@ constructor( private val autoHideControllerStore: AutoHideControllerStore, private val displayScopeRepository: DisplayScopeRepository, private val statusBarWindowStateRepositoryStore: StatusBarWindowStateRepositoryStore, -) : PerDisplayStoreImpl<StatusBarOrchestrator>(backgroundApplicationScope, displayRepository) { +) : + StatusBarPerDisplayStoreImpl<StatusBarOrchestrator>( + backgroundApplicationScope, + displayRepository, + ) { init { StatusBarConnectedDisplays.unsafeAssertInNewMode() @@ -79,21 +77,3 @@ constructor( instance.stop() } } - -@Module -interface MultiDisplayStatusBarOrchestratorStoreModule { - - @Provides - @SysUISingleton - @IntoMap - @ClassKey(MultiDisplayStatusBarOrchestratorStore::class) - fun storeAsCoreStartable( - multiDisplayLazy: Lazy<MultiDisplayStatusBarOrchestratorStore> - ): CoreStartable { - return if (StatusBarConnectedDisplays.isEnabled) { - multiDisplayLazy.get() - } else { - CoreStartable.NOP - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarter.kt index 4d92814ef82d..30c4ca5269d3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarter.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.core import android.view.Display -import android.view.IWindowManager import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton @@ -44,7 +43,6 @@ constructor( private val statusBarInitializerStore: StatusBarInitializerStore, private val privacyDotWindowControllerStore: PrivacyDotWindowControllerStore, private val lightBarControllerStore: LightBarControllerStore, - private val windowManager: IWindowManager, ) : CoreStartable { init { @@ -53,19 +51,13 @@ constructor( override fun start() { applicationScope.launch { - displayRepository.displays + displayRepository.displayIdsWithSystemDecorations .pairwiseBy { previousDisplays, currentDisplays -> currentDisplays - previousDisplays } - .onStart { emit(displayRepository.displays.value) } + .onStart { emit(displayRepository.displayIdsWithSystemDecorations.value) } .collect { newDisplays -> - newDisplays.forEach { - // TODO(b/393191204): Split navbar, status bar, etc. functionality - // from WindowManager#shouldShowSystemDecors. - if (windowManager.shouldShowSystemDecors(it.displayId)) { - createAndStartComponentsForDisplay(it.displayId) - } - } + newDisplays.forEach { createAndStartComponentsForDisplay(it) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt index 69bbb28d94bc..48955cc4ab21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt @@ -20,11 +20,11 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore import com.android.systemui.statusbar.data.repository.StatusBarConfigurationControllerStore import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore +import com.android.systemui.statusbar.data.repository.StatusBarPerDisplayStoreImpl import com.android.systemui.statusbar.window.StatusBarWindowControllerStore import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -45,7 +45,10 @@ constructor( private val darkIconDispatcherStore: DarkIconDispatcherStore, ) : StatusBarInitializerStore, - PerDisplayStoreImpl<StatusBarInitializer>(backgroundApplicationScope, displayRepository) { + StatusBarPerDisplayStoreImpl<StatusBarInitializer>( + backgroundApplicationScope, + displayRepository, + ) { init { StatusBarConnectedDisplays.unsafeAssertInNewMode() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/DarkIconDispatcherStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/DarkIconDispatcherStore.kt index 2eeeea8ad93c..f353c0198b1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/DarkIconDispatcherStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/DarkIconDispatcherStore.kt @@ -24,7 +24,6 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.statusbar.core.StatusBarConnectedDisplays @@ -59,7 +58,10 @@ constructor( private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository, ) : SysuiDarkIconDispatcherStore, - PerDisplayStoreImpl<SysuiDarkIconDispatcher>(backgroundApplicationScope, displayRepository) { + StatusBarPerDisplayStoreImpl<SysuiDarkIconDispatcher>( + backgroundApplicationScope, + displayRepository, + ) { init { StatusBarConnectedDisplays.unsafeAssertInNewMode() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/LightBarControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/LightBarControllerStore.kt index c629d10b90b0..3c0d6c3b8df3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/LightBarControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/LightBarControllerStore.kt @@ -22,7 +22,6 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayScopeRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.statusbar.phone.LightBarController import com.android.systemui.statusbar.phone.LightBarControllerImpl import dagger.Binds @@ -47,7 +46,10 @@ constructor( private val darkIconDispatcherStore: DarkIconDispatcherStore, ) : LightBarControllerStore, - PerDisplayStoreImpl<LightBarController>(backgroundApplicationScope, displayRepository) { + StatusBarPerDisplayStoreImpl<LightBarController>( + backgroundApplicationScope, + displayRepository, + ) { override fun createInstanceForDisplay(displayId: Int): LightBarController? { val darkIconDispatcher = darkIconDispatcherStore.forDisplay(displayId) ?: return null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotViewControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotViewControllerStore.kt index d48c94bd0893..32dc8407ac90 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotViewControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotViewControllerStore.kt @@ -22,7 +22,6 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayScopeRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.events.PrivacyDotViewController @@ -50,7 +49,10 @@ constructor( private val contentInsetsProviderStore: StatusBarContentInsetsProviderStore, ) : PrivacyDotViewControllerStore, - PerDisplayStoreImpl<PrivacyDotViewController>(backgroundApplicationScope, displayRepository) { + StatusBarPerDisplayStoreImpl<PrivacyDotViewController>( + backgroundApplicationScope, + displayRepository, + ) { override fun createInstanceForDisplay(displayId: Int): PrivacyDotViewController? { val configurationController = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStore.kt index 60579318e667..7fc5e8abe904 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStore.kt @@ -25,7 +25,6 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.events.PrivacyDotWindowController import dagger.Binds @@ -52,7 +51,10 @@ constructor( private val viewCaptureAwareWindowManagerFactory: ViewCaptureAwareWindowManager.Factory, ) : PrivacyDotWindowControllerStore, - PerDisplayStoreImpl<PrivacyDotWindowController>(backgroundApplicationScope, displayRepository) { + StatusBarPerDisplayStoreImpl<PrivacyDotWindowController>( + backgroundApplicationScope, + displayRepository, + ) { init { StatusBarConnectedDisplays.unsafeAssertInNewMode() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationControllerStore.kt index 751ddf4520eb..36b11ac60827 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationControllerStore.kt @@ -24,7 +24,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.phone.ConfigurationControllerImpl @@ -53,7 +52,7 @@ constructor( private val configurationControllerFactory: ConfigurationControllerImpl.Factory, ) : StatusBarConfigurationControllerStore, - PerDisplayStoreImpl<StatusBarConfigurationController>( + StatusBarPerDisplayStoreImpl<StatusBarConfigurationController>( backgroundApplicationScope, displayRepository, ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt index 5ea12110b00c..3cd4b5c885de 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt @@ -25,7 +25,6 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider @@ -54,7 +53,7 @@ constructor( private val cameraProtectionLoaderFactory: CameraProtectionLoaderImpl.Factory, ) : StatusBarContentInsetsProviderStore, - PerDisplayStoreImpl<StatusBarContentInsetsProvider>( + StatusBarPerDisplayStoreImpl<StatusBarContentInsetsProvider>( backgroundApplicationScope, displayRepository, ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryStore.kt index e82711d12399..5980c9c57214 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryStore.kt @@ -22,7 +22,6 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.DisplayId import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.core.StatusBarInitializer import com.android.systemui.statusbar.phone.fragment.dagger.HomeStatusBarComponent @@ -48,7 +47,7 @@ constructor( displayRepository: DisplayRepository, ) : StatusBarModeRepositoryStore, - PerDisplayStoreImpl<StatusBarModePerDisplayRepository>( + StatusBarPerDisplayStoreImpl<StatusBarModePerDisplayRepository>( backgroundApplicationScope, displayRepository, ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayStoreImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayStoreImpl.kt new file mode 100644 index 000000000000..e873c02fd4d5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayStoreImpl.kt @@ -0,0 +1,47 @@ +/* + * 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.data.repository + +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.data.repository.DisplayRepository +import com.android.systemui.display.data.repository.PerDisplayStoreImpl +import com.android.systemui.util.kotlin.pairwiseBy +import kotlinx.coroutines.CoroutineScope + +/** [PerDisplayStoreImpl] for Status Bar related classes. */ +abstract class StatusBarPerDisplayStoreImpl<T>( + @Background private val backgroundApplicationScope: CoroutineScope, + private val displayRepository: DisplayRepository, +) : PerDisplayStoreImpl<T>(backgroundApplicationScope, displayRepository) { + + override fun start() { + val instanceType = instanceClass.simpleName + backgroundApplicationScope.launch("StatusBarPerDisplayStore#<$instanceType>start") { + displayRepository.displayIdsWithSystemDecorations + .pairwiseBy { previousDisplays, currentDisplays -> + previousDisplays - currentDisplays + } + .collect { removedDisplayIds -> + removedDisplayIds.forEach { removedDisplayId -> + val removedInstance = perDisplayInstances.remove(removedDisplayId) + removedInstance?.let { onDisplayRemovalAction(it) } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/SystemEventChipAnimationControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/SystemEventChipAnimationControllerStore.kt index 439925a6d4b8..7b9ea697a9ac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/SystemEventChipAnimationControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/SystemEventChipAnimationControllerStore.kt @@ -23,7 +23,6 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.events.SystemEventChipAnimationController import com.android.systemui.statusbar.events.SystemEventChipAnimationControllerImpl @@ -53,7 +52,7 @@ constructor( private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, ) : SystemEventChipAnimationControllerStore, - PerDisplayStoreImpl<SystemEventChipAnimationController>( + StatusBarPerDisplayStoreImpl<SystemEventChipAnimationController>( backgroundApplicationScope, displayRepository, ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt index d2dccc49ffd7..d7e9bb2ad64a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt @@ -21,10 +21,10 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore +import com.android.systemui.statusbar.data.repository.StatusBarPerDisplayStoreImpl import dagger.Lazy import dagger.Module import dagger.Provides @@ -45,7 +45,7 @@ constructor( private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, ) : StatusBarContentInsetsViewModelStore, - PerDisplayStoreImpl<StatusBarContentInsetsViewModel>( + StatusBarPerDisplayStoreImpl<StatusBarContentInsetsViewModel>( backgroundApplicationScope, displayRepository, ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index 4e916804318d..fcb63df1a528 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -409,12 +409,12 @@ constructor( if (counter != null) { if (NotificationBundleUi.isEnabled) { - val entry = (currentNotification as? ExpandableNotificationRow)?.entry - counter.incrementForBucket(entry?.bucket) - } else { val entryAdapter = (currentNotification as? ExpandableNotificationRow)?.entryAdapter counter.incrementForBucket(entryAdapter?.sectionBucket) + } else { + val entry = (currentNotification as? ExpandableNotificationRow)?.entry + counter.incrementForBucket(entry?.bucket) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideControllerStore.kt index c7bbe2cbc846..616bab60e02f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideControllerStore.kt @@ -22,9 +22,9 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.data.repository.StatusBarPerDisplayStoreImpl import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -41,7 +41,10 @@ constructor( private val autoHideControllerFactory: AutoHideControllerImpl.Factory, ) : AutoHideControllerStore, - PerDisplayStoreImpl<AutoHideController>(backgroundApplicationScope, displayRepository) { + StatusBarPerDisplayStoreImpl<AutoHideController>( + backgroundApplicationScope, + displayRepository, + ) { init { StatusBarConnectedDisplays.unsafeAssertInNewMode() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt index 323b7d8eaaeb..ba5570026c1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt @@ -21,6 +21,7 @@ import com.android.systemui.dagger.qualifiers.Default import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.core.CommandQueueInitializer import com.android.systemui.statusbar.core.MultiDisplayStatusBarInitializerStore +import com.android.systemui.statusbar.core.MultiDisplayStatusBarOrchestratorStore import com.android.systemui.statusbar.core.MultiDisplayStatusBarStarter import com.android.systemui.statusbar.core.SingleDisplayStatusBarInitializerStore import com.android.systemui.statusbar.core.StatusBarConnectedDisplays @@ -197,5 +198,19 @@ interface StatusBarPhoneModule { CoreStartable.NOP } } + + @Provides + @SysUISingleton + @IntoMap + @ClassKey(MultiDisplayStatusBarOrchestratorStore::class) + fun orchestratorStoreAsCoreStartable( + multiDisplayLazy: Lazy<MultiDisplayStatusBarOrchestratorStore> + ): CoreStartable { + return if (StatusBarConnectedDisplays.isEnabled) { + multiDisplayLazy.get() + } else { + CoreStartable.NOP + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt index f33367245ee6..39afc38dad11 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt @@ -24,11 +24,11 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository import com.android.systemui.display.data.repository.PerDisplayStore -import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.data.repository.StatusBarConfigurationControllerStore import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore +import com.android.systemui.statusbar.data.repository.StatusBarPerDisplayStoreImpl import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -48,7 +48,10 @@ constructor( displayRepository: DisplayRepository, ) : StatusBarWindowControllerStore, - PerDisplayStoreImpl<StatusBarWindowController>(backgroundApplicationScope, displayRepository) { + StatusBarPerDisplayStoreImpl<StatusBarWindowController>( + backgroundApplicationScope, + displayRepository, + ) { init { StatusBarConnectedDisplays.unsafeAssertInNewMode() diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index feaf1a630a53..8e713495902a 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -302,7 +302,7 @@ public final class WMShell implements .commitUpdate(mDisplayTracker.getDefaultDisplayId()); } }); - mSysUiState.addCallback(sysUiStateFlag -> { + mSysUiState.addCallback((sysUiStateFlag, displayId) -> { mIsSysUiStateValid = (sysUiStateFlag & INVALID_SYSUI_STATE_MASK) == 0; pip.onSystemUiStateChanged(mIsSysUiStateValid, sysUiStateFlag); }); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index a6730ed5c9be..341bd3a38999 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -465,7 +465,7 @@ public class BubblesTest extends SysuiTestCase { when(mZenModeController.getConfig()).thenReturn(mZenModeConfig); mSysUiState = mKosmos.getSysuiState(); - mSysUiState.addCallback(sysUiFlags -> { + mSysUiState.addCallback((sysUiFlags, displayId) -> { mSysUiStateBubblesManageMenuExpanded = (sysUiFlags & QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt index 70b22d7f829d..663a85330f70 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt @@ -53,6 +53,7 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { MutableSharedFlow<DisplayRepository.PendingDisplay?>(replay = 1) private val displayAdditionEventFlow = MutableSharedFlow<Display?>(replay = 0) private val displayRemovalEventFlow = MutableSharedFlow<Int>(replay = 0) + private val displayIdsWithSystemDecorationsFlow = MutableStateFlow<Set<Int>>(emptySet()) suspend fun addDisplay(displayId: Int, type: Int = Display.TYPE_EXTERNAL) { addDisplay(display(type, id = displayId)) @@ -74,6 +75,7 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { flow.value += display displayIdFlow.value += display.displayId displayAdditionEventFlow.emit(display) + displayIdsWithSystemDecorationsFlow.value += display.displayId } suspend fun removeDisplay(displayId: Int) { @@ -82,6 +84,16 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { displayRemovalEventFlow.emit(displayId) } + suspend fun triggerAddDisplaySystemDecorationEvent(displayId: Int) { + displayIdsWithSystemDecorationsFlow.value += displayId + displayIdsWithSystemDecorationsFlow.emit(displayIdsWithSystemDecorationsFlow.value) + } + + suspend fun triggerRemoveSystemDecorationEvent(displayId: Int) { + displayIdsWithSystemDecorationsFlow.value -= displayId + displayIdsWithSystemDecorationsFlow.emit(displayIdsWithSystemDecorationsFlow.value) + } + /** Emits [value] as [displayAdditionEvent] flow value. */ suspend fun emit(value: Display?) = displayAdditionEventFlow.emit(value) @@ -112,7 +124,8 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { private val _displayChangeEvent = MutableSharedFlow<Int>(replay = 1) override val displayChangeEvent: Flow<Int> = _displayChangeEvent - override val displayIdsWithSystemDecorations: StateFlow<Set<Int>> = MutableStateFlow(emptySet()) + override val displayIdsWithSystemDecorations: StateFlow<Set<Int>> = + displayIdsWithSystemDecorationsFlow suspend fun emitDisplayChangeEvent(displayId: Int) = _displayChangeEvent.emit(displayId) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 8543aaadbdfc..af89403c5397 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -53,6 +53,7 @@ import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel import com.android.systemui.model.sceneContainerPlugin +import com.android.systemui.model.sysUIStateDispatcher import com.android.systemui.model.sysUiState import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.power.data.repository.fakePowerRepository @@ -202,4 +203,5 @@ class KosmosJavaAdapter() { val windowRootViewBlurInteractor by lazy { kosmos.windowRootViewBlurInteractor } val sysuiState by lazy { kosmos.sysUiState } val displayTracker by lazy { kosmos.displayTracker } + val sysUIStateDispatcher by lazy { kosmos.sysUIStateDispatcher } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt index 6272aafaa688..00deaafd7009 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt @@ -17,11 +17,33 @@ package com.android.systemui.model import android.view.Display +import com.android.systemui.common.domain.interactor.SysUIStateDisplaysInteractor +import com.android.systemui.display.data.repository.FakePerDisplayRepository import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import org.mockito.Mockito.spy -val Kosmos.sysUiState by Fixture { - spy(SysUiStateImpl(Display.DEFAULT_DISPLAY, sceneContainerPlugin, dumpManager)) +val Kosmos.sysUiState by Fixture { sysUiStateFactory.create(Display.DEFAULT_DISPLAY) } +val Kosmos.sysUIStateDispatcher by Fixture { SysUIStateDispatcher() } + +val Kosmos.sysUiStateFactory by Fixture { + object : SysUiStateImpl.Factory { + override fun create(displayId: Int): SysUiStateImpl { + return spy( + SysUiStateImpl( + displayId, + sceneContainerPlugin, + dumpManager, + sysUIStateDispatcher, + ) + ) + } + } +} + +val Kosmos.fakeSysUIStatePerDisplayRepository by Fixture { FakePerDisplayRepository<SysUiState>() } + +val Kosmos.sysuiStateInteractor by Fixture { + SysUIStateDisplaysInteractor(fakeSysUIStatePerDisplayRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt index 9776fd91134d..66a17514c106 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.core import android.content.testableContext -import android.view.mockIWindowManager import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.display.data.repository.displayScopeRepository @@ -94,6 +93,5 @@ val Kosmos.multiDisplayStatusBarStarter by statusBarInitializerStore, privacyDotWindowControllerStore, lightBarControllerStore, - mockIWindowManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayStoreKosmos.kt new file mode 100644 index 000000000000..0db8ee8a9109 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayStoreKosmos.kt @@ -0,0 +1,55 @@ +/* + * 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.data.repository + +import com.android.systemui.display.data.repository.DisplayRepository +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import kotlinx.coroutines.CoroutineScope + +class FakeStatusBarPerDisplayStore( + backgroundApplicationScope: CoroutineScope, + displayRepository: DisplayRepository, +) : + StatusBarPerDisplayStoreImpl<TestPerDisplayInstance>( + backgroundApplicationScope, + displayRepository, + ) { + + val removalActions = mutableListOf<TestPerDisplayInstance>() + + override fun createInstanceForDisplay(displayId: Int): TestPerDisplayInstance { + return TestPerDisplayInstance(displayId) + } + + override val instanceClass = TestPerDisplayInstance::class.java + + override suspend fun onDisplayRemovalAction(instance: TestPerDisplayInstance) { + removalActions += instance + } +} + +data class TestPerDisplayInstance(val displayId: Int) + +val Kosmos.fakeStatusBarPerDisplayStore by + Kosmos.Fixture { + FakeStatusBarPerDisplayStore( + backgroundApplicationScope = applicationCoroutineScope, + displayRepository = displayRepository, + ) + } diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java index 621a128a736e..94e8ca5464b0 100644 --- a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java +++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java @@ -51,7 +51,9 @@ import java.util.NoSuchElementException; final class VendorVibrationSession extends IVibrationSession.Stub implements VibrationSession, CancellationSignal.OnCancelListener, IBinder.DeathRecipient { private static final String TAG = "VendorVibrationSession"; - private static final boolean DEBUG = false; + // To enable these logs, run: + // 'adb shell setprop persist.log.tag.VendorVibrationSession DEBUG && adb reboot' + private static final boolean DEBUG = VibratorDebugUtils.isDebuggable(TAG); /** Calls into VibratorManager functionality needed for playing an {@link ExternalVibration}. */ interface VibratorManagerHooks { diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java index cb9988fd698e..ab30cdc730eb 100644 --- a/services/core/java/com/android/server/vibrator/VibrationThread.java +++ b/services/core/java/com/android/server/vibrator/VibrationThread.java @@ -36,7 +36,9 @@ import java.util.Objects; /** Plays a {@link HalVibration} in dedicated thread. */ final class VibrationThread extends Thread { static final String TAG = "VibrationThread"; - static final boolean DEBUG = false; + // To enable these logs, run: + // 'adb shell setprop persist.log.tag.VibrationThread DEBUG && adb reboot' + static final boolean DEBUG = VibratorDebugUtils.isDebuggable(TAG); /** Calls into VibratorManager functionality needed for playing a {@link HalVibration}. */ interface VibratorManagerHooks { diff --git a/services/core/java/com/android/server/vibrator/VibratorDebugUtils.java b/services/core/java/com/android/server/vibrator/VibratorDebugUtils.java new file mode 100644 index 000000000000..9f37e76f0d6d --- /dev/null +++ b/services/core/java/com/android/server/vibrator/VibratorDebugUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vibrator; + +import android.util.Log; + +class VibratorDebugUtils { + + /** + * Checks if debugging is enabled for the specified tag or globally. + * + * <p>To enable debugging:<br> + * {@code adb shell setprop persist.log.tag.Vibrator_All DEBUG}<br> + * To disable debugging:<br> + * {@code adb shell setprop persist.log.tag.Vibrator_All \"\" } + * + * @param tag The tag to check for debugging. Use the tag name from the calling class. + * @return True if debugging is enabled for the tag or globally (Vibrator_All), false otherwise. + */ + public static boolean isDebuggable(String tag) { + return Log.isLoggable(tag, Log.DEBUG) || Log.isLoggable("Vibrator_All", Log.DEBUG); + } +} diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index ce91e63b4849..b9530978e850 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -108,7 +108,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private static final String EXTERNAL_VIBRATOR_SERVICE = "external_vibrator_service"; private static final String VIBRATOR_CONTROL_SERVICE = "android.frameworks.vibrator.IVibratorControlService/default"; - private static final boolean DEBUG = false; + // To enable these logs, run: + // 'adb shell setprop persist.log.tag.VibratorManagerService DEBUG && adb reboot' + private static final boolean DEBUG = VibratorDebugUtils.isDebuggable(TAG); private static final VibrationAttributes DEFAULT_ATTRIBUTES = new VibrationAttributes.Builder().build(); private static final int ATTRIBUTES_ALL_BYPASS_FLAGS = |