| /* |
| * 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.PointF; |
| import android.graphics.Rect; |
| import android.graphics.drawable.ShapeDrawable; |
| import android.os.RemoteException; |
| import android.util.AttributeSet; |
| import android.util.FloatProperty; |
| import android.util.IntProperty; |
| 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.annotations.VisibleForTesting; |
| 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.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; |
| |
| /** {@link IntProperty} for updating bottom clip */ |
| public static final IntProperty<BubbleExpandedView> BOTTOM_CLIP_PROPERTY = |
| new IntProperty<BubbleExpandedView>("bottomClip") { |
| @Override |
| public void setValue(BubbleExpandedView expandedView, int value) { |
| expandedView.setBottomClip(value); |
| } |
| |
| @Override |
| public Integer get(BubbleExpandedView expandedView) { |
| return expandedView.mBottomClip; |
| } |
| }; |
| |
| /** {@link FloatProperty} for updating taskView or overflow alpha */ |
| public static final FloatProperty<BubbleExpandedView> CONTENT_ALPHA = |
| new FloatProperty<BubbleExpandedView>("contentAlpha") { |
| @Override |
| public void setValue(BubbleExpandedView expandedView, float value) { |
| expandedView.setContentAlpha(value); |
| } |
| |
| @Override |
| public Float get(BubbleExpandedView expandedView) { |
| return expandedView.getContentAlpha(); |
| } |
| }; |
| |
| /** {@link FloatProperty} for updating manage button alpha */ |
| public static final FloatProperty<BubbleExpandedView> MANAGE_BUTTON_ALPHA = |
| new FloatProperty<BubbleExpandedView>("manageButtonAlpha") { |
| @Override |
| public void setValue(BubbleExpandedView expandedView, float value) { |
| expandedView.mManageButton.setAlpha(value); |
| } |
| |
| @Override |
| public Float get(BubbleExpandedView expandedView) { |
| return expandedView.mManageButton.getAlpha(); |
| } |
| }; |
| |
| // 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 #mIsAnimating} 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 mIsAnimating = false; |
| |
| private int mPointerWidth; |
| private int mPointerHeight; |
| private float mPointerRadius; |
| private float mPointerOverlap; |
| private final PointF mPointerPos = new PointF(); |
| private CornerPathEffect mPointerEffect; |
| private ShapeDrawable mCurrentPointer; |
| private ShapeDrawable mTopPointer; |
| private ShapeDrawable mLeftPointer; |
| private ShapeDrawable mRightPointer; |
| private float mCornerRadius = 0f; |
| private int mBackgroundColorFloating; |
| private boolean mUsingMaxHeight; |
| private int mTopClip = 0; |
| private int mBottomClip = 0; |
| @Nullable private Bubble mBubble; |
| private PendingIntent mPendingIntent; |
| // TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead |
| private boolean mIsOverflow; |
| private boolean mIsClipping; |
| |
| 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) { |
| Rect clip = new Rect(0, mTopClip, view.getWidth(), view.getHeight() - mBottomClip); |
| outline.setRoundRect(clip, 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.getTaskViewTransitions(), 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}); |
| boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( |
| mContext.getResources()); |
| mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0; |
| mBackgroundColorFloating = ta.getColor(1, Color.WHITE); |
| mExpandedViewContainer.setBackgroundColor(mBackgroundColorFloating); |
| ta.recycle(); |
| |
| if (mTaskView != null) { |
| 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); |
| } |
| |
| @VisibleForTesting |
| public String getBubbleKey() { |
| return mBubble != null ? mBubble.getKey() : mIsOverflow ? BubbleOverflow.KEY : 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. |
| */ |
| public 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 */, |
| false /* containsHdrLayers */); |
| } |
| 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}. If this is set to |
| * true, calls to {@link #setContentVisibility} will not be applied until this is set to false |
| * again. |
| */ |
| public void setAnimating(boolean animating) { |
| mIsAnimating = animating; |
| |
| // If we're done animating, apply the correct |
| if (!animating) { |
| setContentVisibility(mIsContentVisible); |
| } |
| } |
| |
| /** |
| * Get alpha from underlying {@code TaskView} if this view is for a bubble. |
| * Or get alpha for the overflow view if this view is for overflow. |
| * |
| * @return alpha for the content being shown |
| */ |
| public float getContentAlpha() { |
| if (mIsOverflow) { |
| return mOverflowView.getAlpha(); |
| } |
| if (mTaskView != null) { |
| return mTaskView.getAlpha(); |
| } |
| return 1f; |
| } |
| |
| /** |
| * Set alpha of the underlying {@code TaskView} if this view is for a bubble. |
| * Or set alpha for the overflow view if this view is for overflow. |
| * |
| * Changing expanded view's alpha does not affect the {@code TaskView} since it uses a Surface. |
| */ |
| public void setContentAlpha(float alpha) { |
| if (mIsOverflow) { |
| mOverflowView.setAlpha(alpha); |
| } else if (mTaskView != null) { |
| mTaskView.setAlpha(alpha); |
| } |
| } |
| |
| /** |
| * Set translation Y for the expanded view content. |
| * Excludes manage button and pointer. |
| */ |
| public void setContentTranslationY(float translationY) { |
| mExpandedViewContainer.setTranslationY(translationY); |
| |
| // Left or right pointer can become detached when moving the view up |
| if (translationY <= 0 && (isShowingLeftPointer() || isShowingRightPointer())) { |
| // Y coordinate where the pointer would start to get detached from the expanded view. |
| // Takes into account bottom clipping and rounded corners |
| float detachPoint = |
| mExpandedViewContainer.getBottom() - mBottomClip - mCornerRadius + translationY; |
| float pointerBottom = mPointerPos.y + mPointerHeight; |
| // If pointer bottom is past detach point, move it in by that many pixels |
| float horizontalShift = 0; |
| if (pointerBottom > detachPoint) { |
| horizontalShift = pointerBottom - detachPoint; |
| } |
| if (isShowingLeftPointer()) { |
| // Move left pointer right |
| movePointerBy(horizontalShift, 0); |
| } else { |
| // Move right pointer left |
| movePointerBy(-horizontalShift, 0); |
| } |
| // Hide pointer if it is moved by entire width |
| mPointerView.setVisibility( |
| horizontalShift > mPointerWidth ? View.INVISIBLE : View.VISIBLE); |
| } |
| } |
| |
| /** |
| * Update alpha value for the manage button |
| */ |
| public void setManageButtonAlpha(float alpha) { |
| mManageButton.setAlpha(alpha); |
| } |
| |
| /** |
| * Set {@link #setTranslationY(float) translationY} for the manage button |
| */ |
| public void setManageButtonTranslationY(float translationY) { |
| mManageButton.setTranslationY(translationY); |
| } |
| |
| /** |
| * Set top clipping for the view |
| */ |
| public void setTopClip(int clip) { |
| mTopClip = clip; |
| onContainerClipUpdate(); |
| } |
| |
| /** |
| * Set bottom clipping for the view |
| */ |
| public void setBottomClip(int clip) { |
| mBottomClip = clip; |
| onContainerClipUpdate(); |
| } |
| |
| private void onContainerClipUpdate() { |
| if (mTopClip == 0 && mBottomClip == 0) { |
| if (mIsClipping) { |
| mIsClipping = false; |
| if (mTaskView != null) { |
| mTaskView.setClipBounds(null); |
| mTaskView.setEnableSurfaceClipping(false); |
| } |
| mExpandedViewContainer.invalidateOutline(); |
| } |
| } else { |
| if (!mIsClipping) { |
| mIsClipping = true; |
| if (mTaskView != null) { |
| mTaskView.setEnableSurfaceClipping(true); |
| } |
| } |
| mExpandedViewContainer.invalidateOutline(); |
| if (mTaskView != null) { |
| mTaskView.setClipBounds(new Rect(0, mTopClip, mTaskView.getWidth(), |
| mTaskView.getHeight() - mBottomClip)); |
| } |
| } |
| } |
| |
| /** |
| * Move pointer from base position |
| */ |
| public void movePointerBy(float x, float y) { |
| mPointerView.setTranslationX(mPointerPos.x + x); |
| mPointerView.setTranslationY(mPointerPos.y + y); |
| } |
| |
| /** |
| * 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. |
| */ |
| public void setContentVisibility(boolean visibility) { |
| if (DEBUG_BUBBLE_EXPANDED_VIEW) { |
| Log.d(TAG, "setContentVisibility: visibility=" + visibility |
| + " bubble=" + getBubbleKey()); |
| } |
| mIsContentVisible = visibility; |
| if (mTaskView != null && !mIsAnimating) { |
| mTaskView.setAlpha(visibility ? 1f : 0f); |
| mPointerView.setAlpha(visibility ? 1f : 0f); |
| } |
| } |
| |
| @Nullable |
| TaskView getTaskView() { |
| return mTaskView; |
| } |
| |
| @VisibleForTesting |
| public BubbleOverflowContainerView getOverflow() { |
| return mOverflowView; |
| } |
| |
| |
| /** |
| * Return content height: taskView or overflow. |
| * Takes into account clippings set by {@link #setTopClip(int)} and {@link #setBottomClip(int)} |
| * |
| * @return if bubble is for overflow, return overflow height, otherwise return taskView height |
| */ |
| public int getContentHeight() { |
| if (mIsOverflow) { |
| return mOverflowView.getHeight() - mTopClip - mBottomClip; |
| } |
| if (mTaskView != null) { |
| return mTaskView.getHeight() - mTopClip - mBottomClip; |
| } |
| return 0; |
| } |
| |
| /** |
| * Return bottom position of the content on screen |
| * |
| * @return if bubble is for overflow, return value for overflow, otherwise taskView |
| */ |
| public int getContentBottomOnScreen() { |
| Rect out = new Rect(); |
| if (mIsOverflow) { |
| mOverflowView.getBoundsOnScreen(out); |
| } |
| if (mTaskView != null) { |
| mTaskView.getBoundsOnScreen(out); |
| } |
| return out.bottom; |
| } |
| |
| 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; |
| } |
| |
| /** |
| * Whether the bubble is using all available height to display or not. |
| */ |
| public boolean isUsingMaxHeight() { |
| return mUsingMaxHeight; |
| } |
| |
| 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); |
| mUsingMaxHeight = height == 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) { |
| post(() -> { |
| 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. |
| * @param animate whether the pointer should animate to this position. |
| */ |
| 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(); |
| if (showVertically) { |
| mPointerPos.y = bubbleCenter - (mPointerWidth / 2f); |
| mPointerPos.x = onLeft |
| ? -mPointerHeight + mPointerOverlap |
| : getWidth() - mPaddingRight - mPointerOverlap; |
| } else { |
| mPointerPos.y = mPointerOverlap; |
| mPointerPos.x = bubbleCenter - (mPointerWidth / 2f); |
| } |
| if (animate) { |
| mPointerView.animate().translationX(mPointerPos.x).translationY( |
| mPointerPos.y).start(); |
| } else { |
| mPointerView.setTranslationY(mPointerPos.y); |
| mPointerView.setTranslationX(mPointerPos.x); |
| mPointerView.setVisibility(VISIBLE); |
| } |
| }); |
| } |
| |
| /** |
| * Return true if pointer is shown on the left |
| */ |
| public boolean isShowingLeftPointer() { |
| return mCurrentPointer == mLeftPointer; |
| } |
| |
| /** |
| * Return true if pointer is shown on the right |
| */ |
| public boolean isShowingRightPointer() { |
| return mCurrentPointer == mRightPointer; |
| } |
| |
| /** |
| * Return width of the current pointer |
| */ |
| public int getPointerWidth() { |
| return mPointerWidth; |
| } |
| |
| /** |
| * 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 PrintWriter pw, @NonNull String[] args) { |
| pw.print("BubbleExpandedView"); |
| pw.print(" taskId: "); pw.println(mTaskId); |
| pw.print(" stackView: "); pw.println(mStackView); |
| } |
| } |