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

Commit cd1ff646 authored by Winson Chung's avatar Winson Chung
Browse files

Ensure PIP retains bounds on device rotation.

- When device rotates, ensure that the PIP continues to show in the
  same aspect ratio, snapped to the same logical snap point.
- Move common snapping code to policy so that it can be shared between
  SystemUI and the framework.

Test: android.server.cts.ActivityManagerPinnedStackTests
Test: #testPinnedStackInBoundsAfterRotation

Change-Id: I2d9f1a2dc077a55c39acc1ccba982c255e2ff3a4
parent 303c6b78
Loading
Loading
Loading
Loading
+174 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.internal.policy;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.view.Gravity;
import android.view.ViewConfiguration;
import android.widget.Scroller;

import java.util.ArrayList;

/**
 * Calculates the snap targets and the snap position for the PIP given a position and a velocity.
 * All bounds are relative to the display top/left.
 */
public class PipSnapAlgorithm {

    // Allows snapping to the four corners
    private static final int SNAP_MODE_CORNERS_ONLY = 0;
    // Allows snapping to the four corners and the mid-points on the long edge in each orientation
    private static final int SNAP_MODE_CORNERS_AND_SIDES = 1;
    // Allows snapping to anywhere along the edge of the screen
    private static final int SNAP_MODE_EDGE = 2;

    private static final float SCROLL_FRICTION_MULTIPLIER = 8f;

    private final Context mContext;

    private final ArrayList<Integer> mSnapGravities = new ArrayList<>();
    private final int mSnapMode = SNAP_MODE_CORNERS_ONLY;

    private final Scroller mScroller;
    private final Rect mDisplayBounds = new Rect();
    private int mOrientation = Configuration.ORIENTATION_UNDEFINED;

    public PipSnapAlgorithm(Context context, int displayId) {
        final DisplayManager displayManager =
                (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
        final ViewConfiguration viewConfig = ViewConfiguration.get(context);
        final Point displaySize = new Point();
        displayManager.getDisplay(displayId).getRealSize(displaySize);
        mContext = context;
        mDisplayBounds.set(0, 0, displaySize.x, displaySize.y);
        mOrientation = context.getResources().getConfiguration().orientation;
        mScroller = new Scroller(context);
        mScroller.setFriction(viewConfig.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
        calculateSnapTargets();
    }

    /**
     * @return the closest absolute snap stack bounds for the given {@param stackBounds} moving at
     * the given {@param velocityX} and {@param velocityY}.  The {@param movementBounds} should be
     * those for the given {@param stackBounds}.
     */
    public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX,
            float velocityY) {
        final Rect finalStackBounds = new Rect(stackBounds);
        mScroller.fling(stackBounds.left, stackBounds.top,
                (int) velocityX, (int) velocityY,
                movementBounds.left, movementBounds.right,
                movementBounds.top, movementBounds.bottom);
        finalStackBounds.offsetTo(mScroller.getFinalX(), mScroller.getFinalY());
        mScroller.abortAnimation();
        return findClosestSnapBounds(movementBounds, finalStackBounds);
    }

    /**
     * @return the closest absolute snap stack bounds for the given {@param stackBounds}.  The
     * {@param movementBounds} should be those for the given {@param stackBounds}.
     */
    public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds) {
        final Rect pipBounds = new Rect(movementBounds.left, movementBounds.top,
                movementBounds.right + stackBounds.width(),
                movementBounds.bottom + stackBounds.height());
        final Rect newBounds = new Rect(stackBounds);
        if (mSnapMode == SNAP_MODE_EDGE) {
            // Find the closest edge to the given stack bounds and snap to it
            final int fromLeft = stackBounds.left - movementBounds.left;
            final int fromTop = stackBounds.top - movementBounds.top;
            final int fromRight = movementBounds.right - stackBounds.left;
            final int fromBottom = movementBounds.bottom - stackBounds.top;
            if (fromLeft <= fromTop && fromLeft <= fromRight && fromLeft <= fromBottom) {
                newBounds.offset(-fromLeft, 0);
            } else if (fromTop <= fromLeft && fromTop <= fromRight && fromTop <= fromBottom) {
                newBounds.offset(0, -fromTop);
            } else if (fromRight < fromLeft && fromRight < fromTop && fromRight < fromBottom) {
                newBounds.offset(fromRight, 0);
            } else {
                newBounds.offset(0, fromBottom);
            }
        } else {
            // Find the closest snap point
            final Rect tmpBounds = new Rect();
            final Point[] snapTargets = new Point[mSnapGravities.size()];
            for (int i = 0; i < mSnapGravities.size(); i++) {
                Gravity.apply(mSnapGravities.get(i), stackBounds.width(), stackBounds.height(),
                        pipBounds, 0, 0, tmpBounds);
                snapTargets[i] = new Point(tmpBounds.left, tmpBounds.top);
            }
            Point snapTarget = findClosestPoint(stackBounds.left, stackBounds.top, snapTargets);
            newBounds.offsetTo(snapTarget.x, snapTarget.y);
        }
        return newBounds;
    }

