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