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