Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2c2cf3ae authored by mattsziklay's avatar mattsziklay
Browse files

Visual indicator of transition to fullscreen

First step in providing animations for transition from freeform to
fullscreen. Blue transparent indicator will appear behind a dragged task
when ending the task in that position will result in a transition to
fullscreen.

Bug: 259278770
Bug: 266973675
Test: Manual, drag desktop mode tasks to taskbar, ensure visual
indicator and animation both appear correctly.
Test: atest WindowDecorationTests, DesktopModeControllerTest,
DesktopModeWindowDecorViewModelTests

Change-Id: Ia2308a61092262f719154e255d86c11fe5901123
parent 15efb18c
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ 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.
  -->
<shape android:shape="rectangle"
       xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#bf309fb5" />
    <corners android:radius="20dp" />
    <stroke android:width="1dp" color="#A00080FF"/>
</shape>
+6 −2
Original line number Diff line number Diff line
@@ -659,13 +659,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
+227 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+61 −7
Original line number Diff line number Diff line
@@ -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
@@ -58,13 +62,17 @@ class DesktopTasksController(
        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<DesktopTasksController>, 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.
     *
+6 −10
Original line number Diff line number Diff line
@@ -302,7 +302,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;
@@ -311,18 +315,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;
Loading