All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.wm.shell.bubbles.BubblePositioner Maven / Gradle / Ivy

/*
 * Copyright (C) 2020 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.bubbles;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.annotation.IntDef;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;
import android.view.Surface;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;

import androidx.annotation.VisibleForTesting;

import com.android.wm.shell.R;

import java.lang.annotation.Retention;

/**
 * Keeps track of display size, configuration, and specific bubble sizes. One place for all
 * placement and positioning calculations to refer to.
 */
public class BubblePositioner {
    private static final String TAG = BubbleDebugConfig.TAG_WITH_CLASS_NAME
            ? "BubblePositioner"
            : BubbleDebugConfig.TAG_BUBBLES;

    @Retention(SOURCE)
    @IntDef({TASKBAR_POSITION_NONE, TASKBAR_POSITION_RIGHT, TASKBAR_POSITION_LEFT,
            TASKBAR_POSITION_BOTTOM})
    @interface TaskbarPosition {}
    public static final int TASKBAR_POSITION_NONE = -1;
    public static final int TASKBAR_POSITION_RIGHT = 0;
    public static final int TASKBAR_POSITION_LEFT = 1;
    public static final int TASKBAR_POSITION_BOTTOM = 2;

    /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/
    public static final int NUM_VISIBLE_WHEN_RESTING = 2;

    private Context mContext;
    private WindowManager mWindowManager;
    private Rect mPositionRect;
    private @Surface.Rotation int mRotation = Surface.ROTATION_0;
    private Insets mInsets;
    private int mDefaultMaxBubbles;
    private int mMaxBubbles;

    private int mBubbleSize;
    private int mBubbleBadgeSize;
    private int mSpacingBetweenBubbles;
    private int mExpandedViewLargeScreenWidth;
    private int mExpandedViewPadding;
    private int mPointerMargin;
    private float mPointerWidth;
    private float mPointerHeight;

    private PointF mPinLocation;
    private PointF mRestingStackPosition;
    private int[] mPaddings = new int[4];

    private boolean mIsLargeScreen;
    private boolean mShowingInTaskbar;
    private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE;
    private int mTaskbarIconSize;
    private int mTaskbarSize;

    public BubblePositioner(Context context, WindowManager windowManager) {
        mContext = context;
        mWindowManager = windowManager;
        update();
    }

    public void setRotation(int rotation) {
        mRotation = rotation;
    }

    /**
     * Available space and inset information. Call this when config changes
     * occur or when added to a window.
     */
    public void update() {
        WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
        if (windowMetrics == null) {
            return;
        }
        WindowInsets metricInsets = windowMetrics.getWindowInsets();
        Insets insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
                | WindowInsets.Type.statusBars()
                | WindowInsets.Type.displayCutout());

        mIsLargeScreen = mContext.getResources().getConfiguration().smallestScreenWidthDp >= 600;