    /**
     * @return the closest point in {@param points} to the given {@param x} and {@param y}.
     */
    private Point findClosestPoint(int x, int y, Point[] points) {
        Point closestPoint = null;
        float minDistance = Float.MAX_VALUE;
        for (Point p : points) {
            float distance = distanceToPoint(p, x, y);
            if (distance < minDistance) {
                closestPoint = p;
                minDistance = distance;
            }
        }
        return closestPoint;
    }

    /**
     * @return the distance between point {@param p} and the given {@param x} and {@param y}.
     */
    private float distanceToPoint(Point p, int x, int y) {
        return PointF.length(p.x - x, p.y - y);
    }

    /**
     * Calculate the snap targets for the discrete snap modes.
     */
    private void calculateSnapTargets() {
        mSnapGravities.clear();
        switch (mSnapMode) {
            case SNAP_MODE_CORNERS_AND_SIDES:
                if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
                    mSnapGravities.add(Gravity.TOP | Gravity.CENTER_HORIZONTAL);
                    mSnapGravities.add(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
                } else {
                    mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.LEFT);
                    mSnapGravities.add(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
                }
                // Fall through
            case SNAP_MODE_CORNERS_ONLY:
                mSnapGravities.add(Gravity.TOP | Gravity.LEFT);
                mSnapGravities.add(Gravity.TOP | Gravity.RIGHT);
                mSnapGravities.add(Gravity.BOTTOM | Gravity.LEFT);
                mSnapGravities.add(Gravity.BOTTOM | Gravity.RIGHT);
                break;
            default:
                // Skip otherwise
                break;
        }
    }
}
+10 −66
Original line number Diff line number Diff line
@@ -29,14 +29,11 @@ import android.animation.ValueAnimator;
import android.app.ActivityManager.StackInfo;
import android.app.IActivityManager;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import android.view.Display;
import android.view.IWindowManager;
import android.view.InputChannel;
import android.view.InputEvent;
@@ -45,9 +42,9 @@ import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.view.animation.Interpolator;
import android.widget.Scroller;

import com.android.internal.os.BackgroundThread;
import com.android.internal.policy.PipSnapAlgorithm;
import com.android.systemui.statusbar.FlingAnimationUtils;

