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