• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */, true /* useTrustedDisplay */);
305 
306         // Set ActivityView's alpha value as zero, since there is no view content to be shown.
307         setContentVisibility(false);
308 
309         mActivityViewContainer.setOutlineProvider(new ViewOutlineProvider() {
310             @Override
311             public void getOutline(View view, Outline outline) {
312                 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
313             }
314         });
315         mActivityViewContainer.setClipToOutline(true);
316         mActivityViewContainer.addView(mActivityView);
317         mActivityViewContainer.setLayoutParams(
318                 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
319         addView(mActivityViewContainer);
320 
321         if (mActivityView != null
322                 && mActivityView.getChildCount() > 0
323                 && mActivityView.getChildAt(0) instanceof SurfaceView) {
324             // Retrieve the surface from the ActivityView so we can screenshot it and change its
325             // z-ordering. This should always be possible, since ActivityView's constructor adds the
326             // SurfaceView as its first child.
327             mActivitySurface = (SurfaceView) mActivityView.getChildAt(0);
328         }
329 
330         // Expanded stack layout, top to bottom:
331         // Expanded view container
332         // ==> bubble row
333         // ==> expanded view
334         //   ==> activity view
335         //   ==> manage button
336         bringChildToFront(mActivityView);
337         bringChildToFront(mSettingsIcon);
338 
339         applyThemeAttrs();
340 
341         setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
342             // Keep track of IME displaying because we should not make any adjustments that might
343             // cause a config change while the IME is displayed otherwise it'll loose focus.
344             final int keyboardHeight = insets.getSystemWindowInsetBottom()
345                     - insets.getStableInsetBottom();
346             mKeyboardVisible = keyboardHeight != 0;
347             if (!mKeyboardVisible && mNeedsNewHeight) {
348                 updateHeight();
349             }
350             return view.onApplyWindowInsets(insets);
351         });
352 
353         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
354         setPadding(mExpandedViewPadding, mExpandedViewPadding, mExpandedViewPadding,
355                 mExpandedViewPadding);
356         setOnTouchListener((view, motionEvent) -> {
357             if (!usingActivityView()) {
358                 return false;
359             }
360 
361             final Rect avBounds = new Rect();
362             mActivityView.getBoundsOnScreen(avBounds);
363 
364             // Consume and ignore events on the expanded view padding that are within the
365             // ActivityView's vertical bounds. These events are part of a back gesture, and so they
366             // should not collapse the stack (which all other touches on areas around the AV would
367             // do).
368             if (motionEvent.getRawY() >= avBounds.top
369                             && motionEvent.getRawY() <= avBounds.bottom
370                             && (motionEvent.getRawX() < avBounds.left
371                                 || motionEvent.getRawX() > avBounds.right)) {
372                 return true;
373             }
374 
375             return false;
376         });
377 
378         // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout
379         // so the Manage button appears on the right.
380         setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
381     }
382 
getBubbleKey()383     private String getBubbleKey() {
384         return mBubble != null ? mBubble.getKey() : "null";
385     }
386 
387     /**
388      * Asks the ActivityView's surface to draw on top of all other views in the window. This is
389      * useful for ordering surfaces during animations, but should otherwise be set to false so that
390      * bubbles and menus can draw over the ActivityView.
391      */
setSurfaceZOrderedOnTop(boolean onTop)392     void setSurfaceZOrderedOnTop(boolean onTop) {
393         if (mActivitySurface == null) {
394             return;
395         }
396 
397         mActivitySurface.setZOrderedOnTop(onTop, true);
398     }
399 
400     /** Return a GraphicBuffer with the contents of the ActivityView's underlying surface. */
snapshotActivitySurface()401     @Nullable SurfaceControl.ScreenshotGraphicBuffer snapshotActivitySurface() {
402         if (mActivitySurface == null) {
403             return null;
404         }
405 
406         return SurfaceControl.captureLayers(
407                 mActivitySurface.getSurfaceControl(),
408                 new Rect(0, 0, mActivityView.getWidth(), mActivityView.getHeight()),
409                 1 /* scale */);
410     }
411 
getActivityViewLocationOnScreen()412     int[] getActivityViewLocationOnScreen() {
413         if (mActivityView != null) {
414             return mActivityView.getLocationOnScreen();
415         } else {
416             return new int[]{0, 0};
417         }
418     }
419 
setManageClickListener(OnClickListener manageClickListener)420     void setManageClickListener(OnClickListener manageClickListener) {
421         findViewById(R.id.settings_button).setOnClickListener(manageClickListener);
422     }
423 
424     /**
425      * Updates the ActivityView's obscured touchable region. This calls onLocationChanged, which
426      * results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is useful
427      * if a view has been added or removed from on top of the ActivityView, such as the manage menu.
428      */
updateObscuredTouchableRegion()429     void updateObscuredTouchableRegion() {
430         if (mActivityView != null) {
431             mActivityView.onLocationChanged();
432         }
433     }
434 
applyThemeAttrs()435     void applyThemeAttrs() {
436         final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
437                 android.R.attr.dialogCornerRadius,
438                 android.R.attr.colorBackgroundFloating});
439         mCornerRadius = ta.getDimensionPixelSize(0, 0);
440         mActivityViewContainer.setBackgroundColor(ta.getColor(1, Color.WHITE));
441         ta.recycle();
442 
443         if (mActivityView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
444                 mContext.getResources())) {
445             mActivityView.setCornerRadius(mCornerRadius);
446         }
447 
448         final int mode =
449                 getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
450         switch (mode) {
451             case Configuration.UI_MODE_NIGHT_NO:
452                 mPointerDrawable.setTint(getResources().getColor(R.color.bubbles_light));
453                 break;
454             case Configuration.UI_MODE_NIGHT_YES:
455                 mPointerDrawable.setTint(getResources().getColor(R.color.bubbles_dark));
456                 break;
457         }
458         mPointerView.setBackground(mPointerDrawable);
459     }
460 
461     /**
462      * Hides the IME if it's showing. This is currently done by dispatching a back press to the AV.
463      */
hideImeIfVisible()464     void hideImeIfVisible() {
465         if (mKeyboardVisible) {
466             performBackPressIfNeeded();
467         }
468     }
469 
470     @Override
onDetachedFromWindow()471     protected void onDetachedFromWindow() {
472         super.onDetachedFromWindow();
473         mKeyboardVisible = false;
474         mNeedsNewHeight = false;
475         if (mActivityView != null) {
476             if (sNewInsetsMode == NEW_INSETS_MODE_FULL) {
477                 setImeWindowToDisplay(0, 0);
478             } else {
479                 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0));
480             }
481         }
482         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
483             Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey());
484         }
485     }
486 
487     /**
488      * Set visibility of contents in the expanded state.
489      *
490      * @param visibility {@code true} if the contents should be visible on the screen.
491      *
492      * Note that this contents visibility doesn't affect visibility at {@link android.view.View},
493      * and setting {@code false} actually means rendering the contents in transparent.
494      */
setContentVisibility(boolean visibility)495     void setContentVisibility(boolean visibility) {
496         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
497             Log.d(TAG, "setContentVisibility: visibility=" + visibility
498                     + " bubble=" + getBubbleKey());
499         }
500         final float alpha = visibility ? 1f : 0f;
501 
502         mPointerView.setAlpha(alpha);
503 
504         if (mActivityView != null && alpha != mActivityView.getAlpha()) {
505             mActivityView.setAlpha(alpha);
506             mActivityView.bringToFront();
507         }
508     }
509 
getActivityView()510     @Nullable ActivityView getActivityView() {
511         return mActivityView;
512     }
513 
getTaskId()514     int getTaskId() {
515         return mTaskId;
516     }
517 
518     /**
519      * Called by {@link BubbleStackView} when the insets for the expanded state should be updated.
520      * This should be done post-move and post-animation.
521      */
updateInsets(WindowInsets insets)522     void updateInsets(WindowInsets insets) {
523         if (usingActivityView()) {
524             int[] screenLoc = mActivityView.getLocationOnScreen();
525             final int activityViewBottom = screenLoc[1] + mActivityView.getHeight();
526             final int keyboardTop = mDisplaySize.y - Math.max(insets.getSystemWindowInsetBottom(),
527                     insets.getDisplayCutout() != null
528                             ? insets.getDisplayCutout().getSafeInsetBottom()
529                             : 0);
530             final int insetsBottom = Math.max(activityViewBottom - keyboardTop, 0);
531 
532             if (sNewInsetsMode == NEW_INSETS_MODE_FULL) {
533                 setImeWindowToDisplay(getWidth(), insetsBottom);
534             } else {
535                 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom));
536             }
537         }
538     }
539 
setImeWindowToDisplay(int w, int h)540     private void setImeWindowToDisplay(int w, int h) {
541         if (getVirtualDisplayId() == INVALID_DISPLAY) {
542             return;
543         }
544         if (h == 0 || w == 0) {
545             if (mImeShowing) {
546                 mVirtualImeView.setVisibility(GONE);
547                 mImeShowing = false;
548             }
549             return;
550         }
551         final Context virtualDisplayContext = mContext.createDisplayContext(
552                 getVirtualDisplay().getDisplay());
553 
554         if (mVirtualDisplayWindowManager == null) {
555             mVirtualDisplayWindowManager =
556                     (WindowManager) virtualDisplayContext.getSystemService(Context.WINDOW_SERVICE);
557         }
558         if (mVirtualImeView == null) {
559             mVirtualImeView = new View(virtualDisplayContext);
560             mVirtualImeView.setVisibility(VISIBLE);
561             mVirtualDisplayWindowManager.addView(mVirtualImeView,
562                     getVirtualImeViewAttrs(w, h));
563         } else {
564             mVirtualDisplayWindowManager.updateViewLayout(mVirtualImeView,
565                     getVirtualImeViewAttrs(w, h));
566             mVirtualImeView.setVisibility(VISIBLE);
567         }
568 
569         mImeShowing = true;
570     }
571 
getVirtualImeViewAttrs(int w, int h)572     private WindowManager.LayoutParams getVirtualImeViewAttrs(int w, int h) {
573         // To use TYPE_NAVIGATION_BAR_PANEL instead of TYPE_IME_BAR to bypass the IME window type
574         // token check when adding the window.
575         final WindowManager.LayoutParams attrs =
576                 new WindowManager.LayoutParams(w, h, TYPE_NAVIGATION_BAR_PANEL,
577                         FLAG_LAYOUT_NO_LIMITS | FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE,
578                         TRANSPARENT);
579         attrs.gravity = Gravity.BOTTOM;
580         attrs.setTitle(WINDOW_TITLE);
581         attrs.token = new Binder();
582         attrs.providesInsetsTypes = new int[]{ITYPE_IME};
583         attrs.alpha = 0.0f;
584         return attrs;
585     }
586 
setStackView(BubbleStackView stackView)587     void setStackView(BubbleStackView stackView) {
588         mStackView = stackView;
589     }
590 
setOverflow(boolean overflow)591     public void setOverflow(boolean overflow) {
592         mIsOverflow = overflow;
593 
594         Intent target = new Intent(mContext, BubbleOverflowActivity.class);
595         mPendingIntent = PendingIntent.getActivity(mContext, /* requestCode */ 0,
596                 target, PendingIntent.FLAG_UPDATE_CURRENT);
597         mSettingsIcon.setVisibility(GONE);
598     }
599 
600     /**
601      * Sets the bubble used to populate this view.
602      */
update(Bubble bubble)603     void update(Bubble bubble) {
604         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
605             Log.d(TAG, "update: bubble=" + (bubble != null ? bubble.getKey() : "null"));
606         }
607         boolean isNew = mBubble == null || didBackingContentChange(bubble);
608         if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) {
609             mBubble = bubble;
610             mSettingsIcon.setContentDescription(getResources().getString(
611                     R.string.bubbles_settings_button_description, bubble.getAppName()));
612 
613             mSettingsIcon.setAccessibilityDelegate(
614                     new AccessibilityDelegate() {
615                         @Override
616                         public void onInitializeAccessibilityNodeInfo(View host,
617                                 AccessibilityNodeInfo info) {
618                             super.onInitializeAccessibilityNodeInfo(host, info);
619                             // On focus, have TalkBack say
620                             // "Actions available. Use swipe up then right to view."
621                             // in addition to the default "double tap to activate".
622                             mStackView.setupLocalMenu(info);
623                         }
624                     });
625 
626             if (isNew) {
627                 mPendingIntent = mBubble.getBubbleIntent();
628                 if (mPendingIntent != null || mBubble.hasMetadataShortcutId()) {
629                     setContentVisibility(false);
630                     mActivityView.setVisibility(VISIBLE);
631                 }
632             }
633             applyThemeAttrs();
634         } else {
635             Log.w(TAG, "Trying to update entry with different key, new bubble: "
636                     + bubble.getKey() + " old bubble: " + bubble.getKey());
637         }
638     }
639 
didBackingContentChange(Bubble newBubble)640     private boolean didBackingContentChange(Bubble newBubble) {
641         boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
642         boolean newIsIntentBased = newBubble.getBubbleIntent() != null;
643         return prevWasIntentBased != newIsIntentBased;
644     }
645 
646     /**
647      * Lets activity view know it should be shown / populated with activity content.
648      */
populateExpandedView()649     void populateExpandedView() {
650         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
651             Log.d(TAG, "populateExpandedView: "
652                     + "bubble=" + getBubbleKey());
653         }
654 
655         if (usingActivityView()) {
656             mActivityView.setCallback(mStateCallback);
657         } else {
658             Log.e(TAG, "Cannot populate expanded view.");
659         }
660     }
661 
performBackPressIfNeeded()662     boolean performBackPressIfNeeded() {
663         if (!usingActivityView()) {
664             return false;
665         }
666         mActivityView.performBackPress();
667         return true;
668     }
669 
updateHeight()670     void updateHeight() {
671         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
672             Log.d(TAG, "updateHeight: bubble=" + getBubbleKey());
673         }
674 
675         if (mExpandedViewContainerLocation == null) {
676             return;
677         }
678 
679         if (usingActivityView()) {
680             float desiredHeight = mOverflowHeight;
681             if (!mIsOverflow) {
682                 desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight);
683             }
684             float height = Math.min(desiredHeight, getMaxExpandedHeight());
685             height = Math.max(height, mMinHeight);
686             ViewGroup.LayoutParams lp = mActivityView.getLayoutParams();
687             mNeedsNewHeight = lp.height != height;
688             if (!mKeyboardVisible) {
689                 // If the keyboard is visible... don't adjust the height because that will cause
690                 // a configuration change and the keyboard will be lost.
691                 lp.height = (int) height;
692                 mActivityView.setLayoutParams(lp);
693                 mNeedsNewHeight = false;
694             }
695             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
696                 Log.d(TAG, "updateHeight: bubble=" + getBubbleKey()
697                         + " height=" + height
698                         + " mNeedsNewHeight=" + mNeedsNewHeight);
699             }
700         }
701     }
702 
getMaxExpandedHeight()703     private int getMaxExpandedHeight() {
704         mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize);
705         int bottomInset = getRootWindowInsets() != null
706                 ? getRootWindowInsets().getStableInsetBottom()
707                 : 0;
708 
709         return mDisplaySize.y
710                 - mExpandedViewContainerLocation[1]
711                 - getPaddingTop()
712                 - getPaddingBottom()
713                 - mSettingsIconHeight
714                 - mPointerHeight
715                 - mPointerMargin - bottomInset;
716     }
717 
718     /**
719      * Update appearance of the expanded view being displayed.
720      *
721      * @param containerLocationOnScreen The location on-screen of the container the expanded view is
722      *                                  added to. This allows us to calculate max height without
723      *                                  waiting for layout.
724      */
updateView(int[] containerLocationOnScreen)725     public void updateView(int[] containerLocationOnScreen) {
726         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
727             Log.d(TAG, "updateView: bubble="
728                     + getBubbleKey());
729         }
730 
731         mExpandedViewContainerLocation = containerLocationOnScreen;
732 
733         if (usingActivityView()
734                 && mActivityView.getVisibility() == VISIBLE
735                 && mActivityView.isAttachedToWindow()) {
736             mActivityView.onLocationChanged();
737             updateHeight();
738         }
739     }
740 
741     /**
742      * Set the x position that the tip of the triangle should point to.
743      */
setPointerPosition(float x)744     public void setPointerPosition(float x) {
745         float halfPointerWidth = mPointerWidth / 2f;
746         float pointerLeft = x - halfPointerWidth - mExpandedViewPadding;
747         mPointerView.setTranslationX(pointerLeft);
748         mPointerView.setVisibility(VISIBLE);
749     }
750 
751     /**
752      * Position of the manage button displayed in the expanded view. Used for placing user
753      * education about the manage button.
754      */
getManageButtonBoundsOnScreen(Rect rect)755     public void getManageButtonBoundsOnScreen(Rect rect) {
756         mSettingsIcon.getBoundsOnScreen(rect);
757     }
758 
759     /**
760      * Removes and releases an ActivityView if one was previously created for this bubble.
761      */
cleanUpExpandedState()762     public void cleanUpExpandedState() {
763         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
764             Log.d(TAG, "cleanUpExpandedState: mActivityViewStatus=" + mActivityViewStatus
765                     + ", bubble=" + getBubbleKey());
766         }
767         if (mActivityView == null) {
768             return;
769         }
770         mActivityView.release();
771         if (mTaskId != -1) {
772             try {
773                 ActivityTaskManager.getService().removeTask(mTaskId);
774             } catch (RemoteException e) {
775                 Log.w(TAG, "Failed to remove taskId " + mTaskId);
776             }
777             mTaskId = -1;
778         }
779         removeView(mActivityView);
780 
781         mActivityView = null;
782     }
783 
784     /**
785      * Called when the last task is removed from a {@link android.hardware.display.VirtualDisplay}
786      * which {@link ActivityView} uses.
787      */
notifyDisplayEmpty()788     void notifyDisplayEmpty() {
789         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
790             Log.d(TAG, "notifyDisplayEmpty: bubble="
791                     + getBubbleKey()
792                     + " mActivityViewStatus=" + mActivityViewStatus);
793         }
794         if (mActivityViewStatus == ActivityViewStatus.ACTIVITY_STARTED) {
795             mActivityViewStatus = ActivityViewStatus.INITIALIZED;
796         }
797     }
798 
usingActivityView()799     private boolean usingActivityView() {
800         return (mPendingIntent != null || mBubble.hasMetadataShortcutId())
801                 && mActivityView != null;
802     }
803 
804     /**
805      * @return the display id of the virtual display.
806      */
getVirtualDisplayId()807     public int getVirtualDisplayId() {
808         if (usingActivityView()) {
809             return mActivityView.getVirtualDisplayId();
810         }
811         return INVALID_DISPLAY;
812     }
813 
getVirtualDisplay()814     private VirtualDisplay getVirtualDisplay() {
815         if (usingActivityView()) {
816             return mActivityView.getVirtualDisplay();
817         }
818         return null;
819     }
820 }
821