diff options
9 files changed, 496 insertions, 11 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 2f4d77baae97..584f2721772a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -75,6 +75,7 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator; import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler; +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; @@ -141,7 +142,7 @@ import java.util.Optional; includes = { WMShellBaseModule.class, PipModule.class, - ShellBackAnimationModule.class, + ShellBackAnimationModule.class }) public abstract class WMShellModule { @@ -247,6 +248,7 @@ public abstract class WMShellModule { AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler, WindowDecorViewHostSupplier windowDecorViewHostSupplier) { if (DesktopModeStatus.canEnterDesktopMode(context)) { @@ -272,6 +274,7 @@ public abstract class WMShellModule { assistContentRequester, multiInstanceHelper, desktopTasksLimiter, + windowDecorCaptionHandleRepository, desktopActivityOrientationHandler, windowDecorViewHostSupplier); } @@ -780,6 +783,12 @@ public abstract class WMShellModule { @WMSingleton @Provides + static WindowDecorCaptionHandleRepository provideAppHandleRepository() { + return new WindowDecorCaptionHandleRepository(); + } + + @WMSingleton + @Provides static AppHandleEducationController provideAppHandleEducationController( AppHandleEducationFilter appHandleEducationFilter, ShellTaskOrganizer shellTaskOrganizer, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt new file mode 100644 index 000000000000..7ae537088832 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt @@ -0,0 +1,58 @@ +/* + * 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.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Rect +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** Repository to observe caption state. */ +class WindowDecorCaptionHandleRepository { + private val _captionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption) + /** Observer for app handle state changes. */ + val captionStateFlow: StateFlow<CaptionState> = _captionStateFlow + + /** Notifies [captionStateFlow] if there is a change to caption state. */ + fun notifyCaptionChanged(captionState: CaptionState) { + _captionStateFlow.value = captionState + } +} + +/** + * Represents the current status of the caption. + * + * It can be one of three options: + * * [AppHandle]: Indicating that there is at least one visible app handle on the screen. + * * [AppHeader]: Indicating that there is at least one visible app chip on the screen. + * * [NoCaption]: Signifying that no caption handle is currently visible on the device. + */ +sealed class CaptionState { + data class AppHandle( + val runningTaskInfo: RunningTaskInfo, + val isHandleMenuExpanded: Boolean, + val globalAppHandleBounds: Rect + ) : CaptionState() + + data class AppHeader( + val runningTaskInfo: RunningTaskInfo, + val isHeaderMenuExpanded: Boolean, + val globalAppChipBounds: Rect + ) : CaptionState() + + data object NoCaption : CaptionState() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index caac2f6bb03e..7ea0bd68f732 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -109,6 +109,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; @@ -164,7 +165,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final InputManager mInputManager; private final InteractionJankMonitor mInteractionJankMonitor; private final MultiInstanceHelper mMultiInstanceHelper; + private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter; + private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private final WindowDecorViewHostSupplier mWindowDecorViewHostSupplier; private boolean mTransitionDragActive; @@ -234,6 +237,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, WindowDecorViewHostSupplier windowDecorViewHostSupplier) { this( @@ -259,10 +263,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new DesktopModeWindowDecoration.Factory(), new InputMonitorFactory(), SurfaceControl.Transaction::new, + new AppHeaderViewHolder.Factory(), rootTaskDisplayAreaOrganizer, new SparseArray<>(), interactionJankMonitor, desktopTasksLimiter, + windowDecorCaptionHandleRepository, activityOrientationChangeHandler, new TaskPositionerFactory()); } @@ -291,10 +297,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory, InputMonitorFactory inputMonitorFactory, Supplier<SurfaceControl.Transaction> transactionFactory, + AppHeaderViewHolder.Factory appHeaderViewHolderFactory, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId, InteractionJankMonitor interactionJankMonitor, Optional<DesktopTasksLimiter> desktopTasksLimiter, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, TaskPositionerFactory taskPositionerFactory) { mContext = context; @@ -317,6 +325,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory; mInputMonitorFactory = inputMonitorFactory; mTransactionFactory = transactionFactory; + mAppHeaderViewHolderFactory = appHeaderViewHolderFactory; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mGenericLinksParser = genericLinksParser; mInputManager = mContext.getSystemService(InputManager.class); @@ -325,6 +334,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { com.android.internal.R.string.config_systemUi); mInteractionJankMonitor = interactionJankMonitor; mDesktopTasksLimiter = desktopTasksLimiter; + mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; mActivityOrientationChangeHandler = activityOrientationChangeHandler; mAssistContentRequester = assistContentRequester; mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> { @@ -1377,10 +1387,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mBgExecutor, mMainChoreographer, mSyncQueue, + mAppHeaderViewHolderFactory, mRootTaskDisplayAreaOrganizer, mGenericLinksParser, mAssistContentRequester, mMultiInstanceHelper, + mWindowDecorCaptionHandleRepository, mWindowDecorViewHostSupplier); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 16036bee75b3..b3cf1ba50279 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -28,6 +28,7 @@ import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_ import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS; import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT; +import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode; import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; @@ -85,6 +86,8 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.CaptionState; +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.desktopmode.DesktopModeFlags; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; @@ -165,6 +168,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private ExclusionRegionListener mExclusionRegionListener; + private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final MaximizeMenuFactory mMaximizeMenuFactory; private final HandleMenuFactory mHandleMenuFactory; @@ -181,6 +185,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu; private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired; private final MultiInstanceHelper mMultiInstanceHelper; + private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; DesktopModeWindowDecoration( Context context, @@ -194,20 +199,24 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, + AppHeaderViewHolder.Factory appHeaderViewHolderFactory, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, AppToWebGenericLinksParser genericLinksParser, AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, WindowDecorViewHostSupplier windowDecorViewHostSupplier) { this (context, userContext, displayController, splitScreenController, taskOrganizer, taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue, - rootTaskDisplayAreaOrganizer, genericLinksParser, assistContentRequester, + appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser, + assistContentRequester, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper( context.getSystemService(WindowManager.class)), new SurfaceControlViewHostFactory() {}, windowDecorViewHostSupplier, DefaultMaximizeMenuFactory.INSTANCE, - DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper); + DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper, + windowDecorCaptionHandleRepository); } DesktopModeWindowDecoration( @@ -222,6 +231,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, + AppHeaderViewHolder.Factory appHeaderViewHolderFactory, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, AppToWebGenericLinksParser genericLinksParser, AssistContentRequester assistContentRequester, @@ -234,7 +244,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin WindowDecorViewHostSupplier windowDecorViewHostSupplier, MaximizeMenuFactory maximizeMenuFactory, HandleMenuFactory handleMenuFactory, - MultiInstanceHelper multiInstanceHelper) { + MultiInstanceHelper multiInstanceHelper, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) { super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, @@ -244,6 +255,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mBgExecutor = bgExecutor; mChoreographer = choreographer; mSyncQueue = syncQueue; + mAppHeaderViewHolderFactory = appHeaderViewHolderFactory; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mGenericLinksParser = genericLinksParser; mAssistContentRequester = assistContentRequester; @@ -251,6 +263,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mHandleMenuFactory = handleMenuFactory; mMultiInstanceHelper = multiInstanceHelper; mWindowManagerWrapper = windowManagerWrapper; + mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; } /** @@ -383,6 +396,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. // Nothing is set up in this case including the decoration surface. + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyNoCaptionHandle(); + } disposeStatusBarInputLayer(); Trace.endSection(); // DesktopModeWindowDecoration#relayout return; @@ -398,6 +414,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin position.set(determineHandlePosition()); } Trace.beginSection("DesktopModeWindowDecoration#relayout-bindData"); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyCaptionStateChanged(); + } mWindowDecorViewHolder.bindData(mTaskInfo, position, mResult.mCaptionWidth, @@ -507,6 +526,67 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return taskInfo.isFreeform() && taskInfo.isResizeable; } + private void notifyCaptionStateChanged() { + // TODO: b/366159408 - Ensure bounds sent with notification account for RTL mode. + if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) { + return; + } + if (!isCaptionVisible()) { + notifyNoCaptionHandle(); + } else if (isAppHandle(mWindowDecorViewHolder)) { + // App handle is visible since `mWindowDecorViewHolder` is of type + // [AppHandleViewHolder]. + final CaptionState captionState = new CaptionState.AppHandle(mTaskInfo, + isHandleMenuActive(), getCurrentAppHandleBounds()); + mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState); + } else { + // App header is visible since `mWindowDecorViewHolder` is of type + // [AppHeaderViewHolder]. + ((AppHeaderViewHolder) mWindowDecorViewHolder).runOnAppChipGlobalLayout( + () -> { + notifyAppChipStateChanged(); + return Unit.INSTANCE; + }); + } + } + + private void notifyNoCaptionHandle() { + if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) { + return; + } + mWindowDecorCaptionHandleRepository.notifyCaptionChanged( + CaptionState.NoCaption.INSTANCE); + } + + private Rect getCurrentAppHandleBounds() { + return new Rect( + mResult.mCaptionX, + /* top= */0, + mResult.mCaptionX + mResult.mCaptionWidth, + mResult.mCaptionHeight); + } + + private void notifyAppChipStateChanged() { + final Rect appChipPositionInWindow = + ((AppHeaderViewHolder) mWindowDecorViewHolder).getAppChipLocationInWindow(); + final Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); + final Rect appChipGlobalPosition = new Rect( + taskBounds.left + appChipPositionInWindow.left, + taskBounds.top + appChipPositionInWindow.top, + taskBounds.left + appChipPositionInWindow.right, + taskBounds.top + appChipPositionInWindow.bottom); + final CaptionState captionState = new CaptionState.AppHeader( + mTaskInfo, + isHandleMenuActive(), + appChipGlobalPosition); + + mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState); + } + + private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) { + return taskInfo.isFreeform() && taskInfo.isResizeable; + } + private void updateMaximizeMenu(SurfaceControl.Transaction startT) { if (!isDragResizable(mTaskInfo, mContext) || !isMaximizeMenuActive()) { return; @@ -556,7 +636,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } else if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_header) { loadAppInfoIfNeeded(); - return new AppHeaderViewHolder( + return mAppHeaderViewHolderFactory.create( mResult.mRootView, mOnCaptionTouchListener, mOnCaptionButtonClickListener, @@ -994,7 +1074,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mAppIconBitmap, mAppName, mSplitScreenController, - DesktopModeStatus.canEnterDesktopMode(mContext), + canEnterDesktopMode(mContext), supportsMultiInstance, shouldShowManageWindowsButton, getBrowserLink(), @@ -1027,6 +1107,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return Unit.INSTANCE; } ); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyCaptionStateChanged(); + } mMinimumInstancesFound = false; } @@ -1089,6 +1172,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mWindowDecorViewHolder.onHandleMenuClosed(); mHandleMenu.close(); mHandleMenu = null; + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyCaptionStateChanged(); + } } @Override @@ -1260,6 +1346,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); disposeStatusBarInputLayer(); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyNoCaptionHandle(); + } + super.close(); } @@ -1367,10 +1457,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, + AppHeaderViewHolder.Factory appHeaderViewHolderFactory, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, AppToWebGenericLinksParser genericLinksParser, AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, WindowDecorViewHostSupplier windowDecorViewHostSupplier) { return new DesktopModeWindowDecoration( context, @@ -1384,10 +1476,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin bgExecutor, choreographer, syncQueue, + appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser, assistContentRequester, multiInstanceHelper, + windowDecorCaptionHandleRepository, windowDecorViewHostSupplier); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 033d69583725..4a8cabca98cf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -22,12 +22,14 @@ import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Color import android.graphics.Point +import android.graphics.Rect import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.RoundRectShape import android.view.View import android.view.View.OnLongClickListener +import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView @@ -62,7 +64,7 @@ import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppeara * finer controls such as a close window button and an "app info" section to pull up additional * controls. */ -internal class AppHeaderViewHolder( +class AppHeaderViewHolder( rootView: View, onCaptionTouchListener: View.OnTouchListener, onCaptionButtonClickListener: View.OnClickListener, @@ -279,6 +281,34 @@ internal class AppHeaderViewHolder( maximizeButtonView.startHoverAnimation() } + fun runOnAppChipGlobalLayout(runnable: () -> Unit) { + if (openMenuButton.isAttachedToWindow) { + // App chip is already inflated. + runnable() + return + } + // Wait for app chip to be inflated before notifying repository. + openMenuButton.viewTreeObserver.addOnGlobalLayoutListener(object : + OnGlobalLayoutListener { + override fun onGlobalLayout() { + runnable() + openMenuButton.viewTreeObserver.removeOnGlobalLayoutListener(this) + } + }) + } + + fun getAppChipLocationInWindow(): Rect { + val appChipBoundsInWindow = IntArray(2) + openMenuButton.getLocationInWindow(appChipBoundsInWindow) + + return Rect( + /* left = */ appChipBoundsInWindow[0], + /* top = */ appChipBoundsInWindow[1], + /* right = */ appChipBoundsInWindow[0] + openMenuButton.width, + /* bottom = */ appChipBoundsInWindow[1] + openMenuButton.height + ) + } + private fun getHeaderStyle(header: Header): HeaderStyle { return HeaderStyle( background = getHeaderBackground(header), @@ -529,4 +559,26 @@ internal class AppHeaderViewHolder( private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65% private const val FOCUSED_OPACITY = 255 } + + class Factory { + fun create( + rootView: View, + onCaptionTouchListener: View.OnTouchListener, + onCaptionButtonClickListener: View.OnClickListener, + onLongClickListener: OnLongClickListener, + onCaptionGenericMotionListener: View.OnGenericMotionListener, + appName: CharSequence, + appIconBitmap: Bitmap, + onMaximizeHoverAnimationFinishedListener: () -> Unit, + ): AppHeaderViewHolder = AppHeaderViewHolder( + rootView, + onCaptionTouchListener, + onCaptionButtonClickListener, + onLongClickListener, + onCaptionGenericMotionListener, + appName, + appIconBitmap, + onMaximizeHoverAnimationFinishedListener, + ) + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt index 2341b099699f..5ea55b367703 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt @@ -24,7 +24,7 @@ import android.view.View * Encapsulates the root [View] of a window decoration and its children to facilitate looking up * children (via findViewById) and updating to the latest data from [RunningTaskInfo]. */ -internal abstract class WindowDecorationViewHolder(rootView: View) { +abstract class WindowDecorationViewHolder(rootView: View) { val context: Context = rootView.context /** diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt new file mode 100644 index 000000000000..e3caf2ede99d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt @@ -0,0 +1,91 @@ +/* + * 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.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class WindowDecorCaptionHandleRepositoryTest { + private lateinit var captionHandleRepository: WindowDecorCaptionHandleRepository + + @Before + fun setUp() { + captionHandleRepository = WindowDecorCaptionHandleRepository() + } + + @Test + fun initialState_noAction_returnsNoCaption() { + // Check the initial value of `captionStateFlow`. + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption) + } + + @Test + fun notifyCaptionChange_toAppHandleVisible_updatesStateWithCorrectData() { + val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME) + val appHandleCaptionState = + CaptionState.AppHandle( + taskInfo, false, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3)) + + captionHandleRepository.notifyCaptionChanged(appHandleCaptionState) + + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHandleCaptionState) + } + + @Test + fun notifyCaptionChange_toAppChipVisible_updatesStateWithCorrectData() { + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME) + val appHeaderCaptionState = + CaptionState.AppHeader( + taskInfo, true, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3)) + + captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState) + + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHeaderCaptionState) + } + + @Test + fun notifyCaptionChange_toNoCaption_updatesState() { + captionHandleRepository.notifyCaptionChanged(CaptionState.NoCaption) + + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption) + } + + private fun createTaskInfo( + deviceWindowingMode: Int = WINDOWING_MODE_UNDEFINED, + runningTaskPackageName: String = LAUNCHER_PACKAGE_NAME + ): RunningTaskInfo = + RunningTaskInfo().apply { + configuration.windowConfiguration.apply { windowingMode = deviceWindowingMode } + topActivityInfo?.apply { packageName = runningTaskPackageName } + } + + private companion object { + const val GMAIL_PACKAGE_NAME = "com.google.android.gm" + const val LAUNCHER_PACKAGE_NAME = "com.google.android.apps.nexuslauncher" + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 85bc7cc287e6..ee2a41c322c9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -88,6 +88,7 @@ import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTasksLimiter +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource @@ -98,6 +99,7 @@ import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener +import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier import java.util.Optional import java.util.function.Consumer @@ -164,6 +166,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { DesktopModeWindowDecorViewModel.InputMonitorFactory @Mock private lateinit var mockShellController: ShellController @Mock private lateinit var mockShellExecutor: ShellExecutor + @Mock private lateinit var mockAppHeaderViewHolderFactory: AppHeaderViewHolder.Factory @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler @Mock private lateinit var mockWindowManager: IWindowManager @@ -182,6 +185,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockTaskPositionerFactory: DesktopModeWindowDecorViewModel.TaskPositionerFactory @Mock private lateinit var mockTaskPositioner: TaskPositioner + @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository @Mock private lateinit var mockWindowDecorViewHostSupplier: WindowDecorViewHostSupplier<*> private lateinit var spyContext: TestableContext @@ -236,10 +240,12 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockDesktopModeWindowDecorFactory, mockInputMonitorFactory, transactionFactory, + mockAppHeaderViewHolderFactory, mockRootTaskDisplayAreaOrganizer, windowDecorByTaskIdSpy, mockInteractionJankMonitor, Optional.of(mockTasksLimiter), + mockCaptionHandleRepository, Optional.of(mockActivityOrientationChangeHandler), mockTaskPositionerFactory ) @@ -1211,7 +1217,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { whenever( mockDesktopModeWindowDecorFactory.create( any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), - any(), any(), any(), any(), any()) + any(), any(), any(), any(), any(), any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.isFocused).thenReturn(task.isFocused) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index dff42dae16a2..a1867f3698fc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -19,9 +19,11 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; +import static android.view.WindowInsets.Type.statusBars; import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; @@ -38,6 +40,7 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -55,6 +58,7 @@ import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.PointF; +import android.graphics.Rect; import android.net.Uri; import android.os.Handler; import android.os.SystemProperties; @@ -68,11 +72,13 @@ import android.view.AttachedSurfaceControl; import android.view.Choreographer; import android.view.Display; import android.view.GestureDetector; +import android.view.InsetsSource; import android.view.InsetsState; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; import android.window.WindowContainerTransaction; @@ -94,9 +100,12 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.CaptionState; +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; +import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; @@ -153,6 +162,10 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private SyncTransactionQueue mMockSyncQueue; @Mock + private AppHeaderViewHolder.Factory mMockAppHeaderViewHolderFactory; + @Mock + private AppHeaderViewHolder mMockAppHeaderViewHolder; + @Mock private RootTaskDisplayAreaOrganizer mMockRootTaskDisplayAreaOrganizer; @Mock private Supplier<SurfaceControl.Transaction> mMockTransactionSupplier; @@ -192,6 +205,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private HandleMenuFactory mMockHandleMenuFactory; @Mock private MultiInstanceHelper mMockMultiInstanceHelper; + @Mock + private WindowDecorCaptionHandleRepository mMockCaptionHandleRepository; @Captor private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener; @Captor @@ -245,6 +260,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { when(mMockWindowDecorViewHostSupplier.acquire(any(), eq(defaultDisplay))) .thenReturn(mMockWindowDecorViewHost); when(mMockWindowDecorViewHost.getSurfaceControl()).thenReturn(mock(SurfaceControl.class)); + when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(), + any())).thenReturn(mMockAppHeaderViewHolder); } @After @@ -838,6 +855,143 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { assertFalse(decoration.isHandleMenuActive()); } + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + public void notifyCaptionStateChanged_flagDisabled_doNoNotify() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + + spyWindowDecor.relayout(taskInfo); + + verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + public void notifyCaptionStateChanged_inFullscreenMode_notifiesAppHandleVisible() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( + CaptionState.class); + + spyWindowDecor.relayout(taskInfo); + + verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( + captionStateArgumentCaptor.capture()); + assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf( + CaptionState.AppHandle.class); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("TODO(b/367235906): Due to MONITOR_INPUT permission error") + public void notifyCaptionStateChanged_inWindowingMode_notifiesAppHeaderVisible() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); + when(mMockAppHeaderViewHolder.getAppChipLocationInWindow()).thenReturn( + new Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3)); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT) + taskInfo.isResizeable = false; + ArgumentCaptor<Function0<Unit>> runnableArgumentCaptor = ArgumentCaptor.forClass( + Function0.class); + ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( + CaptionState.class); + + spyWindowDecor.relayout(taskInfo); + verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout( + runnableArgumentCaptor.capture()); + runnableArgumentCaptor.getValue().invoke(); + + verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( + captionStateArgumentCaptor.capture()); + assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf( + CaptionState.AppHeader.class); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + public void notifyCaptionStateChanged_taskNotVisible_notifiesNoCaptionVisible() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ false); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED); + ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( + CaptionState.class); + + spyWindowDecor.relayout(taskInfo); + + verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( + captionStateArgumentCaptor.capture()); + assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf( + CaptionState.NoCaption.class); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + public void notifyCaptionStateChanged_captionHandleExpanded_notifiesHandleMenuExpanded() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( + CaptionState.class); + + spyWindowDecor.relayout(taskInfo); + createHandleMenu(spyWindowDecor); + + verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( + captionStateArgumentCaptor.capture()); + assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf( + CaptionState.AppHandle.class); + assertThat( + ((CaptionState.AppHandle) captionStateArgumentCaptor.getValue()) + .isHandleMenuExpanded()).isEqualTo( + true); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + public void notifyCaptionStateChanged_captionHandleClosed_notifiesHandleMenuClosed() { + when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), /* visible= */true)); + final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass( + CaptionState.class); + + spyWindowDecor.relayout(taskInfo); + createHandleMenu(spyWindowDecor); + spyWindowDecor.closeHandleMenu(); + + verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged( + captionStateArgumentCaptor.capture()); + assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf( + CaptionState.AppHandle.class); + assertThat( + ((CaptionState.AppHandle) captionStateArgumentCaptor.getValue()) + .isHandleMenuExpanded()).isEqualTo( + false); + + } + private void verifyHandleMenuCreated(@Nullable Uri uri) { verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(), any(), anyBoolean(), anyBoolean(), anyBoolean(), eq(uri), anyInt(), @@ -906,12 +1060,13 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, mContext, mMockDisplayController, mMockSplitScreenController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor, - mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, + mMockChoreographer, mMockSyncQueue, mMockAppHeaderViewHolderFactory, + mMockRootTaskDisplayAreaOrganizer, mMockGenericLinksParser, mMockAssistContentRequester, SurfaceControl.Builder::new, mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory, mMockWindowDecorViewHostSupplier, maximizeMenuFactory, mMockHandleMenuFactory, - mMockMultiInstanceHelper); + mMockMultiInstanceHelper, mMockCaptionHandleRepository); windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener); windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); @@ -951,6 +1106,14 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { != 0; } + private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) { + final InsetsState state = new InsetsState(); + final InsetsSource source = new InsetsSource(/* id= */0, type); + source.setVisible(visible); + state.addSource(source); + return state; + } + private static class TestTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { |