1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.statusbar.notification.row; 18 19 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC; 20 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP; 21 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType; 22 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; 23 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; 24 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; 25 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; 26 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; 27 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; 28 29 import static org.junit.Assert.assertEquals; 30 import static org.junit.Assert.assertFalse; 31 import static org.junit.Assert.assertNotEquals; 32 import static org.junit.Assert.assertNotNull; 33 import static org.junit.Assert.assertNull; 34 import static org.junit.Assert.assertTrue; 35 import static org.mockito.ArgumentMatchers.any; 36 import static org.mockito.ArgumentMatchers.anyInt; 37 import static org.mockito.ArgumentMatchers.eq; 38 import static org.mockito.Mockito.mock; 39 import static org.mockito.Mockito.spy; 40 import static org.mockito.Mockito.times; 41 import static org.mockito.Mockito.verify; 42 import static org.mockito.Mockito.when; 43 44 import android.app.Notification; 45 import android.app.Person; 46 import android.content.Context; 47 import android.graphics.drawable.Icon; 48 import android.os.AsyncTask; 49 import android.os.CancellationSignal; 50 import android.os.Handler; 51 import android.os.Looper; 52 import android.platform.test.annotations.DisableFlags; 53 import android.platform.test.annotations.EnableFlags; 54 import android.testing.TestableLooper; 55 import android.testing.TestableLooper.RunWithLooper; 56 import android.util.TypedValue; 57 import android.view.View; 58 import android.view.ViewGroup; 59 import android.widget.RemoteViews; 60 import android.widget.TextView; 61 62 import androidx.test.ext.junit.runners.AndroidJUnit4; 63 import androidx.test.filters.SmallTest; 64 65 import com.android.systemui.SysuiTestCase; 66 import com.android.systemui.media.controls.util.MediaFeatureFlag; 67 import com.android.systemui.statusbar.NotificationRemoteInputManager; 68 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; 69 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; 70 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 71 import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor; 72 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; 73 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder; 74 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels; 75 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams; 76 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback; 77 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; 78 import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; 79 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor; 80 import com.android.systemui.statusbar.policy.InflatedSmartReplyState; 81 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder; 82 import com.android.systemui.statusbar.policy.SmartReplyStateInflater; 83 import com.android.systemui.tests.R; 84 85 import org.junit.Assert; 86 import org.junit.Before; 87 import org.junit.Ignore; 88 import org.junit.Test; 89 import org.junit.runner.RunWith; 90 import org.mockito.Mock; 91 import org.mockito.MockitoAnnotations; 92 93 import java.util.HashMap; 94 import java.util.concurrent.CountDownLatch; 95 import java.util.concurrent.Executor; 96 import java.util.concurrent.TimeUnit; 97 98 @SmallTest 99 @RunWith(AndroidJUnit4.class) 100 @RunWithLooper(setAsMainLooper = true) 101 @DisableFlags(NotificationRowContentBinderRefactor.FLAG_NAME) 102 public class NotificationContentInflaterTest extends SysuiTestCase { 103 104 private NotificationContentInflater mNotificationInflater; 105 private Notification.Builder mBuilder; 106 private ExpandableNotificationRow mRow; 107 108 private NotificationTestHelper mHelper; 109 110 @Mock private NotifRemoteViewCache mCache; 111 @Mock private ConversationNotificationProcessor mConversationNotificationProcessor; 112 @Mock private InflatedSmartReplyState mInflatedSmartReplyState; 113 @Mock private InflatedSmartReplyViewHolder mInflatedSmartReplies; 114 @Mock private NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider; 115 @Mock private HeadsUpStyleProvider mHeadsUpStyleProvider; 116 @Mock private NotifLayoutInflaterFactory mNotifLayoutInflaterFactory; 117 private final FakePromotedNotificationContentExtractor mPromotedNotificationContentExtractor = 118 new FakePromotedNotificationContentExtractor(); 119 120 private final SmartReplyStateInflater mSmartReplyStateInflater = 121 new SmartReplyStateInflater() { 122 @Override 123 public InflatedSmartReplyViewHolder inflateSmartReplyViewHolder( 124 Context sysuiContext, Context notifPackageContext, NotificationEntry entry, 125 InflatedSmartReplyState existingSmartReplyState, 126 InflatedSmartReplyState newSmartReplyState) { 127 return mInflatedSmartReplies; 128 } 129 130 @Override 131 public InflatedSmartReplyState inflateSmartReplyState(NotificationEntry entry) { 132 return mInflatedSmartReplyState; 133 } 134 }; 135 136 @Before setUp()137 public void setUp() throws Exception { 138 MockitoAnnotations.initMocks(this); 139 mBuilder = new Notification.Builder(mContext).setSmallIcon( 140 com.android.systemui.res.R.drawable.ic_person) 141 .setContentTitle("Title") 142 .setContentText("Text") 143 .setStyle(new Notification.BigTextStyle().bigText("big text")); 144 mHelper = new NotificationTestHelper( 145 mContext, 146 mDependency, 147 TestableLooper.get(this)); 148 ExpandableNotificationRow row = mHelper.createRow(mBuilder.build()); 149 mRow = spy(row); 150 when(mNotifLayoutInflaterFactoryProvider.provide(any(), anyInt())) 151 .thenReturn(mNotifLayoutInflaterFactory); 152 153 mNotificationInflater = new NotificationContentInflater( 154 mCache, 155 mock(NotificationRemoteInputManager.class), 156 mConversationNotificationProcessor, 157 mock(MediaFeatureFlag.class), 158 mock(Executor.class), 159 mSmartReplyStateInflater, 160 mNotifLayoutInflaterFactoryProvider, 161 mHeadsUpStyleProvider, 162 mPromotedNotificationContentExtractor, 163 mock(NotificationRowContentBinderLogger.class)); 164 } 165 166 @Test testInflationCallsUpdated()167 public void testInflationCallsUpdated() throws Exception { 168 inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); 169 verify(mRow).onNotificationUpdated(); 170 } 171 172 @Test testInflationOnlyInflatesSetFlags()173 public void testInflationOnlyInflatesSetFlags() throws Exception { 174 inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, mRow); 175 176 assertNotNull(mRow.getPrivateLayout().getHeadsUpChild()); 177 verify(mRow).onNotificationUpdated(); 178 } 179 180 @Test testInflationThrowsErrorDoesntCallUpdated()181 public void testInflationThrowsErrorDoesntCallUpdated() throws Exception { 182 mRow.getPrivateLayout().removeAllViews(); 183 mRow.getEntry().getSbn().getNotification().contentView 184 = new RemoteViews(mContext.getPackageName(), com.android.systemui.res.R.layout.status_bar); 185 inflateAndWait(true /* expectingException */, mNotificationInflater, FLAG_CONTENT_VIEW_ALL, 186 REDACTION_TYPE_NONE, mRow); 187 assertTrue(mRow.getPrivateLayout().getChildCount() == 0); 188 verify(mRow, times(0)).onNotificationUpdated(); 189 } 190 191 @Test testAsyncTaskRemoved()192 public void testAsyncTaskRemoved() throws Exception { 193 mRow.getEntry().abortTask(); 194 inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); 195 verify(mRow).onNotificationUpdated(); 196 } 197 198 @Test testRemovedNotInflated()199 public void testRemovedNotInflated() throws Exception { 200 mRow.setRemoved(); 201 mNotificationInflater.setInflateSynchronously(true); 202 mNotificationInflater.bindContent( 203 mRow.getEntry(), 204 mRow, 205 FLAG_CONTENT_VIEW_ALL, 206 new BindParams(false, REDACTION_TYPE_NONE), 207 false /* forceInflate */, 208 null /* callback */); 209 Assert.assertNull(mRow.getEntry().getRunningTask()); 210 } 211 212 @Test testInflationProcessesMessagingStyle()213 public void testInflationProcessesMessagingStyle() throws Exception { 214 String displayName = "Display Name"; 215 String messageText = "Message Text"; 216 Icon personIcon = Icon.createWithResource( 217 mContext, com.android.systemui.res.R.drawable.ic_person); 218 Person testPerson = new Person.Builder().setName(displayName).setIcon(personIcon).build(); 219 Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle(testPerson); 220 messagingStyle.addMessage(new Notification.MessagingStyle.Message( 221 messageText, System.currentTimeMillis(), testPerson)); 222 Notification messageNotif = new Notification.Builder(mContext) 223 .setSmallIcon(com.android.systemui.res.R.drawable.ic_person) 224 .setStyle(messagingStyle) 225 .build(); 226 ExpandableNotificationRow newRow = mHelper.createRow(messageNotif); 227 inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, newRow); 228 229 verify(mConversationNotificationProcessor).processNotification(any(), any(), any()); 230 } 231 232 @Test 233 @Ignore testInflationIsRetriedIfAsyncFails()234 public void testInflationIsRetriedIfAsyncFails() throws Exception { 235 NotificationContentInflater.InflationProgress result = 236 new NotificationContentInflater.InflationProgress(); 237 result.packageContext = mContext; 238 CountDownLatch countDownLatch = new CountDownLatch(1); 239 NotificationContentInflater.applyRemoteView( 240 AsyncTask.SERIAL_EXECUTOR, 241 false /* inflateSynchronously */, 242 /* isMinimized= */ false, 243 result, 244 FLAG_CONTENT_VIEW_EXPANDED, 245 0, 246 mock(NotifRemoteViewCache.class), 247 mRow.getEntry(), 248 mRow, 249 true /* isNewView */, (v, p, r) -> true, 250 new InflationCallback() { 251 @Override 252 public void handleInflationException(Exception e) { 253 countDownLatch.countDown(); 254 throw new RuntimeException("No Exception expected"); 255 } 256 257 @Override 258 public void onAsyncInflationFinished() { 259 countDownLatch.countDown(); 260 } 261 }, mRow.getPrivateLayout(), null, null, new HashMap<>(), 262 new NotificationContentInflater.ApplyCallback() { 263 @Override 264 public void setResultView(View v) { 265 } 266 267 @Override 268 public RemoteViews getRemoteView() { 269 return new AsyncFailRemoteView(mContext.getPackageName(), 270 R.layout.custom_view_dark); 271 } 272 }, 273 mock(NotificationRowContentBinderLogger.class)); 274 assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)); 275 } 276 277 @Test doesntReapplyDisallowedRemoteView()278 public void doesntReapplyDisallowedRemoteView() throws Exception { 279 mBuilder.setStyle(new Notification.MediaStyle()); 280 RemoteViews mediaView = mBuilder.createContentView(); 281 mBuilder.setStyle(new Notification.DecoratedCustomViewStyle()); 282 mBuilder.setCustomContentView(new RemoteViews(getContext().getPackageName(), 283 R.layout.custom_view_dark)); 284 RemoteViews decoratedMediaView = mBuilder.createContentView(); 285 assertFalse("The decorated media style doesn't allow a view to be reapplied!", 286 NotificationContentInflater.canReapplyRemoteView(mediaView, decoratedMediaView)); 287 } 288 289 @Test 290 @Ignore testUsesSameViewWhenCachedPossibleToReuse()291 public void testUsesSameViewWhenCachedPossibleToReuse() throws Exception { 292 // GIVEN a cached view. 293 RemoteViews contractedRemoteView = mBuilder.createContentView(); 294 when(mCache.hasCachedView(mRow.getEntry(), FLAG_CONTENT_VIEW_CONTRACTED)) 295 .thenReturn(true); 296 when(mCache.getCachedView(mRow.getEntry(), FLAG_CONTENT_VIEW_CONTRACTED)) 297 .thenReturn(contractedRemoteView); 298 299 // GIVEN existing bound view with same layout id. 300 View view = contractedRemoteView.apply(mContext, null /* parent */); 301 mRow.getPrivateLayout().setContractedChild(view); 302 303 // WHEN inflater inflates 304 inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, mRow); 305 306 // THEN the view should be re-used 307 assertEquals("Binder inflated a new view even though the old one was cached and usable.", 308 view, mRow.getPrivateLayout().getContractedChild()); 309 } 310 311 @Test testInflatesNewViewWhenCachedNotPossibleToReuse()312 public void testInflatesNewViewWhenCachedNotPossibleToReuse() throws Exception { 313 // GIVEN a cached remote view. 314 RemoteViews contractedRemoteView = mBuilder.createHeadsUpContentView(); 315 when(mCache.hasCachedView(mRow.getEntry(), FLAG_CONTENT_VIEW_CONTRACTED)) 316 .thenReturn(true); 317 when(mCache.getCachedView(mRow.getEntry(), FLAG_CONTENT_VIEW_CONTRACTED)) 318 .thenReturn(contractedRemoteView); 319 320 // GIVEN existing bound view with different layout id. 321 View view = new TextView(mContext); 322 mRow.getPrivateLayout().setContractedChild(view); 323 324 // WHEN inflater inflates 325 inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, mRow); 326 327 // THEN the view should be a new view 328 assertNotEquals("Binder (somehow) used the same view when inflating.", 329 view, mRow.getPrivateLayout().getContractedChild()); 330 } 331 332 @Test testInflationCachesCreatedRemoteView()333 public void testInflationCachesCreatedRemoteView() throws Exception { 334 // WHEN inflater inflates 335 inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, mRow); 336 337 // THEN inflater informs cache of the new remote view 338 verify(mCache).putCachedView( 339 eq(mRow.getEntry()), 340 eq(FLAG_CONTENT_VIEW_CONTRACTED), 341 any()); 342 } 343 344 @Test testUnbindRemovesCachedRemoteView()345 public void testUnbindRemovesCachedRemoteView() { 346 // WHEN inflated unbinds content 347 mNotificationInflater.unbindContent(mRow.getEntry(), mRow, FLAG_CONTENT_VIEW_HEADS_UP); 348 349 // THEN inflated informs cache to remove remote view 350 verify(mCache).removeCachedView( 351 eq(mRow.getEntry()), 352 eq(FLAG_CONTENT_VIEW_HEADS_UP)); 353 } 354 355 @Test 356 @Ignore testNotificationViewHeightTooSmallFailsValidation()357 public void testNotificationViewHeightTooSmallFailsValidation() { 358 View view = mock(View.class); 359 when(view.getHeight()) 360 .thenReturn((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10, 361 mContext.getResources().getDisplayMetrics())); 362 String result = NotificationContentInflater.isValidView(view, mRow.getEntry(), 363 mContext.getResources()); 364 assertNotNull(result); 365 } 366 367 @Test testNotificationViewPassesValidation()368 public void testNotificationViewPassesValidation() { 369 View view = mock(View.class); 370 when(view.getHeight()) 371 .thenReturn((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 17, 372 mContext.getResources().getDisplayMetrics())); 373 String result = NotificationContentInflater.isValidView(view, mRow.getEntry(), 374 mContext.getResources()); 375 assertNull(result); 376 } 377 378 @Test testInvalidNotificationDoesNotInvokeCallback()379 public void testInvalidNotificationDoesNotInvokeCallback() throws Exception { 380 mRow.getPrivateLayout().removeAllViews(); 381 mRow.getEntry().getSbn().getNotification().contentView = 382 new RemoteViews(mContext.getPackageName(), R.layout.invalid_notification_height); 383 inflateAndWait(true, mNotificationInflater, FLAG_CONTENT_VIEW_ALL, REDACTION_TYPE_NONE, 384 mRow); 385 assertEquals(0, mRow.getPrivateLayout().getChildCount()); 386 verify(mRow, times(0)).onNotificationUpdated(); 387 } 388 389 @Test 390 @DisableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) testExtractsPromotedContent_notWhenBothFlagsDisabled()391 public void testExtractsPromotedContent_notWhenBothFlagsDisabled() throws Exception { 392 final PromotedNotificationContentModels content = 393 new PromotedNotificationContentBuilder("key").build(); 394 mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); 395 396 inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); 397 398 mPromotedNotificationContentExtractor.verifyZeroExtractCalls(); 399 } 400 401 @Test 402 @EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) testExtractsPromotedContent_whenBothFlagsEnabled()403 public void testExtractsPromotedContent_whenBothFlagsEnabled() throws Exception { 404 final PromotedNotificationContentModels content = 405 new PromotedNotificationContentBuilder("key").build(); 406 mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); 407 408 inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); 409 410 mPromotedNotificationContentExtractor.verifyOneExtractCall(); 411 assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels()); 412 } 413 414 @Test 415 @EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) testExtractsPromotedContent_null()416 public void testExtractsPromotedContent_null() throws Exception { 417 mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), null); 418 419 inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); 420 421 mPromotedNotificationContentExtractor.verifyOneExtractCall(); 422 assertNull(mRow.getEntry().getPromotedNotificationContentModels()); 423 } 424 425 @Test 426 @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) testSensitiveContentPublicView_messageStyle()427 public void testSensitiveContentPublicView_messageStyle() throws Exception { 428 String displayName = "Display Name"; 429 String messageText = "Message Text"; 430 String contentText = "Content Text"; 431 Icon personIcon = Icon.createWithResource(mContext, 432 com.android.systemui.res.R.drawable.ic_person); 433 Person testPerson = new Person.Builder() 434 .setName(displayName) 435 .setIcon(personIcon) 436 .build(); 437 Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle(testPerson); 438 messagingStyle.addMessage(new Notification.MessagingStyle.Message(messageText, 439 System.currentTimeMillis(), testPerson)); 440 messagingStyle.setConversationType(Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL); 441 messagingStyle.setShortcutIcon(personIcon); 442 Notification messageNotif = new Notification.Builder(mContext).setSmallIcon( 443 com.android.systemui.res.R.drawable.ic_person).setStyle(messagingStyle).build(); 444 ExpandableNotificationRow row = mHelper.createRow(messageNotif); 445 inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, 446 REDACTION_TYPE_OTP, row); 447 NotificationContentView publicView = row.getPublicLayout(); 448 assertNotNull(publicView); 449 // The display name should be included, but not the content or message text 450 assertFalse(hasText(publicView, messageText)); 451 assertFalse(hasText(publicView, contentText)); 452 assertTrue(hasText(publicView, displayName)); 453 } 454 455 @Test 456 @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) testSensitiveContentPublicView_nonMessageStyle()457 public void testSensitiveContentPublicView_nonMessageStyle() throws Exception { 458 String contentTitle = "Content Title"; 459 String contentText = "Content Text"; 460 Notification notif = new Notification.Builder(mContext).setSmallIcon( 461 com.android.systemui.res.R.drawable.ic_person) 462 .setContentTitle(contentTitle) 463 .setContentText(contentText) 464 .build(); 465 ExpandableNotificationRow row = mHelper.createRow(notif); 466 inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, 467 REDACTION_TYPE_OTP, row); 468 NotificationContentView publicView = row.getPublicLayout(); 469 assertNotNull(publicView); 470 assertFalse(hasText(publicView, contentText)); 471 assertTrue(hasText(publicView, contentTitle)); 472 473 // The standard public view should not use the content title or text 474 inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, 475 REDACTION_TYPE_PUBLIC, row); 476 publicView = row.getPublicLayout(); 477 assertFalse(hasText(publicView, contentText)); 478 assertFalse(hasText(publicView, contentTitle)); 479 } 480 hasText(ViewGroup parent, CharSequence text)481 private static boolean hasText(ViewGroup parent, CharSequence text) { 482 for (int i = 0; i < parent.getChildCount(); i++) { 483 View child = parent.getChildAt(i); 484 if (child instanceof ViewGroup) { 485 if (hasText((ViewGroup) child, text)) { 486 return true; 487 } 488 } else if (child instanceof TextView) { 489 return ((TextView) child).getText().toString().contains(text); 490 } 491 } 492 return false; 493 } 494 inflateAndWait(NotificationContentInflater inflater, @InflationFlag int contentToInflate, ExpandableNotificationRow row)495 private static void inflateAndWait(NotificationContentInflater inflater, 496 @InflationFlag int contentToInflate, 497 ExpandableNotificationRow row) 498 throws Exception { 499 inflateAndWait(false /* expectingException */, inflater, contentToInflate, 500 REDACTION_TYPE_NONE, row); 501 } 502 inflateAndWait(boolean expectingException, NotificationContentInflater inflater, @InflationFlag int contentToInflate, @RedactionType int redactionType, ExpandableNotificationRow row)503 private static void inflateAndWait(boolean expectingException, 504 NotificationContentInflater inflater, 505 @InflationFlag int contentToInflate, 506 @RedactionType int redactionType, 507 ExpandableNotificationRow row) throws Exception { 508 CountDownLatch countDownLatch = new CountDownLatch(1); 509 final ExceptionHolder exceptionHolder = new ExceptionHolder(); 510 inflater.setInflateSynchronously(true); 511 InflationCallback callback = new InflationCallback() { 512 @Override 513 public void handleInflationException(Exception e) { 514 if (!expectingException) { 515 exceptionHolder.setException(e); 516 } 517 countDownLatch.countDown(); 518 } 519 520 @Override 521 public void onAsyncInflationFinished() { 522 if (expectingException) { 523 exceptionHolder.setException(new RuntimeException( 524 "Inflation finished even though there should be an error")); 525 } 526 countDownLatch.countDown(); 527 } 528 }; 529 inflater.bindContent( 530 row.getEntry(), 531 row, 532 contentToInflate, 533 new BindParams(false, redactionType), 534 false /* forceInflate */, 535 callback /* callback */); 536 assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)); 537 if (exceptionHolder.mException != null) { 538 throw exceptionHolder.mException; 539 } 540 } 541 542 private static class ExceptionHolder { 543 private Exception mException; 544 setException(Exception exception)545 public void setException(Exception exception) { 546 mException = exception; 547 } 548 } 549 550 private static class AsyncFailRemoteView extends RemoteViews { 551 Handler mHandler = Handler.createAsync(Looper.getMainLooper()); 552 AsyncFailRemoteView(String packageName, int layoutId)553 public AsyncFailRemoteView(String packageName, int layoutId) { 554 super(packageName, layoutId); 555 } 556 557 @Override apply(Context context, ViewGroup parent)558 public View apply(Context context, ViewGroup parent) { 559 return super.apply(context, parent); 560 } 561 562 @Override applyAsync(Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener, InteractionHandler handler)563 public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, 564 OnViewAppliedListener listener, InteractionHandler handler) { 565 mHandler.post(() -> listener.onError(new RuntimeException("Failed to inflate async"))); 566 return new CancellationSignal(); 567 } 568 569 @Override applyAsync(Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener)570 public CancellationSignal applyAsync(Context context, ViewGroup parent, Executor executor, 571 OnViewAppliedListener listener) { 572 return applyAsync(context, parent, executor, listener, null); 573 } 574 } 575 } 576