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

Commit 29aee534 authored by Hongwei Wang's avatar Hongwei Wang Committed by Android (Google) Code Review
Browse files

Merge "Replicates bounds calculation to SysUI package"

parents 6290916b 37568ad4
Loading
Loading
Loading
Loading
+375 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.systemui.pip;

import static android.util.TypedValue.COMPLEX_UNIT_DIP;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.RemoteException;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Size;
import android.util.TypedValue;
import android.view.DisplayInfo;
import android.view.Gravity;
import android.view.IPinnedStackController;
import android.view.IWindowManager;
import android.view.WindowManagerGlobal;

import com.android.internal.policy.PipSnapAlgorithm;

import java.io.PrintWriter;

/**
 * Handles bounds calculation for PIP on Phone and other form factors, it keeps tracking variant
 * state changes originated from Window Manager and is the source of truth for PiP window bounds.
 */
public class PipBoundsHandler {

    private static final String TAG = PipBoundsHandler.class.getSimpleName();
    private static final float INVALID_SNAP_FRACTION = -1f;

    // System.identityHashCode guarantees zero for null object.
    private static final int INVALID_SYSTEM_IDENTITY_TOKEN = 0;

    private final Context mContext;
    private final IWindowManager mWindowManager;
    private final PipSnapAlgorithm mSnapAlgorithm;
    private final DisplayInfo mDisplayInfo = new DisplayInfo();
    private final Rect mStableInsets = new Rect();
    private final Rect mTmpInsets = new Rect();
    private final Point mTmpDisplaySize = new Point();

    private IPinnedStackController mPinnedStackController;
    private int mLastPipToken;
    private float mReentrySnapFraction = INVALID_SNAP_FRACTION;

    private float mDefaultAspectRatio;
    private float mMinAspectRatio;
    private float mMaxAspectRatio;
    private float mAspectRatio;
    private int mDefaultStackGravity;
    private int mDefaultMinSize;
    private Point mScreenEdgeInsets;
    private int mCurrentMinSize;

    private boolean mIsMinimized;
    private boolean mIsImeShowing;
    private int mImeHeight;
    private boolean mIsShelfShowing;
    private int mShelfHeight;

    public PipBoundsHandler(Context context) {
        mContext = context;
        mSnapAlgorithm = new PipSnapAlgorithm(context);
        mWindowManager = WindowManagerGlobal.getWindowManagerService();
        mAspectRatio = mDefaultAspectRatio;
        reloadResources();
    }

