• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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