        if (BubbleDebugConfig.DEBUG_POSITIONER) {
            Log.w(TAG, "update positioner:"
                    + " rotation: " + mRotation
                    + " insets: " + insets
                    + " isLargeScreen: " + mIsLargeScreen
                    + " bounds: " + windowMetrics.getBounds()
                    + " showingInTaskbar: " + mShowingInTaskbar);
        }
        updateInternal(mRotation, insets, windowMetrics.getBounds());
    }

    /**
     * Updates position information to account for taskbar state.
     *
     * @param taskbarPosition which position the taskbar is displayed in.
     * @param showingInTaskbar whether the taskbar is being shown.
     */
    public void updateForTaskbar(int iconSize,
            @TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize) {
        mShowingInTaskbar = showingInTaskbar;
        mTaskbarIconSize =  iconSize;
        mTaskbarPosition = taskbarPosition;
        mTaskbarSize = taskbarSize;
        update();
    }

    @VisibleForTesting
    public void updateInternal(int rotation, Insets insets, Rect bounds) {
        mRotation = rotation;
        mInsets = insets;

        mPositionRect = new Rect(bounds);
        mPositionRect.left += mInsets.left;
        mPositionRect.top += mInsets.top;
        mPositionRect.right -= mInsets.right;
        mPositionRect.bottom -= mInsets.bottom;

        Resources res = mContext.getResources();
        mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
        mBubbleBadgeSize = res.getDimensionPixelSize(R.dimen.bubble_badge_size);
        mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
        mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);

        mExpandedViewLargeScreenWidth = res.getDimensionPixelSize(
                R.dimen.bubble_expanded_view_tablet_width);
        mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
        mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
        mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
        mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);

        mMaxBubbles = calculateMaxBubbles();

        if (mShowingInTaskbar) {
            adjustForTaskbar();
        }
    }

    /**
     * @return the maximum number of bubbles that can fit on the screen when expanded. If the
     * screen size / screen density is too small to support the default maximum number, then
     * the number will be adjust to something lower to ensure everything is presented nicely.
     */
    private int calculateMaxBubbles() {
        // Use the shortest edge.
        // In portrait the bubbles should align with the expanded view so subtract its padding.
        // We always show the overflow so subtract one bubble size.
        int padding = showBubblesVertically() ? 0 : (mExpandedViewPadding * 2);
        int availableSpace = Math.min(mPositionRect.width(), mPositionRect.height())
                - padding
                - mBubbleSize;
        // Each of the bubbles have spacing because the overflow is at the end.
        int howManyFit = availableSpace / (mBubbleSize + mSpacingBetweenBubbles);
        if (howManyFit < mDefaultMaxBubbles) {
            // Not enough space for the default.
            return howManyFit;
        }
        return mDefaultMaxBubbles;
    }

    /**
     * Taskbar insets appear as navigationBar insets, however, unlike navigationBar this should
     * not inset bubbles UI as bubbles floats above the taskbar. This adjust the available space
     * and insets to account for the taskbar.
     */
    // TODO(b/171559950): When the insets are reported correctly we can remove this logic
    private void adjustForTaskbar() {
        // When bar is showing on edges... subtract that inset because we appear on top
        if (mShowingInTaskbar && mTaskbarPosition != TASKBAR_POSITION_BOTTOM) {
            WindowInsets metricInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
            Insets navBarInsets = metricInsets.getInsetsIgnoringVisibility(
                    WindowInsets.Type.navigationBars());
            int newInsetLeft = mInsets.left;
            int newInsetRight = mInsets.right;
            if (mTaskbarPosition == TASKBAR_POSITION_LEFT) {
                mPositionRect.left -= navBarInsets.left;
                newInsetLeft -= navBarInsets.left;
            } else if (mTaskbarPosition == TASKBAR_POSITION_RIGHT) {
                mPositionRect.right += navBarInsets.right;
                newInsetRight -= navBarInsets.right;
            }
            mInsets = Insets.of(newInsetLeft, mInsets.top, newInsetRight, mInsets.bottom);
        }
    }

    /**
     * @return a rect of available screen space accounting for orientation, system bars and cutouts.
     * Does not account for IME.
     */
    public Rect getAvailableRect() {
        return mPositionRect;
    }

    /**
     * @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its
     * inset is not included here.
     */
    public Insets getInsets() {
        return mInsets;
    }

    /** @return whether the device is in landscape orientation. */
    public boolean isLandscape() {
        return mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270;
    }

    /** @return whether the screen is considered large. */
    public boolean isLargeScreen() {
        return mIsLargeScreen;
    }

    /**
     * Indicates how bubbles appear when expanded.
     *
     * When false, bubbles display at the top of the screen with the expanded view
     * below them. When true, bubbles display at the edges of the screen with the expanded view
     * to the left or right side.
     */
    public boolean showBubblesVertically() {
        return isLandscape() || mShowingInTaskbar || mIsLargeScreen;
    }

    /** Size of the bubble. */
    public int getBubbleSize() {
        return (mShowingInTaskbar && mTaskbarIconSize > 0)
                ? mTaskbarIconSize
                : mBubbleSize;
    }

    /** The maximum number of bubbles that can be displayed comfortably on screen. */
    public int getMaxBubbles() {
        return mMaxBubbles;
    }

    /**
     * Calculates the left & right padding for the bubble expanded view.
     *
     * On larger screens the width of the expanded view is restricted via this padding.
     * On landscape the bubble overflow expanded view is also restricted via this padding.
     */
    public int[] getExpandedViewPadding(boolean onLeft, boolean isOverflow) {
        int leftPadding = mInsets.left + mExpandedViewPadding;
        int rightPadding = mInsets.right + mExpandedViewPadding;
        final boolean isLargeOrOverflow = mIsLargeScreen || isOverflow;
        if (showBubblesVertically()) {
            if (!onLeft) {
                rightPadding += mBubbleSize - mPointerHeight;
                leftPadding += isLargeOrOverflow
                        ? (mPositionRect.width() - rightPadding - mExpandedViewLargeScreenWidth)
                        : 0;
            } else {
                leftPadding += mBubbleSize - mPointerHeight;
                rightPadding += isLargeOrOverflow
                        ? (mPositionRect.width() - leftPadding - mExpandedViewLargeScreenWidth)
                        : 0;
            }
        }
        // [left, top, right, bottom]
        mPaddings[0] = leftPadding;
        mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin;
        mPaddings[2] = rightPadding;
        mPaddings[3] = 0;
        return mPaddings;
    }

    /** Calculates the y position of the expanded view when it is expanded. */
    public float getExpandedViewY() {
        final int top = getAvailableRect().top;
        if (showBubblesVertically()) {
            return top - mPointerWidth;
        } else {
            return top + mBubbleSize + mPointerMargin;
        }
    }

    /**
     * Sets the stack's most recent position along the edge of the screen. This is saved when the
     * last bubble is removed, so that the stack can be restored in its previous position.
     */
    public void setRestingPosition(PointF position) {
        if (mRestingStackPosition == null) {
            mRestingStackPosition = new PointF(position);
        } else {
            mRestingStackPosition.set(position);
        }
    }

    /** The position the bubble stack should rest at when collapsed. */
    public PointF getRestingPosition() {
        if (mPinLocation != null) {
            return mPinLocation;
        }
        if (mRestingStackPosition == null) {
            return getDefaultStartPosition();
        }
        return mRestingStackPosition;
    }

    /**
     * @return the stack position to use if we don't have a saved location or if user education
     * is being shown.
     */
    public PointF getDefaultStartPosition() {
        // Start on the left if we're in LTR, right otherwise.
        final boolean startOnLeft =
                mContext.getResources().getConfiguration().getLayoutDirection()
                        != View.LAYOUT_DIRECTION_RTL;
        final float startingVerticalOffset = mContext.getResources().getDimensionPixelOffset(
                R.dimen.bubble_stack_starting_offset_y);
        // TODO: placement bug here because mPositionRect doesn't handle the overhanging edge
        return new BubbleStackView.RelativeStackPosition(
                startOnLeft,
                startingVerticalOffset / mPositionRect.height())
                .getAbsolutePositionInRegion(new RectF(mPositionRect));
    }

    /**
     * @return whether the bubble stack is pinned to the taskbar.
     */
    public boolean showingInTaskbar() {
        return mShowingInTaskbar;
    }

    /**
     * @return the taskbar position if set.
     */
    public int getTaskbarPosition() {
        return mTaskbarPosition;
    }

    public int getTaskbarSize() {
        return mTaskbarSize;
    }

    /**
     * In some situations bubbles will be pinned to a specific onscreen location. This sets the
     * location to anchor the stack to.
     */
    public void setPinnedLocation(PointF point) {
        mPinLocation = point;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy