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.app.Notification 19 import android.app.Person 20 import android.content.Context 21 import android.graphics.drawable.Icon 22 import android.os.AsyncTask 23 import android.os.Build 24 import android.os.CancellationSignal 25 import android.platform.test.annotations.DisableFlags 26 import android.platform.test.annotations.EnableFlags 27 import android.testing.TestableLooper.RunWithLooper 28 import android.util.TypedValue 29 import android.util.TypedValue.COMPLEX_UNIT_SP 30 import android.view.View 31 import android.view.ViewGroup 32 import android.widget.RemoteViews 33 import android.widget.TextView 34 import androidx.test.ext.junit.runners.AndroidJUnit4 35 import androidx.test.filters.SmallTest 36 import com.android.systemui.SysuiTestCase 37 import com.android.systemui.res.R 38 import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE 39 import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP 40 import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC 41 import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType 42 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips 43 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor 44 import com.android.systemui.statusbar.notification.collection.NotificationEntry 45 import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor 46 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi 47 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder 48 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams 49 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL 50 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED 51 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED 52 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP 53 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC 54 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE 55 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback 56 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag 57 import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel 58 import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction 59 import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews 60 import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel 61 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor 62 import com.android.systemui.statusbar.policy.InflatedSmartReplyState 63 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder 64 import com.android.systemui.statusbar.policy.SmartReplyStateInflater 65 import java.util.concurrent.CountDownLatch 66 import java.util.concurrent.Executor 67 import java.util.concurrent.TimeUnit 68 import org.junit.Assert 69 import org.junit.Before 70 import org.junit.Ignore 71 import org.junit.Test 72 import org.junit.runner.RunWith 73 import org.mockito.kotlin.any 74 import org.mockito.kotlin.eq 75 import org.mockito.kotlin.mock 76 import org.mockito.kotlin.spy 77 import org.mockito.kotlin.times 78 import org.mockito.kotlin.verify 79 import org.mockito.kotlin.whenever 80 81 @SmallTest 82 @RunWith(AndroidJUnit4::class) 83 @RunWithLooper 84 @EnableFlags(NotificationRowContentBinderRefactor.FLAG_NAME, LockscreenOtpRedaction.FLAG_NAME) 85 class NotificationRowContentBinderImplTest : SysuiTestCase() { 86 private lateinit var notificationInflater: NotificationRowContentBinderImpl 87 private lateinit var builder: Notification.Builder 88 private lateinit var row: ExpandableNotificationRow 89 private lateinit var testHelper: NotificationTestHelper 90 91 private val cache: NotifRemoteViewCache = mock() 92 private val layoutInflaterFactoryProvider = 93 object : NotifLayoutInflaterFactory.Provider { 94 override fun provide( 95 row: ExpandableNotificationRow, 96 layoutType: Int, 97 ): NotifLayoutInflaterFactory = mock() 98 } 99 private val smartReplyStateInflater: SmartReplyStateInflater = 100 object : SmartReplyStateInflater { 101 private val inflatedSmartReplyState: InflatedSmartReplyState = mock() 102 private val inflatedSmartReplies: InflatedSmartReplyViewHolder = mock() 103 104 override fun inflateSmartReplyViewHolder( 105 sysuiContext: Context, 106 notifPackageContext: Context, 107 entry: NotificationEntry, 108 existingSmartReplyState: InflatedSmartReplyState?, 109 newSmartReplyState: InflatedSmartReplyState, 110 ): InflatedSmartReplyViewHolder { 111 return inflatedSmartReplies 112 } 113 114 override fun inflateSmartReplyState(entry: NotificationEntry): InflatedSmartReplyState { 115 return inflatedSmartReplyState 116 } 117 } 118 private val promotedNotificationContentExtractor = FakePromotedNotificationContentExtractor() 119 private val conversationNotificationProcessor: ConversationNotificationProcessor = mock() 120 121 @Before 122 fun setUp() { 123 allowTestableLooperAsMainThread() 124 builder = 125 Notification.Builder(mContext, "no-id") 126 .setSmallIcon(R.drawable.ic_person) 127 .setContentTitle("Title") 128 .setContentText("Text") 129 .setStyle(Notification.BigTextStyle().bigText("big text")) 130 testHelper = NotificationTestHelper(mContext, mDependency) 131 row = spy(testHelper.createRow(builder.build())) 132 notificationInflater = 133 NotificationRowContentBinderImpl( 134 cache, 135 mock(), 136 conversationNotificationProcessor, 137 mock(), 138 smartReplyStateInflater, 139 layoutInflaterFactoryProvider, 140 mock<HeadsUpStyleProvider>(), 141 promotedNotificationContentExtractor, 142 mock(), 143 ) 144 } 145 146 @Test 147 fun testInflationCallsUpdated() { 148 inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) 149 verify(row).onNotificationUpdated() 150 } 151 152 @Test 153 fun testInflationOnlyInflatesSetFlags() { 154 inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row) 155 Assert.assertNotNull(row.privateLayout.headsUpChild) 156 verify(row).onNotificationUpdated() 157 } 158 159 @Test 160 fun testInflationThrowsErrorDoesntCallUpdated() { 161 row.privateLayout.removeAllViews() 162 row.entry.sbn.notification.contentView = 163 RemoteViews(mContext.packageName, R.layout.status_bar) 164 inflateAndWait( 165 true, /* expectingException */ 166 notificationInflater, 167 FLAG_CONTENT_VIEW_ALL, 168 REDACTION_TYPE_NONE, 169 row, 170 ) 171 Assert.assertTrue(row.privateLayout.childCount == 0) 172 verify(row, times(0)).onNotificationUpdated() 173 } 174 175 @Test fun testInflationOfSensitiveContentPublicView() {} 176 177 @Test 178 fun testAsyncTaskRemoved() { 179 row.entry.abortTask() 180 inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) 181 verify(row).onNotificationUpdated() 182 } 183 184 @Test 185 fun testRemovedNotInflated() { 186 row.setRemoved() 187 notificationInflater.setInflateSynchronously(true) 188 notificationInflater.bindContent( 189 row.entry, 190 row, 191 FLAG_CONTENT_VIEW_ALL, 192 BindParams(false, REDACTION_TYPE_NONE), 193 false, /* forceInflate */ 194 null, /* callback */ 195 ) 196 Assert.assertNull(row.entry.runningTask) 197 } 198 199 @Test 200 @Ignore("b/345418902") 201 fun testInflationIsRetriedIfAsyncFails() { 202 val headsUpStatusBarModel = HeadsUpStatusBarModel("private", "public") 203 val result = 204 NotificationRowContentBinderImpl.InflationProgress( 205 packageContext = mContext, 206 rowImageInflater = RowImageInflater.newInstance(null, reinflating = false), 207 remoteViews = NewRemoteViews(), 208 contentModel = NotificationContentModel(headsUpStatusBarModel), 209 promotedContent = null, 210 ) 211 val countDownLatch = CountDownLatch(1) 212 NotificationRowContentBinderImpl.applyRemoteView( 213 AsyncTask.SERIAL_EXECUTOR, 214 inflateSynchronously = false, 215 isMinimized = false, 216 result = result, 217 reInflateFlags = FLAG_CONTENT_VIEW_EXPANDED, 218 inflationId = 0, 219 remoteViewCache = mock(), 220 entry = row.entry, 221 row = row, 222 isNewView = true, /* isNewView */ 223 remoteViewClickHandler = { _, _, _ -> true }, 224 callback = 225 object : InflationCallback { 226 override fun handleInflationException(e: Exception) { 227 countDownLatch.countDown() 228 throw RuntimeException("No Exception expected") 229 } 230 231 override fun onAsyncInflationFinished() { 232 countDownLatch.countDown() 233 } 234 }, 235 parentLayout = row.privateLayout, 236 existingView = null, 237 existingWrapper = null, 238 runningInflations = HashMap(), 239 applyCallback = 240 object : NotificationRowContentBinderImpl.ApplyCallback() { 241 override fun setResultView(v: View) {} 242 243 override val remoteView: RemoteViews 244 get() = 245 AsyncFailRemoteView( 246 mContext.packageName, 247 com.android.systemui.tests.R.layout.custom_view_dark, 248 ) 249 }, 250 logger = mock(), 251 ) 252 Assert.assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)) 253 } 254 255 @Test 256 fun doesntReapplyDisallowedRemoteView() { 257 builder.setStyle(Notification.MediaStyle()) 258 val mediaView = builder.createContentView() 259 builder.setStyle(Notification.DecoratedCustomViewStyle()) 260 builder.setCustomContentView( 261 RemoteViews(context.packageName, com.android.systemui.tests.R.layout.custom_view_dark) 262 ) 263 val decoratedMediaView = builder.createContentView() 264 Assert.assertFalse( 265 "The decorated media style doesn't allow a view to be reapplied!", 266 NotificationRowContentBinderImpl.canReapplyRemoteView(mediaView, decoratedMediaView), 267 ) 268 } 269 270 @Test 271 @Ignore("b/345418902") 272 fun testUsesSameViewWhenCachedPossibleToReuse() { 273 // GIVEN a cached view. 274 val contractedRemoteView = builder.createContentView() 275 whenever(cache.hasCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED)).thenReturn(true) 276 whenever(cache.getCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED)) 277 .thenReturn(contractedRemoteView) 278 279 // GIVEN existing bound view with same layout id. 280 val view = contractedRemoteView.apply(mContext, null /* parent */) 281 row.privateLayout.setContractedChild(view) 282 283 // WHEN inflater inflates 284 inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) 285 286 // THEN the view should be re-used 287 Assert.assertEquals( 288 "Binder inflated a new view even though the old one was cached and usable.", 289 view, 290 row.privateLayout.contractedChild, 291 ) 292 } 293 294 @Test 295 fun testInflatesNewViewWhenCachedNotPossibleToReuse() { 296 // GIVEN a cached remote view. 297 val contractedRemoteView = builder.createHeadsUpContentView() 298 whenever(cache.hasCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED)).thenReturn(true) 299 whenever(cache.getCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED)) 300 .thenReturn(contractedRemoteView) 301 302 // GIVEN existing bound view with different layout id. 303 val view: View = TextView(mContext) 304 row.privateLayout.setContractedChild(view) 305 306 // WHEN inflater inflates 307 inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) 308 309 // THEN the view should be a new view 310 Assert.assertNotEquals( 311 "Binder (somehow) used the same view when inflating.", 312 view, 313 row.privateLayout.contractedChild, 314 ) 315 } 316 317 @Test 318 fun testInflationCachesCreatedRemoteView() { 319 // WHEN inflater inflates 320 inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) 321 322 // THEN inflater informs cache of the new remote view 323 verify(cache).putCachedView(eq(row.entry), eq(FLAG_CONTENT_VIEW_CONTRACTED), any()) 324 } 325 326 @Test 327 fun testUnbindRemovesCachedRemoteView() { 328 // WHEN inflated unbinds content 329 notificationInflater.unbindContent(row.entry, row, FLAG_CONTENT_VIEW_HEADS_UP) 330 331 // THEN inflated informs cache to remove remote view 332 verify(cache).removeCachedView(eq(row.entry), eq(FLAG_CONTENT_VIEW_HEADS_UP)) 333 } 334 335 @Test 336 fun testNotificationViewHeightTooSmallFailsValidation() { 337 val validationError = 338 getValidationError( 339 measuredHeightDp = 5f, 340 targetSdk = Build.VERSION_CODES.R, 341 contentView = mock(), 342 ) 343 Assert.assertNotNull(validationError) 344 } 345 346 @Test 347 fun testNotificationViewHeightPassesForNewerSDK() { 348 val validationError = 349 getValidationError( 350 measuredHeightDp = 5f, 351 targetSdk = Build.VERSION_CODES.S, 352 contentView = mock(), 353 ) 354 Assert.assertNull(validationError) 355 } 356 357 @Test 358 fun testNotificationViewHeightPassesForTemplatedViews() { 359 val validationError = 360 getValidationError( 361 measuredHeightDp = 5f, 362 targetSdk = Build.VERSION_CODES.R, 363 contentView = null, 364 ) 365 Assert.assertNull(validationError) 366 } 367 368 @Test 369 fun testNotificationViewPassesValidation() { 370 val validationError = 371 getValidationError( 372 measuredHeightDp = 20f, 373 targetSdk = Build.VERSION_CODES.R, 374 contentView = mock(), 375 ) 376 Assert.assertNull(validationError) 377 } 378 379 private fun getValidationError( 380 measuredHeightDp: Float, 381 targetSdk: Int, 382 contentView: RemoteViews?, 383 ): String? { 384 val view: View = mock() 385 whenever(view.measuredHeight) 386 .thenReturn( 387 TypedValue.applyDimension( 388 COMPLEX_UNIT_SP, 389 measuredHeightDp, 390 mContext.resources.displayMetrics, 391 ) 392 .toInt() 393 ) 394 row.entry.targetSdk = targetSdk 395 row.entry.sbn.notification.contentView = contentView 396 return NotificationRowContentBinderImpl.isValidView(view, row.entry, mContext.resources) 397 } 398 399 @Test 400 fun testInvalidNotificationDoesNotInvokeCallback() { 401 row.privateLayout.removeAllViews() 402 row.entry.sbn.notification.contentView = 403 RemoteViews( 404 mContext.packageName, 405 com.android.systemui.tests.R.layout.invalid_notification_height, 406 ) 407 inflateAndWait(true, notificationInflater, FLAG_CONTENT_VIEW_ALL, REDACTION_TYPE_NONE, row) 408 Assert.assertEquals(0, row.privateLayout.childCount.toLong()) 409 verify(row, times(0)).onNotificationUpdated() 410 } 411 412 // TODO b/356709333: Add screenshot tests for these views 413 @Test 414 fun testInflatePublicSingleLineView() { 415 row.publicLayout.removeAllViews() 416 inflateAndWait( 417 false, 418 notificationInflater, 419 FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, 420 REDACTION_TYPE_NONE, 421 row, 422 ) 423 Assert.assertNotNull(row.publicLayout.mSingleLineView) 424 Assert.assertTrue(row.publicLayout.mSingleLineView is HybridNotificationView) 425 } 426 427 @Test 428 fun testInflatePublicSingleLineConversationView() { 429 val testPerson = Person.Builder().setName("Person").build() 430 val style = Notification.MessagingStyle(testPerson) 431 val messagingBuilder = 432 Notification.Builder(mContext, "no-id") 433 .setSmallIcon(R.drawable.ic_person) 434 .setContentTitle("Title") 435 .setContentText("Text") 436 .setStyle(style) 437 whenever(conversationNotificationProcessor.processNotification(any(), any(), any())) 438 .thenReturn(style) 439 440 val messagingRow = spy(testHelper.createRow(messagingBuilder.build())) 441 messagingRow.publicLayout.removeAllViews() 442 inflateAndWait( 443 false, 444 notificationInflater, 445 FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, 446 REDACTION_TYPE_NONE, 447 messagingRow, 448 ) 449 Assert.assertNotNull(messagingRow.publicLayout.mSingleLineView) 450 // assert this is the conversation layout 451 Assert.assertTrue( 452 messagingRow.publicLayout.mSingleLineView is HybridConversationNotificationView 453 ) 454 } 455 456 @Test 457 @DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) 458 fun testExtractsPromotedContent_notWhenBothFlagsDisabled() { 459 val content = PromotedNotificationContentBuilder("key").build() 460 promotedNotificationContentExtractor.resetForEntry(row.entry, content) 461 462 inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) 463 464 promotedNotificationContentExtractor.verifyZeroExtractCalls() 465 } 466 467 @Test 468 @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) 469 fun testExtractsPromotedContent_whenBothFlagsEnabled() { 470 val content = PromotedNotificationContentBuilder("key").build() 471 promotedNotificationContentExtractor.resetForEntry(row.entry, content) 472 473 inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) 474 475 promotedNotificationContentExtractor.verifyOneExtractCall() 476 Assert.assertEquals(content, row.entry.promotedNotificationContentModels) 477 } 478 479 @Test 480 @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) 481 fun testExtractsPromotedContent_null() { 482 promotedNotificationContentExtractor.resetForEntry(row.entry, null) 483 484 inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) 485 486 promotedNotificationContentExtractor.verifyOneExtractCall() 487 Assert.assertNull(row.entry.promotedNotificationContentModels) 488 } 489 490 @Test 491 @Throws(java.lang.Exception::class) 492 @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) 493 fun testSensitiveContentPublicView_messageStyle() { 494 val displayName = "Display Name" 495 val messageText = "Message Text" 496 val contentText = "Content Text" 497 val personIcon = Icon.createWithResource(mContext, R.drawable.ic_person) 498 val testPerson = Person.Builder().setName(displayName).setIcon(personIcon).build() 499 val messagingStyle = Notification.MessagingStyle(testPerson) 500 messagingStyle.addMessage( 501 Notification.MessagingStyle.Message(messageText, System.currentTimeMillis(), testPerson) 502 ) 503 messagingStyle.setConversationType(Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL) 504 messagingStyle.setShortcutIcon(personIcon) 505 val messageNotif = 506 Notification.Builder(mContext) 507 .setSmallIcon(R.drawable.ic_person) 508 .setStyle(messagingStyle) 509 .build() 510 val newRow: ExpandableNotificationRow = testHelper.createRow(messageNotif) 511 inflateAndWait( 512 false, 513 notificationInflater, 514 FLAG_CONTENT_VIEW_PUBLIC, 515 REDACTION_TYPE_OTP, 516 newRow, 517 ) 518 // The display name should be included, but not the content or message text 519 val publicView = newRow.publicLayout 520 Assert.assertNotNull(publicView) 521 Assert.assertFalse(hasText(publicView, messageText)) 522 Assert.assertFalse(hasText(publicView, contentText)) 523 Assert.assertTrue(hasText(publicView, displayName)) 524 } 525 526 @Test 527 @Throws(java.lang.Exception::class) 528 @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) 529 fun testSensitiveContentPublicView_nonMessageStyle() { 530 val contentTitle = "Content Title" 531 val contentText = "Content Text" 532 val notif = 533 Notification.Builder(mContext) 534 .setSmallIcon(R.drawable.ic_person) 535 .setContentTitle(contentTitle) 536 .setContentText(contentText) 537 .build() 538 val newRow: ExpandableNotificationRow = testHelper.createRow(notif) 539 inflateAndWait( 540 false, 541 notificationInflater, 542 FLAG_CONTENT_VIEW_PUBLIC, 543 REDACTION_TYPE_OTP, 544 newRow, 545 ) 546 var publicView = newRow.publicLayout 547 Assert.assertNotNull(publicView) 548 Assert.assertFalse(hasText(publicView, contentText)) 549 Assert.assertTrue(hasText(publicView, contentTitle)) 550 551 // The standard public view should not use the content title or text 552 inflateAndWait( 553 false, 554 notificationInflater, 555 FLAG_CONTENT_VIEW_PUBLIC, 556 REDACTION_TYPE_PUBLIC, 557 newRow, 558 ) 559 publicView = newRow.publicLayout 560 Assert.assertFalse(hasText(publicView, contentText)) 561 Assert.assertFalse(hasText(publicView, contentTitle)) 562 } 563 564 @Test 565 @Throws(java.lang.Exception::class) 566 @EnableFlags(android.app.Flags.FLAG_NM_SUMMARIZATION) 567 fun testAllMessagingStyleProcessedAsConversations() { 568 val displayName = "Display Name" 569 val messageText = "Message Text" 570 val personIcon = Icon.createWithResource(mContext, R.drawable.ic_person) 571 val testPerson = Person.Builder().setName(displayName).setIcon(personIcon).build() 572 val messagingStyle = Notification.MessagingStyle(testPerson) 573 messagingStyle.addMessage( 574 Notification.MessagingStyle.Message(messageText, System.currentTimeMillis(), testPerson) 575 ) 576 val messageNotif = 577 Notification.Builder(mContext) 578 .setSmallIcon(R.drawable.ic_person) 579 .setStyle(messagingStyle) 580 .build() 581 val newRow: ExpandableNotificationRow = testHelper.createRow(messageNotif) 582 583 inflateAndWait( 584 false, 585 notificationInflater, 586 FLAG_CONTENT_VIEW_ALL, 587 REDACTION_TYPE_NONE, 588 newRow, 589 ) 590 verify(conversationNotificationProcessor).processNotification(any(), any(), any()) 591 } 592 593 private class ExceptionHolder { 594 var exception: Exception? = null 595 } 596 597 private class AsyncFailRemoteView(packageName: String?, layoutId: Int) : 598 RemoteViews(packageName, layoutId) { 599 600 override fun apply(context: Context, parent: ViewGroup): View { 601 return super.apply(context, parent) 602 } 603 604 override fun applyAsync( 605 context: Context, 606 parent: ViewGroup, 607 executor: Executor, 608 listener: OnViewAppliedListener, 609 handler: InteractionHandler?, 610 ): CancellationSignal { 611 executor.execute { listener.onError(RuntimeException("Failed to inflate async")) } 612 return CancellationSignal() 613 } 614 615 override fun applyAsync( 616 context: Context, 617 parent: ViewGroup, 618 executor: Executor, 619 listener: OnViewAppliedListener, 620 ): CancellationSignal { 621 return applyAsync(context, parent, executor, listener, null) 622 } 623 } 624 625 companion object { 626 private fun inflateAndWait( 627 inflater: NotificationRowContentBinderImpl, 628 @InflationFlag contentToInflate: Int, 629 row: ExpandableNotificationRow, 630 ) { 631 inflateAndWait( 632 false /* expectingException */, 633 inflater, 634 contentToInflate, 635 REDACTION_TYPE_NONE, 636 row, 637 ) 638 } 639 640 private fun inflateAndWait( 641 expectingException: Boolean, 642 inflater: NotificationRowContentBinderImpl, 643 @InflationFlag contentToInflate: Int, 644 @RedactionType redactionType: Int, 645 row: ExpandableNotificationRow, 646 ) { 647 val countDownLatch = CountDownLatch(1) 648 val exceptionHolder = ExceptionHolder() 649 inflater.setInflateSynchronously(true) 650 val callback: InflationCallback = 651 object : InflationCallback { 652 override fun handleInflationException(e: Exception) { 653 if (!expectingException) { 654 exceptionHolder.exception = e 655 } 656 countDownLatch.countDown() 657 } 658 659 override fun onAsyncInflationFinished() { 660 if (expectingException) { 661 exceptionHolder.exception = 662 RuntimeException( 663 "Inflation finished even though there should be an error" 664 ) 665 } 666 countDownLatch.countDown() 667 } 668 } 669 inflater.bindContent( 670 row.entry, 671 row, 672 contentToInflate, 673 BindParams(false, redactionType), 674 false, /* forceInflate */ 675 callback, /* callback */ 676 ) 677 Assert.assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)) 678 exceptionHolder.exception?.let { throw it } 679 } 680 681 fun hasText(parent: ViewGroup, text: CharSequence): Boolean { 682 for (i in 0 until parent.childCount) { 683 val child = parent.getChildAt(i) 684 if (child is ViewGroup) { 685 if (hasText(child, text)) { 686 return true 687 } 688 } else if (child is TextView) { 689 return child.text.toString().contains(text) 690 } 691 } 692 return false 693 } 694 } 695 } 696