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