/*
 * 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 android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT;

import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.CornerPathEffect;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Picture;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import androidx.annotation.Nullable;

import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.R;
import com.android.wm.shell.TaskView;
import com.android.wm.shell.common.AlphaOptimizedButton;
import com.android.wm.shell.common.TriangleShape;

import java.io.FileDescriptor;
import java.io.PrintWriter;

/**
 * Container for the expanded bubble view, handles rendering the caret and settings icon.
 */
public class BubbleExpandedView extends LinearLayout {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES;

    // The triangle pointing to the expanded view
    private View mPointerView;
    @Nullable private int[] mExpandedViewContainerLocation;

    private AlphaOptimizedButton mManageButton;
    private TaskView mTaskView;
    private BubbleOverflowContainerView mOverflowView;

    private int mTaskId = INVALID_TASK_ID;

    private boolean mImeVisible;
    private boolean mNeedsNewHeight;

    /**
     * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If
     * {@link #mIsAlphaAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
     * value until the animation ends.
     */
    private boolean mIsContentVisible = false;

    /**
     * Whether we're animating the {@code TaskView}'s alpha value. If so, we will hold off on
     * applying alpha changes from {@link #setContentVisibility} until the animation ends.
     */
    private boolean mIsAlphaAnimating = false;

    private int mPointerWidth;
    private int mPointerHeight;
    private float mPointerRadius;
    private float mPointerOverlap;
    private CornerPathEffect mPointerEffect;
    private ShapeDrawable mCurrentPointer;
    private ShapeDrawable mTopPointer;
    private ShapeDrawable mLeftPointer;
    private ShapeDrawable mRightPointer;
    private float mCornerRadius = 0f;
    private int mBackgroundColorFloating;

    @Nullable private Bubble mBubble;
    private PendingIntent mPendingIntent;
    // TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead
    private boolean mIsOverflow;

    private BubbleController mController;
    private BubbleStackView mStackView;
    private BubblePositioner mPositioner;

    /**
     * Container for the {@code TaskView} that has a solid, round-rect background that shows if the
     * {@code TaskView} hasn't loaded.
     */
    private final FrameLayout mExpandedViewContainer = new FrameLayout(getContext());

    private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
        private boolean mInitialized = false;
        private boolean mDestroyed = false;

        @Override
        public void onInitialized() {
            if (DEBUG_BUBBLE_EXPANDED_VIEW) {
                Log.d(TAG, "onInitialized: destroyed=" + mDestroyed
                        + " initialized=" + mInitialized
                        + " bubble=" + getBubbleKey());
            }

            if (mDestroyed || mInitialized) {
                return;
            }

            // Custom options so there is no activity transition animation
            ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
                    0 /* enterResId */, 0 /* exitResId */);

            Rect launchBounds = new Rect();
            mTaskView.getBoundsOnScreen(launchBounds);

