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