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