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