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.internal.annotations.VisibleForTesting.Visibility.PACKAGE; 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_EXPANDED; 22 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.app.Notification; 27 import android.content.Context; 28 import android.content.ContextWrapper; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.PackageManager; 31 import android.content.res.Resources; 32 import android.os.AsyncTask; 33 import android.os.Build; 34 import android.os.CancellationSignal; 35 import android.os.Trace; 36 import android.os.UserHandle; 37 import android.service.notification.StatusBarNotification; 38 import android.util.Log; 39 import android.view.View; 40 import android.widget.RemoteViews; 41 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.internal.widget.ImageMessageConsumer; 44 import com.android.systemui.R; 45 import com.android.systemui.dagger.SysUISingleton; 46 import com.android.systemui.dagger.qualifiers.Background; 47 import com.android.systemui.media.controls.util.MediaFeatureFlag; 48 import com.android.systemui.statusbar.InflationTask; 49 import com.android.systemui.statusbar.NotificationRemoteInputManager; 50 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; 51 import com.android.systemui.statusbar.notification.InflationException; 52 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 53 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; 54 import com.android.systemui.statusbar.phone.CentralSurfaces; 55 import com.android.systemui.statusbar.policy.InflatedSmartReplyState; 56 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder; 57 import com.android.systemui.statusbar.policy.SmartReplyStateInflater; 58 import com.android.systemui.util.Assert; 59 60 import java.util.HashMap; 61 import java.util.concurrent.Executor; 62 63 import javax.inject.Inject; 64 65 /** 66 * {@link NotificationContentInflater} binds content to a {@link ExpandableNotificationRow} by 67 * asynchronously building the content's {@link RemoteViews} and applying it to the row. 68 */ 69 @SysUISingleton 70 @VisibleForTesting(visibility = PACKAGE) 71 public class NotificationContentInflater implements NotificationRowContentBinder { 72 73 public static final String TAG = "NotifContentInflater"; 74 75 private boolean mInflateSynchronously = false; 76 private final boolean mIsMediaInQS; 77 private final NotificationRemoteInputManager mRemoteInputManager; 78 private final NotifRemoteViewCache mRemoteViewCache; 79 private final ConversationNotificationProcessor mConversationProcessor; 80 private final Executor mBgExecutor; 81 private final SmartReplyStateInflater mSmartReplyStateInflater; 82 83 @Inject NotificationContentInflater( NotifRemoteViewCache remoteViewCache, NotificationRemoteInputManager remoteInputManager, ConversationNotificationProcessor conversationProcessor, MediaFeatureFlag mediaFeatureFlag, @Background Executor bgExecutor, SmartReplyStateInflater smartRepliesInflater)84 NotificationContentInflater( 85 NotifRemoteViewCache remoteViewCache, 86 NotificationRemoteInputManager remoteInputManager, 87 ConversationNotificationProcessor conversationProcessor, 88 MediaFeatureFlag mediaFeatureFlag, 89 @Background Executor bgExecutor, 90 SmartReplyStateInflater smartRepliesInflater) { 91 mRemoteViewCache = remoteViewCache; 92 mRemoteInputManager = remoteInputManager; 93 mConversationProcessor = conversationProcessor; 94 mIsMediaInQS = mediaFeatureFlag.getEnabled(); 95 mBgExecutor = bgExecutor; 96 mSmartReplyStateInflater = smartRepliesInflater; 97 } 98 99 @Override bindContent( NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int contentToBind, BindParams bindParams, boolean forceInflate, @Nullable InflationCallback callback)100 public void bindContent( 101 NotificationEntry entry, 102 ExpandableNotificationRow row, 103 @InflationFlag int contentToBind, 104 BindParams bindParams, 105 boolean forceInflate, 106 @Nullable InflationCallback callback) { 107 if (row.isRemoved()) { 108 // We don't want to reinflate anything for removed notifications. Otherwise views might 109 // be readded to the stack, leading to leaks. This may happen with low-priority groups 110 // where the removal of already removed children can lead to a reinflation. 111 return; 112 } 113 114 StatusBarNotification sbn = entry.getSbn(); 115 116 // To check if the notification has inline image and preload inline image if necessary. 117 row.getImageResolver().preloadImages(sbn.getNotification()); 118 119 if (forceInflate) { 120 mRemoteViewCache.clearCache(entry); 121 } 122 123 // Cancel any pending frees on any view we're trying to bind since we should be bound after. 124 cancelContentViewFrees(row, contentToBind); 125 126 AsyncInflationTask task = new AsyncInflationTask( 127 mBgExecutor, 128 mInflateSynchronously, 129 contentToBind, 130 mRemoteViewCache, 131 entry, 132 mConversationProcessor, 133 row, 134 bindParams.isLowPriority, 135 bindParams.usesIncreasedHeight, 136 bindParams.usesIncreasedHeadsUpHeight, 137 callback, 138 mRemoteInputManager.getRemoteViewsOnClickHandler(), 139 mIsMediaInQS, 140 mSmartReplyStateInflater); 141 if (mInflateSynchronously) { 142 task.onPostExecute(task.doInBackground()); 143 } else { 144 task.executeOnExecutor(mBgExecutor); 145 } 146 } 147 148 @VisibleForTesting inflateNotificationViews( NotificationEntry entry, ExpandableNotificationRow row, BindParams bindParams, boolean inflateSynchronously, @InflationFlag int reInflateFlags, Notification.Builder builder, Context packageContext, SmartReplyStateInflater smartRepliesInflater)149 InflationProgress inflateNotificationViews( 150 NotificationEntry entry, 151 ExpandableNotificationRow row, 152 BindParams bindParams, 153 boolean inflateSynchronously, 154 @InflationFlag int reInflateFlags, 155 Notification.Builder builder, 156 Context packageContext, 157 SmartReplyStateInflater smartRepliesInflater) { 158 InflationProgress result = createRemoteViews(reInflateFlags, 159 builder, 160 bindParams.isLowPriority, 161 bindParams.usesIncreasedHeight, 162 bindParams.usesIncreasedHeadsUpHeight, 163 packageContext); 164 165 result = inflateSmartReplyViews(result, reInflateFlags, entry, 166 row.getContext(), packageContext, 167 row.getExistingSmartReplyState(), 168 smartRepliesInflater); 169 170 apply( 171 mBgExecutor, 172 inflateSynchronously, 173 result, 174 reInflateFlags, 175 mRemoteViewCache, 176 entry, 177 row, 178 mRemoteInputManager.getRemoteViewsOnClickHandler(), 179 null); 180 return result; 181 } 182 183 @Override cancelBind( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row)184 public void cancelBind( 185 @NonNull NotificationEntry entry, 186 @NonNull ExpandableNotificationRow row) { 187 entry.abortTask(); 188 } 189 190 @Override unbindContent( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row, @InflationFlag int contentToUnbind)191 public void unbindContent( 192 @NonNull NotificationEntry entry, 193 @NonNull ExpandableNotificationRow row, 194 @InflationFlag int contentToUnbind) { 195 int curFlag = 1; 196 while (contentToUnbind != 0) { 197 if ((contentToUnbind & curFlag) != 0) { 198 freeNotificationView(entry, row, curFlag); 199 } 200 contentToUnbind &= ~curFlag; 201 curFlag = curFlag << 1; 202 } 203 } 204 205 /** 206 * Frees the content view associated with the inflation flag as soon as the view is not showing. 207 * 208 * @param inflateFlag the flag corresponding to the content view which should be freed 209 */ freeNotificationView( NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int inflateFlag)210 private void freeNotificationView( 211 NotificationEntry entry, 212 ExpandableNotificationRow row, 213 @InflationFlag int inflateFlag) { 214 switch (inflateFlag) { 215 case FLAG_CONTENT_VIEW_CONTRACTED: 216 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> { 217 row.getPrivateLayout().setContractedChild(null); 218 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED); 219 }); 220 break; 221 case FLAG_CONTENT_VIEW_EXPANDED: 222 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_EXPANDED, () -> { 223 row.getPrivateLayout().setExpandedChild(null); 224 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); 225 }); 226 break; 227 case FLAG_CONTENT_VIEW_HEADS_UP: 228 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_HEADSUP, () -> { 229 row.getPrivateLayout().setHeadsUpChild(null); 230 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP); 231 row.getPrivateLayout().setHeadsUpInflatedSmartReplies(null); 232 }); 233 break; 234 case FLAG_CONTENT_VIEW_PUBLIC: 235 row.getPublicLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> { 236 row.getPublicLayout().setContractedChild(null); 237 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC); 238 }); 239 break; 240 default: 241 break; 242 } 243 } 244 245 /** 246 * Cancel any pending content view frees from {@link #freeNotificationView} for the provided 247 * content views. 248 * 249 * @param row top level notification row containing the content views 250 * @param contentViews content views to cancel pending frees on 251 */ cancelContentViewFrees( ExpandableNotificationRow row, @InflationFlag int contentViews)252 private void cancelContentViewFrees( 253 ExpandableNotificationRow row, 254 @InflationFlag int contentViews) { 255 if ((contentViews & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 256 row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED); 257 } 258 if ((contentViews & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 259 row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_EXPANDED); 260 } 261 if ((contentViews & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 262 row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_HEADSUP); 263 } 264 if ((contentViews & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 265 row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED); 266 } 267 } 268 inflateSmartReplyViews( InflationProgress result, @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, Context packageContext, InflatedSmartReplyState previousSmartReplyState, SmartReplyStateInflater inflater)269 private static InflationProgress inflateSmartReplyViews( 270 InflationProgress result, 271 @InflationFlag int reInflateFlags, 272 NotificationEntry entry, 273 Context context, 274 Context packageContext, 275 InflatedSmartReplyState previousSmartReplyState, 276 SmartReplyStateInflater inflater) { 277 boolean inflateContracted = (reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0 278 && result.newContentView != null; 279 boolean inflateExpanded = (reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 280 && result.newExpandedView != null; 281 boolean inflateHeadsUp = (reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 282 && result.newHeadsUpView != null; 283 if (inflateContracted || inflateExpanded || inflateHeadsUp) { 284 result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry); 285 } 286 if (inflateExpanded) { 287 result.expandedInflatedSmartReplies = inflater.inflateSmartReplyViewHolder( 288 context, packageContext, entry, previousSmartReplyState, 289 result.inflatedSmartReplyState); 290 } 291 if (inflateHeadsUp) { 292 result.headsUpInflatedSmartReplies = inflater.inflateSmartReplyViewHolder( 293 context, packageContext, entry, previousSmartReplyState, 294 result.inflatedSmartReplyState); 295 } 296 return result; 297 } 298 createRemoteViews(@nflationFlag int reInflateFlags, Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, Context packageContext)299 private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, 300 Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight, 301 boolean usesIncreasedHeadsUpHeight, Context packageContext) { 302 InflationProgress result = new InflationProgress(); 303 304 if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 305 result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight); 306 } 307 308 if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 309 result.newExpandedView = createExpandedView(builder, isLowPriority); 310 } 311 312 if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 313 result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight); 314 } 315 316 if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 317 result.newPublicView = builder.makePublicContentView(isLowPriority); 318 } 319 320 result.packageContext = packageContext; 321 result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */); 322 result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( 323 true /* showingPublic */); 324 return result; 325 } 326 apply( Executor bgExecutor, boolean inflateSynchronously, InflationProgress result, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, NotificationEntry entry, ExpandableNotificationRow row, RemoteViews.InteractionHandler remoteViewClickHandler, @Nullable InflationCallback callback)327 private static CancellationSignal apply( 328 Executor bgExecutor, 329 boolean inflateSynchronously, 330 InflationProgress result, 331 @InflationFlag int reInflateFlags, 332 NotifRemoteViewCache remoteViewCache, 333 NotificationEntry entry, 334 ExpandableNotificationRow row, 335 RemoteViews.InteractionHandler remoteViewClickHandler, 336 @Nullable InflationCallback callback) { 337 NotificationContentView privateLayout = row.getPrivateLayout(); 338 NotificationContentView publicLayout = row.getPublicLayout(); 339 final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>(); 340 341 int flag = FLAG_CONTENT_VIEW_CONTRACTED; 342 if ((reInflateFlags & flag) != 0) { 343 boolean isNewView = 344 !canReapplyRemoteView(result.newContentView, 345 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)); 346 ApplyCallback applyCallback = new ApplyCallback() { 347 @Override 348 public void setResultView(View v) { 349 result.inflatedContentView = v; 350 } 351 352 @Override 353 public RemoteViews getRemoteView() { 354 return result.newContentView; 355 } 356 }; 357 applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, 358 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, 359 privateLayout, privateLayout.getContractedChild(), 360 privateLayout.getVisibleWrapper( 361 NotificationContentView.VISIBLE_TYPE_CONTRACTED), 362 runningInflations, applyCallback); 363 } 364 365 flag = FLAG_CONTENT_VIEW_EXPANDED; 366 if ((reInflateFlags & flag) != 0) { 367 if (result.newExpandedView != null) { 368 boolean isNewView = 369 !canReapplyRemoteView(result.newExpandedView, 370 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)); 371 ApplyCallback applyCallback = new ApplyCallback() { 372 @Override 373 public void setResultView(View v) { 374 result.inflatedExpandedView = v; 375 } 376 377 @Override 378 public RemoteViews getRemoteView() { 379 return result.newExpandedView; 380 } 381 }; 382 applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, 383 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, 384 callback, privateLayout, privateLayout.getExpandedChild(), 385 privateLayout.getVisibleWrapper( 386 NotificationContentView.VISIBLE_TYPE_EXPANDED), runningInflations, 387 applyCallback); 388 } 389 } 390 391 flag = FLAG_CONTENT_VIEW_HEADS_UP; 392 if ((reInflateFlags & flag) != 0) { 393 if (result.newHeadsUpView != null) { 394 boolean isNewView = 395 !canReapplyRemoteView(result.newHeadsUpView, 396 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)); 397 ApplyCallback applyCallback = new ApplyCallback() { 398 @Override 399 public void setResultView(View v) { 400 result.inflatedHeadsUpView = v; 401 } 402 403 @Override 404 public RemoteViews getRemoteView() { 405 return result.newHeadsUpView; 406 } 407 }; 408 applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, 409 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, 410 callback, privateLayout, privateLayout.getHeadsUpChild(), 411 privateLayout.getVisibleWrapper( 412 VISIBLE_TYPE_HEADSUP), runningInflations, 413 applyCallback); 414 } 415 } 416 417 flag = FLAG_CONTENT_VIEW_PUBLIC; 418 if ((reInflateFlags & flag) != 0) { 419 boolean isNewView = 420 !canReapplyRemoteView(result.newPublicView, 421 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)); 422 ApplyCallback applyCallback = new ApplyCallback() { 423 @Override 424 public void setResultView(View v) { 425 result.inflatedPublicView = v; 426 } 427 428 @Override 429 public RemoteViews getRemoteView() { 430 return result.newPublicView; 431 } 432 }; 433 applyRemoteView(bgExecutor, inflateSynchronously, result, reInflateFlags, flag, 434 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, 435 publicLayout, publicLayout.getContractedChild(), 436 publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED), 437 runningInflations, applyCallback); 438 } 439 440 // Let's try to finish, maybe nobody is even inflating anything 441 finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, callback, entry, 442 row); 443 CancellationSignal cancellationSignal = new CancellationSignal(); 444 cancellationSignal.setOnCancelListener( 445 () -> runningInflations.values().forEach(CancellationSignal::cancel)); 446 447 return cancellationSignal; 448 } 449 450 @VisibleForTesting applyRemoteView( Executor bgExecutor, boolean inflateSynchronously, final InflationProgress result, final @InflationFlag int reInflateFlags, @InflationFlag int inflationId, final NotifRemoteViewCache remoteViewCache, final NotificationEntry entry, final ExpandableNotificationRow row, boolean isNewView, RemoteViews.InteractionHandler remoteViewClickHandler, @Nullable final InflationCallback callback, NotificationContentView parentLayout, View existingView, NotificationViewWrapper existingWrapper, final HashMap<Integer, CancellationSignal> runningInflations, ApplyCallback applyCallback)451 static void applyRemoteView( 452 Executor bgExecutor, 453 boolean inflateSynchronously, 454 final InflationProgress result, 455 final @InflationFlag int reInflateFlags, 456 @InflationFlag int inflationId, 457 final NotifRemoteViewCache remoteViewCache, 458 final NotificationEntry entry, 459 final ExpandableNotificationRow row, 460 boolean isNewView, 461 RemoteViews.InteractionHandler remoteViewClickHandler, 462 @Nullable final InflationCallback callback, 463 NotificationContentView parentLayout, 464 View existingView, 465 NotificationViewWrapper existingWrapper, 466 final HashMap<Integer, CancellationSignal> runningInflations, 467 ApplyCallback applyCallback) { 468 RemoteViews newContentView = applyCallback.getRemoteView(); 469 if (inflateSynchronously) { 470 try { 471 if (isNewView) { 472 View v = newContentView.apply( 473 result.packageContext, 474 parentLayout, 475 remoteViewClickHandler); 476 validateView(v, entry, row.getResources()); 477 v.setIsRootNamespace(true); 478 applyCallback.setResultView(v); 479 } else { 480 newContentView.reapply( 481 result.packageContext, 482 existingView, 483 remoteViewClickHandler); 484 validateView(existingView, entry, row.getResources()); 485 existingWrapper.onReinflated(); 486 } 487 } catch (Exception e) { 488 handleInflationError(runningInflations, e, row.getEntry(), callback); 489 // Add a running inflation to make sure we don't trigger callbacks. 490 // Safe to do because only happens in tests. 491 runningInflations.put(inflationId, new CancellationSignal()); 492 } 493 return; 494 } 495 RemoteViews.OnViewAppliedListener listener = new RemoteViews.OnViewAppliedListener() { 496 497 @Override 498 public void onViewInflated(View v) { 499 if (v instanceof ImageMessageConsumer) { 500 ((ImageMessageConsumer) v).setImageResolver(row.getImageResolver()); 501 } 502 } 503 504 @Override 505 public void onViewApplied(View v) { 506 String invalidReason = isValidView(v, entry, row.getResources()); 507 if (invalidReason != null) { 508 handleInflationError(runningInflations, new InflationException(invalidReason), 509 row.getEntry(), callback); 510 runningInflations.remove(inflationId); 511 return; 512 } 513 if (isNewView) { 514 v.setIsRootNamespace(true); 515 applyCallback.setResultView(v); 516 } else if (existingWrapper != null) { 517 existingWrapper.onReinflated(); 518 } 519 runningInflations.remove(inflationId); 520 finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, 521 callback, entry, row); 522 } 523 524 @Override 525 public void onError(Exception e) { 526 // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could 527 // actually also be a system issue, so let's try on the UI thread again to be safe. 528 try { 529 View newView = existingView; 530 if (isNewView) { 531 newView = newContentView.apply( 532 result.packageContext, 533 parentLayout, 534 remoteViewClickHandler); 535 } else { 536 newContentView.reapply( 537 result.packageContext, 538 existingView, 539 remoteViewClickHandler); 540 } 541 Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.", 542 e); 543 onViewApplied(newView); 544 } catch (Exception anotherException) { 545 runningInflations.remove(inflationId); 546 handleInflationError(runningInflations, e, row.getEntry(), 547 callback); 548 } 549 } 550 }; 551 CancellationSignal cancellationSignal; 552 if (isNewView) { 553 cancellationSignal = newContentView.applyAsync( 554 result.packageContext, 555 parentLayout, 556 bgExecutor, 557 listener, 558 remoteViewClickHandler); 559 } else { 560 cancellationSignal = newContentView.reapplyAsync( 561 result.packageContext, 562 existingView, 563 bgExecutor, 564 listener, 565 remoteViewClickHandler); 566 } 567 runningInflations.put(inflationId, cancellationSignal); 568 } 569 570 /** 571 * Checks if the given View is a valid notification View. 572 * 573 * @return null == valid, non-null == invalid, String represents reason for rejection. 574 */ 575 @VisibleForTesting 576 @Nullable isValidView(View view, NotificationEntry entry, Resources resources)577 static String isValidView(View view, 578 NotificationEntry entry, 579 Resources resources) { 580 if (!satisfiesMinHeightRequirement(view, entry, resources)) { 581 return "inflated notification does not meet minimum height requirement"; 582 } 583 return null; 584 } 585 satisfiesMinHeightRequirement(View view, NotificationEntry entry, Resources resources)586 private static boolean satisfiesMinHeightRequirement(View view, 587 NotificationEntry entry, 588 Resources resources) { 589 if (!requiresHeightCheck(entry)) { 590 return true; 591 } 592 Trace.beginSection("NotificationContentInflater#satisfiesMinHeightRequirement"); 593 int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 594 int referenceWidth = resources.getDimensionPixelSize( 595 R.dimen.notification_validation_reference_width); 596 int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth, View.MeasureSpec.EXACTLY); 597 view.measure(widthSpec, heightSpec); 598 int minHeight = resources.getDimensionPixelSize( 599 R.dimen.notification_validation_minimum_allowed_height); 600 boolean result = view.getMeasuredHeight() >= minHeight; 601 Trace.endSection(); 602 return result; 603 } 604 requiresHeightCheck(NotificationEntry entry)605 private static boolean requiresHeightCheck(NotificationEntry entry) { 606 // Undecorated custom views are disallowed from S onwards 607 if (entry.targetSdk >= Build.VERSION_CODES.S) { 608 return false; 609 } 610 // No need to check if the app isn't using any custom views 611 Notification notification = entry.getSbn().getNotification(); 612 if (notification.contentView == null 613 && notification.bigContentView == null 614 && notification.headsUpContentView == null) { 615 return false; 616 } 617 return true; 618 } 619 validateView(View view, NotificationEntry entry, Resources resources)620 private static void validateView(View view, 621 NotificationEntry entry, 622 Resources resources) throws InflationException { 623 String invalidReason = isValidView(view, entry, resources); 624 if (invalidReason != null) { 625 throw new InflationException(invalidReason); 626 } 627 } 628 handleInflationError( HashMap<Integer, CancellationSignal> runningInflations, Exception e, NotificationEntry notification, @Nullable InflationCallback callback)629 private static void handleInflationError( 630 HashMap<Integer, CancellationSignal> runningInflations, Exception e, 631 NotificationEntry notification, @Nullable InflationCallback callback) { 632 Assert.isMainThread(); 633 runningInflations.values().forEach(CancellationSignal::cancel); 634 if (callback != null) { 635 callback.handleInflationException(notification, e); 636 } 637 } 638 639 /** 640 * Finish the inflation of the views 641 * 642 * @return true if the inflation was finished 643 */ finishIfDone(InflationProgress result, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, HashMap<Integer, CancellationSignal> runningInflations, @Nullable InflationCallback endListener, NotificationEntry entry, ExpandableNotificationRow row)644 private static boolean finishIfDone(InflationProgress result, 645 @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, 646 HashMap<Integer, CancellationSignal> runningInflations, 647 @Nullable InflationCallback endListener, NotificationEntry entry, 648 ExpandableNotificationRow row) { 649 Assert.isMainThread(); 650 NotificationContentView privateLayout = row.getPrivateLayout(); 651 NotificationContentView publicLayout = row.getPublicLayout(); 652 if (runningInflations.isEmpty()) { 653 boolean setRepliesAndActions = true; 654 if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 655 if (result.inflatedContentView != null) { 656 // New view case 657 privateLayout.setContractedChild(result.inflatedContentView); 658 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, 659 result.newContentView); 660 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) { 661 // Reinflation case. Only update if it's still cached (i.e. view has not been 662 // freed while inflating). 663 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, 664 result.newContentView); 665 } 666 setRepliesAndActions = true; 667 } 668 669 if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 670 if (result.inflatedExpandedView != null) { 671 privateLayout.setExpandedChild(result.inflatedExpandedView); 672 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, 673 result.newExpandedView); 674 } else if (result.newExpandedView == null) { 675 privateLayout.setExpandedChild(null); 676 remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); 677 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) { 678 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, 679 result.newExpandedView); 680 } 681 if (result.newExpandedView != null) { 682 privateLayout.setExpandedInflatedSmartReplies( 683 result.expandedInflatedSmartReplies); 684 } else { 685 privateLayout.setExpandedInflatedSmartReplies(null); 686 } 687 row.setExpandable(result.newExpandedView != null); 688 setRepliesAndActions = true; 689 } 690 691 if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 692 if (result.inflatedHeadsUpView != null) { 693 privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); 694 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, 695 result.newHeadsUpView); 696 } else if (result.newHeadsUpView == null) { 697 privateLayout.setHeadsUpChild(null); 698 remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP); 699 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) { 700 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, 701 result.newHeadsUpView); 702 } 703 if (result.newHeadsUpView != null) { 704 privateLayout.setHeadsUpInflatedSmartReplies( 705 result.headsUpInflatedSmartReplies); 706 } else { 707 privateLayout.setHeadsUpInflatedSmartReplies(null); 708 } 709 setRepliesAndActions = true; 710 } 711 if (setRepliesAndActions) { 712 privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState); 713 } 714 715 if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 716 if (result.inflatedPublicView != null) { 717 publicLayout.setContractedChild(result.inflatedPublicView); 718 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, 719 result.newPublicView); 720 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) { 721 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, 722 result.newPublicView); 723 } 724 } 725 726 entry.headsUpStatusBarText = result.headsUpStatusBarText; 727 entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic; 728 if (endListener != null) { 729 endListener.onAsyncInflationFinished(entry); 730 } 731 return true; 732 } 733 return false; 734 } 735 createExpandedView(Notification.Builder builder, boolean isLowPriority)736 private static RemoteViews createExpandedView(Notification.Builder builder, 737 boolean isLowPriority) { 738 RemoteViews bigContentView = builder.createBigContentView(); 739 if (bigContentView != null) { 740 return bigContentView; 741 } 742 if (isLowPriority) { 743 RemoteViews contentView = builder.createContentView(); 744 Notification.Builder.makeHeaderExpanded(contentView); 745 return contentView; 746 } 747 return null; 748 } 749 createContentView(Notification.Builder builder, boolean isLowPriority, boolean useLarge)750 private static RemoteViews createContentView(Notification.Builder builder, 751 boolean isLowPriority, boolean useLarge) { 752 if (isLowPriority) { 753 return builder.makeLowPriorityContentView(false /* useRegularSubtext */); 754 } 755 return builder.createContentView(useLarge); 756 } 757 758 /** 759 * @param newView The new view that will be applied 760 * @param oldView The old view that was applied to the existing view before 761 * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply. 762 */ 763 @VisibleForTesting canReapplyRemoteView(final RemoteViews newView, final RemoteViews oldView)764 static boolean canReapplyRemoteView(final RemoteViews newView, 765 final RemoteViews oldView) { 766 return (newView == null && oldView == null) || 767 (newView != null && oldView != null 768 && oldView.getPackage() != null 769 && newView.getPackage() != null 770 && newView.getPackage().equals(oldView.getPackage()) 771 && newView.getLayoutId() == oldView.getLayoutId() 772 && !oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED)); 773 } 774 775 /** 776 * Sets whether to perform inflation on the same thread as the caller. This method should only 777 * be used in tests, not in production. 778 */ 779 @VisibleForTesting setInflateSynchronously(boolean inflateSynchronously)780 public void setInflateSynchronously(boolean inflateSynchronously) { 781 mInflateSynchronously = inflateSynchronously; 782 } 783 784 public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress> 785 implements InflationCallback, InflationTask { 786 787 private static final long IMG_PRELOAD_TIMEOUT_MS = 1000L; 788 private final NotificationEntry mEntry; 789 private final Context mContext; 790 private final boolean mInflateSynchronously; 791 private final boolean mIsLowPriority; 792 private final boolean mUsesIncreasedHeight; 793 private final InflationCallback mCallback; 794 private final boolean mUsesIncreasedHeadsUpHeight; 795 private final @InflationFlag int mReInflateFlags; 796 private final NotifRemoteViewCache mRemoteViewCache; 797 private final Executor mBgExecutor; 798 private ExpandableNotificationRow mRow; 799 private Exception mError; 800 private RemoteViews.InteractionHandler mRemoteViewClickHandler; 801 private CancellationSignal mCancellationSignal; 802 private final ConversationNotificationProcessor mConversationProcessor; 803 private final boolean mIsMediaInQS; 804 private final SmartReplyStateInflater mSmartRepliesInflater; 805 AsyncInflationTask( Executor bgExecutor, boolean inflateSynchronously, @InflationFlag int reInflateFlags, NotifRemoteViewCache cache, NotificationEntry entry, ConversationNotificationProcessor conversationProcessor, ExpandableNotificationRow row, boolean isLowPriority, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, InflationCallback callback, RemoteViews.InteractionHandler remoteViewClickHandler, boolean isMediaFlagEnabled, SmartReplyStateInflater smartRepliesInflater)806 private AsyncInflationTask( 807 Executor bgExecutor, 808 boolean inflateSynchronously, 809 @InflationFlag int reInflateFlags, 810 NotifRemoteViewCache cache, 811 NotificationEntry entry, 812 ConversationNotificationProcessor conversationProcessor, 813 ExpandableNotificationRow row, 814 boolean isLowPriority, 815 boolean usesIncreasedHeight, 816 boolean usesIncreasedHeadsUpHeight, 817 InflationCallback callback, 818 RemoteViews.InteractionHandler remoteViewClickHandler, 819 boolean isMediaFlagEnabled, 820 SmartReplyStateInflater smartRepliesInflater) { 821 mEntry = entry; 822 mRow = row; 823 mBgExecutor = bgExecutor; 824 mInflateSynchronously = inflateSynchronously; 825 mReInflateFlags = reInflateFlags; 826 mRemoteViewCache = cache; 827 mSmartRepliesInflater = smartRepliesInflater; 828 mContext = mRow.getContext(); 829 mIsLowPriority = isLowPriority; 830 mUsesIncreasedHeight = usesIncreasedHeight; 831 mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; 832 mRemoteViewClickHandler = remoteViewClickHandler; 833 mCallback = callback; 834 mConversationProcessor = conversationProcessor; 835 mIsMediaInQS = isMediaFlagEnabled; 836 entry.setInflationTask(this); 837 } 838 839 @VisibleForTesting 840 @InflationFlag getReInflateFlags()841 public int getReInflateFlags() { 842 return mReInflateFlags; 843 } 844 updateApplicationInfo(StatusBarNotification sbn)845 void updateApplicationInfo(StatusBarNotification sbn) { 846 String packageName = sbn.getPackageName(); 847 int userId = UserHandle.getUserId(sbn.getUid()); 848 final ApplicationInfo appInfo; 849 try { 850 // This method has an internal cache, so we don't need to add our own caching here. 851 appInfo = mContext.getPackageManager().getApplicationInfoAsUser(packageName, 852 PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); 853 } catch (PackageManager.NameNotFoundException e) { 854 return; 855 } 856 Notification.addFieldsFromContext(appInfo, sbn.getNotification()); 857 } 858 859 @Override doInBackground(Void... params)860 protected InflationProgress doInBackground(Void... params) { 861 try { 862 final StatusBarNotification sbn = mEntry.getSbn(); 863 // Ensure the ApplicationInfo is updated before a builder is recovered. 864 updateApplicationInfo(sbn); 865 final Notification.Builder recoveredBuilder 866 = Notification.Builder.recoverBuilder(mContext, 867 sbn.getNotification()); 868 869 Context packageContext = sbn.getPackageContext(mContext); 870 if (recoveredBuilder.usesTemplate()) { 871 // For all of our templates, we want it to be RTL 872 packageContext = new RtlEnabledContext(packageContext); 873 } 874 if (mEntry.getRanking().isConversation()) { 875 mConversationProcessor.processNotification(mEntry, recoveredBuilder); 876 } 877 InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, 878 recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, 879 mUsesIncreasedHeadsUpHeight, packageContext); 880 InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState(); 881 InflationProgress result = inflateSmartReplyViews( 882 inflationProgress, 883 mReInflateFlags, 884 mEntry, 885 mContext, 886 packageContext, 887 previousSmartReplyState, 888 mSmartRepliesInflater); 889 890 // wait for image resolver to finish preloading 891 mRow.getImageResolver().waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS); 892 893 return result; 894 } catch (Exception e) { 895 mError = e; 896 return null; 897 } 898 } 899 900 @Override onPostExecute(InflationProgress result)901 protected void onPostExecute(InflationProgress result) { 902 if (mError == null) { 903 mCancellationSignal = apply( 904 mBgExecutor, 905 mInflateSynchronously, 906 result, 907 mReInflateFlags, 908 mRemoteViewCache, 909 mEntry, 910 mRow, 911 mRemoteViewClickHandler, 912 this); 913 } else { 914 handleError(mError); 915 } 916 } 917 handleError(Exception e)918 private void handleError(Exception e) { 919 mEntry.onInflationTaskFinished(); 920 StatusBarNotification sbn = mEntry.getSbn(); 921 final String ident = sbn.getPackageName() + "/0x" 922 + Integer.toHexString(sbn.getId()); 923 Log.e(CentralSurfaces.TAG, "couldn't inflate view for notification " + ident, e); 924 if (mCallback != null) { 925 mCallback.handleInflationException(mRow.getEntry(), 926 new InflationException("Couldn't inflate contentViews" + e)); 927 } 928 929 // Cancel any image loading tasks, not useful any more 930 mRow.getImageResolver().cancelRunningTasks(); 931 } 932 933 @Override abort()934 public void abort() { 935 cancel(true /* mayInterruptIfRunning */); 936 if (mCancellationSignal != null) { 937 mCancellationSignal.cancel(); 938 } 939 } 940 941 @Override handleInflationException(NotificationEntry entry, Exception e)942 public void handleInflationException(NotificationEntry entry, Exception e) { 943 handleError(e); 944 } 945 946 @Override onAsyncInflationFinished(NotificationEntry entry)947 public void onAsyncInflationFinished(NotificationEntry entry) { 948 mEntry.onInflationTaskFinished(); 949 mRow.onNotificationUpdated(); 950 if (mCallback != null) { 951 mCallback.onAsyncInflationFinished(mEntry); 952 } 953 954 // Notify the resolver that the inflation task has finished, 955 // try to purge unnecessary cached entries. 956 mRow.getImageResolver().purgeCache(); 957 958 // Cancel any image loading tasks that have not completed at this point 959 mRow.getImageResolver().cancelRunningTasks(); 960 } 961 962 private static class RtlEnabledContext extends ContextWrapper { RtlEnabledContext(Context packageContext)963 private RtlEnabledContext(Context packageContext) { 964 super(packageContext); 965 } 966 967 @Override getApplicationInfo()968 public ApplicationInfo getApplicationInfo() { 969 ApplicationInfo applicationInfo = super.getApplicationInfo(); 970 applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_RTL; 971 return applicationInfo; 972 } 973 } 974 } 975 976 @VisibleForTesting 977 static class InflationProgress { 978 private RemoteViews newContentView; 979 private RemoteViews newHeadsUpView; 980 private RemoteViews newExpandedView; 981 private RemoteViews newPublicView; 982 983 @VisibleForTesting 984 Context packageContext; 985 986 private View inflatedContentView; 987 private View inflatedHeadsUpView; 988 private View inflatedExpandedView; 989 private View inflatedPublicView; 990 private CharSequence headsUpStatusBarText; 991 private CharSequence headsUpStatusBarTextPublic; 992 993 private InflatedSmartReplyState inflatedSmartReplyState; 994 private InflatedSmartReplyViewHolder expandedInflatedSmartReplies; 995 private InflatedSmartReplyViewHolder headsUpInflatedSmartReplies; 996 } 997 998 @VisibleForTesting 999 abstract static class ApplyCallback { setResultView(View v)1000 public abstract void setResultView(View v); getRemoteView()1001 public abstract RemoteViews getRemoteView(); 1002 } 1003 } 1004