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