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