1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.bubbles;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
20 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
21 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
22 import static android.graphics.PixelFormat.TRANSPARENT;
23 import static android.view.Display.INVALID_DISPLAY;
24 import static android.view.InsetsState.ITYPE_IME;
25 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
26 import static android.view.ViewRootImpl.NEW_INSETS_MODE_FULL;
27 import static android.view.ViewRootImpl.sNewInsetsMode;
28 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
29 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
30 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
31 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
32 
33 import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW;
34 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
35 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
36 
37 import android.annotation.SuppressLint;
38 import android.app.ActivityManager;
39 import android.app.ActivityOptions;
40 import android.app.ActivityTaskManager;
41 import android.app.ActivityView;
42 import android.app.PendingIntent;
43 import android.content.ComponentName;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.res.Configuration;
47 import android.content.res.Resources;
48 import android.content.res.TypedArray;
49 import android.graphics.Color;
50 import android.graphics.Insets;
51 import android.graphics.Outline;
52 import android.graphics.Point;
53 import android.graphics.Rect;
54 import android.graphics.drawable.ShapeDrawable;
55 import android.hardware.display.VirtualDisplay;
56 import android.os.Binder;
57 import android.os.RemoteException;
58 import android.util.AttributeSet;
59 import android.util.Log;
60 import android.view.Gravity;
61 import android.view.SurfaceControl;
62 import android.view.SurfaceView;
63 import android.view.View;
64 import android.view.ViewGroup;
65 import android.view.ViewOutlineProvider;
66 import android.view.WindowInsets;
67 import android.view.WindowManager;
68 import android.view.accessibility.AccessibilityNodeInfo;
69 import android.widget.FrameLayout;
70 import android.widget.LinearLayout;
71 
72 import androidx.annotation.Nullable;
73 
74 import com.android.internal.policy.ScreenDecorationsUtils;
75 import com.android.systemui.Dependency;
76 import com.android.systemui.R;
77 import com.android.systemui.recents.TriangleShape;
78 import com.android.systemui.statusbar.AlphaOptimizedButton;
79 
80 /**
81  * Container for the expanded bubble view, handles rendering the caret and settings icon.
82  */
83 public class BubbleExpandedView extends LinearLayout {
84     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES;
85     private static final String WINDOW_TITLE = "ImeInsetsWindowWithoutContent";
86 
87     private enum ActivityViewStatus {
88         // ActivityView is being initialized, cannot start an activity yet.
89         INITIALIZING,
90         // ActivityView is initialized, and ready to start an activity.
91         INITIALIZED,
92         // Activity runs in the ActivityView.
93         ACTIVITY_STARTED,
94         // ActivityView is released, so activity launching will no longer be permitted.
95         RELEASED,
96     }
97 
98     // The triangle pointing to the expanded view
99     private View mPointerView;
100     private int mPointerMargin;
101     @Nullable private int[] mExpandedViewContainerLocation;
102 
103     private AlphaOptimizedButton mSettingsIcon;
104 
105     // Views for expanded state
106     private ActivityView mActivityView;
107 
108     private ActivityViewStatus mActivityViewStatus = ActivityViewStatus.INITIALIZING;
109     private int mTaskId = -1;
110 
111     private PendingIntent mPendingIntent;
112 
113     private boolean mKeyboardVisible;
114     private boolean mNeedsNewHeight;
115 
116     private Point mDisplaySize;
117     private int mMinHeight;
118     private int mOverflowHeight;
119     private int mSettingsIconHeight;
120     private int mPointerWidth;
121     private int mPointerHeight;
122     private ShapeDrawable mPointerDrawable;
123     private int mExpandedViewPadding;
124 
125 
126     @Nullable private Bubble mBubble;
127 
128     private boolean mIsOverflow;
129 
130     private BubbleController mBubbleController = Dependency.get(BubbleController.class);
131     private WindowManager mWindowManager;
132     private ActivityManager mActivityManager;
133 
134     private BubbleStackView mStackView;
135     private View mVirtualImeView;
136     private WindowManager mVirtualDisplayWindowManager;
137     private boolean mImeShowing = false;
138     private float mCornerRadius = 0f;
139 
140     /**
141      * Container for the ActivityView that has a solid, round-rect background that shows if the
142      * ActivityView hasn't loaded.
143      */
144     private FrameLayout mActivityViewContainer = new FrameLayout(getContext());
145 
146     /** The SurfaceView that the ActivityView draws to. */
147     @Nullable private SurfaceView mActivitySurface;
148 
149     private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() {
150         @Override
151         public void onActivityViewReady(ActivityView view) {
152             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
153                 Log.d(TAG, "onActivityViewReady: mActivityViewStatus=" + mActivityViewStatus
154                         + " bubble=" + getBubbleKey());
155             }
156             switch (mActivityViewStatus) {
157                 case INITIALIZING:
158                 case INITIALIZED:
159                     // Custom options so there is no activity transition animation
160                     ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
161                             0 /* enterResId */, 0 /* exitResId */);
162                     options.setTaskAlwaysOnTop(true);
163                     options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
164                     // Post to keep the lifecycle normal
165                     post(() -> {
166                         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
167                             Log.d(TAG, "onActivityViewReady: calling startActivity, "
168                                     + "bubble=" + getBubbleKey());
169                         }
170                         if (mActivityView == null) {
171                             mBubbleController.removeBubble(getBubbleKey(),
172                                     BubbleController.DISMISS_INVALID_INTENT);
173                             return;
174                         }
175                         try {
176                             if (!mIsOverflow && mBubble.hasMetadataShortcutId()
177                                     && mBubble.getShortcutInfo() != null) {
178                                 options.setApplyActivityFlagsForBubbles(true);
179                                 mActivityView.startShortcutActivity(mBubble.getShortcutInfo(),
180                                         options, null /* sourceBounds */);
181                             } else {
182                                 Intent fillInIntent = new Intent();
183                                 // Apply flags to make behaviour match documentLaunchMode=always.
184                                 fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
185                                 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
186                                 if (mBubble != null) {
187                                     mBubble.setIntentActive();
188                                 }
189                                 mActivityView.startActivity(mPendingIntent, fillInIntent, options);
190                             }
191                         } catch (RuntimeException e) {
192                             // If there's a runtime exception here then there's something
193                             // wrong with the intent, we can't really recover / try to populate
194                             // the bubble again so we'll just remove it.
195                             Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
196                                     + ", " + e.getMessage() + "; removing bubble");
197                             mBubbleController.removeBubble(getBubbleKey(),
198                                     BubbleController.DISMISS_INVALID_INTENT);
199                         }
200                     });
201                     mActivityViewStatus = ActivityViewStatus.ACTIVITY_STARTED;
202                     break;
203                 case ACTIVITY_STARTED:
204                     post(() -> mActivityManager.moveTaskToFront(mTaskId, 0));
205                     break;
206             }
207         }
208 
209         @Override
210         public void onActivityViewDestroyed(ActivityView view) {
211             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
212                 Log.d(TAG, "onActivityViewDestroyed: mActivityViewStatus=" + mActivityViewStatus
213                         + " bubble=" + getBubbleKey());
214             }
215             mActivityViewStatus = ActivityViewStatus.RELEASED;
216         }
217 
218         @Override
219         public void onTaskCreated(int taskId, ComponentName componentName) {
220             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
221                 Log.d(TAG, "onTaskCreated: taskId=" + taskId
222                         + " bubble=" + getBubbleKey());
223             }
224             // Since Bubble ActivityView applies singleTaskDisplay this is
225             // guaranteed to only be called once per ActivityView. The taskId is
226             // saved to use for removeTask, preventing appearance in recent tasks.
227             mTaskId = taskId;
228         }
229 
230         /**
231          * This is only called for tasks on this ActivityView, which is also set to
232          * single-task mode -- meaning never more than one task on this display. If a task
233          * is being removed, it's the top Activity finishing and this bubble should
234          * be removed or collapsed.
235          */
236         @Override
237         public void onTaskRemovalStarted(int taskId) {
238             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
239                 Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId
240                         + " mActivityViewStatus=" + mActivityViewStatus
241                         + " bubble=" + getBubbleKey());
242             }
243             if (mBubble != null) {
244                 // Must post because this is called from a binder thread.
245                 post(() -> mBubbleController.removeBubble(mBubble.getKey(),
246                         BubbleController.DISMISS_TASK_FINISHED));
247             }
248         }
249     };
250 
BubbleExpandedView(Context context)251     public BubbleExpandedView(Context context) {
252         this(context, null);
253     }
254 
BubbleExpandedView(Context context, AttributeSet attrs)255     public BubbleExpandedView(Context context, AttributeSet attrs) {
256         this(context, attrs, 0);
257     }
258 
BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr)259     public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
260         this(context, attrs, defStyleAttr, 0);
261     }
262 
BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)263     public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
264             int defStyleRes) {
265         super(context, attrs, defStyleAttr, defStyleRes);
266         updateDimensions();
267         mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
268     }
269 
updateDimensions()270     void updateDimensions() {
271         mDisplaySize = new Point();
272         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
273         // Get the real size -- this includes screen decorations (notches, statusbar, navbar).
274         mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize);
275         Resources res = getResources();
276         mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
277         mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
278         mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
279     }
280 
281     @SuppressLint("ClickableViewAccessibility")
282     @Override
onFinishInflate()283     protected void onFinishInflate() {
284         super.onFinishInflate();
285         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
286             Log.d(TAG, "onFinishInflate: bubble=" + getBubbleKey());
287         }
288 
289         Resources res = getResources();
290         mPointerView = findViewById(R.id.pointer_view);
291         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
292         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
293 
294         mPointerDrawable = new ShapeDrawable(TriangleShape.create(
295                 mPointerWidth, mPointerHeight, true /* pointUp */));
296         mPointerView.setVisibility(INVISIBLE);
297 
298         mSettingsIconHeight = getContext().getResources().getDimensionPixelSize(
299                 R.dimen.bubble_manage_button_height);
300         mSettingsIcon = findViewById(R.id.settings_button);
301 
302         mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
303                 true /* singleTaskInstance */, false /* usePublicVirtualDisplay*/,
304                 true /* disableSurfaceViewBackgroundLayer */);
305 
306         // Set ActivityView's alpha value as zero, since there is no view content to be shown.
307         setContentVisibility(false);
308 
309         mActivityViewContainer.setBackgroundColor(Color.WHITE);
310         mActivityViewContainer.setOutlineProvider(new ViewOutlineProvider() {
311             @Override
312             public void getOutline(View view, Outline outline) {
313                 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
314             }
315         });
316         mActivityViewContainer.setClipToOutline(true);
317         mActivityViewContainer.addView(mActivityView);
318         mActivityViewContainer.setLayoutParams(
319                 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
320         addView(mActivityViewContainer);
321 
322         if (mActivityView != null
323                 && mActivityView.getChildCount() > 0
324                 && mActivityView.getChildAt(0) instanceof SurfaceView) {
325             // Retrieve the surface from the ActivityView so we can screenshot it and change its
326             // z-ordering. This should always be possible, since ActivityView's constructor adds the
327             // SurfaceView as its first child.
328             mActivitySurface = (SurfaceView) mActivityView.getChildAt(0);
329         }
330 
331         // Expanded stack layout, top to bottom:
332         // Expanded view container
333         // ==> bubble row
334         // ==> expanded view
335         //   ==> activity view
336         //   ==> manage button
337         bringChildToFront(mActivityView);
338         bringChildToFront(mSettingsIcon);
339 
340         applyThemeAttrs();
341 
342         setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
343             // Keep track of IME displaying because we should not make any adjustments that might
344             // cause a config change while the IME is displayed otherwise it'll loose focus.
345             final int keyboardHeight = insets.getSystemWindowInsetBottom()
346                     - insets.getStableInsetBottom();
347             mKeyboardVisible = keyboardHeight != 0;
348             if (!mKeyboardVisible && mNeedsNewHeight) {
349                 updateHeight();
350             }
351             return view.onApplyWindowInsets(insets);
352         });
353 
354         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
355         setPadding(mExpandedViewPadding, mExpandedViewPadding, mExpandedViewPadding,
356                 mExpandedViewPadding);
357         setOnTouchListener((view, motionEvent) -> {
358             if (!usingActivityView()) {
359                 return false;
360             }
361 
362             final Rect avBounds = new Rect();
363             mActivityView.getBoundsOnScreen(avBounds);
364 
365             // Consume and ignore events on the expanded view padding that are within the
366             // ActivityView's vertical bounds. These events are part of a back gesture, and so they
367             // should not collapse the stack (which all other touches on areas around the AV would
368             // do).
369             if (motionEvent.getRawY() >= avBounds.top
370                             && motionEvent.getRawY() <= avBounds.bottom
371                             && (motionEvent.getRawX() < avBounds.left
372                                 || motionEvent.getRawX() > avBounds.right)) {
373                 return true;
374             }
375 
376             return false;
377         });
378 
379         // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout
380         // so the Manage button appears on the right.
381         setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
382     }
383 
getBubbleKey()384     private String getBubbleKey() {
385         return mBubble != null ? mBubble.getKey() : "null";
386     }
387 
388     /**
389      * Asks the ActivityView's surface to draw on top of all other views in the window. This is
390      * useful for ordering surfaces during animations, but should otherwise be set to false so that
391      * bubbles and menus can draw over the ActivityView.
392      */
setSurfaceZOrderedOnTop(boolean onTop)393     void setSurfaceZOrderedOnTop(boolean onTop) {
394         if (mActivitySurface == null) {
395             return;
396         }
397 
398         mActivitySurface.setZOrderedOnTop(onTop, true);
399     }
400 
401     /** Return a GraphicBuffer with the contents of the ActivityView's underlying surface. */
snapshotActivitySurface()402     @Nullable SurfaceControl.ScreenshotGraphicBuffer snapshotActivitySurface() {
403         if (mActivitySurface == null) {
404             return null;
405         }
406 
407         return SurfaceControl.captureLayers(
408                 mActivitySurface.getSurfaceControl(),
409                 new Rect(0, 0, mActivityView.getWidth(), mActivityView.getHeight()),
410                 1 /* scale */);
411     }
412 
getActivityViewLocationOnScreen()413     int[] getActivityViewLocationOnScreen() {
414         if (mActivityView != null) {
415             return mActivityView.getLocationOnScreen();
416         } else {
417             return new int[]{0, 0};
418         }
419     }
420 
setManageClickListener(OnClickListener manageClickListener)421     void setManageClickListener(OnClickListener manageClickListener) {
422         findViewById(R.id.settings_button).setOnClickListener(manageClickListener);
423     }
424 
425     /**
426      * Updates the ActivityView's obscured touchable region. This calls onLocationChanged, which
427      * results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is useful
428      * if a view has been added or removed from on top of the ActivityView, such as the manage menu.
429      */
updateObscuredTouchableRegion()430     void updateObscuredTouchableRegion() {
431         if (mActivityView != null) {
432             mActivityView.onLocationChanged();
433         }
434     }
435 
applyThemeAttrs()436     void applyThemeAttrs() {
437         final TypedArray ta = mContext.obtainStyledAttributes(
438                 new int[] {android.R.attr.dialogCornerRadius});
439         mCornerRadius = ta.getDimensionPixelSize(0, 0);
440         ta.recycle();
441 
442         if (mActivityView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
443                 mContext.getResources())) {
444             mActivityView.setCornerRadius(mCornerRadius);
445         }
446 
447         final int mode =
448                 getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
449         switch (mode) {
450             case Configuration.UI_MODE_NIGHT_NO:
451                 mPointerDrawable.setTint(getResources().getColor(R.color.bubbles_light));
452                 break;
453             case Configuration.UI_MODE_NIGHT_YES:
454                 mPointerDrawable.setTint(getResources().getColor(R.color.bubbles_dark));
455                 break;
456         }
457         mPointerView.setBackground(mPointerDrawable);
458     }
459 
460     /**
461      * Hides the IME if it's showing. This is currently done by dispatching a back press to the AV.
462      */
hideImeIfVisible()463     void hideImeIfVisible() {
464         if (mKeyboardVisible) {
465             performBackPressIfNeeded();
466         }
467     }
468 
469     @Override
onDetachedFromWindow()470     protected void onDetachedFromWindow() {
471         super.onDetachedFromWindow();
472         mKeyboardVisible = false;
473         mNeedsNewHeight = false;
474         if (mActivityView != null) {
475             if (sNewInsetsMode == NEW_INSETS_MODE_FULL) {
476                 setImeWindowToDisplay(0, 0);
477             } else {
478                 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0));
479             }
480         }
481         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
482             Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey());
483         }
484     }
485 
486     /**
487      * Set visibility of contents in the expanded state.
488      *
489      * @param visibility {@code true} if the contents should be visible on the screen.
490      *
491      * Note that this contents visibility doesn't affect visibility at {@link android.view.View},
492      * and setting {@code false} actually means rendering the contents in transparent.
493      */
setContentVisibility(boolean visibility)494     void setContentVisibility(boolean visibility) {
495         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
496             Log.d(TAG, "setContentVisibility: visibility=" + visibility
497                     + " bubble=" + getBubbleKey());
498         }
499         final float alpha = visibility ? 1f : 0f;
500 
501         mPointerView.setAlpha(alpha);
502 
503         if (mActivityView != null && alpha != mActivityView.getAlpha()) {
504             mActivityView.setAlpha(alpha);
505             mActivityView.bringToFront();
506         }
507     }
508 
getActivityView()509     @Nullable ActivityView getActivityView() {
510         return mActivityView;
511     }
512 
getTaskId()513     int getTaskId() {
514         return mTaskId;
515     }
516 
517     /**
518      * Called by {@link BubbleStackView} when the insets for the expanded state should be updated.
519      * This should be done post-move and post-animation.
520      */
updateInsets(WindowInsets insets)521     void updateInsets(WindowInsets insets) {
522         if (usingActivityView()) {
523             int[] screenLoc = mActivityView.getLocationOnScreen();
524             final int activityViewBottom = screenLoc[1] + mActivityView.getHeight();
525             final int keyboardTop = mDisplaySize.y - Math.max(insets.getSystemWindowInsetBottom(),
526                     insets.getDisplayCutout() != null
527                             ? insets.getDisplayCutout().getSafeInsetBottom()
528                             : 0);
529             final int insetsBottom = Math.max(activityViewBottom - keyboardTop, 0);
530 
531             if (sNewInsetsMode == NEW_INSETS_MODE_FULL) {
532                 setImeWindowToDisplay(getWidth(), insetsBottom);
533             } else {
534                 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom));
535             }
536         }
537     }
538 
setImeWindowToDisplay(int w, int h)539     private void setImeWindowToDisplay(int w, int h) {
540         if (getVirtualDisplayId() == INVALID_DISPLAY) {
541             return;
542         }
543         if (h == 0 || w == 0) {
544             if (mImeShowing) {
545                 mVirtualImeView.setVisibility(GONE);
546                 mImeShowing = false;
547             }
548             return;
549         }
550         final Context virtualDisplayContext = mContext.createDisplayContext(
551                 getVirtualDisplay().getDisplay());
552 
553         if (mVirtualDisplayWindowManager == null) {
554             mVirtualDisplayWindowManager =
555                     (WindowManager) virtualDisplayContext.getSystemService(Context.WINDOW_SERVICE);
556         }
557         if (mVirtualImeView == null) {
558             mVirtualImeView = new View(virtualDisplayContext);
559             mVirtualImeView.setVisibility(VISIBLE);
560             mVirtualDisplayWindowManager.addView(mVirtualImeView,
561                     getVirtualImeViewAttrs(w, h));
562         } else {
563             mVirtualDisplayWindowManager.updateViewLayout(mVirtualImeView,
564                     getVirtualImeViewAttrs(w, h));
565             mVirtualImeView.setVisibility(VISIBLE);
566         }
567 
568         mImeShowing = true;
569     }
570 
getVirtualImeViewAttrs(int w, int h)571     private WindowManager.LayoutParams getVirtualImeViewAttrs(int w, int h) {
572         // To use TYPE_NAVIGATION_BAR_PANEL instead of TYPE_IME_BAR to bypass the IME window type
573         // token check when adding the window.
574         final WindowManager.LayoutParams attrs =
575                 new WindowManager.LayoutParams(w, h, TYPE_NAVIGATION_BAR_PANEL,
576                         FLAG_LAYOUT_NO_LIMITS | FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE,
577                         TRANSPARENT);
578         attrs.gravity = Gravity.BOTTOM;
579         attrs.setTitle(WINDOW_TITLE);
580         attrs.token = new Binder();
581         attrs.providesInsetsTypes = new int[]{ITYPE_IME};
582         attrs.alpha = 0.0f;
583         return attrs;
584     }
585 
setStackView(BubbleStackView stackView)586     void setStackView(BubbleStackView stackView) {
587         mStackView = stackView;
588     }
589 
setOverflow(boolean overflow)590     public void setOverflow(boolean overflow) {
591         mIsOverflow = overflow;
592 
593         Intent target = new Intent(mContext, BubbleOverflowActivity.class);
594         mPendingIntent = PendingIntent.getActivity(mContext, /* requestCode */ 0,
595                 target, PendingIntent.FLAG_UPDATE_CURRENT);
596         mSettingsIcon.setVisibility(GONE);
597     }
598 
599     /**
600      * Sets the bubble used to populate this view.
601      */
update(Bubble bubble)602     void update(Bubble bubble) {
603         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
604             Log.d(TAG, "update: bubble=" + (bubble != null ? bubble.getKey() : "null"));
605         }
606         boolean isNew = mBubble == null || didBackingContentChange(bubble);
607         if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) {
608             mBubble = bubble;
609             mSettingsIcon.setContentDescription(getResources().getString(
610                     R.string.bubbles_settings_button_description, bubble.getAppName()));
611 
612             mSettingsIcon.setAccessibilityDelegate(
613                     new AccessibilityDelegate() {
614                         @Override
615                         public void onInitializeAccessibilityNodeInfo(View host,
616                                 AccessibilityNodeInfo info) {
617                             super.onInitializeAccessibilityNodeInfo(host, info);
618                             // On focus, have TalkBack say
619                             // "Actions available. Use swipe up then right to view."
620                             // in addition to the default "double tap to activate".
621                             mStackView.setupLocalMenu(info);
622                         }
623                     });
624 
625             if (isNew) {
626                 mPendingIntent = mBubble.getBubbleIntent();
627                 if (mPendingIntent != null || mBubble.hasMetadataShortcutId()) {
628                     setContentVisibility(false);
629                     mActivityView.setVisibility(VISIBLE);
630                 }
631             }
632             applyThemeAttrs();
633         } else {
634             Log.w(TAG, "Trying to update entry with different key, new bubble: "
635                     + bubble.getKey() + " old bubble: " + bubble.getKey());
636         }
637     }
638 
didBackingContentChange(Bubble newBubble)639     private boolean didBackingContentChange(Bubble newBubble) {
640         boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
641         boolean newIsIntentBased = newBubble.getBubbleIntent() != null;
642         return prevWasIntentBased != newIsIntentBased;
643     }
644 
645     /**
646      * Lets activity view know it should be shown / populated with activity content.
647      */
populateExpandedView()648     void populateExpandedView() {
649         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
650             Log.d(TAG, "populateExpandedView: "
651                     + "bubble=" + getBubbleKey());
652         }
653 
654         if (usingActivityView()) {
655             mActivityView.setCallback(mStateCallback);
656         } else {
657             Log.e(TAG, "Cannot populate expanded view.");
658         }
659     }
660 
performBackPressIfNeeded()661     boolean performBackPressIfNeeded() {
662         if (!usingActivityView()) {
663             return false;
664         }
665         mActivityView.performBackPress();
666         return true;
667     }
668 
updateHeight()669     void updateHeight() {
670         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
671             Log.d(TAG, "updateHeight: bubble=" + getBubbleKey());
672         }
673 
674         if (mExpandedViewContainerLocation == null) {
675             return;
676         }
677 
678         if (usingActivityView()) {
679             float desiredHeight = mOverflowHeight;
680             if (!mIsOverflow) {
681                 desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight);
682             }
683             float height = Math.min(desiredHeight, getMaxExpandedHeight());
684             height = Math.max(height, mMinHeight);
685             ViewGroup.LayoutParams lp = mActivityView.getLayoutParams();
686             mNeedsNewHeight = lp.height != height;
687             if (!mKeyboardVisible) {
688                 // If the keyboard is visible... don't adjust the height because that will cause
689                 // a configuration change and the keyboard will be lost.
690                 lp.height = (int) height;
691                 mActivityView.setLayoutParams(lp);
692                 mNeedsNewHeight = false;
693             }
694             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
695                 Log.d(TAG, "updateHeight: bubble=" + getBubbleKey()
696                         + " height=" + height
697                         + " mNeedsNewHeight=" + mNeedsNewHeight);
698             }
699         }
700     }
701 
getMaxExpandedHeight()702     private int getMaxExpandedHeight() {
703         mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize);
704         int bottomInset = getRootWindowInsets() != null
705                 ? getRootWindowInsets().getStableInsetBottom()
706                 : 0;
707 
708         return mDisplaySize.y
709                 - mExpandedViewContainerLocation[1]
710                 - getPaddingTop()
711                 - getPaddingBottom()
712                 - mSettingsIconHeight
713                 - mPointerHeight
714                 - mPointerMargin - bottomInset;
715     }
716 
717     /**
718      * Update appearance of the expanded view being displayed.
719      *
720      * @param containerLocationOnScreen The location on-screen of the container the expanded view is
721      *                                  added to. This allows us to calculate max height without
722      *                                  waiting for layout.
723      */
updateView(int[] containerLocationOnScreen)724     public void updateView(int[] containerLocationOnScreen) {
725         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
726             Log.d(TAG, "updateView: bubble="
727                     + getBubbleKey());
728         }
729 
730         mExpandedViewContainerLocation = containerLocationOnScreen;
731 
732         if (usingActivityView()
733                 && mActivityView.getVisibility() == VISIBLE
734                 && mActivityView.isAttachedToWindow()) {
735             mActivityView.onLocationChanged();
736             updateHeight();
737         }
738     }
739 
740     /**
741      * Set the x position that the tip of the triangle should point to.
742      */
setPointerPosition(float x)743     public void setPointerPosition(float x) {
744         float halfPointerWidth = mPointerWidth / 2f;
745         float pointerLeft = x - halfPointerWidth - mExpandedViewPadding;
746         mPointerView.setTranslationX(pointerLeft);
747         mPointerView.setVisibility(VISIBLE);
748     }
749 
750     /**
751      * Position of the manage button displayed in the expanded view. Used for placing user
752      * education about the manage button.
753      */
getManageButtonBoundsOnScreen(Rect rect)754     public void getManageButtonBoundsOnScreen(Rect rect) {
755         mSettingsIcon.getBoundsOnScreen(rect);
756     }
757 
758     /**
759      * Removes and releases an ActivityView if one was previously created for this bubble.
760      */
cleanUpExpandedState()761     public void cleanUpExpandedState() {
762         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
763             Log.d(TAG, "cleanUpExpandedState: mActivityViewStatus=" + mActivityViewStatus
764                     + ", bubble=" + getBubbleKey());
765         }
766         if (mActivityView == null) {
767             return;
768         }
769         mActivityView.release();
770         if (mTaskId != -1) {
771             try {
772                 ActivityTaskManager.getService().removeTask(mTaskId);
773             } catch (RemoteException e) {
774                 Log.w(TAG, "Failed to remove taskId " + mTaskId);
775             }
776             mTaskId = -1;
777         }
778         removeView(mActivityView);
779 
780         mActivityView = null;
781     }
782 
783     /**
784      * Called when the last task is removed from a {@link android.hardware.display.VirtualDisplay}
785      * which {@link ActivityView} uses.
786      */
notifyDisplayEmpty()787     void notifyDisplayEmpty() {
788         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
789             Log.d(TAG, "notifyDisplayEmpty: bubble="
790                     + getBubbleKey()
791                     + " mActivityViewStatus=" + mActivityViewStatus);
792         }
793         if (mActivityViewStatus == ActivityViewStatus.ACTIVITY_STARTED) {
794             mActivityViewStatus = ActivityViewStatus.INITIALIZED;
795         }
796     }
797 
usingActivityView()798     private boolean usingActivityView() {
799         return (mPendingIntent != null || mBubble.hasMetadataShortcutId())
800                 && mActivityView != null;
801     }
802 
803     /**
804      * @return the display id of the virtual display.
805      */
getVirtualDisplayId()806     public int getVirtualDisplayId() {
807         if (usingActivityView()) {
808             return mActivityView.getVirtualDisplayId();
809         }
810         return INVALID_DISPLAY;
811     }
812 
getVirtualDisplay()813     private VirtualDisplay getVirtualDisplay() {
814         if (usingActivityView()) {
815             return mActivityView.getVirtualDisplay();
816         }
817         return null;
818     }
819 }
820