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