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