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