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.provider.Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS;
20 import static android.provider.Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME;
21 
22 import android.app.NotificationManager;
23 import android.app.PendingIntent;
24 import android.app.Service;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.PackageManager;
29 import android.os.Bundle;
30 import android.os.IBinder;
31 import android.os.Parcelable;
32 import android.provider.Settings.Secure;
33 import android.util.Log;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.Button;
38 import android.widget.ImageView;
39 import android.widget.TextView;
40 
41 import com.android.cts.verifier.PassFailButtons;
42 import com.android.cts.verifier.R;
43 
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Iterator;
47 import java.util.List;
48 import java.util.Objects;
49 import java.util.concurrent.LinkedBlockingQueue;
50 
51 public abstract class InteractiveVerifierActivity extends PassFailButtons.Activity
52         implements Runnable {
53     private static final String TAG = "InteractiveVerifier";
54     private static final String STATE = "state";
55     private static final String STATUS = "status";
56     private static LinkedBlockingQueue<String> sDeletedQueue = new LinkedBlockingQueue<String>();
57     protected static final String LISTENER_PATH = "com.android.cts.verifier/" +
58             "com.android.cts.verifier.notifications.MockListener";
59     protected static final int SETUP = 0;
60     protected static final int READY = 1;
61     protected static final int RETEST = 2;
62     protected static final int PASS = 3;
63     protected static final int FAIL = 4;
64     protected static final int WAIT_FOR_USER = 5;
65     protected static final int RETEST_AFTER_LONG_DELAY = 6;
66     protected static final int READY_AFTER_LONG_DELAY = 7;
67 
68     protected static final int NOTIFICATION_ID = 1001;
69 
70     // TODO remove these once b/10023397 is fixed
71     public static final String ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners";
72 
73     protected InteractiveTestCase mCurrentTest;
74     protected PackageManager mPackageManager;
75     protected NotificationManager mNm;
76     protected Context mContext;
77     protected Runnable mRunner;
78     protected View mHandler;
79     protected String mPackageString;
80 
81     private LayoutInflater mInflater;
82     private ViewGroup mItemList;
83     private List<InteractiveTestCase> mTestList;
84     private Iterator<InteractiveTestCase> mTestOrder;
85 
86     public static class DismissService extends Service {
87         @Override
onBind(Intent intent)88         public IBinder onBind(Intent intent) {
89             return null;
90         }
91 
92         @Override
onStart(Intent intent, int startId)93         public void onStart(Intent intent, int startId) {
94             if(intent != null) { sDeletedQueue.offer(intent.getAction()); }
95         }
96     }
97 
98     protected abstract class InteractiveTestCase {
99         protected boolean mUserVerified;
100         protected int status;
101         private View view;
102         protected long delayTime = 3000;
103         boolean buttonPressed;
104 
inflate(ViewGroup parent)105         protected abstract View inflate(ViewGroup parent);
getView(ViewGroup parent)106         View getView(ViewGroup parent) {
107             if (view == null) {
108                 view = inflate(parent);
109             }
110             return view;
111         }
112 
113         /** @return true if the test should re-run when the test activity starts. */
autoStart()114         boolean autoStart() {
115             return false;
116         }
117 
118         /** Set status to {@link #READY} to proceed, or {@link #SETUP} to try again. */
setUp()119         protected void setUp() { status = READY; next(); };
120 
121         /** Set status to {@link #PASS} or @{link #FAIL} to proceed, or {@link #READY} to retry. */
test()122         protected void test() { status = FAIL; next(); };
123 
124         /** Do not modify status. */
tearDown()125         protected void tearDown() { next(); };
126 
setFailed()127         protected void setFailed() {
128             status = FAIL;
129             logFail();
130         }
131 
logFail()132         protected void logFail() {
133             logFail(null);
134         }
135 
logFail(String message)136         protected void logFail(String message) {
137             logWithStack("failed " + this.getClass().getSimpleName() +
138                     ((message == null) ? "" : ": " + message));
139         }
140 
logFail(String message, Throwable e)141         protected void logFail(String message, Throwable e) {
142             Log.e(TAG, "failed " + this.getClass().getSimpleName() +
143                     ((message == null) ? "" : ": " + message), e);
144         }
145 
146         // If this test contains a button that launches another activity, override this
147         // method to provide the intent to launch.
getIntent()148         protected Intent getIntent() {
149             return null;
150         }
151     }
152 
getTitleResource()153     protected abstract int getTitleResource();
getInstructionsResource()154     protected abstract int getInstructionsResource();
155 
onCreate(Bundle savedState)156     protected void onCreate(Bundle savedState) {
157         super.onCreate(savedState);
158         int savedStateIndex = (savedState == null) ? 0 : savedState.getInt(STATE, 0);
159         int savedStatus = (savedState == null) ? SETUP : savedState.getInt(STATUS, SETUP);
160         Log.i(TAG, "restored state(" + savedStateIndex + "}, status(" + savedStatus + ")");
161         mContext = this;
162         mRunner = this;
163         mNm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
164         mPackageManager = getPackageManager();
165         mInflater = getLayoutInflater();
166         View view = mInflater.inflate(R.layout.nls_main, null);
167         mItemList = (ViewGroup) view.findViewById(R.id.nls_test_items);
168         mHandler = mItemList;
169         mTestList = new ArrayList<>();
170         mTestList.addAll(createTestItems());
171         for (InteractiveTestCase test: mTestList) {
172             mItemList.addView(test.getView(mItemList));
173         }
174         mTestOrder = mTestList.iterator();
175         for (int i = 0; i < savedStateIndex; i++) {
176             mCurrentTest = mTestOrder.next();
177             mCurrentTest.status = PASS;
178         }
179         mCurrentTest = mTestOrder.next();
180         mCurrentTest.status = savedStatus;
181 
182         setContentView(view);
183         setPassFailButtonClickListeners();
184         getPassButton().setEnabled(false);
185 
186         setInfoResources(getTitleResource(), getInstructionsResource(), -1);
187     }
188 
189     @Override
onSaveInstanceState(Bundle outState)190     protected void onSaveInstanceState (Bundle outState) {
191         final int stateIndex = mTestList.indexOf(mCurrentTest);
192         outState.putInt(STATE, stateIndex);
193         final int status = mCurrentTest == null ? SETUP : mCurrentTest.status;
194         outState.putInt(STATUS, status);
195         Log.i(TAG, "saved state(" + stateIndex + "), status(" + status + ")");
196     }
197 
198     @Override
onResume()199     protected void onResume() {
200         super.onResume();
201         //To avoid NPE during onResume,before start to iterate next test order
202         if (mCurrentTest != null && mCurrentTest.status != SETUP && mCurrentTest.autoStart()) {
203             Log.i(TAG, "auto starting: " + mCurrentTest.getClass().getSimpleName());
204             mCurrentTest.status = READY;
205         }
206         next();
207     }
208 
209     // Interface Utilities
210 
setButtonsEnabled(View view, boolean enabled)211     protected final void setButtonsEnabled(View view, boolean enabled) {
212         if (view instanceof Button) {
213             view.setEnabled(enabled);
214         } else if (view instanceof ViewGroup) {
215             ViewGroup viewGroup = (ViewGroup) view;
216             for (int i = 0; i < viewGroup.getChildCount(); i++) {
217                 View child = viewGroup.getChildAt(i);
218                 setButtonsEnabled(child, enabled);
219             }
220         }
221     }
222 
markItem(InteractiveTestCase test)223     protected void markItem(InteractiveTestCase test) {
224         if (test == null) { return; }
225         View item = test.view;
226         ImageView status = item.findViewById(R.id.nls_status);
227         switch (test.status) {
228             case WAIT_FOR_USER:
229                 status.setImageResource(R.drawable.fs_warning);
230                 break;
231 
232             case SETUP:
233             case READY:
234             case RETEST:
235                 status.setImageResource(R.drawable.fs_clock);
236                 break;
237 
238             case FAIL:
239                 status.setImageResource(R.drawable.fs_error);
240                 setButtonsEnabled(test.view, false);
241                 break;
242 
243             case PASS:
244                 status.setImageResource(R.drawable.fs_good);
245                 setButtonsEnabled(test.view, false);
246                 break;
247 
248         }
249         status.invalidate();
250     }
251 
createNlsSettingsItem(ViewGroup parent, int messageId)252     protected View createNlsSettingsItem(ViewGroup parent, int messageId) {
253         return createUserItem(parent, R.string.nls_start_settings, messageId);
254     }
255 
createRetryItem(ViewGroup parent, int messageId, Object... messageFormatArgs)256     protected View createRetryItem(ViewGroup parent, int messageId, Object... messageFormatArgs) {
257         return createUserItem(parent, R.string.attention_ready, messageId, messageFormatArgs);
258     }
259 
createUserItem(ViewGroup parent, int actionId, int messageId, Object... messageFormatArgs)260     protected View createUserItem(ViewGroup parent, int actionId, int messageId,
261             Object... messageFormatArgs) {
262         View item = mInflater.inflate(R.layout.nls_item, parent, false);
263         TextView instructions = item.findViewById(R.id.nls_instructions);
264         instructions.setText(getString(messageId, messageFormatArgs));
265         Button button = item.findViewById(R.id.nls_action_button);
266         button.setText(actionId);
267         button.setTag(actionId);
268         return item;
269     }
270 
createAutoItem(ViewGroup parent, int stringId)271     protected ViewGroup createAutoItem(ViewGroup parent, int stringId) {
272         ViewGroup item = (ViewGroup) mInflater.inflate(R.layout.nls_item, parent, false);
273         TextView instructions = item.findViewById(R.id.nls_instructions);
274         instructions.setText(stringId);
275         View button = item.findViewById(R.id.nls_action_button);
276         button.setVisibility(View.GONE);
277         return item;
278     }
279 
createPassFailItem(ViewGroup parent, int stringId)280     protected View createPassFailItem(ViewGroup parent, int stringId) {
281         View item = mInflater.inflate(R.layout.iva_pass_fail_item, parent, false);
282         TextView instructions = item.findViewById(R.id.nls_instructions);
283         instructions.setText(stringId);
284         return item;
285     }
286 
createUserAndPassFailItem(ViewGroup parent, int actionId, int stringId)287     protected View createUserAndPassFailItem(ViewGroup parent, int actionId, int stringId) {
288         View item = mInflater.inflate(R.layout.iva_pass_fail_item, parent, false);
289         TextView instructions = item.findViewById(R.id.nls_instructions);
290         instructions.setText(stringId);
291         Button button = item.findViewById(R.id.nls_action_button);
292         button.setVisibility(View.VISIBLE);
293         button.setText(actionId);
294         button.setTag(actionId);
295         return item;
296     }
297 
298     // Test management
299 
createTestItems()300     abstract protected List<InteractiveTestCase> createTestItems();
301 
run()302     public void run() {
303         if (mCurrentTest == null) { return; }
304         markItem(mCurrentTest);
305         switch (mCurrentTest.status) {
306             case SETUP:
307                 Log.i(TAG, "running setup for: " + mCurrentTest.getClass().getSimpleName());
308                 mCurrentTest.setUp();
309                 if (mCurrentTest.status == READY_AFTER_LONG_DELAY) {
310                     delay(mCurrentTest.delayTime);
311                 } else {
312                     delay();
313                 }
314                 break;
315 
316             case WAIT_FOR_USER:
317                 Log.i(TAG, "waiting for user: " + mCurrentTest.getClass().getSimpleName());
318                 break;
319 
320             case READY_AFTER_LONG_DELAY:
321             case RETEST_AFTER_LONG_DELAY:
322             case READY:
323             case RETEST:
324                 Log.i(TAG, "running test for: " + mCurrentTest.getClass().getSimpleName());
325                 try {
326                     mCurrentTest.test();
327                     if (mCurrentTest.status == RETEST_AFTER_LONG_DELAY) {
328                         delay(mCurrentTest.delayTime);
329                     } else {
330                         delay();
331                     }
332                 } catch (Throwable t) {
333                     mCurrentTest.status = FAIL;
334                     markItem(mCurrentTest);
335                     Log.e(TAG, "FAIL: " + mCurrentTest.getClass().getSimpleName(), t);
336                     mCurrentTest.tearDown();
337                     mCurrentTest = null;
338                     delay();
339                 }
340 
341                 break;
342 
343             case FAIL:
344                 Log.i(TAG, "FAIL: " + mCurrentTest.getClass().getSimpleName());
345                 mCurrentTest.tearDown();
346                 mCurrentTest = null;
347                 delay();
348                 break;
349 
350             case PASS:
351                 Log.i(TAG, "pass for: " + mCurrentTest.getClass().getSimpleName());
352                 mCurrentTest.tearDown();
353                 if (mTestOrder.hasNext()) {
354                     mCurrentTest = mTestOrder.next();
355                     Log.i(TAG, "next test is: " + mCurrentTest.getClass().getSimpleName());
356                     next();
357                 } else {
358                     Log.i(TAG, "no more tests");
359                     mCurrentTest = null;
360                     getPassButton().setEnabled(true);
361                     mNm.cancelAll();
362                 }
363                 break;
364         }
365         markItem(mCurrentTest);
366     }
367 
368     /**
369      * Return to the state machine to progress through the tests.
370      */
next()371     protected void next() {
372         mHandler.removeCallbacks(mRunner);
373         mHandler.post(mRunner);
374     }
375 
376     /**
377      * Wait for things to settle before returning to the state machine.
378      */
delay()379     protected void delay() {
380         delay(3000);
381     }
382 
sleep(long time)383     protected void sleep(long time) {
384         try {
385             Thread.sleep(time);
386         } catch (InterruptedException e) {
387             e.printStackTrace();
388         }
389     }
390 
391     /**
392      * Wait for some time.
393      */
delay(long waitTime)394     protected void delay(long waitTime) {
395         mHandler.removeCallbacks(mRunner);
396         mHandler.postDelayed(mRunner, waitTime);
397     }
398 
399     // UI callbacks
400 
actionPressed(View v)401     public void actionPressed(View v) {
402         Object tag = v.getTag();
403         if (tag instanceof Integer) {
404             int id = ((Integer) tag).intValue();
405             if (mCurrentTest != null && mCurrentTest.getIntent() != null) {
406                 startActivity(mCurrentTest.getIntent());
407             } else if (id == R.string.attention_ready) {
408                 if (mCurrentTest != null) {
409                     mCurrentTest.status = READY;
410                     next();
411                 }
412             }
413             if (mCurrentTest != null) {
414                 mCurrentTest.mUserVerified = true;
415                 mCurrentTest.buttonPressed = true;
416             }
417         }
418     }
419 
actionPassed(View v)420     public void actionPassed(View v) {
421         if (mCurrentTest != null) {
422             mCurrentTest.mUserVerified = true;
423             mCurrentTest.status = PASS;
424             next();
425         }
426     }
427 
actionFailed(View v)428     public void actionFailed(View v) {
429         if (mCurrentTest != null) {
430             mCurrentTest.setFailed();
431         }
432     }
433 
434     // Utilities
435 
makeIntent(int code, String tag)436     protected PendingIntent makeIntent(int code, String tag) {
437         Intent intent = new Intent(tag);
438         intent.setComponent(new ComponentName(mContext, DismissService.class));
439         PendingIntent pi = PendingIntent.getService(mContext, code, intent,
440                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
441         return pi;
442     }
443 
makeBroadcastIntent(int code, String tag)444     protected PendingIntent makeBroadcastIntent(int code, String tag) {
445         Intent intent = new Intent(tag);
446         intent.setComponent(new ComponentName(mContext, ActionTriggeredReceiver.class));
447         PendingIntent pi = PendingIntent.getBroadcast(mContext, code, intent,
448                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
449         return pi;
450     }
451 
checkEquals(long[] expected, long[] actual, String message)452     protected boolean checkEquals(long[] expected, long[] actual, String message) {
453         if (Arrays.equals(expected, actual)) {
454             return true;
455         }
456         logWithStack(String.format(message, expected, actual));
457         return false;
458     }
459 
checkEquals(Object[] expected, Object[] actual, String message)460     protected boolean checkEquals(Object[] expected, Object[] actual, String message) {
461         if (Arrays.equals(expected, actual)) {
462             return true;
463         }
464         logWithStack(String.format(message, expected, actual));
465         return false;
466     }
467 
checkEquals(Parcelable expected, Parcelable actual, String message)468     protected boolean checkEquals(Parcelable expected, Parcelable actual, String message) {
469         if (Objects.equals(expected, actual)) {
470             return true;
471         }
472         logWithStack(String.format(message, expected, actual));
473         return false;
474     }
475 
checkEquals(boolean expected, boolean actual, String message)476     protected boolean checkEquals(boolean expected, boolean actual, String message) {
477         if (expected == actual) {
478             return true;
479         }
480         logWithStack(String.format(message, expected, actual));
481         return false;
482     }
483 
checkEquals(long expected, long actual, String message)484     protected boolean checkEquals(long expected, long actual, String message) {
485         if (expected == actual) {
486             return true;
487         }
488         logWithStack(String.format(message, expected, actual));
489         return false;
490     }
491 
checkEquals(CharSequence expected, CharSequence actual, String message)492     protected boolean checkEquals(CharSequence expected, CharSequence actual, String message) {
493         if (expected.equals(actual)) {
494             return true;
495         }
496         logWithStack(String.format(message, expected, actual));
497         return false;
498     }
499 
checkFlagSet(int expected, int actual, String message)500     protected boolean checkFlagSet(int expected, int actual, String message) {
501         if ((expected & actual) != 0) {
502             return true;
503         }
504         logWithStack(String.format(message, expected, actual));
505         return false;
506     };
507 
logWithStack(String message)508     protected void logWithStack(String message) {
509         Throwable stackTrace = new Throwable();
510         stackTrace.fillInStackTrace();
511         Log.e(TAG, message, stackTrace);
512     }
513 
514     // Common Tests: useful for the side-effects they generate
515 
516     protected class IsEnabledTest extends InteractiveTestCase {
517         @Override
inflate(ViewGroup parent)518         protected View inflate(ViewGroup parent) {
519             return createNlsSettingsItem(parent, R.string.nls_enable_service);
520         }
521 
522         @Override
autoStart()523         boolean autoStart() {
524             return true;
525         }
526 
527         @Override
test()528         protected void test() {
529             mNm.cancelAll();
530 
531             if (getIntent().resolveActivity(mPackageManager) == null) {
532                 logFail("no settings activity");
533                 status = FAIL;
534             } else {
535                 String listeners = Secure.getString(getContentResolver(),
536                         ENABLED_NOTIFICATION_LISTENERS);
537                 if (listeners != null && listeners.contains(LISTENER_PATH)) {
538                     status = PASS;
539                 } else {
540                     status = WAIT_FOR_USER;
541                 }
542                 next();
543             }
544         }
545 
546         @Override
tearDown()547         protected void tearDown() {
548             // wait for the service to start
549             delay();
550         }
551 
552         @Override
getIntent()553         protected Intent getIntent() {
554             Intent settings = new Intent(ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS);
555             settings.putExtra(EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME,
556                     MockListener.COMPONENT_NAME.flattenToString());
557             return settings;
558         }
559     }
560 
561     protected class ServiceStartedTest extends InteractiveTestCase {
562         @Override
inflate(ViewGroup parent)563         protected View inflate(ViewGroup parent) {
564             return createAutoItem(parent, R.string.nls_service_started);
565         }
566 
567         @Override
test()568         protected void test() {
569             if (MockListener.getInstance() != null && MockListener.getInstance().isConnected) {
570                 status = PASS;
571                 next();
572             } else {
573                 logFail();
574                 status = RETEST;
575                 delay();
576             }
577         }
578     }
579 }
580