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.NotificationLockscreenUserManager.REDACTION_TYPE_OTP; 21 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; 22 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED; 23 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; 24 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_SINGLELINE; 25 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.app.Notification; 29 import android.app.Notification.MessagingStyle; 30 import android.content.Context; 31 import android.content.ContextWrapper; 32 import android.content.pm.ApplicationInfo; 33 import android.content.pm.PackageManager; 34 import android.content.res.Resources; 35 import android.os.AsyncTask; 36 import android.os.Build; 37 import android.os.CancellationSignal; 38 import android.os.Trace; 39 import android.os.UserHandle; 40 import android.service.notification.StatusBarNotification; 41 import android.util.Log; 42 import android.view.NotificationHeaderView; 43 import android.view.View; 44 import android.view.ViewGroup; 45 import android.widget.RemoteViews; 46 47 import com.android.app.tracing.TraceUtils; 48 import com.android.internal.annotations.VisibleForTesting; 49 import com.android.internal.widget.ImageMessageConsumer; 50 import com.android.systemui.dagger.SysUISingleton; 51 import com.android.systemui.dagger.qualifiers.NotifInflation; 52 import com.android.systemui.media.controls.util.MediaFeatureFlag; 53 import com.android.systemui.res.R; 54 import com.android.systemui.statusbar.InflationTask; 55 import com.android.systemui.statusbar.NotificationRemoteInputManager; 56 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; 57 import com.android.systemui.statusbar.notification.InflationException; 58 import com.android.systemui.statusbar.notification.NmSummarizationUiFlag; 59 import com.android.systemui.statusbar.notification.NotificationUtils; 60 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 61 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; 62 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; 63 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels; 64 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; 65 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; 66 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider; 67 import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; 68 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor; 69 import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder; 70 import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel; 71 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; 72 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; 73 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer; 74 import com.android.systemui.statusbar.phone.CentralSurfaces; 75 import com.android.systemui.statusbar.policy.InflatedSmartReplyState; 76 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder; 77 import com.android.systemui.statusbar.policy.SmartReplyStateInflater; 78 import com.android.systemui.util.Assert; 79 80 import java.util.HashMap; 81 import java.util.concurrent.Executor; 82 83 import javax.inject.Inject; 84 85 /** 86 * {@link NotificationContentInflater} binds content to a {@link ExpandableNotificationRow} by 87 * asynchronously building the content's {@link RemoteViews} and applying it to the row. 88 */ 89 @SysUISingleton 90 @VisibleForTesting(visibility = PACKAGE) 91 public class NotificationContentInflater implements NotificationRowContentBinder { 92 93 public static final String TAG = "NotifContentInflater"; 94 95 private boolean mInflateSynchronously = false; 96 private final boolean mIsMediaInQS; 97 private final NotificationRemoteInputManager mRemoteInputManager; 98 private final NotifRemoteViewCache mRemoteViewCache; 99 private final ConversationNotificationProcessor mConversationProcessor; 100 private final Executor mInflationExecutor; 101 private final SmartReplyStateInflater mSmartReplyStateInflater; 102 private final NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider; 103 private final HeadsUpStyleProvider mHeadsUpStyleProvider; 104 private final PromotedNotificationContentExtractor mPromotedNotificationContentExtractor; 105 106 private final NotificationRowContentBinderLogger mLogger; 107 108 @Inject NotificationContentInflater( NotifRemoteViewCache remoteViewCache, NotificationRemoteInputManager remoteInputManager, ConversationNotificationProcessor conversationProcessor, MediaFeatureFlag mediaFeatureFlag, @NotifInflation Executor inflationExecutor, SmartReplyStateInflater smartRepliesInflater, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, HeadsUpStyleProvider headsUpStyleProvider, PromotedNotificationContentExtractor promotedNotificationContentExtractor, NotificationRowContentBinderLogger logger)109 NotificationContentInflater( 110 NotifRemoteViewCache remoteViewCache, 111 NotificationRemoteInputManager remoteInputManager, 112 ConversationNotificationProcessor conversationProcessor, 113 MediaFeatureFlag mediaFeatureFlag, 114 @NotifInflation Executor inflationExecutor, 115 SmartReplyStateInflater smartRepliesInflater, 116 NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, 117 HeadsUpStyleProvider headsUpStyleProvider, 118 PromotedNotificationContentExtractor promotedNotificationContentExtractor, 119 NotificationRowContentBinderLogger logger) { 120 NotificationRowContentBinderRefactor.assertInLegacyMode(); 121 mRemoteViewCache = remoteViewCache; 122 mRemoteInputManager = remoteInputManager; 123 mConversationProcessor = conversationProcessor; 124 mIsMediaInQS = mediaFeatureFlag.getEnabled(); 125 mInflationExecutor = inflationExecutor; 126 mSmartReplyStateInflater = smartRepliesInflater; 127 mNotifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider; 128 mHeadsUpStyleProvider = headsUpStyleProvider; 129 mPromotedNotificationContentExtractor = promotedNotificationContentExtractor; 130 mLogger = logger; 131 } 132 133 @Override bindContent( NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int contentToBind, BindParams bindParams, boolean forceInflate, @Nullable InflationCallback callback)134 public void bindContent( 135 NotificationEntry entry, 136 ExpandableNotificationRow row, 137 @InflationFlag int contentToBind, 138 BindParams bindParams, 139 boolean forceInflate, 140 @Nullable InflationCallback callback) { 141 if (row.isRemoved()) { 142 // We don't want to reinflate anything for removed notifications. Otherwise views might 143 // be readded to the stack, leading to leaks. This may happen with low-priority groups 144 // where the removal of already removed children can lead to a reinflation. 145 mLogger.logNotBindingRowWasRemoved(row.getLoggingKey()); 146 return; 147 } 148 149 mLogger.logBinding(row.getLoggingKey(), contentToBind); 150 151 StatusBarNotification sbn = entry.getSbn(); 152 153 // To check if the notification has inline image and preload inline image if necessary. 154 row.getImageResolver().preloadImages(sbn.getNotification()); 155 156 if (forceInflate) { 157 mRemoteViewCache.clearCache(entry); 158 } 159 160 // Cancel any pending frees on any view we're trying to bind since we should be bound after. 161 cancelContentViewFrees(row, contentToBind); 162 163 AsyncInflationTask task = new AsyncInflationTask( 164 mInflationExecutor, 165 mInflateSynchronously, 166 /* reInflateFlags = */ contentToBind, 167 mRemoteViewCache, 168 entry, 169 mConversationProcessor, 170 row, 171 bindParams, 172 callback, 173 mRemoteInputManager.getRemoteViewsOnClickHandler(), 174 /* isMediaFlagEnabled = */ mIsMediaInQS, 175 mSmartReplyStateInflater, 176 mNotifLayoutInflaterFactoryProvider, 177 mHeadsUpStyleProvider, 178 mPromotedNotificationContentExtractor, 179 mLogger); 180 if (mInflateSynchronously) { 181 task.onPostExecute(task.doInBackground()); 182 } else { 183 task.executeOnExecutor(mInflationExecutor); 184 } 185 } 186 187 @VisibleForTesting inflateNotificationViews( NotificationEntry entry, ExpandableNotificationRow row, BindParams bindParams, boolean inflateSynchronously, @InflationFlag int reInflateFlags, Notification.Builder builder, Context systemUiContext, Context packageContext, SmartReplyStateInflater smartRepliesInflater)188 InflationProgress inflateNotificationViews( 189 NotificationEntry entry, 190 ExpandableNotificationRow row, 191 BindParams bindParams, 192 boolean inflateSynchronously, 193 @InflationFlag int reInflateFlags, 194 Notification.Builder builder, 195 Context systemUiContext, 196 Context packageContext, 197 SmartReplyStateInflater smartRepliesInflater) { 198 InflationProgress result = createRemoteViews(reInflateFlags, 199 builder, 200 bindParams, 201 systemUiContext, 202 packageContext, 203 row, 204 mNotifLayoutInflaterFactoryProvider, 205 mHeadsUpStyleProvider, 206 mLogger); 207 result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(), 208 packageContext, row.getExistingSmartReplyState(), smartRepliesInflater, mLogger); 209 boolean isConversation = entry.getRanking().isConversation(); 210 Notification.MessagingStyle messagingStyle = null; 211 if (NmSummarizationUiFlag.isEnabled() 212 || (isConversation && (AsyncHybridViewInflation.isEnabled() 213 || LockscreenOtpRedaction.isSingleLineViewEnabled()))) { 214 messagingStyle = mConversationProcessor 215 .processNotification(entry, builder, mLogger); 216 } 217 if (AsyncHybridViewInflation.isEnabled()) { 218 SingleLineViewModel viewModel = SingleLineViewInflater 219 .inflateSingleLineViewModel( 220 entry.getSbn().getNotification(), 221 messagingStyle, 222 builder, 223 row.getContext(), 224 false, 225 entry.getRanking().getSummarization() 226 ); 227 // If the messagingStyle is null, we want to inflate the normal view 228 isConversation = viewModel.isConversation(); 229 result.mInflatedSingleLineViewModel = viewModel; 230 result.mInflatedSingleLineView = 231 SingleLineViewInflater.inflatePrivateSingleLineView( 232 isConversation, 233 reInflateFlags, 234 entry, 235 row.getContext(), 236 mLogger 237 ); 238 } 239 if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { 240 if (bindParams.redactionType == REDACTION_TYPE_OTP) { 241 result.mPublicInflatedSingleLineViewModel = 242 SingleLineViewInflater.inflateSingleLineViewModel( 243 entry.getSbn().getNotification(), 244 messagingStyle, 245 builder, 246 row.getContext(), 247 true, 248 entry.getRanking().getSummarization()); 249 } else { 250 result.mPublicInflatedSingleLineViewModel = 251 SingleLineViewInflater.inflatePublicSingleLineViewModel( 252 row.getContext(), 253 isConversation 254 ); 255 } 256 result.mPublicInflatedSingleLineView = 257 SingleLineViewInflater.inflatePublicSingleLineView( 258 isConversation, 259 reInflateFlags, 260 entry, 261 row.getContext(), 262 mLogger 263 ); 264 } 265 266 apply( 267 mInflationExecutor, 268 inflateSynchronously, 269 bindParams.isMinimized, 270 result, 271 reInflateFlags, 272 mRemoteViewCache, 273 entry, 274 row, 275 mRemoteInputManager.getRemoteViewsOnClickHandler(), 276 null /* callback */, 277 mLogger); 278 return result; 279 } 280 281 @Override cancelBind( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row)282 public boolean cancelBind( 283 @NonNull NotificationEntry entry, 284 @NonNull ExpandableNotificationRow row) { 285 final boolean abortedTask = entry.abortTask(); 286 if (abortedTask) { 287 mLogger.logCancelBindAbortedTask(row.getLoggingKey()); 288 } 289 return abortedTask; 290 } 291 292 @Override unbindContent( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row, @InflationFlag int contentToUnbind)293 public void unbindContent( 294 @NonNull NotificationEntry entry, 295 @NonNull ExpandableNotificationRow row, 296 @InflationFlag int contentToUnbind) { 297 mLogger.logUnbinding(row.getLoggingKey(), contentToUnbind); 298 int curFlag = 1; 299 while (contentToUnbind != 0) { 300 if ((contentToUnbind & curFlag) != 0) { 301 freeNotificationView(entry, row, curFlag); 302 } 303 contentToUnbind &= ~curFlag; 304 curFlag = curFlag << 1; 305 } 306 } 307 308 /** 309 * Frees the content view associated with the inflation flag as soon as the view is not showing. 310 * 311 * @param inflateFlag the flag corresponding to the content view which should be freed 312 */ freeNotificationView( NotificationEntry entry, ExpandableNotificationRow row, @InflationFlag int inflateFlag)313 private void freeNotificationView( 314 NotificationEntry entry, 315 ExpandableNotificationRow row, 316 @InflationFlag int inflateFlag) { 317 switch (inflateFlag) { 318 case FLAG_CONTENT_VIEW_CONTRACTED: 319 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> { 320 row.getPrivateLayout().setContractedChild(null); 321 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED); 322 }); 323 break; 324 case FLAG_CONTENT_VIEW_EXPANDED: 325 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_EXPANDED, () -> { 326 row.getPrivateLayout().setExpandedChild(null); 327 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); 328 }); 329 break; 330 case FLAG_CONTENT_VIEW_HEADS_UP: 331 row.getPrivateLayout().performWhenContentInactive(VISIBLE_TYPE_HEADSUP, () -> { 332 row.getPrivateLayout().setHeadsUpChild(null); 333 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP); 334 row.getPrivateLayout().setHeadsUpInflatedSmartReplies(null); 335 }); 336 break; 337 case FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE: 338 if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { 339 row.getPublicLayout() 340 .performWhenContentInactive(VISIBLE_TYPE_SINGLELINE, () -> { 341 row.getPublicLayout().setSingleLineView(null); 342 mRemoteViewCache.removeCachedView(entry, 343 FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); 344 }); 345 } 346 break; 347 case FLAG_CONTENT_VIEW_PUBLIC: 348 row.getPublicLayout().performWhenContentInactive(VISIBLE_TYPE_CONTRACTED, () -> { 349 row.getPublicLayout().setContractedChild(null); 350 mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC); 351 }); 352 break; 353 case FLAG_CONTENT_VIEW_SINGLE_LINE: { 354 if (AsyncHybridViewInflation.isEnabled()) { 355 row.getPrivateLayout().performWhenContentInactive( 356 VISIBLE_TYPE_SINGLELINE, 357 () -> row.getPrivateLayout().setSingleLineView(null) 358 ); 359 } 360 break; 361 } 362 default: 363 break; 364 } 365 } 366 367 /** 368 * Cancel any pending content view frees from {@link #freeNotificationView} for the provided 369 * content views. 370 * 371 * @param row top level notification row containing the content views 372 * @param contentViews content views to cancel pending frees on 373 */ cancelContentViewFrees( ExpandableNotificationRow row, @InflationFlag int contentViews)374 private void cancelContentViewFrees( 375 ExpandableNotificationRow row, 376 @InflationFlag int contentViews) { 377 if ((contentViews & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 378 row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED); 379 } 380 if ((contentViews & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 381 row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_EXPANDED); 382 } 383 if ((contentViews & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 384 row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_HEADSUP); 385 } 386 if ((contentViews & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 387 row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED); 388 } 389 if (LockscreenOtpRedaction.isSingleLineViewEnabled() 390 && (contentViews & FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE) != 0) { 391 row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_SINGLELINE); 392 } 393 if (AsyncHybridViewInflation.isEnabled() 394 && (contentViews & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) { 395 row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_SINGLELINE); 396 } 397 } 398 inflateSmartReplyViews( InflationProgress result, @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, Context packageContext, InflatedSmartReplyState previousSmartReplyState, SmartReplyStateInflater inflater, NotificationRowContentBinderLogger logger)399 private static InflationProgress inflateSmartReplyViews( 400 InflationProgress result, 401 @InflationFlag int reInflateFlags, 402 NotificationEntry entry, 403 Context context, 404 Context packageContext, 405 InflatedSmartReplyState previousSmartReplyState, 406 SmartReplyStateInflater inflater, 407 NotificationRowContentBinderLogger logger) { 408 boolean inflateContracted = (reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0 409 && result.newContentView != null; 410 boolean inflateExpanded = (reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 411 && result.newExpandedView != null; 412 boolean inflateHeadsUp = (reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 413 && result.newHeadsUpView != null; 414 String logKey = NotificationUtils.logKey(entry); 415 if (inflateContracted || inflateExpanded || inflateHeadsUp) { 416 logger.logAsyncTaskProgress(logKey, "inflating contracted smart reply state"); 417 result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry); 418 } 419 if (inflateExpanded) { 420 logger.logAsyncTaskProgress(logKey, "inflating expanded smart reply state"); 421 result.expandedInflatedSmartReplies = inflater.inflateSmartReplyViewHolder( 422 context, packageContext, entry, previousSmartReplyState, 423 result.inflatedSmartReplyState); 424 } 425 if (inflateHeadsUp) { 426 logger.logAsyncTaskProgress(logKey, "inflating heads up smart reply state"); 427 result.headsUpInflatedSmartReplies = inflater.inflateSmartReplyViewHolder( 428 context, packageContext, entry, previousSmartReplyState, 429 result.inflatedSmartReplyState); 430 } 431 return result; 432 } 433 createRemoteViews(@nflationFlag int reInflateFlags, Notification.Builder builder, BindParams bindParams, Context systemUiContext, Context packageContext, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, HeadsUpStyleProvider headsUpStyleProvider, NotificationRowContentBinderLogger logger)434 private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, 435 Notification.Builder builder, BindParams bindParams, Context systemUiContext, 436 Context packageContext, 437 ExpandableNotificationRow row, 438 NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, 439 HeadsUpStyleProvider headsUpStyleProvider, 440 NotificationRowContentBinderLogger logger) { 441 return TraceUtils.trace("NotificationContentInflater.createRemoteViews", () -> { 442 InflationProgress result = new InflationProgress(); 443 444 // inflating the contracted view is the legacy invalidation trigger 445 boolean reinflating = (reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0; 446 // create an image inflater 447 result.mRowImageInflater = RowImageInflater.newInstance(row.mImageModelIndex, 448 reinflating); 449 450 if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 451 logger.logAsyncTaskProgress(row.getLoggingKey(), "creating contracted remote view"); 452 result.newContentView = createContentView(builder, bindParams.isMinimized); 453 } 454 455 if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 456 logger.logAsyncTaskProgress(row.getLoggingKey(), "creating expanded remote view"); 457 result.newExpandedView = createExpandedView(builder, bindParams.isMinimized); 458 } 459 460 if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 461 logger.logAsyncTaskProgress(row.getLoggingKey(), "creating heads up remote view"); 462 final boolean isHeadsUpCompact = headsUpStyleProvider.shouldApplyCompactStyle(); 463 if (isHeadsUpCompact) { 464 result.newHeadsUpView = builder.createCompactHeadsUpContentView(); 465 } else { 466 result.newHeadsUpView = builder.createHeadsUpContentView(); 467 } 468 } 469 470 if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 471 logger.logAsyncTaskProgress(row.getLoggingKey(), "creating public remote view"); 472 if (LockscreenOtpRedaction.isEnabled() 473 && bindParams.redactionType == REDACTION_TYPE_OTP) { 474 result.newPublicView = createSensitiveContentMessageNotification( 475 NotificationBundleUi.isEnabled() 476 ? row.getEntryAdapter().getSbn().getNotification() 477 : row.getEntryLegacy().getSbn().getNotification(), 478 builder.getStyle(), 479 systemUiContext, packageContext).createContentView(); 480 } else { 481 result.newPublicView = builder.makePublicContentView(bindParams.isMinimized); 482 } 483 } 484 485 if (AsyncGroupHeaderViewInflation.isEnabled()) { 486 if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) { 487 logger.logAsyncTaskProgress(row.getLoggingKey(), 488 "creating group summary remote view"); 489 result.mNewGroupHeaderView = builder.makeNotificationGroupHeader(); 490 } 491 492 if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { 493 logger.logAsyncTaskProgress(row.getLoggingKey(), 494 "creating low-priority group summary remote view"); 495 result.mNewMinimizedGroupHeaderView = 496 builder.makeLowPriorityContentView(true /* useRegularSubtext */); 497 } 498 } 499 setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider); 500 result.packageContext = packageContext; 501 result.headsUpStatusBarText = builder.getHeadsUpStatusBarText( 502 false /* showingPublic */); 503 result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( 504 true /* showingPublic */); 505 506 return result; 507 }); 508 } 509 createSensitiveContentMessageNotification( Notification original, Notification.Style originalStyle, Context systemUiContext, Context packageContext)510 private static Notification.Builder createSensitiveContentMessageNotification( 511 Notification original, 512 Notification.Style originalStyle, 513 Context systemUiContext, 514 Context packageContext) { 515 Notification.Builder redacted = 516 new Notification.Builder(packageContext, original.getChannelId()); 517 redacted.setContentTitle(original.extras.getCharSequence(Notification.EXTRA_TITLE)); 518 CharSequence redactedMessage = systemUiContext.getString( 519 R.string.redacted_otp_notification_single_line_text 520 ); 521 redacted.setWhen(original.getWhen()); 522 523 if (originalStyle instanceof MessagingStyle oldStyle) { 524 MessagingStyle newStyle = new MessagingStyle(oldStyle.getUser()); 525 newStyle.setConversationTitle(oldStyle.getConversationTitle()); 526 newStyle.setGroupConversation(false); 527 newStyle.setConversationType(oldStyle.getConversationType()); 528 newStyle.setShortcutIcon(oldStyle.getShortcutIcon()); 529 newStyle.setBuilder(redacted); 530 MessagingStyle.Message latestMessage = 531 MessagingStyle.findLatestIncomingMessage(oldStyle.getMessages()); 532 if (latestMessage != null) { 533 MessagingStyle.Message newMessage = new MessagingStyle.Message(redactedMessage, 534 latestMessage.getTimestamp(), latestMessage.getSenderPerson()); 535 newStyle.addMessage(newMessage); 536 } 537 redacted.setStyle(newStyle); 538 } else { 539 redacted.setContentText(redactedMessage); 540 } 541 redacted.setLargeIcon(original.getLargeIcon()); 542 redacted.setSmallIcon(original.getSmallIcon()); 543 return redacted; 544 } 545 546 setNotifsViewsInflaterFactory(InflationProgress result, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider)547 private static void setNotifsViewsInflaterFactory(InflationProgress result, 548 ExpandableNotificationRow row, 549 NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider) { 550 setRemoteViewsInflaterFactory(result.newContentView, 551 notifLayoutInflaterFactoryProvider.provide(row, FLAG_CONTENT_VIEW_CONTRACTED)); 552 setRemoteViewsInflaterFactory(result.newExpandedView, 553 notifLayoutInflaterFactoryProvider.provide(row, FLAG_CONTENT_VIEW_EXPANDED)); 554 setRemoteViewsInflaterFactory(result.newHeadsUpView, 555 notifLayoutInflaterFactoryProvider.provide(row, FLAG_CONTENT_VIEW_HEADS_UP)); 556 setRemoteViewsInflaterFactory(result.newPublicView, 557 notifLayoutInflaterFactoryProvider.provide(row, FLAG_CONTENT_VIEW_PUBLIC)); 558 if (android.app.Flags.notificationsRedesignAppIcons()) { 559 setRemoteViewsInflaterFactory(result.mNewGroupHeaderView, 560 notifLayoutInflaterFactoryProvider.provide(row, FLAG_GROUP_SUMMARY_HEADER)); 561 setRemoteViewsInflaterFactory(result.mNewMinimizedGroupHeaderView, 562 notifLayoutInflaterFactoryProvider.provide(row, 563 FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)); 564 } 565 } 566 setRemoteViewsInflaterFactory(RemoteViews remoteViews, NotifLayoutInflaterFactory notifLayoutInflaterFactory)567 private static void setRemoteViewsInflaterFactory(RemoteViews remoteViews, 568 NotifLayoutInflaterFactory notifLayoutInflaterFactory) { 569 if (remoteViews != null) { 570 remoteViews.setLayoutInflaterFactory(notifLayoutInflaterFactory); 571 } 572 } 573 apply( Executor inflationExecutor, boolean inflateSynchronously, boolean isMinimized, InflationProgress result, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, NotificationEntry entry, ExpandableNotificationRow row, RemoteViews.InteractionHandler remoteViewClickHandler, @Nullable InflationCallback callback, NotificationRowContentBinderLogger logger)574 private static CancellationSignal apply( 575 Executor inflationExecutor, 576 boolean inflateSynchronously, 577 boolean isMinimized, 578 InflationProgress result, 579 @InflationFlag int reInflateFlags, 580 NotifRemoteViewCache remoteViewCache, 581 NotificationEntry entry, 582 ExpandableNotificationRow row, 583 RemoteViews.InteractionHandler remoteViewClickHandler, 584 @Nullable InflationCallback callback, 585 NotificationRowContentBinderLogger logger) { 586 Trace.beginAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row)); 587 String logKey = NotificationUtils.logKey(entry); 588 589 NotificationContentView privateLayout = row.getPrivateLayout(); 590 NotificationContentView publicLayout = row.getPublicLayout(); 591 final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>(); 592 593 int flag = FLAG_CONTENT_VIEW_CONTRACTED; 594 if ((reInflateFlags & flag) != 0) { 595 boolean isNewView = 596 !canReapplyRemoteView(result.newContentView, 597 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)); 598 ApplyCallback applyCallback = new ApplyCallback() { 599 @Override 600 public void setResultView(View v) { 601 logger.logAsyncTaskProgress(logKey, "contracted view applied"); 602 result.inflatedContentView = v; 603 } 604 605 @Override 606 public RemoteViews getRemoteView() { 607 return result.newContentView; 608 } 609 }; 610 logger.logAsyncTaskProgress(logKey, "applying contracted view"); 611 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result, 612 reInflateFlags, flag, 613 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, 614 privateLayout, privateLayout.getContractedChild(), 615 privateLayout.getVisibleWrapper( 616 NotificationContentView.VISIBLE_TYPE_CONTRACTED), 617 runningInflations, applyCallback, logger); 618 } 619 620 flag = FLAG_CONTENT_VIEW_EXPANDED; 621 if ((reInflateFlags & flag) != 0) { 622 if (result.newExpandedView != null) { 623 boolean isNewView = 624 !canReapplyRemoteView(result.newExpandedView, 625 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)); 626 ApplyCallback applyCallback = new ApplyCallback() { 627 @Override 628 public void setResultView(View v) { 629 logger.logAsyncTaskProgress(logKey, "expanded view applied"); 630 result.inflatedExpandedView = v; 631 } 632 633 @Override 634 public RemoteViews getRemoteView() { 635 return result.newExpandedView; 636 } 637 }; 638 logger.logAsyncTaskProgress(logKey, "applying expanded view"); 639 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result, 640 reInflateFlags, 641 flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, 642 callback, privateLayout, privateLayout.getExpandedChild(), 643 privateLayout.getVisibleWrapper(VISIBLE_TYPE_EXPANDED), runningInflations, 644 applyCallback, logger); 645 } 646 } 647 648 flag = FLAG_CONTENT_VIEW_HEADS_UP; 649 if ((reInflateFlags & flag) != 0) { 650 if (result.newHeadsUpView != null) { 651 boolean isNewView = 652 !canReapplyRemoteView(result.newHeadsUpView, 653 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)); 654 ApplyCallback applyCallback = new ApplyCallback() { 655 @Override 656 public void setResultView(View v) { 657 logger.logAsyncTaskProgress(logKey, "heads up view applied"); 658 result.inflatedHeadsUpView = v; 659 } 660 661 @Override 662 public RemoteViews getRemoteView() { 663 return result.newHeadsUpView; 664 } 665 }; 666 logger.logAsyncTaskProgress(logKey, "applying heads up view"); 667 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, 668 result, reInflateFlags, 669 flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, 670 callback, privateLayout, privateLayout.getHeadsUpChild(), 671 privateLayout.getVisibleWrapper(VISIBLE_TYPE_HEADSUP), runningInflations, 672 applyCallback, logger); 673 } 674 } 675 676 flag = FLAG_CONTENT_VIEW_PUBLIC; 677 if ((reInflateFlags & flag) != 0) { 678 boolean isNewView = 679 !canReapplyRemoteView(result.newPublicView, 680 remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)); 681 ApplyCallback applyCallback = new ApplyCallback() { 682 @Override 683 public void setResultView(View v) { 684 logger.logAsyncTaskProgress(logKey, "public view applied"); 685 result.inflatedPublicView = v; 686 } 687 688 @Override 689 public RemoteViews getRemoteView() { 690 return result.newPublicView; 691 } 692 }; 693 logger.logAsyncTaskProgress(logKey, "applying public view"); 694 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, 695 result, reInflateFlags, flag, 696 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, 697 publicLayout, publicLayout.getContractedChild(), 698 publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED), 699 runningInflations, applyCallback, logger); 700 } 701 702 if (AsyncGroupHeaderViewInflation.isEnabled()) { 703 NotificationChildrenContainer childrenContainer = row.getChildrenContainerNonNull(); 704 if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) { 705 boolean isNewView = 706 !canReapplyRemoteView( 707 /* newView = */ result.mNewGroupHeaderView, 708 /* oldView = */ remoteViewCache 709 .getCachedView(entry, FLAG_GROUP_SUMMARY_HEADER)); 710 ApplyCallback applyCallback = new ApplyCallback() { 711 @Override 712 public void setResultView(View v) { 713 logger.logAsyncTaskProgress(logKey, "group header view applied"); 714 result.mInflatedGroupHeaderView = (NotificationHeaderView) v; 715 } 716 717 @Override 718 public RemoteViews getRemoteView() { 719 return result.mNewGroupHeaderView; 720 } 721 }; 722 logger.logAsyncTaskProgress(logKey, "applying group header view"); 723 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, 724 result, reInflateFlags, 725 /* inflationId = */ FLAG_GROUP_SUMMARY_HEADER, 726 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, 727 /* parentLayout = */ childrenContainer, 728 /* existingView = */ childrenContainer.getGroupHeader(), 729 /* existingWrapper = */ childrenContainer.getNotificationHeaderWrapper(), 730 runningInflations, applyCallback, logger); 731 } 732 733 if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { 734 boolean isNewView = 735 !canReapplyRemoteView( 736 /* newView = */ result.mNewMinimizedGroupHeaderView, 737 /* oldView = */ remoteViewCache.getCachedView( 738 entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)); 739 ApplyCallback applyCallback = new ApplyCallback() { 740 @Override 741 public void setResultView(View v) { 742 logger.logAsyncTaskProgress(logKey, 743 "low-priority group header view applied"); 744 result.mInflatedMinimizedGroupHeaderView = (NotificationHeaderView) v; 745 } 746 747 @Override 748 public RemoteViews getRemoteView() { 749 return result.mNewMinimizedGroupHeaderView; 750 } 751 }; 752 logger.logAsyncTaskProgress(logKey, "applying low priority group header view"); 753 applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, 754 result, reInflateFlags, 755 /* inflationId = */ FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, 756 remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, 757 /* parentLayout = */ childrenContainer, 758 /* existingView = */ childrenContainer.getMinimizedNotificationHeader(), 759 /* existingWrapper = */ childrenContainer 760 .getMinimizedGroupHeaderWrapper(), 761 runningInflations, applyCallback, logger); 762 } 763 } 764 765 // Let's try to finish, maybe nobody is even inflating anything 766 finishIfDone(result, isMinimized, reInflateFlags, remoteViewCache, runningInflations, 767 callback, entry, row, logger); 768 CancellationSignal cancellationSignal = new CancellationSignal(); 769 cancellationSignal.setOnCancelListener( 770 () -> { 771 logger.logAsyncTaskProgress(logKey, "apply cancelled"); 772 Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row)); 773 runningInflations.values().forEach(CancellationSignal::cancel); 774 }); 775 776 return cancellationSignal; 777 } 778 779 @VisibleForTesting applyRemoteView( Executor inflationExecutor, boolean inflateSynchronously, boolean isMinimized, 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, ViewGroup parentLayout, View existingView, NotificationViewWrapper existingWrapper, final HashMap<Integer, CancellationSignal> runningInflations, ApplyCallback applyCallback, NotificationRowContentBinderLogger logger)780 static void applyRemoteView( 781 Executor inflationExecutor, 782 boolean inflateSynchronously, 783 boolean isMinimized, 784 final InflationProgress result, 785 final @InflationFlag int reInflateFlags, 786 @InflationFlag int inflationId, 787 final NotifRemoteViewCache remoteViewCache, 788 final NotificationEntry entry, 789 final ExpandableNotificationRow row, 790 boolean isNewView, 791 RemoteViews.InteractionHandler remoteViewClickHandler, 792 @Nullable final InflationCallback callback, 793 ViewGroup parentLayout, 794 View existingView, 795 NotificationViewWrapper existingWrapper, 796 final HashMap<Integer, CancellationSignal> runningInflations, 797 ApplyCallback applyCallback, 798 NotificationRowContentBinderLogger logger) { 799 RemoteViews newContentView = applyCallback.getRemoteView(); 800 if (inflateSynchronously) { 801 try { 802 if (isNewView) { 803 View v = newContentView.apply( 804 result.packageContext, 805 parentLayout, 806 remoteViewClickHandler); 807 validateView(v, entry, row.getResources()); 808 applyCallback.setResultView(v); 809 } else { 810 newContentView.reapply( 811 result.packageContext, 812 existingView, 813 remoteViewClickHandler); 814 validateView(existingView, entry, row.getResources()); 815 existingWrapper.onReinflated(); 816 } 817 } catch (Exception e) { 818 handleInflationError(runningInflations, e, row, entry, callback, logger, 819 "applying view synchronously"); 820 // Add a running inflation to make sure we don't trigger callbacks. 821 // Safe to do because only happens in tests. 822 runningInflations.put(inflationId, new CancellationSignal()); 823 } 824 return; 825 } 826 RemoteViews.OnViewAppliedListener listener = new RemoteViews.OnViewAppliedListener() { 827 828 @Override 829 public void onViewInflated(View v) { 830 if (v instanceof ImageMessageConsumer) { 831 ((ImageMessageConsumer) v).setImageResolver(row.getImageResolver()); 832 } 833 } 834 835 @Override 836 public void onViewApplied(View v) { 837 String invalidReason = isValidView(v, entry, row.getResources()); 838 if (invalidReason != null) { 839 handleInflationError(runningInflations, new InflationException(invalidReason), 840 row, entry, callback, logger, "applied invalid view"); 841 runningInflations.remove(inflationId); 842 return; 843 } 844 if (isNewView) { 845 applyCallback.setResultView(v); 846 } else if (existingWrapper != null) { 847 existingWrapper.onReinflated(); 848 } 849 runningInflations.remove(inflationId); 850 finishIfDone(result, isMinimized, 851 reInflateFlags, remoteViewCache, runningInflations, 852 callback, entry, row, logger); 853 } 854 855 @Override 856 public void onError(Exception e) { 857 // Uh oh the async inflation failed. Due to some bugs (see b/38190555), this could 858 // actually also be a system issue, so let's try on the UI thread again to be safe. 859 try { 860 View newView = existingView; 861 if (isNewView) { 862 newView = newContentView.apply( 863 result.packageContext, 864 parentLayout, 865 remoteViewClickHandler); 866 } else { 867 newContentView.reapply( 868 result.packageContext, 869 existingView, 870 remoteViewClickHandler); 871 } 872 Log.wtf(TAG, "Async Inflation failed but normal inflation finished normally.", 873 e); 874 onViewApplied(newView); 875 } catch (Exception anotherException) { 876 runningInflations.remove(inflationId); 877 handleInflationError(runningInflations, e, row, entry, 878 callback, logger, "applying view"); 879 } 880 } 881 }; 882 CancellationSignal cancellationSignal; 883 if (isNewView) { 884 cancellationSignal = newContentView.applyAsync( 885 result.packageContext, 886 parentLayout, 887 inflationExecutor, 888 listener, 889 remoteViewClickHandler); 890 } else { 891 cancellationSignal = newContentView.reapplyAsync( 892 result.packageContext, 893 existingView, 894 inflationExecutor, 895 listener, 896 remoteViewClickHandler); 897 } 898 runningInflations.put(inflationId, cancellationSignal); 899 } 900 901 /** 902 * Checks if the given View is a valid notification View. 903 * 904 * @return null == valid, non-null == invalid, String represents reason for rejection. 905 */ 906 @VisibleForTesting 907 @Nullable isValidView(View view, NotificationEntry entry, Resources resources)908 static String isValidView(View view, 909 NotificationEntry entry, 910 Resources resources) { 911 if (!satisfiesMinHeightRequirement(view, entry, resources)) { 912 return "inflated notification does not meet minimum height requirement"; 913 } 914 915 if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) { 916 if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) { 917 return "inflated notification does not meet maximum memory size requirement"; 918 } 919 } 920 921 return null; 922 } 923 satisfiesMinHeightRequirement(View view, NotificationEntry entry, Resources resources)924 private static boolean satisfiesMinHeightRequirement(View view, 925 NotificationEntry entry, 926 Resources resources) { 927 if (!requiresHeightCheck(entry)) { 928 return true; 929 } 930 return TraceUtils.trace("NotificationContentInflater#satisfiesMinHeightRequirement", () -> { 931 int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 932 int referenceWidth = resources.getDimensionPixelSize( 933 R.dimen.notification_validation_reference_width); 934 int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth, 935 View.MeasureSpec.EXACTLY); 936 view.measure(widthSpec, heightSpec); 937 int minHeight = resources.getDimensionPixelSize( 938 R.dimen.notification_validation_minimum_allowed_height); 939 return view.getMeasuredHeight() >= minHeight; 940 }); 941 } 942 943 /** 944 * Notifications with undecorated custom views need to satisfy a minimum height to avoid visual 945 * issues. 946 */ 947 private static boolean requiresHeightCheck(NotificationEntry entry) { 948 // Undecorated custom views are disallowed from S onwards 949 if (entry.targetSdk >= Build.VERSION_CODES.S) { 950 return false; 951 } 952 // No need to check if the app isn't using any custom views 953 Notification notification = entry.getSbn().getNotification(); 954 if (notification.contentView == null 955 && notification.bigContentView == null 956 && notification.headsUpContentView == null) { 957 return false; 958 } 959 return true; 960 } 961 962 private static void validateView(View view, 963 NotificationEntry entry, 964 Resources resources) throws InflationException { 965 String invalidReason = isValidView(view, entry, resources); 966 if (invalidReason != null) { 967 throw new InflationException(invalidReason); 968 } 969 } 970 971 private static void handleInflationError( 972 HashMap<Integer, CancellationSignal> runningInflations, Exception e, 973 ExpandableNotificationRow row, NotificationEntry entry, 974 @Nullable InflationCallback callback, 975 NotificationRowContentBinderLogger logger, String logContext) { 976 Assert.isMainThread(); 977 logger.logAsyncTaskException(row.getLoggingKey(), logContext, e); 978 runningInflations.values().forEach(CancellationSignal::cancel); 979 if (callback != null) { 980 callback.handleInflationException(entry, e); 981 } 982 } 983 984 /** 985 * Finish the inflation of the views 986 * 987 * @return true if the inflation was finished 988 */ 989 private static boolean finishIfDone(InflationProgress result, 990 boolean isMinimized, 991 @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, 992 HashMap<Integer, CancellationSignal> runningInflations, 993 @Nullable InflationCallback endListener, NotificationEntry entry, 994 ExpandableNotificationRow row, NotificationRowContentBinderLogger logger) { 995 Assert.isMainThread(); 996 if (!runningInflations.isEmpty()) { 997 return false; 998 } 999 NotificationContentView privateLayout = row.getPrivateLayout(); 1000 NotificationContentView publicLayout = row.getPublicLayout(); 1001 logger.logAsyncTaskProgress(NotificationUtils.logKey(entry), "finishing"); 1002 1003 // Put the new image index on the row 1004 row.mImageModelIndex = result.mRowImageInflater.getNewImageIndex(); 1005 1006 if (PromotedNotificationContentModel.featureFlagEnabled()) { 1007 entry.setPromotedNotificationContentModels(result.mPromotedContent); 1008 } 1009 1010 boolean setRepliesAndActions = true; 1011 if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { 1012 if (result.inflatedContentView != null) { 1013 // New view case 1014 privateLayout.setContractedChild(result.inflatedContentView); 1015 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, 1016 result.newContentView); 1017 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) { 1018 // Reinflation case. Only update if it's still cached (i.e. view has not been 1019 // freed while inflating). 1020 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED, 1021 result.newContentView); 1022 } 1023 setRepliesAndActions = true; 1024 } 1025 1026 if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { 1027 if (result.inflatedExpandedView != null) { 1028 privateLayout.setExpandedChild(result.inflatedExpandedView); 1029 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, 1030 result.newExpandedView); 1031 } else if (result.newExpandedView == null) { 1032 privateLayout.setExpandedChild(null); 1033 remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED); 1034 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) { 1035 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED, 1036 result.newExpandedView); 1037 } 1038 if (result.newExpandedView != null) { 1039 privateLayout.setExpandedInflatedSmartReplies( 1040 result.expandedInflatedSmartReplies); 1041 } else { 1042 privateLayout.setExpandedInflatedSmartReplies(null); 1043 } 1044 row.setExpandable(result.newExpandedView != null); 1045 setRepliesAndActions = true; 1046 } 1047 1048 if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { 1049 if (result.inflatedHeadsUpView != null) { 1050 privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); 1051 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, 1052 result.newHeadsUpView); 1053 } else if (result.newHeadsUpView == null) { 1054 privateLayout.setHeadsUpChild(null); 1055 remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP); 1056 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) { 1057 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP, 1058 result.newHeadsUpView); 1059 } 1060 if (result.newHeadsUpView != null) { 1061 privateLayout.setHeadsUpInflatedSmartReplies( 1062 result.headsUpInflatedSmartReplies); 1063 } else { 1064 privateLayout.setHeadsUpInflatedSmartReplies(null); 1065 } 1066 setRepliesAndActions = true; 1067 } 1068 1069 if (AsyncHybridViewInflation.isEnabled() 1070 && (reInflateFlags & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) { 1071 HybridNotificationView view = result.mInflatedSingleLineView; 1072 SingleLineViewModel viewModel = result.mInflatedSingleLineViewModel; 1073 if (view != null && viewModel != null) { 1074 SingleLineViewBinder.bind(viewModel, view); 1075 privateLayout.setSingleLineView(result.mInflatedSingleLineView); 1076 } 1077 } 1078 1079 if (LockscreenOtpRedaction.isSingleLineViewEnabled() 1080 && (reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE) != 0) { 1081 HybridNotificationView view = result.mPublicInflatedSingleLineView; 1082 SingleLineViewModel viewModel = result.mPublicInflatedSingleLineViewModel; 1083 if (view != null && viewModel != null) { 1084 SingleLineViewBinder.bind(viewModel, view); 1085 publicLayout.setSingleLineView(result.mPublicInflatedSingleLineView); 1086 } 1087 } 1088 1089 if (setRepliesAndActions) { 1090 privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState); 1091 } 1092 1093 if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { 1094 if (result.inflatedPublicView != null) { 1095 publicLayout.setContractedChild(result.inflatedPublicView); 1096 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, 1097 result.newPublicView); 1098 } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) { 1099 remoteViewCache.putCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC, 1100 result.newPublicView); 1101 } 1102 } 1103 1104 if (AsyncGroupHeaderViewInflation.isEnabled()) { 1105 if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) { 1106 if (result.mInflatedGroupHeaderView != null) { 1107 // We need to set if the row is minimized before setting the group header to 1108 // make sure the setting of header view works correctly 1109 row.setIsMinimized(isMinimized); 1110 row.setGroupHeader(/* headerView= */ result.mInflatedGroupHeaderView); 1111 remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER, 1112 result.mNewGroupHeaderView); 1113 } else if (remoteViewCache.hasCachedView(entry, FLAG_GROUP_SUMMARY_HEADER)) { 1114 // Re-inflation case. Only update if it's still cached (i.e. view has not 1115 // been freed while inflating). 1116 remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER, 1117 result.mNewGroupHeaderView); 1118 } 1119 } 1120 1121 if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { 1122 if (result.mInflatedMinimizedGroupHeaderView != null) { 1123 // We need to set if the row is minimized before setting the group header to 1124 // make sure the setting of header view works correctly 1125 row.setIsMinimized(isMinimized); 1126 row.setMinimizedGroupHeader( 1127 /* headerView= */ result.mInflatedMinimizedGroupHeaderView); 1128 remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, 1129 result.mNewMinimizedGroupHeaderView); 1130 } else if (remoteViewCache.hasCachedView(entry, 1131 FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)) { 1132 // Re-inflation case. Only update if it's still cached (i.e. view has not 1133 // been freed while inflating). 1134 remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, 1135 result.mNewGroupHeaderView); 1136 } 1137 } 1138 } 1139 1140 entry.setHeadsUpStatusBarText(result.headsUpStatusBarText); 1141 entry.setHeadsUpStatusBarTextPublic(result.headsUpStatusBarTextPublic); 1142 1143 Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row)); 1144 if (endListener != null) { 1145 endListener.onAsyncInflationFinished(entry); 1146 } 1147 return true; 1148 } 1149 1150 private static RemoteViews createExpandedView(Notification.Builder builder, 1151 boolean isMinimized) { 1152 RemoteViews bigContentView = builder.createBigContentView(); 1153 if (bigContentView != null) { 1154 return bigContentView; 1155 } 1156 if (isMinimized) { 1157 RemoteViews contentView = builder.createContentView(); 1158 Notification.Builder.makeHeaderExpanded(contentView); 1159 return contentView; 1160 } 1161 return null; 1162 } 1163 1164 private static RemoteViews createContentView(Notification.Builder builder, 1165 boolean isMinimized) { 1166 if (isMinimized) { 1167 return builder.makeLowPriorityContentView(false /* useRegularSubtext */); 1168 } 1169 return builder.createContentView(); 1170 } 1171 1172 /** 1173 * @param newView The new view that will be applied 1174 * @param oldView The old view that was applied to the existing view before 1175 * @return {@code true} if the RemoteViews are the same and the view can be reused to reapply. 1176 */ 1177 @VisibleForTesting 1178 static boolean canReapplyRemoteView(final RemoteViews newView, 1179 final RemoteViews oldView) { 1180 return (newView == null && oldView == null) || 1181 (newView != null && oldView != null 1182 && oldView.getPackage() != null 1183 && newView.getPackage() != null 1184 && newView.getPackage().equals(oldView.getPackage()) 1185 && newView.getLayoutId() == oldView.getLayoutId() 1186 && !oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED)); 1187 } 1188 1189 /** 1190 * Sets whether to perform inflation on the same thread as the caller. This method should only 1191 * be used in tests, not in production. 1192 */ 1193 @VisibleForTesting 1194 public void setInflateSynchronously(boolean inflateSynchronously) { 1195 mInflateSynchronously = inflateSynchronously; 1196 } 1197 1198 public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress> 1199 implements InflationCallback, InflationTask { 1200 1201 private static final long IMG_PRELOAD_TIMEOUT_MS = 1000L; 1202 private final NotificationEntry mEntry; 1203 private final Context mContext; 1204 private final boolean mInflateSynchronously; 1205 private final BindParams mBindParams; 1206 private final InflationCallback mCallback; 1207 private final @InflationFlag int mReInflateFlags; 1208 private final NotifRemoteViewCache mRemoteViewCache; 1209 private final Executor mInflationExecutor; 1210 private ExpandableNotificationRow mRow; 1211 private Exception mError; 1212 private RemoteViews.InteractionHandler mRemoteViewClickHandler; 1213 private CancellationSignal mCancellationSignal; 1214 private final ConversationNotificationProcessor mConversationProcessor; 1215 private final boolean mIsMediaInQS; 1216 private final SmartReplyStateInflater mSmartRepliesInflater; 1217 private final NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider; 1218 private final HeadsUpStyleProvider mHeadsUpStyleProvider; 1219 private final PromotedNotificationContentExtractor mPromotedNotificationContentExtractor; 1220 private final NotificationRowContentBinderLogger mLogger; 1221 1222 private AsyncInflationTask( 1223 Executor inflationExecutor, 1224 boolean inflateSynchronously, 1225 @InflationFlag int reInflateFlags, 1226 NotifRemoteViewCache cache, 1227 NotificationEntry entry, 1228 ConversationNotificationProcessor conversationProcessor, 1229 ExpandableNotificationRow row, 1230 BindParams bindParams, 1231 InflationCallback callback, 1232 RemoteViews.InteractionHandler remoteViewClickHandler, 1233 boolean isMediaFlagEnabled, 1234 SmartReplyStateInflater smartRepliesInflater, 1235 NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, 1236 HeadsUpStyleProvider headsUpStyleProvider, 1237 PromotedNotificationContentExtractor promotedNotificationContentExtractor, 1238 NotificationRowContentBinderLogger logger) { 1239 mEntry = entry; 1240 mRow = row; 1241 mInflationExecutor = inflationExecutor; 1242 mInflateSynchronously = inflateSynchronously; 1243 mReInflateFlags = reInflateFlags; 1244 mRemoteViewCache = cache; 1245 mSmartRepliesInflater = smartRepliesInflater; 1246 mContext = mRow.getContext(); 1247 mBindParams = bindParams; 1248 mRemoteViewClickHandler = remoteViewClickHandler; 1249 mCallback = callback; 1250 mConversationProcessor = conversationProcessor; 1251 mIsMediaInQS = isMediaFlagEnabled; 1252 mNotifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider; 1253 mHeadsUpStyleProvider = headsUpStyleProvider; 1254 mPromotedNotificationContentExtractor = promotedNotificationContentExtractor; 1255 mLogger = logger; 1256 entry.setInflationTask(this); 1257 } 1258 1259 @VisibleForTesting 1260 @InflationFlag 1261 public int getReInflateFlags() { 1262 return mReInflateFlags; 1263 } 1264 1265 void updateApplicationInfo(StatusBarNotification sbn) { 1266 String packageName = sbn.getPackageName(); 1267 int userId = UserHandle.getUserId(sbn.getUid()); 1268 final ApplicationInfo appInfo; 1269 try { 1270 // This method has an internal cache, so we don't need to add our own caching here. 1271 appInfo = mContext.getPackageManager().getApplicationInfoAsUser(packageName, 1272 PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); 1273 } catch (PackageManager.NameNotFoundException e) { 1274 return; 1275 } 1276 Notification.addFieldsFromContext(appInfo, sbn.getNotification()); 1277 } 1278 1279 @Override 1280 protected void onPreExecute() { 1281 Trace.beginAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this)); 1282 } 1283 1284 @Override 1285 protected InflationProgress doInBackground(Void... params) { 1286 return TraceUtils.trace("NotificationContentInflater.AsyncInflationTask#doInBackground", 1287 () -> { 1288 try { 1289 return doInBackgroundInternal(); 1290 } catch (Exception e) { 1291 mError = e; 1292 mLogger.logAsyncTaskException( 1293 NotificationUtils.logKey(mEntry), "inflating", e); 1294 return null; 1295 } 1296 }); 1297 } 1298 1299 private InflationProgress doInBackgroundInternal() { 1300 final StatusBarNotification sbn = mEntry.getSbn(); 1301 // Ensure the ApplicationInfo is updated before a builder is recovered. 1302 updateApplicationInfo(sbn); 1303 final Notification.Builder recoveredBuilder = Notification.Builder.recoverBuilder( 1304 mContext, sbn.getNotification()); 1305 1306 Context packageContext = sbn.getPackageContext(mContext); 1307 if (recoveredBuilder.usesTemplate()) { 1308 // For all of our templates, we want it to be RTL 1309 packageContext = new RtlEnabledContext(packageContext); 1310 } 1311 boolean isConversation = mEntry.getRanking().isConversation(); 1312 Notification.MessagingStyle messagingStyle = null; 1313 if (isConversation) { 1314 messagingStyle = mConversationProcessor.processNotification( 1315 mEntry, recoveredBuilder, mLogger); 1316 } 1317 InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, 1318 recoveredBuilder, mBindParams, mContext, packageContext, mRow, 1319 mNotifLayoutInflaterFactoryProvider, mHeadsUpStyleProvider, mLogger); 1320 String logKey = NotificationUtils.logKey(mEntry); 1321 mLogger.logAsyncTaskProgress(logKey, 1322 "getting existing smart reply state (on wrong thread!)"); 1323 InflatedSmartReplyState previousSmartReplyState = 1324 mRow.getExistingSmartReplyState(); 1325 mLogger.logAsyncTaskProgress(logKey, "inflating smart reply views"); 1326 InflationProgress result = inflateSmartReplyViews( 1327 /* result = */ inflationProgress, 1328 mReInflateFlags, 1329 mEntry, 1330 mContext, 1331 packageContext, 1332 previousSmartReplyState, 1333 mSmartRepliesInflater, 1334 mLogger); 1335 1336 if (AsyncHybridViewInflation.isEnabled()) { 1337 // Inflate the single-line content view's ViewModel and ViewHolder from the 1338 // background thread, the ViewHolder needs to be bind with ViewModel later from 1339 // the main thread. 1340 result.mInflatedSingleLineViewModel = SingleLineViewInflater 1341 .inflateSingleLineViewModel( 1342 mEntry.getSbn().getNotification(), 1343 messagingStyle, 1344 recoveredBuilder, 1345 mContext, 1346 false, 1347 mEntry.getRanking().getSummarization() 1348 ); 1349 result.mInflatedSingleLineView = 1350 SingleLineViewInflater.inflatePrivateSingleLineView( 1351 isConversation, 1352 mReInflateFlags, 1353 mEntry, 1354 mContext, 1355 mLogger 1356 ); 1357 } 1358 1359 if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { 1360 if (mBindParams.redactionType == REDACTION_TYPE_OTP) { 1361 result.mPublicInflatedSingleLineViewModel = 1362 SingleLineViewInflater.inflateSingleLineViewModel( 1363 mEntry.getSbn().getNotification(), 1364 messagingStyle, 1365 recoveredBuilder, 1366 mContext, 1367 true, 1368 null 1369 ); 1370 } else { 1371 result.mPublicInflatedSingleLineViewModel = 1372 SingleLineViewInflater.inflatePublicSingleLineViewModel( 1373 mContext, 1374 isConversation 1375 ); 1376 } 1377 result.mPublicInflatedSingleLineView = 1378 SingleLineViewInflater.inflatePublicSingleLineView( 1379 isConversation, 1380 mReInflateFlags, 1381 mEntry, 1382 mContext, 1383 mLogger 1384 ); 1385 } 1386 1387 if (PromotedNotificationContentModel.featureFlagEnabled()) { 1388 mLogger.logAsyncTaskProgress(logKey, "extracting promoted notification content"); 1389 final ImageModelProvider imageModelProvider = 1390 result.mRowImageInflater.useForContentModel(); 1391 final PromotedNotificationContentModels promotedContent = 1392 mPromotedNotificationContentExtractor.extractContent(mEntry, 1393 recoveredBuilder, mBindParams.redactionType, imageModelProvider); 1394 mLogger.logAsyncTaskProgress(logKey, "extracted promoted notification content: " 1395 + (promotedContent != null ? promotedContent.toRedactedString() : null)); 1396 1397 result.mPromotedContent = promotedContent; 1398 } 1399 1400 mLogger.logAsyncTaskProgress(logKey, "loading RON images"); 1401 inflationProgress.mRowImageInflater.loadImagesSynchronously(packageContext); 1402 1403 mLogger.logAsyncTaskProgress(logKey, 1404 "getting row image resolver (on wrong thread!)"); 1405 final NotificationInlineImageResolver imageResolver = mRow.getImageResolver(); 1406 // wait for image resolver to finish preloading 1407 mLogger.logAsyncTaskProgress(logKey, "waiting for preloaded images"); 1408 imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS); 1409 1410 return result; 1411 } 1412 1413 @Override 1414 protected void onPostExecute(InflationProgress result) { 1415 Trace.endAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this)); 1416 1417 if (mError == null) { 1418 // Logged in detail in apply. 1419 mCancellationSignal = apply( 1420 mInflationExecutor, 1421 mInflateSynchronously, 1422 mBindParams.isMinimized, 1423 result, 1424 mReInflateFlags, 1425 mRemoteViewCache, 1426 mEntry, 1427 mRow, 1428 mRemoteViewClickHandler, 1429 this /* callback */, 1430 mLogger); 1431 } else { 1432 handleError(mError); 1433 } 1434 } 1435 1436 @Override 1437 protected void onCancelled(InflationProgress result) { 1438 Trace.endAsyncSection(ASYNC_TASK_TRACE_METHOD, System.identityHashCode(this)); 1439 } 1440 1441 private void handleError(Exception e) { 1442 mEntry.onInflationTaskFinished(); 1443 StatusBarNotification sbn = mEntry.getSbn(); 1444 final String ident = sbn.getPackageName() + "/0x" 1445 + Integer.toHexString(sbn.getId()); 1446 Log.e(CentralSurfaces.TAG, "couldn't inflate view for notification " + ident, e); 1447 if (mCallback != null) { 1448 mCallback.handleInflationException(mEntry, 1449 new InflationException("Couldn't inflate contentViews" + e)); 1450 } 1451 1452 // Cancel any image loading tasks, not useful any more 1453 mRow.getImageResolver().cancelRunningTasks(); 1454 } 1455 1456 @Override 1457 public void abort() { 1458 String logKey = NotificationUtils.logKey(mEntry); 1459 mLogger.logAsyncTaskProgress(logKey, "cancelling inflate"); 1460 cancel(true /* mayInterruptIfRunning */); 1461 if (mCancellationSignal != null) { 1462 mLogger.logAsyncTaskProgress(logKey, "cancelling apply"); 1463 mCancellationSignal.cancel(); 1464 } 1465 mLogger.logAsyncTaskProgress(logKey, "aborted"); 1466 } 1467 1468 @Override 1469 public void handleInflationException(Exception e) { 1470 handleError(e); 1471 } 1472 1473 @Override 1474 public void onAsyncInflationFinished() { 1475 mEntry.onInflationTaskFinished(); 1476 mRow.onNotificationUpdated(); 1477 if (mCallback != null) { 1478 mCallback.onAsyncInflationFinished(mEntry); 1479 } 1480 1481 // Notify the resolver that the inflation task has finished, 1482 // try to purge unnecessary cached entries. 1483 mRow.getImageResolver().purgeCache(); 1484 1485 // Cancel any image loading tasks that have not completed at this point 1486 mRow.getImageResolver().cancelRunningTasks(); 1487 } 1488 1489 private static class RtlEnabledContext extends ContextWrapper { 1490 private RtlEnabledContext(Context packageContext) { 1491 super(packageContext); 1492 } 1493 1494 @Override 1495 public ApplicationInfo getApplicationInfo() { 1496 ApplicationInfo applicationInfo = new ApplicationInfo(super.getApplicationInfo()); 1497 applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_RTL; 1498 return applicationInfo; 1499 } 1500 } 1501 } 1502 1503 @VisibleForTesting 1504 static class InflationProgress { 1505 RowImageInflater mRowImageInflater; 1506 1507 PromotedNotificationContentModels mPromotedContent; 1508 1509 private RemoteViews newContentView; 1510 private RemoteViews newHeadsUpView; 1511 private RemoteViews newExpandedView; 1512 private RemoteViews newPublicView; 1513 private RemoteViews mNewGroupHeaderView; 1514 private RemoteViews mNewMinimizedGroupHeaderView; 1515 1516 @VisibleForTesting 1517 Context packageContext; 1518 1519 private View inflatedContentView; 1520 private View inflatedHeadsUpView; 1521 private View inflatedExpandedView; 1522 private View inflatedPublicView; 1523 private NotificationHeaderView mInflatedGroupHeaderView; 1524 private NotificationHeaderView mInflatedMinimizedGroupHeaderView; 1525 private CharSequence headsUpStatusBarText; 1526 private CharSequence headsUpStatusBarTextPublic; 1527 1528 private InflatedSmartReplyState inflatedSmartReplyState; 1529 private InflatedSmartReplyViewHolder expandedInflatedSmartReplies; 1530 private InflatedSmartReplyViewHolder headsUpInflatedSmartReplies; 1531 1532 // ViewModel for SingleLineView, holds the UI State 1533 SingleLineViewModel mInflatedSingleLineViewModel; 1534 // ViewModel for public SingleLineView, holds the UI State 1535 SingleLineViewModel mPublicInflatedSingleLineViewModel; 1536 // Inflated SingleLineViewHolder, SingleLineView that lacks the UI State 1537 HybridNotificationView mInflatedSingleLineView; 1538 // Inflated SingleLineViewHolder, SingleLineView that lacks the UI State for the public 1539 // single line view 1540 HybridNotificationView mPublicInflatedSingleLineView; 1541 } 1542 1543 @VisibleForTesting 1544 abstract static class ApplyCallback { 1545 public abstract void setResultView(View v); 1546 1547 public abstract RemoteViews getRemoteView(); 1548 } 1549 1550 private static final String ASYNC_TASK_TRACE_METHOD = 1551 "NotificationContentInflater.AsyncInflationTask"; 1552 private static final String APPLY_TRACE_METHOD = "NotificationContentInflater#apply"; 1553 } 1554