1 /*
2  * Copyright (C) 2013 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.cts.verifier.notifications;
18 
19 import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
20 import static android.app.NotificationManager.IMPORTANCE_LOW;
21 import static android.app.NotificationManager.IMPORTANCE_MAX;
22 import static android.app.NotificationManager.IMPORTANCE_NONE;
23 import static android.provider.Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS;
24 import static android.provider.Settings.EXTRA_APP_PACKAGE;
25 import static android.provider.Settings.EXTRA_CHANNEL_ID;
26 
27 import static com.android.cts.verifier.notifications.MockListener.JSON_FLAGS;
28 import static com.android.cts.verifier.notifications.MockListener.JSON_ICON;
29 import static com.android.cts.verifier.notifications.MockListener.JSON_ID;
30 import static com.android.cts.verifier.notifications.MockListener.JSON_LAST_AUDIBLY_ALERTED;
31 import static com.android.cts.verifier.notifications.MockListener.JSON_PACKAGE;
32 import static com.android.cts.verifier.notifications.MockListener.JSON_REASON;
33 import static com.android.cts.verifier.notifications.MockListener.JSON_STATS;
34 import static com.android.cts.verifier.notifications.MockListener.JSON_TAG;
35 import static com.android.cts.verifier.notifications.MockListener.JSON_WHEN;
36 import static com.android.cts.verifier.notifications.MockListener.REASON_LISTENER_CANCEL;
37 
38 import android.annotation.SuppressLint;
39 import android.app.Notification;
40 import android.app.NotificationChannel;
41 import android.app.NotificationChannelGroup;
42 import android.app.PendingIntent;
43 import android.app.Person;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.SharedPreferences;
47 import android.content.pm.PackageManager;
48 import android.content.pm.ShortcutInfo;
49 import android.content.pm.ShortcutManager;
50 import android.graphics.drawable.Icon;
51 import android.os.Bundle;
52 import android.os.SystemClock;
53 import android.os.VibratorManager;
54 import android.provider.Settings;
55 import android.provider.Settings.Secure;
56 import android.service.notification.StatusBarNotification;
57 import android.util.ArraySet;
58 import android.util.Log;
59 import android.view.View;
60 import android.view.ViewGroup;
61 import android.widget.Button;
62 import android.widget.RemoteViews;
63 
64 import androidx.core.app.NotificationCompat;
65 
66 import com.android.cts.verifier.R;
67 import com.android.cts.verifier.TestListActivity;
68 
69 import org.json.JSONException;
70 import org.json.JSONObject;
71 
72 import java.util.ArrayList;
73 import java.util.Arrays;
74 import java.util.HashSet;
75 import java.util.List;
76 import java.util.Set;
77 import java.util.UUID;
78 
79 public class NotificationListenerVerifierActivity extends InteractiveVerifierActivity
80         implements Runnable {
81     static final String TAG = "NoListenerVerifier";
82     private static final String NOTIFICATION_CHANNEL_ID = TAG;
83     private static final String NOISY_NOTIFICATION_CHANNEL_ID = TAG + "noisy";
84     protected static final String PREFS = "listener_prefs";
85     final int NUM_NOTIFICATIONS_SENT = 3; // # notifications sent by sendNotifications()
86 
87     private String mTag1;
88     private String mTag2;
89     private String mTag3;
90     private String mTag4;
91     private String mTag5;
92     private int mIcon1;
93     private int mIcon2;
94     private int mIcon3;
95     private int mIcon4;
96     private int mIcon5;
97     private int mId1;
98     private int mId2;
99     private int mId3;
100     private int mId4;
101     private int mId5;
102     private long mWhen1;
103     private long mWhen2;
104     private long mWhen3;
105     private long mWhen4;
106     private long mWhen5;
107     private int mFlag1;
108     private int mFlag2;
109     private int mFlag3;
110 
111     @Override
getTitleResource()112     protected int getTitleResource() {
113         return R.string.nls_test;
114     }
115 
116     @Override
getInstructionsResource()117     protected int getInstructionsResource() {
118         return R.string.nls_info;
119     }
120 
121     // Test Setup
122 
123     @Override
onCreate(Bundle savedInstanceState)124     protected void onCreate(Bundle savedInstanceState) {
125         super.onCreate(savedInstanceState);
126         String displayMode = getIntent().getStringExtra("DISPLAY_MODE");
127         if (displayMode != null) {
128             // Update the display mode for NotificationListenerVerifierActivity due to b/255265824.
129             Log.i(TAG, "onCreate with displayMode=" + displayMode);
130             TestListActivity.sCurrentDisplayMode = displayMode;
131         }
132     }
133 
134     @Override
createTestItems()135     protected List<InteractiveTestCase> createTestItems() {
136         boolean isAutomotive =
137                 getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
138         List<InteractiveTestCase> tests = new ArrayList<>(17);
139         tests.add(new IsEnabledTest());
140         tests.add(new ServiceStartedTest());
141         tests.add(new NotificationReceivedTest());
142         /* TODO(b/284478205): Reenable or remove along with NLS meta-data in manifest. */
143         /* if (!isAutomotive) {
144             tests.add(new SendUserToChangeFilter());
145             tests.add(new AskIfFilterChanged());
146             tests.add(new NotificationTypeFilterTest());
147             tests.add(new ResetTypeFilterAddAppFilter());
148             tests.add(new AskIfReadyToProceed());
149             tests.add(new NotificationAppFilterTest());
150             tests.add(new ResetAppFilter());
151             tests.add(new AskIfReadyToProceed());
152         } */
153         tests.add(new LongMessageTest());
154         tests.add(new DataIntactTest());
155         tests.add(new AudiblyAlertedTest());
156         tests.add(new DismissOneTest());
157         tests.add(new DismissOneWithReasonTest());
158         tests.add(new DismissOneWithStatsTest());
159         tests.add(new DismissOngoingTest());
160         tests.add(new DismissAllTest());
161         tests.add(new SnoozeNotificationForTimeTest());
162         tests.add(new SnoozeNotificationForTimeCancelTest());
163         tests.add(new GetSnoozedNotificationTest());
164         tests.add(new EnableHintsTest());
165         tests.add(new ReceiveAppBlockNoticeTest());
166         tests.add(new ReceiveAppUnblockNoticeTest());
167         if (!isAutomotive) {
168             tests.add(new ReceiveChannelBlockNoticeTest());
169             tests.add(new ReceiveGroupBlockNoticeTest());
170             tests.add(new UpdateChannelWithFilterTest());
171             tests.add(new UpdateChannelWithFilterHalfSheetTest());
172         }
173         tests.add(new RequestUnbindTest());
174         tests.add(new RequestBindTest());
175         tests.add(new MessageBundleTest());
176         tests.add(new ConversationOrderingTest());
177         tests.add(new HunDisplayTest());
178         tests.add(new EnableHintsTest());
179         tests.add(new IsDisabledTest());
180         tests.add(new ServiceStoppedTest());
181         tests.add(new NotificationNotReceivedTest());
182         return tests;
183     }
184 
createChannels()185     private void createChannels() {
186         NotificationChannel channel =
187                 new NotificationChannel(
188                         NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, IMPORTANCE_LOW);
189         NotificationChannel noisyChannel =
190                 new NotificationChannel(
191                         NOISY_NOTIFICATION_CHANNEL_ID,
192                         NOISY_NOTIFICATION_CHANNEL_ID,
193                         IMPORTANCE_MAX);
194         noisyChannel.setVibrationPattern(new long[] {100, 0, 100});
195         mNm.createNotificationChannel(channel);
196         mNm.createNotificationChannel(noisyChannel);
197     }
198 
deleteChannels()199     private void deleteChannels() {
200         mNm.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
201         mNm.deleteNotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID);
202     }
203 
204     @SuppressLint("NewApi")
sendNotifications()205     private void sendNotifications() {
206         mTag1 = UUID.randomUUID().toString();
207         Log.d(TAG, "Sending #1: " + mTag1);
208         mTag2 = UUID.randomUUID().toString();
209         Log.d(TAG, "Sending #2: " + mTag2);
210         mTag3 = UUID.randomUUID().toString();
211         Log.d(TAG, "Sending #3: " + mTag3);
212 
213         mWhen1 = System.currentTimeMillis() + 1;
214         mWhen2 = System.currentTimeMillis() + 2;
215         mWhen3 = System.currentTimeMillis() + 3;
216 
217         mIcon1 = R.drawable.ic_stat_alice;
218         mIcon2 = R.drawable.ic_stat_bob;
219         mIcon3 = R.drawable.ic_stat_charlie;
220 
221         mId1 = NOTIFICATION_ID + 1;
222         mId2 = NOTIFICATION_ID + 2;
223         mId3 = NOTIFICATION_ID + 3;
224 
225         mPackageString = "com.android.cts.verifier";
226 
227         Notification n1 =
228                 new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
229                         .setContentTitle("ClearTest 1")
230                         .setContentText(mTag1)
231                         .setSmallIcon(mIcon1)
232                         .setWhen(mWhen1)
233                         .setDeleteIntent(makeIntent(1, mTag1))
234                         .setOnlyAlertOnce(true)
235                         .build();
236         mNm.notify(mTag1, mId1, n1);
237         mFlag1 = Notification.FLAG_ONLY_ALERT_ONCE;
238 
239         Notification n2 =
240                 new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
241                         .setContentTitle("ClearTest 2")
242                         .setContentText(mTag2)
243                         .setSmallIcon(mIcon2)
244                         .setWhen(mWhen2)
245                         .setDeleteIntent(makeIntent(2, mTag2))
246                         .setAutoCancel(true)
247                         .build();
248         mNm.notify(mTag2, mId2, n2);
249         mFlag2 = Notification.FLAG_AUTO_CANCEL;
250 
251         Notification n3 =
252                 new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
253                         .setContentTitle("ClearTest 3")
254                         .setContentText(mTag3)
255                         .setSmallIcon(mIcon3)
256                         .setWhen(mWhen3)
257                         .setDeleteIntent(makeIntent(3, mTag3))
258                         .setAutoCancel(true)
259                         .setOnlyAlertOnce(true)
260                         .build();
261         mNm.notify(mTag3, mId3, n3);
262         mFlag3 = Notification.FLAG_ONLY_ALERT_ONCE | Notification.FLAG_AUTO_CANCEL;
263     }
264 
sendNoisyNotification()265     private void sendNoisyNotification() {
266         mTag4 = UUID.randomUUID().toString();
267         Log.d(TAG, "Sending noisy notif: " + mTag4);
268 
269         mWhen4 = System.currentTimeMillis() + 4;
270         mIcon4 = R.drawable.ic_stat_charlie;
271         mId4 = NOTIFICATION_ID + 4;
272         mPackageString = "com.android.cts.verifier";
273 
274         Notification n1 =
275                 new Notification.Builder(mContext, NOISY_NOTIFICATION_CHANNEL_ID)
276                         .setContentTitle("NoisyTest 1")
277                         .setContentText(mTag4)
278                         .setSmallIcon(mIcon4)
279                         .setWhen(mWhen4)
280                         .setDeleteIntent(makeIntent(4, mTag4))
281                         .setCategory(Notification.CATEGORY_REMINDER)
282                         .build();
283         mNm.notify(mTag4, mId4, n1);
284     }
285 
286     /**
287      * NotificationListener cannot dismiss notifications with FLAG_ONGOING_EVENT
288      */
sendOngoingNotification()289     private void sendOngoingNotification() {
290         mTag5 = UUID.randomUUID().toString();
291         Log.d(TAG, "Sending ongoing notif: " + mTag5);
292 
293         mWhen5 = System.currentTimeMillis() + 5;
294         mIcon5 = R.drawable.ic_stat_charlie;
295         mId5 = NOTIFICATION_ID + 5;
296         mPackageString = "com.android.cts.verifier";
297 
298         Notification n =
299                 new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
300                         .setContentTitle("ClearOngoingTest 1")
301                         .setContentText(mTag5)
302                         .setSmallIcon(mIcon5)
303                         .setWhen(mWhen5)
304                         .setDeleteIntent(makeIntent(5, mTag5))
305                         .setOnlyAlertOnce(true)
306                         .setOngoing(true)
307                         .build();
308         mNm.notify(mTag5, mId5, n);
309     }
310 
311     // Tests
312     private class NotificationReceivedTest extends InteractiveTestCase {
313         @Override
inflate(ViewGroup parent)314         protected View inflate(ViewGroup parent) {
315             return createAutoItem(parent, R.string.nls_note_received);
316         }
317 
318         @Override
setUp()319         protected void setUp() {
320             createChannels();
321             sendNotifications();
322             status = READY;
323         }
324 
325         @Override
tearDown()326         protected void tearDown() {
327             mNm.cancelAll();
328             MockListener.getInstance().resetData();
329             deleteChannels();
330         }
331 
332         @Override
test()333         protected void test() {
334             if (MockListener.getInstance().getPosted(mTag1) != null) {
335                 status = PASS;
336             } else {
337                 logFail();
338                 status = FAIL;
339             }
340         }
341     }
342 
343     private class LongMessageTest extends InteractiveTestCase {
344         private ViewGroup mParent;
345 
346         @Override
inflate(ViewGroup parent)347         protected View inflate(ViewGroup parent) {
348             mParent = createAutoItem(parent, R.string.nls_anr);
349             return mParent;
350         }
351 
352         @Override
setUp()353         protected void setUp() {
354             createChannels();
355             StringBuilder sb = new StringBuilder();
356             for (int i = 0; i < 20000; i++) {
357                 sb.append("\u2009\u200a" + "\u200E\u200F" + "stuff");
358             }
359             Notification.Builder builder =
360                     new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
361                             .setSmallIcon(R.drawable.ic_stat_alice)
362                             .setContentTitle("This is an long notification")
363                             .setContentText("Innocuous content")
364                             .setStyle(
365                                     new Notification.MessagingStyle("Fake person")
366                                             .addMessage("hey how is it goin", 0, "Person 1")
367                                             .addMessage("hey", 0, "Person 1")
368                                             .addMessage("u there", 0, "Person 1")
369                                             .addMessage("how you like tHIS", 0, "Person 1")
370                                             .addMessage(sb.toString(), 0, "Person 1"));
371             mTag1 = UUID.randomUUID().toString();
372             mId1 = NOTIFICATION_ID + 1;
373             mPackageString = "com.android.cts.verifier";
374             mNm.notify(mTag1, mId1, builder.build());
375             status = READY;
376         }
377 
378         @Override
tearDown()379         protected void tearDown() {
380             mNm.cancelAll();
381             MockListener.getInstance().resetData();
382             deleteChannels();
383         }
384 
385         @Override
test()386         protected void test() {
387             StatusBarNotification sbn = MockListener.getInstance().getPosted(mTag1);
388             if (sbn == null) {
389                 logFail();
390                 status = FAIL;
391             } else {
392                 ViewGroup parent = mParent.findViewById(R.id.feedback);
393                 parent.setVisibility(View.VISIBLE);
394                 final Notification.Builder recoveredBuilder =
395                         Notification.Builder.recoverBuilder(
396                                 NotificationListenerVerifierActivity.this, sbn.getNotification());
397                 RemoteViews rv = recoveredBuilder.createContentView();
398                 View v = rv.apply(NotificationListenerVerifierActivity.this, parent);
399                 parent.addView(v);
400             }
401             if (MockListener.getInstance().getPosted(mTag1) != null) {
402                 status = PASS;
403             } else {
404                 logFail();
405                 status = FAIL;
406             }
407         }
408     }
409 
410     /**
411      * Creates a notification channel. Sends the user to settings to block the channel. Waits to
412      * receive the broadcast that the channel was blocked, and confirms that the broadcast contains
413      * the correct extras.
414      */
415     protected class ReceiveChannelBlockNoticeTest extends InteractiveTestCase {
416         private String mChannelId;
417         private int mRetries = 2;
418         private View mView;
419 
420         @Override
inflate(ViewGroup parent)421         protected View inflate(ViewGroup parent) {
422             mView = createNlsSettingsItem(parent, R.string.nls_block_channel);
423             Button button = mView.findViewById(R.id.nls_action_button);
424             button.setEnabled(false);
425             return mView;
426         }
427 
428         @Override
setUp()429         protected void setUp() {
430             mChannelId = UUID.randomUUID().toString();
431             NotificationChannel channel =
432                     new NotificationChannel(
433                             mChannelId, "ReceiveChannelBlockNoticeTest", IMPORTANCE_LOW);
434             mNm.createNotificationChannel(channel);
435             status = READY;
436             Button button = mView.findViewById(R.id.nls_action_button);
437             button.setEnabled(true);
438         }
439 
440         @Override
autoStart()441         boolean autoStart() {
442             return true;
443         }
444 
445         @Override
test()446         protected void test() {
447             NotificationChannel channel = mNm.getNotificationChannel(mChannelId);
448             SharedPreferences prefs =
449                     mContext.getSharedPreferences(
450                             NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
451 
452             if (channel.getImportance() == IMPORTANCE_NONE) {
453                 if (prefs.contains(mChannelId) && prefs.getBoolean(mChannelId, false)) {
454                     status = PASS;
455                 } else {
456                     if (mRetries > 0) {
457                         mRetries--;
458                         status = RETEST;
459                     } else {
460                         status = FAIL;
461                     }
462                 }
463             } else {
464                 // user hasn't jumped to settings to block the channel yet
465                 status = WAIT_FOR_USER;
466             }
467 
468             next();
469         }
470 
471         @Override
tearDown()472         protected void tearDown() {
473             MockListener.getInstance().resetData();
474             mNm.deleteNotificationChannel(mChannelId);
475             SharedPreferences prefs =
476                     mContext.getSharedPreferences(
477                             NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
478             SharedPreferences.Editor editor = prefs.edit();
479             editor.remove(mChannelId);
480         }
481 
482         @Override
getIntent()483         protected Intent getIntent() {
484             return new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
485                     .putExtra(EXTRA_APP_PACKAGE, mContext.getPackageName())
486                     .putExtra(EXTRA_CHANNEL_ID, mChannelId);
487         }
488     }
489 
hasVibrator()490     private boolean hasVibrator() {
491         return mContext.getSystemService(VibratorManager.class).getDefaultVibrator().hasVibrator();
492     }
493 
494     /**
495      * Creates a notification channel. Sends the user to the channel settings half-sheet to toggle
496      * vibration settings and waits for that to be done.
497      */
498     protected class UpdateChannelWithFilterTest extends InteractiveTestCase {
499         private String mChannelId;
500         private View mView;
501 
502         @Override
inflate(ViewGroup parent)503         protected View inflate(ViewGroup parent) {
504             int instructions = hasVibrator()
505                     ? R.string.nls_channel_settings_with_filter_instructions
506                     : R.string.nls_channel_settings_with_filter_instructions_no_vibrator;
507             mView = createNlsSettingsItem(parent, instructions);
508             Button button = mView.findViewById(R.id.nls_action_button);
509             button.setEnabled(false);
510             return mView;
511         }
512 
513         @Override
setUp()514         protected void setUp() {
515             mChannelId = UUID.randomUUID().toString();
516             NotificationChannel channel =
517                     new NotificationChannel(
518                             mChannelId, "UpdateChannelWithFilterTest", IMPORTANCE_DEFAULT);
519             channel.enableVibration(false);
520             channel.setSound(null, null);
521             mNm.createNotificationChannel(channel);
522             status = READY;
523             Button button = mView.findViewById(R.id.nls_action_button);
524             button.setEnabled(true);
525         }
526 
527         @Override
autoStart()528         boolean autoStart() {
529             return true;
530         }
531 
532         @Override
test()533         protected void test() {
534             NotificationChannel channel = mNm.getNotificationChannel(mChannelId);
535             if (hasVibrator()) {
536                 status = channel.shouldVibrate() ? PASS : WAIT_FOR_USER;
537             } else {
538                 status = channel.getSound() != null ? PASS : WAIT_FOR_USER;
539             }
540             next();
541         }
542 
543         @Override
tearDown()544         protected void tearDown() {
545             mNm.deleteNotificationChannel(mChannelId);
546         }
547 
548         @Override
getIntent()549         protected Intent getIntent() {
550             return new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
551                     .putExtra(EXTRA_APP_PACKAGE, mContext.getPackageName())
552                     .putExtra(EXTRA_CHANNEL_ID, mChannelId)
553                     .putStringArrayListExtra(Settings.EXTRA_CHANNEL_FILTER_LIST,
554                             new ArrayList<>(Arrays.asList(
555                                     NotificationChannel.EDIT_SOUND,
556                                     NotificationChannel.EDIT_VIBRATION)));
557         }
558     }
559 
560     /**
561      * Asks the user to verify the appearance of the channel settings sheet they just saw.
562      */
563     protected class UpdateChannelWithFilterHalfSheetTest extends InteractiveTestCase {
564 
565         @Override
inflate(ViewGroup parent)566         protected View inflate(ViewGroup parent) {
567             int verification = hasVibrator()
568                     ? R.string.nls_channel_settings_with_filter_verification
569                     : R.string.nls_channel_settings_with_filter_verification_no_vibrator;
570             return createPassFailItem(parent, verification);
571         }
572 
573         @Override
autoStart()574         boolean autoStart() {
575             return true;
576         }
577 
578         @Override
test()579         protected void test() {
580             status = WAIT_FOR_USER;
581             next();
582         }
583     }
584 
585     /**
586      * Creates a notification channel group. Sends the user to settings to block the group. Waits to
587      * receive the broadcast that the group was blocked, and confirms that the broadcast contains
588      * the correct extras.
589      */
590     protected class ReceiveGroupBlockNoticeTest extends InteractiveTestCase {
591         private String mGroupId;
592         private int mRetries = 2;
593         private View mView;
594 
595         @Override
inflate(ViewGroup parent)596         protected View inflate(ViewGroup parent) {
597             mView = createNlsSettingsItem(parent, R.string.nls_block_group);
598             Button button = mView.findViewById(R.id.nls_action_button);
599             button.setEnabled(false);
600             return mView;
601         }
602 
603         @Override
setUp()604         protected void setUp() {
605             mGroupId = UUID.randomUUID().toString();
606             NotificationChannelGroup group =
607                     new NotificationChannelGroup(mGroupId, "ReceiveChannelGroupBlockNoticeTest");
608             mNm.createNotificationChannelGroup(group);
609             NotificationChannel channel =
610                     new NotificationChannel(
611                             mGroupId, "ReceiveChannelBlockNoticeTest", IMPORTANCE_NONE);
612             channel.setGroup(mGroupId);
613             mNm.createNotificationChannel(channel);
614             status = READY;
615             Button button = mView.findViewById(R.id.nls_action_button);
616             button.setEnabled(true);
617         }
618 
619         @Override
autoStart()620         boolean autoStart() {
621             return true;
622         }
623 
624         @Override
test()625         protected void test() {
626             NotificationChannelGroup group = mNm.getNotificationChannelGroup(mGroupId);
627             SharedPreferences prefs =
628                     mContext.getSharedPreferences(
629                             NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
630 
631             if (group.isBlocked()) {
632                 if (prefs.contains(mGroupId) && prefs.getBoolean(mGroupId, false)) {
633                     status = PASS;
634                 } else {
635                     if (mRetries > 0) {
636                         mRetries--;
637                         status = RETEST;
638                     } else {
639                         status = FAIL;
640                     }
641                 }
642             } else {
643                 // user hasn't jumped to settings to block the group yet
644                 status = WAIT_FOR_USER;
645             }
646 
647             next();
648         }
649 
tearDown()650         protected void tearDown() {
651             MockListener.getInstance().resetData();
652             mNm.deleteNotificationChannelGroup(mGroupId);
653             SharedPreferences prefs =
654                     mContext.getSharedPreferences(
655                             NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
656             SharedPreferences.Editor editor = prefs.edit();
657             editor.remove(mGroupId);
658         }
659 
660         @Override
getIntent()661         protected Intent getIntent() {
662             return new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
663                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
664                     .putExtra(EXTRA_APP_PACKAGE, mContext.getPackageName());
665         }
666     }
667 
668     /**
669      * Sends the user to settings to block the app. Waits to receive the broadcast that the app was
670      * blocked, and confirms that the broadcast contains the correct extras.
671      */
672     protected class ReceiveAppBlockNoticeTest extends InteractiveTestCase {
673         private int mRetries = 2;
674         private View mView;
675 
676         @Override
inflate(ViewGroup parent)677         protected View inflate(ViewGroup parent) {
678             mView = createNlsSettingsItem(parent, R.string.nls_block_app);
679             Button button = mView.findViewById(R.id.nls_action_button);
680             button.setEnabled(false);
681             return mView;
682         }
683 
684         @Override
setUp()685         protected void setUp() {
686             status = READY;
687             Button button = mView.findViewById(R.id.nls_action_button);
688             button.setEnabled(true);
689         }
690 
691         @Override
autoStart()692         boolean autoStart() {
693             return true;
694         }
695 
696         @Override
test()697         protected void test() {
698             SharedPreferences prefs =
699                     mContext.getSharedPreferences(
700                             NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
701 
702             if (!mNm.areNotificationsEnabled()) {
703                 Log.d(TAG, "Got broadcast " + prefs.contains(mContext.getPackageName()));
704                 Log.d(
705                         TAG,
706                         "Broadcast contains correct data? "
707                                 + prefs.getBoolean(mContext.getPackageName(), false));
708                 if (prefs.contains(mContext.getPackageName())
709                         && prefs.getBoolean(mContext.getPackageName(), false)) {
710                     status = PASS;
711                 } else {
712                     if (mRetries > 0) {
713                         mRetries--;
714                         status = RETEST;
715                     } else {
716                         status = FAIL;
717                     }
718                 }
719             } else {
720                 Log.d(TAG, "Notifications still enabled");
721                 // user hasn't jumped to settings to block the app yet
722                 status = WAIT_FOR_USER;
723             }
724 
725             next();
726         }
727 
tearDown()728         protected void tearDown() {
729             MockListener.getInstance().resetData();
730             SharedPreferences prefs =
731                     mContext.getSharedPreferences(
732                             NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
733             SharedPreferences.Editor editor = prefs.edit();
734             editor.remove(mContext.getPackageName());
735         }
736 
737         @Override
getIntent()738         protected Intent getIntent() {
739             return new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
740                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
741                     .putExtra(EXTRA_APP_PACKAGE, mContext.getPackageName());
742         }
743     }
744 
745     /**
746      * Sends the user to settings to unblock the app. Waits to receive the broadcast that the app
747      * was unblocked, and confirms that the broadcast contains the correct extras.
748      */
749     protected class ReceiveAppUnblockNoticeTest extends InteractiveTestCase {
750         private int mRetries = 2;
751         private View mView;
752 
753         @Override
inflate(ViewGroup parent)754         protected View inflate(ViewGroup parent) {
755             mView = createNlsSettingsItem(parent, R.string.nls_unblock_app);
756             Button button = mView.findViewById(R.id.nls_action_button);
757             button.setEnabled(false);
758             return mView;
759         }
760 
761         @Override
setUp()762         protected void setUp() {
763             status = READY;
764             Button button = mView.findViewById(R.id.nls_action_button);
765             button.setEnabled(true);
766         }
767 
768         @Override
autoStart()769         boolean autoStart() {
770             return true;
771         }
772 
773         @Override
test()774         protected void test() {
775             SharedPreferences prefs =
776                     mContext.getSharedPreferences(
777                             NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
778 
779             if (mNm.areNotificationsEnabled()) {
780                 if (prefs.contains(mContext.getPackageName())
781                         && !prefs.getBoolean(mContext.getPackageName(), true)) {
782                     status = PASS;
783                 } else {
784                     if (mRetries > 0) {
785                         mRetries--;
786                         status = RETEST;
787                     } else {
788                         status = FAIL;
789                     }
790                 }
791             } else {
792                 // user hasn't jumped to settings to block the app yet
793                 status = WAIT_FOR_USER;
794             }
795 
796             next();
797         }
798 
tearDown()799         protected void tearDown() {
800             MockListener.getInstance().resetData();
801             SharedPreferences prefs =
802                     mContext.getSharedPreferences(
803                             NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
804             SharedPreferences.Editor editor = prefs.edit();
805             editor.remove(mContext.getPackageName());
806         }
807 
808         @Override
getIntent()809         protected Intent getIntent() {
810             return new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
811                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
812                     .putExtra(EXTRA_APP_PACKAGE, mContext.getPackageName());
813         }
814     }
815 
816     private class DataIntactTest extends InteractiveTestCase {
817         @Override
inflate(ViewGroup parent)818         protected View inflate(ViewGroup parent) {
819             return createAutoItem(parent, R.string.nls_payload_intact);
820         }
821 
822         @Override
setUp()823         protected void setUp() {
824             createChannels();
825             sendNotifications();
826             status = READY;
827         }
828 
829         @Override
test()830         protected void test() {
831             List<JSONObject> result = new ArrayList<>(MockListener.getInstance().getPosted());
832 
833             Set<String> found = new HashSet<String>();
834             if (result.size() == 0) {
835                 status = FAIL;
836                 return;
837             }
838             boolean pass = true;
839             for (JSONObject payload : result) {
840                 try {
841                     pass &=
842                             checkEquals(
843                                     mPackageString,
844                                     payload.getString(JSON_PACKAGE),
845                                     "data integrity test: notification package (%s, %s)");
846                     String tag = payload.getString(JSON_TAG);
847                     if (mTag1.equals(tag)) {
848                         found.add(mTag1);
849                         pass &=
850                                 checkEquals(
851                                         mIcon1,
852                                         payload.getInt(JSON_ICON),
853                                         "data integrity test: notification icon (%d, %d)");
854                         pass &=
855                                 checkFlagSet(
856                                         mFlag1,
857                                         payload.getInt(JSON_FLAGS),
858                                         "data integrity test: notification flags (%d, %d)");
859                         pass &=
860                                 checkEquals(
861                                         mId1,
862                                         payload.getInt(JSON_ID),
863                                         "data integrity test: notification ID (%d, %d)");
864                         pass &=
865                                 checkEquals(
866                                         mWhen1,
867                                         payload.getLong(JSON_WHEN),
868                                         "data integrity test: notification when (%d, %d)");
869                     } else if (mTag2.equals(tag)) {
870                         found.add(mTag2);
871                         pass &=
872                                 checkEquals(
873                                         mIcon2,
874                                         payload.getInt(JSON_ICON),
875                                         "data integrity test: notification icon (%d, %d)");
876                         pass &=
877                                 checkFlagSet(
878                                         mFlag2,
879                                         payload.getInt(JSON_FLAGS),
880                                         "data integrity test: notification flags (%d, %d)");
881                         pass &=
882                                 checkEquals(
883                                         mId2,
884                                         payload.getInt(JSON_ID),
885                                         "data integrity test: notification ID (%d, %d)");
886                         pass &=
887                                 checkEquals(
888                                         mWhen2,
889                                         payload.getLong(JSON_WHEN),
890                                         "data integrity test: notification when (%d, %d)");
891                     } else if (mTag3.equals(tag)) {
892                         found.add(mTag3);
893                         pass &=
894                                 checkEquals(
895                                         mIcon3,
896                                         payload.getInt(JSON_ICON),
897                                         "data integrity test: notification icon (%d, %d)");
898                         pass &=
899                                 checkFlagSet(
900                                         mFlag3,
901                                         payload.getInt(JSON_FLAGS),
902                                         "data integrity test: notification flags (%d, %d)");
903                         pass &=
904                                 checkEquals(
905                                         mId3,
906                                         payload.getInt(JSON_ID),
907                                         "data integrity test: notification ID (%d, %d)");
908                         pass &=
909                                 checkEquals(
910                                         mWhen3,
911                                         payload.getLong(JSON_WHEN),
912                                         "data integrity test: notification when (%d, %d)");
913                     }
914                 } catch (JSONException e) {
915                     pass = false;
916                     Log.e(TAG, "failed to unpack data from mocklistener", e);
917                 }
918             }
919 
920             pass &= found.size() >= 3;
921             status = pass ? PASS : FAIL;
922         }
923 
924         @Override
tearDown()925         protected void tearDown() {
926             mNm.cancelAll();
927             MockListener.getInstance().resetData();
928             deleteChannels();
929         }
930     }
931 
932     private class AudiblyAlertedTest extends InteractiveTestCase {
933         @Override
inflate(ViewGroup parent)934         protected View inflate(ViewGroup parent) {
935             return createAutoItem(parent, R.string.nls_audibly_alerted);
936         }
937 
938         @Override
setUp()939         protected void setUp() {
940             createChannels();
941             sendNotifications();
942             sendNoisyNotification();
943             status = READY;
944         }
945 
946         @Override
test()947         protected void test() {
948             List<JSONObject> result = new ArrayList<>(MockListener.getInstance().getPosted());
949 
950             Set<String> found = new HashSet<>();
951             if (result.size() == 0) {
952                 status = FAIL;
953                 return;
954             }
955             boolean pass = true;
956             for (JSONObject payload : result) {
957                 try {
958                     String tag = payload.getString(JSON_TAG);
959                     if (mTag4.equals(tag)) {
960                         found.add(mTag4);
961                         boolean lastAudiblyAlertedSet =
962                                 payload.getLong(JSON_LAST_AUDIBLY_ALERTED) > -1;
963                         if (!lastAudiblyAlertedSet) {
964                             logWithStack(
965                                     "noisy notification test: getLastAudiblyAlertedMillis not set");
966                         }
967                         pass &= lastAudiblyAlertedSet;
968                     } else if (payload.getString(JSON_PACKAGE).equals(mPackageString)) {
969                         found.add(tag);
970                         boolean lastAudiblyAlertedSet =
971                                 payload.getLong(JSON_LAST_AUDIBLY_ALERTED) > 0;
972                         if (lastAudiblyAlertedSet) {
973                             logWithStack(
974                                     "noisy notification test: getLastAudiblyAlertedMillis set "
975                                             + "incorrectly");
976                         }
977                         pass &= !lastAudiblyAlertedSet;
978                     }
979                 } catch (JSONException e) {
980                     pass = false;
981                     Log.e(TAG, "failed to unpack data from mocklistener", e);
982                 }
983             }
984 
985             pass &= found.size() >= 4;
986             status = pass ? PASS : FAIL;
987         }
988 
989         @Override
tearDown()990         protected void tearDown() {
991             mNm.cancelAll();
992             MockListener.getInstance().resetData();
993             deleteChannels();
994         }
995     }
996 
997     private class DismissOneTest extends InteractiveTestCase {
998         @Override
inflate(ViewGroup parent)999         protected View inflate(ViewGroup parent) {
1000             return createAutoItem(parent, R.string.nls_clear_one);
1001         }
1002 
1003         @Override
setUp()1004         protected void setUp() {
1005             createChannels();
1006             sendNotifications();
1007             status = READY;
1008         }
1009 
1010         @Override
test()1011         protected void test() {
1012             if (status == READY) {
1013                 MockListener.getInstance()
1014                         .cancelNotification(MockListener.getInstance().getKeyForTag(mTag1));
1015                 status = RETEST;
1016             } else {
1017                 List<String> result = new ArrayList<>(MockListener.getInstance().mRemoved);
1018                 if (result.size() != 0
1019                         && result.contains(mTag1)
1020                         && !result.contains(mTag2)
1021                         && !result.contains(mTag3)) {
1022                     status = PASS;
1023                 } else {
1024                     logFail();
1025                     status = FAIL;
1026                 }
1027             }
1028         }
1029 
1030         @Override
tearDown()1031         protected void tearDown() {
1032             mNm.cancelAll();
1033             deleteChannels();
1034             MockListener.getInstance().resetData();
1035         }
1036     }
1037 
1038     private class DismissOngoingTest extends InteractiveTestCase {
1039         @Override
inflate(ViewGroup parent)1040         protected View inflate(ViewGroup parent) {
1041             return createAutoItem(parent, R.string.nls_clear_ongoing);
1042         }
1043 
1044         @Override
setUp()1045         protected void setUp() {
1046             createChannels();
1047             sendNotifications();
1048             sendOngoingNotification();
1049             status = READY;
1050         }
1051 
1052         @Override
test()1053         protected void test() {
1054             if (status == READY) {
1055                 MockListener.getInstance()
1056                         .cancelNotification(MockListener.getInstance().getKeyForTag(mTag5));
1057                 status = RETEST;
1058             } else {
1059                 List<String> result = new ArrayList<>(MockListener.getInstance().mRemoved);
1060                 if (result.size() != 0
1061                         && !result.contains(mTag1)
1062                         && !result.contains(mTag2)
1063                         && !result.contains(mTag3)
1064                         && !result.contains(mTag5)) {
1065                     status = PASS;
1066                 } else {
1067                     logFail();
1068                     status = FAIL;
1069                 }
1070             }
1071         }
1072 
1073         @Override
tearDown()1074         protected void tearDown() {
1075             mNm.cancelAll();
1076             deleteChannels();
1077             MockListener.getInstance().resetData();
1078         }
1079     }
1080 
1081     private class DismissOneWithReasonTest extends InteractiveTestCase {
1082         int mRetries = 3;
1083 
1084         @Override
inflate(ViewGroup parent)1085         protected View inflate(ViewGroup parent) {
1086             return createAutoItem(parent, R.string.nls_clear_one_reason);
1087         }
1088 
1089         @Override
setUp()1090         protected void setUp() {
1091             createChannels();
1092             sendNotifications();
1093             status = READY;
1094         }
1095 
1096         @Override
test()1097         protected void test() {
1098             if (status == READY) {
1099                 MockListener.getInstance()
1100                         .cancelNotification(MockListener.getInstance().getKeyForTag(mTag1));
1101                 status = RETEST;
1102             } else {
1103                 List<JSONObject> result =
1104                         new ArrayList<>(MockListener.getInstance().mRemovedReason.values());
1105                 boolean pass = false;
1106                 for (JSONObject payload : result) {
1107                     try {
1108                         pass |=
1109                                 (checkEquals(
1110                                                 mTag1,
1111                                                 payload.getString(JSON_TAG),
1112                                                 "data dismissal test: notification tag (%s, %s)")
1113                                         && checkEquals(
1114                                                 REASON_LISTENER_CANCEL,
1115                                                 payload.getInt(JSON_REASON),
1116                                                 "data dismissal test: reason (%d, %d)"));
1117                         if (pass) {
1118                             break;
1119                         }
1120                     } catch (JSONException e) {
1121                         e.printStackTrace();
1122                     }
1123                 }
1124                 if (pass) {
1125                     status = PASS;
1126                 } else {
1127                     if (--mRetries > 0) {
1128                         sleep(100);
1129                         status = RETEST;
1130                     } else {
1131                         status = FAIL;
1132                     }
1133                 }
1134             }
1135         }
1136 
1137         @Override
tearDown()1138         protected void tearDown() {
1139             mNm.cancelAll();
1140             deleteChannels();
1141             MockListener.getInstance().resetData();
1142         }
1143     }
1144 
1145     private class DismissOneWithStatsTest extends InteractiveTestCase {
1146         int mRetries = 3;
1147 
1148         @Override
inflate(ViewGroup parent)1149         protected View inflate(ViewGroup parent) {
1150             return createAutoItem(parent, R.string.nls_clear_one_stats);
1151         }
1152 
1153         @Override
setUp()1154         protected void setUp() {
1155             createChannels();
1156             sendNotifications();
1157             status = READY;
1158         }
1159 
1160         @Override
test()1161         protected void test() {
1162             if (status == READY) {
1163                 MockListener.getInstance()
1164                         .cancelNotification(MockListener.getInstance().getKeyForTag(mTag1));
1165                 status = RETEST;
1166             } else {
1167                 List<JSONObject> result =
1168                         new ArrayList<>(MockListener.getInstance().mRemovedReason.values());
1169                 boolean pass = true;
1170                 for (JSONObject payload : result) {
1171                     try {
1172                         pass &= (payload.getBoolean(JSON_STATS) == false);
1173                     } catch (JSONException e) {
1174                         e.printStackTrace();
1175                         pass = false;
1176                     }
1177                 }
1178                 if (pass) {
1179                     status = PASS;
1180                 } else {
1181                     if (--mRetries > 0) {
1182                         sleep(100);
1183                         status = RETEST;
1184                     } else {
1185                         logFail("Notification listener got populated stats object.");
1186                         status = FAIL;
1187                     }
1188                 }
1189             }
1190         }
1191 
1192         @Override
tearDown()1193         protected void tearDown() {
1194             mNm.cancelAll();
1195             deleteChannels();
1196             MockListener.getInstance().resetData();
1197         }
1198     }
1199 
1200     private class DismissAllTest extends InteractiveTestCase {
1201         @Override
inflate(ViewGroup parent)1202         protected View inflate(ViewGroup parent) {
1203             return createAutoItem(parent, R.string.nls_clear_all);
1204         }
1205 
1206         @Override
setUp()1207         protected void setUp() {
1208             createChannels();
1209             sendNotifications();
1210             sendOngoingNotification();
1211             status = READY;
1212         }
1213 
1214         @Override
test()1215         protected void test() {
1216             if (status == READY) {
1217                 MockListener.getInstance().cancelAllNotifications();
1218                 status = RETEST;
1219             } else {
1220                 List<String> result = new ArrayList<>(MockListener.getInstance().mRemoved);
1221                 if (result.size() != 0
1222                         && result.contains(mTag1)
1223                         && result.contains(mTag2)
1224                         && result.contains(mTag3)
1225                         // NotificationListenerService cannot dismiss ongoing notifications
1226                         && !result.contains(mTag5)) {
1227                     status = PASS;
1228                 } else {
1229                     logFail();
1230                     status = FAIL;
1231                 }
1232             }
1233         }
1234 
1235         @Override
tearDown()1236         protected void tearDown() {
1237             mNm.cancelAll();
1238             deleteChannels();
1239             MockListener.getInstance().resetData();
1240         }
1241     }
1242 
1243     private class IsDisabledTest extends InteractiveTestCase {
1244         @Override
inflate(ViewGroup parent)1245         protected View inflate(ViewGroup parent) {
1246             return createNlsSettingsItem(parent, R.string.nls_disable_service);
1247         }
1248 
1249         @Override
autoStart()1250         boolean autoStart() {
1251             return true;
1252         }
1253 
1254         @Override
test()1255         protected void test() {
1256             String listeners =
1257                     Secure.getString(getContentResolver(), ENABLED_NOTIFICATION_LISTENERS);
1258             if (listeners == null || !listeners.contains(LISTENER_PATH)) {
1259                 status = PASS;
1260             } else {
1261                 status = WAIT_FOR_USER;
1262             }
1263         }
1264 
1265         @Override
tearDown()1266         protected void tearDown() {
1267             MockListener.getInstance().resetData();
1268         }
1269 
1270         @Override
getIntent()1271         protected Intent getIntent() {
1272             return new Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS);
1273         }
1274     }
1275 
1276     private class ServiceStoppedTest extends InteractiveTestCase {
1277         int mRetries = 3;
1278 
1279         @Override
inflate(ViewGroup parent)1280         protected View inflate(ViewGroup parent) {
1281             return createAutoItem(parent, R.string.nls_service_stopped);
1282         }
1283 
1284         @Override
test()1285         protected void test() {
1286             if (mNm.getEffectsSuppressor() == null
1287                     && (MockListener.getInstance() == null
1288                             || !MockListener.getInstance().isConnected)) {
1289                 status = PASS;
1290             } else {
1291                 if (--mRetries > 0) {
1292                     sleep(100);
1293                     status = RETEST;
1294                 } else {
1295                     status = FAIL;
1296                 }
1297             }
1298         }
1299 
1300         @Override
getIntent()1301         protected Intent getIntent() {
1302             return new Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS);
1303         }
1304     }
1305 
1306     private class NotificationNotReceivedTest extends InteractiveTestCase {
1307         @Override
inflate(ViewGroup parent)1308         protected View inflate(ViewGroup parent) {
1309             return createAutoItem(parent, R.string.nls_note_missed);
1310         }
1311 
1312         @Override
setUp()1313         protected void setUp() {
1314             createChannels();
1315             sendNotifications();
1316             status = READY;
1317         }
1318 
1319         @Override
test()1320         protected void test() {
1321             if (MockListener.getInstance() == null) {
1322                 status = PASS;
1323             } else {
1324                 if (MockListener.getInstance().mPosted.size() == 0) {
1325                     status = PASS;
1326                 } else {
1327                     logFail();
1328                     status = FAIL;
1329                 }
1330             }
1331             next();
1332         }
1333 
1334         @Override
tearDown()1335         protected void tearDown() {
1336             mNm.cancelAll();
1337             deleteChannels();
1338             if (MockListener.getInstance() != null) {
1339                 MockListener.getInstance().resetData();
1340             }
1341         }
1342     }
1343 
1344     private class RequestUnbindTest extends InteractiveTestCase {
1345         int mRetries = 5;
1346 
1347         @Override
inflate(ViewGroup parent)1348         protected View inflate(ViewGroup parent) {
1349             return createAutoItem(parent, R.string.nls_snooze);
1350         }
1351 
1352         @Override
setUp()1353         protected void setUp() {
1354             status = READY;
1355             MockListener.getInstance()
1356                     .requestListenerHints(MockListener.HINT_HOST_DISABLE_CALL_EFFECTS);
1357         }
1358 
1359         @Override
test()1360         protected void test() {
1361             if (status == READY) {
1362                 MockListener.getInstance().requestUnbind();
1363                 status = RETEST;
1364             } else {
1365                 if (mNm.getEffectsSuppressor() == null && !MockListener.getInstance().isConnected) {
1366                     status = PASS;
1367                 } else {
1368                     if (--mRetries > 0) {
1369                         status = RETEST;
1370                     } else {
1371                         logFail();
1372                         status = FAIL;
1373                     }
1374                 }
1375                 next();
1376             }
1377         }
1378     }
1379 
1380     private class RequestBindTest extends InteractiveTestCase {
1381         int mRetries = 5;
1382 
1383         @Override
inflate(ViewGroup parent)1384         protected View inflate(ViewGroup parent) {
1385             return createAutoItem(parent, R.string.nls_unsnooze);
1386         }
1387 
1388         @Override
test()1389         protected void test() {
1390             if (status == READY) {
1391                 MockListener.requestRebind(MockListener.COMPONENT_NAME);
1392                 status = RETEST;
1393             } else {
1394                 if (MockListener.getInstance().isConnected) {
1395                     status = PASS;
1396                     next();
1397                 } else {
1398                     if (--mRetries > 0) {
1399                         status = RETEST;
1400                         next();
1401                     } else {
1402                         logFail();
1403                         status = FAIL;
1404                     }
1405                 }
1406             }
1407         }
1408     }
1409 
1410     private class EnableHintsTest extends InteractiveTestCase {
1411         @Override
inflate(ViewGroup parent)1412         protected View inflate(ViewGroup parent) {
1413             return createAutoItem(parent, R.string.nls_hints);
1414         }
1415 
1416         @Override
test()1417         protected void test() {
1418             if (status == READY) {
1419                 MockListener.getInstance()
1420                         .requestListenerHints(MockListener.HINT_HOST_DISABLE_CALL_EFFECTS);
1421                 status = RETEST;
1422             } else {
1423                 int result = MockListener.getInstance().getCurrentListenerHints();
1424                 if ((result & MockListener.HINT_HOST_DISABLE_CALL_EFFECTS)
1425                         == MockListener.HINT_HOST_DISABLE_CALL_EFFECTS) {
1426                     status = PASS;
1427                     next();
1428                 } else {
1429                     logFail();
1430                     status = FAIL;
1431                 }
1432             }
1433         }
1434     }
1435 
1436     private class SnoozeNotificationForTimeTest extends InteractiveTestCase {
1437         static final int READY_TO_SNOOZE = 0;
1438         static final int SNOOZED = 1;
1439         static final int READY_TO_CHECK_FOR_UNSNOOZE = 2;
1440         int state = -1;
1441         long snoozeTime = 3000;
1442 
1443         @Override
inflate(ViewGroup parent)1444         protected View inflate(ViewGroup parent) {
1445             return createAutoItem(parent, R.string.nls_snooze_one_time);
1446         }
1447 
1448         @Override
setUp()1449         protected void setUp() {
1450             createChannels();
1451             sendNotifications();
1452             status = READY;
1453             state = READY_TO_SNOOZE;
1454             delay();
1455         }
1456 
1457         @Override
test()1458         protected void test() {
1459             status = RETEST;
1460             if (state == READY_TO_SNOOZE) {
1461                 MockListener.getInstance()
1462                         .snoozeNotification(
1463                                 MockListener.getInstance().getKeyForTag(mTag1), snoozeTime);
1464                 state = SNOOZED;
1465             } else if (state == SNOOZED) {
1466                 List<JSONObject> result =
1467                         new ArrayList<>(MockListener.getInstance().mRemovedReason.values());
1468                 boolean pass = false;
1469                 for (JSONObject payload : result) {
1470                     try {
1471                         pass |=
1472                                 (checkEquals(
1473                                                 mTag1,
1474                                                 payload.getString(JSON_TAG),
1475                                                 "data dismissal test: notification tag (%s, %s)")
1476                                         && checkEquals(
1477                                                 MockListener.REASON_SNOOZED,
1478                                                 payload.getInt(JSON_REASON),
1479                                                 "data dismissal test: reason (%d, %d)"));
1480                         if (pass) {
1481                             break;
1482                         }
1483                     } catch (JSONException e) {
1484                         e.printStackTrace();
1485                     }
1486                 }
1487                 if (!pass) {
1488                     logFail();
1489                     status = FAIL;
1490                     next();
1491                     return;
1492                 } else {
1493                     state = READY_TO_CHECK_FOR_UNSNOOZE;
1494                 }
1495             } else {
1496                 if (MockListener.getInstance().getPosted(mTag1) != null) {
1497                     status = PASS;
1498                 } else {
1499                     logFail();
1500                     status = FAIL;
1501                 }
1502             }
1503         }
1504 
1505         @Override
tearDown()1506         protected void tearDown() {
1507             mNm.cancelAll();
1508             deleteChannels();
1509             MockListener.getInstance().resetData();
1510             delay();
1511         }
1512     }
1513 
1514     /**
1515      * Posts notifications, snoozes one of them. Verifies that it is snoozed. Cancels all
1516      * notifications and reposts them. Confirms that the original notification is still snoozed.
1517      */
1518     private class SnoozeNotificationForTimeCancelTest extends InteractiveTestCase {
1519 
1520         static final int READY_TO_SNOOZE = 0;
1521         static final int SNOOZED = 1;
1522         static final int READY_TO_CHECK_FOR_SNOOZE = 2;
1523         int state = -1;
1524         long snoozeTime = 10000;
1525         private String tag;
1526 
1527         @Override
inflate(ViewGroup parent)1528         protected View inflate(ViewGroup parent) {
1529             return createAutoItem(parent, R.string.nls_snooze_one_time);
1530         }
1531 
1532         @Override
setUp()1533         protected void setUp() {
1534             createChannels();
1535             sendNotifications();
1536             tag = mTag1;
1537             status = READY;
1538             state = READY_TO_SNOOZE;
1539             delay();
1540         }
1541 
1542         @Override
test()1543         protected void test() {
1544             status = RETEST;
1545             if (state == READY_TO_SNOOZE) {
1546                 MockListener.getInstance()
1547                         .snoozeNotification(
1548                                 MockListener.getInstance().getKeyForTag(tag), snoozeTime);
1549                 state = SNOOZED;
1550             } else if (state == SNOOZED) {
1551                 List<String> result = getSnoozed();
1552                 if (result.size() >= 1 && result.contains(tag)) {
1553                     // cancel and repost
1554                     sendNotifications();
1555                     state = READY_TO_CHECK_FOR_SNOOZE;
1556                 } else {
1557                     logFail();
1558                     status = FAIL;
1559                 }
1560             } else {
1561                 List<String> result = getSnoozed();
1562                 if (result.size() >= 1 && result.contains(tag)) {
1563                     status = PASS;
1564                 } else {
1565                     logFail();
1566                     status = FAIL;
1567                 }
1568             }
1569         }
1570 
getSnoozed()1571         private List<String> getSnoozed() {
1572             List<String> result = new ArrayList<>();
1573             StatusBarNotification[] snoozed = MockListener.getInstance().getSnoozedNotifications();
1574             for (StatusBarNotification sbn : snoozed) {
1575                 result.add(sbn.getTag());
1576             }
1577             return result;
1578         }
1579 
1580         @Override
tearDown()1581         protected void tearDown() {
1582             mNm.cancelAll();
1583             deleteChannels();
1584             MockListener.getInstance().resetData();
1585         }
1586     }
1587 
1588     private class GetSnoozedNotificationTest extends InteractiveTestCase {
1589         static final int READY_TO_SNOOZE = 0;
1590         static final int SNOOZED = 1;
1591         static final int READY_TO_CHECK_FOR_GET_SNOOZE = 2;
1592         int state = -1;
1593         long snoozeTime = 30000;
1594 
1595         @Override
inflate(ViewGroup parent)1596         protected View inflate(ViewGroup parent) {
1597             return createAutoItem(parent, R.string.nls_get_snoozed);
1598         }
1599 
1600         @Override
setUp()1601         protected void setUp() {
1602             createChannels();
1603             sendNotifications();
1604             status = READY;
1605             state = READY_TO_SNOOZE;
1606         }
1607 
1608         @Override
test()1609         protected void test() {
1610             status = RETEST;
1611             if (state == READY_TO_SNOOZE) {
1612                 MockListener.getInstance()
1613                         .snoozeNotification(
1614                                 MockListener.getInstance().getKeyForTag(mTag1), snoozeTime);
1615                 MockListener.getInstance()
1616                         .snoozeNotification(
1617                                 MockListener.getInstance().getKeyForTag(mTag2), snoozeTime);
1618                 state = SNOOZED;
1619             } else if (state == SNOOZED) {
1620                 List<JSONObject> result =
1621                         new ArrayList<>(MockListener.getInstance().mRemovedReason.values());
1622                 if (result.size() == 0) {
1623                     status = FAIL;
1624                     return;
1625                 }
1626                 boolean pass = false;
1627                 for (JSONObject payload : result) {
1628                     try {
1629                         pass |=
1630                                 (checkEquals(
1631                                                 mTag1,
1632                                                 payload.getString(JSON_TAG),
1633                                                 "data dismissal test: notification tag (%s, %s)")
1634                                         && checkEquals(
1635                                                 MockListener.REASON_SNOOZED,
1636                                                 payload.getInt(JSON_REASON),
1637                                                 "data dismissal test: reason (%d, %d)"));
1638                         if (pass) {
1639                             break;
1640                         }
1641                     } catch (JSONException e) {
1642                         e.printStackTrace();
1643                     }
1644                 }
1645                 if (!pass) {
1646                     logFail();
1647                     status = FAIL;
1648                 } else {
1649                     state = READY_TO_CHECK_FOR_GET_SNOOZE;
1650                 }
1651             } else {
1652                 List<String> result = new ArrayList<>();
1653                 StatusBarNotification[] snoozed =
1654                         MockListener.getInstance().getSnoozedNotifications();
1655                 for (StatusBarNotification sbn : snoozed) {
1656                     result.add(sbn.getTag());
1657                 }
1658                 if (result.size() >= 2 && result.contains(mTag1) && result.contains(mTag2)) {
1659                     status = PASS;
1660                 } else {
1661                     logFail();
1662                     status = FAIL;
1663                 }
1664             }
1665         }
1666 
1667         @Override
tearDown()1668         protected void tearDown() {
1669             mNm.cancelAll();
1670             deleteChannels();
1671             MockListener.getInstance().resetData();
1672             delay();
1673         }
1674     }
1675 
1676     /** Tests that the extras {@link Bundle} in a MessagingStyle#Message is preserved. */
1677     private class MessageBundleTest extends InteractiveTestCase {
1678         private final String extrasKey1 = "extras_key_1";
1679         private final CharSequence extrasValue1 = "extras_value_1";
1680         private final String extrasKey2 = "extras_key_2";
1681         private final CharSequence extrasValue2 = "extras_value_2";
1682 
1683         @Override
inflate(ViewGroup parent)1684         protected View inflate(ViewGroup parent) {
1685             return createAutoItem(parent, R.string.msg_extras_preserved);
1686         }
1687 
1688         @Override
setUp()1689         protected void setUp() {
1690             createChannels();
1691             sendMessagingNotification();
1692             status = READY;
1693         }
1694 
1695         @Override
tearDown()1696         protected void tearDown() {
1697             mNm.cancelAll();
1698             deleteChannels();
1699             delay();
1700         }
1701 
sendMessagingNotification()1702         private void sendMessagingNotification() {
1703             mTag1 = UUID.randomUUID().toString();
1704             mNm.cancelAll();
1705             mWhen1 = System.currentTimeMillis() + 1;
1706             mIcon1 = R.drawable.ic_stat_alice;
1707             mId1 = NOTIFICATION_ID + 1;
1708 
1709             Notification.MessagingStyle.Message msg1 =
1710                     new Notification.MessagingStyle.Message("text 1", 0 /* timestamp */,
1711                             "sender 1");
1712             msg1.getExtras().putCharSequence(extrasKey1, extrasValue1);
1713 
1714             Notification.MessagingStyle.Message msg2 =
1715                     new Notification.MessagingStyle.Message("text 2", 1 /* timestamp */,
1716                             "sender 2");
1717             msg2.getExtras().putCharSequence(extrasKey2, extrasValue2);
1718 
1719             Notification.MessagingStyle style = new Notification.MessagingStyle("display_name");
1720             style.addMessage(msg1);
1721             style.addMessage(msg2);
1722 
1723             Notification n1 =
1724                     new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
1725                             .setContentTitle("ClearTest 1")
1726                             .setContentText("Content Title")
1727                             .setPriority(Notification.PRIORITY_LOW)
1728                             .setSmallIcon(mIcon1)
1729                             .setWhen(mWhen1)
1730                             .setDeleteIntent(makeIntent(1, mTag1))
1731                             .setOnlyAlertOnce(true)
1732                             .setStyle(style)
1733                             .build();
1734             mNm.notify(mTag1, mId1, n1);
1735             mFlag1 = Notification.FLAG_ONLY_ALERT_ONCE;
1736         }
1737 
1738         // Returns true on success.
verifyMessage( NotificationCompat.MessagingStyle.Message message, String extrasKey, CharSequence extrasValue)1739         private boolean verifyMessage(
1740                 NotificationCompat.MessagingStyle.Message message,
1741                 String extrasKey,
1742                 CharSequence extrasValue) {
1743             return message.getExtras() != null
1744                     && message.getExtras().getCharSequence(extrasKey) != null
1745                     && message.getExtras().getCharSequence(extrasKey).equals(extrasValue);
1746         }
1747 
1748         @Override
test()1749         protected void test() {
1750             List<Notification> result =
1751                     new ArrayList<>(MockListener.getInstance().mPostedNotifications);
1752             if (result.size() != 1 || result.get(0) == null) {
1753                 logFail();
1754                 status = FAIL;
1755                 next();
1756                 return;
1757             }
1758             // Can only read in MessagingStyle using the compat class.
1759             NotificationCompat.MessagingStyle readStyle =
1760                     NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
1761                             result.get(0));
1762             if (readStyle == null || readStyle.getMessages().size() != 2) {
1763                 status = FAIL;
1764                 logFail();
1765                 next();
1766                 return;
1767             }
1768 
1769             if (!verifyMessage(readStyle.getMessages().get(0), extrasKey1, extrasValue1)
1770                     || !verifyMessage(readStyle.getMessages().get(1), extrasKey2, extrasValue2)) {
1771                 status = FAIL;
1772                 logFail();
1773                 next();
1774                 return;
1775             }
1776 
1777             status = PASS;
1778         }
1779     }
1780 
1781     /**
1782      * Tests that conversation notifications appear at the top of the shade
1783      */
1784     private class ConversationOrderingTest extends InteractiveTestCase {
1785         private static final String SHARE_SHORTCUT_ID = "shareShortcut";
1786         private static final String SHORTCUT_CATEGORY =
1787                 "com.android.cts.verifier.notifications.SHORTCUT_CATEGORY";
1788 
1789         @Override
setUp()1790         protected void setUp() {
1791             createChannels();
1792             createDynamicShortcut();
1793             sendNotifications();
1794             status = READY;
1795         }
1796 
1797         @Override
tearDown()1798         protected void tearDown() {
1799             mNm.cancelAll();
1800             deleteChannels();
1801             delay();
1802         }
1803 
1804         @Override
inflate(ViewGroup parent)1805         protected View inflate(ViewGroup parent) {
1806             return createPassFailItem(parent, R.string.conversation_section_ordering);
1807         }
1808 
createDynamicShortcut()1809         private void createDynamicShortcut() {
1810             Person person =
1811                     new Person.Builder()
1812                             .setBot(false)
1813                             .setIcon(Icon.createWithResource(mContext, R.drawable.ic_stat_alice))
1814                             .setName("Person A")
1815                             .setImportant(true)
1816                             .build();
1817 
1818             Set<String> categorySet = new ArraySet<>();
1819             categorySet.add(SHORTCUT_CATEGORY);
1820             Intent shortcutIntent = new Intent(mContext, BubbleActivity.class);
1821             shortcutIntent.setAction(Intent.ACTION_VIEW);
1822 
1823             ShortcutInfo shortcut =
1824                     new ShortcutInfo.Builder(mContext, SHARE_SHORTCUT_ID)
1825                             .setShortLabel(SHARE_SHORTCUT_ID)
1826                             .setIcon(Icon.createWithResource(mContext, R.drawable.ic_stat_alice))
1827                             .setIntent(shortcutIntent)
1828                             .setPerson(person)
1829                             .setCategories(categorySet)
1830                             .setLongLived(true)
1831                             .build();
1832 
1833             ShortcutManager scManager =
1834                     (ShortcutManager) mContext.getSystemService(Context.SHORTCUT_SERVICE);
1835             scManager.addDynamicShortcuts(Arrays.asList(shortcut));
1836         }
1837 
sendNotifications()1838         private void sendNotifications() {
1839             mTag1 = UUID.randomUUID().toString();
1840             mId1 = NOTIFICATION_ID + 1;
1841 
1842             Person person = new Person.Builder().setName("Person A").build();
1843 
1844             Notification n1 =
1845                     new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
1846                             .setContentTitle("ConversationOrderingTest")
1847                             .setContentText(mTag1)
1848                             .setSmallIcon(R.drawable.ic_stat_alice)
1849                             .setGroup("conversations")
1850                             .setShortcutId(SHARE_SHORTCUT_ID)
1851                             .setStyle(
1852                                     new Notification.MessagingStyle(person)
1853                                             .setConversationTitle("Bubble Chat")
1854                                             .addMessage(
1855                                                     "Hello?",
1856                                                     SystemClock.currentThreadTimeMillis() - 300000,
1857                                                     person)
1858                                             .addMessage(
1859                                                     "Is it me you're looking for?",
1860                                                     SystemClock.currentThreadTimeMillis(),
1861                                                     person))
1862                             .build();
1863             mNm.notify(mTag1, mId1, n1);
1864 
1865             mTag2 = UUID.randomUUID().toString();
1866             mId2 = mId1 + 1;
1867             Notification n2 =
1868                     new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
1869                             .setContentTitle("Non-Person Notification")
1870                             .setContentText(mTag1)
1871                             .setSmallIcon(R.drawable.ic_stat_alice)
1872                             .setGroup("non-conversation")
1873                             .build();
1874             mNm.notify(mTag2, mId2, n2);
1875         }
1876 
1877         @Override
autoStart()1878         boolean autoStart() {
1879             return true;
1880         }
1881 
1882         @Override
test()1883         protected void test() {
1884             status = WAIT_FOR_USER;
1885             next();
1886         }
1887     }
1888 
1889     /**
1890      * Tests that heads-up notifications appear with the view, resources, and actions provided in
1891      * Notification.Builder.
1892      */
1893     private class HunDisplayTest extends InteractiveTestCase {
1894 
1895         @Override
setUp()1896         protected void setUp() {
1897             createChannels();
1898             sendNotifications();
1899             status = READY;
1900         }
1901 
1902         @Override
tearDown()1903         protected void tearDown() {
1904             mNm.cancelAll();
1905             deleteChannels();
1906             delay();
1907         }
1908 
1909         @Override
inflate(ViewGroup parent)1910         protected View inflate(ViewGroup parent) {
1911             return createPassFailItem(parent, R.string.hun_display);
1912         }
1913 
sendNotifications()1914         private void sendNotifications() {
1915             mTag1 = UUID.randomUUID().toString();
1916             mId1 = NOTIFICATION_ID + 1;
1917 
1918             Notification n1 =
1919                     new Notification.Builder(mContext, NOISY_NOTIFICATION_CHANNEL_ID)
1920                             .setContentTitle("HunDisplayTest")
1921                             .setContentText(mTag1)
1922                             .setSmallIcon(R.drawable.ic_stat_alice)
1923                             .setLargeIcon(
1924                                     Icon.createWithResource(
1925                                             mContext, R.drawable.test_pass_gradient))
1926                             .addAction(generateAction(1))
1927                             .addAction(generateAction(2))
1928                             .build();
1929             mNm.notify(mTag1, mId1, n1);
1930         }
1931 
generateAction(int num)1932         private Notification.Action generateAction(int num) {
1933             PendingIntent pi =
1934                     PendingIntent.getActivity(
1935                             mContext,
1936                             num,
1937                             new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS),
1938                             PendingIntent.FLAG_IMMUTABLE);
1939             return new Notification.Action.Builder(
1940                             Icon.createWithResource(mContext, R.drawable.ic_android),
1941                             mContext.getString(R.string.action, num),
1942                             pi)
1943                     .build();
1944         }
1945 
1946         @Override
autoStart()1947         boolean autoStart() {
1948             return true;
1949         }
1950 
1951         @Override
test()1952         protected void test() {
1953             status = WAIT_FOR_USER;
1954             next();
1955         }
1956     }
1957 
1958     /**
1959      * Sends the user to settings filter out silent notifications for this notification listener.
1960      * Sends silent and not silent notifs and makes sure only the non silent is received
1961      */
1962     private class NotificationTypeFilterTest extends InteractiveTestCase {
1963         int mRetries = 3;
1964 
1965         @Override
inflate(ViewGroup parent)1966         protected View inflate(ViewGroup parent) {
1967             return createAutoItem(parent, R.string.nls_filter_test);
1968         }
1969 
1970         @Override
setUp()1971         protected void setUp() {
1972             createChannels();
1973             sendNotifications();
1974             sendNoisyNotification();
1975             status = READY;
1976         }
1977 
1978         @Override
tearDown()1979         protected void tearDown() {
1980             mNm.cancelAll();
1981             MockListener.getInstance().resetData();
1982             deleteChannels();
1983         }
1984 
1985         @Override
test()1986         protected void test() {
1987             if (MockListener.getInstance().getPosted(mTag4) == null) {
1988                 Log.d(TAG, "Could not find " + mTag4);
1989                 if (--mRetries > 0) {
1990                     sleep(100);
1991                     status = RETEST;
1992                 } else {
1993                     status = FAIL;
1994                 }
1995             } else if (MockListener.getInstance().getPosted(mTag2) != null) {
1996                 logFail("Found" + mTag2);
1997                 status = FAIL;
1998             } else {
1999                 status = PASS;
2000             }
2001         }
2002     }
2003 
2004     /**
2005      * Sends an alerting notification and makes sure this listener cannot see it.
2006      */
2007     private class NotificationAppFilterTest extends InteractiveTestCase {
2008         int mRetries = 3;
2009 
2010         @Override
inflate(ViewGroup parent)2011         protected View inflate(ViewGroup parent) {
2012             return createAutoItem(parent, R.string.nls_confirm_notification_not_visible);
2013         }
2014 
2015         @Override
setUp()2016         protected void setUp() {
2017             createChannels();
2018             sendNoisyNotification();
2019             status = READY;
2020         }
2021 
2022         @Override
tearDown()2023         protected void tearDown() {
2024             mNm.cancelAll();
2025             MockListener.getInstance().resetData();
2026             deleteChannels();
2027         }
2028 
2029         @Override
test()2030         protected void test() {
2031             if (MockListener.getInstance().getPosted(mTag4) != null) {
2032                 logFail("Found" + mTag4);
2033                 status = FAIL;
2034             } else {
2035                 status = PASS;
2036             }
2037         }
2038     }
2039 
2040     protected class SendUserToChangeFilter extends InteractiveTestCase {
2041         @Override
inflate(ViewGroup parent)2042         protected View inflate(ViewGroup parent) {
2043             return createUserItem(
2044                     parent, R.string.cp_start_settings, R.string.nls_change_type_filter);
2045         }
2046 
2047         @Override
setUp()2048         protected void setUp() {
2049             // note: it's expected that the '0' type will be ignored since we've specified a
2050             // type in the manifest
2051             ArrayList<String> pkgs = new ArrayList<>();
2052             pkgs.add("com.android.settings");
2053             MockListener.getInstance().migrateNotificationFilter(0, pkgs);
2054             status = READY;
2055         }
2056 
2057         @Override
autoStart()2058         boolean autoStart() {
2059             return true;
2060         }
2061 
2062         @Override
test()2063         protected void test() {
2064             if (getIntent().resolveActivity(mPackageManager) == null) {
2065                 logFail("no settings activity");
2066                 status = FAIL;
2067             } else {
2068                 if (buttonPressed) {
2069                     status = PASS;
2070                 } else {
2071                     status = RETEST_AFTER_LONG_DELAY;
2072                 }
2073                 next();
2074             }
2075         }
2076 
tearDown()2077         protected void tearDown() {
2078             // wait for the service to start
2079             delay();
2080         }
2081 
2082         @Override
getIntent()2083         protected Intent getIntent() {
2084             Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS);
2085             intent.putExtra(
2086                     Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME,
2087                     MockListener.COMPONENT_NAME.flattenToString());
2088             return intent;
2089         }
2090     }
2091 
2092     protected class ResetTypeFilterAddAppFilter extends SendUserToChangeFilter {
2093         @Override
inflate(ViewGroup parent)2094         protected View inflate(ViewGroup parent) {
2095             return createUserItem(
2096                     parent, R.string.cp_start_settings, R.string.nls_reset_type_add_app_filter);
2097         }
2098     }
2099 
2100     protected class ResetAppFilter extends SendUserToChangeFilter {
2101         @Override
inflate(ViewGroup parent)2102         protected View inflate(ViewGroup parent) {
2103             return createUserItem(
2104                     parent, R.string.cp_start_settings, R.string.nls_reset_app_filter);
2105         }
2106     }
2107 
2108     protected class AskIfFilterChanged extends InteractiveTestCase {
2109         @Override
inflate(ViewGroup parent)2110         protected View inflate(ViewGroup parent) {
2111             return createPassFailItem(parent, R.string.nls_original_filter_verification);
2112         }
2113 
2114         @Override
autoStart()2115         boolean autoStart() {
2116             return true;
2117         }
2118 
2119         @Override
test()2120         protected void test() {
2121             status = WAIT_FOR_USER;
2122             next();
2123         }
2124     }
2125 
2126     protected class AskIfReadyToProceed extends InteractiveTestCase {
2127         @Override
inflate(ViewGroup parent)2128         protected View inflate(ViewGroup parent) {
2129             return createPassFailItem(parent, R.string.nls_confirm_filter_selection);
2130         }
2131 
2132         @Override
autoStart()2133         boolean autoStart() {
2134             return true;
2135         }
2136 
2137         @Override
test()2138         protected void test() {
2139             status = WAIT_FOR_USER;
2140             next();
2141         }
2142     }
2143 }
2144