/**
@@ -62,8 +59,6 @@ public class PipTouchHandler {
    private static final int DISMISS_STACK_DURATION = 375;
    private static final int EXPAND_STACK_DURATION = 225;

    private static final float SCROLL_FRICTION_MULTIPLIER = 8f;

    private final Context mContext;
    private final IActivityManager mActivityManager;
    private final ViewConfiguration mViewConfig;
@@ -71,6 +66,7 @@ public class PipTouchHandler {

    private final PipInputEventReceiver mInputEventReceiver;
    private final PipDismissViewController mDismissViewController;
    private PipSnapAlgorithm mSnapAlgorithm;

    private final Rect mPinnedStackBounds = new Rect();
    private final Rect mBoundedPinnedStackBounds = new Rect();
@@ -82,7 +78,6 @@ public class PipTouchHandler {
    private int mActivePointerId;

    private final FlingAnimationUtils mFlingAnimationUtils;
    private final Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    /**
@@ -123,8 +118,6 @@ public class PipTouchHandler {
        mViewConfig = ViewConfiguration.get(context);
        mInputEventReceiver = new PipInputEventReceiver(mInputChannel, Looper.myLooper());
        mDismissViewController = new PipDismissViewController(context);
        mScroller = new Scroller(context);
        mScroller.setFriction(mViewConfig.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
        mFlingAnimationUtils = new FlingAnimationUtils(context, 2f);
    }

@@ -218,7 +211,7 @@ public class PipTouchHandler {
                        if (dismissBounds.contains(x, y)) {
                            animateDismissPinnedStack(dismissBounds);
                        } else {
                            animateToSnapTarget();
                            animateToClosestSnapTarget();
                        }
                    }
                } else {
@@ -255,13 +248,8 @@ public class PipTouchHandler {
     * Creates an animation that continues the fling to a snap target.
     */
    private void flingToSnapTarget(float velocity, float velocityX, float velocityY) {
        mScroller.fling(mPinnedStackBounds.left, mPinnedStackBounds.top,
            (int) velocityX, (int) velocityY,
            mBoundedPinnedStackBounds.left, mBoundedPinnedStackBounds.right,
            mBoundedPinnedStackBounds.top, mBoundedPinnedStackBounds.bottom);
        Rect toBounds = findClosestBoundedPinnedStackSnapTarget(
            mScroller.getFinalX(), mScroller.getFinalY());
        mScroller.abortAnimation();
        Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds,
                mPinnedStackBounds, velocityX, velocityY);
        if (!mPinnedStackBounds.equals(toBounds)) {
            mPinnedStackBoundsAnimator = createResizePinnedStackAnimation(
                toBounds, 0, FAST_OUT_SLOW_IN);
@@ -275,9 +263,9 @@ public class PipTouchHandler {
    /**
     * Animates the pinned stack to the closest snap target.
     */
    private void animateToSnapTarget() {
        Rect toBounds = findClosestBoundedPinnedStackSnapTarget(
            mPinnedStackBounds.left, mPinnedStackBounds.top);
    private void animateToClosestSnapTarget() {
        Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds,
                mPinnedStackBounds);
        if (!mPinnedStackBounds.equals(toBounds)) {
            mPinnedStackBoundsAnimator = createResizePinnedStackAnimation(
                toBounds, SNAP_STACK_DURATION, FAST_OUT_SLOW_IN);
@@ -334,7 +322,8 @@ public class PipTouchHandler {
            if (info != null) {
                mPinnedStackBounds.set(info.bounds);
                mBoundedPinnedStackBounds.set(mActivityManager.getPictureInPictureMovementBounds(
                        Display.DEFAULT_DISPLAY));
                        info.displayId));
                mSnapAlgorithm = new PipSnapAlgorithm(mContext, info.displayId);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Could not fetch PIP movement bounds.", e);
@@ -394,51 +383,6 @@ public class PipTouchHandler {
        return anim;
    }

    /**
     * @return the closest absolute bounded stack left/top to the given {@param x} and {@param y}.
     */
    private Rect findClosestBoundedPinnedStackSnapTarget(int x, int y) {
        Point[] snapTargets;
        int orientation = mContext.getResources().getConfiguration().orientation;
        if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
            snapTargets = new Point[] {
                new Point(mBoundedPinnedStackBounds.left, mBoundedPinnedStackBounds.top),
                new Point(mBoundedPinnedStackBounds.right, mBoundedPinnedStackBounds.top),
                new Point(mBoundedPinnedStackBounds.left, mBoundedPinnedStackBounds.bottom),
                new Point(mBoundedPinnedStackBounds.right, mBoundedPinnedStackBounds.bottom)
            };
        } else {
            snapTargets = new Point[] {
                new Point(mBoundedPinnedStackBounds.left, mBoundedPinnedStackBounds.top),
                new Point(mBoundedPinnedStackBounds.right, mBoundedPinnedStackBounds.top),
                new Point(mBoundedPinnedStackBounds.left, mBoundedPinnedStackBounds.bottom),
                new Point(mBoundedPinnedStackBounds.right, mBoundedPinnedStackBounds.bottom)
            };
        }

        Point closestSnapTarget = null;
        float minDistance = Float.MAX_VALUE;
        for (Point p : snapTargets) {
            float distance = distanceToPoint(p, x, y);
            if (distance < minDistance) {
                closestSnapTarget = p;
                minDistance = distance;
            }
        }

        Rect toBounds = new Rect(mPinnedStackBounds);
        toBounds.offsetTo(closestSnapTarget.x, closestSnapTarget.y);
        return toBounds;
    }

    /**
     * @return the distance between point {@param p} and the given {@param x} and {@param y}.
     */
    private float distanceToPoint(Point p, int x, int y) {
        return PointF.length(p.x - x, p.y - y);
    }


    /**
     * @return the distance between points {@param p1} and {@param p2}.
     */
+39 −13
Original line number Diff line number Diff line
@@ -39,7 +39,10 @@ import static com.android.server.wm.WindowManagerService.H.RESIZE_STACK;
import static com.android.server.wm.WindowManagerService.LAYER_OFFSET_DIM;

import android.app.ActivityManager.StackId;
import android.app.IActivityManager;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Debug;
@@ -54,6 +57,7 @@ import android.view.WindowManagerPolicy;
import com.android.internal.policy.DividerSnapAlgorithm;
import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
import com.android.internal.policy.DockedDividerUtils;
import com.android.internal.policy.PipSnapAlgorithm;
import com.android.server.EventLogTags;

import java.io.PrintWriter;
@@ -381,7 +385,28 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye

        mTmpRect2.set(mBounds);
        mDisplayContent.rotateBounds(mRotation, newRotation, mTmpRect2);
        if (mStackId == DOCKED_STACK_ID) {
        switch (mStackId) {
            case PINNED_STACK_ID:
                // Keep the pinned stack in the same aspect ratio as in the old orientation, but
                // move it into the position in the rotated space, and snap to the closest space
                // in the new orientation.

                try {
                    final IActivityManager am = mService.mActivityManager;
                    final Rect movementBounds = am.getPictureInPictureMovementBounds(
                            mDisplayContent.getDisplayId());
                    final int width = mBounds.width();
                    final int height = mBounds.height();
                    final int left = mTmpRect2.centerX() - (width / 2);
                    final int top = mTmpRect2.centerY() - (height / 2);
                    mTmpRect2.set(left, top, left + width, top + height);

                    final PipSnapAlgorithm snapAlgorithm = new PipSnapAlgorithm(mService.mContext,
                            mDisplayContent.getDisplayId());
                    mTmpRect2.set(snapAlgorithm.findClosestSnapBounds(movementBounds, mTmpRect2));
                } catch (RemoteException e) {}
                break;
            case DOCKED_STACK_ID:
                repositionDockedStackAfterRotation(mTmpRect2);
                snapDockedStackAfterRotation(mTmpRect2);
                final int newDockSide = getDockSide(mTmpRect2);
@@ -394,6 +419,7 @@ public class TaskStack extends WindowContainer<Task> implements DimLayer.DimLaye
                                : DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT,
                        null);
                mDisplayContent.getDockedDividerController().notifyDockSideChanged(newDockSide);
                break;
        }

        mBoundsAfterRotation.set(mTmpRect2);