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