• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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 package com.android.launcher3.taskbar.bubbles;
17 
18 import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA;
19 import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
20 import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
21 import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
22 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
23 
24 import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA;
25 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
26 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
27 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
28 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
29 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
30 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
31 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
32 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
33 
34 import static java.lang.Math.abs;
35 
36 import android.annotation.BinderThread;
37 import android.annotation.Nullable;
38 import android.app.Notification;
39 import android.content.Context;
40 import android.content.pm.ApplicationInfo;
41 import android.content.pm.LauncherApps;
42 import android.content.pm.PackageManager;
43 import android.content.pm.ShortcutInfo;
44 import android.content.res.TypedArray;
45 import android.graphics.Bitmap;
46 import android.graphics.Color;
47 import android.graphics.Matrix;
48 import android.graphics.Path;
49 import android.graphics.drawable.AdaptiveIconDrawable;
50 import android.graphics.drawable.ColorDrawable;
51 import android.graphics.drawable.Drawable;
52 import android.graphics.drawable.InsetDrawable;
53 import android.os.Bundle;
54 import android.os.SystemProperties;
55 import android.os.UserHandle;
56 import android.util.ArrayMap;
57 import android.util.Log;
58 import android.util.PathParser;
59 import android.view.LayoutInflater;
60 
61 import androidx.appcompat.content.res.AppCompatResources;
62 
63 import com.android.internal.graphics.ColorUtils;
64 import com.android.launcher3.R;
65 import com.android.launcher3.icons.BitmapInfo;
66 import com.android.launcher3.icons.BubbleIconFactory;
67 import com.android.launcher3.shortcuts.ShortcutRequest;
68 import com.android.launcher3.taskbar.TaskbarControllers;
69 import com.android.launcher3.util.Executors.SimpleThreadFactory;
70 import com.android.quickstep.SystemUiProxy;
71 import com.android.wm.shell.bubbles.IBubblesListener;
72 import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
73 import com.android.wm.shell.common.bubbles.BubbleInfo;
74 import com.android.wm.shell.common.bubbles.RemovedBubble;
75 
76 import java.util.ArrayList;
77 import java.util.List;
78 import java.util.Objects;
79 import java.util.concurrent.Executor;
80 import java.util.concurrent.Executors;
81 
82 /**
83  * This registers a listener with SysUIProxy to get information about changes to the bubble
84  * stack state from WMShell (SysUI). The controller is also responsible for loading the necessary
85  * information to render each of the bubbles & dispatches changes to
86  * {@link BubbleBarViewController} which will then update {@link BubbleBarView} as needed.
87  *
88  * For details around the behavior of the bubble bar, see {@link BubbleBarView}.
89  */
90 public class BubbleBarController extends IBubblesListener.Stub {
91 
92     private static final String TAG = BubbleBarController.class.getSimpleName();
93     private static final boolean DEBUG = false;
94 
95     // Whether bubbles are showing in the bubble bar from launcher
96     public static final boolean BUBBLE_BAR_ENABLED =
97             SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
98 
99     private static final int MASK_HIDE_BUBBLE_BAR = SYSUI_STATE_BOUNCER_SHOWING
100             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
101             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
102             | SYSUI_STATE_IME_SHOWING
103             | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
104             | SYSUI_STATE_QUICK_SETTINGS_EXPANDED
105             | SYSUI_STATE_IME_SWITCHER_SHOWING;
106 
107     private static final int MASK_HIDE_HANDLE_VIEW = SYSUI_STATE_BOUNCER_SHOWING
108             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
109             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
110 
111     private static final int MASK_SYSUI_LOCKED = SYSUI_STATE_BOUNCER_SHOWING
112             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
113             | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
114 
115     private final Context mContext;
116     private final BubbleBarView mBarView;
117     private final ArrayMap<String, BubbleBarBubble> mBubbles = new ArrayMap<>();
118 
119     private static final Executor BUBBLE_STATE_EXECUTOR = Executors.newSingleThreadExecutor(
120             new SimpleThreadFactory("BubbleStateUpdates-", THREAD_PRIORITY_BACKGROUND));
121     private final Executor mMainExecutor;
122     private final LauncherApps mLauncherApps;
123     private final BubbleIconFactory mIconFactory;
124     private final SystemUiProxy mSystemUiProxy;
125 
126     private BubbleBarItem mSelectedBubble;
127     private BubbleBarOverflow mOverflowBubble;
128 
129     private BubbleBarViewController mBubbleBarViewController;
130     private BubbleStashController mBubbleStashController;
131     private BubbleStashedHandleViewController mBubbleStashedHandleViewController;
132 
133     /**
134      * Similar to {@link BubbleBarUpdate} but rather than {@link BubbleInfo}s it uses
135      * {@link BubbleBarBubble}s so that it can be used to update the views.
136      */
137     private static class BubbleBarViewUpdate {
138         boolean expandedChanged;
139         boolean expanded;
140         String selectedBubbleKey;
141         String suppressedBubbleKey;
142         String unsuppressedBubbleKey;
143         List<RemovedBubble> removedBubbles;
144         List<String> bubbleKeysInOrder;
145 
146         // These need to be loaded in the background
147         BubbleBarBubble addedBubble;
148         BubbleBarBubble updatedBubble;
149         List<BubbleBarBubble> currentBubbles;
150 
BubbleBarViewUpdate(BubbleBarUpdate update)151         BubbleBarViewUpdate(BubbleBarUpdate update) {
152             expandedChanged = update.expandedChanged;
153             expanded = update.expanded;
154             selectedBubbleKey = update.selectedBubbleKey;
155             suppressedBubbleKey = update.suppressedBubbleKey;
156             unsuppressedBubbleKey = update.unsupressedBubbleKey;
157             removedBubbles = update.removedBubbles;
158             bubbleKeysInOrder = update.bubbleKeysInOrder;
159         }
160     }
161 
BubbleBarController(Context context, BubbleBarView bubbleView)162     public BubbleBarController(Context context, BubbleBarView bubbleView) {
163         mContext = context;
164         mBarView = bubbleView; // Need the view for inflating bubble views.
165 
166         mSystemUiProxy = SystemUiProxy.INSTANCE.get(context);
167 
168         if (BUBBLE_BAR_ENABLED) {
169             mSystemUiProxy.setBubblesListener(this);
170         }
171         mMainExecutor = MAIN_EXECUTOR;
172         mLauncherApps = context.getSystemService(LauncherApps.class);
173         mIconFactory = new BubbleIconFactory(context,
174                 context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size),
175                 context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size),
176                 context.getResources().getColor(R.color.important_conversation),
177                 context.getResources().getDimensionPixelSize(
178                         com.android.internal.R.dimen.importance_ring_stroke_width));
179     }
180 
onDestroy()181     public void onDestroy() {
182         mSystemUiProxy.setBubblesListener(null);
183     }
184 
init(TaskbarControllers controllers, BubbleControllers bubbleControllers)185     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
186         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
187         mBubbleStashController = bubbleControllers.bubbleStashController;
188         mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
189 
190         bubbleControllers.runAfterInit(() -> {
191             mBubbleBarViewController.setHiddenForBubbles(!BUBBLE_BAR_ENABLED);
192             mBubbleStashedHandleViewController.setHiddenForBubbles(!BUBBLE_BAR_ENABLED);
193             mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse(
194                     key -> setSelectedBubble(mBubbles.get(key)));
195         });
196     }
197 
198     /**
199      * Creates and adds the overflow bubble to the bubble bar if it hasn't been created yet.
200      *
201      * <p>This should be called on the {@link #BUBBLE_STATE_EXECUTOR} executor to avoid inflating
202      * the overflow multiple times.
203      */
createAndAddOverflowIfNeeded()204     private void createAndAddOverflowIfNeeded() {
205         if (mOverflowBubble == null) {
206             BubbleBarOverflow overflow = createOverflow(mContext);
207             mMainExecutor.execute(() -> {
208                 // we're on the main executor now, so check that the overflow hasn't been created
209                 // again to avoid races.
210                 if (mOverflowBubble == null) {
211                     mBubbleBarViewController.addBubble(overflow);
212                     mOverflowBubble = overflow;
213                 }
214             });
215         }
216     }
217 
218     /**
219      * Updates the bubble bar, handle bar, and stash controllers based on sysui state flags.
220      */
updateStateForSysuiFlags(int flags)221     public void updateStateForSysuiFlags(int flags) {
222         boolean hideBubbleBar = (flags & MASK_HIDE_BUBBLE_BAR) != 0;
223         mBubbleBarViewController.setHiddenForSysui(hideBubbleBar);
224 
225         boolean hideHandleView = (flags & MASK_HIDE_HANDLE_VIEW) != 0;
226         mBubbleStashedHandleViewController.setHiddenForSysui(hideHandleView);
227 
228         boolean sysuiLocked = (flags & MASK_SYSUI_LOCKED) != 0;
229         mBubbleStashController.onSysuiLockedStateChange(sysuiLocked);
230     }
231 
232     //
233     // Bubble data changes
234     //
235 
236     @BinderThread
237     @Override
onBubbleStateChange(Bundle bundle)238     public void onBubbleStateChange(Bundle bundle) {
239         bundle.setClassLoader(BubbleBarUpdate.class.getClassLoader());
240         BubbleBarUpdate update = bundle.getParcelable("update", BubbleBarUpdate.class);
241         BubbleBarViewUpdate viewUpdate = new BubbleBarViewUpdate(update);
242         if (update.addedBubble != null
243                 || update.updatedBubble != null
244                 || !update.currentBubbleList.isEmpty()) {
245             // We have bubbles to load
246             BUBBLE_STATE_EXECUTOR.execute(() -> {
247                 createAndAddOverflowIfNeeded();
248                 if (update.addedBubble != null) {
249                     viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView,
250                             null /* existingBubble */);
251                 }
252                 if (update.updatedBubble != null) {
253                     BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
254                     viewUpdate.updatedBubble =
255                             populateBubble(mContext, update.updatedBubble, mBarView,
256                                     existingBubble);
257                 }
258                 if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) {
259                     List<BubbleBarBubble> currentBubbles = new ArrayList<>();
260                     for (int i = 0; i < update.currentBubbleList.size(); i++) {
261                         BubbleBarBubble b =
262                                 populateBubble(mContext, update.currentBubbleList.get(i), mBarView,
263                                         null /* existingBubble */);
264                         currentBubbles.add(b);
265                     }
266                     viewUpdate.currentBubbles = currentBubbles;
267                 }
268                 mMainExecutor.execute(() -> applyViewChanges(viewUpdate));
269             });
270         } else {
271             // No bubbles to load, immediately apply the changes.
272             BUBBLE_STATE_EXECUTOR.execute(
273                     () -> mMainExecutor.execute(() -> applyViewChanges(viewUpdate)));
274         }
275     }
276 
applyViewChanges(BubbleBarViewUpdate update)277     private void applyViewChanges(BubbleBarViewUpdate update) {
278         final boolean isCollapsed = (update.expandedChanged && !update.expanded)
279                 || (!update.expandedChanged && !mBubbleBarViewController.isExpanded());
280         BubbleBarItem previouslySelectedBubble = mSelectedBubble;
281         BubbleBarBubble bubbleToSelect = null;
282         if (!update.removedBubbles.isEmpty()) {
283             for (int i = 0; i < update.removedBubbles.size(); i++) {
284                 RemovedBubble removedBubble = update.removedBubbles.get(i);
285                 BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey());
286                 if (bubble != null) {
287                     mBubbleBarViewController.removeBubble(bubble);
288                 } else {
289                     Log.w(TAG, "trying to remove bubble that doesn't exist: "
290                             + removedBubble.getKey());
291                 }
292             }
293         }
294         if (update.addedBubble != null) {
295             mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
296             mBubbleBarViewController.addBubble(update.addedBubble);
297             if (isCollapsed) {
298                 // If we're collapsed, the most recently added bubble will be selected.
299                 bubbleToSelect = update.addedBubble;
300             }
301 
302         }
303         if (update.currentBubbles != null && !update.currentBubbles.isEmpty()) {
304             // Iterate in reverse because new bubbles are added in front and the list is in order.
305             for (int i = update.currentBubbles.size() - 1; i >= 0; i--) {
306                 BubbleBarBubble bubble = update.currentBubbles.get(i);
307                 if (bubble != null) {
308                     mBubbles.put(bubble.getKey(), bubble);
309                     mBubbleBarViewController.addBubble(bubble);
310                     if (isCollapsed) {
311                         // If we're collapsed, the most recently added bubble will be selected.
312                         bubbleToSelect = bubble;
313                     }
314                 } else {
315                     Log.w(TAG, "trying to add bubble but null after loading! "
316                             + update.addedBubble.getKey());
317                 }
318             }
319         }
320 
321         // Adds and removals have happened, update visibility before any other visual changes.
322         mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
323         mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());
324 
325         if (mBubbles.isEmpty()) {
326             // all bubbles were removed. clear the selected bubble
327             mSelectedBubble = null;
328         }
329 
330         if (update.updatedBubble != null) {
331             // Updates mean the dot state may have changed; any other changes were updated in
332             // the populateBubble step.
333             BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
334             // If we're not stashed, we're visible so animate
335             bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
336         }
337         if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
338             // Create the new list
339             List<BubbleBarBubble> newOrder = update.bubbleKeysInOrder.stream()
340                     .map(mBubbles::get).filter(Objects::nonNull).toList();
341             if (!newOrder.isEmpty()) {
342                 mBubbleBarViewController.reorderBubbles(newOrder);
343             }
344         }
345         if (update.suppressedBubbleKey != null) {
346             // TODO: (b/273316505) handle suppression
347         }
348         if (update.unsuppressedBubbleKey != null) {
349             // TODO: (b/273316505) handle suppression
350         }
351         if (update.selectedBubbleKey != null) {
352             if (mSelectedBubble == null
353                     || !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) {
354                 BubbleBarBubble newlySelected = mBubbles.get(update.selectedBubbleKey);
355                 if (newlySelected != null) {
356                     bubbleToSelect = newlySelected;
357                 } else {
358                     Log.w(TAG, "trying to select bubble that doesn't exist:"
359                             + update.selectedBubbleKey);
360                 }
361             }
362         }
363         if (bubbleToSelect != null) {
364             setSelectedBubble(bubbleToSelect);
365             if (previouslySelectedBubble == null) {
366                 mBubbleStashController.animateToInitialState(update.expanded);
367             }
368         }
369 
370         if (update.expandedChanged) {
371             if (update.expanded != mBubbleBarViewController.isExpanded()) {
372                 mBubbleBarViewController.setExpandedFromSysui(update.expanded);
373             } else {
374                 Log.w(TAG, "expansion was changed but is the same");
375             }
376         }
377     }
378 
379     /** Tells WMShell to show the currently selected bubble. */
showSelectedBubble()380     public void showSelectedBubble() {
381         if (getSelectedBubbleKey() != null) {
382             if (mSelectedBubble instanceof BubbleBarBubble) {
383                 // Because we've visited this bubble, we should suppress the notification.
384                 // This is updated on WMShell side when we show the bubble, but that update isn't
385                 // passed to launcher, instead we apply it directly here.
386                 BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo();
387                 info.setFlags(
388                         info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
389                 mSelectedBubble.getView().updateDotVisibility(true /* animate */);
390             }
391             mSystemUiProxy.showBubble(getSelectedBubbleKey(),
392                     getBubbleBarOffsetX(), getBubbleBarOffsetY());
393         } else {
394             Log.w(TAG, "Trying to show the selected bubble but it's null");
395         }
396     }
397 
398     /** Updates the currently selected bubble for launcher views and tells WMShell to show it. */
showAndSelectBubble(BubbleBarItem b)399     public void showAndSelectBubble(BubbleBarItem b) {
400         if (DEBUG) Log.w(TAG, "showingSelectedBubble: " + b.getKey());
401         setSelectedBubble(b);
402         showSelectedBubble();
403     }
404 
405     /**
406      * Sets the bubble that should be selected. This notifies the views, it does not notify
407      * WMShell that the selection has changed, that should go through either
408      * {@link #showSelectedBubble()} or {@link #showAndSelectBubble(BubbleBarItem)}.
409      */
setSelectedBubble(BubbleBarItem b)410     private void setSelectedBubble(BubbleBarItem b) {
411         if (!Objects.equals(b, mSelectedBubble)) {
412             if (DEBUG) Log.w(TAG, "selectingBubble: " + b.getKey());
413             mSelectedBubble = b;
414             mBubbleBarViewController.updateSelectedBubble(mSelectedBubble);
415         }
416     }
417 
418     /**
419      * Returns the selected bubble or null if no bubble is selected.
420      */
421     @Nullable
getSelectedBubbleKey()422     public String getSelectedBubbleKey() {
423         if (mSelectedBubble != null) {
424             return mSelectedBubble.getKey();
425         }
426         return null;
427     }
428 
429     //
430     // Loading data for the bubbles
431     //
432 
433     @Nullable
populateBubble(Context context, BubbleInfo b, BubbleBarView bbv, @Nullable BubbleBarBubble existingBubble)434     private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
435             @Nullable BubbleBarBubble existingBubble) {
436         String appName;
437         Bitmap badgeBitmap;
438         Bitmap bubbleBitmap;
439         Path dotPath;
440         int dotColor;
441 
442         boolean isImportantConvo = b.isImportantConversation();
443 
444         ShortcutRequest.QueryResult result = new ShortcutRequest(context,
445                 new UserHandle(b.getUserId()))
446                 .forPackage(b.getPackageName(), b.getShortcutId())
447                 .query(FLAG_MATCH_DYNAMIC
448                         | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
449                         | FLAG_MATCH_CACHED
450                         | FLAG_GET_PERSONS_DATA);
451 
452         ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null;
453         if (shortcutInfo == null) {
454             Log.w(TAG, "No shortcutInfo found for bubble: " + b.getKey()
455                     + " with shortcutId: " + b.getShortcutId());
456         }
457 
458         ApplicationInfo appInfo;
459         try {
460             appInfo = mLauncherApps.getApplicationInfo(
461                     b.getPackageName(),
462                     0,
463                     new UserHandle(b.getUserId()));
464         } catch (PackageManager.NameNotFoundException e) {
465             // If we can't find package... don't think we should show the bubble.
466             Log.w(TAG, "Unable to find packageName: " + b.getPackageName());
467             return null;
468         }
469         if (appInfo == null) {
470             Log.w(TAG, "Unable to find appInfo: " + b.getPackageName());
471             return null;
472         }
473         PackageManager pm = context.getPackageManager();
474         appName = String.valueOf(appInfo.loadLabel(pm));
475         Drawable appIcon = appInfo.loadUnbadgedIcon(pm);
476         Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(b.getUserId()));
477 
478         // Badged bubble image
479         Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo,
480                 b.getIcon());
481         if (bubbleDrawable == null) {
482             // Default to app icon
483             bubbleDrawable = appIcon;
484         }
485 
486         BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo);
487         badgeBitmap = badgeBitmapInfo.icon;
488 
489         float[] bubbleBitmapScale = new float[1];
490         bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
491 
492         // Dot color & placement
493         Path iconPath = PathParser.createPathFromPathData(
494                 context.getResources().getString(
495                         com.android.internal.R.string.config_icon_mask));
496         Matrix matrix = new Matrix();
497         float scale = bubbleBitmapScale[0];
498         float radius = BubbleView.DEFAULT_PATH_SIZE / 2f;
499         matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
500                 radius /* pivot y */);
501         iconPath.transform(matrix);
502         dotPath = iconPath;
503         dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
504                 Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
505 
506         if (existingBubble == null) {
507             LayoutInflater inflater = LayoutInflater.from(context);
508             BubbleView bubbleView = (BubbleView) inflater.inflate(
509                     R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
510 
511             BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
512                     badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
513             bubbleView.setBubble(bubble);
514             return bubble;
515         } else {
516             // If we already have a bubble (so it already has an inflated view), update it.
517             existingBubble.setInfo(b);
518             existingBubble.setBadge(badgeBitmap);
519             existingBubble.setIcon(bubbleBitmap);
520             existingBubble.setDotColor(dotColor);
521             existingBubble.setDotPath(dotPath);
522             existingBubble.setAppName(appName);
523             return existingBubble;
524         }
525     }
526 
createOverflow(Context context)527     private BubbleBarOverflow createOverflow(Context context) {
528         Bitmap bitmap = createOverflowBitmap(context);
529         LayoutInflater inflater = LayoutInflater.from(context);
530         BubbleView bubbleView = (BubbleView) inflater.inflate(
531                 R.layout.bubblebar_item_view, mBarView, false /* attachToRoot */);
532         BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
533         bubbleView.setOverflow(overflow, bitmap);
534         return overflow;
535     }
536 
createOverflowBitmap(Context context)537     private Bitmap createOverflowBitmap(Context context) {
538         Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
539                 R.drawable.bubble_ic_overflow_button);
540 
541         final TypedArray ta = mContext.obtainStyledAttributes(
542                 new int[]{
543                         com.android.internal.R.attr.materialColorOnPrimaryFixed,
544                         com.android.internal.R.attr.materialColorPrimaryFixed
545                 });
546         int overflowIconColor = ta.getColor(0, Color.WHITE);
547         int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
548         ta.recycle();
549 
550         iconDrawable.setTint(overflowIconColor);
551 
552         int inset = context.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset);
553         Drawable foreground = new InsetDrawable(iconDrawable, inset);
554         Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor),
555                 foreground);
556 
557         return mIconFactory.createBadgedIconBitmap(drawable).icon;
558     }
559 
getBubbleBarOffsetY()560     private int getBubbleBarOffsetY() {
561         final int translation = (int) abs(mBubbleStashController.getBubbleBarTranslationY());
562         return translation + mBarView.getHeight();
563     }
564 
getBubbleBarOffsetX()565     private int getBubbleBarOffsetX() {
566         return mBarView.getWidth() + mBarView.getHorizontalMargin();
567     }
568 }
569