            // TODO: I notice inconsistencies in lifecycle
            // Post to keep the lifecycle normal
            post(() -> {
                if (DEBUG_BUBBLE_EXPANDED_VIEW) {
                    Log.d(TAG, "onInitialized: calling startActivity, bubble="
                            + getBubbleKey());
                }
                try {
                    options.setTaskAlwaysOnTop(true);
                    options.setLaunchedFromBubble(true);
                    if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
                        options.setApplyActivityFlagsForBubbles(true);
                        mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
                                options, launchBounds);
                    } else {
                        Intent fillInIntent = new Intent();
                        // Apply flags to make behaviour match documentLaunchMode=always.
                        fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
                        fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                        if (mBubble != null) {
                            mBubble.setIntentActive();
                        }
                        mTaskView.startActivity(mPendingIntent, fillInIntent, options,
                                launchBounds);
                    }
                } catch (RuntimeException e) {
                    // If there's a runtime exception here then there's something
                    // wrong with the intent, we can't really recover / try to populate
                    // the bubble again so we'll just remove it.
                    Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
                            + ", " + e.getMessage() + "; removing bubble");
                    mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT);
                }
            });
            mInitialized = true;
        }

        @Override
        public void onReleased() {
            mDestroyed = true;
        }

        @Override
        public void onTaskCreated(int taskId, ComponentName name) {
            if (DEBUG_BUBBLE_EXPANDED_VIEW) {
                Log.d(TAG, "onTaskCreated: taskId=" + taskId
                        + " bubble=" + getBubbleKey());
            }
            // The taskId is saved to use for removeTask, preventing appearance in recent tasks.
            mTaskId = taskId;

            // With the task org, the taskAppeared callback will only happen once the task has
            // already drawn
            setContentVisibility(true);
        }

        @Override
        public void onTaskVisibilityChanged(int taskId, boolean visible) {
            setContentVisibility(visible);
        }

        @Override
        public void onTaskRemovalStarted(int taskId) {
            if (DEBUG_BUBBLE_EXPANDED_VIEW) {
                Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId
                        + " bubble=" + getBubbleKey());
            }
            if (mBubble != null) {
                // Must post because this is called from a binder thread.
                post(() -> mController.removeBubble(
                        mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED));
            }
        }

        @Override
        public void onBackPressedOnTaskRoot(int taskId) {
            if (mTaskId == taskId && mStackView.isExpanded()) {
                mStackView.onBackPressed();
            }
        }
    };

    public BubbleExpandedView(Context context) {
        this(context, null);
    }

    public BubbleExpandedView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mManageButton = (AlphaOptimizedButton) LayoutInflater.from(getContext()).inflate(
                R.layout.bubble_manage_button, this /* parent */, false /* attach */);
        updateDimensions();
        mPointerView = findViewById(R.id.pointer_view);
        mCurrentPointer = mTopPointer;
        mPointerView.setVisibility(INVISIBLE);

        // Set {@code TaskView}'s alpha value as zero, since there is no view content to be shown.
        setContentVisibility(false);

        mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() {
            @Override
            public void getOutline(View view, Outline outline) {
                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
            }
        });
        mExpandedViewContainer.setClipToOutline(true);
        mExpandedViewContainer.setLayoutParams(
                new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
        addView(mExpandedViewContainer);

        // Expanded stack layout, top to bottom:
        // Expanded view container
        // ==> bubble row
        // ==> expanded view
        //   ==> activity view
        //   ==> manage button
        bringChildToFront(mManageButton);

        applyThemeAttrs();

        setClipToPadding(false);
        setOnTouchListener((view, motionEvent) -> {
            if (mTaskView == null) {
                return false;
            }

            final Rect avBounds = new Rect();
            mTaskView.getBoundsOnScreen(avBounds);

            // Consume and ignore events on the expanded view padding that are within the
            // {@code TaskView}'s vertical bounds. These events are part of a back gesture, and so
            // they should not collapse the stack (which all other touches on areas around the AV
            // would do).
            if (motionEvent.getRawY() >= avBounds.top
                            && motionEvent.getRawY() <= avBounds.bottom
                            && (motionEvent.getRawX() < avBounds.left
                                || motionEvent.getRawX() > avBounds.right)) {
                return true;
            }

            return false;
        });

        // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout
        // so the Manage button appears on the right.
        setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
    }

    /**
     * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need
     * to be called after view inflate.
     */
    void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow) {
        mController = controller;
        mStackView = stackView;
        mIsOverflow = isOverflow;
        mPositioner = mController.getPositioner();

        if (mIsOverflow) {
            mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate(
                    R.layout.bubble_overflow_container, null /* root */);
            mOverflowView.setBubbleController(mController);
            FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
            mExpandedViewContainer.addView(mOverflowView, lp);
            mExpandedViewContainer.setLayoutParams(
                    new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
            bringChildToFront(mOverflowView);
            mManageButton.setVisibility(GONE);
        } else {
            mTaskView = new TaskView(mContext, mController.getTaskOrganizer(),
                    mController.getSyncTransactionQueue());
            mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener);
            mExpandedViewContainer.addView(mTaskView);
            bringChildToFront(mTaskView);
        }
    }

    void updateDimensions() {
        Resources res = getResources();
        updateFontSize();

        mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
        mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
        mPointerRadius = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_radius);
        mPointerEffect = new CornerPathEffect(mPointerRadius);
        mPointerOverlap = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_overlap);
        mTopPointer = new ShapeDrawable(TriangleShape.create(
                mPointerWidth, mPointerHeight, true /* pointUp */));
        mLeftPointer = new ShapeDrawable(TriangleShape.createHorizontal(
                mPointerWidth, mPointerHeight, true /* pointLeft */));
        mRightPointer = new ShapeDrawable(TriangleShape.createHorizontal(
                mPointerWidth, mPointerHeight, false /* pointLeft */));
        if (mPointerView != null) {
            updatePointerView();
        }

        if (mManageButton != null) {
            int visibility = mManageButton.getVisibility();
            removeView(mManageButton);
            mManageButton = (AlphaOptimizedButton) LayoutInflater.from(getContext()).inflate(
                    R.layout.bubble_manage_button, this /* parent */, false /* attach */);
            addView(mManageButton);
            mManageButton.setVisibility(visibility);
        }
    }

    void updateFontSize() {
        final float fontSize = mContext.getResources()
                .getDimensionPixelSize(com.android.internal.R.dimen.text_size_body_2_material);
        if (mManageButton != null) {
            mManageButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
        }
        if (mOverflowView != null) {
            mOverflowView.updateFontSize();
        }
    }

    void applyThemeAttrs() {
        final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
                android.R.attr.dialogCornerRadius,
                android.R.attr.colorBackgroundFloating});
        mCornerRadius = ta.getDimensionPixelSize(0, 0);
        mBackgroundColorFloating = ta.getColor(1, Color.WHITE);
        mExpandedViewContainer.setBackgroundColor(mBackgroundColorFloating);
        ta.recycle();

        if (mTaskView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
                mContext.getResources())) {
            mTaskView.setCornerRadius(mCornerRadius);
        }
        updatePointerView();
    }

    /** Updates the size and visuals of the pointer. **/
    private void updatePointerView() {
        LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams();
        if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) {
            lp.width = mPointerHeight;
            lp.height = mPointerWidth;
        } else {
            lp.width = mPointerWidth;
            lp.height = mPointerHeight;
        }
        mCurrentPointer.setTint(mBackgroundColorFloating);

        Paint arrowPaint = mCurrentPointer.getPaint();
        arrowPaint.setColor(mBackgroundColorFloating);
        arrowPaint.setPathEffect(mPointerEffect);
        mPointerView.setLayoutParams(lp);
        mPointerView.setBackground(mCurrentPointer);
    }

    private String getBubbleKey() {
        return mBubble != null ? mBubble.getKey() : "null";
    }

    /**
     * Sets whether the surface displaying app content should sit on top. This is useful for
     * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble
     * being dragged out, the manage menu) this is set to false, otherwise it should be true.
     */
    void setSurfaceZOrderedOnTop(boolean onTop) {
        if (mTaskView == null) {
            return;
        }
        mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */);
    }

    void setImeVisible(boolean visible) {
        mImeVisible = visible;
        if (!mImeVisible && mNeedsNewHeight) {
            updateHeight();
        }
    }

    /** Return a GraphicBuffer with the contents of the task view surface. */
    @Nullable
    SurfaceControl.ScreenshotHardwareBuffer snapshotActivitySurface() {
        if (mIsOverflow) {
            // For now, just snapshot the view and return it as a hw buffer so that the animation
            // code for both the tasks and overflow can be the same
            Picture p = new Picture();
            mOverflowView.draw(
                    p.beginRecording(mOverflowView.getWidth(), mOverflowView.getHeight()));
            p.endRecording();
            Bitmap snapshot = Bitmap.createBitmap(p);
            return new SurfaceControl.ScreenshotHardwareBuffer(snapshot.getHardwareBuffer(),
                    snapshot.getColorSpace(), false /* containsSecureLayers */);
        }
        if (mTaskView == null || mTaskView.getSurfaceControl() == null) {
            return null;
        }
        return SurfaceControl.captureLayers(
                mTaskView.getSurfaceControl(),
                new Rect(0, 0, mTaskView.getWidth(), mTaskView.getHeight()),
                1 /* scale */);
    }

    int[] getTaskViewLocationOnScreen() {
        if (mIsOverflow) {
            // This is only used for animating away the surface when switching bubbles, just use the
            // view location on screen for now to allow us to use the same animation code with tasks
            return mOverflowView.getLocationOnScreen();
        }
        if (mTaskView != null) {
            return mTaskView.getLocationOnScreen();
        } else {
            return new int[]{0, 0};
        }
    }

    // TODO: Could listener be passed when we pass StackView / can we avoid setting this like this
    void setManageClickListener(OnClickListener manageClickListener) {
        mManageButton.setOnClickListener(manageClickListener);
    }

    /**
     * Updates the obscured touchable region for the task surface. This calls onLocationChanged,
     * which results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is
     * useful if a view has been added or removed from on top of the {@code TaskView}, such as the
     * manage menu.
     */
    void updateObscuredTouchableRegion() {
        if (mTaskView != null) {
            mTaskView.onLocationChanged();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mImeVisible = false;
        mNeedsNewHeight = false;
        if (DEBUG_BUBBLE_EXPANDED_VIEW) {
            Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey());
        }
    }

    /**
     * Whether we are currently animating the {@code TaskView}'s alpha value. If this is set to
     * true, calls to {@link #setContentVisibility} will not be applied until this is set to false
     * again.
     */
    void setAlphaAnimating(boolean animating) {
        mIsAlphaAnimating = animating;

        // If we're done animating, apply the correct
        if (!animating) {
            setContentVisibility(mIsContentVisible);
        }
    }

    /**
     * Sets the alpha of the underlying {@code TaskView}, since changing the expanded view's alpha
     * does not affect the {@code TaskView} since it uses a Surface.
     */
    void setTaskViewAlpha(float alpha) {
        if (mTaskView != null) {
            mTaskView.setAlpha(alpha);
        }
        mPointerView.setAlpha(alpha);
        setAlpha(alpha);
    }

    /**
     * Set visibility of contents in the expanded state.
     *
     * @param visibility {@code true} if the contents should be visible on the screen.
     *
     * Note that this contents visibility doesn't affect visibility at {@link android.view.View},
     * and setting {@code false} actually means rendering the contents in transparent.
     */
    void setContentVisibility(boolean visibility) {
        if (DEBUG_BUBBLE_EXPANDED_VIEW) {
            Log.d(TAG, "setContentVisibility: visibility=" + visibility
                    + " bubble=" + getBubbleKey());
        }
        mIsContentVisible = visibility;
        if (mTaskView != null && !mIsAlphaAnimating) {
            mTaskView.setAlpha(visibility ? 1f : 0f);
            mPointerView.setAlpha(visibility ? 1f : 0f);
        }
    }

    @Nullable
    TaskView getTaskView() {
        return mTaskView;
    }

    int getTaskId() {
        return mTaskId;
    }

    /**
     * Sets the bubble used to populate this view.
     */
    void update(Bubble bubble) {
        if (DEBUG_BUBBLE_EXPANDED_VIEW) {
            Log.d(TAG, "update: bubble=" + bubble);
        }
        if (mStackView == null) {
            Log.w(TAG, "Stack is null for bubble: " + bubble);
            return;
        }
        boolean isNew = mBubble == null || didBackingContentChange(bubble);
        if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) {
            mBubble = bubble;
            mManageButton.setContentDescription(getResources().getString(
                    R.string.bubbles_settings_button_description, bubble.getAppName()));
            mManageButton.setAccessibilityDelegate(
                    new AccessibilityDelegate() {
                        @Override
                        public void onInitializeAccessibilityNodeInfo(View host,
                                AccessibilityNodeInfo info) {
                            super.onInitializeAccessibilityNodeInfo(host, info);
                            // On focus, have TalkBack say
                            // "Actions available. Use swipe up then right to view."
                            // in addition to the default "double tap to activate".
                            mStackView.setupLocalMenu(info);
                        }
                    });

            if (isNew) {
                mPendingIntent = mBubble.getBubbleIntent();
                if ((mPendingIntent != null || mBubble.hasMetadataShortcutId())
                        && mTaskView != null) {
                    setContentVisibility(false);
                    mTaskView.setVisibility(VISIBLE);
                }
            }
            applyThemeAttrs();
        } else {
            Log.w(TAG, "Trying to update entry with different key, new bubble: "
                    + bubble.getKey() + " old bubble: " + bubble.getKey());
        }
    }

    /**
     * Bubbles are backed by a pending intent or a shortcut, once the activity is
     * started we never change it / restart it on notification updates -- unless the bubbles'
     * backing data switches.
     *
     * This indicates if the new bubble is backed by a different data source than what was
     * previously shown here (e.g. previously a pending intent & now a shortcut).
     *
     * @param newBubble the bubble this view is being updated with.
     * @return true if the backing content has changed.
     */
    private boolean didBackingContentChange(Bubble newBubble) {
        boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
        boolean newIsIntentBased = newBubble.getBubbleIntent() != null;
        return prevWasIntentBased != newIsIntentBased;
    }

    void updateHeight() {
        if (mExpandedViewContainerLocation == null) {
            return;
        }

        if ((mBubble != null && mTaskView != null) || mIsOverflow) {
            float desiredHeight = mPositioner.getExpandedViewHeight(mBubble);
            int maxHeight = mPositioner.getMaxExpandedViewHeight(mIsOverflow);
            float height = desiredHeight == MAX_HEIGHT
                    ? maxHeight
                    : Math.min(desiredHeight, maxHeight);
            FrameLayout.LayoutParams lp = mIsOverflow
                    ? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams()
                    : (FrameLayout.LayoutParams) mTaskView.getLayoutParams();
            mNeedsNewHeight = lp.height != height;
            if (!mImeVisible) {
                // If the ime is visible... don't adjust the height because that will cause
                // a configuration change and the ime will be lost.
                lp.height = (int) height;
                if (mIsOverflow) {
                    mOverflowView.setLayoutParams(lp);
                } else {
                    mTaskView.setLayoutParams(lp);
                }
                mNeedsNewHeight = false;
            }
            if (DEBUG_BUBBLE_EXPANDED_VIEW) {
                Log.d(TAG, "updateHeight: bubble=" + getBubbleKey()
                        + " height=" + height
                        + " mNeedsNewHeight=" + mNeedsNewHeight);
            }
        }
    }

    /**
     * Update appearance of the expanded view being displayed.
     *
     * @param containerLocationOnScreen The location on-screen of the container the expanded view is
     *                                  added to. This allows us to calculate max height without
     *                                  waiting for layout.
     */
    public void updateView(int[] containerLocationOnScreen) {
        if (DEBUG_BUBBLE_EXPANDED_VIEW) {
            Log.d(TAG, "updateView: bubble="
                    + getBubbleKey());
        }
        mExpandedViewContainerLocation = containerLocationOnScreen;
        updateHeight();
        if (mTaskView != null
                && mTaskView.getVisibility() == VISIBLE
                && mTaskView.isAttachedToWindow()) {
            mTaskView.onLocationChanged();
        }
        if (mIsOverflow) {
            mOverflowView.show();
        }
    }

    /**
     * Sets the position of the pointer.
     *
     * When bubbles are showing "vertically" they display along the left / right sides of the
     * screen with the expanded view beside them.
     *
     * If they aren't showing vertically they're positioned along the top of the screen with the
     * expanded view below them.
     *
     * @param bubblePosition the x position of the bubble if showing on top, the y position of
     *                       the bubble if showing vertically.
     * @param onLeft whether the stack was on the left side of the screen when expanded.
     */
    public void setPointerPosition(float bubblePosition, boolean onLeft, boolean animate) {
        // Pointer gets drawn in the padding
        final boolean showVertically = mPositioner.showBubblesVertically();
        final float paddingLeft = (showVertically && onLeft)
                ? mPointerHeight - mPointerOverlap
                : 0;
        final float paddingRight = (showVertically && !onLeft)
                ? mPointerHeight - mPointerOverlap
                : 0;
        final float paddingTop = showVertically
                ? 0
                : mPointerHeight - mPointerOverlap;
        setPadding((int) paddingLeft, (int) paddingTop, (int) paddingRight, 0);

        // Subtract the expandedViewY here because the pointer is placed within the expandedView.
        float pointerPosition = mPositioner.getPointerPosition(bubblePosition);
        final float bubbleCenter = mPositioner.showBubblesVertically()
                ? pointerPosition - mPositioner.getExpandedViewY(mBubble, bubblePosition)
                : pointerPosition;
        // Post because we need the width of the view
        post(() -> {
            mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer;
            updatePointerView();
            float pointerY;
            float pointerX;
            if (showVertically) {
                pointerY = bubbleCenter - (mPointerWidth / 2f);
                pointerX = onLeft
                        ? -mPointerHeight + mPointerOverlap
                        : getWidth() - mPaddingRight - mPointerOverlap;
            } else {
                pointerY = mPointerOverlap;
                pointerX = bubbleCenter - (mPointerWidth / 2f);
            }
            if (animate) {
                mPointerView.animate().translationX(pointerX).translationY(pointerY).start();
            } else {
                mPointerView.setTranslationY(pointerY);
                mPointerView.setTranslationX(pointerX);
                mPointerView.setVisibility(VISIBLE);
            }
        });
    }

    /**
     * Position of the manage button displayed in the expanded view. Used for placing user
     * education about the manage button.
     */
    public void getManageButtonBoundsOnScreen(Rect rect) {
        mManageButton.getBoundsOnScreen(rect);
    }

    public int getManageButtonMargin() {
        return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart();
    }

    /**
     * Cleans up anything related to the task and {@code TaskView}. If this view should be reused
     * after this method is called, then
     * {@link #initialize(BubbleController, BubbleStackView, boolean)} must be invoked first.
     */
    public void cleanUpExpandedState() {
        if (DEBUG_BUBBLE_EXPANDED_VIEW) {
            Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
        }
        if (getTaskId() != INVALID_TASK_ID) {
            try {
                ActivityTaskManager.getService().removeTask(getTaskId());
            } catch (RemoteException e) {
                Log.w(TAG, e.getMessage());
            }
        }
        if (mTaskView != null) {
            mTaskView.release();
            removeView(mTaskView);
            mTaskView = null;
        }
    }

    /**
     * Description of current expanded view state.
     */
    public void dump(
            @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
        pw.print("BubbleExpandedView");
        pw.print("  taskId:               "); pw.println(mTaskId);
        pw.print("  stackView:            "); pw.println(mStackView);
    }
}
