diff options
Diffstat (limited to 'libs')
43 files changed, 1586 insertions, 1016 deletions
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index dd86a1a0edbb..83619efefd3b 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -90,9 +90,6 @@ public class DesktopModeStatus { /** The maximum override density allowed for tasks inside the desktop. */ private static final int DESKTOP_DENSITY_MAX = 1000; - /** The number of [WindowDecorViewHost] instances to warm up on system start. */ - private static final int WINDOW_DECOR_PRE_WARM_SIZE = 2; - /** * Sysprop declaring whether to enters desktop mode by default when the windowing mode of the * display's root TaskDisplayArea is set to WINDOWING_MODE_FREEFORM. @@ -115,14 +112,6 @@ public class DesktopModeStatus { private static final String MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit"; /** - * Sysprop declaring the number of [WindowDecorViewHost] instances to warm up on system start. - * - * <p>If it is not defined, then [WINDOW_DECOR_PRE_WARM_SIZE] is used. - */ - private static final String WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP = - "persist.wm.debug.desktop_window_decor_pre_warm_size"; - - /** * Return {@code true} if veiled resizing is active. If false, fluid resizing is used. */ public static boolean isVeiledResizeEnabled() { @@ -162,12 +151,6 @@ public class DesktopModeStatus { context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks)); } - /** The number of [WindowDecorViewHost] instances to warm up on system start. */ - public static int getWindowDecorPreWarmSize() { - return SystemProperties.getInt(WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP, - WINDOW_DECOR_PRE_WARM_SIZE); - } - /** * Return {@code true} if the current device supports desktop mode. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index c545d73734f0..af4a0c55f28d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1241,8 +1241,9 @@ public class BubbleController implements ConfigurationChangeListener, mBubbleData.dismissBubbleWithKey( bubbleKey, Bubbles.DISMISS_USER_GESTURE_FROM_LAUNCHER, timestamp); } - if (selectedBubbleKey != null && !selectedBubbleKey.equals(bubbleKey)) { - // We did not remove the selected bubble. Expand it again + if (mBubbleData.hasBubbles()) { + // We still have bubbles, if we dragged an individual bubble to dismiss we were expanded + // so re-expand to whatever is selected. showExpandedViewForBubbleBar(); } } @@ -2007,7 +2008,7 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void selectionChanged(BubbleViewProvider selectedBubble) { // Only need to update the layer view if we're currently expanded for selection changes. - if (mLayerView != null && isStackExpanded()) { + if (mLayerView != null && mLayerView.isExpanded()) { mLayerView.showExpandedView(selectedBubble); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 1c9c195cf718..1367b7e24bc7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -186,6 +186,10 @@ public class BubbleBarLayerView extends FrameLayout if (expandedView == null) { return; } + if (mExpandedBubble != null && mIsExpanded && b.getKey().equals(mExpandedBubble.getKey())) { + // Already showing this bubble, skip animating + return; + } if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) { removeView(mExpandedView); mExpandedView = null; 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 b47adb43c2a6..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; @@ -116,8 +117,6 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import com.android.wm.shell.windowdecor.viewhost.DefaultWindowDecorViewHostSupplier; -import com.android.wm.shell.windowdecor.viewhost.PooledWindowDecorViewHostSupplier; -import com.android.wm.shell.windowdecor.viewhost.ReusableWindowDecorViewHost; import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier; import dagger.Binds; @@ -143,7 +142,7 @@ import java.util.Optional; includes = { WMShellBaseModule.class, PipModule.class, - ShellBackAnimationModule.class, + ShellBackAnimationModule.class }) public abstract class WMShellModule { @@ -249,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)) { @@ -274,6 +274,7 @@ public abstract class WMShellModule { assistContentRequester, multiInstanceHelper, desktopTasksLimiter, + windowDecorCaptionHandleRepository, desktopActivityOrientationHandler, windowDecorViewHostSupplier); } @@ -384,19 +385,8 @@ public abstract class WMShellModule { @WMSingleton @Provides static WindowDecorViewHostSupplier provideWindowDecorViewHostSupplier( - @NonNull Context context, - @ShellMainThread @NonNull CoroutineScope mainScope, - @NonNull ShellInit shellInit) { - if (DesktopModeStatus.canEnterDesktopMode(context) - && Flags.enableDesktopWindowingScvhCache()) { - final int maxPoolSize = DesktopModeStatus.getMaxTaskLimit(context); - final int preWarmSize = DesktopModeStatus.getWindowDecorPreWarmSize(); - return new PooledWindowDecorViewHostSupplier( - context, mainScope, shellInit, - ReusableWindowDecorViewHost.DefaultFactory.INSTANCE, maxPoolSize, preWarmSize); - } else { - return new DefaultWindowDecorViewHostSupplier(mainScope); - } + @ShellMainThread @NonNull CoroutineScope mainScope) { + return new DefaultWindowDecorViewHostSupplier(mainScope); } // @@ -793,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/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 853284a58904..b8ebbcdbfb9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -305,13 +305,18 @@ class DesktopTasksController( private fun getSplitFocusedTask(task1: RunningTaskInfo, task2: RunningTaskInfo) = if (task1.taskId == task2.parentTaskId) task2 else task1 - private fun isFreeformDisplay(displayId: Int): Boolean { + private fun forceEnterDesktop(displayId: Int): Boolean { + if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)) { + return false + } + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) requireNotNull(tdaInfo) { "This method can only be called with the ID of a display having non-null DisplayArea." } val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode - return tdaWindowingMode == WINDOWING_MODE_FREEFORM + val isFreeformDisplay = tdaWindowingMode == WINDOWING_MODE_FREEFORM + return isFreeformDisplay } /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ @@ -1191,10 +1196,11 @@ class DesktopTasksController( val wct = WindowContainerTransaction() if (!isDesktopModeShowing(task.displayId)) { logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId) - // We are outside of desktop mode and already existing desktop task is being launched. - // We should make this task go to fullscreen instead of freeform. Note that this means - // any re-launch of a freeform window outside of desktop will be in fullscreen. - if (taskRepository.isActiveTask(task.taskId)) { + if (taskRepository.isActiveTask(task.taskId) && !forceEnterDesktop(task.displayId)) { + // We are outside of desktop mode and already existing desktop task is being + // launched. We should make this task go to fullscreen instead of freeform. Note + // that this means any re-launch of a freeform window outside of desktop will be in + // fullscreen as long as default-desktop flag is disabled. addMoveToFullscreenChanges(wct, task) return wct } @@ -1231,9 +1237,7 @@ class DesktopTasksController( transition: IBinder ): WindowContainerTransaction? { logV("handleFullscreenTaskLaunch") - val forceEnterDesktop = DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context) && - isFreeformDisplay(task.displayId) - if (isDesktopModeShowing(task.displayId) || forceEnterDesktop) { + if (isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId)) { logD("Switch fullscreen task to freeform on transition: taskId=%d", task.taskId) return WindowContainerTransaction().also { wct -> addMoveToDesktopChanges(wct, task) 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/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 5a905cfd317f..e8eb10c984af 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2456,6 +2456,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final StageChangeRecord record = new StageChangeRecord(); final int transitType = info.getType(); TransitionInfo.Change pipChange = null; + int closingSplitTaskId = -1; for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); if (change.getMode() == TRANSIT_CHANGE @@ -2516,21 +2517,31 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " with " + taskInfo.taskId + " before startAnimation()."); } } + if (isClosingType(change.getMode()) && + getStageOfTask(change.getTaskInfo().taskId) != STAGE_TYPE_UNDEFINED) { + // If either one of the 2 stages is closing we're assuming we'll break split + closingSplitTaskId = change.getTaskInfo().taskId; + } } if (pipChange != null) { TransitionInfo.Change pipReplacingChange = getPipReplacingChange(info, pipChange, mMainStage.mRootTaskInfo.taskId, mSideStage.mRootTaskInfo.taskId, getSplitItemStage(pipChange.getLastParent())); - if (pipReplacingChange != null) { + boolean keepSplitWithPip = pipReplacingChange != null && closingSplitTaskId == -1; + if (keepSplitWithPip) { // Set an enter transition for when startAnimation gets called again mSplitTransitions.setEnterTransition(transition, /*remoteTransition*/ null, TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, /*resizeAnim*/ false); + } else { + int finalClosingTaskId = closingSplitTaskId; + mRecentTasks.ifPresent(recentTasks -> + recentTasks.removeSplitPair(finalClosingTaskId)); + logExit(EXIT_REASON_FULLSCREEN_REQUEST); } mMixedHandler.animatePendingEnterPipFromSplit(transition, info, - startTransaction, finishTransaction, finishCallback, - pipReplacingChange != null); + startTransaction, finishTransaction, finishCallback, keepSplitWithPip); notifySplitAnimationFinished(); return true; } @@ -2821,8 +2832,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false); mWindowDecorViewModel.ifPresent(viewModel -> { - viewModel.onTaskInfoChanged(finalMainChild.getTaskInfo()); - viewModel.onTaskInfoChanged(finalSideChild.getTaskInfo()); + if (finalMainChild != null) { + viewModel.onTaskInfoChanged(finalMainChild.getTaskInfo()); + } + if (finalSideChild != null) { + viewModel.onTaskInfoChanged(finalSideChild.getTaskInfo()); + } }); mPausingTasks.clear(); }); 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 02c818ffa906..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) -> { @@ -1191,8 +1201,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { : SPLIT_POSITION_TOP_OR_LEFT; final RunningTaskInfo oppositeTaskInfo = mSplitScreenController.getTaskInfo(oppositePosition); - mWindowDecorByTaskId.get(oppositeTaskInfo.taskId) - .disposeStatusBarInputLayer(); + if (oppositeTaskInfo != null) { + mWindowDecorByTaskId.get(oppositeTaskInfo.taskId) + .disposeStatusBarInputLayer(); + } } } mMoveToDesktopAnimator = null; @@ -1375,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..10e4a39fc4ef 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, @@ -407,6 +426,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (!mTaskInfo.isFocused) { closeHandleMenu(); + closeManageWindowsMenu(); closeMaximizeMenu(); } updateDragResizeListener(oldDecorationSurface); @@ -507,6 +527,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 +637,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 +1075,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mAppIconBitmap, mAppName, mSplitScreenController, - DesktopModeStatus.canEnterDesktopMode(mContext), + canEnterDesktopMode(mContext), supportsMultiInstance, shouldShowManageWindowsButton, getBrowserLink(), @@ -1027,6 +1108,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return Unit.INSTANCE; } ); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyCaptionStateChanged(); + } mMinimumInstancesFound = false; } @@ -1067,7 +1151,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } void closeManageWindowsMenu() { - mManageWindowsMenu.close(); + if (mManageWindowsMenu != null) { + mManageWindowsMenu.close(); + } + mManageWindowsMenu = null; } private void updateGenericLink() { @@ -1089,11 +1176,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mWindowDecorViewHolder.onHandleMenuClosed(); mHandleMenu.close(); mHandleMenu = null; + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyCaptionStateChanged(); + } } @Override void releaseViews(WindowContainerTransaction wct) { closeHandleMenu(); + closeManageWindowsMenu(); closeMaximizeMenu(); super.releaseViews(wct); } @@ -1257,9 +1348,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin public void close() { closeDragResizeListener(); closeHandleMenu(); + closeManageWindowsMenu(); mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); disposeStatusBarInputLayer(); + if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + notifyNoCaptionHandle(); + } + super.close(); } @@ -1367,10 +1463,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 +1482,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/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index 6f3f41191485..6eb5cca9ad1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -26,6 +26,8 @@ import android.graphics.PointF; import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.view.Choreographer; import android.view.Surface; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -124,6 +126,11 @@ public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.T @Override public Rect onDragPositioningMove(float x, float y) { + if (Looper.myLooper() != mHandler.getLooper()) { + // This method must run on the shell main thread to use the correct Choreographer + // instance below. + throw new IllegalStateException("This method must run on the shell main thread."); + } PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint); if (isResizing() && DragPositioningCallbackUtility.changeBounds(mCtrlType, mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta, @@ -141,6 +148,7 @@ public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.T final SurfaceControl.Transaction t = mTransactionSupplier.get(); DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration, mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, t, x, y); + t.setFrameTimeline(Choreographer.getInstance().getVsyncId()); t.apply(); } return new Rect(mRepositionTaskBounds); 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/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt index 5156e47cfd13..139e6790b744 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt @@ -19,33 +19,51 @@ import android.content.Context import android.content.res.Configuration import android.view.Display import android.view.SurfaceControl +import android.view.SurfaceControlViewHost import android.view.View import android.view.WindowManager +import android.view.WindowlessWindowManager import androidx.tracing.Trace import com.android.internal.annotations.VisibleForTesting import com.android.wm.shell.shared.annotations.ShellMainThread import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch +typealias SurfaceControlViewHostFactory = + (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost /** - * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHostAdapter]. + * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHost]. * - * It supports asynchronously updating the view hierarchy using [updateViewAsync], in which + * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and + * any attempts to do will throw, which means that once a [View] is added using [updateView] or + * [updateViewAsync], only its properties and binding may be changed, its children views may be + * added, removed or changed and its [WindowManager.LayoutParams] may be changed. + * It also supports asynchronously updating the view hierarchy using [updateViewAsync], in which * case the update work will be posted on the [ShellMainThread] with no delay. */ class DefaultWindowDecorViewHost( - context: Context, + private val context: Context, @ShellMainThread private val mainScope: CoroutineScope, - display: Display, - @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter = - SurfaceControlViewHostAdapter(context, display) + private val display: Display, + private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s -> + SurfaceControlViewHost(c, d, wwm, s) + } ) : WindowDecorViewHost { + private val rootSurface: SurfaceControl = SurfaceControl.Builder() + .setName("DefaultWindowDecorViewHost surface") + .setContainerLayer() + .setCallsite("DefaultWindowDecorViewHost#init") + .build() + + private var wwm: WindowlessWindowManager? = null + @VisibleForTesting + var viewHost: SurfaceControlViewHost? = null private var currentUpdateJob: Job? = null override val surfaceControl: SurfaceControl - get() = viewHostAdapter.rootSurface + get() = rootSurface override fun updateView( view: View, @@ -74,7 +92,8 @@ class DefaultWindowDecorViewHost( override fun release(t: SurfaceControl.Transaction) { clearCurrentUpdateJob() - viewHostAdapter.release(t) + viewHost?.release() + t.remove(rootSurface) } private fun updateViewHost( @@ -83,15 +102,45 @@ class DefaultWindowDecorViewHost( configuration: Configuration, onDrawTransaction: SurfaceControl.Transaction? ) { - viewHostAdapter.prepareViewHost(configuration) + Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost") + if (wwm == null) { + wwm = WindowlessWindowManager(configuration, rootSurface, null) + } + requireWindowlessWindowManager().setConfiguration(configuration) + if (viewHost == null) { + viewHost = surfaceControlViewHostFactory.invoke( + context, + display, + requireWindowlessWindowManager(), + "DefaultWindowDecorViewHost#updateViewHost" + ) + } onDrawTransaction?.let { - viewHostAdapter.applyTransactionOnDraw(it) + requireViewHost().rootSurfaceControl.applyTransactionOnDraw(it) + } + if (requireViewHost().view == null) { + Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-setView") + requireViewHost().setView(view, attrs) + Trace.endSection() + } else { + check(requireViewHost().view == view) { "Changing view is not allowed" } + Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-relayout") + requireViewHost().relayout(attrs) + Trace.endSection() } - viewHostAdapter.updateView(view, attrs) + Trace.endSection() } private fun clearCurrentUpdateJob() { currentUpdateJob?.cancel() currentUpdateJob = null } + + private fun requireWindowlessWindowManager(): WindowlessWindowManager { + return wwm ?: error("Expected non-null windowless window manager") + } + + private fun requireViewHost(): SurfaceControlViewHost { + return viewHost ?: error("Expected non-null view host") + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt deleted file mode 100644 index b04188fa82a8..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.wm.shell.windowdecor.viewhost - -import android.content.Context -import android.os.Trace -import android.util.Pools -import android.view.Display -import android.view.SurfaceControl -import com.android.wm.shell.shared.annotations.ShellMainThread -import com.android.wm.shell.sysui.ShellInit -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -/** - * A [WindowDecorViewHostSupplier] backed by a pool to allow recycling view hosts which may be - * expensive to recreate for each new/updated window decoration. - * - * Callers can obtain [ReusableWindowDecorViewHost] using [acquire], which will return a pooled - * object if available, or create a new instance and return it if needed. When done using a - * [ReusableWindowDecorViewHost], it must be released using [release] to allow it to be sent back - * into the pool and reused later on. - * - * This class also supports pre-warming [ReusableWindowDecorViewHost] instances, which will be put - * into the pool immediately after creation. - */ -class PooledWindowDecorViewHostSupplier( - private val context: Context, - @ShellMainThread private val mainScope: CoroutineScope, - shellInit: ShellInit, - private val viewHostFactory: ReusableWindowDecorViewHost.Factory = - ReusableWindowDecorViewHost.DefaultFactory, - maxPoolSize: Int, - private val preWarmSize: Int, -) : WindowDecorViewHostSupplier<ReusableWindowDecorViewHost> { - - private val pool: Pools.Pool<ReusableWindowDecorViewHost> = Pools.SynchronizedPool(maxPoolSize) - private var nextDecorViewHostId = 0 - - init { - require(preWarmSize <= maxPoolSize) { "Pre-warm size should not exceed pool size" } - shellInit.addInitCallback(this::onShellInit, this) - } - - private fun onShellInit() { - if (preWarmSize <= 0) { - return - } - preWarmViewHosts(preWarmSize) - } - - private fun preWarmViewHosts(preWarmSize: Int) { - mainScope.launch { - // Applying isn't needed, as the surface was never actually shown. - val t = SurfaceControl.Transaction() - repeat(preWarmSize) { - val warmedViewHost = create(context, context.display).apply { - warmUp() - } - // Put the warmed view host in the pool by releasing it. - release(warmedViewHost, t) - } - } - } - - override fun acquire(context: Context, display: Display): ReusableWindowDecorViewHost { - val reusedDecorViewHost = pool.acquire() - if (reusedDecorViewHost != null) { - return reusedDecorViewHost - } - Trace.beginSection("WindowDecorViewHostPool#acquire-new") - val newDecorViewHost = create(context, display) - Trace.endSection() - return newDecorViewHost - } - - override fun release(viewHost: ReusableWindowDecorViewHost, t: SurfaceControl.Transaction) { - val cached = pool.release(viewHost) - if (!cached) { - viewHost.release(t) - } - } - - private fun create(context: Context, display: Display): ReusableWindowDecorViewHost { - return viewHostFactory.create( - context, - mainScope, - display, - nextDecorViewHostId++ - ) - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt deleted file mode 100644 index 64536d1a7897..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.wm.shell.windowdecor.viewhost - -import android.content.Context -import android.content.res.Configuration -import android.graphics.PixelFormat -import android.os.Trace -import android.view.Display -import android.view.SurfaceControl -import android.view.SurfaceControlViewHost -import android.view.View -import android.view.WindowManager -import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE -import android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH -import android.view.WindowManager.LayoutParams.TYPE_APPLICATION -import android.widget.FrameLayout -import com.android.internal.annotations.VisibleForTesting -import com.android.wm.shell.shared.annotations.ShellMainThread -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch - -/** - * An implementation of [WindowDecorViewHost] that supports: - * 1) Replacing the root [View], meaning [WindowDecorViewHost.updateView] maybe be - * called with different [View] instances. This is useful when reusing [WindowDecorViewHost]s - * instances for vastly different view hierarchies, such as Desktop Windowing's App Handles and - * App Headers. - * 2) Pre-warming of the underlying [SurfaceControlViewHost]s. Useful because their creation and - * first root view assignment are expensive, which is undesirable in latency-sensitive code - * paths like during a shell transition. - */ -class ReusableWindowDecorViewHost( - private val context: Context, - @ShellMainThread private val mainScope: CoroutineScope, - display: Display, - val id: Int, - @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter = - SurfaceControlViewHostAdapter(context, display) -) : WindowDecorViewHost, Warmable { - - @VisibleForTesting - val rootView = FrameLayout(context) - - private var currentUpdateJob: Job? = null - - override val surfaceControl: SurfaceControl - get() = viewHostAdapter.rootSurface - - override fun warmUp() { - if (viewHostAdapter.isInitialized()) { - // Already warmed up. - return - } - Trace.beginSection("$TAG#warmUp") - viewHostAdapter.prepareViewHost(context.resources.configuration) - viewHostAdapter.updateView( - rootView, - WindowManager.LayoutParams( - 0 /* width*/, - 0 /* height */, - TYPE_APPLICATION, - FLAG_NOT_FOCUSABLE or FLAG_SPLIT_TOUCH, - PixelFormat.TRANSPARENT - ).apply { - setTitle("View root of $TAG#$id") - setTrustedOverlay() - } - ) - Trace.endSection() - } - - override fun updateView( - view: View, - attrs: WindowManager.LayoutParams, - configuration: Configuration, - onDrawTransaction: SurfaceControl.Transaction? - ) { - clearCurrentUpdateJob() - updateViewHost(view, attrs, configuration, onDrawTransaction) - } - - override fun updateViewAsync( - view: View, - attrs: WindowManager.LayoutParams, - configuration: Configuration - ) { - clearCurrentUpdateJob() - currentUpdateJob = mainScope.launch { - updateViewHost(view, attrs, configuration, onDrawTransaction = null) - } - } - - override fun release(t: SurfaceControl.Transaction) { - clearCurrentUpdateJob() - viewHostAdapter.release(t) - } - - private fun updateViewHost( - view: View, - attrs: WindowManager.LayoutParams, - configuration: Configuration, - onDrawTransaction: SurfaceControl.Transaction? - ) { - viewHostAdapter.prepareViewHost(configuration) - onDrawTransaction?.let { - viewHostAdapter.applyTransactionOnDraw(it) - } - rootView.removeAllViews() - rootView.addView(view) - viewHostAdapter.updateView(rootView, attrs) - } - - private fun clearCurrentUpdateJob() { - currentUpdateJob?.cancel() - currentUpdateJob = null - } - - interface Factory { - fun create( - context: Context, - @ShellMainThread mainScope: CoroutineScope, - display: Display, - id: Int - ): ReusableWindowDecorViewHost - } - - object DefaultFactory : Factory { - override fun create( - context: Context, - @ShellMainThread mainScope: CoroutineScope, - display: Display, - id: Int - ): ReusableWindowDecorViewHost { - return ReusableWindowDecorViewHost( - context, - mainScope, - display, - id - ) - } - } - - companion object { - private const val TAG = "ReusableWindowDecorViewHost" - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt deleted file mode 100644 index a54c9ba67cf8..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.wm.shell.windowdecor.viewhost - -import android.content.Context -import android.content.res.Configuration -import android.view.AttachedSurfaceControl -import android.view.Display -import android.view.SurfaceControl -import android.view.SurfaceControlViewHost -import android.view.View -import android.view.WindowManager -import android.view.WindowlessWindowManager -import androidx.tracing.Trace -import com.android.internal.annotations.VisibleForTesting -typealias SurfaceControlViewHostFactory = - (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost - -/** - * Adapter for a [SurfaceControlViewHost] and its backing [SurfaceControl]. - * - * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and - * any attempts to do will throw, which means that once a [View] is added using [updateView], only - * its properties and binding may be changed, its children views may be added, removed or changed - * and its [WindowManager.LayoutParams] may be changed. - */ -class SurfaceControlViewHostAdapter( - private val context: Context, - private val display: Display, - private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s -> - SurfaceControlViewHost(c, d, wwm, s) - } -) { - val rootSurface: SurfaceControl = SurfaceControl.Builder() - .setName("SurfaceControlViewHostAdapter surface") - .setContainerLayer() - .setCallsite("SurfaceControlViewHostAdapter#init") - .build() - - private var wwm: WindowlessWindowManager? = null - @VisibleForTesting - var viewHost: SurfaceControlViewHost? = null - - /** Initialize the [SurfaceControlViewHost] if needed. */ - fun prepareViewHost(configuration: Configuration) { - if (wwm == null) { - wwm = WindowlessWindowManager(configuration, rootSurface, null) - } - requireWindowlessWindowManager().setConfiguration(configuration) - if (viewHost == null) { - viewHost = surfaceControlViewHostFactory.invoke( - context, - display, - requireWindowlessWindowManager(), - "SurfaceControlViewHostAdapter#prepareViewHost" - ) - } - } - - /** - * Request to apply the transaction atomically with the next draw of the view hierarchy. - * See [AttachedSurfaceControl.applyTransactionOnDraw]. - */ - fun applyTransactionOnDraw(t: SurfaceControl.Transaction) { - requireViewHost().rootSurfaceControl.applyTransactionOnDraw(t) - } - - /** Update the view hierarchy of the view host. */ - fun updateView(view: View, attrs: WindowManager.LayoutParams) { - if (requireViewHost().view == null) { - Trace.beginSection("SurfaceControlViewHostAdapter#updateView-setView") - requireViewHost().setView(view, attrs) - Trace.endSection() - } else { - check(requireViewHost().view == view) { "Changing view is not allowed" } - Trace.beginSection("SurfaceControlViewHostAdapter#updateView-relayout") - requireViewHost().relayout(attrs) - Trace.endSection() - } - } - - /** Release the view host and remove the backing surface. */ - fun release(t: SurfaceControl.Transaction) { - viewHost?.release() - t.remove(rootSurface) - } - - /** Whether the view host has had a view hierarchy set. */ - fun isInitialized(): Boolean = viewHost?.view != null - - private fun requireWindowlessWindowManager(): WindowlessWindowManager { - return wwm ?: error("Expected non-null windowless window manager") - } - - private fun requireViewHost(): SurfaceControlViewHost { - return viewHost ?: error("Expected non-null view host") - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt deleted file mode 100644 index 0df9bfa2ee78..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.wm.shell.windowdecor.viewhost - -/** - * An interface for an object that can be warmed up before it's needed. - */ -interface Warmable { - fun warmUp() -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index e610ebd6bfab..8f20841e76b3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -1764,6 +1764,37 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNotNull(wct, "should handle request") + assertFalse(wct.anyWindowingModeChange(freeformTask.token)) + } + + @Test + fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNotNull(wct, "should handle request") + assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -3493,6 +3524,14 @@ private fun WindowContainerTransaction?.anyDensityConfigChange( } ?: false } +private fun WindowContainerTransaction?.anyWindowingModeChange( + token: WindowContainerToken +): Boolean { +return this?.changes?.any { change -> + change.key == token.asBinder() && change.value.windowingMode >= 0 +} ?: false +} + private fun createTaskInfo(id: Int) = RecentTaskInfo().apply { taskId = id 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/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index fec9e3ebd1ef..aea14b900647 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -332,6 +332,35 @@ public class ShellTransitionTests extends ShellTestCase { } @Test + public void testTransitionFilterTaskFragmentToken() { + final IBinder taskFragmentToken = new Binder(); + + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = + new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + filter.mRequirements[0].mTaskFragmentToken = taskFragmentToken; + + // Transition with the same token should match. + final TransitionInfo infoHasTaskFragmentToken = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, taskFragmentToken).build(); + assertTrue(filter.matches(infoHasTaskFragmentToken)); + + // Transition with a different token should not match. + final IBinder differentTaskFragmentToken = new Binder(); + final TransitionInfo infoDifferentTaskFragmentToken = + new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, differentTaskFragmentToken).build(); + assertFalse(filter.matches(infoDifferentTaskFragmentToken)); + + // Transition without a token should not match. + final TransitionInfo infoNoTaskFragmentToken = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, createTaskInfo( + 1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD)).build(); + assertFalse(filter.matches(infoNoTaskFragmentToken)); + } + + @Test public void testTransitionFilterMultiRequirement() { // filter that requires at-least one opening and one closing app TransitionFilter filter = new TransitionFilter(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java index b8939e6ff623..49ae182fef34 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java @@ -20,8 +20,10 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static org.mockito.Mockito.mock; +import android.annotation.Nullable; import android.app.ActivityManager; import android.content.ComponentName; +import android.os.IBinder; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -51,21 +53,24 @@ public class TransitionInfoBuilder { } public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, - @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo, - ComponentName activityComponent) { + @TransitionInfo.ChangeFlags int flags, + @Nullable ActivityManager.RunningTaskInfo taskInfo, + @Nullable ComponentName activityComponent, @Nullable IBinder taskFragmentToken) { final TransitionInfo.Change change = new TransitionInfo.Change( taskInfo != null ? taskInfo.token : null, createMockSurface(true /* valid */)); change.setMode(mode); change.setFlags(flags); change.setTaskInfo(taskInfo); change.setActivityComponent(activityComponent); + change.setTaskFragmentToken(taskFragmentToken); return addChange(change); } /** Add a change to the TransitionInfo */ public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) { - return addChange(mode, flags, taskInfo, null /* activityComponent */); + return addChange(mode, flags, taskInfo, null /* activityComponent */, + null /* taskFragmentToken */); } public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, @@ -76,13 +81,21 @@ public class TransitionInfoBuilder { /** Add a change to the TransitionInfo */ public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, ComponentName activityComponent) { - return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent); + return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent, + null /* taskFragmentToken */); } public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) { return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */); } + /** Add a change with a TaskFragment token to the TransitionInfo */ + public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, + @Nullable IBinder taskFragmentToken) { + return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */, + null /* activityComponent */, taskFragmentToken); + } + public TransitionInfoBuilder addChange(TransitionInfo.Change change) { change.setDisplayId(DISPLAY_ID, DISPLAY_ID); mInfo.addChange(change); 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 { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index ab41d9c80177..1273ee823159 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -23,6 +23,7 @@ import android.graphics.Point import android.graphics.Rect import android.os.Handler import android.os.IBinder +import android.os.Looper import android.testing.AndroidTestingRunner import android.view.Display import android.view.Surface.ROTATION_0 @@ -34,6 +35,7 @@ import android.view.WindowManager.TRANSIT_CHANGE import android.window.TransitionInfo import android.window.WindowContainerToken import androidx.test.filters.SmallTest +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase @@ -108,8 +110,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { private lateinit var mockResources: Resources @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor - @Mock - private lateinit var mockHandler: Handler + private val mainHandler = Handler(Looper.getMainLooper()) private lateinit var taskPositioner: VeiledResizeTaskPositioner @@ -159,12 +160,12 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { mockTransactionFactory, mockTransitions, mockInteractionJankMonitor, - mockHandler, + mainHandler, ) } @Test - fun testDragResize_noMove_doesNotShowResizeVeil() { + fun testDragResize_noMove_doesNotShowResizeVeil() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, STARTING_BOUNDS.left.toFloat(), @@ -176,6 +177,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) + verify(mockTransitions, never()).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> return@argThat wct.changes.any { (token, change) -> token == taskBinder && @@ -186,7 +188,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_movesTask_doesNotShowResizeVeil() { + fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, STARTING_BOUNDS.left.toFloat(), @@ -221,7 +223,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_resize_boundsUpdateOnEnd() { + fun testDragResize_resize_boundsUpdateOnEnd() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, STARTING_BOUNDS.right.toFloat(), @@ -262,7 +264,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() { + fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, STARTING_BOUNDS.left.toFloat(), @@ -294,9 +296,8 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { }) } - @Test - fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() { + fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag STARTING_BOUNDS.left.toFloat(), @@ -321,7 +322,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() { + fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() = runOnUiThread { mockDesktopWindowDecoration.mTaskInfo.isFocused = false taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT, // Resize right @@ -337,7 +338,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() { + fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() = runOnUiThread { mockDesktopWindowDecoration.mTaskInfo.isFocused = true taskPositioner.onDragPositioningStart( CTRL_TYPE_RIGHT, // Resize right @@ -353,7 +354,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_drag_draggedTaskNotReorderedToTop() { + fun testDragResize_drag_draggedTaskNotReorderedToTop() = runOnUiThread { mockDesktopWindowDecoration.mTaskInfo.isFocused = false taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag @@ -370,7 +371,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_drag_updatesStableBoundsOnRotate() { + fun testDragResize_drag_updatesStableBoundsOnRotate() = runOnUiThread { // Test landscape stable bounds performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(), STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat() + 2000, @@ -416,7 +417,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testIsResizingOrAnimatingResizeSet() { + fun testIsResizingOrAnimatingResizeSet() = runOnUiThread { Assert.assertFalse(taskPositioner.isResizingOrAnimating) taskPositioner.onDragPositioningStart( @@ -443,7 +444,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() { + fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() = runOnUiThread { performDrag( STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat(), STARTING_BOUNDS.left.toFloat() - 20, STARTING_BOUNDS.top.toFloat() - 20, @@ -457,7 +458,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testStartAnimation_useEndRelOffset() { + fun testStartAnimation_useEndRelOffset() = runOnUiThread { val changeMock = mock(TransitionInfo.Change::class.java) val startTransaction = mock(Transaction::class.java) val finishTransaction = mock(Transaction::class.java) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt index 1b0b7d95e657..1b2ce9e4df36 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor.viewhost import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.SurfaceControl +import android.view.SurfaceControlViewHost import android.view.View import android.view.WindowManager import androidx.test.filters.SmallTest @@ -27,6 +28,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock @@ -57,8 +59,54 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { onDrawTransaction = null ) - assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue() - assertThat(windowDecorViewHost.view()).isEqualTo(view) + assertThat(windowDecorViewHost.viewHost).isNotNull() + assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view) + } + + @Test + fun updateView_alreadyLaidOut_relayouts() = runTest { + val windowDecorViewHost = createDefaultViewHost() + val view = View(context) + windowDecorViewHost.updateView( + view = view, + attrs = WindowManager.LayoutParams(100, 100), + configuration = context.resources.configuration, + onDrawTransaction = null + ) + + val otherParams = WindowManager.LayoutParams(200, 200) + windowDecorViewHost.updateView( + view = view, + attrs = otherParams, + configuration = context.resources.configuration, + onDrawTransaction = null + ) + + assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view) + assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width) + .isEqualTo(otherParams.width) + } + + @Test + fun updateView_replacingView_throws() = runTest { + val windowDecorViewHost = createDefaultViewHost() + val view = View(context) + windowDecorViewHost.updateView( + view = view, + attrs = WindowManager.LayoutParams(100, 100), + configuration = context.resources.configuration, + onDrawTransaction = null + ) + + val otherView = View(context) + assertThrows(Exception::class.java) { + windowDecorViewHost.updateView( + view = otherView, + attrs = WindowManager.LayoutParams(100, 100), + configuration = context.resources.configuration, + onDrawTransaction = null + ) + } } @OptIn(ExperimentalCoroutinesApi::class) @@ -77,7 +125,7 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { ) // No view host yet, since the coroutine hasn't run. - assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isFalse() + assertThat(windowDecorViewHost.viewHost).isNull() windowDecorViewHost.updateView( view = syncView, @@ -89,13 +137,14 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { // Would run coroutine if it hadn't been cancelled. advanceUntilIdle() - assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue() - assertThat(windowDecorViewHost.view()).isNotNull() + assertThat(windowDecorViewHost.viewHost).isNotNull() + assertThat(windowDecorViewHost.viewHost!!.view).isNotNull() // View host view/attrs should match the ones from the sync call, plus, since the // sync/async were made with different views, if the job hadn't been cancelled there // would've been an exception thrown as replacing views isn't allowed. - assertThat(windowDecorViewHost.view()).isEqualTo(syncView) - assertThat(windowDecorViewHost.view()!!.layoutParams.width).isEqualTo(syncAttrs.width) + assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(syncView) + assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width) + .isEqualTo(syncAttrs.width) } @OptIn(ExperimentalCoroutinesApi::class) @@ -111,11 +160,11 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { configuration = context.resources.configuration, ) - assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isFalse() + assertThat(windowDecorViewHost.viewHost).isNull() advanceUntilIdle() - assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue() + assertThat(windowDecorViewHost.viewHost).isNotNull() } @OptIn(ExperimentalCoroutinesApi::class) @@ -138,8 +187,9 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { advanceUntilIdle() - assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue() - assertThat(windowDecorViewHost.view()).isEqualTo(otherView) + assertThat(windowDecorViewHost.viewHost).isNotNull() + assertThat(windowDecorViewHost.viewHost!!.view).isNotNull() + assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(otherView) } @Test @@ -157,15 +207,16 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() { val t = mock(SurfaceControl.Transaction::class.java) windowDecorViewHost.release(t) - verify(windowDecorViewHost.viewHostAdapter).release(t) + verify(windowDecorViewHost.viewHost!!).release() + verify(t).remove(windowDecorViewHost.surfaceControl) } private fun CoroutineScope.createDefaultViewHost() = DefaultWindowDecorViewHost( context = context, mainScope = this, display = context.display, - viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)), + surfaceControlViewHostFactory = { c, d, wwm, s -> + spy(SurfaceControlViewHost(c, d, wwm, s)) + } ) - - private fun DefaultWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt deleted file mode 100644 index a7e4213ad01d..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.wm.shell.windowdecor.viewhost - -import android.testing.AndroidTestingRunner -import android.view.SurfaceControl -import androidx.test.filters.SmallTest -import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.TestShellExecutor -import com.android.wm.shell.sysui.ShellInit -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -/** - * Tests for [PooledWindowDecorViewHostSupplier]. - * - * Build/Install/Run: - * atest WMShellUnitTests:PooledWindowDecorViewHostSupplierTest - */ -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(AndroidTestingRunner::class) -class PooledWindowDecorViewHostSupplierTest : ShellTestCase() { - - private val testExecutor = TestShellExecutor() - private val testShellInit = ShellInit(testExecutor) - @Mock - private lateinit var mockViewHostFactory: ReusableWindowDecorViewHost.Factory - - private lateinit var supplier: PooledWindowDecorViewHostSupplier - - @Test - fun setUp() { - MockitoAnnotations.initMocks(this) - } - - @Test - fun onInit_warmsAndPoolsViewHosts() = runTest { - supplier = createSupplier(maxPoolSize = 5, preWarmSize = 2) - val mockViewHost1 = mock<ReusableWindowDecorViewHost>() - val mockViewHost2 = mock<ReusableWindowDecorViewHost>() - whenever(mockViewHostFactory - .create(context, this, context.display, id = 0)) - .thenReturn(mockViewHost1) - whenever(mockViewHostFactory - .create(context, this, context.display, id = 1)) - .thenReturn(mockViewHost2) - - testExecutor.flushAll() - advanceUntilIdle() - - // Both were warmed up. - verify(mockViewHost1).warmUp() - verify(mockViewHost2).warmUp() - // Both were released, so re-acquiring them provides the same instance. - assertThat(mockViewHost2) - .isEqualTo(supplier.acquire(context, context.display)) - assertThat(mockViewHost1) - .isEqualTo(supplier.acquire(context, context.display)) - } - - @Test(expected = Throwable::class) - fun onInit_warmUpSizeExceedsPoolSize_throws() = runTest { - createSupplier(maxPoolSize = 3, preWarmSize = 4) - } - - @Test - fun acquire_poolHasInstances_reuses() = runTest { - supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0) - - // Prepare the pool with one instance. - val mockViewHost = mock<ReusableWindowDecorViewHost>() - supplier.release(mockViewHost, SurfaceControl.Transaction()) - - assertThat(mockViewHost) - .isEqualTo(supplier.acquire(context, context.display)) - verify(mockViewHostFactory, never()).create(any(), any(), any(), any()) - } - - @Test - fun acquire_pooledHasZeroInstances_creates() = runTest { - supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0) - - supplier.acquire(context, context.display) - - verify(mockViewHostFactory).create(context, this, context.display, id = 0) - } - - @Test - fun release_poolBelowLimit_caches() = runTest { - supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0) - - val mockViewHost = mock<ReusableWindowDecorViewHost>() - val mockT = mock<SurfaceControl.Transaction>() - supplier.release(mockViewHost, mockT) - - assertThat(mockViewHost) - .isEqualTo(supplier.acquire(context, context.display)) - } - - @Test - fun release_poolBelowLimit_doesNotReleaseViewHost() = runTest { - supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0) - - val mockViewHost = mock<ReusableWindowDecorViewHost>() - val mockT = mock<SurfaceControl.Transaction>() - supplier.release(mockViewHost, mockT) - - verify(mockViewHost, never()).release(mockT) - } - - @Test - fun release_poolAtLimit_doesNotCache() = runTest { - supplier = createSupplier(maxPoolSize = 1, preWarmSize = 0) - val mockT = mock<SurfaceControl.Transaction>() - val mockViewHost = mock<ReusableWindowDecorViewHost>() - supplier.release(mockViewHost, mockT) // Maxes pool. - - val mockViewHost2 = mock<ReusableWindowDecorViewHost>() - supplier.release(mockViewHost2, mockT) // Beyond limit. - - assertThat(mockViewHost) - .isEqualTo(supplier.acquire(context, context.display)) - // Second one wasn't cached, so the acquired one should've been a new instance. - assertThat(mockViewHost2) - .isNotEqualTo(supplier.acquire(context, context.display)) - } - - @Test - fun release_poolAtLimit_releasesViewHost() = runTest { - supplier = createSupplier(maxPoolSize = 1, preWarmSize = 0) - val mockT = mock<SurfaceControl.Transaction>() - val mockViewHost = mock<ReusableWindowDecorViewHost>() - supplier.release(mockViewHost, mockT) // Maxes pool. - - val mockViewHost2 = mock<ReusableWindowDecorViewHost>() - supplier.release(mockViewHost2, mockT) // Beyond limit. - - // Second one doesn't fit, so it needs to be released. - verify(mockViewHost2).release(mockT) - } - - private fun CoroutineScope.createSupplier( - maxPoolSize: Int, - preWarmSize: Int - ) = PooledWindowDecorViewHostSupplier( - context, - this, - testShellInit, - mockViewHostFactory, - maxPoolSize, - preWarmSize - ).also { - testShellInit.init() - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt deleted file mode 100644 index de2444e34ca9..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.wm.shell.windowdecor.viewhost - -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.SurfaceControl -import android.view.View -import android.view.WindowManager -import androidx.test.filters.SmallTest -import com.android.wm.shell.ShellTestCase -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.mockito.kotlin.spy -import org.mockito.kotlin.verify - -/** - * Tests for [ReusableWindowDecorViewHost]. - * - * Build/Install/Run: - * atest WMShellUnitTests:ReusableWindowDecorViewHostTest - */ -@SmallTest -@TestableLooper.RunWithLooper -@RunWith(AndroidTestingRunner::class) -class ReusableWindowDecorViewHostTest : ShellTestCase() { - - @Test - fun warmUp_addsRootView() = runTest { - val reusableVH = createReusableViewHost().apply { - warmUp() - } - - assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() - assertThat(reusableVH.view()).isEqualTo(reusableVH.rootView) - } - - @Test - fun update_differentView_replacesView() = runTest { - val view = View(context) - val lp = WindowManager.LayoutParams() - val reusableVH = createReusableViewHost() - reusableVH.updateView(view, lp, context.resources.configuration, null) - - assertThat(reusableVH.rootView.childCount).isEqualTo(1) - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(view) - - val newView = View(context) - val newLp = WindowManager.LayoutParams() - reusableVH.updateView(newView, newLp, context.resources.configuration, null) - - assertThat(reusableVH.rootView.childCount).isEqualTo(1) - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(newView) - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun updateView_clearsPendingAsyncJob() = runTest { - val reusableVH = createReusableViewHost() - val asyncView = View(context) - val syncView = View(context) - val asyncAttrs = WindowManager.LayoutParams(100, 100) - val syncAttrs = WindowManager.LayoutParams(200, 200) - - reusableVH.updateViewAsync( - view = asyncView, - attrs = asyncAttrs, - configuration = context.resources.configuration, - ) - - // No view host yet, since the coroutine hasn't run. - assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse() - - reusableVH.updateView( - view = syncView, - attrs = syncAttrs, - configuration = context.resources.configuration, - onDrawTransaction = null - ) - - // Would run coroutine if it hadn't been cancelled. - advanceUntilIdle() - - assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() - // View host view/attrs should match the ones from the sync call, plus, since the - // sync/async were made with different views, if the job hadn't been cancelled there - // would've been an exception thrown as replacing views isn't allowed. - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(syncView) - assertThat(reusableVH.view()!!.layoutParams.width).isEqualTo(syncAttrs.width) - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun updateViewAsync() = runTest { - val reusableVH = createReusableViewHost() - val view = View(context) - val attrs = WindowManager.LayoutParams(100, 100) - - reusableVH.updateViewAsync( - view = view, - attrs = attrs, - configuration = context.resources.configuration, - ) - - assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse() - - advanceUntilIdle() - - assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun updateViewAsync_clearsPendingAsyncJob() = runTest { - val reusableVH = createReusableViewHost() - - val view = View(context) - reusableVH.updateViewAsync( - view = view, - attrs = WindowManager.LayoutParams(100, 100), - configuration = context.resources.configuration, - ) - val otherView = View(context) - reusableVH.updateViewAsync( - view = otherView, - attrs = WindowManager.LayoutParams(100, 100), - configuration = context.resources.configuration, - ) - - advanceUntilIdle() - - assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(otherView) - } - - @Test - fun release() = runTest { - val reusableVH = createReusableViewHost() - - val view = View(context) - reusableVH.updateView( - view = view, - attrs = WindowManager.LayoutParams(100, 100), - configuration = context.resources.configuration, - onDrawTransaction = null - ) - - val t = mock(SurfaceControl.Transaction::class.java) - reusableVH.release(t) - - verify(reusableVH.viewHostAdapter).release(t) - } - - private fun CoroutineScope.createReusableViewHost() = ReusableWindowDecorViewHost( - context = context, - mainScope = this, - display = context.display, - id = 1, - viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)), - ) - - private fun ReusableWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt deleted file mode 100644 index d6c80a7fffc1..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.wm.shell.windowdecor.viewhost - -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.SurfaceControl -import android.view.SurfaceControlViewHost -import android.view.View -import android.view.WindowManager -import androidx.test.filters.SmallTest -import com.android.wm.shell.ShellTestCase -import com.google.common.truth.Truth.assertThat -import org.junit.Assert.assertThrows -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.mockito.kotlin.spy -import org.mockito.kotlin.verify - -/** - * Tests for [SurfaceControlViewHostAdapter]. - * - * Build/Install/Run: - * atest WMShellUnitTests:SurfaceControlViewHostAdapterTest - */ -@SmallTest -@TestableLooper.RunWithLooper -@RunWith(AndroidTestingRunner::class) -class SurfaceControlViewHostAdapterTest : ShellTestCase() { - - private lateinit var adapter: SurfaceControlViewHostAdapter - - @Before - fun setUp() { - adapter = SurfaceControlViewHostAdapter( - context, - context.display, - surfaceControlViewHostFactory = { c, d, wwm, s -> - spy(SurfaceControlViewHost(c, d, wwm, s)) - } - ) - } - - @Test - fun prepareViewHost() { - adapter.prepareViewHost(context.resources.configuration) - - assertThat(adapter.viewHost).isNotNull() - } - - @Test - fun prepareViewHost_alreadyCreated_skips() { - adapter.prepareViewHost(context.resources.configuration) - - val viewHost = adapter.viewHost!! - - adapter.prepareViewHost(context.resources.configuration) - - assertThat(adapter.viewHost).isEqualTo(viewHost) - } - - @Test - fun updateView_layoutInViewHost() { - val view = View(context) - adapter.prepareViewHost(context.resources.configuration) - - adapter.updateView( - view = view, - attrs = WindowManager.LayoutParams(100, 100) - ) - - assertThat(adapter.isInitialized()).isTrue() - assertThat(adapter.view()).isEqualTo(view) - } - - @Test - fun updateView_alreadyLaidOut_relayouts() { - val view = View(context) - adapter.prepareViewHost(context.resources.configuration) - adapter.updateView( - view = view, - attrs = WindowManager.LayoutParams(100, 100) - ) - - val otherParams = WindowManager.LayoutParams(200, 200) - adapter.updateView( - view = view, - attrs = otherParams - ) - - assertThat(adapter.view()).isEqualTo(view) - assertThat(adapter.view()!!.layoutParams.width).isEqualTo(otherParams.width) - } - - @Test - fun updateView_replacingView_throws() { - val view = View(context) - adapter.prepareViewHost(context.resources.configuration) - adapter.updateView( - view = view, - attrs = WindowManager.LayoutParams(100, 100) - ) - - val otherView = View(context) - assertThrows(Exception::class.java) { - adapter.updateView( - view = otherView, - attrs = WindowManager.LayoutParams(100, 100) - ) - } - } - - @Test - fun release() { - adapter.prepareViewHost(context.resources.configuration) - adapter.updateView( - view = View(context), - attrs = WindowManager.LayoutParams(100, 100) - ) - - val mockT = mock(SurfaceControl.Transaction::class.java) - adapter.release(mockT) - - verify(adapter.viewHost!!).release() - verify(mockT).remove(adapter.rootSurface) - } - - private fun SurfaceControlViewHostAdapter.view(): View? = viewHost?.view -} diff --git a/libs/appfunctions/Android.bp b/libs/appfunctions/Android.bp new file mode 100644 index 000000000000..09e2f423c3ba --- /dev/null +++ b/libs/appfunctions/Android.bp @@ -0,0 +1,31 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_sdk_library { + name: "com.google.android.appfunctions.sidecar", + owner: "google", + srcs: ["java/**/*.java"], + api_packages: ["com.google.android.appfunctions.sidecar"], + dex_preopt: { + enabled: false, + }, + system_ext_specific: true, + no_dist: true, + unsafe_ignore_missing_latest_api: true, +} diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt new file mode 100644 index 000000000000..504e3290b0ae --- /dev/null +++ b/libs/appfunctions/api/current.txt @@ -0,0 +1,49 @@ +// Signature format: 2.0 +package com.google.android.appfunctions.sidecar { + + public final class AppFunctionManager { + ctor public AppFunctionManager(android.content.Context); + method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + } + + public abstract class AppFunctionService extends android.app.Service { + ctor public AppFunctionService(); + method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); + method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; + field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; + } + + public final class ExecuteAppFunctionRequest { + method @NonNull public android.os.Bundle getExtras(); + method @NonNull public String getFunctionIdentifier(); + method @NonNull public android.app.appsearch.GenericDocument getParameters(); + method @NonNull public String getTargetPackageName(); + } + + public static final class ExecuteAppFunctionRequest.Builder { + ctor public ExecuteAppFunctionRequest.Builder(@NonNull String, @NonNull String); + method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest build(); + method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle); + method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument); + } + + public final class ExecuteAppFunctionResponse { + method @Nullable public String getErrorMessage(); + method @NonNull public android.os.Bundle getExtras(); + method public int getResultCode(); + method @NonNull public android.app.appsearch.GenericDocument getResultDocument(); + method public boolean isSuccess(); + method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle); + method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle); + field public static final String PROPERTY_RETURN_VALUE = "returnValue"; + field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2 + field public static final int RESULT_DENIED = 1; // 0x1 + field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3 + field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4 + field public static final int RESULT_OK = 0; // 0x0 + field public static final int RESULT_TIMED_OUT = 5; // 0x5 + } + +} + diff --git a/libs/appfunctions/api/removed.txt b/libs/appfunctions/api/removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/appfunctions/api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/appfunctions/api/system-current.txt b/libs/appfunctions/api/system-current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/appfunctions/api/system-current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/appfunctions/api/system-removed.txt b/libs/appfunctions/api/system-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/appfunctions/api/system-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/appfunctions/api/test-current.txt b/libs/appfunctions/api/test-current.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/appfunctions/api/test-current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/appfunctions/api/test-removed.txt b/libs/appfunctions/api/test-removed.txt new file mode 100644 index 000000000000..d802177e249b --- /dev/null +++ b/libs/appfunctions/api/test-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java new file mode 100644 index 000000000000..b1dd4676a35e --- /dev/null +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java @@ -0,0 +1,82 @@ +/* + * 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.google.android.appfunctions.sidecar; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.content.Context; + +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + + +/** + * Provides app functions related functionalities. + * + * <p>App function is a specific piece of functionality that an app offers to the system. These + * functionalities can be integrated into various system features. + * + * <p>This class wraps {@link android.app.appfunctions.AppFunctionManager} functionalities and + * exposes it here as a sidecar library (avoiding direct dependency on the platform API). + */ +// TODO(b/357551503): Implement get and set enabled app function APIs. +// TODO(b/367329899): Add sidecar library to Android B builds. +public final class AppFunctionManager { + private final android.app.appfunctions.AppFunctionManager mManager; + private final Context mContext; + + /** + * Creates an instance. + * + * @param context A {@link Context}. + * @throws java.lang.IllegalStateException if the underlying {@link + * android.app.appfunctions.AppFunctionManager} is not found. + */ + public AppFunctionManager(Context context) { + mContext = Objects.requireNonNull(context); + mManager = context.getSystemService(android.app.appfunctions.AppFunctionManager.class); + if (mManager == null) { + throw new IllegalStateException( + "Underlying AppFunctionManager system service not found."); + } + } + + /** + * Executes the app function. + * + * <p>Proxies request and response to the underlying {@link + * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and + * response in the appropriate type required by the function. + */ + public void executeAppFunction( + @NonNull ExecuteAppFunctionRequest sidecarRequest, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + Objects.requireNonNull(sidecarRequest); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + android.app.appfunctions.ExecuteAppFunctionRequest platformRequest = + SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest); + mManager.executeAppFunction( + platformRequest, executor, (platformResponse) -> { + callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse( + platformResponse)); + }); + } +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java new file mode 100644 index 000000000000..65959dfdf561 --- /dev/null +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java @@ -0,0 +1,114 @@ +/* + * 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.google.android.appfunctions.sidecar; + +import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE; + +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; + +import java.util.function.Consumer; + +/** + * Abstract base class to provide app functions to the system. + * + * <p>Include the following in the manifest: + * + * <pre> + * {@literal + * <service android:name=".YourService" + * android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"> + * <intent-filter> + * <action android:name="android.app.appfunctions.AppFunctionService" /> + * </intent-filter> + * </service> + * } + * </pre> + * + * <p>This class wraps {@link android.app.appfunctions.AppFunctionService} functionalities and + * exposes it here as a sidecar library (avoiding direct dependency on the platform API). + * + * @see AppFunctionManager + */ +public abstract class AppFunctionService extends Service { + /** + * The permission to only allow system access to the functions through {@link + * AppFunctionManagerService}. + */ + @NonNull + public static final String BIND_APP_FUNCTION_SERVICE = + "android.permission.BIND_APP_FUNCTION_SERVICE"; + + /** + * The {@link Intent} that must be declared as handled by the service. To be supported, the + * service must also require the {@link BIND_APP_FUNCTION_SERVICE} permission so that other + * applications can not abuse it. + */ + @NonNull + public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; + + private final Binder mBinder = + android.app.appfunctions.AppFunctionService.createBinder( + /* context= */ this, + /* onExecuteFunction= */ (platformRequest, callback) -> { + AppFunctionService.this.onExecuteFunction( + SidecarConverter.getSidecarExecuteAppFunctionRequest( + platformRequest), + (sidecarResponse) -> { + callback.accept( + SidecarConverter.getPlatformExecuteAppFunctionResponse( + sidecarResponse)); + }); + } + ); + + @NonNull + @Override + public final IBinder onBind(@Nullable Intent intent) { + return mBinder; + } + + /** + * Called by the system to execute a specific app function. + * + * <p>This method is triggered when the system requests your AppFunctionService to handle a + * particular function you have registered and made available. + * + * <p>To ensure proper routing of function requests, assign a unique identifier to each + * function. This identifier doesn't need to be globally unique, but it must be unique within + * your app. For example, a function to order food could be identified as "orderFood". In most + * cases this identifier should come from the ID automatically generated by the AppFunctions + * SDK. You can determine the specific function to invoke by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * + * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker + * thread and dispatch the result with the given callback. You should always report back the + * result using the callback, no matter if the execution was successful or not. + * + * @param request The function execution request. + * @param callback A callback to report back the result. + */ + @MainThread + public abstract void onExecuteFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull Consumer<ExecuteAppFunctionResponse> callback); +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java new file mode 100644 index 000000000000..fa6d2ff12313 --- /dev/null +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java @@ -0,0 +1,137 @@ +/* + * 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.google.android.appfunctions.sidecar; + +import android.annotation.NonNull; +import android.app.appsearch.GenericDocument; +import android.os.Bundle; + +import java.util.Objects; + +/** + * A request to execute an app function. + * + * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionRequest} without parcel + * functionality and exposes it here as a sidecar library (avoiding direct dependency on the + * platform API). + */ +public final class ExecuteAppFunctionRequest { + /** Returns the package name of the app that hosts the function. */ + @NonNull private final String mTargetPackageName; + + /** + * Returns the unique string identifier of the app function to be executed. TODO(b/357551503): + * Document how callers can get the available function identifiers. + */ + @NonNull private final String mFunctionIdentifier; + + /** Returns additional metadata relevant to this function execution request. */ + @NonNull private final Bundle mExtras; + + /** + * Returns the parameters required to invoke this function. Within this [GenericDocument], the + * property names are the names of the function parameters and the property values are the + * values of those parameters. + * + * <p>The document may have missing parameters. Developers are advised to implement defensive + * handling measures. + * + * <p>TODO(b/357551503): Document how function parameters can be obtained for function execution + */ + @NonNull private final GenericDocument mParameters; + + private ExecuteAppFunctionRequest( + @NonNull String targetPackageName, + @NonNull String functionIdentifier, + @NonNull Bundle extras, + @NonNull GenericDocument parameters) { + mTargetPackageName = Objects.requireNonNull(targetPackageName); + mFunctionIdentifier = Objects.requireNonNull(functionIdentifier); + mExtras = Objects.requireNonNull(extras); + mParameters = Objects.requireNonNull(parameters); + } + + /** Returns the package name of the app that hosts the function. */ + @NonNull + public String getTargetPackageName() { + return mTargetPackageName; + } + + /** Returns the unique string identifier of the app function to be executed. */ + @NonNull + public String getFunctionIdentifier() { + return mFunctionIdentifier; + } + + /** + * Returns the function parameters. The key is the parameter name, and the value is the + * parameter value. + * + * <p>The bundle may have missing parameters. Developers are advised to implement defensive + * handling measures. + */ + @NonNull + public GenericDocument getParameters() { + return mParameters; + } + + /** Returns the additional data relevant to this function execution. */ + @NonNull + public Bundle getExtras() { + return mExtras; + } + + /** Builder for {@link ExecuteAppFunctionRequest}. */ + public static final class Builder { + @NonNull private final String mTargetPackageName; + @NonNull private final String mFunctionIdentifier; + @NonNull private Bundle mExtras = Bundle.EMPTY; + + @NonNull + private GenericDocument mParameters = new GenericDocument.Builder<>("", "", "").build(); + + public Builder(@NonNull String targetPackageName, @NonNull String functionIdentifier) { + mTargetPackageName = Objects.requireNonNull(targetPackageName); + mFunctionIdentifier = Objects.requireNonNull(functionIdentifier); + } + + /** Sets the additional data relevant to this function execution. */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = Objects.requireNonNull(extras); + return this; + } + + /** Sets the function parameters. */ + @NonNull + public Builder setParameters(@NonNull GenericDocument parameters) { + Objects.requireNonNull(parameters); + mParameters = parameters; + return this; + } + + /** Builds the {@link ExecuteAppFunctionRequest}. */ + @NonNull + public ExecuteAppFunctionRequest build() { + return new ExecuteAppFunctionRequest( + mTargetPackageName, + mFunctionIdentifier, + mExtras, + mParameters); + } + } +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java new file mode 100644 index 000000000000..60c25fae58d1 --- /dev/null +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java @@ -0,0 +1,240 @@ +/* + * 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.google.android.appfunctions.sidecar; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.appsearch.GenericDocument; +import android.os.Bundle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * The response to an app function execution. + * + * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionResponse} without parcel + * functionality and exposes it here as a sidecar library (avoiding direct dependency on the + * platform API). + */ +public final class ExecuteAppFunctionResponse { + /** + * The name of the property that stores the function return value within the {@code + * resultDocument}. + * + * <p>See {@link GenericDocument#getProperty(String)} for more information. + * + * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will + * be empty {@link GenericDocument}. + * + * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will + * return {@code null}. + * + * <p>See {@link #getResultDocument} for more information on extracting the return value. + */ + public static final String PROPERTY_RETURN_VALUE = "returnValue"; + + /** The call was successful. */ + public static final int RESULT_OK = 0; + + /** The caller does not have the permission to execute an app function. */ + public static final int RESULT_DENIED = 1; + + /** An unknown error occurred while processing the call in the AppFunctionService. */ + public static final int RESULT_APP_UNKNOWN_ERROR = 2; + + /** + * An internal error occurred within AppFunctionManagerService. + * + * <p>This error may be considered similar to {@link IllegalStateException} + */ + public static final int RESULT_INTERNAL_ERROR = 3; + + /** + * The caller supplied invalid arguments to the call. + * + * <p>This error may be considered similar to {@link IllegalArgumentException}. + */ + public static final int RESULT_INVALID_ARGUMENT = 4; + + /** The operation was timed out. */ + public static final int RESULT_TIMED_OUT = 5; + + /** The result code of the app function execution. */ + @ResultCode private final int mResultCode; + + /** + * The error message associated with the result, if any. This is {@code null} if the result code + * is {@link #RESULT_OK}. + */ + @Nullable private final String mErrorMessage; + + /** + * Returns the return value of the executed function. + * + * <p>The return value is stored in a {@link GenericDocument} with the key {@link + * #PROPERTY_RETURN_VALUE}. + * + * <p>See {@link #getResultDocument} for more information on extracting the return value. + */ + @NonNull private final GenericDocument mResultDocument; + + /** Returns the additional metadata data relevant to this function execution response. */ + @NonNull private final Bundle mExtras; + + private ExecuteAppFunctionResponse( + @NonNull GenericDocument resultDocument, + @NonNull Bundle extras, + @ResultCode int resultCode, + @Nullable String errorMessage) { + mResultDocument = Objects.requireNonNull(resultDocument); + mExtras = Objects.requireNonNull(extras); + mResultCode = resultCode; + mErrorMessage = errorMessage; + } + + /** + * Returns result codes from throwable. + * + * @hide + */ + static @ResultCode int getResultCode(@NonNull Throwable t) { + if (t instanceof IllegalArgumentException) { + return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT; + } + return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR; + } + + /** + * Returns a successful response. + * + * @param resultDocument The return value of the executed function. + * @param extras The additional metadata data relevant to this function execution response. + */ + @NonNull + public static ExecuteAppFunctionResponse newSuccess( + @NonNull GenericDocument resultDocument, @Nullable Bundle extras) { + Objects.requireNonNull(resultDocument); + Bundle actualExtras = getActualExtras(extras); + + return new ExecuteAppFunctionResponse( + resultDocument, actualExtras, RESULT_OK, /* errorMessage= */ null); + } + + /** + * Returns a failure response. + * + * @param resultCode The result code of the app function execution. + * @param extras The additional metadata data relevant to this function execution response. + * @param errorMessage The error message associated with the result, if any. + */ + @NonNull + public static ExecuteAppFunctionResponse newFailure( + @ResultCode int resultCode, @Nullable String errorMessage, @Nullable Bundle extras) { + if (resultCode == RESULT_OK) { + throw new IllegalArgumentException("resultCode must not be RESULT_OK"); + } + Bundle actualExtras = getActualExtras(extras); + GenericDocument emptyDocument = new GenericDocument.Builder<>("", "", "").build(); + return new ExecuteAppFunctionResponse( + emptyDocument, actualExtras, resultCode, errorMessage); + } + + private static Bundle getActualExtras(@Nullable Bundle extras) { + if (extras == null) { + return Bundle.EMPTY; + } + return extras; + } + + /** + * Returns a generic document containing the return value of the executed function. + * + * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. + * + * <p>An empty document is returned if {@link #isSuccess} is {@code false} or if the executed + * function does not produce a return value. + * + * <p>Sample code for extracting the return value: + * + * <pre> + * GenericDocument resultDocument = response.getResultDocument(); + * Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE); + * if (returnValue != null) { + * // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString}, + * // {@link GenericDocument#getPropertyLong} etc. + * // Do something with the returnValue + * } + * </pre> + */ + @NonNull + public GenericDocument getResultDocument() { + return mResultDocument; + } + + /** Returns the extras of the app function execution. */ + @NonNull + public Bundle getExtras() { + return mExtras; + } + + /** + * Returns {@code true} if {@link #getResultCode} equals {@link + * ExecuteAppFunctionResponse#RESULT_OK}. + */ + public boolean isSuccess() { + return getResultCode() == RESULT_OK; + } + + /** + * Returns one of the {@code RESULT} constants defined in {@link ExecuteAppFunctionResponse}. + */ + @ResultCode + public int getResultCode() { + return mResultCode; + } + + /** + * Returns the error message associated with this result. + * + * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. + */ + @Nullable + public String getErrorMessage() { + return mErrorMessage; + } + + /** + * Result codes. + * + * @hide + */ + @IntDef( + prefix = {"RESULT_"}, + value = { + RESULT_OK, + RESULT_DENIED, + RESULT_APP_UNKNOWN_ERROR, + RESULT_INTERNAL_ERROR, + RESULT_INVALID_ARGUMENT, + RESULT_TIMED_OUT, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResultCode {} +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java new file mode 100644 index 000000000000..b1b05f79f33f --- /dev/null +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java @@ -0,0 +1,104 @@ +/* + * 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.google.android.appfunctions.sidecar; + +import android.annotation.NonNull; + +/** + * Utility class containing methods to convert Sidecar objects of AppFunctions API into the + * underlying platform classes. + * + * @hide + */ +public final class SidecarConverter { + private SidecarConverter() {} + + /** + * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest} + * into platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} + * + * @hide + */ + @NonNull + public static android.app.appfunctions.ExecuteAppFunctionRequest + getPlatformExecuteAppFunctionRequest(@NonNull ExecuteAppFunctionRequest request) { + return new + android.app.appfunctions.ExecuteAppFunctionRequest.Builder( + request.getTargetPackageName(), + request.getFunctionIdentifier()) + .setExtras(request.getExtras()) + .setParameters(request.getParameters()) + .build(); + } + + /** + * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse} + * into platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} + * + * @hide + */ + @NonNull + public static android.app.appfunctions.ExecuteAppFunctionResponse + getPlatformExecuteAppFunctionResponse(@NonNull ExecuteAppFunctionResponse response) { + if (response.isSuccess()) { + return android.app.appfunctions.ExecuteAppFunctionResponse.newSuccess( + response.getResultDocument(), response.getExtras()); + } else { + return android.app.appfunctions.ExecuteAppFunctionResponse.newFailure( + response.getResultCode(), + response.getErrorMessage(), + response.getExtras()); + } + } + + /** + * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} + * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest} + * + * @hide + */ + @NonNull + public static ExecuteAppFunctionRequest getSidecarExecuteAppFunctionRequest( + @NonNull android.app.appfunctions.ExecuteAppFunctionRequest request) { + return new ExecuteAppFunctionRequest.Builder( + request.getTargetPackageName(), + request.getFunctionIdentifier()) + .setExtras(request.getExtras()) + .setParameters(request.getParameters()) + .build(); + } + + /** + * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} + * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse} + * + * @hide + */ + @NonNull + public static ExecuteAppFunctionResponse getSidecarExecuteAppFunctionResponse( + @NonNull android.app.appfunctions.ExecuteAppFunctionResponse response) { + if (response.isSuccess()) { + return ExecuteAppFunctionResponse.newSuccess( + response.getResultDocument(), response.getExtras()); + } else { + return ExecuteAppFunctionResponse.newFailure( + response.getResultCode(), + response.getErrorMessage(), + response.getExtras()); + } + } +} diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index 185436160349..84bd45dfc012 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -264,6 +264,7 @@ Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBy , mPixelStorageType(PixelStorageType::Heap) { mPixelStorage.heap.address = address; mPixelStorage.heap.size = size; + traceBitmapCreate(); } Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info) @@ -272,6 +273,7 @@ Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info) , mPixelStorageType(PixelStorageType::WrappedPixelRef) { pixelRef.ref(); mPixelStorage.wrapped.pixelRef = &pixelRef; + traceBitmapCreate(); } Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes) @@ -281,6 +283,7 @@ Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info mPixelStorage.ashmem.address = address; mPixelStorage.ashmem.fd = fd; mPixelStorage.ashmem.size = mappedSize; + traceBitmapCreate(); } #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration @@ -297,10 +300,12 @@ Bitmap::Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes setImmutable(); // HW bitmaps are always immutable mImage = SkImages::DeferredFromAHardwareBuffer(buffer, mInfo.alphaType(), mInfo.refColorSpace()); + traceBitmapCreate(); } #endif Bitmap::~Bitmap() { + traceBitmapDelete(); switch (mPixelStorageType) { case PixelStorageType::WrappedPixelRef: mPixelStorage.wrapped.pixelRef->unref(); @@ -572,4 +577,28 @@ void Bitmap::setGainmap(sp<uirenderer::Gainmap>&& gainmap) { mGainmap = std::move(gainmap); } +std::mutex Bitmap::mLock{}; +size_t Bitmap::mTotalBitmapBytes = 0; +size_t Bitmap::mTotalBitmapCount = 0; + +void Bitmap::traceBitmapCreate() { + if (ATRACE_ENABLED()) { + std::lock_guard lock{mLock}; + mTotalBitmapBytes += getAllocationByteCount(); + mTotalBitmapCount++; + ATRACE_INT64("Bitmap Memory", mTotalBitmapBytes); + ATRACE_INT64("Bitmap Count", mTotalBitmapCount); + } +} + +void Bitmap::traceBitmapDelete() { + if (ATRACE_ENABLED()) { + std::lock_guard lock{mLock}; + mTotalBitmapBytes -= getAllocationByteCount(); + mTotalBitmapCount--; + ATRACE_INT64("Bitmap Memory", mTotalBitmapBytes); + ATRACE_INT64("Bitmap Count", mTotalBitmapCount); + } +} + } // namespace android diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index dd344e2f5517..3d55d859ed5f 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -25,6 +25,7 @@ #include <cutils/compiler.h> #include <utils/StrongPointer.h> +#include <mutex> #include <optional> #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration @@ -227,6 +228,13 @@ private: } mPixelStorage; sk_sp<SkImage> mImage; // Cache is used only for HW Bitmaps with Skia pipeline. + + // for tracing total number and memory usage of bitmaps + static std::mutex mLock; + static size_t mTotalBitmapBytes; + static size_t mTotalBitmapCount; + void traceBitmapCreate(); + void traceBitmapDelete(); }; } // namespace android diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index d7bf20130b71..e13e136550ca 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -73,6 +73,7 @@ static void simplifyPaint(int color, Paint* paint) { } paint->setStrokeJoin(SkPaint::kRound_Join); paint->setLooper(nullptr); + paint->setBlendMode(SkBlendMode::kSrcOver); } class DrawTextFunctor { |