summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java104
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt54
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt91
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java167
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 {