diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml new file mode 100644 index 0000000000000..022594982ca33 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml @@ -0,0 +1,22 @@ + + + + + + + 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 cc0da2840fa03..eb7c32fe8227c 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 @@ -671,13 +671,17 @@ public abstract class WMShellModule { Context context, ShellInit shellInit, ShellController shellController, + DisplayController displayController, ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, Transitions transitions, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor ) { - return new DesktopTasksController(context, shellInit, shellController, shellTaskOrganizer, - transitions, desktopModeTaskRepository, mainExecutor); + return new DesktopTasksController(context, shellInit, shellController, displayController, + shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, transitions, + desktopModeTaskRepository, mainExecutor); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java new file mode 100644 index 0000000000000..015d5c1705e7f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2023 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.RectEvaluator; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; + +import com.android.wm.shell.R; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Animated visual indicator for Desktop Mode windowing transitions. + */ +public class DesktopModeVisualIndicator { + + private final Context mContext; + private final DisplayController mDisplayController; + private final ShellTaskOrganizer mTaskOrganizer; + private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer; + private final ActivityManager.RunningTaskInfo mTaskInfo; + private final SurfaceControl mTaskSurface; + private SurfaceControl mLeash; + + private final SyncTransactionQueue mSyncQueue; + private SurfaceControlViewHost mViewHost; + + public DesktopModeVisualIndicator(SyncTransactionQueue syncQueue, + ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, + Context context, SurfaceControl taskSurface, ShellTaskOrganizer taskOrganizer, + RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer) { + mSyncQueue = syncQueue; + mTaskInfo = taskInfo; + mDisplayController = displayController; + mContext = context; + mTaskSurface = taskSurface; + mTaskOrganizer = taskOrganizer; + mRootTdaOrganizer = taskDisplayAreaOrganizer; + } + + /** + * Create and animate the indicator for the exit desktop mode transition. + */ + public void createFullscreenIndicator() { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + final Resources resources = mContext.getResources(); + final DisplayMetrics metrics = resources.getDisplayMetrics(); + final int screenWidth = metrics.widthPixels; + final int screenHeight = metrics.heightPixels; + final int padding = mDisplayController + .getDisplayLayout(mTaskInfo.displayId).stableInsets().top; + final ImageView v = new ImageView(mContext); + v.setImageResource(R.drawable.desktop_windowing_transition_background); + final SurfaceControl.Builder builder = new SurfaceControl.Builder(); + mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder); + mLeash = builder + .setName("Fullscreen Indicator") + .setContainerLayer() + .build(); + t.show(mLeash); + final WindowManager.LayoutParams lp = + new WindowManager.LayoutParams(screenWidth, screenHeight, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); + lp.setTitle("Fullscreen indicator for Task=" + mTaskInfo.taskId); + lp.setTrustedOverlay(); + final WindowlessWindowManager windowManager = new WindowlessWindowManager( + mTaskInfo.configuration, mLeash, + null /* hostInputToken */); + mViewHost = new SurfaceControlViewHost(mContext, + mDisplayController.getDisplay(mTaskInfo.displayId), windowManager, + "FullscreenVisualIndicator"); + mViewHost.setView(v, lp); + // We want this indicator to be behind the dragged task, but in front of all others. + t.setRelativeLayer(mLeash, mTaskSurface, -1); + + mSyncQueue.runInSync(transaction -> { + transaction.merge(t); + t.close(); + }); + final Rect startBounds = new Rect(padding, padding, + screenWidth - padding, screenHeight - padding); + final VisualIndicatorAnimator animator = VisualIndicatorAnimator.fullscreenIndicator(v, + startBounds); + animator.start(); + } + + /** + * Release the indicator and its components when it is no longer needed. + */ + public void releaseFullscreenIndicator() { + if (mViewHost == null) return; + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + + if (mLeash != null) { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.remove(mLeash); + mLeash = null; + mSyncQueue.runInSync(transaction -> { + transaction.merge(t); + t.close(); + }); + } + } + /** + * Animator for Desktop Mode transitions which supports bounds and alpha animation. + */ + private static class VisualIndicatorAnimator extends ValueAnimator { + private static final int FULLSCREEN_INDICATOR_DURATION = 200; + private static final float SCALE_ADJUSTMENT_PERCENT = 0.015f; + private static final float INDICATOR_FINAL_OPACITY = 0.7f; + + private final ImageView mView; + private final Rect mStartBounds; + private final Rect mEndBounds; + private final RectEvaluator mRectEvaluator; + + private VisualIndicatorAnimator(ImageView view, Rect startBounds, + Rect endBounds) { + mView = view; + mStartBounds = new Rect(startBounds); + mEndBounds = endBounds; + setFloatValues(0, 1); + mRectEvaluator = new RectEvaluator(new Rect()); + } + + /** + * Create animator for visual indicator of fullscreen transition + * + * @param view the view for this indicator + * @param startBounds the starting bounds of the fullscreen indicator + */ + public static VisualIndicatorAnimator fullscreenIndicator(ImageView view, + Rect startBounds) { + view.getDrawable().setBounds(startBounds); + int width = startBounds.width(); + int height = startBounds.height(); + Rect endBounds = new Rect((int) (startBounds.left - (SCALE_ADJUSTMENT_PERCENT * width)), + (int) (startBounds.top - (SCALE_ADJUSTMENT_PERCENT * height)), + (int) (startBounds.right + (SCALE_ADJUSTMENT_PERCENT * width)), + (int) (startBounds.bottom + (SCALE_ADJUSTMENT_PERCENT * height))); + VisualIndicatorAnimator animator = new VisualIndicatorAnimator( + view, startBounds, endBounds); + animator.setInterpolator(new DecelerateInterpolator()); + setupFullscreenIndicatorAnimation(animator); + return animator; + } + + /** + * Add necessary listener for animation of fullscreen indicator + */ + private static void setupFullscreenIndicatorAnimation( + VisualIndicatorAnimator animator) { + animator.addUpdateListener(a -> { + if (animator.mView != null) { + animator.updateBounds(a.getAnimatedFraction(), animator.mView); + animator.updateIndicatorAlpha(a.getAnimatedFraction(), animator.mView); + } else { + animator.cancel(); + } + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animator.mView.getDrawable().setBounds(animator.mEndBounds); + } + }); + animator.setDuration(FULLSCREEN_INDICATOR_DURATION); + } + + /** + * Update bounds of view based on current animation fraction. + * Use of delta is to animate bounds independently, in case we need to + * run multiple animations simultaneously. + * + * @param fraction fraction to use, compared against previous fraction + * @param view the view to update + */ + private void updateBounds(float fraction, ImageView view) { + Rect currentBounds = mRectEvaluator.evaluate(fraction, mStartBounds, mEndBounds); + view.getDrawable().setBounds(currentBounds); + } + + /** + * Fade in the fullscreen indicator + * + * @param fraction current animation fraction + */ + private void updateIndicatorAlpha(float fraction, View view) { + view.setAlpha(fraction * INDICATOR_FINAL_OPACITY); + } + } +} 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 31c5e33f21e32..5696dfc5069e7 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -37,11 +38,14 @@ import android.window.WindowContainerToken import android.window.WindowContainerTransaction import androidx.annotation.BinderThread import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.ExecutorUtils import com.android.wm.shell.common.ExternalInterfaceBinder import com.android.wm.shell.common.RemoteCallable import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.common.annotations.ExternalThread import com.android.wm.shell.common.annotations.ShellMainThread import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener @@ -55,16 +59,20 @@ import java.util.function.Consumer /** Handles moving tasks in and out of desktop */ class DesktopTasksController( - private val context: Context, - shellInit: ShellInit, - private val shellController: ShellController, - private val shellTaskOrganizer: ShellTaskOrganizer, - private val transitions: Transitions, - private val desktopModeTaskRepository: DesktopModeTaskRepository, - @ShellMainThread private val mainExecutor: ShellExecutor + private val context: Context, + shellInit: ShellInit, + private val shellController: ShellController, + private val displayController: DisplayController, + private val shellTaskOrganizer: ShellTaskOrganizer, + private val syncQueue: SyncTransactionQueue, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val transitions: Transitions, + private val desktopModeTaskRepository: DesktopModeTaskRepository, + @ShellMainThread private val mainExecutor: ShellExecutor ) : RemoteCallable, Transitions.TransitionHandler { private val desktopMode: DesktopModeImpl + private var visualIndicator: DesktopModeVisualIndicator? = null init { desktopMode = DesktopModeImpl() @@ -297,6 +305,52 @@ class DesktopTasksController( return desktopMode } + /** + * Perform checks required on drag move. Create/release fullscreen indicator as needed. + * + * @param taskInfo the task being dragged. + * @param taskSurface SurfaceControl of dragged task. + * @param y coordinate of dragged task. Used for checks against status bar height. + */ + fun onDragPositioningMove( + taskInfo: RunningTaskInfo, + taskSurface: SurfaceControl, + y: Float + ) { + val statusBarHeight = displayController + .getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0 + if (taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { + if (y <= statusBarHeight && visualIndicator == null) { + visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo, + displayController, context, taskSurface, shellTaskOrganizer, + rootTaskDisplayAreaOrganizer) + visualIndicator?.createFullscreenIndicator() + } else if (y > statusBarHeight && visualIndicator != null) { + visualIndicator?.releaseFullscreenIndicator() + visualIndicator = null + } + } + } + + /** + * Perform checks required on drag end. Move to fullscreen if drag ends in status bar area. + * + * @param taskInfo the task being dragged. + * @param y height of drag, to be checked against status bar height. + */ + fun onDragPositioningEnd( + taskInfo: RunningTaskInfo, + y: Float + ) { + val statusBarHeight = displayController + .getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0 + if (y <= statusBarHeight && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) { + moveToFullscreen(taskInfo.taskId) + visualIndicator?.releaseFullscreenIndicator() + visualIndicator = null + } + } + /** * Adds a listener to find out about changes in the visibility of freeform tasks. * 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 d2a80471a46f9..317b9a322fbc0 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 @@ -300,7 +300,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return false; } case MotionEvent.ACTION_MOVE: { + final DesktopModeWindowDecoration decoration = + mWindowDecorByTaskId.get(mTaskId); final int dragPointerIdx = e.findPointerIndex(mDragPointerId); + mDesktopTasksController.ifPresent(c -> c.onDragPositioningMove(taskInfo, + decoration.mTaskSurface, e.getRawY(dragPointerIdx))); mDragPositioningCallback.onDragPositioningMove( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); mIsDragging = true; @@ -309,18 +313,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { final int dragPointerIdx = e.findPointerIndex(mDragPointerId); - final int statusBarHeight = mDisplayController - .getDisplayLayout(taskInfo.displayId).stableInsets().top; mDragPositioningCallback.onDragPositioningEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); - if (e.getRawY(dragPointerIdx) <= statusBarHeight) { - if (DesktopModeStatus.isProto2Enabled() - && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { - // Switch a single task to fullscreen - mDesktopTasksController.ifPresent( - c -> c.moveToFullscreen(taskInfo)); - } - } + mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo, + e.getRawY(dragPointerIdx))); final boolean wasDragging = mIsDragging; mIsDragging = false; return wasDragging; 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 43605e30b8135..a6a4d1d2cc722 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 @@ -297,7 +297,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration { 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 95e78a8b7bcc1..5cad50da7e423 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 @@ -37,11 +37,14 @@ import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask @@ -73,7 +76,10 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var shellController: ShellController + @Mock lateinit var displayController: DisplayController @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock lateinit var syncQueue: SyncTransactionQueue + @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock lateinit var transitions: Transitions lateinit var mockitoSession: StaticMockitoSession @@ -105,7 +111,10 @@ class DesktopTasksControllerTest : ShellTestCase() { context, shellInit, shellController, + displayController, shellTaskOrganizer, + syncQueue, + rootTaskDisplayAreaOrganizer, transitions, desktopModeTaskRepository, TestShellExecutor()