1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.statusbar.notification.row; 18 19 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; 20 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; 21 22 import android.annotation.IntDef; 23 import android.annotation.Nullable; 24 import android.app.Notification; 25 import android.content.Context; 26 import android.os.AsyncTask; 27 import android.os.CancellationSignal; 28 import android.service.notification.StatusBarNotification; 29 import android.util.ArrayMap; 30 import android.util.Log; 31 import android.view.View; 32 import android.widget.RemoteViews; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.internal.widget.ImageMessageConsumer; 36 import com.android.systemui.Dependency; 37 import com.android.systemui.statusbar.InflationTask; 38 import com.android.systemui.statusbar.SmartReplyController; 39 import com.android.systemui.statusbar.notification.InflationException; 40 import com.android.systemui.statusbar.notification.MediaNotificationProcessor; 41 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 42 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; 43 import com.android.systemui.statusbar.phone.StatusBar; 44 import com.android.systemui.statusbar.policy.HeadsUpManager; 45 import com.android.systemui.statusbar.policy.InflatedSmartReplies; 46 import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions; 47 import com.android.systemui.statusbar.policy.SmartReplyConstants; 48 import com.android.systemui.util.Assert; 49 50 import java.lang.annotation.Retention; 51 import java.lang.annotation.RetentionPolicy; 52 import java.util.HashMap; 53 54 /** 55 * A utility that inflates the right kind of contentView based on the state 56 */ 57 public class NotificationContentInflater { 58 59 public static final String TAG = "NotifContentInflater"; 60 61 @Retention(RetentionPolicy.SOURCE) 62 @IntDef(flag = true, 63 prefix = {"FLAG_CONTENT_VIEW_"}, 64 value = { 65 FLAG_CONTENT_VIEW_CONTRACTED, 66 FLAG_CONTENT_VIEW_EXPANDED, 67 FLAG_CONTENT_VIEW_HEADS_UP, 68 FLAG_CONTENT_VIEW_PUBLIC, 69 FLAG_CONTENT_VIEW_ALL}) 70 public @interface InflationFlag {} 71 /** 72 * The default, contracted view. Seen when the shade is pulled down and in the lock screen 73 * if there is no worry about content sensitivity. 74 */ 75 public static final int FLAG_CONTENT_VIEW_CONTRACTED = 1; 76 77 /** 78 * The expanded view. Seen when the user expands a notification. 79 */ 80 public static final int FLAG_CONTENT_VIEW_EXPANDED = 1 << 1; 81 82 /** 83 * The heads up view. Seen when a high priority notification peeks in from the top. 84 */ 85 public static final int FLAG_CONTENT_VIEW_HEADS_UP = 1 << 2; 86 87 /** 88 * The public view. This is a version of the contracted view that hides sensitive 89 * information and is used on the lock screen if we determine that the notification's 90 * content should be hidden. 91 */ 92 public static final int FLAG_CONTENT_VIEW_PUBLIC = 1 << 3; 93 94 public static final int FLAG_CONTENT_VIEW_ALL = ~0; 95 96 /** 97 * Content views that must be inflated at all times. 98 */ 99 @InflationFlag 100 private static final int REQUIRED_INFLATION_FLAGS = 101 FLAG_CONTENT_VIEW_CONTRACTED 102 | FLAG_CONTENT_VIEW_EXPANDED; 103 104 /** 105 * The set of content views to inflate. 106 */ 107 @InflationFlag 108 private int mInflationFlags = REQUIRED_INFLATION_FLAGS; 109 110 private final ExpandableNotificationRow mRow; 111 private boolean mIsLowPriority; 112 private boolean mUsesIncreasedHeight; 113 private boolean mUsesIncreasedHeadsUpHeight; 114 private RemoteViews.OnClickHandler mRemoteViewClickHandler; 115 private boolean mIsChildInGroup; 116 private InflationCallback mCallback; 117 private boolean mInflateSynchronously = false; 118 private final ArrayMap<Integer, RemoteViews> mCachedContentViews = new ArrayMap<>(); 119 NotificationContentInflater(ExpandableNotificationRow row)120 public NotificationContentInflater(ExpandableNotificationRow row) { 121 mRow = row; 122 } 123 setIsLowPriority(boolean isLowPriority)124 public void setIsLowPriority(boolean isLowPriority) { 125 mIsLowPriority = isLowPriority; 126 } 127 128 /** 129 * Set whether the notification is a child in a group 130 * 131 * @return whether the view was re-inflated 132 */ setIsChildInGroup(boolean childInGroup)133 public void setIsChildInGroup(boolean childInGroup) { 134 if (childInGroup != mIsChildInGroup) { 135 mIsChildInGroup = childInGroup; 136 if (mIsLowPriority) { 137 int flags = FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED; 138 inflateNotificationViews(flags); 139 } 140 } 141 } 142 setUsesIncreasedHeight(boolean usesIncreasedHeight)143 public void setUsesIncreasedHeight(boolean usesIncreasedHeight) { 144 mUsesIncreasedHeight = usesIncreasedHeight; 145 } 146 setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight)147 public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) { 148 mUsesIncreasedHeadsUpHeight = usesIncreasedHeight; 149 } 150 setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler)151 public void setRemoteViewClickHandler(RemoteViews.OnClickHandler remoteViewClickHandler) { 152 mRemoteViewClickHandler = remoteViewClickHandler; 153 } 154 155 /** 156 * Update whether or not the notification is redacted on the lock screen. If the notification 157 * is now redacted, we should inflate the public contracted view to now show on the lock screen. 158 * 159 * @param needsRedaction true if the notification should now be redacted on the lock screen 160 */ updateNeedsRedaction(boolean needsRedaction)161 public void updateNeedsRedaction(boolean needsRedaction) { 162 if (mRow.getEntry() == null) { 163 return; 164 } 165 if (needsRedaction) { 166 int flags = FLAG_CONTENT_VIEW_PUBLIC; 167 inflateNotificationViews(flags); 168 } 169 } 170 171 /** 172 * Set whether or not a particular content view is needed and whether or not it should be 173 * inflated. These flags will be used when we inflate or reinflate. 174 * 175 * @param flag the {@link InflationFlag} corresponding to the view that should/should not be 176 * inflated 177 * @param shouldInflate true if the view should be inflated, false otherwise 178 */ updateInflationFlag(@nflationFlag int flag, boolean shouldInflate)179 public void updateInflationFlag(@InflationFlag int flag, boolean shouldInflate) { 180 if (shouldInflate) { 181 mInflationFlags |= flag; 182 } else if ((REQUIRED_INFLATION_FLAGS & flag) == 0) { 183 mInflationFlags &= ~flag; 184 } 185 } 186 187 /** 188 * Convenience method for setting multiple flags at once. 189 * 190 * @param flags a set of {@link InflationFlag} corresponding to content views that should be 191 * inflated 192 */ 193 @VisibleForTesting addInflationFlags(@nflationFlag int flags)194 public void addInflationFlags(@InflationFlag int flags) { 195 mInflationFlags |= flags; 196 } 197 198 /** 199 * Whether or not the view corresponding to the flag is set to be inflated currently. 200 * 201 * @param flag the {@link InflationFlag} corresponding to the view 202 * @return true if the flag is set and view will be inflated, false o/w 203 */ isInflationFlagSet(@nflationFlag int flag)204 public boolean isInflationFlagSet(@InflationFlag int flag) { 205 return ((mInflationFlags & flag) != 0); 206 } 207 208 /** 209 * Inflate views for set flags on a background thread. This is asynchronous and will 210 * notify the callback once it's finished. 211 */ inflateNotificationViews()212 public void inflateNotificationViews() { 213 inflateNotificationViews(mInflationFlags); 214 } 215 216 /** 217 * Inflate all views for the specified flags on a background thread. This is asynchronous and 218 * will notify the callback once it's finished. If the content view is already inflated, this 219 * will reinflate it. 220 * 221 * @param reInflateFlags flags which views should be inflated. Should be a subset of 222 * {@link #mInflationFlags} as only those will be inflated/reinflated. 223 */ inflateNotificationViews(@nflationFlag int reInflateFlags)224 private void inflateNotificationViews(@InflationFlag int reInflateFlags) { 225 if (mRow.isRemoved()) { 226 // We don't want to reinflate anything for removed notifications. Otherwise views might 227 // be readded to the stack, leading to leaks. This may happen with low-priority groups 228 // where the removal of already removed children can lead to a reinflation. 229 return; 230 } 231 // Only inflate the ones that are set. 232 reInflateFlags &= mInflationFlags; 233 StatusBarNotification sbn = mRow.getEntry().notification; 234 235 // To check if the notification has inline image and preload inline image if necessary. 236 mRow.getImageResolver().preloadImages(sbn.getNotification()); 237 238 AsyncInflationTask task = new AsyncInflationTask( 239 sbn, 240 mInflateSynchronously, 241 reInflateFlags, 242 mCachedContentViews, 243 mRow, 244 mIsLowPriority, 245 mIsChildInGroup, 246 mUsesIncreasedHeight, 247 mUsesIncreasedHeadsUpHeight, 248 mCallback, 249 mRemoteViewClickHandler); 250 if (mInflateSynchronously) { 251 task.onPostExecute(task.doInBackground()); 252 } else { 253 task.execute(); 254 } 255 } 256 257 @VisibleForTesting inflateNotificationViews( boolean inflateSynchronously, @InflationFlag int reInflateFlags, Notification.Builder builder, Context packageContext)258 InflationProgress inflateNotificationViews( 259 boolean inflateSynchronously, 260 @InflationFlag int reInflateFlags, 261 Notification.Builder builder, 262 Context packageContext) { 263 InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority, 264 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, 265 packageContext); 266 result = inflateSmartReplyViews(result, reInflateFlags, mRow.getEntry(), 267 mRow.getContext(), packageContext, mRow.getHeadsUpManager(), 268 mRow.getExistingSmartRepliesAndActions()); 269 apply( 270 inflateSynchronously, 271 result, 272 reInflateFlags, 273 mCachedContentViews, 274 mRow, 275 mRemoteViewClickHandler, 276 null); 277 return result; 278 } 279 280 /** 281 * Frees the content view associated with the inflation flag. Will only succeed if the 282 * view is safe to remove. 283 * 284 * @param inflateFlag the flag corresponding to the content view which should be freed 285 */ freeNotificationView(@nflationFlag int inflateFlag)286 public void freeNotificationView(@InflationFlag int inflateFlag) { 287 if ((mInflationFlags & inflateFlag) != 0) { 288 // The view should still be inflated. 289 return; 290 } 291 switch (inflateFlag) { 292 case FLAG_CONTENT_VIEW_HEADS_UP: 293 if (mRow.getPrivateLayout().isContentViewInactive(VISIBLE_TYPE_HEADSUP)) { 294 mRow.getPrivateLayout().setHeadsUpChild(null); 295 mCachedContentViews.remove(FLAG_CONTENT_VIEW_HEADS_UP); 296 mRow.getPrivateLayout().setHeadsUpInflatedSmartReplies(null); 297 } 298 break; 299 case FLAG_CONTENT_VIEW_PUBLIC: 300 if (mRow.getPublicLayout().isContentViewInactive(VISIBLE_TYPE_CONTRACTED)) { 301 mRow.getPublicLayout().setContractedChild(null); 302 mCachedContentViews.remove(FLAG_CONTENT_VIEW_PUBLIC); 303 } 304 break; 305 case FLAG_CONTENT_VIEW_CONTRACTED: 306 case FLAG_CONTENT_VIEW_EXPANDED: 307 default: 308 break; 309 } 310 } 311 inflateSmartReplyViews(InflationProgress result, @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, Context packageContext, HeadsUpManager headsUpManager, SmartRepliesAndActions previousSmartRepliesAndActions)312 private static InflationProgress inflateSmartReplyViews(InflationProgress result, 313 @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, 314 Context packageContext, HeadsUpManager headsUpManager, 315 SmartRepliesAndActions previousSmartRepliesAndActions) { 316 SmartReplyConstants smartReplyConstants = Dependency.get(SmartReplyConstants.class); 317 SmartReplyController smartReplyController = Dependency.get(SmartReplyController.class); 318 if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 && result.newExpandedView != null) { 319 result.expandedInflatedSmartReplies = 320 InflatedSmartReplies.inflate( 321 context, packageContext, entry, smartReplyConstants, 322 smartReplyController, headsUpManager, previousSmartRepliesAndActions); 323 } 324 if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 && result.newHeadsUpView != null) { 325 result.headsUpInflatedSmartReplies = 326 InflatedSmartReplies.inflate( 327 context, packageContext, entry, smartReplyConstants, 328 smartReplyController, headsUpManager, previousSmartRepliesAndActions); 329 } 330 return result; 331 } 332 createRemoteViews(@nflationFlag int reInflateFlags, Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, Context packageContext)333 private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, 334 Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, 335 boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, 336 Context packageContext) { 337 InflationProgress result = new InflationProgress(); 338 isLowPriority = isLowPriority && !isChildInGroup; 339 340 if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 341 result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight); 342 } 343 344 if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 345 result.newExpandedView = createExpandedView(builder, isLowPriority); 346 } 347 348 if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 349 result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight); 350 } 351 352 if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 353 result.newPublicView = builder.makePublicContentView(isLowPriority); 354 } 355 356 result.packageContext = packageContext; 357 result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */); 358 result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( 359 true /* showingPublic */); 360 return result; 361 } 362 apply( boolean inflateSynchronously, InflationProgress result, @InflationFlag int reInflateFlags, ArrayMap<Integer, RemoteViews> cachedContentViews, ExpandableNotificationRow row, RemoteViews.OnClickHandler remoteViewClickHandler, @Nullable InflationCallback callback)363 public static CancellationSignal apply( 364 boolean inflateSynchronously, 365 InflationProgress result, 366 @InflationFlag int reInflateFlags, 367 ArrayMap<Integer, RemoteViews> cachedContentViews, 368 ExpandableNotificationRow row, 369 RemoteViews.OnClickHandler remoteViewClickHandler, 370 @Nullable InflationCallback callback) { 371 NotificationContentView privateLayout = row.getPrivateLayout(); 372 NotificationContentView publicLayout = row.getPublicLayout(); 373 final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>(); 374 375 int flag = FLAG_CONTENT_VIEW_CONTRACTED; 376 if ((reInflateFlags & flag) != 0) { 377 boolean isNewView = 378 !canReapplyRemoteView(result.newContentView, 379 cachedContentViews.get(FLAG_CONTENT_VIEW_CONTRACTED)); 380 ApplyCallback applyCallback = new ApplyCallback() { 381 @Override 382 public void setResultView(View v) { 383 result.inflatedContentView = v; 384 } 385 386 @Override 387 public RemoteViews getRemoteView() { 388 return result.newContentView; 389 } 390 }; 391 applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, cachedContentViews, 392 row, isNewView, remoteViewClickHandler, callback, privateLayout, 393 privateLayout.getContractedChild(), privateLayout.getVisibleWrapper( 394 NotificationContentView.VISIBLE_TYPE_CONTRACTED), 395 runningInflations, applyCallback); 396 } 397 398 flag = FLAG_CONTENT_VIEW_EXPANDED; 399 if ((reInflateFlags & flag) != 0) { 400 if (result.newExpandedView != null) { 401 boolean isNewView = 402 !canReapplyRemoteView(result.newExpandedView, 403 cachedContentViews.get(FLAG_CONTENT_VIEW_EXPANDED)); 404 ApplyCallback applyCallback = new ApplyCallback() { 405 @Override 406 public void setResultView(View v) { 407 result.inflatedExpandedView = v; 408 } 409 410 @Override 411 public RemoteViews getRemoteView() { 412 return result.newExpandedView; 413 } 414 }; 415 applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, 416 cachedContentViews, row, isNewView, remoteViewClickHandler, 417 callback, privateLayout, privateLayout.getExpandedChild(), 418 privateLayout.getVisibleWrapper( 419 NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations, 420 applyCallback); 421 } 422 } 423 424 flag = FLAG_CONTENT_VIEW_HEADS_UP; 425 if ((reInflateFlags & flag) != 0) { 426 if (result.newHeadsUpView != null) { 427 boolean isNewView = 428 !canReapplyRemoteView(result.newHeadsUpView, 429 cachedContentViews.get(FLAG_CONTENT_VIEW_HEADS_UP)); 430 ApplyCallback applyCallback = new ApplyCallback() { 431 @Override 432 public void setResultView(View v) { 433 result.inflatedHeadsUpView = v; 434 } 435 436 @Override 437 public RemoteViews getRemoteView() { 438 return result.newHeadsUpView; 439 } 440 }; 441 applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, 442 cachedContentViews, row, isNewView, remoteViewClickHandler, 443 callback, privateLayout, privateLayout.getHeadsUpChild(), 444 privateLayout.getVisibleWrapper( 445 VISIBLE_TYPE_HEADSUP), runningInflations, 446 applyCallback); 447 } 448 } 449 450 flag = FLAG_CONTENT_VIEW_PUBLIC; 451 if ((reInflateFlags & flag) != 0) { 452 boolean isNewView = 453 !canReapplyRemoteView(result.newPublicView, 454 cachedContentViews.get(FLAG_CONTENT_VIEW_PUBLIC)); 455 ApplyCallback applyCallback = new ApplyCallback() { 456 @Override 457 public void setResultView(View v) { 458 result.inflatedPublicView = v; 459 } 460 461 @Override 462 public RemoteViews getRemoteView() { 463 return result.newPublicView; 464 } 465 }; 466 applyRemoteView(inflateSynchronously, result, reInflateFlags, flag, cachedContentViews, 467 row, isNewView, remoteViewClickHandler, callback, 468 publicLayout, publicLayout.getContractedChild(), 469 publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED), 470 runningInflations, applyCallback); 471 } 472 473 // Let's try to finish, maybe nobody is even inflating anything 474 finishIfDone(result, reInflateFlags, cachedContentViews, runningInflations, callback, row); 475 CancellationSignal cancellationSignal = new CancellationSignal(); 476 cancellationSignal.setOnCancelListener( 477 () -> runningInflations.values().forEach(CancellationSignal::cancel)); 478 return cancellationSignal; 479 } 480 481 @VisibleForTesting applyRemoteView( boolean inflateSynchronously, final InflationProgress result, final @InflationFlag int reInflateFlags, @InflationFlag int inflationId, final ArrayMap<Integer, RemoteViews> cachedContentViews, final ExpandableNotificationRow row, boolean isNewView, RemoteViews.OnClickHandler remoteViewClickHandler, @Nullable final InflationCallback callback, NotificationContentView parentLayout, View existingView, NotificationViewWrapper existingWrapper, final HashMap<Integer, CancellationSignal> runningInflations, ApplyCallback applyCallback)482 static void applyRemoteView( 483 boolean inflateSynchronously, 484 final InflationProgress result, 485 final @InflationFlag int reInflateFlags, 486 @InflationFlag int inflationId, 487 final ArrayMap<Integer, RemoteViews> cachedContentViews, 488 final ExpandableNotificationRow row, 489 boolean isNewView, 490 RemoteViews.OnClickHandler remoteViewClickHandler, 491 @Nullable final InflationCallback callback, 492 NotificationContentView parentLayout, 493 View existingView, 494 NotificationViewWrapper existingWrapper, 495 final HashMap<Integer, CancellationSignal> runningInflations, 496 ApplyCallback applyCallback) { 497 RemoteViews newContentView = applyCallback.getRemoteView(); 498 if (inflateSynchronously) { 499 try { 500 if (isNewView) { 501 View v = newContentView.apply( 502 result.packageContext, 503 parentLayout, 504 remoteViewClickHandler); 505 v.setIsRootNamespace(true); 506 applyCallback.setResultView(v); 507 } else { 508 newContentView.reapply( 509 result.packageContext, 510 existingView, 511 remoteViewClickHandler); 512 existingWrapper.onReinflated(); 513 } 514 } catch (Exception e) { 515 handleInflationError(runningInflations, e, row.getStatusBarNotification(), callback); 516 // Add a running inflation to make sure we don't trigger callbacks. 517 // Safe to do because only happens in tests. 518 runningInflations.put(inflationId, new CancellationSignal()); 519 } 520 return; 521 } 522 RemoteViews.OnViewAppliedListener listener = new RemoteViews.OnViewAppliedListener() { 523 524 @Override 525 public void onViewInflated(View v) { 526 if (v instanceof ImageMessageConsumer) { 527 ((ImageMessageConsumer) v).setImageResolver(row.getImageResolver()); 528 } 529 } 530 531 @Override 532 public void onViewApplied(View v) { 533 if (isNewView) { 534 v.setIsRootNamespace(true); 535 applyCallback.setResultView(v); 536 } else if (existingWrapper != null) { 537 existingWrapper.onReinflated(); 538 } 539 runningInflations.remove(inflationId); 540 finishIfDone(result, reInflateFlags, cachedContentViews, runningInflations, 541 callback, row); 542 } 543 544 @Override 545 public void onError(Exception e) { 546 // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could 547 // actually also be a system issue, so let's try on the UI thread again to be safe. 548 try { 549 View newView = existingView; 550 if (isNewView) { 551 newView = newContentView.apply( 552 result.packageContext, 553 parentLayout, 554 remoteViewClickHandler); 555 } else { 556 newContentView.reapply( 557 result.packageContext, 558 existingView, 559 remoteViewClickHandler); 560 } 561 Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.", 562 e); 563 onViewApplied(newView); 564 } catch (Exception anotherException) { 565 runningInflations.remove(inflationId); 566 handleInflationError(runningInflations, e, row.getStatusBarNotification(), 567 callback); 568 } 569 } 570 }; 571 CancellationSignal cancellationSignal; 572 if (isNewView) { 573 cancellationSignal = newContentView.applyAsync( 574 result.packageContext, 575 parentLayout, 576 null, 577 listener, 578 remoteViewClickHandler); 579 } else { 580 cancellationSignal = newContentView.reapplyAsync( 581 result.packageContext, 582 existingView, 583 null, 584 listener, 585 remoteViewClickHandler); 586 } 587 runningInflations.put(inflationId, cancellationSignal); 588 } 589 handleInflationError( HashMap<Integer, CancellationSignal> runningInflations, Exception e, StatusBarNotification notification, @Nullable InflationCallback callback)590 private static void handleInflationError( 591 HashMap<Integer, CancellationSignal> runningInflations, Exception e, 592 StatusBarNotification notification, @Nullable InflationCallback callback) { 593 Assert.isMainThread(); 594 runningInflations.values().forEach(CancellationSignal::cancel); 595 if (callback != null) { 596 callback.handleInflationException(notification, e); 597 } 598 } 599 600 /** 601 * Finish the inflation of the views 602 * 603 * @return true if the inflation was finished 604 */ finishIfDone(InflationProgress result, @InflationFlag int reInflateFlags, ArrayMap<Integer, RemoteViews> cachedContentViews, HashMap<Integer, CancellationSignal> runningInflations, @Nullable InflationCallback endListener, ExpandableNotificationRow row)605 private static boolean finishIfDone(InflationProgress result, 606 @InflationFlag int reInflateFlags, ArrayMap<Integer, RemoteViews> cachedContentViews, 607 HashMap<Integer, CancellationSignal> runningInflations, 608 @Nullable InflationCallback endListener, ExpandableNotificationRow row) { 609 Assert.isMainThread(); 610 NotificationEntry entry = row.getEntry(); 611 NotificationContentView privateLayout = row.getPrivateLayout(); 612 NotificationContentView publicLayout = row.getPublicLayout(); 613 if (runningInflations.isEmpty()) { 614 if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 615 if (result.inflatedContentView != null) { 616 // New view case 617 privateLayout.setContractedChild(result.inflatedContentView); 618 cachedContentViews.put(FLAG_CONTENT_VIEW_CONTRACTED, result.newContentView); 619 } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_CONTRACTED) != null) { 620 // Reinflation case. Only update if it's still cached (i.e. view has not been 621 // freed while inflating). 622 cachedContentViews.put(FLAG_CONTENT_VIEW_CONTRACTED, result.newContentView); 623 } 624 } 625 626 if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 627 if (result.inflatedExpandedView != null) { 628 privateLayout.setExpandedChild(result.inflatedExpandedView); 629 cachedContentViews.put(FLAG_CONTENT_VIEW_EXPANDED, result.newExpandedView); 630 } else if (result.newExpandedView == null) { 631 privateLayout.setExpandedChild(null); 632 cachedContentViews.put(FLAG_CONTENT_VIEW_EXPANDED, null); 633 } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_EXPANDED) != null) { 634 cachedContentViews.put(FLAG_CONTENT_VIEW_EXPANDED, result.newExpandedView); 635 } 636 if (result.newExpandedView != null) { 637 privateLayout.setExpandedInflatedSmartReplies( 638 result.expandedInflatedSmartReplies); 639 } else { 640 privateLayout.setExpandedInflatedSmartReplies(null); 641 } 642 row.setExpandable(result.newExpandedView != null); 643 } 644 645 if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 646 if (result.inflatedHeadsUpView != null) { 647 privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); 648 cachedContentViews.put(FLAG_CONTENT_VIEW_HEADS_UP, result.newHeadsUpView); 649 } else if (result.newHeadsUpView == null) { 650 privateLayout.setHeadsUpChild(null); 651 cachedContentViews.put(FLAG_CONTENT_VIEW_HEADS_UP, null); 652 } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_HEADS_UP) != null) { 653 cachedContentViews.put(FLAG_CONTENT_VIEW_HEADS_UP, result.newHeadsUpView); 654 } 655 if (result.newHeadsUpView != null) { 656 privateLayout.setHeadsUpInflatedSmartReplies( 657 result.headsUpInflatedSmartReplies); 658 } else { 659 privateLayout.setHeadsUpInflatedSmartReplies(null); 660 } 661 } 662 663 if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 664 if (result.inflatedPublicView != null) { 665 publicLayout.setContractedChild(result.inflatedPublicView); 666 cachedContentViews.put(FLAG_CONTENT_VIEW_PUBLIC, result.newPublicView); 667 } else if (cachedContentViews.get(FLAG_CONTENT_VIEW_PUBLIC) != null) { 668 cachedContentViews.put(FLAG_CONTENT_VIEW_PUBLIC, result.newPublicView); 669 } 670 } 671 672 entry.headsUpStatusBarText = result.headsUpStatusBarText; 673 entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic; 674 if (endListener != null) { 675 endListener.onAsyncInflationFinished(row.getEntry(), reInflateFlags); 676 } 677 return true; 678 } 679 return false; 680 } 681 createExpandedView(Notification.Builder builder, boolean isLowPriority)682 private static RemoteViews createExpandedView(Notification.Builder builder, 683 boolean isLowPriority) { 684 RemoteViews bigContentView = builder.createBigContentView(); 685 if (bigContentView != null) { 686 return bigContentView; 687 } 688 if (isLowPriority) { 689 RemoteViews contentView = builder.createContentView(); 690 Notification.Builder.makeHeaderExpanded(contentView); 691 return contentView; 692 } 693 return null; 694 } 695 createContentView(Notification.Builder builder, boolean isLowPriority, boolean useLarge)696 private static RemoteViews createContentView(Notification.Builder builder, 697 boolean isLowPriority, boolean useLarge) { 698 if (isLowPriority) { 699 return builder.makeLowPriorityContentView(false /* useRegularSubtext */); 700 } 701 return builder.createContentView(useLarge); 702 } 703 704 /** 705 * @param newView The new view that will be applied 706 * @param oldView The old view that was applied to the existing view before 707 * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply. 708 */ 709 @VisibleForTesting canReapplyRemoteView(final RemoteViews newView, final RemoteViews oldView)710 static boolean canReapplyRemoteView(final RemoteViews newView, 711 final RemoteViews oldView) { 712 return (newView == null && oldView == null) || 713 (newView != null && oldView != null 714 && oldView.getPackage() != null 715 && newView.getPackage() != null 716 && newView.getPackage().equals(oldView.getPackage()) 717 && newView.getLayoutId() == oldView.getLayoutId() 718 && !oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED)); 719 } 720 setInflationCallback(InflationCallback callback)721 public void setInflationCallback(InflationCallback callback) { 722 mCallback = callback; 723 } 724 725 public interface InflationCallback { handleInflationException(StatusBarNotification notification, Exception e)726 void handleInflationException(StatusBarNotification notification, Exception e); 727 728 /** 729 * Callback for after the content views finish inflating. 730 * 731 * @param entry the entry with the content views set 732 * @param inflatedFlags the flags associated with the content views that were inflated 733 */ onAsyncInflationFinished(NotificationEntry entry, @InflationFlag int inflatedFlags)734 void onAsyncInflationFinished(NotificationEntry entry, @InflationFlag int inflatedFlags); 735 } 736 clearCachesAndReInflate()737 public void clearCachesAndReInflate() { 738 mCachedContentViews.clear(); 739 inflateNotificationViews(); 740 } 741 742 /** 743 * Sets whether to perform inflation on the same thread as the caller. This method should only 744 * be used in tests, not in production. 745 */ 746 @VisibleForTesting setInflateSynchronously(boolean inflateSynchronously)747 void setInflateSynchronously(boolean inflateSynchronously) { 748 mInflateSynchronously = inflateSynchronously; 749 } 750 751 public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress> 752 implements InflationCallback, InflationTask { 753 754 private final StatusBarNotification mSbn; 755 private final Context mContext; 756 private final boolean mInflateSynchronously; 757 private final boolean mIsLowPriority; 758 private final boolean mIsChildInGroup; 759 private final boolean mUsesIncreasedHeight; 760 private final InflationCallback mCallback; 761 private final boolean mUsesIncreasedHeadsUpHeight; 762 private @InflationFlag int mReInflateFlags; 763 private final ArrayMap<Integer, RemoteViews> mCachedContentViews; 764 private ExpandableNotificationRow mRow; 765 private Exception mError; 766 private RemoteViews.OnClickHandler mRemoteViewClickHandler; 767 private CancellationSignal mCancellationSignal; 768 AsyncInflationTask( StatusBarNotification notification, boolean inflateSynchronously, @InflationFlag int reInflateFlags, ArrayMap<Integer, RemoteViews> cachedContentViews, ExpandableNotificationRow row, boolean isLowPriority, boolean isChildInGroup, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, InflationCallback callback, RemoteViews.OnClickHandler remoteViewClickHandler)769 private AsyncInflationTask( 770 StatusBarNotification notification, 771 boolean inflateSynchronously, 772 @InflationFlag int reInflateFlags, 773 ArrayMap<Integer, RemoteViews> cachedContentViews, 774 ExpandableNotificationRow row, 775 boolean isLowPriority, 776 boolean isChildInGroup, 777 boolean usesIncreasedHeight, 778 boolean usesIncreasedHeadsUpHeight, 779 InflationCallback callback, 780 RemoteViews.OnClickHandler remoteViewClickHandler) { 781 mRow = row; 782 mSbn = notification; 783 mInflateSynchronously = inflateSynchronously; 784 mReInflateFlags = reInflateFlags; 785 mCachedContentViews = cachedContentViews; 786 mContext = mRow.getContext(); 787 mIsLowPriority = isLowPriority; 788 mIsChildInGroup = isChildInGroup; 789 mUsesIncreasedHeight = usesIncreasedHeight; 790 mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; 791 mRemoteViewClickHandler = remoteViewClickHandler; 792 mCallback = callback; 793 NotificationEntry entry = row.getEntry(); 794 entry.setInflationTask(this); 795 } 796 797 @VisibleForTesting 798 @InflationFlag getReInflateFlags()799 public int getReInflateFlags() { 800 return mReInflateFlags; 801 } 802 803 @Override doInBackground(Void... params)804 protected InflationProgress doInBackground(Void... params) { 805 try { 806 final Notification.Builder recoveredBuilder 807 = Notification.Builder.recoverBuilder(mContext, 808 mSbn.getNotification()); 809 810 Context packageContext = mSbn.getPackageContext(mContext); 811 Notification notification = mSbn.getNotification(); 812 if (notification.isMediaNotification()) { 813 MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext, 814 packageContext); 815 processor.processNotification(notification, recoveredBuilder); 816 } 817 InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, 818 recoveredBuilder, mIsLowPriority, mIsChildInGroup, mUsesIncreasedHeight, 819 mUsesIncreasedHeadsUpHeight, packageContext); 820 return inflateSmartReplyViews(inflationProgress, mReInflateFlags, mRow.getEntry(), 821 mRow.getContext(), packageContext, mRow.getHeadsUpManager(), 822 mRow.getExistingSmartRepliesAndActions()); 823 } catch (Exception e) { 824 mError = e; 825 return null; 826 } 827 } 828 829 @Override onPostExecute(InflationProgress result)830 protected void onPostExecute(InflationProgress result) { 831 if (mError == null) { 832 mCancellationSignal = apply(mInflateSynchronously, result, mReInflateFlags, 833 mCachedContentViews, mRow, mRemoteViewClickHandler, this); 834 } else { 835 handleError(mError); 836 } 837 } 838 handleError(Exception e)839 private void handleError(Exception e) { 840 mRow.getEntry().onInflationTaskFinished(); 841 StatusBarNotification sbn = mRow.getStatusBarNotification(); 842 final String ident = sbn.getPackageName() + "/0x" 843 + Integer.toHexString(sbn.getId()); 844 Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e); 845 mCallback.handleInflationException(sbn, 846 new InflationException("Couldn't inflate contentViews" + e)); 847 } 848 849 @Override abort()850 public void abort() { 851 cancel(true /* mayInterruptIfRunning */); 852 if (mCancellationSignal != null) { 853 mCancellationSignal.cancel(); 854 } 855 } 856 857 @Override supersedeTask(InflationTask task)858 public void supersedeTask(InflationTask task) { 859 if (task instanceof AsyncInflationTask) { 860 // We want to inflate all flags of the previous task as well 861 mReInflateFlags |= ((AsyncInflationTask) task).mReInflateFlags; 862 } 863 } 864 865 @Override handleInflationException(StatusBarNotification notification, Exception e)866 public void handleInflationException(StatusBarNotification notification, Exception e) { 867 handleError(e); 868 } 869 870 @Override onAsyncInflationFinished(NotificationEntry entry, @InflationFlag int inflatedFlags)871 public void onAsyncInflationFinished(NotificationEntry entry, 872 @InflationFlag int inflatedFlags) { 873 mRow.getEntry().onInflationTaskFinished(); 874 mRow.onNotificationUpdated(); 875 mCallback.onAsyncInflationFinished(mRow.getEntry(), inflatedFlags); 876 877 // Notify the resolver that the inflation task has finished, 878 // try to purge unnecessary cached entries. 879 mRow.getImageResolver().purgeCache(); 880 } 881 } 882 883 @VisibleForTesting 884 static class InflationProgress { 885 private RemoteViews newContentView; 886 private RemoteViews newHeadsUpView; 887 private RemoteViews newExpandedView; 888 private RemoteViews newPublicView; 889 890 @VisibleForTesting 891 Context packageContext; 892 893 private View inflatedContentView; 894 private View inflatedHeadsUpView; 895 private View inflatedExpandedView; 896 private View inflatedPublicView; 897 private CharSequence headsUpStatusBarText; 898 private CharSequence headsUpStatusBarTextPublic; 899 900 private InflatedSmartReplies expandedInflatedSmartReplies; 901 private InflatedSmartReplies headsUpInflatedSmartReplies; 902 } 903 904 @VisibleForTesting 905 abstract static class ApplyCallback { setResultView(View v)906 public abstract void setResultView(View v); getRemoteView()907 public abstract RemoteViews getRemoteView(); 908 } 909 } 910