1 /* 2 * Copyright (C) 2010 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; 18 19 import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode; 20 import static com.android.cts.verifier.TestListAdapter.setTestNameSuffix; 21 22 import android.app.ActionBar; 23 import android.app.AlertDialog; 24 import android.app.Dialog; 25 import android.content.ContentResolver; 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.DialogInterface.OnCancelListener; 30 import android.content.pm.PackageManager; 31 import android.database.Cursor; 32 import android.os.Bundle; 33 import android.os.PowerManager; 34 import android.os.PowerManager.WakeLock; 35 import android.view.LayoutInflater; 36 import android.view.MenuItem; 37 import android.view.View; 38 import android.view.View.OnClickListener; 39 import android.widget.ImageButton; 40 import android.widget.Toast; 41 42 import com.android.compatibility.common.util.ReportLog; 43 44 import java.util.List; 45 import java.util.stream.Collectors; 46 import java.util.stream.IntStream; 47 48 /** 49 * {@link Activity}s to handle clicks to the pass and fail buttons of the pass fail buttons layout. 50 * 51 * <ol> 52 * <li>Include the pass fail buttons layout in your layout: 53 * <pre><include layout="@layout/pass_fail_buttons" /></pre> 54 * </li> 55 * <li>Extend one of the activities and call setPassFailButtonClickListeners after 56 * setting your content view.</li> 57 * <li>Make sure to call setResult(RESULT_CANCEL) in your Activity initially.</li> 58 * <li>Optionally call setInfoTextResources to add an info button that will show a 59 * dialog with instructional text.</li> 60 * </ol> 61 */ 62 public class PassFailButtons { 63 private static final String TAG = PassFailButtons.class.getSimpleName(); 64 65 private static final int INFO_DIALOG_ID = 1337; 66 67 private static final String INFO_DIALOG_VIEW_ID = "infoDialogViewId"; 68 private static final String INFO_DIALOG_TITLE_ID = "infoDialogTitleId"; 69 private static final String INFO_DIALOG_MESSAGE_ID = "infoDialogMessageId"; 70 71 // ReportLog file for CTS-Verifier. The "stream" name gets mapped to the test class name. 72 public static final String GENERAL_TESTS_REPORT_LOG_NAME = "CtsVerifierGeneralTestCases"; 73 public static final String AUDIO_TESTS_REPORT_LOG_NAME = "CtsVerifierAudioTestCases"; 74 75 private static final String SECTION_UNDEFINED = "undefined_section_name"; 76 77 // Interface mostly for making documentation and refactoring easier... 78 public interface PassFailActivity { 79 80 /** 81 * Hooks up the pass and fail buttons to click listeners that will record the test results. 82 * <p> 83 * Call from {@link Activity#onCreate} after {@link Activity #setContentView(int)}. 84 */ setPassFailButtonClickListeners()85 void setPassFailButtonClickListeners(); 86 87 /** 88 * Adds an initial informational dialog that appears when entering the test activity for 89 * the first time. Also enables the visibility of an "Info" button between the "Pass" and 90 * "Fail" buttons that can be clicked to show the information dialog again. 91 * <p> 92 * Call from {@link Activity#onCreate} after {@link Activity #setContentView(int)}. 93 * 94 * @param titleId for the text shown in the dialog title area 95 * @param messageId for the text shown in the dialog's body area 96 */ setInfoResources(int titleId, int messageId, int viewId)97 void setInfoResources(int titleId, int messageId, int viewId); 98 getPassButton()99 View getPassButton(); 100 101 /** 102 * Returns a unique identifier for the test. Usually, this is just the class name. 103 */ getTestId()104 String getTestId(); 105 106 /** @return null or details about the test run. */ getTestDetails()107 String getTestDetails(); 108 109 /** 110 * Set the result of the test and finish the activity. 111 * 112 * @param passed Whether or not the test passed. 113 */ setTestResultAndFinish(boolean passed)114 void setTestResultAndFinish(boolean passed); 115 116 /** 117 * @return The name of the file to store the (suite of) ReportLog information. 118 */ getReportFileName()119 public String getReportFileName(); 120 121 /** 122 * @return A unique name to serve as a section header in the CtsVerifierReportLog file. 123 * Tests need to conform to the underscore_delineated_name standard for use with 124 * the protobuff/json ReportLog parsing in Google3 125 */ getReportSectionName()126 public String getReportSectionName(); 127 128 /** 129 * Test subclasses can override this to record their CtsVerifierReportLogs. 130 * This is called when the test is exited 131 */ recordTestResults()132 void recordTestResults(); 133 134 /** @return A {@link ReportLog} that is used to record test metric data. */ getReportLog()135 CtsVerifierReportLog getReportLog(); 136 137 /** 138 * @return A {@link TestResultHistoryCollection} that is used to record test execution time. 139 */ getHistoryCollection()140 TestResultHistoryCollection getHistoryCollection(); 141 } /* class PassFailButtons.PassFailActivity */ 142 143 public static class Activity extends android.app.Activity implements PassFailActivity { 144 private WakeLock mWakeLock; 145 private CtsVerifierReportLog mReportLog; 146 private final TestResultHistoryCollection mHistoryCollection; 147 148 protected boolean mRequireReportLogToPass; 149 Activity()150 public Activity() { 151 this.mHistoryCollection = new TestResultHistoryCollection(); 152 if (requiresReportLog()) { 153 // if the subclass reports a report filename, they need a ReportLog object 154 newReportLog(); 155 } 156 } 157 158 @Override onResume()159 protected void onResume() { 160 super.onResume(); 161 if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { 162 mWakeLock = ((PowerManager) getSystemService(Context.POWER_SERVICE)) 163 .newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "PassFailButtons"); 164 mWakeLock.acquire(); 165 } 166 167 if (mReportLog != null && !mReportLog.isOpen()) { 168 showReportLogWarningDialog(this); 169 } 170 } 171 172 @Override onPause()173 protected void onPause() { 174 super.onPause(); 175 if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { 176 mWakeLock.release(); 177 } 178 } 179 180 @Override setPassFailButtonClickListeners()181 public void setPassFailButtonClickListeners() { 182 setPassFailClickListeners(this); 183 } 184 185 @Override setInfoResources(int titleId, int messageId, int viewId)186 public void setInfoResources(int titleId, int messageId, int viewId) { 187 setInfo(this, titleId, messageId, viewId); 188 } 189 190 @Override getPassButton()191 public View getPassButton() { 192 return getPassButtonView(this); 193 } 194 195 @Override onCreateDialog(int id, Bundle args)196 public Dialog onCreateDialog(int id, Bundle args) { 197 return createDialog(this, id, args); 198 } 199 200 @Override getTestId()201 public String getTestId() { 202 return setTestNameSuffix(sCurrentDisplayMode, getClass().getName()); 203 } 204 205 @Override getTestDetails()206 public String getTestDetails() { 207 return null; 208 } 209 210 @Override setTestResultAndFinish(boolean passed)211 public void setTestResultAndFinish(boolean passed) { 212 PassFailButtons.setTestResultAndFinishHelper( 213 this, getTestId(), getTestDetails(), passed, getReportLog(), 214 getHistoryCollection()); 215 } 216 newReportLog()217 protected CtsVerifierReportLog newReportLog() { 218 return mReportLog = new CtsVerifierReportLog( 219 getReportFileName(), getReportSectionName()); 220 } 221 222 /** 223 * Specifies if the test module will write a ReportLog entry 224 * @return true if the test module will write a ReportLog entry 225 */ requiresReportLog()226 public boolean requiresReportLog() { 227 return false; 228 } 229 230 @Override getReportLog()231 public CtsVerifierReportLog getReportLog() { 232 return mReportLog; 233 } 234 235 /** 236 * A mechanism to block tests from passing if no ReportLog data has been collected. 237 * @return true if the ReportLog is open OR if the test does not require that. 238 */ isReportLogOkToPass()239 public boolean isReportLogOkToPass() { 240 return !mRequireReportLogToPass || (mReportLog != null & mReportLog.isOpen()); 241 } 242 243 /** 244 * @return The name of the file to store the (suite of) ReportLog information. 245 */ 246 @Override getReportFileName()247 public String getReportFileName() { return GENERAL_TESTS_REPORT_LOG_NAME; } 248 249 @Override getReportSectionName()250 public String getReportSectionName() { 251 return setTestNameSuffix(sCurrentDisplayMode, SECTION_UNDEFINED); 252 } 253 254 @Override getHistoryCollection()255 public TestResultHistoryCollection getHistoryCollection() { return mHistoryCollection; } 256 257 @Override onCreate(Bundle savedInstanceState)258 protected void onCreate(Bundle savedInstanceState) { 259 super.onCreate(savedInstanceState); 260 ActionBar actBar = getActionBar(); 261 if (actBar != null) { 262 actBar.setDisplayHomeAsUpEnabled(true); 263 } 264 } 265 266 @Override onOptionsItemSelected(MenuItem item)267 public boolean onOptionsItemSelected(MenuItem item) { 268 if (item.getItemId() == android.R.id.home) { 269 onBackPressed(); 270 return true; 271 } 272 return super.onOptionsItemSelected(item); 273 } 274 275 @Override recordTestResults()276 public void recordTestResults() { 277 // default - NOP 278 } 279 } /* class PassFailButtons.Activity */ 280 281 public static class ListActivity extends android.app.ListActivity implements PassFailActivity { 282 283 private final CtsVerifierReportLog mReportLog; 284 private final TestResultHistoryCollection mHistoryCollection; 285 ListActivity()286 public ListActivity() { 287 mHistoryCollection = new TestResultHistoryCollection(); 288 mReportLog = null; 289 } 290 291 @Override setPassFailButtonClickListeners()292 public void setPassFailButtonClickListeners() { 293 setPassFailClickListeners(this); 294 } 295 296 @Override setInfoResources(int titleId, int messageId, int viewId)297 public void setInfoResources(int titleId, int messageId, int viewId) { 298 setInfo(this, titleId, messageId, viewId); 299 } 300 301 @Override getPassButton()302 public View getPassButton() { 303 return getPassButtonView(this); 304 } 305 306 @Override onCreateDialog(int id, Bundle args)307 public Dialog onCreateDialog(int id, Bundle args) { 308 return createDialog(this, id, args); 309 } 310 311 @Override getTestId()312 public String getTestId() { 313 return setTestNameSuffix(sCurrentDisplayMode, getClass().getName()); 314 } 315 316 @Override getTestDetails()317 public String getTestDetails() { 318 return null; 319 } 320 321 @Override setTestResultAndFinish(boolean passed)322 public void setTestResultAndFinish(boolean passed) { 323 PassFailButtons.setTestResultAndFinishHelper( 324 this, getTestId(), getTestDetails(), passed, getReportLog(), 325 getHistoryCollection()); 326 } 327 328 @Override getReportLog()329 public CtsVerifierReportLog getReportLog() { 330 return mReportLog; 331 } 332 333 /** 334 * @return The name of the file to store the (suite of) ReportLog information. 335 */ 336 @Override getReportFileName()337 public String getReportFileName() { return GENERAL_TESTS_REPORT_LOG_NAME; } 338 339 @Override getReportSectionName()340 public String getReportSectionName() { 341 return setTestNameSuffix(sCurrentDisplayMode, SECTION_UNDEFINED); 342 } 343 344 @Override getHistoryCollection()345 public TestResultHistoryCollection getHistoryCollection() { return mHistoryCollection; } 346 347 @Override onCreate(Bundle savedInstanceState)348 protected void onCreate(Bundle savedInstanceState) { 349 super.onCreate(savedInstanceState); 350 ActionBar actBar = getActionBar(); 351 if (actBar != null) { 352 actBar.setDisplayHomeAsUpEnabled(true); 353 } 354 } 355 356 @Override onOptionsItemSelected(MenuItem item)357 public boolean onOptionsItemSelected(MenuItem item) { 358 if (item.getItemId() == android.R.id.home) { 359 onBackPressed(); 360 return true; 361 } 362 return super.onOptionsItemSelected(item); 363 } 364 365 @Override recordTestResults()366 public void recordTestResults() { 367 // default - NOP 368 } 369 } // class PassFailButtons.ListActivity 370 371 public static class TestListActivity extends AbstractTestListActivity 372 implements PassFailActivity { 373 374 private final CtsVerifierReportLog mReportLog; 375 TestListActivity()376 public TestListActivity() { 377 // TODO(b/186555602): temporary hack^H^H^H^H workaround to fix crash 378 // This DOES NOT in fact fix that bug. 379 // if (true) this.mReportLog = new CtsVerifierReportLog(b/186555602, "42"); else 380 381 this.mReportLog = new CtsVerifierReportLog(getReportFileName(), getReportSectionName()); 382 } 383 384 @Override setPassFailButtonClickListeners()385 public void setPassFailButtonClickListeners() { 386 setPassFailClickListeners(this); 387 } 388 389 @Override setInfoResources(int titleId, int messageId, int viewId)390 public void setInfoResources(int titleId, int messageId, int viewId) { 391 setInfo(this, titleId, messageId, viewId); 392 } 393 394 @Override getPassButton()395 public View getPassButton() { 396 return getPassButtonView(this); 397 } 398 399 @Override onCreateDialog(int id, Bundle args)400 public Dialog onCreateDialog(int id, Bundle args) { 401 return createDialog(this, id, args); 402 } 403 404 @Override getTestId()405 public String getTestId() { 406 return setTestNameSuffix(sCurrentDisplayMode, getClass().getName()); 407 } 408 409 @Override getTestDetails()410 public String getTestDetails() { 411 return null; 412 } 413 414 @Override setTestResultAndFinish(boolean passed)415 public void setTestResultAndFinish(boolean passed) { 416 PassFailButtons.setTestResultAndFinishHelper( 417 this, getTestId(), getTestDetails(), passed, getReportLog(), 418 getHistoryCollection()); 419 } 420 421 @Override getReportLog()422 public CtsVerifierReportLog getReportLog() { 423 return mReportLog; 424 } 425 426 /** 427 * @return The name of the file to store the (suite of) ReportLog information. 428 */ 429 @Override getReportFileName()430 public String getReportFileName() { return GENERAL_TESTS_REPORT_LOG_NAME; } 431 432 @Override getReportSectionName()433 public String getReportSectionName() { 434 return setTestNameSuffix(sCurrentDisplayMode, SECTION_UNDEFINED); 435 } 436 437 438 /** 439 * Get existing test history to aggregate. 440 */ 441 @Override getHistoryCollection()442 public TestResultHistoryCollection getHistoryCollection() { 443 List<TestResultHistoryCollection> histories = 444 IntStream.range(0, mAdapter.getCount()) 445 .mapToObj(mAdapter::getHistoryCollection) 446 .collect(Collectors.toList()); 447 TestResultHistoryCollection historyCollection = new TestResultHistoryCollection(); 448 historyCollection.merge(getTestId(), histories); 449 return historyCollection; 450 } 451 updatePassButton()452 public void updatePassButton() { 453 getPassButton().setEnabled(mAdapter.allTestsPassed()); 454 } 455 456 @Override onCreate(Bundle savedInstanceState)457 protected void onCreate(Bundle savedInstanceState) { 458 super.onCreate(savedInstanceState); 459 ActionBar actBar = getActionBar(); 460 if (actBar != null) { 461 actBar.setDisplayHomeAsUpEnabled(true); 462 } 463 } 464 465 @Override onOptionsItemSelected(MenuItem item)466 public boolean onOptionsItemSelected(MenuItem item) { 467 if (item.getItemId() == android.R.id.home) { 468 onBackPressed(); 469 return true; 470 } 471 return super.onOptionsItemSelected(item); 472 } 473 474 @Override recordTestResults()475 public void recordTestResults() { 476 // default - NOP 477 } 478 } // class PassFailButtons.TestListActivity 479 480 protected static <T extends android.app.Activity & PassFailActivity> setPassFailClickListeners(final T activity)481 void setPassFailClickListeners(final T activity) { 482 View.OnClickListener clickListener = new View.OnClickListener() { 483 @Override 484 public void onClick(View target) { 485 setTestResultAndFinish(activity, activity.getTestId(), activity.getTestDetails(), 486 activity.getReportLog(), activity.getHistoryCollection(), target); 487 } 488 }; 489 490 View passButton = activity.findViewById(R.id.pass_button); 491 passButton.setOnClickListener(clickListener); 492 passButton.setOnLongClickListener(new View.OnLongClickListener() { 493 @Override 494 public boolean onLongClick(View view) { 495 Toast.makeText(activity, R.string.pass_button_text, Toast.LENGTH_SHORT).show(); 496 return true; 497 } 498 }); 499 500 View failButton = activity.findViewById(R.id.fail_button); 501 failButton.setOnClickListener(clickListener); 502 failButton.setOnLongClickListener(new View.OnLongClickListener() { 503 @Override 504 public boolean onLongClick(View view) { 505 Toast.makeText(activity, R.string.fail_button_text, Toast.LENGTH_SHORT).show(); 506 return true; 507 } 508 }); 509 } // class PassFailButtons.<T extends android.app.Activity & PassFailActivity> 510 setInfo(final android.app.Activity activity, final int titleId, final int messageId, final int viewId)511 protected static void setInfo(final android.app.Activity activity, final int titleId, 512 final int messageId, final int viewId) { 513 // Show the middle "info" button and make it show the info dialog when clicked. 514 View infoButton = activity.findViewById(R.id.info_button); 515 infoButton.setVisibility(View.VISIBLE); 516 infoButton.setOnClickListener(new OnClickListener() { 517 @Override 518 public void onClick(View view) { 519 showInfoDialog(activity, titleId, messageId, viewId); 520 } 521 }); 522 infoButton.setOnLongClickListener(new View.OnLongClickListener() { 523 @Override 524 public boolean onLongClick(View view) { 525 Toast.makeText(activity, R.string.info_button_text, Toast.LENGTH_SHORT).show(); 526 return true; 527 } 528 }); 529 530 // Show the info dialog if the user has never seen it before. 531 if (!hasSeenInfoDialog(activity)) { 532 showInfoDialog(activity, titleId, messageId, viewId); 533 } 534 } 535 hasSeenInfoDialog(android.app.Activity activity)536 protected static boolean hasSeenInfoDialog(android.app.Activity activity) { 537 ContentResolver resolver = activity.getContentResolver(); 538 Cursor cursor = null; 539 try { 540 cursor = resolver.query(TestResultsProvider.getTestNameUri(activity), 541 new String[] {TestResultsProvider.COLUMN_TEST_INFO_SEEN}, null, null, null); 542 return cursor.moveToFirst() && cursor.getInt(0) > 0; 543 } finally { 544 if (cursor != null) { 545 cursor.close(); 546 } 547 } 548 } 549 showInfoDialog(final android.app.Activity activity, int titleId, int messageId, int viewId)550 protected static void showInfoDialog(final android.app.Activity activity, int titleId, 551 int messageId, int viewId) { 552 Bundle args = new Bundle(); 553 args.putInt(INFO_DIALOG_TITLE_ID, titleId); 554 args.putInt(INFO_DIALOG_MESSAGE_ID, messageId); 555 args.putInt(INFO_DIALOG_VIEW_ID, viewId); 556 activity.showDialog(INFO_DIALOG_ID, args); 557 } 558 showReportLogWarningDialog(final android.app.Activity activity)559 protected static void showReportLogWarningDialog(final android.app.Activity activity) { 560 showInfoDialog(activity, 561 R.string.reportlog_warning_title, R.string.reportlog_warning_body, -1); 562 } 563 564 createDialog(final android.app.Activity activity, int id, Bundle args)565 protected static Dialog createDialog(final android.app.Activity activity, int id, Bundle args) { 566 switch (id) { 567 case INFO_DIALOG_ID: 568 return createInfoDialog(activity, id, args); 569 default: 570 throw new IllegalArgumentException("Bad dialog id: " + id); 571 } 572 } 573 createInfoDialog(final android.app.Activity activity, int id, Bundle args)574 protected static Dialog createInfoDialog(final android.app.Activity activity, int id, 575 Bundle args) { 576 int viewId = args.getInt(INFO_DIALOG_VIEW_ID); 577 int titleId = args.getInt(INFO_DIALOG_TITLE_ID); 578 int messageId = args.getInt(INFO_DIALOG_MESSAGE_ID); 579 580 AlertDialog.Builder builder = new AlertDialog.Builder(activity).setIcon( 581 android.R.drawable.ic_dialog_info).setTitle(titleId); 582 if (viewId > 0) { 583 LayoutInflater inflater = (LayoutInflater) activity 584 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 585 builder.setView(inflater.inflate(viewId, null)); 586 } else { 587 builder.setMessage(messageId); 588 } 589 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 590 @Override 591 public void onClick(DialogInterface dialog, int which) { 592 markSeenInfoDialog(activity); 593 } 594 }).setOnCancelListener(new OnCancelListener() { 595 @Override 596 public void onCancel(DialogInterface dialog) { 597 markSeenInfoDialog(activity); 598 } 599 }); 600 return builder.create(); 601 } 602 markSeenInfoDialog(android.app.Activity activity)603 protected static void markSeenInfoDialog(android.app.Activity activity) { 604 ContentResolver resolver = activity.getContentResolver(); 605 ContentValues values = new ContentValues(2); 606 String activityName = setTestNameSuffix(sCurrentDisplayMode, activity.getClass().getName()); 607 values.put(TestResultsProvider.COLUMN_TEST_NAME, activityName); 608 values.put(TestResultsProvider.COLUMN_TEST_INFO_SEEN, 1); 609 int numUpdated = resolver.update( 610 TestResultsProvider.getTestNameUri(activity), values, null, null); 611 if (numUpdated == 0) { 612 resolver.insert(TestResultsProvider.getResultContentUri(activity), values); 613 } 614 } 615 616 /** Set the test result corresponding to the button clicked and finish the activity. */ setTestResultAndFinish(android.app.Activity activity, String testId, String testDetails, ReportLog reportLog, TestResultHistoryCollection historyCollection, View target)617 protected static void setTestResultAndFinish(android.app.Activity activity, String testId, 618 String testDetails, ReportLog reportLog, TestResultHistoryCollection historyCollection, 619 View target) { 620 621 boolean passed; 622 if (target.getId() == R.id.pass_button) { 623 passed = true; 624 } else if (target.getId() == R.id.fail_button) { 625 passed = false; 626 } else { 627 throw new IllegalArgumentException("Unknown id: " + target.getId()); 628 } 629 630 // Let test classes record their CTSVerifierReportLogs 631 ((PassFailActivity) activity).recordTestResults(); 632 633 setTestResultAndFinishHelper(activity, testId, testDetails, passed, reportLog, historyCollection); 634 } 635 636 /** Set the test result and finish the activity. */ setTestResultAndFinishHelper(android.app.Activity activity, String testId, String testDetails, boolean passed, ReportLog reportLog, TestResultHistoryCollection historyCollection)637 protected static void setTestResultAndFinishHelper(android.app.Activity activity, String testId, 638 String testDetails, boolean passed, ReportLog reportLog, 639 TestResultHistoryCollection historyCollection) { 640 if (passed) { 641 TestResult.setPassedResult(activity, testId, testDetails, reportLog, historyCollection); 642 } else { 643 TestResult.setFailedResult(activity, testId, testDetails, reportLog, historyCollection); 644 } 645 646 activity.finish(); 647 } 648 getPassButtonView(android.app.Activity activity)649 protected static ImageButton getPassButtonView(android.app.Activity activity) { 650 return (ImageButton) activity.findViewById(R.id.pass_button); 651 } 652 653 } // class PassFailButtons 654