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