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