1 /* 2 * Copyright (C) 2020 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.wm.shell.bubbles; 17 18 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 19 import static android.os.AsyncTask.Status.FINISHED; 20 21 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; 22 23 import android.annotation.DimenRes; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.app.Notification; 27 import android.app.PendingIntent; 28 import android.app.Person; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.LocusId; 32 import android.content.pm.ApplicationInfo; 33 import android.content.pm.PackageManager; 34 import android.content.pm.ShortcutInfo; 35 import android.content.res.Resources; 36 import android.graphics.Bitmap; 37 import android.graphics.Path; 38 import android.graphics.drawable.Drawable; 39 import android.graphics.drawable.Icon; 40 import android.os.Parcelable; 41 import android.os.UserHandle; 42 import android.provider.Settings; 43 import android.service.notification.StatusBarNotification; 44 import android.text.TextUtils; 45 import android.util.Log; 46 47 import com.android.internal.annotations.VisibleForTesting; 48 import com.android.internal.logging.InstanceId; 49 50 import java.io.FileDescriptor; 51 import java.io.PrintWriter; 52 import java.util.List; 53 import java.util.Objects; 54 import java.util.concurrent.Executor; 55 56 /** 57 * Encapsulates the data and UI elements of a bubble. 58 */ 59 @VisibleForTesting 60 public class Bubble implements BubbleViewProvider { 61 private static final String TAG = "Bubble"; 62 63 private final String mKey; 64 @Nullable 65 private final String mGroupKey; 66 @Nullable 67 private final LocusId mLocusId; 68 69 private final Executor mMainExecutor; 70 71 private long mLastUpdated; 72 private long mLastAccessed; 73 74 @Nullable 75 private Bubbles.SuppressionChangedListener mSuppressionListener; 76 77 /** Whether the bubble should show a dot for the notification indicating updated content. */ 78 private boolean mShowBubbleUpdateDot = true; 79 80 /** Whether flyout text should be suppressed, regardless of any other flags or state. */ 81 private boolean mSuppressFlyout; 82 83 // Items that are typically loaded later 84 private String mAppName; 85 private ShortcutInfo mShortcutInfo; 86 private String mMetadataShortcutId; 87 private BadgedImageView mIconView; 88 private BubbleExpandedView mExpandedView; 89 90 private BubbleViewInfoTask mInflationTask; 91 private boolean mInflateSynchronously; 92 private boolean mPendingIntentCanceled; 93 private boolean mIsImportantConversation; 94 95 /** 96 * Presentational info about the flyout. 97 */ 98 public static class FlyoutMessage { 99 @Nullable public Icon senderIcon; 100 @Nullable public Drawable senderAvatar; 101 @Nullable public CharSequence senderName; 102 @Nullable public CharSequence message; 103 @Nullable public boolean isGroupChat; 104 } 105 106 private FlyoutMessage mFlyoutMessage; 107 // The developer provided image for the bubble 108 private Bitmap mBubbleBitmap; 109 // The app badge for the bubble 110 private Bitmap mBadgeBitmap; 111 private int mDotColor; 112 private Path mDotPath; 113 private int mFlags; 114 115 @NonNull 116 private UserHandle mUser; 117 @NonNull 118 private String mPackageName; 119 @Nullable 120 private String mTitle; 121 @Nullable 122 private Icon mIcon; 123 private boolean mIsBubble; 124 private boolean mIsTextChanged; 125 private boolean mIsClearable; 126 private boolean mShouldSuppressNotificationDot; 127 private boolean mShouldSuppressNotificationList; 128 private boolean mShouldSuppressPeek; 129 private int mDesiredHeight; 130 @DimenRes 131 private int mDesiredHeightResId; 132 private int mTaskId; 133 134 /** for logging **/ 135 @Nullable 136 private InstanceId mInstanceId; 137 @Nullable 138 private String mChannelId; 139 private int mNotificationId; 140 private int mAppUid = -1; 141 142 /** 143 * A bubble is created and can be updated. This intent is updated until the user first 144 * expands the bubble. Once the user has expanded the contents, we ignore the intent updates 145 * to prevent restarting the intent & possibly altering UI state in the activity in front of 146 * the user. 147 * 148 * Once the bubble is overflowed, the activity is finished and updates to the 149 * notification are respected. Typically an update to an overflowed bubble would result in 150 * that bubble being added back to the stack anyways. 151 */ 152 @Nullable 153 private PendingIntent mIntent; 154 private boolean mIntentActive; 155 @Nullable 156 private PendingIntent.CancelListener mIntentCancelListener; 157 158 /** 159 * Sent when the bubble & notification are no longer visible to the user (i.e. no 160 * notification in the shade, no bubble in the stack or overflow). 161 */ 162 @Nullable 163 private PendingIntent mDeleteIntent; 164 165 /** 166 * Create a bubble with limited information based on given {@link ShortcutInfo}. 167 * Note: Currently this is only being used when the bubble is persisted to disk. 168 */ 169 @VisibleForTesting(visibility = PRIVATE) Bubble(@onNull final String key, @NonNull final ShortcutInfo shortcutInfo, final int desiredHeight, final int desiredHeightResId, @Nullable final String title, int taskId, @Nullable final String locus, Executor mainExecutor)170 public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo, 171 final int desiredHeight, final int desiredHeightResId, @Nullable final String title, 172 int taskId, @Nullable final String locus, Executor mainExecutor) { 173 Objects.requireNonNull(key); 174 Objects.requireNonNull(shortcutInfo); 175 mMetadataShortcutId = shortcutInfo.getId(); 176 mShortcutInfo = shortcutInfo; 177 mKey = key; 178 mGroupKey = null; 179 mLocusId = locus != null ? new LocusId(locus) : null; 180 mFlags = 0; 181 mUser = shortcutInfo.getUserHandle(); 182 mPackageName = shortcutInfo.getPackage(); 183 mIcon = shortcutInfo.getIcon(); 184 mDesiredHeight = desiredHeight; 185 mDesiredHeightResId = desiredHeightResId; 186 mTitle = title; 187 mShowBubbleUpdateDot = false; 188 mMainExecutor = mainExecutor; 189 mTaskId = taskId; 190 } 191 192 @VisibleForTesting(visibility = PRIVATE) Bubble(@onNull final BubbleEntry entry, @Nullable final Bubbles.SuppressionChangedListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, Executor mainExecutor)193 public Bubble(@NonNull final BubbleEntry entry, 194 @Nullable final Bubbles.SuppressionChangedListener listener, 195 final Bubbles.PendingIntentCanceledListener intentCancelListener, 196 Executor mainExecutor) { 197 mKey = entry.getKey(); 198 mGroupKey = entry.getGroupKey(); 199 mLocusId = entry.getLocusId(); 200 mSuppressionListener = listener; 201 mIntentCancelListener = intent -> { 202 if (mIntent != null) { 203 mIntent.unregisterCancelListener(mIntentCancelListener); 204 } 205 mainExecutor.execute(() -> { 206 intentCancelListener.onPendingIntentCanceled(this); 207 }); 208 }; 209 mMainExecutor = mainExecutor; 210 mTaskId = INVALID_TASK_ID; 211 setEntry(entry); 212 } 213 214 @Override getKey()215 public String getKey() { 216 return mKey; 217 } 218 219 /** 220 * @see StatusBarNotification#getGroupKey() 221 * @return the group key for this bubble, if one exists. 222 */ getGroupKey()223 public String getGroupKey() { 224 return mGroupKey; 225 } 226 getLocusId()227 public LocusId getLocusId() { 228 return mLocusId; 229 } 230 getUser()231 public UserHandle getUser() { 232 return mUser; 233 } 234 235 @NonNull getPackageName()236 public String getPackageName() { 237 return mPackageName; 238 } 239 240 @Override getBubbleIcon()241 public Bitmap getBubbleIcon() { 242 return mBubbleBitmap; 243 } 244 245 @Override getAppBadge()246 public Bitmap getAppBadge() { 247 return mBadgeBitmap; 248 } 249 250 @Override getDotColor()251 public int getDotColor() { 252 return mDotColor; 253 } 254 255 @Override getDotPath()256 public Path getDotPath() { 257 return mDotPath; 258 } 259 260 @Nullable getAppName()261 public String getAppName() { 262 return mAppName; 263 } 264 265 @Nullable getShortcutInfo()266 public ShortcutInfo getShortcutInfo() { 267 return mShortcutInfo; 268 } 269 270 @Nullable 271 @Override getIconView()272 public BadgedImageView getIconView() { 273 return mIconView; 274 } 275 276 @Override 277 @Nullable getExpandedView()278 public BubbleExpandedView getExpandedView() { 279 return mExpandedView; 280 } 281 282 @Nullable getTitle()283 public String getTitle() { 284 return mTitle; 285 } 286 287 /** 288 * @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise. 289 */ getShortcutId()290 String getShortcutId() { 291 return getShortcutInfo() != null 292 ? getShortcutInfo().getId() 293 : getMetadataShortcutId(); 294 } 295 getMetadataShortcutId()296 String getMetadataShortcutId() { 297 return mMetadataShortcutId; 298 } 299 hasMetadataShortcutId()300 boolean hasMetadataShortcutId() { 301 return (mMetadataShortcutId != null && !mMetadataShortcutId.isEmpty()); 302 } 303 304 /** 305 * Call this to clean up the task for the bubble. Ensure this is always called when done with 306 * the bubble. 307 */ cleanupExpandedView()308 void cleanupExpandedView() { 309 if (mExpandedView != null) { 310 mExpandedView.cleanUpExpandedState(); 311 mExpandedView = null; 312 } 313 if (mIntent != null) { 314 mIntent.unregisterCancelListener(mIntentCancelListener); 315 } 316 mIntentActive = false; 317 } 318 319 /** 320 * Call when all the views should be removed/cleaned up. 321 */ cleanupViews()322 void cleanupViews() { 323 cleanupExpandedView(); 324 mIconView = null; 325 } 326 setPendingIntentCanceled()327 void setPendingIntentCanceled() { 328 mPendingIntentCanceled = true; 329 } 330 getPendingIntentCanceled()331 boolean getPendingIntentCanceled() { 332 return mPendingIntentCanceled; 333 } 334 335 /** 336 * Sets whether to perform inflation on the same thread as the caller. This method should only 337 * be used in tests, not in production. 338 */ 339 @VisibleForTesting setInflateSynchronously(boolean inflateSynchronously)340 void setInflateSynchronously(boolean inflateSynchronously) { 341 mInflateSynchronously = inflateSynchronously; 342 } 343 344 /** 345 * Sets whether this bubble is considered text changed. This method is purely for 346 * testing. 347 */ 348 @VisibleForTesting setTextChangedForTest(boolean textChanged)349 void setTextChangedForTest(boolean textChanged) { 350 mIsTextChanged = textChanged; 351 } 352 353 /** 354 * Starts a task to inflate & load any necessary information to display a bubble. 355 * 356 * @param callback the callback to notify one the bubble is ready to be displayed. 357 * @param context the context for the bubble. 358 * @param controller the bubble controller. 359 * @param stackView the stackView the bubble is eventually added to. 360 * @param iconFactory the iconfactory use to create badged images for the bubble. 361 */ inflate(BubbleViewInfoTask.Callback callback, Context context, BubbleController controller, BubbleStackView stackView, BubbleIconFactory iconFactory, boolean skipInflation)362 void inflate(BubbleViewInfoTask.Callback callback, 363 Context context, 364 BubbleController controller, 365 BubbleStackView stackView, 366 BubbleIconFactory iconFactory, 367 boolean skipInflation) { 368 if (isBubbleLoading()) { 369 mInflationTask.cancel(true /* mayInterruptIfRunning */); 370 } 371 mInflationTask = new BubbleViewInfoTask(this, 372 context, 373 controller, 374 stackView, 375 iconFactory, 376 skipInflation, 377 callback, 378 mMainExecutor); 379 if (mInflateSynchronously) { 380 mInflationTask.onPostExecute(mInflationTask.doInBackground()); 381 } else { 382 mInflationTask.execute(); 383 } 384 } 385 isBubbleLoading()386 private boolean isBubbleLoading() { 387 return mInflationTask != null && mInflationTask.getStatus() != FINISHED; 388 } 389 isInflated()390 boolean isInflated() { 391 return mIconView != null && mExpandedView != null; 392 } 393 stopInflation()394 void stopInflation() { 395 if (mInflationTask == null) { 396 return; 397 } 398 mInflationTask.cancel(true /* mayInterruptIfRunning */); 399 } 400 setViewInfo(BubbleViewInfoTask.BubbleViewInfo info)401 void setViewInfo(BubbleViewInfoTask.BubbleViewInfo info) { 402 if (!isInflated()) { 403 mIconView = info.imageView; 404 mExpandedView = info.expandedView; 405 } 406 407 mShortcutInfo = info.shortcutInfo; 408 mAppName = info.appName; 409 mFlyoutMessage = info.flyoutMessage; 410 411 mBadgeBitmap = info.badgeBitmap; 412 mBubbleBitmap = info.bubbleBitmap; 413 414 mDotColor = info.dotColor; 415 mDotPath = info.dotPath; 416 417 if (mExpandedView != null) { 418 mExpandedView.update(this /* bubble */); 419 } 420 if (mIconView != null) { 421 mIconView.setRenderedBubble(this /* bubble */); 422 } 423 } 424 425 /** 426 * Set visibility of bubble in the expanded state. 427 * 428 * @param visibility {@code true} if the expanded bubble should be visible on the screen. 429 * 430 * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, 431 * and setting {@code false} actually means rendering the expanded view in transparent. 432 */ 433 @Override setTaskViewVisibility(boolean visibility)434 public void setTaskViewVisibility(boolean visibility) { 435 if (mExpandedView != null) { 436 mExpandedView.setContentVisibility(visibility); 437 } 438 } 439 440 /** 441 * Sets the entry associated with this bubble. 442 */ setEntry(@onNull final BubbleEntry entry)443 void setEntry(@NonNull final BubbleEntry entry) { 444 Objects.requireNonNull(entry); 445 mLastUpdated = entry.getStatusBarNotification().getPostTime(); 446 mIsBubble = entry.getStatusBarNotification().getNotification().isBubbleNotification(); 447 mPackageName = entry.getStatusBarNotification().getPackageName(); 448 mUser = entry.getStatusBarNotification().getUser(); 449 mTitle = getTitle(entry); 450 mChannelId = entry.getStatusBarNotification().getNotification().getChannelId(); 451 mNotificationId = entry.getStatusBarNotification().getId(); 452 mAppUid = entry.getStatusBarNotification().getUid(); 453 mInstanceId = entry.getStatusBarNotification().getInstanceId(); 454 mFlyoutMessage = extractFlyoutMessage(entry); 455 if (entry.getRanking() != null) { 456 mShortcutInfo = entry.getRanking().getConversationShortcutInfo(); 457 mIsTextChanged = entry.getRanking().isTextChanged(); 458 if (entry.getRanking().getChannel() != null) { 459 mIsImportantConversation = 460 entry.getRanking().getChannel().isImportantConversation(); 461 } 462 } 463 if (entry.getBubbleMetadata() != null) { 464 mMetadataShortcutId = entry.getBubbleMetadata().getShortcutId(); 465 mFlags = entry.getBubbleMetadata().getFlags(); 466 mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight(); 467 mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId(); 468 mIcon = entry.getBubbleMetadata().getIcon(); 469 470 if (!mIntentActive || mIntent == null) { 471 if (mIntent != null) { 472 mIntent.unregisterCancelListener(mIntentCancelListener); 473 } 474 mIntent = entry.getBubbleMetadata().getIntent(); 475 if (mIntent != null) { 476 mIntent.registerCancelListener(mIntentCancelListener); 477 } 478 } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) { 479 // Was an intent bubble now it's a shortcut bubble... still unregister the listener 480 mIntent.unregisterCancelListener(mIntentCancelListener); 481 mIntentActive = false; 482 mIntent = null; 483 } 484 mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); 485 } 486 487 mIsClearable = entry.isClearable(); 488 mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot(); 489 mShouldSuppressNotificationList = entry.shouldSuppressNotificationList(); 490 mShouldSuppressPeek = entry.shouldSuppressPeek(); 491 } 492 493 @Nullable getIcon()494 Icon getIcon() { 495 return mIcon; 496 } 497 isTextChanged()498 boolean isTextChanged() { 499 return mIsTextChanged; 500 } 501 502 /** 503 * @return the last time this bubble was updated or accessed, whichever is most recent. 504 */ getLastActivity()505 long getLastActivity() { 506 return Math.max(mLastUpdated, mLastAccessed); 507 } 508 509 /** 510 * Sets if the intent used for this bubble is currently active (i.e. populating an 511 * expanded view, expanded or not). 512 */ setIntentActive()513 void setIntentActive() { 514 mIntentActive = true; 515 } 516 isIntentActive()517 boolean isIntentActive() { 518 return mIntentActive; 519 } 520 getInstanceId()521 public InstanceId getInstanceId() { 522 return mInstanceId; 523 } 524 525 @Nullable getChannelId()526 public String getChannelId() { 527 return mChannelId; 528 } 529 getNotificationId()530 public int getNotificationId() { 531 return mNotificationId; 532 } 533 534 /** 535 * @return the task id of the task in which bubble contents is drawn. 536 */ 537 @Override getTaskId()538 public int getTaskId() { 539 return mExpandedView != null ? mExpandedView.getTaskId() : mTaskId; 540 } 541 542 /** 543 * Should be invoked whenever a Bubble is accessed (selected while expanded). 544 */ markAsAccessedAt(long lastAccessedMillis)545 void markAsAccessedAt(long lastAccessedMillis) { 546 mLastAccessed = lastAccessedMillis; 547 setSuppressNotification(true); 548 setShowDot(false /* show */); 549 } 550 551 /** 552 * Should be invoked whenever a Bubble is promoted from overflow. 553 */ markUpdatedAt(long lastAccessedMillis)554 void markUpdatedAt(long lastAccessedMillis) { 555 mLastUpdated = lastAccessedMillis; 556 } 557 558 /** 559 * Whether this notification should be shown in the shade. 560 */ showInShade()561 boolean showInShade() { 562 return !shouldSuppressNotification() || !mIsClearable; 563 } 564 565 /** 566 * Whether this bubble is currently being hidden from the stack. 567 */ isSuppressed()568 boolean isSuppressed() { 569 return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE) != 0; 570 } 571 572 /** 573 * Whether this bubble is able to be suppressed (i.e. has the developer opted into the API to 574 * hide the bubble when in the same content). 575 */ isSuppressable()576 boolean isSuppressable() { 577 return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE) != 0; 578 } 579 580 /** 581 * Whether this notification conversation is important. 582 */ isImportantConversation()583 boolean isImportantConversation() { 584 return mIsImportantConversation; 585 } 586 587 /** 588 * Sets whether this notification should be suppressed in the shade. 589 */ 590 @VisibleForTesting setSuppressNotification(boolean suppressNotification)591 public void setSuppressNotification(boolean suppressNotification) { 592 boolean prevShowInShade = showInShade(); 593 if (suppressNotification) { 594 mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 595 } else { 596 mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 597 } 598 599 if (showInShade() != prevShowInShade && mSuppressionListener != null) { 600 mSuppressionListener.onBubbleNotificationSuppressionChange(this); 601 } 602 } 603 604 /** 605 * Sets whether this bubble should be suppressed from the stack. 606 */ setSuppressBubble(boolean suppressBubble)607 public void setSuppressBubble(boolean suppressBubble) { 608 if (!isSuppressable()) { 609 Log.e(TAG, "calling setSuppressBubble on " 610 + getKey() + " when bubble not suppressable"); 611 return; 612 } 613 boolean prevSuppressed = isSuppressed(); 614 if (suppressBubble) { 615 mFlags |= Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; 616 } else { 617 mFlags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE; 618 } 619 if (prevSuppressed != suppressBubble && mSuppressionListener != null) { 620 mSuppressionListener.onBubbleNotificationSuppressionChange(this); 621 } 622 } 623 624 /** 625 * Sets whether the bubble for this notification should show a dot indicating updated content. 626 */ setShowDot(boolean showDot)627 void setShowDot(boolean showDot) { 628 mShowBubbleUpdateDot = showDot; 629 630 if (mIconView != null) { 631 mIconView.updateDotVisibility(true /* animate */); 632 } 633 } 634 635 /** 636 * Whether the bubble for this notification should show a dot indicating updated content. 637 */ 638 @Override showDot()639 public boolean showDot() { 640 return mShowBubbleUpdateDot 641 && !mShouldSuppressNotificationDot 642 && !shouldSuppressNotification(); 643 } 644 645 /** 646 * Whether the flyout for the bubble should be shown. 647 */ 648 @VisibleForTesting showFlyout()649 public boolean showFlyout() { 650 return !mSuppressFlyout && !mShouldSuppressPeek 651 && !shouldSuppressNotification() 652 && !mShouldSuppressNotificationList; 653 } 654 655 /** 656 * Set whether the flyout text for the bubble should be shown when an update is received. 657 * 658 * @param suppressFlyout whether the flyout text is shown 659 */ setSuppressFlyout(boolean suppressFlyout)660 void setSuppressFlyout(boolean suppressFlyout) { 661 mSuppressFlyout = suppressFlyout; 662 } 663 getFlyoutMessage()664 FlyoutMessage getFlyoutMessage() { 665 return mFlyoutMessage; 666 } 667 getRawDesiredHeight()668 int getRawDesiredHeight() { 669 return mDesiredHeight; 670 } 671 getRawDesiredHeightResId()672 int getRawDesiredHeightResId() { 673 return mDesiredHeightResId; 674 } 675 getDesiredHeight(Context context)676 float getDesiredHeight(Context context) { 677 boolean useRes = mDesiredHeightResId != 0; 678 if (useRes) { 679 return getDimenForPackageUser(context, mDesiredHeightResId, mPackageName, 680 mUser.getIdentifier()); 681 } else { 682 return mDesiredHeight * context.getResources().getDisplayMetrics().density; 683 } 684 } 685 getDesiredHeightString()686 String getDesiredHeightString() { 687 boolean useRes = mDesiredHeightResId != 0; 688 if (useRes) { 689 return String.valueOf(mDesiredHeightResId); 690 } else { 691 return String.valueOf(mDesiredHeight); 692 } 693 } 694 695 @Nullable getBubbleIntent()696 PendingIntent getBubbleIntent() { 697 return mIntent; 698 } 699 700 @Nullable getDeleteIntent()701 PendingIntent getDeleteIntent() { 702 return mDeleteIntent; 703 } 704 getSettingsIntent(final Context context)705 Intent getSettingsIntent(final Context context) { 706 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS); 707 intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); 708 final int uid = getUid(context); 709 if (uid != -1) { 710 intent.putExtra(Settings.EXTRA_APP_UID, uid); 711 } 712 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 713 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 714 intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 715 return intent; 716 } 717 getAppUid()718 public int getAppUid() { 719 return mAppUid; 720 } 721 getUid(final Context context)722 private int getUid(final Context context) { 723 if (mAppUid != -1) return mAppUid; 724 final PackageManager pm = BubbleController.getPackageManagerForUser(context, 725 mUser.getIdentifier()); 726 if (pm == null) return -1; 727 try { 728 final ApplicationInfo info = pm.getApplicationInfo(mShortcutInfo.getPackage(), 0); 729 return info.uid; 730 } catch (PackageManager.NameNotFoundException e) { 731 Log.e(TAG, "cannot find uid", e); 732 } 733 return -1; 734 } 735 getDimenForPackageUser(Context context, int resId, String pkg, int userId)736 private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) { 737 Resources r; 738 if (pkg != null) { 739 try { 740 if (userId == UserHandle.USER_ALL) { 741 userId = UserHandle.USER_SYSTEM; 742 } 743 r = context.createContextAsUser(UserHandle.of(userId), /* flags */ 0) 744 .getPackageManager().getResourcesForApplication(pkg); 745 return r.getDimensionPixelSize(resId); 746 } catch (PackageManager.NameNotFoundException ex) { 747 // Uninstalled, don't care 748 } catch (Resources.NotFoundException e) { 749 // Invalid res id, return 0 and user our default 750 Log.e(TAG, "Couldn't find desired height res id", e); 751 } 752 } 753 return 0; 754 } 755 shouldSuppressNotification()756 private boolean shouldSuppressNotification() { 757 return isEnabled(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); 758 } 759 shouldAutoExpand()760 public boolean shouldAutoExpand() { 761 return isEnabled(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); 762 } 763 setShouldAutoExpand(boolean shouldAutoExpand)764 void setShouldAutoExpand(boolean shouldAutoExpand) { 765 if (shouldAutoExpand) { 766 enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); 767 } else { 768 disable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); 769 } 770 } 771 setIsBubble(final boolean isBubble)772 public void setIsBubble(final boolean isBubble) { 773 mIsBubble = isBubble; 774 } 775 isBubble()776 public boolean isBubble() { 777 return mIsBubble; 778 } 779 enable(int option)780 public void enable(int option) { 781 mFlags |= option; 782 } 783 disable(int option)784 public void disable(int option) { 785 mFlags &= ~option; 786 } 787 isEnabled(int option)788 public boolean isEnabled(int option) { 789 return (mFlags & option) != 0; 790 } 791 792 @Override toString()793 public String toString() { 794 return "Bubble{" + mKey + '}'; 795 } 796 797 /** 798 * Description of current bubble state. 799 */ dump( @onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)800 public void dump( 801 @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { 802 pw.print("key: "); pw.println(mKey); 803 pw.print(" showInShade: "); pw.println(showInShade()); 804 pw.print(" showDot: "); pw.println(showDot()); 805 pw.print(" showFlyout: "); pw.println(showFlyout()); 806 pw.print(" lastActivity: "); pw.println(getLastActivity()); 807 pw.print(" desiredHeight: "); pw.println(getDesiredHeightString()); 808 pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification()); 809 pw.print(" autoExpand: "); pw.println(shouldAutoExpand()); 810 if (mExpandedView != null) { 811 mExpandedView.dump(fd, pw, args); 812 } 813 } 814 815 @Override equals(Object o)816 public boolean equals(Object o) { 817 if (this == o) return true; 818 if (!(o instanceof Bubble)) return false; 819 Bubble bubble = (Bubble) o; 820 return Objects.equals(mKey, bubble.mKey); 821 } 822 823 @Override hashCode()824 public int hashCode() { 825 return Objects.hash(mKey); 826 } 827 828 @Nullable getTitle(@onNull final BubbleEntry e)829 private static String getTitle(@NonNull final BubbleEntry e) { 830 final CharSequence titleCharSeq = e.getStatusBarNotification() 831 .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE); 832 return titleCharSeq == null ? null : titleCharSeq.toString(); 833 } 834 835 /** 836 * Returns our best guess for the most relevant text summary of the latest update to this 837 * notification, based on its type. Returns null if there should not be an update message. 838 */ 839 @NonNull extractFlyoutMessage(BubbleEntry entry)840 static Bubble.FlyoutMessage extractFlyoutMessage(BubbleEntry entry) { 841 Objects.requireNonNull(entry); 842 final Notification underlyingNotif = entry.getStatusBarNotification().getNotification(); 843 final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); 844 845 Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage(); 846 bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean( 847 Notification.EXTRA_IS_GROUP_CONVERSATION); 848 try { 849 if (Notification.BigTextStyle.class.equals(style)) { 850 // Return the big text, it is big so probably important. If it's not there use the 851 // normal text. 852 CharSequence bigText = 853 underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); 854 bubbleMessage.message = !TextUtils.isEmpty(bigText) 855 ? bigText 856 : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); 857 return bubbleMessage; 858 } else if (Notification.MessagingStyle.class.equals(style)) { 859 final List<Notification.MessagingStyle.Message> messages = 860 Notification.MessagingStyle.Message.getMessagesFromBundleArray( 861 (Parcelable[]) underlyingNotif.extras.get( 862 Notification.EXTRA_MESSAGES)); 863 864 final Notification.MessagingStyle.Message latestMessage = 865 Notification.MessagingStyle.findLatestIncomingMessage(messages); 866 if (latestMessage != null) { 867 bubbleMessage.message = latestMessage.getText(); 868 Person sender = latestMessage.getSenderPerson(); 869 bubbleMessage.senderName = sender != null ? sender.getName() : null; 870 bubbleMessage.senderAvatar = null; 871 bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null; 872 return bubbleMessage; 873 } 874 } else if (Notification.InboxStyle.class.equals(style)) { 875 CharSequence[] lines = 876 underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); 877 878 // Return the last line since it should be the most recent. 879 if (lines != null && lines.length > 0) { 880 bubbleMessage.message = lines[lines.length - 1]; 881 return bubbleMessage; 882 } 883 } else if (Notification.MediaStyle.class.equals(style)) { 884 // Return nothing, media updates aren't typically useful as a text update. 885 return bubbleMessage; 886 } else { 887 // Default to text extra. 888 bubbleMessage.message = 889 underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); 890 return bubbleMessage; 891 } 892 } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { 893 // No use crashing, we'll just return null and the caller will assume there's no update 894 // message. 895 e.printStackTrace(); 896 } 897 898 return bubbleMessage; 899 } 900 } 901