    /**
     * TODO: move the resources to SysUI package.
     */
    private void reloadResources() {
        final Resources res = mContext.getResources();
        mDefaultAspectRatio = res.getFloat(
                com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio);
        mDefaultStackGravity = res.getInteger(
                com.android.internal.R.integer.config_defaultPictureInPictureGravity);
        mDefaultMinSize = res.getDimensionPixelSize(
                com.android.internal.R.dimen.default_minimal_size_pip_resizable_task);
        mCurrentMinSize = mDefaultMinSize;
        final String screenEdgeInsetsDpString = res.getString(
                com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets);
        final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
                ? Size.parseSize(screenEdgeInsetsDpString)
                : null;
        mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point()
                : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), res.getDisplayMetrics()),
                        dpToPx(screenEdgeInsetsDp.getHeight(), res.getDisplayMetrics()));
        mMinAspectRatio = res.getFloat(
                com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
        mMaxAspectRatio = res.getFloat(
                com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
    }

    public void setPinnedStackController(IPinnedStackController controller) {
        mPinnedStackController = controller;
    }

    public void setMinEdgeSize(int minEdgeSize) {
        mCurrentMinSize = minEdgeSize;
    }

    /**
     * Responds to IPinnedStackListener on IME visibility change.
     */
    public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
        mIsImeShowing = imeVisible;
        mImeHeight = imeHeight;
    }

    /**
     * Responds to IPinnedStackListener on shelf visibility change.
     */
    public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) {
        mIsShelfShowing = shelfVisible;
        mShelfHeight = shelfHeight;
    }

    /**
     * Responds to IPinnedStackListener on minimized state change.
     */
    public void onMinimizedStateChanged(boolean minimized) {
        mIsMinimized = minimized;
    }

    /**
     * Responds to IPinnedStackListener on movement bounds change.
     * Note that both inset and normal bounds will be calculated here rather than in the caller.
     */
    public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds,
            Rect animatingBounds, DisplayInfo displayInfo) {
        getInsetBounds(insetBounds);
        final Rect defaultBounds = getDefaultBounds(INVALID_SNAP_FRACTION);
        normalBounds.set(defaultBounds);
        if (animatingBounds.isEmpty()) {
            animatingBounds.set(defaultBounds);
        }
        if (isValidPictureInPictureAspectRatio(mAspectRatio)) {
            transformBoundsToAspectRatio(normalBounds, mAspectRatio,
                    false /* useCurrentMinEdgeSize */);
        }
        displayInfo.copyFrom(mDisplayInfo);
    }

    /**
     * Responds to IPinnedStackListener on saving reentry snap fraction for a given token.
     * Token should be generated via {@link System#identityHashCode(Object)}
     */
    public void onSaveReentrySnapFraction(int token, Rect stackBounds) {
        mReentrySnapFraction = getSnapFraction(stackBounds);
        mLastPipToken = token;
    }

    /**
     * Responds to IPinnedStackListener on resetting reentry snap fraction for a given token.
     * Token should be generated via {@link System#identityHashCode(Object)}
     */
    public void onResetReentrySnapFraction(int token) {
        if (mLastPipToken == token) {
            onResetReentrySnapFractionUnchecked();
        }
    }

    private void onResetReentrySnapFractionUnchecked() {
        mReentrySnapFraction = INVALID_SNAP_FRACTION;
        mLastPipToken = INVALID_SYSTEM_IDENTITY_TOKEN;
    }

    /**
     * Responds to IPinnedStackListener on {@link DisplayInfo} change.
     * It will normally follow up with a
     * {@link #onMovementBoundsChanged(Rect, Rect, Rect, DisplayInfo)} callback.
     */
    public void onDisplayInfoChanged(DisplayInfo displayInfo) {
        mDisplayInfo.copyFrom(displayInfo);
    }

    /**
     * Responds to IPinnedStackListener on configuration change.
     */
    public void onConfigurationChanged() {
        reloadResources();
    }

    /**
     * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window.
     * It will normally follow up with a
     * {@link #onMovementBoundsChanged(Rect, Rect, Rect, DisplayInfo)} callback.
     */
    public void onAspectRatioChanged(float aspectRatio) {
        mAspectRatio = aspectRatio;
    }

    /**
     * Responds to IPinnedStackListener on preparing the pinned stack animation.
     */
    public void onPrepareAnimation(Rect sourceRectHint, float aspectRatio, Rect stackBounds) {
        final Rect targetStackBounds;
        if (stackBounds == null) {
            targetStackBounds = getDefaultBounds(mReentrySnapFraction);
        } else {
            targetStackBounds = new Rect();
            targetStackBounds.set(stackBounds);
        }
        if (isValidPictureInPictureAspectRatio(aspectRatio)) {
            transformBoundsToAspectRatio(targetStackBounds, aspectRatio,
                    true /* useCurrentMinEdgeSize */);
        }
        if (targetStackBounds.equals(stackBounds)) {
            return;
        }
        mAspectRatio = aspectRatio;
        onResetReentrySnapFractionUnchecked();
        // TODO: callback Window Manager on starting animation with calculated bounds
    }

    /**
     * @return whether the given {@param aspectRatio} is valid.
     */
    private boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
        return Float.compare(mMinAspectRatio, aspectRatio) <= 0
                && Float.compare(aspectRatio, mMaxAspectRatio) <= 0;
    }

    /**
     * Set the current bounds (or the default bounds if there are no current bounds) with the
     * specified aspect ratio.
     */
    private void transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio,
            boolean useCurrentMinEdgeSize) {
        // Save the snap fraction, calculate the aspect ratio based on screen size
        final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
                getMovementBounds(stackBounds));

        final int minEdgeSize = useCurrentMinEdgeSize ? mCurrentMinSize : mDefaultMinSize;
        final Size size = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, minEdgeSize,
                mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
        final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
        final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f);
        stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight());
        mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
        if (mIsMinimized) {
            applyMinimizedOffset(stackBounds, getMovementBounds(stackBounds));
        }
    }

    /**
     * @return the default bounds to show the PIP, if a {@param snapFraction} is provided, then it
     * will apply the default bounds to the provided snap fraction.
     */
    private Rect getDefaultBounds(float snapFraction) {
        final Rect insetBounds = new Rect();
        getInsetBounds(insetBounds);

        final Rect defaultBounds = new Rect();
        final Size size = mSnapAlgorithm.getSizeForAspectRatio(mDefaultAspectRatio,
                mDefaultMinSize, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
        if (snapFraction != INVALID_SNAP_FRACTION) {
            defaultBounds.set(0, 0, size.getWidth(), size.getHeight());
            final Rect movementBounds = getMovementBounds(defaultBounds);
            mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction);
        } else {
            Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds,
                    0, Math.max(mIsImeShowing ? mImeHeight : 0,
                            mIsShelfShowing ? mShelfHeight : 0),
                    defaultBounds);
        }
        return defaultBounds;
    }

    /**
     * Populates the bounds on the screen that the PIP can be visible in.
     */
    private void getInsetBounds(Rect outRect) {
        try {
            mWindowManager.getStableInsets(mContext.getDisplayId(), mTmpInsets);
            outRect.set(mTmpInsets.left + mScreenEdgeInsets.x,
                    mTmpInsets.top + mScreenEdgeInsets.y,
                    mDisplayInfo.logicalWidth - mTmpInsets.right - mScreenEdgeInsets.x,
                    mDisplayInfo.logicalHeight - mTmpInsets.bottom - mScreenEdgeInsets.y);
        } catch (RemoteException e) {
            Log.e(TAG, "Failed to get stable insets from WM", e);
        }
    }

    /**
     * @return the movement bounds for the given {@param stackBounds} and the current state of the
     *         controller.
     */
    private Rect getMovementBounds(Rect stackBounds) {
        return getMovementBounds(stackBounds, true /* adjustForIme */);
    }

    /**
     * @return the movement bounds for the given {@param stackBounds} and the current state of the
     *         controller.
     */
    private Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) {
        final Rect movementBounds = new Rect();
        getInsetBounds(movementBounds);

        // Apply the movement bounds adjustments based on the current state.
        mSnapAlgorithm.getMovementBounds(stackBounds, movementBounds, movementBounds,
                (adjustForIme && mIsImeShowing) ? mImeHeight : 0);
        return movementBounds;
    }

    /**
     * Applies the minimized offsets to the given stack bounds.
     */
    private void applyMinimizedOffset(Rect stackBounds, Rect movementBounds) {
        mTmpDisplaySize.set(mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
        try {
            mWindowManager.getStableInsets(mContext.getDisplayId(), mStableInsets);
            mSnapAlgorithm.applyMinimizedOffset(stackBounds, movementBounds, mTmpDisplaySize,
                    mStableInsets);
        } catch (RemoteException e) {
            Log.e(TAG, "Failed to get stable insets from WM", e);
        }
    }

    /**
     * @return the default snap fraction to apply instead of the default gravity when calculating
     *         the default stack bounds when first entering PiP.
     */
    private float getSnapFraction(Rect stackBounds) {
        return mSnapAlgorithm.getSnapFraction(stackBounds, getMovementBounds(stackBounds));
    }

    /**
     * @return the pixels for a given dp value.
     */
    private int dpToPx(float dpValue, DisplayMetrics dm) {
        return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
    }

    /**
     * Dumps internal states.
     */
    public void dump(PrintWriter pw, String prefix) {
        final String innerPrefix = prefix + "  ";
        pw.println(prefix + TAG);
        pw.println(innerPrefix + "mReentrySnapFraction=" + mReentrySnapFraction);
        pw.println(innerPrefix + "mDisplayInfo=" + mDisplayInfo);
        pw.println(innerPrefix + "mDefaultAspectRatio=" + mDefaultAspectRatio);
        pw.println(innerPrefix + "mMinAspectRatio=" + mMinAspectRatio);
        pw.println(innerPrefix + "mMaxAspectRatio=" + mMaxAspectRatio);
        pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio);
        pw.println(innerPrefix + "mDefaultStackGravity=" + mDefaultStackGravity);
        pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized);
        pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
        pw.println(innerPrefix + "mImeHeight=" + mImeHeight);
        pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing);
        pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight);
        mSnapAlgorithm.dump(pw, innerPrefix);
    }
}