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