diff options
100 files changed, 2149 insertions, 1020 deletions
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 73de1b67dc66..c74bd1a092ee 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -6981,6 +6981,8 @@ public class DevicePolicyManager { * <p>The caller must hold the * {@link android.Manifest.permission#TRIGGER_LOST_MODE} permission. * + * <p>This API accesses the device's location and will only be used when a device is lost. + * * <p>Register a broadcast receiver to receive lost mode location updates. This receiver should * subscribe to the {@link #ACTION_LOST_MODE_LOCATION_UPDATE} action and receive the location * from an intent extra {@link #EXTRA_LOST_MODE_LOCATION}. diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 6f346bdae70a..394ac8f8c6e9 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -1986,7 +1986,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // report its requested visibility at the end of the animation, otherwise we would // lose the leash, and it would disappear during the animation // TODO(b/326377046) revisit this part and see if we can make it more general - typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime()); + if (Flags.reportAnimatingInsetsTypes()) { + typesToReport = mRequestedVisibleTypes; + } else { + typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime()); + } } else { typesToReport = mRequestedVisibleTypes; } diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index eca798d6eb4f..290885593ee6 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -379,7 +379,7 @@ final class IInputMethodManagerGlobalInvoker { @Nullable IRemoteInputConnection remoteInputConnection, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) { final IInputMethodManager service = getService(); if (service == null) { return InputBindResult.NULL; @@ -388,7 +388,7 @@ final class IInputMethodManagerGlobalInvoker { return service.startInputOrWindowGainedFocus(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection, remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId, - imeDispatcher); + imeDispatcher, imeRequestedVisible); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -408,7 +408,8 @@ final class IInputMethodManagerGlobalInvoker { @Nullable IRemoteInputConnection remoteInputConnection, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean useAsyncShowHideMethod) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + boolean useAsyncShowHideMethod) { final IInputMethodManager service = getService(); if (service == null) { return -1; @@ -417,7 +418,8 @@ final class IInputMethodManagerGlobalInvoker { service.startInputOrWindowGainedFocusAsync(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection, remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId, - imeDispatcher, advanceAngGetStartInputSequenceNumber(), useAsyncShowHideMethod); + imeDispatcher, imeRequestedVisible, advanceAngGetStartInputSequenceNumber(), + useAsyncShowHideMethod); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 0b34600f4104..a41ab368aed8 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -871,6 +871,19 @@ public final class InputMethodManager { IInputMethodManagerGlobalInvoker.reportPerceptibleAsync(windowToken, perceptible); } + private static boolean hasViewImeRequestedVisible(View view) { + // before the refactor, the requestedVisibleTypes for the IME were not in sync with + // the state that was actually requested. + if (Flags.refactorInsetsController() && view != null) { + final var controller = view.getWindowInsetsController(); + if (controller != null) { + return (view.getWindowInsetsController() + .getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0; + } + } + return false; + } + private final class DelegateImpl implements ImeFocusController.InputMethodManagerDelegate { @@ -941,6 +954,9 @@ public final class InputMethodManager { Log.v(TAG, "Reporting focus gain, without startInput"); } + final boolean imeRequestedVisible = hasViewImeRequestedVisible( + mCurRootView.getView()); + // ignore the result Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMM.startInputOrWindowGainedFocus"); IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus( @@ -950,7 +966,7 @@ public final class InputMethodManager { null, null, null, mCurRootView.mContext.getApplicationInfo().targetSdkVersion, - UserHandle.myUserId(), mImeDispatcher); + UserHandle.myUserId(), mImeDispatcher, imeRequestedVisible); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } } @@ -2441,9 +2457,8 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_NO_ONGOING_USER_ANIMATION); if (resultReceiver != null) { - final boolean imeReqVisible = - (viewRootImpl.getInsetsController().getRequestedVisibleTypes() - & WindowInsets.Type.ime()) != 0; + final boolean imeReqVisible = hasViewImeRequestedVisible( + viewRootImpl.getView()); resultReceiver.send( imeReqVisible ? InputMethodManager.RESULT_UNCHANGED_SHOWN : InputMethodManager.RESULT_SHOWN, null); @@ -2656,9 +2671,8 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); - final boolean imeReqVisible = - (viewRootImpl.getInsetsController().getRequestedVisibleTypes() - & WindowInsets.Type.ime()) != 0; + final boolean imeReqVisible = hasViewImeRequestedVisible( + viewRootImpl.getView()); if (resultReceiver != null) { resultReceiver.send( !imeReqVisible ? InputMethodManager.RESULT_UNCHANGED_HIDDEN @@ -3412,6 +3426,7 @@ public final class InputMethodManager { final Handler icHandler; InputBindResult res = null; final boolean hasServedView; + final boolean imeRequestedVisible; synchronized (mH) { // Now that we are locked again, validate that our state hasn't // changed. @@ -3479,10 +3494,13 @@ public final class InputMethodManager { } mServedInputConnection = servedInputConnection; + imeRequestedVisible = hasViewImeRequestedVisible(servedView); + if (DEBUG) { Log.v(TAG, "START INPUT: view=" + InputMethodDebug.dumpViewInfo(view) + " ic=" + ic + " editorInfo=" + editorInfo + " startInputFlags=" - + InputMethodDebug.startInputFlagsToString(startInputFlags)); + + InputMethodDebug.startInputFlagsToString(startInputFlags) + + " imeRequestedVisible=" + imeRequestedVisible); } // When we switch between non-editable views, do not call into the IMMS. @@ -3513,7 +3531,7 @@ public final class InputMethodManager { servedInputConnection == null ? null : servedInputConnection.asIRemoteAccessibilityInputConnection(), view.getContext().getApplicationInfo().targetSdkVersion, targetUserId, - mImeDispatcher, mAsyncShowHideMethodEnabled); + mImeDispatcher, imeRequestedVisible, mAsyncShowHideMethodEnabled); } else { res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus( startInputReason, mClient, windowGainingFocus, startInputFlags, @@ -3521,7 +3539,7 @@ public final class InputMethodManager { servedInputConnection == null ? null : servedInputConnection.asIRemoteAccessibilityInputConnection(), view.getContext().getApplicationInfo().targetSdkVersion, targetUserId, - mImeDispatcher); + mImeDispatcher, imeRequestedVisible); } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (Flags.useZeroJankProxy()) { diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 3f611c7efbdd..a328c78f3738 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -72,7 +72,7 @@ import java.util.function.Predicate; /** * <p>Displays a vertically-scrollable collection of views, where each view is positioned - * immediatelybelow the previous view in the list. For a more modern, flexible, and performant + * immediately below the previous view in the list. For a more modern, flexible, and performant * approach to displaying lists, use {@link androidx.recyclerview.widget.RecyclerView}.</p> * * <p>To display a list, you can include a list view in your layout XML file:</p> diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 17165cdcf7b1..aecf6eb261b1 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -113,7 +113,7 @@ public enum DesktopModeFlags { ENABLE_MINIMIZE_BUTTON(Flags::enableMinimizeButton, true), ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS(Flags::enableModalsFullscreenWithPermission, false), ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS( - Flags::enableOpaqueBackgroundForTransparentWindows, false), + Flags::enableOpaqueBackgroundForTransparentWindows, true), ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX(Flags::enableQuickswitchDesktopSplitBugfix, true), ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true), ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE( diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 9380d99b7de3..0791612fa0e8 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -105,7 +105,7 @@ interface IInputMethodManager { in @nullable EditorInfo editorInfo, in @nullable IRemoteInputConnection inputConnection, in @nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, int userId, - in ImeOnBackInvokedDispatcher imeDispatcher); + in ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible); // If windowToken is null, this just does startInput(). Otherwise this reports that a window // has gained focus, and if 'editorInfo' is non-null then also does startInput. @@ -120,8 +120,8 @@ interface IInputMethodManager { in @nullable EditorInfo editorInfo, in @nullable IRemoteInputConnection inputConnection, in @nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, int userId, - in ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq, - boolean useAsyncShowHideMethod); + in ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + int startInputSeq, boolean useAsyncShowHideMethod); void showInputMethodPickerFromClient(in IInputMethodClient client, int auxiliarySubtypeMode); @@ -224,7 +224,7 @@ interface IInputMethodManager { * async **/ oneway void acceptStylusHandwritingDelegationAsync(in IInputMethodClient client, in int userId, in String delegatePackageName, in String delegatorPackageName, int flags, - in IBooleanListener callback); + in IBooleanListener callback); /** Returns {@code true} if currently selected IME supports Stylus handwriting. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt index 2dbbeaebd3c0..651e776891db 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt @@ -73,7 +73,11 @@ class DropTargetManager( val newDragZone = state.getMatchingDragZone(x = x, y = y) state.currentDragZone = newDragZone if (oldDragZone != newDragZone) { - dragZoneChangedListener.onDragZoneChanged(from = oldDragZone, to = newDragZone) + dragZoneChangedListener.onDragZoneChanged( + draggedObject = state.draggedObject, + from = oldDragZone, + to = newDragZone + ) updateDropTarget() } } @@ -136,7 +140,7 @@ class DropTargetManager( /** Stores the current drag state. */ private inner class DragState( private val dragZones: List<DragZone>, - draggedObject: DraggedObject + val draggedObject: DraggedObject ) { val initialDragZone = if (draggedObject.initialLocation.isOnLeft(isLayoutRtl)) { @@ -157,7 +161,7 @@ class DropTargetManager( fun onInitialDragZoneSet(dragZone: DragZone) /** Called when the object was dragged to a different drag zone. */ - fun onDragZoneChanged(from: DragZone, to: DragZone) + fun onDragZoneChanged(draggedObject: DraggedObject, from: DragZone, to: DragZone) /** Called when the drag has ended with the zone it ended in. */ fun onDragEnded(zone: DragZone) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 3e95a0b1100f..efc952644f0b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -2644,12 +2644,7 @@ public class BubbleController implements ConfigurationChangeListener, } private void moveBubbleToFullscreen(String key) { - Bubble b = mBubbleData.getBubbleInStackWithKey(key); - if (b == null) { - Log.w(TAG, "can't find bubble with key " + key + " to move to fullscreen"); - return; - } - b.getTaskView().moveToFullscreen(); + // TODO b/388858013: convert the bubble to full screen } private boolean isDeviceLocked() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index bdb21f246359..3997412ab459 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -55,6 +55,7 @@ import com.android.wm.shell.shared.bubbles.DeviceConfig; import com.android.wm.shell.shared.bubbles.DismissView; import com.android.wm.shell.shared.bubbles.DragZone; import com.android.wm.shell.shared.bubbles.DragZoneFactory; +import com.android.wm.shell.shared.bubbles.DraggedObject; import com.android.wm.shell.shared.bubbles.DropTargetManager; import kotlin.Unit; @@ -168,8 +169,8 @@ public class BubbleBarLayerView extends FrameLayout } @Override - public void onDragZoneChanged(@NonNull DragZone from, - @NonNull DragZone to) { + public void onDragZoneChanged(@NonNull DraggedObject draggedObject, + @NonNull DragZone from, @NonNull DragZone to) { final boolean isBubbleLeft = to instanceof DragZone.Bubble.Left; final boolean isBubbleRight = to instanceof DragZone.Bubble.Right; if ((isBubbleLeft || isBubbleRight) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index dd2050a5fd5d..f73788486d04 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -440,11 +440,18 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged statsToken); } - // In case of a hide, the statsToken should not been send yet (as the animation - // is still ongoing). It will be sent at the end of the animation - boolean hideAnimOngoing = !mImeRequestedVisible && mAnimation != null; - setVisibleDirectly(mImeRequestedVisible || mAnimation != null, - hideAnimOngoing ? null : statsToken); + boolean hideAnimOngoing; + boolean reportVisible; + if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { + hideAnimOngoing = false; + reportVisible = mImeRequestedVisible; + } else { + // In case of a hide, the statsToken should not been send yet (as the animation + // is still ongoing). It will be sent at the end of the animation. + hideAnimOngoing = !mImeRequestedVisible && mAnimation != null; + reportVisible = mImeRequestedVisible || mAnimation != null; + } + setVisibleDirectly(reportVisible, hideAnimOngoing ? null : statsToken); } } @@ -628,7 +635,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged + " showing:" + (mAnimationDirection == DIRECTION_SHOW)); } if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { - setAnimating(true); + setAnimating(true /* imeAnimationOngoing */); } int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY), imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW, @@ -678,7 +685,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged if (!android.view.inputmethod.Flags.refactorInsetsController()) { dispatchEndPositioning(mDisplayId, mCancelled, t); } else if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { - setAnimating(false); + setAnimating(false /* imeAnimationOngoing */); } if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { ImeTracker.forLogging().onProgress(mStatsToken, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt index c3d15df6eae5..9c55f0ecda93 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor.tiling import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.content.Context import android.content.res.Configuration import android.content.res.Resources @@ -28,9 +29,11 @@ import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo +import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import com.android.internal.annotations.VisibleForTesting @@ -422,6 +425,8 @@ class DesktopTilingWindowDecoration( change.taskInfo?.let { if (it.isFullscreen || isMinimized(change.mode, info.type)) { removeTaskIfTiled(it.taskId, /* taskVanished= */ false, it.isFullscreen) + } else if (isEnteringPip(change, info.type)) { + removeTaskIfTiled(it.taskId, /* taskVanished= */ true, it.isFullscreen) } } } @@ -434,6 +439,27 @@ class DesktopTilingWindowDecoration( infoType == TRANSIT_OPEN)) } + private fun isEnteringPip(change: Change, transitType: Int): Boolean { + if (change.taskInfo != null && change.taskInfo?.windowingMode == WINDOWING_MODE_PINNED) { + // - TRANSIT_PIP: type (from RootWindowContainer) + // - TRANSIT_OPEN (from apps that enter PiP instantly on opening, mostly from + // CTS/Flicker tests). + // - TRANSIT_TO_FRONT, though uncommon with triggering PiP, should semantically also + // be allowed to animate if the task in question is pinned already - see b/308054074. + // - TRANSIT_CHANGE: This can happen if the request to enter PIP happens when we are + // collecting for another transition, such as TRANSIT_CHANGE (display rotation). + if ( + transitType == TRANSIT_PIP || + transitType == TRANSIT_OPEN || + transitType == TRANSIT_TO_FRONT || + transitType == TRANSIT_CHANGE + ) { + return true + } + } + return false + } + class AppResizingHelper( val taskInfo: RunningTaskInfo, val desktopModeWindowDecoration: DesktopModeWindowDecoration, @@ -550,12 +576,16 @@ class DesktopTilingWindowDecoration( taskVanished: Boolean = false, shouldDelayUpdate: Boolean = false, ) { + val taskRepository = desktopUserRepositories.current if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) { removeTask(leftTaskResizingHelper, taskVanished, shouldDelayUpdate) leftTaskResizingHelper = null - rightTaskResizingHelper - ?.desktopModeWindowDecoration - ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) + val taskId = rightTaskResizingHelper?.taskInfo?.taskId + if (taskId != null && taskRepository.isVisibleTask(taskId)) { + rightTaskResizingHelper + ?.desktopModeWindowDecoration + ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) + } tearDownTiling() return } @@ -563,9 +593,12 @@ class DesktopTilingWindowDecoration( if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) { removeTask(rightTaskResizingHelper, taskVanished, shouldDelayUpdate) rightTaskResizingHelper = null - leftTaskResizingHelper - ?.desktopModeWindowDecoration - ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) + val taskId = leftTaskResizingHelper?.taskInfo?.taskId + if (taskId != null && taskRepository.isVisibleTask(taskId)) { + leftTaskResizingHelper + ?.desktopModeWindowDecoration + ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) + } tearDownTiling() } } @@ -600,7 +633,6 @@ class DesktopTilingWindowDecoration( fun onOverviewAnimationStateChange(isRunning: Boolean) { if (!isTilingManagerInitialised) return - if (isRunning) { desktopTilingDividerWindowManager?.hideDividerBar() } else if (allTiledTasksVisible()) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt index c40a04c47b9b..b511fc34fa89 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.content.ComponentName import android.graphics.Rect import android.view.Display.DEFAULT_DISPLAY @@ -45,6 +46,17 @@ object DesktopTestHelpers { .apply { bounds?.let { setBounds(it) } } .build() + fun createPinnedTask(displayId: Int = DEFAULT_DISPLAY, bounds: Rect? = null): RunningTaskInfo = + TestRunningTaskInfoBuilder() + .setDisplayId(displayId) + .setParentTaskId(displayId) + .setToken(MockToken().token()) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_PINNED) + .setLastActiveTime(100) + .apply { bounds?.let { setBounds(it) } } + .build() + fun createFullscreenTaskBuilder(displayId: Int = DEFAULT_DISPLAY): TestRunningTaskInfoBuilder = TestRunningTaskInfoBuilder() .setDisplayId(displayId) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt index 95498cbbe53c..3b21e365e911 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt @@ -352,7 +352,7 @@ class DropTargetManagerTest { initialDragZone = dragZone } - override fun onDragZoneChanged(from: DragZone, to: DragZone) { + override fun onDragZoneChanged(draggedObject: DraggedObject, from: DragZone, to: DragZone) { fromDragZone = from toDragZone = to } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt new file mode 100644 index 000000000000..048981d634ef --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/RemoteTransitionHandlerTest.kt @@ -0,0 +1,115 @@ +/* + * 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.transition + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.WindowManager +import android.window.RemoteTransition +import android.window.TransitionFilter +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestSyncExecutor +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** + * Test class for [RemoteTransitionHandler]. + * + * atest WMShellUnitTests:RemoteTransitionHandlerTest + */ +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +class RemoteTransitionHandlerTest : ShellTestCase() { + + private val testExecutor: TestSyncExecutor = TestSyncExecutor() + + private val testRemoteTransition = RemoteTransition(TestRemoteTransition()) + private lateinit var handler: RemoteTransitionHandler + + @Before + fun setUp() { + handler = RemoteTransitionHandler(testExecutor) + } + + @Test + fun handleRequest_noRemoteTransition_returnsNull() { + val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, null) + + assertNull(handler.handleRequest(mock(), request)) + } + + @Test + fun handleRequest_testRemoteTransition_returnsWindowContainerTransaction() { + val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, testRemoteTransition) + + assertTrue(handler.handleRequest(mock(), request) is WindowContainerTransaction) + } + + @Test + fun startAnimation_noRemoteTransition_returnsFalse() { + val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, null) + handler.handleRequest(mock(), request) + + val isHandled = handler.startAnimation( + /* transition= */ mock(), + /* info= */ createTransitionInfo(), + /* startTransaction= */ mock(), + /* finishTransaction= */ mock(), + /* finishCallback= */ {}, + ) + + assertFalse(isHandled) + } + + @Test + fun startAnimation_remoteTransition_returnsTrue() { + val request = TransitionRequestInfo(WindowManager.TRANSIT_OPEN, null, testRemoteTransition) + handler.addFiltered(TransitionFilter(), testRemoteTransition) + handler.handleRequest(mock(), request) + + val isHandled = handler.startAnimation( + /* transition= */ testRemoteTransition.remoteTransition.asBinder(), + /* info= */ createTransitionInfo(), + /* startTransaction= */ mock(), + /* finishTransaction= */ mock(), + /* finishCallback= */ {}, + ) + + assertTrue(isHandled) + } + + private fun createTransitionInfo( + type: Int = WindowManager.TRANSIT_OPEN, + changeMode: Int = WindowManager.TRANSIT_CLOSE, + ): TransitionInfo = + TransitionInfo(type, /* flags= */ 0).apply { + addChange( + TransitionInfo.Change(mock(), mock()).apply { + mode = changeMode + parent = null + } + ) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt index bc8faedd77a9..e4424f3c57f2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.windowdecor.tiling import android.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.content.res.Resources import android.graphics.Rect @@ -24,8 +25,10 @@ import android.testing.AndroidTestingRunner import android.view.MotionEvent import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo +import android.window.TransitionInfo.Change import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest import com.android.wm.shell.RootTaskDisplayAreaOrganizer @@ -40,6 +43,7 @@ import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeT import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.desktopmode.DesktopTestHelpers.createPinnedTask import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler @@ -552,6 +556,37 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { } @Test + fun taskTiled_shouldBeRemoved_whenEnteringPip() { + val task1 = createPipTask() + val stableBounds = STABLE_BOUNDS_MOCK + whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout) + whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(stableBounds) + } + whenever(context.resources).thenReturn(resources) + whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width) + whenever(tiledTaskHelper.taskInfo).thenReturn(task1) + whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration) + tilingDecoration.onAppTiled( + task1, + desktopWindowDecoration, + DesktopTasksController.SnapPosition.LEFT, + BOUNDS, + ) + tilingDecoration.leftTaskResizingHelper = tiledTaskHelper + val changeInfo = createPipChangeTransition(task1) + tilingDecoration.onTransitionReady( + transition = mock(), + info = changeInfo, + startTransaction = mock(), + finishTransaction = mock(), + ) + + assertThat(tilingDecoration.leftTaskResizingHelper).isNull() + verify(tiledTaskHelper, times(1)).dispose() + } + + @Test fun taskNotTiled_shouldNotBeRemoved_whenNotTiled() { val task1 = createVisibleTask() val task2 = createVisibleTask() @@ -652,6 +687,23 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { whenever(userRepositories.current.isVisibleTask(eq(it.taskId))).thenReturn(true) } + private fun createPipTask() = + createPinnedTask().also { + whenever(userRepositories.current.isVisibleTask(eq(it.taskId))).thenReturn(true) + } + + private fun createPipChangeTransition(task: RunningTaskInfo?, type: Int = TRANSIT_PIP) = + TransitionInfo(type, /* flags= */ 0).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_PIP + parent = null + taskInfo = task + flags = flags + } + ) + } + companion object { private val NON_STABLE_BOUNDS_MOCK = Rect(50, 55, 100, 100) private val STABLE_BOUNDS_MOCK = Rect(0, 0, 100, 100) diff --git a/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionLog.kt b/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionLog.kt new file mode 100644 index 000000000000..92455c05e3d7 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionLog.kt @@ -0,0 +1,22 @@ +/* + * 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.settingslib.supervision + +/** Constants used in supervision logs. */ +object SupervisionLog { + const val TAG = "SupervisionSettings" +} diff --git a/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionRestrictionsHelper.kt b/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionRestrictionsHelper.kt new file mode 100644 index 000000000000..1be8a17915a1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/supervision/SupervisionRestrictionsHelper.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.supervision + +import android.app.admin.DeviceAdminReceiver +import android.app.supervision.SupervisionManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.UserHandle +import android.util.Log +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin + +/** Helper class for supervision-enforced restrictions. */ +object SupervisionRestrictionsHelper { + + /** + * Creates an instance of [EnforcedAdmin] that uses the correct supervision component or returns + * null if supervision is not enabled. + */ + @JvmStatic + fun createEnforcedAdmin( + context: Context, + restriction: String, + user: UserHandle, + ): EnforcedAdmin? { + val supervisionManager = context.getSystemService(SupervisionManager::class.java) + val supervisionAppPackage = supervisionManager?.activeSupervisionAppPackage ?: return null + var supervisionComponent: ComponentName? = null + + // Try to find the service whose package matches the active supervision app. + val resolveSupervisionApps = + context.packageManager.queryIntentServicesAsUser( + Intent("android.app.action.BIND_SUPERVISION_APP_SERVICE"), + PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS, + user.identifier, + ) + resolveSupervisionApps + .mapNotNull { it.serviceInfo?.componentName } + .find { it.packageName == supervisionAppPackage } + ?.let { supervisionComponent = it } + + if (supervisionComponent == null) { + // Try to find the PO receiver whose package matches the active supervision app, for + // backwards compatibility. + val resolveDeviceAdmins = + context.packageManager.queryBroadcastReceiversAsUser( + Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED), + PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS, + user.identifier, + ) + resolveDeviceAdmins + .mapNotNull { it.activityInfo?.componentName } + .find { it.packageName == supervisionAppPackage } + ?.let { supervisionComponent = it } + } + + if (supervisionComponent == null) { + Log.d(SupervisionLog.TAG, "Could not find the supervision component.") + } + return EnforcedAdmin(supervisionComponent, restriction, user) + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java index c5e6f60e3fa6..2b95fd1cdd9d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java @@ -93,18 +93,10 @@ public class CreateUserActivity extends Activity { @Override public boolean onTouchEvent(@Nullable MotionEvent event) { - onBackInvoked(); + cancel(); return super.onTouchEvent(event); } - private void onBackInvoked() { - if (mSetupUserDialog != null) { - mSetupUserDialog.dismiss(); - } - setResult(RESULT_CANCELED); - finish(); - } - @VisibleForTesting void setSuccessResult(String userName, Drawable userIcon, String path, Boolean isAdmin) { Intent intent = new Intent(this, CreateUserActivity.class); @@ -112,14 +104,12 @@ public class CreateUserActivity extends Activity { intent.putExtra(EXTRA_IS_ADMIN, isAdmin); intent.putExtra(EXTRA_USER_ICON_PATH, path); - mSetupUserDialog.dismiss(); setResult(RESULT_OK, intent); finish(); } @VisibleForTesting void cancel() { - mSetupUserDialog.dismiss(); setResult(RESULT_CANCELED); finish(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java index d9f1b632323c..bce229f5a13d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java @@ -282,7 +282,7 @@ public class CreateUserDialogController { mCustomDialogHelper.getDialog().dismiss(); break; case EXIT_DIALOG: - finish(); + mUserCreationDialog.dismiss(); break; default: if (mCurrentState < EXIT_DIALOG) { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/OWNERS b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/OWNERS new file mode 100644 index 000000000000..04e7058b4384 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/OWNERS @@ -0,0 +1 @@ +file:platform/frameworks/base:/core/java/android/app/supervision/OWNERS diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionIntentProviderTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionIntentProviderTest.kt index 2ceed2875cb4..83ffa9399dc0 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionIntentProviderTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionIntentProviderTest.kt @@ -19,6 +19,7 @@ package com.android.settingslib.supervision import android.app.supervision.SupervisionManager import android.content.Context import android.content.ContextWrapper +import android.content.Intent import android.content.pm.PackageManager import android.content.pm.ResolveInfo import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -77,8 +78,8 @@ class SupervisionIntentProviderTest { fun getSettingsIntent_unresolvedIntent() { `when`(mockSupervisionManager.activeSupervisionAppPackage) .thenReturn(SUPERVISION_APP_PACKAGE) - `when`(mockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt())) - .thenReturn(emptyList()) + `when`(mockPackageManager.queryIntentActivitiesAsUser(any<Intent>(), anyInt(), anyInt())) + .thenReturn(emptyList<ResolveInfo>()) val intent = SupervisionIntentProvider.getSettingsIntent(context) @@ -89,7 +90,7 @@ class SupervisionIntentProviderTest { fun getSettingsIntent_resolvedIntent() { `when`(mockSupervisionManager.activeSupervisionAppPackage) .thenReturn(SUPERVISION_APP_PACKAGE) - `when`(mockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt())) + `when`(mockPackageManager.queryIntentActivitiesAsUser(any<Intent>(), anyInt(), anyInt())) .thenReturn(listOf(ResolveInfo())) val intent = SupervisionIntentProvider.getSettingsIntent(context) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionRestrictionsHelperTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionRestrictionsHelperTest.kt new file mode 100644 index 000000000000..872fc2a44b3d --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/supervision/SupervisionRestrictionsHelperTest.kt @@ -0,0 +1,178 @@ +/* + * 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.settingslib.supervision + +import android.app.admin.DeviceAdminReceiver +import android.app.supervision.SupervisionManager +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatcher +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.argThat +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +/** + * Unit tests for [SupervisionRestrictionsHelper]. + * + * Run with `atest SupervisionRestrictionsHelperTest`. + */ +@RunWith(AndroidJUnit4::class) +class SupervisionRestrictionsHelperTest { + @get:Rule val mocks: MockitoRule = MockitoJUnit.rule() + + @Mock private lateinit var mockPackageManager: PackageManager + + @Mock private lateinit var mockSupervisionManager: SupervisionManager + + private lateinit var context: Context + + @Before + fun setUp() { + context = + object : ContextWrapper(InstrumentationRegistry.getInstrumentation().context) { + override fun getPackageManager() = mockPackageManager + + override fun getSystemService(name: String) = + when (name) { + Context.SUPERVISION_SERVICE -> mockSupervisionManager + else -> super.getSystemService(name) + } + } + } + + @Test + fun createEnforcedAdmin_nullSupervisionPackage() { + `when`(mockSupervisionManager.activeSupervisionAppPackage).thenReturn(null) + + val enforcedAdmin = + SupervisionRestrictionsHelper.createEnforcedAdmin(context, RESTRICTION, USER_HANDLE) + + assertThat(enforcedAdmin).isNull() + } + + @Test + fun createEnforcedAdmin_supervisionAppService() { + val resolveInfo = + ResolveInfo().apply { + serviceInfo = + ServiceInfo().apply { + packageName = SUPERVISION_APP_PACKAGE + name = "service.class" + } + } + + `when`(mockSupervisionManager.activeSupervisionAppPackage) + .thenReturn(SUPERVISION_APP_PACKAGE) + `when`( + mockPackageManager.queryIntentServicesAsUser( + argThat(hasAction("android.app.action.BIND_SUPERVISION_APP_SERVICE")), + anyInt(), + eq(USER_ID), + ) + ) + .thenReturn(listOf(resolveInfo)) + + val enforcedAdmin = + SupervisionRestrictionsHelper.createEnforcedAdmin(context, RESTRICTION, USER_HANDLE) + + assertThat(enforcedAdmin).isNotNull() + assertThat(enforcedAdmin!!.component).isEqualTo(resolveInfo.serviceInfo.componentName) + assertThat(enforcedAdmin.enforcedRestriction).isEqualTo(RESTRICTION) + assertThat(enforcedAdmin.user).isEqualTo(USER_HANDLE) + } + + @Test + fun createEnforcedAdmin_profileOwnerReceiver() { + val resolveInfo = + ResolveInfo().apply { + activityInfo = + ActivityInfo().apply { + packageName = SUPERVISION_APP_PACKAGE + name = "service.class" + } + } + + `when`(mockSupervisionManager.activeSupervisionAppPackage) + .thenReturn(SUPERVISION_APP_PACKAGE) + `when`(mockPackageManager.queryIntentServicesAsUser(any<Intent>(), anyInt(), eq(USER_ID))) + .thenReturn(emptyList<ResolveInfo>()) + `when`( + mockPackageManager.queryBroadcastReceiversAsUser( + argThat(hasAction(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED)), + anyInt(), + eq(USER_ID), + ) + ) + .thenReturn(listOf(resolveInfo)) + + val enforcedAdmin = + SupervisionRestrictionsHelper.createEnforcedAdmin(context, RESTRICTION, USER_HANDLE) + + assertThat(enforcedAdmin).isNotNull() + assertThat(enforcedAdmin!!.component).isEqualTo(resolveInfo.activityInfo.componentName) + assertThat(enforcedAdmin.enforcedRestriction).isEqualTo(RESTRICTION) + assertThat(enforcedAdmin.user).isEqualTo(USER_HANDLE) + } + + @Test + fun createEnforcedAdmin_noSupervisionComponent() { + `when`(mockSupervisionManager.activeSupervisionAppPackage) + .thenReturn(SUPERVISION_APP_PACKAGE) + `when`(mockPackageManager.queryIntentServicesAsUser(any<Intent>(), anyInt(), anyInt())) + .thenReturn(emptyList<ResolveInfo>()) + `when`(mockPackageManager.queryBroadcastReceiversAsUser(any<Intent>(), anyInt(), anyInt())) + .thenReturn(emptyList<ResolveInfo>()) + + val enforcedAdmin = + SupervisionRestrictionsHelper.createEnforcedAdmin(context, RESTRICTION, USER_HANDLE) + + assertThat(enforcedAdmin).isNotNull() + assertThat(enforcedAdmin!!.component).isNull() + assertThat(enforcedAdmin.enforcedRestriction).isEqualTo(RESTRICTION) + assertThat(enforcedAdmin.user).isEqualTo(USER_HANDLE) + } + + private fun hasAction(action: String) = + object : ArgumentMatcher<Intent> { + override fun matches(intent: Intent?) = intent?.action == action + } + + private companion object { + const val SUPERVISION_APP_PACKAGE = "app.supervision" + const val RESTRICTION = "restriction" + val USER_HANDLE = UserHandle.CURRENT + val USER_ID = USER_HANDLE.identifier + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java index f58eb7cc2e31..220c03e53be2 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java @@ -65,23 +65,23 @@ public class CreateUserActivityTest { } @Test - public void onTouchEvent_dismissesDialogAndCancelsResult() { + public void onTouchEvent_finishesActivityAndCancelsResult() { mCreateUserActivity.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0)); - assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse(); + assertThat(mCreateUserActivity.isFinishing()).isTrue(); assertThat(shadowOf(mCreateUserActivity).getResultCode()) .isEqualTo(Activity.RESULT_CANCELED); } @Test - public void setSuccessResult_dismissesDialogAndSetsSuccessResult() { + public void setSuccessResult_finishesActivityAndSetsSuccessResult() { Drawable mockDrawable = mock(Drawable.class); mCreateUserActivity.setSuccessResult(TEST_USER_NAME, mockDrawable, TEST_USER_ICON_PATH, TEST_IS_ADMIN); - assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse(); + assertThat(mCreateUserActivity.isFinishing()).isTrue(); assertThat(shadowOf(mCreateUserActivity).getResultCode()).isEqualTo(Activity.RESULT_OK); Intent resultIntent = shadowOf(mCreateUserActivity).getResultIntent(); @@ -92,10 +92,10 @@ public class CreateUserActivityTest { } @Test - public void cancel_dismissesDialogAndSetsCancelResult() { + public void cancel_finishesActivityAndSetsCancelResult() { mCreateUserActivity.cancel(); - assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse(); + assertThat(mCreateUserActivity.isFinishing()).isTrue(); assertThat(shadowOf(mCreateUserActivity).getResultCode()) .isEqualTo(Activity.RESULT_CANCELED); } diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 72ae76a45cac..f628a420d8fa 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -312,6 +312,9 @@ <!-- Permission necessary to change car audio volume through CarAudioManager --> <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME" /> + <!-- To detect when projecting to Android Auto --> + <uses-permission android:name="android.permission.READ_PROJECTION_STATE" /> + <!-- Permission to control Android Debug Bridge (ADB) --> <uses-permission android:name="android.permission.MANAGE_DEBUGGING" /> diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt index e43b8a0b9297..7ee6a6e5ebf4 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -107,6 +107,16 @@ constructor( */ // TODO(b/301385865): Remove this flag. private val disableWmTimeout: Boolean = false, + + /** + * Whether we should disable the reparent transaction that puts the opening/closing window above + * the view's window. This should be set to true in tests only, where we can't currently use a + * valid leash. + * + * TODO(b/397180418): Remove this flag when we don't have the RemoteAnimation wrapper anymore + * and we can just inject a fake transaction. + */ + private val skipReparentTransaction: Boolean = false, ) { @JvmOverloads constructor( @@ -1140,6 +1150,7 @@ constructor( DelegatingAnimationCompletionListener(listener, this::dispose), transitionAnimator, disableWmTimeout, + skipReparentTransaction, ) } @@ -1173,6 +1184,16 @@ constructor( */ // TODO(b/301385865): Remove this flag. disableWmTimeout: Boolean = false, + + /** + * Whether we should disable the reparent transaction that puts the opening/closing window + * above the view's window. This should be set to true in tests only, where we can't + * currently use a valid leash. + * + * TODO(b/397180418): Remove this flag when we don't have the RemoteAnimation wrapper + * anymore and we can just inject a fake transaction. + */ + private val skipReparentTransaction: Boolean = false, ) : RemoteAnimationDelegate<IRemoteAnimationFinishedCallback> { private val transitionContainer = controller.transitionContainer private val context = transitionContainer.context @@ -1515,7 +1536,7 @@ constructor( ) } - if (moveTransitionAnimationLayer()) { + if (moveTransitionAnimationLayer() && !skipReparentTransaction) { // Ensure that the launching window is rendered above the view's window, // so it is not obstructed. // TODO(b/397180418): re-use the start transaction once the diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt index 5d9c441db003..a4a96d19e8bb 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt @@ -1006,13 +1006,32 @@ class TransitionAnimator( Log.d(TAG, "Animation ended") } - // TODO(b/330672236): Post this to the main thread instead so that it does not - // flicker with Flexiglass enabled. - controller.onTransitionAnimationEnd(isExpandingFullyAbove) - transitionContainerOverlay.remove(windowBackgroundLayer) + val onEnd = { + controller.onTransitionAnimationEnd(isExpandingFullyAbove) + transitionContainerOverlay.remove(windowBackgroundLayer) - if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) { - openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) + if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) { + openingWindowSyncViewOverlay?.remove(windowBackgroundLayer) + } + } + // TODO(b/330672236): Post this to the main thread for launches as well, so that they do not + // flicker with Flexiglass enabled. + if (controller.isLaunching) { + onEnd() + } else { + // onAnimationEnd is called at the end of the animation, on a Choreographer animation + // tick. During dialog launches, the following calls will move the animated content from + // the dialog overlay back to its original position, and this change must be reflected + // in the next frame given that we then sync the next frame of both the content and + // dialog ViewRoots. During SysUI activity launches, we will instantly collapse the + // shade at the end of the transition. However, if those are rendered by Compose, whose + // compositions are also scheduled on a Choreographer frame, any state change made + // *right now* won't be reflected in the next frame given that a Choreographer frame + // can't schedule another and have it happen in the same frame. So we post the forwarded + // calls to [Controller.onLaunchAnimationEnd] in the main executor, leaving this + // Choreographer frame, ensuring that any state change applied by + // onTransitionAnimationEnd() will be reflected in the same frame. + mainExecutor.execute { onEnd() } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt index ed73d89db2c7..6a25069a4e5e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt @@ -73,12 +73,12 @@ class CommunalOngoingContentStartableTest : SysuiTestCase() { assertThat(fakeCommunalMediaRepository.isListening()).isFalse() assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() - kosmos.setCommunalEnabled(true) + setCommunalEnabled(true) assertThat(fakeCommunalMediaRepository.isListening()).isTrue() assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue() - kosmos.setCommunalEnabled(false) + setCommunalEnabled(false) assertThat(fakeCommunalMediaRepository.isListening()).isFalse() assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() @@ -93,13 +93,13 @@ class CommunalOngoingContentStartableTest : SysuiTestCase() { assertThat(fakeCommunalMediaRepository.isListening()).isFalse() assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() - kosmos.setCommunalEnabled(true) + setCommunalEnabled(true) // Media listening does not start when UMO is disabled. assertThat(fakeCommunalMediaRepository.isListening()).isFalse() assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue() - kosmos.setCommunalEnabled(false) + setCommunalEnabled(false) assertThat(fakeCommunalMediaRepository.isListening()).isFalse() assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt new file mode 100644 index 000000000000..f9b29e9bc5b5 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryImplTest.kt @@ -0,0 +1,120 @@ +/* + * 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.communal.data.repository + +import android.app.UiModeManager +import android.app.UiModeManager.OnProjectionStateChangedListener +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CarProjectionRepositoryImplTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val capturedListeners = mutableListOf<OnProjectionStateChangedListener>() + + private val Kosmos.uiModeManager by + Kosmos.Fixture<UiModeManager> { + mock { + on { + addOnProjectionStateChangedListener( + eq(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE), + any(), + any(), + ) + } doAnswer + { + val listener = it.getArgument<OnProjectionStateChangedListener>(2) + capturedListeners.add(listener) + Unit + } + + on { removeOnProjectionStateChangedListener(any()) } doAnswer + { + val listener = it.getArgument<OnProjectionStateChangedListener>(0) + capturedListeners.remove(listener) + Unit + } + + on { activeProjectionTypes } doReturn UiModeManager.PROJECTION_TYPE_NONE + } + } + + private val Kosmos.underTest by + Kosmos.Fixture { + CarProjectionRepositoryImpl( + uiModeManager = uiModeManager, + bgDispatcher = testDispatcher, + ) + } + + @Test + fun testProjectionActiveUpdatesAfterCallback() = + kosmos.runTest { + val projectionActive by collectLastValue(underTest.projectionActive) + assertThat(projectionActive).isFalse() + + setActiveProjectionType(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) + assertThat(projectionActive).isTrue() + + setActiveProjectionType(UiModeManager.PROJECTION_TYPE_NONE) + assertThat(projectionActive).isFalse() + } + + @Test + fun testProjectionInitialValueTrue() = + kosmos.runTest { + setActiveProjectionType(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) + + val projectionActive by collectLastValue(underTest.projectionActive) + assertThat(projectionActive).isTrue() + } + + @Test + fun testUnsubscribeWhenCancelled() = + kosmos.runTest { + val job = underTest.projectionActive.launchIn(backgroundScope) + assertThat(capturedListeners).hasSize(1) + + job.cancel() + assertThat(capturedListeners).isEmpty() + } + + private fun Kosmos.setActiveProjectionType(@UiModeManager.ProjectionType projectionType: Int) { + uiModeManager.stub { on { activeProjectionTypes } doReturn projectionType } + capturedListeners.forEach { it.onProjectionStateChanged(projectionType, emptySet()) } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt index 5c983656225e..09d44a5e18d9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt @@ -34,9 +34,7 @@ import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher -import com.android.systemui.communal.data.model.DisabledReason import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_BACKGROUND_SETTING -import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED @@ -202,63 +200,6 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT @EnableFlags(FLAG_COMMUNAL_HUB) @Test - fun secondaryUserIsInvalid() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getEnabledState(SECONDARY_USER)) - - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_INVALID_USER) - } - - @EnableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) - @Test - fun classicFlagIsDisabled() = - kosmos.runTest { - setCommunalV2Enabled(false) - val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG) - } - - @DisableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) - @Test - fun communalHubFlagIsDisabled() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG) - } - - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test - fun hubIsDisabledByUser() = - kosmos.runTest { - fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id) - val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_USER_SETTING) - - fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, SECONDARY_USER.id) - assertThat(enabledState?.enabled).isFalse() - - fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 1, PRIMARY_USER.id) - assertThat(enabledState?.enabled).isTrue() - } - - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test - fun hubIsDisabledByDevicePolicy() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledState?.enabled).isTrue() - - setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL) - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_DEVICE_POLICY) - } - - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test fun widgetsAllowedForWorkProfile_isFalse_whenDisallowedByDevicePolicy() = kosmos.runTest { val widgetsAllowedForWorkProfile by @@ -269,36 +210,6 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT assertThat(widgetsAllowedForWorkProfile).isFalse() } - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test - fun hubIsEnabled_whenDisallowedByDevicePolicyForWorkProfile() = - kosmos.runTest { - val enabledStateForPrimaryUser by - collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledStateForPrimaryUser?.enabled).isTrue() - - setKeyguardFeaturesDisabled(WORK_PROFILE, KEYGUARD_DISABLE_WIDGETS_ALL) - assertThat(enabledStateForPrimaryUser?.enabled).isTrue() - } - - @EnableFlags(FLAG_COMMUNAL_HUB) - @Test - fun hubIsDisabledByUserAndDevicePolicy() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) - assertThat(enabledState?.enabled).isTrue() - - fakeSettings.putIntForUser(Settings.Secure.GLANCEABLE_HUB_ENABLED, 0, PRIMARY_USER.id) - setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_WIDGETS_ALL) - - assertThat(enabledState?.enabled).isFalse() - assertThat(enabledState) - .containsExactly( - DisabledReason.DISABLED_REASON_DEVICE_POLICY, - DisabledReason.DISABLED_REASON_USER_SETTING, - ) - } - @Test @DisableFlags(FLAG_GLANCEABLE_HUB_BLURRED_BACKGROUND) fun backgroundType_defaultValue() = @@ -327,26 +238,6 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT } @Test - fun screensaverDisabledByUser() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER)) - - fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, 0, PRIMARY_USER.id) - - assertThat(enabledState).isFalse() - } - - @Test - fun screensaverEnabledByUser() = - kosmos.runTest { - val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER)) - - fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ENABLED, 1, PRIMARY_USER.id) - - assertThat(enabledState).isTrue() - } - - @Test fun whenToDream_charging() = kosmos.runTest { val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt new file mode 100644 index 000000000000..fc4cd43577b1 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorTest.kt @@ -0,0 +1,49 @@ +/* + * 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.communal.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.carProjectionRepository +import com.android.systemui.communal.data.repository.fake +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CarProjectionInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Kosmos.Fixture { carProjectionInteractor } + + @Test + fun testProjectionActive() = + kosmos.runTest { + val projectionActive by collectLastValue(underTest.projectionActive) + assertThat(projectionActive).isFalse() + + carProjectionRepository.fake.setProjectionActive(true) + assertThat(projectionActive).isTrue() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt new file mode 100644 index 000000000000..f4a1c90a5471 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt @@ -0,0 +1,173 @@ +/* + * 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.communal.domain.interactor + +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake +import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN +import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN +import com.android.systemui.communal.data.model.SuppressionReason +import com.android.systemui.communal.posturing.data.repository.fake +import com.android.systemui.communal.posturing.data.repository.posturingRepository +import com.android.systemui.communal.posturing.shared.model.PosturedState +import com.android.systemui.dock.DockManager +import com.android.systemui.dock.fakeDockManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalAutoOpenInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Kosmos.Fixture { communalAutoOpenInteractor } + + @Before + fun setUp() { + runBlocking { kosmos.fakeUserRepository.asMainUser() } + with(kosmos.fakeSettings) { + putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, false, MAIN_USER_ID) + putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, false, MAIN_USER_ID) + putBoolForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, false, MAIN_USER_ID) + } + } + + @Test + fun testStartWhileCharging() = + kosmos.runTest { + val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen) + val suppressionReason by collectLastValue(underTest.suppressionReason) + + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + true, + MAIN_USER_ID, + ) + + batteryRepository.fake.setDevicePluggedIn(false) + assertThat(shouldAutoOpen).isFalse() + assertThat(suppressionReason) + .isEqualTo( + SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) + ) + + batteryRepository.fake.setDevicePluggedIn(true) + assertThat(shouldAutoOpen).isTrue() + assertThat(suppressionReason).isNull() + } + + @Test + fun testStartWhileDocked() = + kosmos.runTest { + val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen) + val suppressionReason by collectLastValue(underTest.suppressionReason) + + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + true, + MAIN_USER_ID, + ) + + batteryRepository.fake.setDevicePluggedIn(true) + fakeDockManager.setIsDocked(false) + + assertThat(shouldAutoOpen).isFalse() + assertThat(suppressionReason) + .isEqualTo( + SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) + ) + + fakeDockManager.setIsDocked(true) + fakeDockManager.setDockEvent(DockManager.STATE_DOCKED) + assertThat(shouldAutoOpen).isTrue() + assertThat(suppressionReason).isNull() + } + + @Test + fun testStartWhilePostured() = + kosmos.runTest { + val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen) + val suppressionReason by collectLastValue(underTest.suppressionReason) + + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + true, + MAIN_USER_ID, + ) + + batteryRepository.fake.setDevicePluggedIn(true) + posturingRepository.fake.setPosturedState(PosturedState.NotPostured) + + assertThat(shouldAutoOpen).isFalse() + assertThat(suppressionReason) + .isEqualTo( + SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) + ) + + posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) + assertThat(shouldAutoOpen).isTrue() + assertThat(suppressionReason).isNull() + } + + @Test + fun testStartNever() = + kosmos.runTest { + val shouldAutoOpen by collectLastValue(underTest.shouldAutoOpen) + val suppressionReason by collectLastValue(underTest.suppressionReason) + + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + false, + MAIN_USER_ID, + ) + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + false, + MAIN_USER_ID, + ) + fakeSettings.putBoolForUser( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + false, + MAIN_USER_ID, + ) + + batteryRepository.fake.setDevicePluggedIn(true) + posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) + fakeDockManager.setIsDocked(true) + + assertThat(shouldAutoOpen).isFalse() + assertThat(suppressionReason) + .isEqualTo( + SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt deleted file mode 100644 index beec184b80e7..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.communal.domain.interactor - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_COMMUNAL_HUB -import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository -import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository -import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository -import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository -import com.android.systemui.kosmos.testScope -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -/** - * This class is a variation of the [CommunalInteractorTest] for cases where communal is disabled. - */ -@SmallTest -@RunWith(AndroidJUnit4::class) -class CommunalInteractorCommunalDisabledTest : SysuiTestCase() { - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - - private lateinit var communalRepository: FakeCommunalSceneRepository - private lateinit var widgetRepository: FakeCommunalWidgetRepository - private lateinit var keyguardRepository: FakeKeyguardRepository - - private lateinit var underTest: CommunalInteractor - - @Before - fun setUp() { - communalRepository = kosmos.fakeCommunalSceneRepository - widgetRepository = kosmos.fakeCommunalWidgetRepository - keyguardRepository = kosmos.fakeKeyguardRepository - - mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB) - - underTest = kosmos.communalInteractor - } - - @Test - fun isCommunalEnabled_false() = - testScope.runTest { assertThat(underTest.isCommunalEnabled.value).isFalse() } - - @Test - fun isCommunalAvailable_whenStorageUnlock_false() = - testScope.runTest { - val isCommunalAvailable by collectLastValue(underTest.isCommunalAvailable) - - assertThat(isCommunalAvailable).isFalse() - - keyguardRepository.setIsEncryptedOrLockdown(false) - runCurrent() - - assertThat(isCommunalAvailable).isFalse() - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 8424746f3db5..b65ecf46dcca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -21,7 +21,6 @@ import android.app.admin.DevicePolicyManager import android.app.admin.devicePolicyManager import android.content.Intent import android.content.pm.UserInfo -import android.content.res.mainResources import android.os.UserHandle import android.os.UserManager import android.os.userManager @@ -39,9 +38,8 @@ import com.android.systemui.Flags.FLAG_COMMUNAL_WIDGET_RESIZING import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher -import com.android.systemui.common.data.repository.batteryRepository -import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.model.CommunalSmartspaceTimer +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository @@ -50,14 +48,9 @@ import com.android.systemui.communal.data.repository.fakeCommunalTutorialReposit import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel -import com.android.systemui.communal.posturing.data.repository.fake -import com.android.systemui.communal.posturing.data.repository.posturingRepository -import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.EditModeState -import com.android.systemui.dock.DockManager -import com.android.systemui.dock.fakeDockManager import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -75,19 +68,16 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.fakeUserTracker import com.android.systemui.statusbar.phone.fakeManagedProfileController import com.android.systemui.testKosmos -import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceTimeBy -import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -98,10 +88,6 @@ import org.mockito.Mockito.verify import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters -/** - * This class of test cases assume that communal is enabled. For disabled cases, see - * [CommunalInteractorCommunalDisabledTest]. - */ @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -109,10 +95,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN) private val secondaryUser = UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0) - private val kosmos = - testKosmos() - .apply { mainResources = mContext.orCreateTestableResources.resources } - .useUnconfinedTestDispatcher() + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val Kosmos.underTest by Kosmos.Fixture { communalInteractor } @@ -128,104 +111,40 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) - - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault, - false, - ) - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault, - false, - ) - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault, - false, - ) - } - - @After - fun tearDown() { - mContext.orCreateTestableResources.removeOverride( - com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault - ) - mContext.orCreateTestableResources.removeOverride( - com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault - ) - mContext.orCreateTestableResources.removeOverride( - com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault - ) } @Test fun communalEnabled_true() = kosmos.runTest { - fakeUserRepository.setSelectedUserInfo(mainUser) + communalSettingsInteractor.setSuppressionReasons(emptyList()) assertThat(underTest.isCommunalEnabled.value).isTrue() } @Test - fun isCommunalAvailable_mainUserUnlockedAndMainUser_true() = + fun isCommunalAvailable_whenKeyguardShowing_true() = kosmos.runTest { - val isAvailable by collectLastValue(underTest.isCommunalAvailable) - assertThat(isAvailable).isFalse() - - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) - - assertThat(isAvailable).isTrue() - } + communalSettingsInteractor.setSuppressionReasons(emptyList()) + fakeKeyguardRepository.setKeyguardShowing(false) - @Test - fun isCommunalAvailable_mainUserLockedAndMainUser_false() = - kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false) - fakeUserRepository.setSelectedUserInfo(mainUser) fakeKeyguardRepository.setKeyguardShowing(true) - - assertThat(isAvailable).isFalse() + assertThat(isAvailable).isTrue() } @Test - fun isCommunalAvailable_mainUserUnlockedAndSecondaryUser_false() = + fun isCommunalAvailable_suppressed() = kosmos.runTest { - val isAvailable by collectLastValue(underTest.isCommunalAvailable) - assertThat(isAvailable).isFalse() - - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(secondaryUser) + communalSettingsInteractor.setSuppressionReasons(emptyList()) fakeKeyguardRepository.setKeyguardShowing(true) - assertThat(isAvailable).isFalse() - } - - @Test - fun isCommunalAvailable_whenKeyguardShowing_true() = - kosmos.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) - assertThat(isAvailable).isFalse() - - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) - assertThat(isAvailable).isTrue() - } - - @Test - fun isCommunalAvailable_communalDisabled_false() = - kosmos.runTest { - mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) - val isAvailable by collectLastValue(underTest.isCommunalAvailable) - assertThat(isAvailable).isFalse() - - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) + communalSettingsInteractor.setSuppressionReasons( + listOf(SuppressionReason.ReasonUnknown()) + ) assertThat(isAvailable).isFalse() } @@ -1280,66 +1199,6 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { .inOrder() } - @Test - fun showCommunalWhileCharging() = - kosmos.runTest { - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) - fakeSettings.putIntForUser( - Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, - 1, - mainUser.id, - ) - - val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal) - batteryRepository.fake.setDevicePluggedIn(false) - assertThat(shouldShowCommunal).isFalse() - - batteryRepository.fake.setDevicePluggedIn(true) - assertThat(shouldShowCommunal).isTrue() - } - - @Test - fun showCommunalWhilePosturedAndCharging() = - kosmos.runTest { - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) - fakeSettings.putIntForUser( - Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, - 1, - mainUser.id, - ) - - val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal) - batteryRepository.fake.setDevicePluggedIn(true) - posturingRepository.fake.setPosturedState(PosturedState.NotPostured) - assertThat(shouldShowCommunal).isFalse() - - posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) - assertThat(shouldShowCommunal).isTrue() - } - - @Test - fun showCommunalWhileDocked() = - kosmos.runTest { - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(mainUser) - fakeKeyguardRepository.setKeyguardShowing(true) - fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, 1, mainUser.id) - - batteryRepository.fake.setDevicePluggedIn(true) - fakeDockManager.setIsDocked(false) - - val shouldShowCommunal by collectLastValue(underTest.shouldShowCommunal) - assertThat(shouldShowCommunal).isFalse() - - fakeDockManager.setIsDocked(true) - fakeDockManager.setDockEvent(DockManager.STATE_DOCKED) - assertThat(shouldShowCommunal).isTrue() - } - private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id))) .thenReturn(disabledFlags) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 8dc7a331dc2d..b8dbc9f77076 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -32,9 +32,9 @@ import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_DIRECT_EDIT_MODE import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository -import com.android.systemui.common.data.repository.batteryRepository -import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.model.CommunalSmartspaceTimer +import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository @@ -50,6 +50,7 @@ import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.communal.domain.interactor.communalTutorialInteractor +import com.android.systemui.communal.domain.interactor.setCommunalEnabled import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalMetricsLogger @@ -101,7 +102,6 @@ import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository -import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceTimeBy @@ -128,7 +128,9 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost + @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler + @Mock private lateinit var metricsLogger: CommunalMetricsLogger private val kosmos = testKosmos() @@ -212,11 +214,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() = testScope.runTest { - // Keyguard showing, storage unlocked, main user, and tutorial not started. keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - userRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true) - setIsMainUser(true) + kosmos.setCommunalEnabled(true) tutorialRepository.setTutorialSettingState( Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED ) @@ -951,21 +950,16 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { fun swipeToCommunal() = kosmos.runTest { setCommunalV2ConfigEnabled(true) - val mainUser = fakeUserRepository.asMainUser() - fakeKeyguardRepository.setKeyguardShowing(true) - fakeUserRepository.setUserUnlocked(mainUser.id, true) - fakeUserTracker.set(userInfos = listOf(mainUser), selectedUserIndex = 0) - fakeSettings.putIntForUser( - Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, - 1, - mainUser.id, + // Suppress manual opening + communalSettingsInteractor.setSuppressionReasons( + listOf(SuppressionReason.ReasonUnknown(FEATURE_MANUAL_OPEN)) ) val viewModel = createViewModel() val swipeToHubEnabled by collectLastValue(viewModel.swipeToHubEnabled) assertThat(swipeToHubEnabled).isFalse() - batteryRepository.fake.setDevicePluggedIn(true) + communalSettingsInteractor.setSuppressionReasons(emptyList()) assertThat(swipeToHubEnabled).isTrue() keyguardTransitionRepository.sendTransitionStep( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt index c15f797aad5d..df10d058c5d1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.communal.widgets import android.content.pm.UserInfo -import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_COMMUNAL_HUB @@ -25,6 +24,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor +import com.android.systemui.communal.domain.interactor.setCommunalEnabled import com.android.systemui.communal.shared.model.FakeGlanceableHubMultiUserHelper import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.coroutines.collectLastValue @@ -37,11 +37,9 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos -import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.user.domain.interactor.userLockedInteractor import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.runCurrent @@ -282,22 +280,12 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { } } - private suspend fun setCommunalAvailable( - available: Boolean, - setKeyguardShowing: Boolean = true, - ) = + private fun setCommunalAvailable(available: Boolean, setKeyguardShowing: Boolean = true) = with(kosmos) { - fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true) - fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + setCommunalEnabled(available) if (setKeyguardShowing) { fakeKeyguardRepository.setKeyguardShowing(true) } - val settingsValue = if (available) 1 else 0 - fakeSettings.putIntForUser( - Settings.Secure.GLANCEABLE_HUB_ENABLED, - settingsValue, - MAIN_USER_INFO.id, - ) } private companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt index 6c4325adced4..046d92d58978 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt @@ -35,7 +35,6 @@ package com.android.systemui.keyguard.domain.interactor import android.os.PowerManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags @@ -43,8 +42,6 @@ import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository -import com.android.systemui.common.data.repository.batteryRepository -import com.android.systemui.common.data.repository.fake import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.setCommunalV2Available @@ -72,7 +69,6 @@ import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.testKosmos -import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth import junit.framework.Assert.assertEquals import kotlinx.coroutines.runBlocking @@ -433,9 +429,7 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun testTransitionToGlanceableHub_onWakeUpFromAod() = kosmos.runTest { - val user = setCommunalV2Available(true) - fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1, user.id) - batteryRepository.fake.setDevicePluggedIn(true) + setCommunalV2Available(true) val currentScene by collectLastValue(communalSceneInteractor.currentScene) fakeCommunalSceneRepository.changeScene(CommunalScenes.Blank) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index 9be786fab34d..096c3dafd01c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -193,6 +193,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun testTransitionToLockscreen_onWake_canNotDream_glanceableHubAvailable() = kosmos.runTest { whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 8df70ef0fd2e..7d5e9a5ed178 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -33,7 +33,7 @@ import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepositor import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSceneTransitionInteractor -import com.android.systemui.communal.domain.interactor.setCommunalAvailable +import com.android.systemui.communal.domain.interactor.setCommunalV2Available import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled import com.android.systemui.communal.shared.model.CommunalScenes @@ -1004,16 +1004,15 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @BrokenWithSceneContainer(339465026) fun occludedToGlanceableHub_communalKtfRefactor() = testScope.runTest { - // GIVEN a device on lockscreen and communal is available - keyguardRepository.setKeyguardShowing(true) - kosmos.setCommunalAvailable(true) - runCurrent() - // GIVEN a prior transition has run to OCCLUDED from GLANCEABLE_HUB runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.OCCLUDED) keyguardRepository.setKeyguardOccluded(true) runCurrent() + // GIVEN a device on lockscreen and communal is available + kosmos.setCommunalV2Available(true) + runCurrent() + // WHEN occlusion ends keyguardRepository.setKeyguardOccluded(false) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt index b82f5fce9e14..ff2e13b51e14 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateDispatcherTest.kt @@ -81,7 +81,7 @@ class SysUIStateDispatcherTest : SysuiTestCase() { private companion object { const val DISPLAY_1 = 1 const val DISPLAY_2 = 2 - const val FLAG_1 = 10L - const val FLAG_2 = 20L + const val FLAG_1 = 1L + const val FLAG_2 = 2L } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt index 09588f9f3751..d1b552906fbb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt @@ -35,10 +35,10 @@ class SysUiStateExtTest : SysuiTestCase() { @Test fun updateFlags() { - underTest.updateFlags(Display.DEFAULT_DISPLAY, 1L to true, 2L to false, 3L to true) + underTest.updateFlags(Display.DEFAULT_DISPLAY, 1L to true, 2L to false, 4L to true) assertThat(underTest.flags and 1L).isNotEqualTo(0L) assertThat(underTest.flags and 2L).isEqualTo(0L) - assertThat(underTest.flags and 3L).isNotEqualTo(0L) + assertThat(underTest.flags and 4L).isNotEqualTo(0L) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index f54c28f4295b..72b003f9f463 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -82,7 +82,6 @@ import com.android.systemui.flags.DisableSceneContainer; import com.android.systemui.flags.EnableSceneContainer; import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.log.LogWtfHandlerRule; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.UserTracker; @@ -108,7 +107,6 @@ import kotlinx.coroutines.flow.StateFlow; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -207,8 +205,6 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { private final FakeExecutor mBackgroundExecutor = new FakeExecutor(mFakeSystemClock); private final Executor mMainExecutor = Runnable::run; // Direct executor - @Rule public final LogWtfHandlerRule wtfHandlerRule = new LogWtfHandlerRule(); - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt new file mode 100644 index 000000000000..6a611ec5b647 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSuppressionStartable.kt @@ -0,0 +1,66 @@ +/* + * 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.communal + +import com.android.systemui.CoreStartable +import com.android.systemui.communal.data.model.SuppressionReason +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.dagger.CommunalTableLog +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@SysUISingleton +class CommunalSuppressionStartable +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, + private val suppressionFlows: Set<@JvmSuppressWildcards Flow<SuppressionReason?>>, + private val communalSettingsInteractor: CommunalSettingsInteractor, + @CommunalTableLog private val tableLogBuffer: TableLogBuffer, +) : CoreStartable { + override fun start() { + getSuppressionReasons() + .onEach { reasons -> communalSettingsInteractor.setSuppressionReasons(reasons) } + .logDiffsForTable( + tableLogBuffer = tableLogBuffer, + columnName = "suppressionReasons", + initialValue = emptyList(), + ) + .flowOn(bgDispatcher) + .launchIn(applicationScope) + } + + private fun getSuppressionReasons(): Flow<List<SuppressionReason>> { + if (!communalSettingsInteractor.isCommunalFlagEnabled()) { + return flowOf(listOf(SuppressionReason.ReasonFlagDisabled)) + } + return combine(suppressionFlows) { reasons -> reasons.filterNotNull() } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index bb3be531aa8a..a31c0bd35453 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -29,6 +29,7 @@ import com.android.systemui.communal.data.repository.CommunalSmartspaceRepositor import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor +import com.android.systemui.communal.domain.suppression.dagger.CommunalSuppressionModule import com.android.systemui.communal.shared.log.CommunalMetricsLogger import com.android.systemui.communal.shared.log.CommunalStatsLogProxyImpl import com.android.systemui.communal.shared.model.CommunalScenes @@ -70,6 +71,7 @@ import kotlinx.coroutines.CoroutineScope CommunalSmartspaceRepositoryModule::class, CommunalStartableModule::class, GlanceableHubWidgetManagerModule::class, + CommunalSuppressionModule::class, ] ) interface CommunalModule { diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt index 7358aa7b3fcd..a4f75e81b6ae 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt @@ -22,6 +22,7 @@ import com.android.systemui.communal.CommunalDreamStartable import com.android.systemui.communal.CommunalMetricsStartable import com.android.systemui.communal.CommunalOngoingContentStartable import com.android.systemui.communal.CommunalSceneStartable +import com.android.systemui.communal.CommunalSuppressionStartable import com.android.systemui.communal.DevicePosturingListener import com.android.systemui.communal.log.CommunalLoggerStartable import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable @@ -73,4 +74,9 @@ interface CommunalStartableModule { @IntoMap @ClassKey(DevicePosturingListener::class) fun bindDevicePosturingistener(impl: DevicePosturingListener): CoreStartable + + @Binds + @IntoMap + @ClassKey(CommunalSuppressionStartable::class) + fun bindCommunalSuppressionStartable(impl: CommunalSuppressionStartable): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt deleted file mode 100644 index 83a5bdb14ebd..000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalEnabledState.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.communal.data.model - -import com.android.systemui.log.table.Diffable -import com.android.systemui.log.table.TableRowLogger -import java.util.EnumSet - -/** Reasons that communal is disabled, primarily for logging. */ -enum class DisabledReason(val loggingString: String) { - /** Communal should be disabled due to invalid current user */ - DISABLED_REASON_INVALID_USER("invalidUser"), - /** Communal should be disabled due to the flag being off */ - DISABLED_REASON_FLAG("flag"), - /** Communal should be disabled because the user has turned off the setting */ - DISABLED_REASON_USER_SETTING("userSetting"), - /** Communal is disabled by the device policy app */ - DISABLED_REASON_DEVICE_POLICY("devicePolicy"), -} - -/** - * Model representing the reasons communal hub should be disabled. Allows logging reasons separately - * for debugging. - */ -@JvmInline -value class CommunalEnabledState( - private val disabledReasons: EnumSet<DisabledReason> = - EnumSet.noneOf(DisabledReason::class.java) -) : Diffable<CommunalEnabledState>, Set<DisabledReason> by disabledReasons { - - /** Creates [CommunalEnabledState] with a single reason for being disabled */ - constructor(reason: DisabledReason) : this(EnumSet.of(reason)) - - /** Checks if there are any reasons communal should be disabled. If none, returns true. */ - val enabled: Boolean - get() = isEmpty() - - override fun logDiffs(prevVal: CommunalEnabledState, row: TableRowLogger) { - for (reason in DisabledReason.entries) { - val newVal = contains(reason) - if (newVal != prevVal.contains(reason)) { - row.logChange( - columnName = reason.loggingString, - value = newVal, - ) - } - } - } - - override fun logFull(row: TableRowLogger) { - for (reason in DisabledReason.entries) { - row.logChange(columnName = reason.loggingString, value = contains(reason)) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt new file mode 100644 index 000000000000..5fb1c4e84eef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalFeature.kt @@ -0,0 +1,41 @@ +/* + * 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.communal.data.model + +import android.annotation.IntDef + +@Retention(AnnotationRetention.SOURCE) +@IntDef( + flag = true, + prefix = ["FEATURE_"], + value = [FEATURE_AUTO_OPEN, FEATURE_MANUAL_OPEN, FEATURE_ENABLED, FEATURE_ALL], +) +annotation class CommunalFeature + +/** If we should automatically open the hub */ +const val FEATURE_AUTO_OPEN: Int = 1 + +/** If the user is allowed to manually open the hub */ +const val FEATURE_MANUAL_OPEN: Int = 1 shl 1 + +/** + * If the hub should be considered enabled. If not, it may be cleaned up entirely to reduce memory + * footprint. + */ +const val FEATURE_ENABLED: Int = 1 shl 2 + +const val FEATURE_ALL: Int = FEATURE_ENABLED or FEATURE_MANUAL_OPEN or FEATURE_AUTO_OPEN diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt new file mode 100644 index 000000000000..de05bed7ef57 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/SuppressionReason.kt @@ -0,0 +1,63 @@ +/* + * 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.communal.data.model + +sealed interface SuppressionReason { + @CommunalFeature val suppressedFeatures: Int + + /** Whether this reason suppresses a particular feature. */ + fun isSuppressed(@CommunalFeature feature: Int): Boolean { + return (suppressedFeatures and feature) != 0 + } + + /** Suppress hub automatically opening due to Android Auto projection */ + data object ReasonCarProjection : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_AUTO_OPEN + } + + /** Suppress hub due to the "When to dream" conditions not being met */ + data class ReasonWhenToAutoShow(override val suppressedFeatures: Int) : SuppressionReason + + /** Suppress hub due to device policy */ + data object ReasonDevicePolicy : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_ALL + } + + /** Suppress hub due to the user disabling the setting */ + data object ReasonSettingDisabled : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_ALL + } + + /** Suppress hub due to the user being locked */ + data object ReasonUserLocked : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_ALL + } + + /** Suppress hub due the a secondary user being active */ + data object ReasonSecondaryUser : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_ALL + } + + /** Suppress hub due to the flag being disabled */ + data object ReasonFlagDisabled : SuppressionReason { + override val suppressedFeatures: Int = FEATURE_ALL + } + + /** Suppress hub due to an unknown reason, used as initial state and in tests */ + data class ReasonUnknown(override val suppressedFeatures: Int = FEATURE_ALL) : + SuppressionReason +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt new file mode 100644 index 000000000000..4fe641a78d4b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CarProjectionRepository.kt @@ -0,0 +1,71 @@ +/* + * 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.communal.data.repository + +import android.app.UiModeManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.util.kotlin.emitOnStart +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +interface CarProjectionRepository { + /** Whether car projection is active. */ + val projectionActive: Flow<Boolean> + + /** + * Checks the system for the current car projection state. + * + * @return True if projection is active, false otherwise. + */ + suspend fun isProjectionActive(): Boolean +} + +@SysUISingleton +class CarProjectionRepositoryImpl +@Inject +constructor( + private val uiModeManager: UiModeManager, + @Background private val bgDispatcher: CoroutineDispatcher, +) : CarProjectionRepository { + override val projectionActive: Flow<Boolean> = + conflatedCallbackFlow { + val listener = + UiModeManager.OnProjectionStateChangedListener { _, _ -> trySend(Unit) } + uiModeManager.addOnProjectionStateChangedListener( + UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, + bgDispatcher.asExecutor(), + listener, + ) + awaitClose { uiModeManager.removeOnProjectionStateChangedListener(listener) } + } + .emitOnStart() + .map { isProjectionActive() } + .flowOn(bgDispatcher) + + override suspend fun isProjectionActive(): Boolean = + withContext(bgDispatcher) { + (uiModeManager.activeProjectionTypes and UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) != 0 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt index 7f137f3b976b..0d590db97860 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt @@ -22,4 +22,6 @@ import dagger.Module @Module interface CommunalRepositoryModule { @Binds fun communalRepository(impl: CommunalSceneRepositoryImpl): CommunalSceneRepository + + @Binds fun carProjectionRepository(impl: CarProjectionRepositoryImpl): CarProjectionRepository } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt index 4c291a0c5a2e..6f688d172843 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt @@ -26,12 +26,9 @@ import android.provider.Settings import com.android.systemui.Flags.communalHub import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.communal.data.model.CommunalEnabledState -import com.android.systemui.communal.data.model.DisabledReason -import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_DEVICE_POLICY -import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_FLAG -import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_INVALID_USER -import com.android.systemui.communal.data.model.DisabledReason.DISABLED_REASON_USER_SETTING +import com.android.systemui.communal.data.model.CommunalFeature +import com.android.systemui.communal.data.model.FEATURE_ALL +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule.Companion.DEFAULT_BACKGROUND_TYPE import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.WhenToDream @@ -43,22 +40,23 @@ import com.android.systemui.flags.Flags import com.android.systemui.util.kotlin.emitOnStart import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow -import java.util.EnumSet import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart interface CommunalSettingsRepository { - /** A [CommunalEnabledState] for the specified user. */ - fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState> + /** Whether a particular feature is enabled */ + fun isEnabled(@CommunalFeature feature: Int): Flow<Boolean> - fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean> + /** + * Suppresses the hub with the given reasons. If there are no reasons, the hub will not be + * suppressed. + */ + fun setSuppressionReasons(reasons: List<SuppressionReason>) /** * Returns a [WhenToDream] for the specified user, indicating what state the device should be in @@ -66,6 +64,9 @@ interface CommunalSettingsRepository { */ fun getWhenToDreamState(user: UserInfo): Flow<WhenToDream> + /** Returns whether glanceable hub is enabled by the current user. */ + fun getSettingEnabledByUser(user: UserInfo): Flow<Boolean> + /** * Returns true if any glanceable hub functionality should be enabled via configs and flags. * @@ -123,6 +124,19 @@ constructor( resources.getBoolean(com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault) } + private val _suppressionReasons = + MutableStateFlow<List<SuppressionReason>>( + // Suppress hub by default until we get an initial update. + listOf(SuppressionReason.ReasonUnknown(FEATURE_ALL)) + ) + + override fun isEnabled(@CommunalFeature feature: Int): Flow<Boolean> = + _suppressionReasons.map { reasons -> reasons.none { it.isSuppressed(feature) } } + + override fun setSuppressionReasons(reasons: List<SuppressionReason>) { + _suppressionReasons.value = reasons + } + override fun getFlagEnabled(): Boolean { return if (getV2FlagEnabled()) { true @@ -138,44 +152,6 @@ constructor( glanceableHubV2() } - override fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState> { - if (!user.isMain) { - return flowOf(CommunalEnabledState(DISABLED_REASON_INVALID_USER)) - } - if (!getFlagEnabled()) { - return flowOf(CommunalEnabledState(DISABLED_REASON_FLAG)) - } - return combine( - getEnabledByUser(user).mapToReason(DISABLED_REASON_USER_SETTING), - getAllowedByDevicePolicy(user).mapToReason(DISABLED_REASON_DEVICE_POLICY), - ) { reasons -> - reasons.filterNotNull() - } - .map { reasons -> - if (reasons.isEmpty()) { - EnumSet.noneOf(DisabledReason::class.java) - } else { - EnumSet.copyOf(reasons) - } - } - .map { reasons -> CommunalEnabledState(reasons) } - .flowOn(bgDispatcher) - } - - override fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean> = - secureSettings - .observerFlow(userId = user.id, names = arrayOf(Settings.Secure.SCREENSAVER_ENABLED)) - // Force an update - .onStart { emit(Unit) } - .map { - secureSettings.getIntForUser( - Settings.Secure.SCREENSAVER_ENABLED, - SCREENSAVER_ENABLED_SETTING_DEFAULT, - user.id, - ) == 1 - } - .flowOn(bgDispatcher) - override fun getWhenToDreamState(user: UserInfo): Flow<WhenToDream> = secureSettings .observerFlow( @@ -247,11 +223,11 @@ constructor( ?: defaultBackgroundType } - private fun getEnabledByUser(user: UserInfo): Flow<Boolean> = + override fun getSettingEnabledByUser(user: UserInfo): Flow<Boolean> = secureSettings .observerFlow(userId = user.id, names = arrayOf(Settings.Secure.GLANCEABLE_HUB_ENABLED)) // Force an update - .onStart { emit(Unit) } + .emitOnStart() .map { secureSettings.getIntForUser( Settings.Secure.GLANCEABLE_HUB_ENABLED, @@ -259,17 +235,13 @@ constructor( user.id, ) == 1 } + .flowOn(bgDispatcher) companion object { const val GLANCEABLE_HUB_BACKGROUND_SETTING = "glanceable_hub_background" private const val ENABLED_SETTING_DEFAULT = 1 - private const val SCREENSAVER_ENABLED_SETTING_DEFAULT = 0 } } private fun DevicePolicyManager.areKeyguardWidgetsAllowed(userId: Int): Boolean = (getKeyguardDisabledFeatures(null, userId) and KEYGUARD_DISABLE_WIDGETS_ALL) == 0 - -private fun Flow<Boolean>.mapToReason(reason: DisabledReason) = map { enabled -> - if (enabled) null else reason -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt new file mode 100644 index 000000000000..17b61e1c6fdf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractor.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.domain.interactor + +import com.android.systemui.communal.data.repository.CarProjectionRepository +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +@SysUISingleton +class CarProjectionInteractor @Inject constructor(repository: CarProjectionRepository) { + /** Whether car projection is active. */ + val projectionActive = repository.projectionActive +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt new file mode 100644 index 000000000000..51df3338a18e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt @@ -0,0 +1,79 @@ +/* + * 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.communal.domain.interactor + +import com.android.systemui.common.domain.interactor.BatteryInteractor +import com.android.systemui.communal.dagger.CommunalModule.Companion.SWIPE_TO_HUB +import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN +import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN +import com.android.systemui.communal.data.model.SuppressionReason +import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor +import com.android.systemui.communal.shared.model.WhenToDream +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dock.DockManager +import com.android.systemui.dock.retrieveIsDocked +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated +import javax.inject.Inject +import javax.inject.Named +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +@SysUISingleton +class CommunalAutoOpenInteractor +@Inject +constructor( + communalSettingsInteractor: CommunalSettingsInteractor, + @Background private val backgroundContext: CoroutineContext, + private val batteryInteractor: BatteryInteractor, + private val posturingInteractor: PosturingInteractor, + private val dockManager: DockManager, + @Named(SWIPE_TO_HUB) private val allowSwipeAlways: Boolean, +) { + val shouldAutoOpen: Flow<Boolean> = + communalSettingsInteractor.whenToDream + .flatMapLatestConflated { whenToDream -> + when (whenToDream) { + WhenToDream.WHILE_CHARGING -> batteryInteractor.isDevicePluggedIn + WhenToDream.WHILE_DOCKED -> { + allOf(batteryInteractor.isDevicePluggedIn, dockManager.retrieveIsDocked()) + } + WhenToDream.WHILE_POSTURED -> { + allOf(batteryInteractor.isDevicePluggedIn, posturingInteractor.postured) + } + WhenToDream.NEVER -> flowOf(false) + } + } + .flowOn(backgroundContext) + + val suppressionReason: Flow<SuppressionReason?> = + shouldAutoOpen.map { conditionMet -> + if (conditionMet) { + null + } else { + var suppressedFeatures = FEATURE_AUTO_OPEN + if (!allowSwipeAlways) { + suppressedFeatures = suppressedFeatures or FEATURE_MANUAL_OPEN + } + SuppressionReason.ReasonWhenToAutoShow(suppressedFeatures) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 564628d3f52f..684c52ad45f3 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -30,13 +30,11 @@ import com.android.compose.animation.scene.TransitionKey import com.android.systemui.Flags.communalResponsiveGrid import com.android.systemui.Flags.glanceableHubBlurredBackground import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.common.domain.interactor.BatteryInteractor import com.android.systemui.communal.data.repository.CommunalMediaRepository import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent -import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize.FULL @@ -45,14 +43,11 @@ import com.android.systemui.communal.shared.model.CommunalContentSize.FixedSize. import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.shared.model.EditModeState -import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dock.DockManager -import com.android.systemui.dock.retrieveIsDocked import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -69,11 +64,8 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.ManagedProfileController -import com.android.systemui.user.domain.interactor.UserLockedInteractor import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf -import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.emitOnStart -import com.android.systemui.util.kotlin.isDevicePluggedIn import javax.inject.Inject import kotlin.time.Duration.Companion.minutes import kotlinx.coroutines.CoroutineDispatcher @@ -125,10 +117,6 @@ constructor( @CommunalLog logBuffer: LogBuffer, @CommunalTableLog tableLogBuffer: TableLogBuffer, private val managedProfileController: ManagedProfileController, - private val batteryInteractor: BatteryInteractor, - private val dockManager: DockManager, - private val posturingInteractor: PosturingInteractor, - private val userLockedInteractor: UserLockedInteractor, ) { private val logger = Logger(logBuffer, "CommunalInteractor") @@ -162,11 +150,7 @@ constructor( /** Whether communal features are enabled and available. */ val isCommunalAvailable: Flow<Boolean> = - allOf( - communalSettingsInteractor.isCommunalEnabled, - userLockedInteractor.isUserUnlocked(userManager.mainUser), - keyguardInteractor.isKeyguardShowing, - ) + allOf(communalSettingsInteractor.isCommunalEnabled, keyguardInteractor.isKeyguardShowing) .distinctUntilChanged() .onEach { available -> logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) { @@ -184,37 +168,6 @@ constructor( replay = 1, ) - /** - * Whether communal hub should be shown automatically, depending on the user's [WhenToDream] - * state. - */ - val shouldShowCommunal: StateFlow<Boolean> = - allOf( - isCommunalAvailable, - communalSettingsInteractor.whenToDream - .flatMapLatest { whenToDream -> - when (whenToDream) { - WhenToDream.NEVER -> flowOf(false) - - WhenToDream.WHILE_CHARGING -> batteryInteractor.isDevicePluggedIn - - WhenToDream.WHILE_DOCKED -> - allOf( - batteryInteractor.isDevicePluggedIn, - dockManager.retrieveIsDocked(), - ) - - WhenToDream.WHILE_POSTURED -> - allOf( - batteryInteractor.isDevicePluggedIn, - posturingInteractor.postured, - ) - } - } - .flowOn(bgDispatcher), - ) - .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) - private val _isDisclaimerDismissed = MutableStateFlow(false) val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt index a0b1261df346..ae89b39175c1 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt @@ -18,17 +18,18 @@ package com.android.systemui.communal.domain.interactor import android.content.pm.UserInfo import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.communal.data.model.CommunalEnabledState +import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN +import com.android.systemui.communal.data.model.FEATURE_ENABLED +import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.CommunalSettingsRepository import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.log.dagger.CommunalTableLog -import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.settings.UserTracker import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -53,33 +54,43 @@ constructor( private val repository: CommunalSettingsRepository, userInteractor: SelectedUserInteractor, private val userTracker: UserTracker, - @CommunalTableLog tableLogBuffer: TableLogBuffer, ) { - /** Whether or not communal is enabled for the currently selected user. */ + /** Whether communal is enabled at all. */ val isCommunalEnabled: StateFlow<Boolean> = - userInteractor.selectedUserInfo - .flatMapLatest { user -> repository.getEnabledState(user) } - .logDiffsForTable( - tableLogBuffer = tableLogBuffer, - columnPrefix = "disabledReason", - initialValue = CommunalEnabledState(), - ) - .map { model -> model.enabled } - // Start this eagerly since the value is accessed synchronously in many places. + repository + .isEnabled(FEATURE_ENABLED) .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) - /** Whether or not screensaver (dreams) is enabled for the currently selected user. */ - val isScreensaverEnabled: Flow<Boolean> = - userInteractor.selectedUserInfo.flatMapLatest { user -> - repository.getScreensaverEnabledState(user) - } + /** Whether manually opening the hub is enabled */ + val manualOpenEnabled: StateFlow<Boolean> = + repository + .isEnabled(FEATURE_MANUAL_OPEN) + .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) + + /** Whether auto-opening the hub is enabled */ + val autoOpenEnabled: StateFlow<Boolean> = + repository + .isEnabled(FEATURE_AUTO_OPEN) + .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) /** When to dream for the currently selected user. */ val whenToDream: Flow<WhenToDream> = - userInteractor.selectedUserInfo.flatMapLatest { user -> + userInteractor.selectedUserInfo.flatMapLatestConflated { user -> repository.getWhenToDreamState(user) } + /** Whether communal hub is allowed by device policy for the current user */ + val allowedForCurrentUserByDevicePolicy: Flow<Boolean> = + userInteractor.selectedUserInfo.flatMapLatestConflated { user -> + repository.getAllowedByDevicePolicy(user) + } + + /** Whether the hub is enabled for the current user */ + val settingEnabledForCurrentUser: Flow<Boolean> = + userInteractor.selectedUserInfo.flatMapLatestConflated { user -> + repository.getSettingEnabledByUser(user) + } + /** * Returns true if any glanceable hub functionality should be enabled via configs and flags. * @@ -109,6 +120,14 @@ constructor( */ fun isV2FlagEnabled(): Boolean = repository.getV2FlagEnabled() + /** + * Suppresses the hub with the given reasons. If there are no reasons, the hub will not be + * suppressed. + */ + fun setSuppressionReasons(reasons: List<SuppressionReason>) { + repository.setSuppressionReasons(reasons) + } + /** The type of background to use for the hub. Used to experiment with different backgrounds */ val communalBackground: Flow<CommunalBackgroundType> = userInteractor.selectedUserInfo diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt new file mode 100644 index 000000000000..a10e90f09cc2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/FlowExt.kt @@ -0,0 +1,24 @@ +/* + * 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.communal.domain.suppression + +import com.android.systemui.communal.data.model.SuppressionReason +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +fun Flow<Boolean>.mapToReasonIfNotAllowed(reason: SuppressionReason): Flow<SuppressionReason?> = + this.map { allowed -> if (allowed) null else reason } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt new file mode 100644 index 000000000000..c62d77eee287 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/suppression/dagger/CommunalSuppressionModule.kt @@ -0,0 +1,103 @@ +/* + * 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.communal.domain.suppression.dagger + +import com.android.systemui.Flags.glanceableHubV2 +import com.android.systemui.communal.data.model.SuppressionReason +import com.android.systemui.communal.domain.interactor.CarProjectionInteractor +import com.android.systemui.communal.domain.interactor.CommunalAutoOpenInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.communal.domain.suppression.mapToReasonIfNotAllowed +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.user.domain.interactor.UserLockedInteractor +import com.android.systemui.util.kotlin.BooleanFlowOperators.not +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import dagger.multibindings.Multibinds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@Module +interface CommunalSuppressionModule { + /** + * A set of reasons why communal may be suppressed. Ensures that this can be injected even if + * it's empty. + */ + @Multibinds fun suppressorSet(): Set<Flow<SuppressionReason?>> + + companion object { + @Provides + @IntoSet + fun provideCarProjectionSuppressor( + interactor: CarProjectionInteractor + ): Flow<SuppressionReason?> { + if (!glanceableHubV2()) { + return flowOf(null) + } + return not(interactor.projectionActive) + .mapToReasonIfNotAllowed(SuppressionReason.ReasonCarProjection) + } + + @Provides + @IntoSet + fun provideDevicePolicySuppressor( + interactor: CommunalSettingsInteractor + ): Flow<SuppressionReason?> { + return interactor.allowedForCurrentUserByDevicePolicy.mapToReasonIfNotAllowed( + SuppressionReason.ReasonDevicePolicy + ) + } + + @Provides + @IntoSet + fun provideSettingDisabledSuppressor( + interactor: CommunalSettingsInteractor + ): Flow<SuppressionReason?> { + return interactor.settingEnabledForCurrentUser.mapToReasonIfNotAllowed( + SuppressionReason.ReasonSettingDisabled + ) + } + + @Provides + @IntoSet + fun bindUserLockedSuppressor(interactor: UserLockedInteractor): Flow<SuppressionReason?> { + return interactor.currentUserUnlocked.mapToReasonIfNotAllowed( + SuppressionReason.ReasonUserLocked + ) + } + + @Provides + @IntoSet + fun provideAutoOpenSuppressor( + interactor: CommunalAutoOpenInteractor + ): Flow<SuppressionReason?> { + return interactor.suppressionReason + } + + @Provides + @IntoSet + fun provideMainUserSuppressor( + interactor: SelectedUserInteractor + ): Flow<SuppressionReason?> { + return interactor.selectedUserInfo + .map { it.isMain } + .mapToReasonIfNotAllowed(SuppressionReason.ReasonSecondaryUser) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 62a98d7a48ea..857fa5cac3e2 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -369,12 +369,10 @@ constructor( val swipeToHubEnabled: Flow<Boolean> by lazy { val inAllowedDeviceState = - if (swipeToHub) { - MutableStateFlow(true) - } else if (v2FlagEnabled()) { - communalInteractor.shouldShowCommunal + if (v2FlagEnabled()) { + communalSettingsInteractor.manualOpenEnabled } else { - MutableStateFlow(false) + MutableStateFlow(swipeToHub) } if (v2FlagEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt index a7c078f235b4..36b75c6fc6b8 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt @@ -61,7 +61,7 @@ constructor( fun startTransitionFromDream() { val showGlanceableHub = if (communalSettingsInteractor.isV2FlagEnabled()) { - communalInteractor.shouldShowCommunal.value + communalSettingsInteractor.autoOpenEnabled.value } else { communalInteractor.isCommunalEnabled.value && !keyguardUpdateMonitor.isEncryptedOrLockdown(userTracker.userId) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index ef06a85bd0d9..54af8f5b9806 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -20,7 +20,6 @@ import android.animation.ValueAnimator import android.util.Log import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalScenes @@ -60,7 +59,6 @@ constructor( private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor, private val communalSettingsInteractor: CommunalSettingsInteractor, private val communalSceneInteractor: CommunalSceneInteractor, - private val communalInteractor: CommunalInteractor, ) : TransitionInteractor( fromState = KeyguardState.AOD, @@ -110,7 +108,7 @@ constructor( val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val biometricUnlockMode = keyguardInteractor.biometricUnlockState.value.mode val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value - val shouldShowCommunal = communalInteractor.shouldShowCommunal.value + val shouldShowCommunal = communalSettingsInteractor.autoOpenEnabled.value if (!maybeHandleInsecurePowerGesture()) { val shouldTransitionToLockscreen = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 6f5f662d6fa3..1fc41085f772 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -153,7 +153,7 @@ constructor( .filterRelevantKeyguardStateAnd { isAwake -> isAwake } .sample( communalInteractor.isCommunalAvailable, - communalInteractor.shouldShowCommunal, + communalSettingsInteractor.autoOpenEnabled, ) .collect { (_, isCommunalAvailable, shouldShowCommunal) -> val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value @@ -209,7 +209,7 @@ constructor( powerInteractor.detailedWakefulness .filterRelevantKeyguardStateAnd { it.isAwake() } .sample( - communalInteractor.shouldShowCommunal, + communalSettingsInteractor.autoOpenEnabled, communalInteractor.isCommunalAvailable, keyguardInteractor.biometricUnlockState, wakeToGoneInteractor.canWakeDirectlyToGone, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 0fb98ffa4a30..3b1b6fcc45f2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -115,7 +115,7 @@ constructor( powerInteractor.isAwake .debounce(50L) .filterRelevantKeyguardStateAnd { isAwake -> isAwake } - .sample(communalInteractor.shouldShowCommunal) + .sample(communalSettingsInteractor.autoOpenEnabled) .collect { shouldShowCommunal -> if (shouldShowCommunal) { // This case handles tapping the power button to transition through diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index a01dc02bbd9f..f8c7a86687dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -20,7 +20,6 @@ import android.animation.ValueAnimator import android.util.MathUtils import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalScenes @@ -69,7 +68,6 @@ constructor( private val shadeRepository: ShadeRepository, powerInteractor: PowerInteractor, private val communalSettingsInteractor: CommunalSettingsInteractor, - private val communalInteractor: CommunalInteractor, private val communalSceneInteractor: CommunalSceneInteractor, private val swipeToDismissInteractor: SwipeToDismissInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, @@ -355,7 +353,7 @@ constructor( private fun listenForLockscreenToGlanceableHubV2() { scope.launch { - communalInteractor.shouldShowCommunal + communalSettingsInteractor.autoOpenEnabled .filterRelevantKeyguardStateAnd { shouldShow -> shouldShow } .collect { communalSceneInteractor.changeScene( diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaViewHolder.kt index 0e8e595f5b06..848d8221e4db 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaViewHolder.kt @@ -26,6 +26,10 @@ import android.widget.SeekBar import android.widget.TextView import androidx.constraintlayout.widget.Barrier import com.android.internal.widget.CachingIconView +import com.android.systemui.FontStyles.GSF_HEADLINE_SMALL +import com.android.systemui.FontStyles.GSF_LABEL_LARGE +import com.android.systemui.FontStyles.GSF_LABEL_MEDIUM +import com.android.systemui.FontStyles.GSF_TITLE_MEDIUM import com.android.systemui.res.R import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView import com.android.systemui.surfaceeffects.ripple.MultiRippleView @@ -177,9 +181,9 @@ class MediaViewHolder constructor(itemView: View) { R.id.touch_ripple_view, ) - val headlineSmallTF: Typeface = Typeface.create("gsf-headline-small", Typeface.NORMAL) - val titleMediumTF: Typeface = Typeface.create("gsf-title-medium", Typeface.NORMAL) - val labelMediumTF: Typeface = Typeface.create("gsf-label-medium", Typeface.NORMAL) - val labelLargeTF: Typeface = Typeface.create("gsf-label-large", Typeface.NORMAL) + val headlineSmallTF: Typeface = Typeface.create(GSF_HEADLINE_SMALL, Typeface.NORMAL) + val titleMediumTF: Typeface = Typeface.create(GSF_TITLE_MEDIUM, Typeface.NORMAL) + val labelMediumTF: Typeface = Typeface.create(GSF_LABEL_MEDIUM, Typeface.NORMAL) + val labelLargeTF: Typeface = Typeface.create(GSF_LABEL_LARGE, Typeface.NORMAL) } } diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt b/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt index aaed606f8fb2..cec846e84f01 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt @@ -43,8 +43,6 @@ class StateChange { return this } - fun hasChanges() = flagsToSet != 0L || flagsToClear != 0L - /** * Applies all changed flags to [sysUiState]. * @@ -83,6 +81,7 @@ class StateChange { iterateBits(flagsToSet or flagsToClear) { bit -> sysUiState.setFlag(bit, false) } } + /** Resets all the pending changes. */ fun clear() { flagsToSet = 0 flagsToClear = 0 diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt index e99ee7ddb919..71cb74543485 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.display.data.repository.PerDisplayInstanceProviderWithTeardown import com.android.systemui.dump.DumpManager import com.android.systemui.model.SysUiState.SysUiStateCallback +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.shared.system.QuickStepContract import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags import dagger.assisted.Assisted @@ -29,6 +30,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dalvik.annotation.optimization.NeverCompile import java.io.PrintWriter +import java.lang.Long.bitCount import javax.inject.Inject /** Contains sysUi state flags and notifies registered listeners whenever changes happen. */ @@ -111,8 +113,7 @@ constructor( get() = _flags private var _flags: Long = 0 - private var flagsToSet: Long = 0 - private var flagsToClear: Long = 0 + private val stateChange = StateChange() /** * Add listener to be notified of changes made to SysUI state. The callback will also be called @@ -132,13 +133,11 @@ constructor( /** Methods to this call can be chained together before calling [.commitUpdate]. */ override fun setFlag(@SystemUiStateFlags flag: Long, enabled: Boolean): SysUiState { - val toSet = flagWithOptionalOverrides(flag, enabled, displayId, sceneContainerPlugin) - - if (toSet) { - flagsToSet = flagsToSet or flag - } else { - flagsToClear = flagsToClear or flag + if (ShadeWindowGoesAround.isEnabled && bitCount(flag) > 1) { + error("Flags should be a single bit.") } + val toSet = flagWithOptionalOverrides(flag, enabled, displayId, sceneContainerPlugin) + stateChange.setFlag(flag, toSet) return this } @@ -147,27 +146,19 @@ constructor( ReplaceWith("commitUpdate()"), ) override fun commitUpdate(displayId: Int) { - // TODO b/398011576 - handle updates for different displays. commitUpdate() } override fun commitUpdate() { - updateFlags() - flagsToSet = 0 - flagsToClear = 0 - } - - private fun updateFlags() { - var newState = flags - newState = newState or flagsToSet - newState = newState and flagsToClear.inv() + val newState = stateChange.applyTo(flags) notifyAndSetSystemUiStateChanged(newState, flags) + stateChange.clear() } /** Notify all those who are registered that the state has changed. */ private fun notifyAndSetSystemUiStateChanged(newFlags: Long, oldFlags: Long) { if (SysUiState.DEBUG) { - Log.d(TAG, "SysUiState changed: old=$oldFlags new=$newFlags") + Log.d(TAG, "SysUiState changed for displayId=$displayId: old=$oldFlags new=$newFlags") } if (newFlags != oldFlags) { _flags = newFlags @@ -185,6 +176,8 @@ constructor( pw.println(QuickStepContract.isBackGestureDisabled(flags, false /* forTrackpad */)) pw.print(" assistantGestureDisabled=") pw.println(QuickStepContract.isAssistantGestureDisabled(flags)) + pw.print(" pendingStateChanges=") + pw.println(stateChange.toString()) } override fun destroy() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS index b2764e1a2302..6cebcd98a0ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OWNERS @@ -24,6 +24,8 @@ per-file *Blur* = set noparent per-file *Blur* = shanh@google.com, rahulbanerjee@google.com # Not setting noparent here, since *Mode* matches many other classes (e.g., *ViewModel*) per-file *Mode* = file:notification/OWNERS +per-file *SmartReply* = set noparent +per-file *SmartReply* = file:notification/OWNERS per-file *RemoteInput* = set noparent per-file *RemoteInput* = file:notification/OWNERS per-file *EmptyShadeView* = set noparent diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt index 3bd8af690763..6657c428a594 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt @@ -20,6 +20,7 @@ import android.os.UserHandle import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -31,7 +32,14 @@ class UserLockedInteractor constructor( @Background val backgroundDispatcher: CoroutineDispatcher, val userRepository: UserRepository, + val selectedUserInteractor: SelectedUserInteractor, ) { + /** Whether the current user is unlocked */ + val currentUserUnlocked: Flow<Boolean> = + selectedUserInteractor.selectedUserInfo.flatMapLatestConflated { user -> + isUserUnlocked(user.userHandle) + } + fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> = userRepository.isUserUnlocked(userHandle).flowOn(backgroundDispatcher) } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt index 94b3fd244a92..6cdc94251d21 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt @@ -47,6 +47,11 @@ abstract class SettingsForUserRepository( .distinctUntilChanged() .flowOn(backgroundDispatcher) + fun intSettingForUser(userId: Int, name: String, defaultValue: Int = 0): Flow<Int> = + settingObserver(name, userId) { userSettings.getIntForUser(name, defaultValue, userId) } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + fun <T> settingObserver(name: String, userId: Int, settingsReader: () -> T): Flow<T> { return userSettings .observerFlow(userId, name) @@ -63,4 +68,14 @@ abstract class SettingsForUserRepository( userSettings.getBoolForUser(name, defaultValue, userId) } } + + suspend fun setIntForUser(userId: Int, name: String, value: Int) { + withContext(backgroundContext) { userSettings.putIntForUser(name, value, userId) } + } + + suspend fun getIntForUser(userId: Int, name: String, defaultValue: Int = 0): Int { + return withContext(backgroundContext) { + userSettings.getIntForUser(name, defaultValue, userId) + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt index 845be0252581..60345a358bac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -24,12 +24,15 @@ import android.widget.LinearLayout import android.window.RemoteTransition import android.window.TransitionFilter import android.window.WindowAnimationState +import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.activity.EmptyTestActivity +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope -import com.android.systemui.shared.Flags +import com.android.systemui.shared.Flags as SharedFlags import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.wm.shell.shared.ShellTransitions @@ -43,7 +46,6 @@ import kotlin.concurrent.thread import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before @@ -57,8 +59,8 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` -import org.mockito.Spy import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.spy @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -66,21 +68,23 @@ import org.mockito.junit.MockitoJUnit @RunWithLooper class ActivityTransitionAnimatorTest : SysuiTestCase() { private val kosmos = testKosmos() - private val transitionContainer = LinearLayout(mContext) + private val mainExecutor = context.mainExecutor private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor) private val testShellTransitions = FakeShellTransitions() + + private val Kosmos.underTest by Kosmos.Fixture { activityTransitionAnimator } + @Mock lateinit var callback: ActivityTransitionAnimator.Callback @Mock lateinit var listener: ActivityTransitionAnimator.Listener - @Spy private val controller = TestTransitionAnimatorController(transitionContainer) @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback - private lateinit var underTest: ActivityTransitionAnimator - @get:Rule val rule = MockitoJUnit.rule() + @get:Rule(order = 0) val mockitoRule = MockitoJUnit.rule() + @get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java) @Before fun setup() { - underTest = + kosmos.activityTransitionAnimator = ActivityTransitionAnimator( mainExecutor, ActivityTransitionAnimator.TransitionRegister.fromShellTransitions( @@ -89,19 +93,20 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { testTransitionAnimator, testTransitionAnimator, disableWmTimeout = true, + skipReparentTransaction = true, ) - underTest.callback = callback - underTest.addListener(listener) + kosmos.activityTransitionAnimator.callback = callback + kosmos.activityTransitionAnimator.addListener(listener) } @After fun tearDown() { - underTest.removeListener(listener) + kosmos.activityTransitionAnimator.removeListener(listener) } private fun startIntentWithAnimation( - animator: ActivityTransitionAnimator = underTest, - controller: ActivityTransitionAnimator.Controller? = this.controller, + controller: ActivityTransitionAnimator.Controller?, + animator: ActivityTransitionAnimator = kosmos.activityTransitionAnimator, animate: Boolean = true, intentStarter: (RemoteAnimationAdapter?) -> Int, ) { @@ -119,129 +124,152 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun animationAdapterIsNullIfControllerIsNull() { - var startedIntent = false - var animationAdapter: RemoteAnimationAdapter? = null + kosmos.runTest { + var startedIntent = false + var animationAdapter: RemoteAnimationAdapter? = null - startIntentWithAnimation(controller = null) { adapter -> - startedIntent = true - animationAdapter = adapter + startIntentWithAnimation(controller = null) { adapter -> + startedIntent = true + animationAdapter = adapter - ActivityManager.START_SUCCESS - } + ActivityManager.START_SUCCESS + } - assertTrue(startedIntent) - assertNull(animationAdapter) + assertTrue(startedIntent) + assertNull(animationAdapter) + } } @Test fun animatesIfActivityOpens() { - val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) - var animationAdapter: RemoteAnimationAdapter? = null - startIntentWithAnimation { adapter -> - animationAdapter = adapter - ActivityManager.START_SUCCESS - } + kosmos.runTest { + val controller = createController() + val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) + var animationAdapter: RemoteAnimationAdapter? = null + startIntentWithAnimation(controller) { adapter -> + animationAdapter = adapter + ActivityManager.START_SUCCESS + } - assertNotNull(animationAdapter) - waitForIdleSync() - verify(controller).onIntentStarted(willAnimateCaptor.capture()) - assertTrue(willAnimateCaptor.value) + assertNotNull(animationAdapter) + waitForIdleSync() + verify(controller).onIntentStarted(willAnimateCaptor.capture()) + assertTrue(willAnimateCaptor.value) + } } @Test fun doesNotAnimateIfActivityIsAlreadyOpen() { - val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) - startIntentWithAnimation { ActivityManager.START_DELIVERED_TO_TOP } + kosmos.runTest { + val controller = createController() + val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) + startIntentWithAnimation(controller) { ActivityManager.START_DELIVERED_TO_TOP } - waitForIdleSync() - verify(controller).onIntentStarted(willAnimateCaptor.capture()) - assertFalse(willAnimateCaptor.value) + waitForIdleSync() + verify(controller).onIntentStarted(willAnimateCaptor.capture()) + assertFalse(willAnimateCaptor.value) + } } @Test fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() { - `when`(callback.isOnKeyguard()).thenReturn(true) + kosmos.runTest { + `when`(callback.isOnKeyguard()).thenReturn(true) - val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) - var animationAdapter: RemoteAnimationAdapter? = null + val controller = createController() + val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) + var animationAdapter: RemoteAnimationAdapter? = null - startIntentWithAnimation(underTest) { adapter -> - animationAdapter = adapter - ActivityManager.START_DELIVERED_TO_TOP - } + startIntentWithAnimation(controller, underTest) { adapter -> + animationAdapter = adapter + ActivityManager.START_DELIVERED_TO_TOP + } - waitForIdleSync() - verify(controller).onIntentStarted(willAnimateCaptor.capture()) - verify(callback).hideKeyguardWithAnimation(any()) + waitForIdleSync() + verify(controller).onIntentStarted(willAnimateCaptor.capture()) + verify(callback).hideKeyguardWithAnimation(any()) - assertTrue(willAnimateCaptor.value) - assertNull(animationAdapter) + assertTrue(willAnimateCaptor.value) + assertNull(animationAdapter) + } } @Test fun doesNotAnimateIfAnimateIsFalse() { - val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) - startIntentWithAnimation(animate = false) { ActivityManager.START_SUCCESS } + kosmos.runTest { + val controller = createController() + val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) + startIntentWithAnimation(controller, animate = false) { ActivityManager.START_SUCCESS } - waitForIdleSync() - verify(controller).onIntentStarted(willAnimateCaptor.capture()) - assertFalse(willAnimateCaptor.value) + waitForIdleSync() + verify(controller).onIntentStarted(willAnimateCaptor.capture()) + assertFalse(willAnimateCaptor.value) + } } - @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) + @EnableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY) @Test fun registersReturnIffCookieIsPresent() { - `when`(callback.isOnKeyguard()).thenReturn(false) + kosmos.runTest { + `when`(callback.isOnKeyguard()).thenReturn(false) - startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP } + val controller = createController() + startIntentWithAnimation(controller, underTest) { + ActivityManager.START_DELIVERED_TO_TOP + } - waitForIdleSync() - assertTrue(testShellTransitions.remotes.isEmpty()) - assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) + waitForIdleSync() + assertTrue(testShellTransitions.remotes.isEmpty()) + assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) - val controller = - object : DelegateTransitionAnimatorController(controller) { - override val transitionCookie - get() = ActivityTransitionAnimator.TransitionCookie("testCookie") - } + val controllerWithCookie = + object : DelegateTransitionAnimatorController(controller) { + override val transitionCookie + get() = ActivityTransitionAnimator.TransitionCookie("testCookie") + } - startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP } + startIntentWithAnimation(controllerWithCookie, underTest) { + ActivityManager.START_DELIVERED_TO_TOP + } - waitForIdleSync() - assertEquals(1, testShellTransitions.remotes.size) - assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) + waitForIdleSync() + assertEquals(1, testShellTransitions.remotes.size) + assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) + } } @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun registersLongLivedTransition() { kosmos.runTest { - var factory = controllerFactory() + val controller = createController() + var factory = controllerFactory(controller) underTest.register(factory.cookie, factory, testScope) assertEquals(2, testShellTransitions.remotes.size) - factory = controllerFactory() + factory = controllerFactory(controller) underTest.register(factory.cookie, factory, testScope) assertEquals(4, testShellTransitions.remotes.size) } } @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun registersLongLivedTransitionOverridingPreviousRegistration() { kosmos.runTest { + val controller = createController() val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie") - var factory = controllerFactory(cookie) + var factory = controllerFactory(controller, cookie) underTest.register(cookie, factory, testScope) val transitions = testShellTransitions.remotes.values.toList() - factory = controllerFactory(cookie) + factory = controllerFactory(controller, cookie) underTest.register(cookie, factory, testScope) assertEquals(2, testShellTransitions.remotes.size) for (transition in transitions) { @@ -250,23 +278,25 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } } - @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) + @DisableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() { kosmos.runTest { - val factory = controllerFactory(component = null) + val factory = controllerFactory(createController(), component = null) assertThrows(IllegalStateException::class.java) { underTest.register(factory.cookie, factory, testScope) } } } - @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) + @EnableFlags(SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() { kosmos.runTest { + val controller = createController() + // No ComponentName - var factory = controllerFactory(component = null) + var factory = controllerFactory(controller, component = null) assertThrows(IllegalStateException::class.java) { underTest.register(factory.cookie, factory, testScope) } @@ -280,7 +310,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { testTransitionAnimator, disableWmTimeout = true, ) - factory = controllerFactory() + factory = controllerFactory(controller) assertThrows(IllegalStateException::class.java) { activityTransitionAnimator.register(factory.cookie, factory, testScope) } @@ -288,17 +318,18 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun unregistersLongLivedTransition() { kosmos.runTest { + val controller = createController() val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3) for (index in 0 until 3) { cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java) - val factory = controllerFactory(cookies[index]!!) + val factory = controllerFactory(controller, cookies[index]!!) underTest.register(factory.cookie, factory, testScope) } @@ -315,75 +346,98 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun doesNotStartIfAnimationIsCancelled() { - val runner = underTest.createEphemeralRunner(controller) - runner.onAnimationCancelled() - runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback) + kosmos.runTest { + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + runner.onAnimationCancelled() + runner.onAnimationStart( + TRANSIT_NONE, + emptyArray(), + emptyArray(), + emptyArray(), + iCallback, + ) - waitForIdleSync() - verify(controller).onTransitionAnimationCancelled() - verify(controller, never()).onTransitionAnimationStart(anyBoolean()) - verify(listener).onTransitionAnimationCancelled() - verify(listener, never()).onTransitionAnimationStart() - assertNull(runner.delegate) + waitForIdleSync() + verify(controller).onTransitionAnimationCancelled() + verify(controller, never()).onTransitionAnimationStart(anyBoolean()) + verify(listener).onTransitionAnimationCancelled() + verify(listener, never()).onTransitionAnimationStart() + assertNull(runner.delegate) + } } @Test fun cancelsIfNoOpeningWindowIsFound() { - val runner = underTest.createEphemeralRunner(controller) - runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback) + kosmos.runTest { + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + runner.onAnimationStart( + TRANSIT_NONE, + emptyArray(), + emptyArray(), + emptyArray(), + iCallback, + ) - waitForIdleSync() - verify(controller).onTransitionAnimationCancelled() - verify(controller, never()).onTransitionAnimationStart(anyBoolean()) - verify(listener).onTransitionAnimationCancelled() - verify(listener, never()).onTransitionAnimationStart() - assertNull(runner.delegate) + waitForIdleSync() + verify(controller).onTransitionAnimationCancelled() + verify(controller, never()).onTransitionAnimationStart(anyBoolean()) + verify(listener).onTransitionAnimationCancelled() + verify(listener, never()).onTransitionAnimationStart() + assertNull(runner.delegate) + } } @Test fun startsAnimationIfWindowIsOpening() { - val runner = underTest.createEphemeralRunner(controller) - runner.onAnimationStart( - TRANSIT_NONE, - arrayOf(fakeWindow()), - emptyArray(), - emptyArray(), - iCallback, - ) - waitForIdleSync() - verify(listener).onTransitionAnimationStart() - verify(controller).onTransitionAnimationStart(anyBoolean()) + kosmos.runTest { + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + runner.onAnimationStart( + TRANSIT_NONE, + arrayOf(fakeWindow()), + emptyArray(), + emptyArray(), + iCallback, + ) + waitForIdleSync() + verify(listener).onTransitionAnimationStart() + verify(controller).onTransitionAnimationStart(anyBoolean()) + } } @Test fun creatingControllerFromNormalViewThrows() { - assertThrows(IllegalArgumentException::class.java) { - ActivityTransitionAnimator.Controller.fromView(FrameLayout(mContext)) + kosmos.runTest { + assertThrows(IllegalArgumentException::class.java) { + ActivityTransitionAnimator.Controller.fromView(FrameLayout(mContext)) + } } } @DisableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun creatingRunnerWithLazyInitializationThrows_whenTheFlagsAreDisabled() { kosmos.runTest { assertThrows(IllegalStateException::class.java) { - val factory = controllerFactory() + val factory = controllerFactory(createController()) underTest.createLongLivedRunner(factory, testScope, forLaunch = true) } } } @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun runnerCreatesDelegateLazily_onAnimationStart() { kosmos.runTest { - val factory = controllerFactory() + val factory = controllerFactory(createController()) val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = true) assertNull(runner.delegate) @@ -412,13 +466,13 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } @EnableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun runnerCreatesDelegateLazily_onAnimationTakeover() { kosmos.runTest { - val factory = controllerFactory() + val factory = controllerFactory(createController()) val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = false) assertNull(runner.delegate) @@ -446,58 +500,78 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } @DisableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun animationTakeoverThrows_whenTheFlagsAreDisabled() { - val runner = underTest.createEphemeralRunner(controller) - assertThrows(IllegalStateException::class.java) { - runner.takeOverAnimation( - arrayOf(fakeWindow()), - emptyArray(), - SurfaceControl.Transaction(), - iCallback, - ) + kosmos.runTest { + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + assertThrows(IllegalStateException::class.java) { + runner.takeOverAnimation( + arrayOf(fakeWindow()), + emptyArray(), + SurfaceControl.Transaction(), + iCallback, + ) + } } } @DisableFlags( - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, - Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + SharedFlags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, ) @Test fun disposeRunner_delegateDereferenced() { - val runner = underTest.createEphemeralRunner(controller) - assertNotNull(runner.delegate) - runner.dispose() - waitForIdleSync() - assertNull(runner.delegate) + kosmos.runTest { + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + assertNotNull(runner.delegate) + runner.dispose() + waitForIdleSync() + assertNull(runner.delegate) + } } @Test fun concurrentListenerModification_doesNotThrow() { - // Need a second listener to trigger the concurrent modification. - underTest.addListener(object : ActivityTransitionAnimator.Listener {}) - `when`(listener.onTransitionAnimationStart()).thenAnswer { - underTest.removeListener(listener) - listener - } + kosmos.runTest { + // Need a second listener to trigger the concurrent modification. + underTest.addListener(object : ActivityTransitionAnimator.Listener {}) + `when`(listener.onTransitionAnimationStart()).thenAnswer { + underTest.removeListener(listener) + listener + } - val runner = underTest.createEphemeralRunner(controller) - runner.onAnimationStart( - TRANSIT_NONE, - arrayOf(fakeWindow()), - emptyArray(), - emptyArray(), - iCallback, - ) + val controller = createController() + val runner = underTest.createEphemeralRunner(controller) + runner.onAnimationStart( + TRANSIT_NONE, + arrayOf(fakeWindow()), + emptyArray(), + emptyArray(), + iCallback, + ) + + waitForIdleSync() + verify(listener).onTransitionAnimationStart() + } + } + private fun createController(): TestTransitionAnimatorController { + lateinit var transitionContainer: ViewGroup + activityRule.scenario.onActivity { activity -> + transitionContainer = LinearLayout(activity) + activity.setContentView(transitionContainer) + } waitForIdleSync() - verify(listener).onTransitionAnimationStart() + return spy(TestTransitionAnimatorController(transitionContainer)) } private fun controllerFactory( + controller: ActivityTransitionAnimator.Controller, cookie: ActivityTransitionAnimator.TransitionCookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java), component: ComponentName? = mock(ComponentName::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt index 155059ea5ed9..e0118b18ff64 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt @@ -244,7 +244,7 @@ class LauncherProxyServiceTest : SysuiTestCase() { `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(true) `when`(userManager.isUserForeground()).thenReturn(true) val spyContext = spy(context) - val ops = createLauncherProxyService(spyContext) + val ops = assertLogsWtf { createLauncherProxyService(spyContext) }.result ops.startConnectionToCurrentUser() verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 51bb38fa5ba9..f72645eb8596 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -51,6 +51,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.log.assertLogsWtf import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootViewKeyEventHandler @@ -413,7 +414,9 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : // THEN move is ignored, down is handled, and window is notified assertThat(interactionEventHandler.handleDispatchTouchEvent(MOVE_EVENT)).isFalse() - assertThat(interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)).isTrue() + assertLogsWtf { + assertThat(interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)).isTrue() + } verify(notificationShadeWindowController).setLaunchingActivity(false) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 4315c0f638ac..84f39be2eeed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row; import static android.app.Flags.FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES; +import static com.android.systemui.log.LogAssertKt.assertRunnableLogsWtf; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.PKG; import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.USER_HANDLE; @@ -1147,7 +1148,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { public void hasStatusBarChipDuringHeadsUpAnimation_flagOff_false() throws Exception { final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setHasStatusBarChipDuringHeadsUpAnimation(true); + assertRunnableLogsWtf(() -> row.setHasStatusBarChipDuringHeadsUpAnimation(true)); assertThat(row.hasStatusBarChipDuringHeadsUpAnimation()).isFalse(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java index 1cadb3c0a909..e1bab8ec47e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java @@ -46,6 +46,7 @@ import androidx.test.filters.SmallTest; import com.android.server.notification.Flags; import com.android.systemui.SysuiTestCase; +import com.android.systemui.log.LogAssertKt; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; @@ -173,8 +174,10 @@ public class NotificationCustomContentMemoryVerifierTest extends SysuiTestCase { public void satisfiesMemoryLimits_viewWithoutCustomNotificationRoot_returnsTrue() { NotificationEntry entry = new NotificationEntryBuilder().build(); View view = new FrameLayout(mContext); - assertThat(NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) - .isTrue(); + LogAssertKt.assertRunnableLogsWtf(() -> { + assertThat(NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) + .isTrue(); + }); } @Test 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 8de931a7af40..0d7ce5353cd4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -2055,6 +2055,9 @@ public class BubblesTest extends SysuiTestCase { @Test public void testShowStackEdu_isConversationBubble() { + // TODO(b/401025577): Prevent this test from raising a WTF, and remove this exemption + mLogWtfRule.addFailureLogExemption(log-> log.getTag().equals("FloatingCoordinator")); + // Setup setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, false); BubbleEntry bubbleEntry = createBubbleEntry(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java index 252c70a61b86..e550e88b7bc7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -46,6 +46,7 @@ import androidx.test.uiautomator.UiDevice; import com.android.internal.protolog.ProtoLog; import com.android.systemui.broadcast.FakeBroadcastDispatcher; import com.android.systemui.flags.SceneContainerRule; +import com.android.systemui.log.LogWtfHandlerRule; import org.junit.After; import org.junit.AfterClass; @@ -127,6 +128,8 @@ public abstract class SysuiTestCase { @Rule public final SetFlagsRule mSetFlagsRule = isRobolectricTest() ? new SetFlagsRule() : mSetFlagsClassRule.createSetFlagsRule(); + @Rule public final LogWtfHandlerRule mLogWtfRule = new LogWtfHandlerRule(); + @Rule(order = 10) public final SceneContainerRule mSceneContainerRule = new SceneContainerRule(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt new file mode 100644 index 000000000000..130c2987912e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CarProjectionRepositoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.communal.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.carProjectionRepository by + Kosmos.Fixture<CarProjectionRepository> { FakeCarProjectionRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt new file mode 100644 index 000000000000..4042342923ea --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCarProjectionRepository.kt @@ -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.systemui.communal.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeCarProjectionRepository : CarProjectionRepository { + private val _projectionActive = MutableStateFlow(false) + override val projectionActive: Flow<Boolean> = _projectionActive.asStateFlow() + + override suspend fun isProjectionActive(): Boolean { + return _projectionActive.value + } + + fun setProjectionActive(active: Boolean) { + _projectionActive.value = active + } +} + +val CarProjectionRepository.fake + get() = this as FakeCarProjectionRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt new file mode 100644 index 000000000000..23bbe36203e6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CarProjectionInteractorKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.communal.domain.interactor + +import com.android.systemui.communal.data.repository.carProjectionRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.carProjectionInteractor by Fixture { CarProjectionInteractor(carProjectionRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt new file mode 100644 index 000000000000..5735cf82cca0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorKosmos.kt @@ -0,0 +1,35 @@ +/* + * 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.communal.domain.interactor + +import com.android.systemui.common.domain.interactor.batteryInteractor +import com.android.systemui.communal.posturing.domain.interactor.posturingInteractor +import com.android.systemui.dock.dockManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.backgroundCoroutineContext + +val Kosmos.communalAutoOpenInteractor by Fixture { + CommunalAutoOpenInteractor( + communalSettingsInteractor = communalSettingsInteractor, + backgroundContext = backgroundCoroutineContext, + batteryInteractor = batteryInteractor, + posturingInteractor = posturingInteractor, + dockManager = dockManager, + allowSwipeAlways = false, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index b8b2ec5a58ae..316fcbb85b26 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -16,17 +16,14 @@ package com.android.systemui.communal.domain.interactor -import android.content.pm.UserInfo import android.content.testableContext import android.os.userManager import com.android.systemui.broadcast.broadcastDispatcher -import com.android.systemui.common.domain.interactor.batteryInteractor +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.communalMediaRepository import com.android.systemui.communal.data.repository.communalSmartspaceRepository import com.android.systemui.communal.data.repository.communalWidgetRepository -import com.android.systemui.communal.posturing.domain.interactor.posturingInteractor import com.android.systemui.communal.widgets.EditWidgetsActivityStarter -import com.android.systemui.dock.dockManager import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -39,13 +36,9 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.activityStarter -import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.settings.userTracker import com.android.systemui.statusbar.phone.fakeManagedProfileController -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.data.repository.fakeUserRepository -import com.android.systemui.user.domain.interactor.userLockedInteractor import com.android.systemui.util.mockito.mock val Kosmos.communalInteractor by Fixture { @@ -70,10 +63,6 @@ val Kosmos.communalInteractor by Fixture { logBuffer = logcatLogBuffer("CommunalInteractor"), tableLogBuffer = mock(), managedProfileController = fakeManagedProfileController, - batteryInteractor = batteryInteractor, - dockManager = dockManager, - posturingInteractor = posturingInteractor, - userLockedInteractor = userLockedInteractor, ) } @@ -86,28 +75,28 @@ fun Kosmos.setCommunalV2ConfigEnabled(enabled: Boolean) { ) } -suspend fun Kosmos.setCommunalEnabled(enabled: Boolean): UserInfo { +fun Kosmos.setCommunalEnabled(enabled: Boolean) { fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, enabled) - return if (enabled) { - fakeUserRepository.asMainUser() - } else { - fakeUserRepository.asDefaultUser() - } + val suppressionReasons = + if (enabled) { + emptyList() + } else { + listOf(SuppressionReason.ReasonUnknown()) + } + communalSettingsInteractor.setSuppressionReasons(suppressionReasons) } -suspend fun Kosmos.setCommunalV2Enabled(enabled: Boolean): UserInfo { +fun Kosmos.setCommunalV2Enabled(enabled: Boolean) { setCommunalV2ConfigEnabled(enabled) return setCommunalEnabled(enabled) } -suspend fun Kosmos.setCommunalAvailable(available: Boolean): UserInfo { - val user = setCommunalEnabled(available) +fun Kosmos.setCommunalAvailable(available: Boolean) { + setCommunalEnabled(available) fakeKeyguardRepository.setKeyguardShowing(available) - fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, available) - return user } -suspend fun Kosmos.setCommunalV2Available(available: Boolean): UserInfo { +fun Kosmos.setCommunalV2Available(available: Boolean) { setCommunalV2ConfigEnabled(available) - return setCommunalAvailable(available) + setCommunalAvailable(available) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt index fb983f7c605f..d2fbb515e686 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractorKosmos.kt @@ -24,7 +24,6 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.settings.userTracker import com.android.systemui.user.domain.interactor.selectedUserInteractor -import com.android.systemui.util.mockito.mock val Kosmos.communalSettingsInteractor by Fixture { CommunalSettingsInteractor( @@ -34,6 +33,5 @@ val Kosmos.communalSettingsInteractor by Fixture { repository = communalSettingsRepository, userInteractor = selectedUserInteractor, userTracker = userTracker, - tableLogBuffer = mock(), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java deleted file mode 100644 index b99310bcbe38..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2018 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.dock; - -/** - * A rudimentary fake for DockManager. - */ -public class DockManagerFake implements DockManager { - DockEventListener mCallback; - AlignmentStateListener mAlignmentListener; - private boolean mDocked; - - @Override - public void addListener(DockEventListener callback) { - this.mCallback = callback; - } - - @Override - public void removeListener(DockEventListener callback) { - this.mCallback = null; - } - - @Override - public void addAlignmentStateListener(AlignmentStateListener listener) { - mAlignmentListener = listener; - } - - @Override - public void removeAlignmentStateListener(AlignmentStateListener listener) { - mAlignmentListener = listener; - } - - @Override - public boolean isDocked() { - return mDocked; - } - - /** Sets the docked state */ - public void setIsDocked(boolean docked) { - mDocked = docked; - } - - @Override - public boolean isHidden() { - return false; - } - - /** Notifies callbacks of dock state change */ - public void setDockEvent(int event) { - mCallback.onEvent(event); - } -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt new file mode 100644 index 000000000000..6a43c40612a7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dock/DockManagerFake.kt @@ -0,0 +1,61 @@ +/* + * 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.dock + +import com.android.systemui.dock.DockManager.AlignmentStateListener + +/** A rudimentary fake for DockManager. */ +class DockManagerFake : DockManager { + private val callbacks = mutableSetOf<DockManager.DockEventListener>() + private val alignmentListeners = mutableSetOf<AlignmentStateListener>() + private var docked = false + + override fun addListener(callback: DockManager.DockEventListener) { + callbacks.add(callback) + } + + override fun removeListener(callback: DockManager.DockEventListener) { + callbacks.remove(callback) + } + + override fun addAlignmentStateListener(listener: AlignmentStateListener) { + alignmentListeners.add(listener) + } + + override fun removeAlignmentStateListener(listener: AlignmentStateListener) { + alignmentListeners.remove(listener) + } + + override fun isDocked(): Boolean { + return docked + } + + /** Sets the docked state */ + fun setIsDocked(docked: Boolean) { + this.docked = docked + } + + override fun isHidden(): Boolean { + return false + } + + /** Notifies callbacks of dock state change */ + fun setDockEvent(event: Int) { + for (callback in callbacks) { + callback.onEvent(event) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt index bdfa875f5429..9b0a9830dcc4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.keyguard.domain.interactor -import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.deviceentry.data.repository.deviceEntryRepository @@ -43,6 +42,5 @@ val Kosmos.fromAodTransitionInteractor by wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, communalSettingsInteractor = communalSettingsInteractor, communalSceneInteractor = communalSceneInteractor, - communalInteractor = communalInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt index 985044c80f18..511bede7349b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.keyguard.domain.interactor -import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository @@ -42,7 +41,6 @@ var Kosmos.fromLockscreenTransitionInteractor by communalSettingsInteractor = communalSettingsInteractor, swipeToDismissInteractor = swipeToDismissInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, - communalInteractor = communalInteractor, communalSceneInteractor = communalSceneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt index b41ceff5f581..a42f2025cdaa 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt @@ -18,7 +18,6 @@ package com.android.systemui.log import android.util.Log import android.util.Log.TerribleFailureHandler import com.google.common.truth.Truth.assertWithMessage -import java.util.concurrent.Callable /** Asserts that [notLoggingBlock] does not make a call to [Log.wtf] */ fun <T> assertDoesNotLogWtf( @@ -65,15 +64,6 @@ fun <T> assertLogsWtf( return WtfBlockResult(caught, result) } -/** Assert that [loggingCallable] makes a call to [Log.wtf] */ -@JvmOverloads -fun <T> assertLogsWtf( - message: String = "Expected Log.wtf to be called", - allowMultiple: Boolean = false, - loggingCallable: Callable<T>, -): WtfBlockResult<T> = - assertLogsWtf(message = message, allowMultiple = allowMultiple, loggingCallable::call) - /** Assert that [loggingBlock] makes at least one call to [Log.wtf] */ @JvmOverloads fun <T> assertLogsWtfs( @@ -81,13 +71,6 @@ fun <T> assertLogsWtfs( loggingBlock: () -> T, ): WtfBlockResult<T> = assertLogsWtf(message, allowMultiple = true, loggingBlock) -/** Assert that [loggingCallable] makes at least one call to [Log.wtf] */ -@JvmOverloads -fun <T> assertLogsWtfs( - message: String = "Expected Log.wtf to be called once or more", - loggingCallable: Callable<T>, -): WtfBlockResult<T> = assertLogsWtf(message, allowMultiple = true, loggingCallable) - /** The data passed to [TerribleFailureHandler.onTerribleFailure] */ data class TerribleFailureLog( val tag: String, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt index 933c351679a4..6bb908a6ef07 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt @@ -22,5 +22,9 @@ import com.android.systemui.user.data.repository.userRepository val Kosmos.userLockedInteractor by Kosmos.Fixture { - UserLockedInteractor(backgroundDispatcher = testDispatcher, userRepository = userRepository) + UserLockedInteractor( + backgroundDispatcher = testDispatcher, + userRepository = userRepository, + selectedUserInteractor = selectedUserInteractor, + ) } diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java index 02987a98417f..15f186b047f2 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java @@ -132,8 +132,8 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { @Nullable EditorInfo editorInfo, IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq, - boolean useAsyncShowHideMethod); + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + int startInputSeq, boolean useAsyncShowHideMethod); InputBindResult startInputOrWindowGainedFocus( @StartInputReason int startInputReason, IInputMethodClient client, @@ -142,7 +142,7 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { @Nullable EditorInfo editorInfo, IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher); + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible); void showInputMethodPickerFromClient(IInputMethodClient client, int auxiliarySubtypeMode); @@ -324,11 +324,11 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) { return mCallback.startInputOrWindowGainedFocus( startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, - unverifiedTargetSdkVersion, userId, imeDispatcher); + unverifiedTargetSdkVersion, userId, imeDispatcher, imeRequestedVisible); } @Override @@ -340,13 +340,13 @@ final class IInputMethodManagerImpl extends IInputMethodManager.Stub { IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq, - boolean useAsyncShowHideMethod) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + int startInputSeq, boolean useAsyncShowHideMethod) { mCallback.startInputOrWindowGainedFocusAsync( startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, - unverifiedTargetSdkVersion, userId, imeDispatcher, startInputSeq, - useAsyncShowHideMethod); + unverifiedTargetSdkVersion, userId, imeDispatcher, imeRequestedVisible, + startInputSeq, useAsyncShowHideMethod); } @Override diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java index 69353becc692..2c07a3179344 100644 --- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -443,7 +443,8 @@ public final class ImeVisibilityStateComputer { } @GuardedBy("ImfLock.class") - ImeVisibilityResult computeState(ImeTargetWindowState state, boolean allowVisible) { + ImeVisibilityResult computeState(ImeTargetWindowState state, boolean allowVisible, + boolean imeRequestedVisible) { // TODO: Output the request IME visibility state according to the requested window state final int softInputVisibility = state.mSoftInputModeState & SOFT_INPUT_MASK_STATE; // Should we auto-show the IME even if the caller has not @@ -576,7 +577,8 @@ public final class ImeVisibilityStateComputer { SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR); } } - if (!state.hasEditorFocused() && mInputShown && state.isStartInputByGainFocus() + if (!state.hasEditorFocused() && (mInputShown || (Flags.refactorInsetsController() + && imeRequestedVisible)) && state.isStartInputByGainFocus() && mService.mInputMethodDeviceConfigs.shouldHideImeWhenNoEditorFocus()) { // Hide the soft-keyboard when the system do nothing for softInputModeState // of the window being gained focus without an editor. This behavior benefits diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 68ad8f7e9433..23757757e336 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -3725,8 +3725,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq, - boolean useAsyncShowHideMethod) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + int startInputSeq, boolean useAsyncShowHideMethod) { // implemented by ZeroJankProxy } @@ -3739,7 +3739,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) { if (UserHandle.getCallingUserId() != userId) { mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); @@ -3870,7 +3870,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. result = startInputOrWindowGainedFocusInternalLocked(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, - unverifiedTargetSdkVersion, bindingController, imeDispatcher, cs); + unverifiedTargetSdkVersion, bindingController, imeDispatcher, cs, + imeRequestedVisible); } finally { Binder.restoreCallingIdentity(ident); } @@ -3899,7 +3900,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. IRemoteInputConnection inputContext, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @NonNull InputMethodBindingController bindingController, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, @NonNull ClientState cs, + boolean imeRequestedVisible) { ProtoLog.v(IMMS_DEBUG, "startInputOrWindowGainedFocusInternalLocked: reason=%s" + " client=%s" + " inputContext=%s" @@ -3910,12 +3912,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. + " unverifiedTargetSdkVersion=%s" + " bindingController=%s" + " imeDispatcher=%s" - + " cs=%s", + + " cs=%s" + + " imeRequestedVisible=%s", InputMethodDebug.startInputReasonToString(startInputReason), client.asBinder(), inputContext, editorInfo, InputMethodDebug.startInputFlagsToString(startInputFlags), InputMethodDebug.softInputModeToString(softInputMode), Integer.toHexString(windowFlags), unverifiedTargetSdkVersion, bindingController, - imeDispatcher, cs); + imeDispatcher, cs, imeRequestedVisible); final int userId = bindingController.getUserId(); final var userData = getUserData(userId); @@ -3963,7 +3966,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. InputBindResult res = null; final ImeVisibilityResult imeVisRes = visibilityStateComputer.computeState(windowState, - isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)); + isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags), + imeRequestedVisible); if (imeVisRes != null) { boolean isShow = false; switch (imeVisRes.getReason()) { diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index 72529254545e..12c1d9cbb2a1 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -234,15 +234,15 @@ final class ZeroJankProxy implements IInputMethodManagerImpl.Callback { IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq, - boolean useAsyncShowHideMethod) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible, + int startInputSeq, boolean useAsyncShowHideMethod) { offload(() -> { InputBindResult result = mInner.startInputOrWindowGainedFocus(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, - userId, imeDispatcher); + userId, imeDispatcher, imeRequestedVisible); sendOnStartInputResult(client, result, startInputSeq); // For first-time client bind, MSG_BIND should arrive after MSG_START_INPUT_RESULT. if (result.result == InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION) { @@ -269,7 +269,7 @@ final class ZeroJankProxy implements IInputMethodManagerImpl.Callback { IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, boolean imeRequestedVisible) { // Should never be called when flag is enabled i.e. when this proxy is used. return null; } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 353ccd5836c8..42b63d125d6b 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -7104,6 +7104,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp public void setAnimatingTypes(@InsetsType int animatingTypes) { if (mAnimatingTypes != animatingTypes) { mAnimatingTypes = animatingTypes; + + if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { + getInsetsStateController().onAnimatingTypesChanged(this); + } } } } diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 1c90f40085db..040bbe46c3aa 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -332,8 +332,7 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { if (changed) { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_UPDATE_CLIENT_VISIBILITY); - invokeOnImeRequestedChangedListener(mDisplayContent.getImeInputTarget(), - statsToken); + invokeOnImeRequestedChangedListener(controlTarget, statsToken); } else { // TODO(b/353463205) check cancelled / failed ImeTracker.forLogging().onCancelled(statsToken, @@ -387,7 +386,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { // not all virtual displays have an ImeInsetsSourceProvider, so it is not // guaranteed that the IME will be started when the control target reports its // requested visibility back. Thus, invoking the listener here. - invokeOnImeRequestedChangedListener(imeInsetsTarget, statsToken); + invokeOnImeRequestedChangedListener((InsetsControlTarget) imeInsetsTarget, + statsToken); } else { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY); @@ -396,18 +396,21 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { } // TODO(b/353463205) check callers to see if we can make statsToken @NonNull - private void invokeOnImeRequestedChangedListener(InsetsTarget insetsTarget, + private void invokeOnImeRequestedChangedListener(InsetsControlTarget controlTarget, @Nullable ImeTracker.Token statsToken) { final var imeListener = mDisplayContent.mWmService.mOnImeRequestedChangedListener; if (imeListener != null) { - if (insetsTarget != null) { + if (controlTarget != null) { + final boolean imeAnimating = Flags.reportAnimatingInsetsTypes() + && (controlTarget.getAnimatingTypes() & WindowInsets.Type.ime()) != 0; ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_POSTING_CHANGED_IME_VISIBILITY); mDisplayContent.mWmService.mH.post(() -> { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_WM_INVOKING_IME_REQUESTED_LISTENER); - imeListener.onImeRequestedChanged(insetsTarget.getWindowToken(), - insetsTarget.isRequestedVisible(WindowInsets.Type.ime()), statsToken); + imeListener.onImeRequestedChanged(controlTarget.getWindowToken(), + controlTarget.isRequestedVisible(WindowInsets.Type.ime()) + || imeAnimating, statsToken); }); } else { ImeTracker.forLogging().onFailed(statsToken, @@ -420,6 +423,21 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { } } + @Override + void onAnimatingTypesChanged(InsetsControlTarget caller) { + if (Flags.reportAnimatingInsetsTypes()) { + final InsetsControlTarget controlTarget = getControlTarget(); + // If the IME is not being requested anymore and the animation is finished, we need to + // invoke the listener, to let IMS eventually know + if (caller != null && caller == controlTarget && !caller.isRequestedVisible( + WindowInsets.Type.ime()) + && (caller.getAnimatingTypes() & WindowInsets.Type.ime()) == 0) { + // TODO(b/353463205) check statsToken + invokeOnImeRequestedChangedListener(caller, null); + } + } + } + private void reportImeDrawnForOrganizerIfNeeded(@NonNull InsetsControlTarget caller) { final WindowState callerWindow = caller.getWindow(); if (callerWindow == null) { diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index b7489029768a..1b693fc05b21 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -673,6 +673,9 @@ class InsetsSourceProvider { mServerVisible, mClientVisible); } + void onAnimatingTypesChanged(InsetsControlTarget caller) { + } + protected boolean isLeashReadyForDispatching() { return isLeashInitialized(); } diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 5e0395f70e65..810e48f492e1 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -393,6 +393,13 @@ class InsetsStateController { } } + void onAnimatingTypesChanged(InsetsControlTarget target) { + for (int i = mProviders.size() - 1; i >= 0; i--) { + final InsetsSourceProvider provider = mProviders.valueAt(i); + provider.onAnimatingTypesChanged(target); + } + } + private void notifyPendingInsetsControlChanged() { if (mPendingTargetProvidersMap.isEmpty()) { return; diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 1022d18ac0e9..ce91fc5baba1 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -862,6 +862,12 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mWmService.scheduleAnimationLocked(); mAnimatingTypes = animatingTypes; + + if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) { + final InsetsStateController insetsStateController = + getDisplayContent().getInsetsStateController(); + insetsStateController.onAnimatingTypesChanged(this); + } } } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java index 05615f68427d..2339a940e2d0 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java @@ -287,6 +287,7 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */, mTargetSdkVersion /* unverifiedTargetSdkVersion */, mUserId /* userId */, - mMockImeOnBackInvokedDispatcher /* imeDispatcher */); + mMockImeOnBackInvokedDispatcher /* imeDispatcher */, + true /* imeRequestedVisible */); } } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java index 70eeae648dd0..aa779197f301 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java @@ -267,7 +267,8 @@ public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTes // visibility state will be preserved to the current window state. final ImeTargetWindowState stateWithUnChangedFlag = initImeTargetWindowState( mWindowToken); - mComputer.computeState(stateWithUnChangedFlag, true /* allowVisible */); + mComputer.computeState(stateWithUnChangedFlag, true /* allowVisible */, + true /* imeRequestedVisible */); assertThat(stateWithUnChangedFlag.isRequestedImeVisible()).isEqualTo( lastState.isRequestedImeVisible()); } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java index 11abc9469c82..b81b570389da 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java @@ -320,7 +320,8 @@ public class InputMethodManagerServiceWindowGainedFocusTest mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */, mTargetSdkVersion /* unverifiedTargetSdkVersion */, mUserId /* userId */, - mMockImeOnBackInvokedDispatcher /* imeDispatcher */); + mMockImeOnBackInvokedDispatcher /* imeDispatcher */, + true /* imeRequestedVisible */); } @Test |