1 /* 2 3 * Copyright (C) 2014 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.cts.verifier.sensors.base; 19 20 import android.content.ActivityNotFoundException; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.PackageManager; 24 import android.hardware.cts.helpers.ActivityResultMultiplexedLatch; 25 import android.media.MediaPlayer; 26 import android.opengl.GLSurfaceView; 27 import android.os.Bundle; 28 import android.os.SystemClock; 29 import android.os.Vibrator; 30 import android.provider.Settings; 31 import android.text.TextUtils; 32 import android.text.format.DateUtils; 33 import android.util.Log; 34 import android.view.View; 35 import android.widget.Button; 36 import android.widget.LinearLayout; 37 import android.widget.ScrollView; 38 import android.widget.TextView; 39 40 import com.android.cts.verifier.PassFailButtons; 41 import com.android.cts.verifier.R; 42 import com.android.cts.verifier.TestListActivity; 43 import com.android.cts.verifier.TestListAdapter; 44 import com.android.cts.verifier.TestResult; 45 import com.android.cts.verifier.sensors.helpers.SensorFeaturesDeactivator; 46 import com.android.cts.verifier.sensors.reporting.SensorTestDetails; 47 48 import junit.framework.Assert; 49 50 import java.util.ArrayList; 51 import java.util.concurrent.CountDownLatch; 52 import java.util.concurrent.ExecutorService; 53 import java.util.concurrent.Executors; 54 import java.util.concurrent.TimeUnit; 55 56 /** 57 * A base Activity that is used to build different methods to execute tests inside CtsVerifier. i.e. 58 * CTS tests, and semi-automated CtsVerifier tests. 59 * 60 * <p>This class provides access to the following flow: Activity set up Execute tests (implemented 61 * by sub-classes) Activity clean up 62 * 63 * <p>Currently the following class structure is available: - BaseSensorTestActivity : provides the 64 * platform to execute Sensor tests inside | CtsVerifier, and logging support | -- 65 * SensorCtsTestActivity : an activity that can be inherited from to wrap a CTS | sensor test, and 66 * execute it inside CtsVerifier | these tests do not require any operator interaction | -- 67 * SensorCtsVerifierTestActivity : an activity that can be inherited to write sensor tests that 68 * require operator interaction 69 */ 70 public abstract class BaseSensorTestActivity extends PassFailButtons.Activity 71 implements View.OnClickListener, Runnable, ISensorTestStateContainer { 72 /** 73 * @deprecated use subclass instead? 74 */ 75 @Deprecated protected static final String LOG_TAG = "SensorTest"; 76 77 protected final Class mTestClass; 78 79 private final int mLayoutId; 80 private final SensorFeaturesDeactivator mSensorFeaturesDeactivator; 81 82 private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor(); 83 private final SensorTestLogger mTestLogger = new SensorTestLogger(); 84 private final ActivityResultMultiplexedLatch mActivityResultMultiplexedLatch = 85 new ActivityResultMultiplexedLatch(); 86 private final ArrayList<CountDownLatch> mWaitForUserLatches = new ArrayList<CountDownLatch>(); 87 88 private ScrollView mLogScrollView; 89 private LinearLayout mLogLayout; 90 private Button mNextButton; 91 private Button mPassButton; 92 private Button mFailButton; 93 private Button mRetryButton; 94 95 private GLSurfaceView mGLSurfaceView; 96 private boolean mUsingGlSurfaceView; 97 98 // Flag for Retry button appearance. 99 private boolean mShouldRetry = false; 100 private int mRetryCount = 0; 101 102 /** 103 * Constructor to be used by subclasses. 104 * 105 * @param testClass The class that contains the tests. It is dependant on test executor 106 * implemented by subclasses. 107 */ BaseSensorTestActivity(Class testClass)108 protected BaseSensorTestActivity(Class testClass) { 109 this(testClass, R.layout.sensor_test); 110 } 111 112 /** 113 * Constructor to be used by subclasses. It allows to provide a custom layout for the test UI. 114 * 115 * @param testClass The class that contains the tests. It is dependant on test executor 116 * implemented by subclasses. 117 * @param layoutId The Id of the layout to use for the test UI. The layout must contain all the 118 * elements in the base layout {@code R.layout.sensor_test}. 119 */ BaseSensorTestActivity(Class testClass, int layoutId)120 protected BaseSensorTestActivity(Class testClass, int layoutId) { 121 mTestClass = testClass; 122 mLayoutId = layoutId; 123 mSensorFeaturesDeactivator = new SensorFeaturesDeactivator(this); 124 } 125 126 @Override onCreate(Bundle savedInstanceState)127 protected void onCreate(Bundle savedInstanceState) { 128 super.onCreate(savedInstanceState); 129 setContentView(mLayoutId); 130 131 mLogScrollView = (ScrollView) findViewById(R.id.log_scroll_view); 132 mLogLayout = (LinearLayout) findViewById(R.id.log_layout); 133 mNextButton = (Button) findViewById(R.id.next_button); 134 mNextButton.setOnClickListener(this); 135 mPassButton = (Button) findViewById(R.id.pass_button); 136 mFailButton = (Button) findViewById(R.id.fail_button); 137 mGLSurfaceView = (GLSurfaceView) findViewById(R.id.gl_surface_view); 138 mRetryButton = (Button) findViewById(R.id.retry_button); 139 mRetryButton.setOnClickListener(new retryButtonListener()); 140 141 updateNextButton(false /*enabled*/); 142 mExecutorService.execute(this); 143 } 144 145 @Override onDestroy()146 protected void onDestroy() { 147 super.onDestroy(); 148 mExecutorService.shutdownNow(); 149 } 150 151 @Override onPause()152 protected void onPause() { 153 super.onPause(); 154 if (mUsingGlSurfaceView) { 155 mGLSurfaceView.onPause(); 156 } 157 } 158 159 @Override onResume()160 protected void onResume() { 161 super.onResume(); 162 if (mUsingGlSurfaceView) { 163 mGLSurfaceView.onResume(); 164 } 165 } 166 167 @Override onClick(View target)168 public void onClick(View target) { 169 mShouldRetry = false; 170 171 synchronized (mWaitForUserLatches) { 172 for (CountDownLatch latch : mWaitForUserLatches) { 173 latch.countDown(); 174 } 175 mWaitForUserLatches.clear(); 176 } 177 } 178 179 private class retryButtonListener implements View.OnClickListener { 180 181 @Override onClick(View v)182 public void onClick(View v) { 183 mShouldRetry = true; 184 ++mRetryCount; 185 186 synchronized (mWaitForUserLatches) { 187 for (CountDownLatch latch : mWaitForUserLatches) { 188 latch.countDown(); 189 } 190 mWaitForUserLatches.clear(); 191 } 192 } 193 } 194 195 @Override onActivityResult(int requestCode, int resultCode, Intent data)196 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 197 mActivityResultMultiplexedLatch.onActivityResult(requestCode, resultCode); 198 } 199 200 /** 201 * The main execution {@link Thread}. 202 * 203 * <p>This function executes in a background thread, allowing the test run freely behind the 204 * scenes. It provides the following execution hooks: - Activity SetUp/CleanUp (not available in 205 * JUnit) - executeTests: to implement several execution engines 206 */ 207 @Override run()208 public void run() { 209 long startTimeNs = SystemClock.elapsedRealtimeNanos(); 210 String testName = getTestClassName(); 211 212 SensorTestDetails testDetails; 213 try { 214 mSensorFeaturesDeactivator.requestDeactivationOfFeatures(); 215 testDetails = new SensorTestDetails(testName, SensorTestDetails.ResultCode.PASS); 216 } catch (Throwable e) { 217 testDetails = new SensorTestDetails(testName, "DeactivateSensorFeatures", e); 218 } 219 220 SensorTestDetails.ResultCode resultCode = testDetails.getResultCode(); 221 if (resultCode == SensorTestDetails.ResultCode.SKIPPED) { 222 // this is an invalid state at this point of the test setup 223 throw new IllegalStateException("Deactivation of features cannot skip the test."); 224 } 225 if (resultCode == SensorTestDetails.ResultCode.PASS) { 226 testDetails = executeActivityTests(testName); 227 } 228 229 // we consider all remaining states at this point, because we could have been half way 230 // deactivating features 231 try { 232 mSensorFeaturesDeactivator.requestToRestoreFeatures(); 233 } catch (Throwable e) { 234 testDetails = new SensorTestDetails(testName, "RestoreSensorFeatures", e); 235 } 236 237 mTestLogger.logTestDetails(testDetails); 238 mTestLogger.logExecutionTime(startTimeNs); 239 240 // because we cannot enforce test failures in several devices, set the test UI so the 241 // operator can report the result of the test 242 promptUserToSetResult(testDetails); 243 } 244 245 /** 246 * A general set up routine. It executes only once before the first test case. 247 * 248 * <p>NOTE: implementers must be aware of the interrupted status of the worker thread, and let 249 * {@link InterruptedException} propagate. 250 * 251 * @throws Throwable An exception that denotes the failure of set up. No tests will be executed. 252 */ activitySetUp()253 protected void activitySetUp() throws Throwable {} 254 255 /** 256 * A general clean up routine. It executes upon successful execution of {@link #activitySetUp()} 257 * and after all the test cases. 258 * 259 * <p>NOTE: implementers must be aware of the interrupted status of the worker thread, and 260 * handle it in two cases: - let {@link InterruptedException} propagate - if it is invoked with 261 * the interrupted status, prevent from showing any UI 262 * 263 * @throws Throwable An exception that will be logged and ignored, for ease of implementation by 264 * subclasses. 265 */ activityCleanUp()266 protected void activityCleanUp() throws Throwable {} 267 268 /** 269 * Performs the work of executing the tests. Sub-classes implementing different execution 270 * methods implement this method. 271 * 272 * @return A {@link SensorTestDetails} object containing information about the executed tests. 273 */ executeTests()274 protected abstract SensorTestDetails executeTests() throws InterruptedException; 275 276 /** Get mShouldRetry to check if test is required to retry. */ getShouldRetry()277 protected boolean getShouldRetry() { 278 return mShouldRetry; 279 } 280 281 @Override getTestLogger()282 public SensorTestLogger getTestLogger() { 283 return mTestLogger; 284 } 285 286 @Deprecated appendText(int resId)287 protected void appendText(int resId) { 288 mTestLogger.logInstructions(resId); 289 } 290 291 @Deprecated appendText(String text)292 protected void appendText(String text) { 293 TextAppender textAppender = new TextAppender(R.layout.snsr_instruction); 294 textAppender.setText(text); 295 textAppender.append(); 296 } 297 298 @Deprecated clearText()299 protected void clearText() { 300 this.runOnUiThread( 301 new Runnable() { 302 @Override 303 public void run() { 304 mLogLayout.removeAllViews(); 305 } 306 }); 307 } 308 309 /** 310 * Waits for the operator to acknowledge a requested action. 311 * 312 * @param waitMessageResId The action requested to the operator. 313 */ waitForUser(int waitMessageResId)314 protected void waitForUser(int waitMessageResId) throws InterruptedException { 315 CountDownLatch latch = new CountDownLatch(1); 316 synchronized (mWaitForUserLatches) { 317 mWaitForUserLatches.add(latch); 318 } 319 320 mTestLogger.logInstructions(waitMessageResId); 321 setNextButtonText(waitMessageResId); 322 323 updateRetryButton(true); 324 updateNextButton(true); 325 latch.await(); 326 updateRetryButton(false); 327 updateNextButton(false); 328 } 329 330 /** Waits for the operator to acknowledge to begin execution. */ waitForUserToBegin()331 protected void waitForUserToBegin() throws InterruptedException { 332 waitForUser(R.string.snsr_wait_to_begin); 333 } 334 335 /** Waits for the operator to acknowledge to retry execution. */ waitForUserToRetry()336 protected void waitForUserToRetry() throws InterruptedException { 337 mShouldRetry = true; 338 waitForUser(R.string.snsr_wait_to_retry); 339 } 340 341 /** Waits for the operator to acknowledge to finish execution. */ waitForUserToFinish()342 protected void waitForUserToFinish() throws InterruptedException { 343 mShouldRetry = true; 344 waitForUser(R.string.snsr_wait_to_finish); 345 } 346 347 /** {@inheritDoc} */ 348 @Override waitForUserToContinue()349 public void waitForUserToContinue() throws InterruptedException { 350 waitForUser(R.string.snsr_wait_for_user); 351 } 352 353 /** {@inheritDoc} */ 354 @Override executeActivity(String action)355 public int executeActivity(String action) throws InterruptedException { 356 return executeActivity(new Intent(action)); 357 } 358 359 /** {@inheritDoc} */ 360 @Override executeActivity(Intent intent)361 public int executeActivity(Intent intent) throws InterruptedException { 362 ActivityResultMultiplexedLatch.Latch latch = mActivityResultMultiplexedLatch.bindThread(); 363 try { 364 startActivityForResult(intent, latch.getRequestCode()); 365 } catch (ActivityNotFoundException e) { 366 // handle exception gracefully 367 // Among all defined activity results, RESULT_CANCELED offers the semantic closest to 368 // represent absent setting activity. 369 return RESULT_CANCELED; 370 } 371 return latch.await(); 372 } 373 374 /** {@inheritDoc} */ 375 @Override hasSystemFeature(String feature)376 public boolean hasSystemFeature(String feature) { 377 PackageManager pm = getPackageManager(); 378 return pm.hasSystemFeature(feature); 379 } 380 381 /** {@inheritDoc} */ 382 @Override hasActivity(String action)383 public boolean hasActivity(String action) { 384 PackageManager pm = getPackageManager(); 385 return pm.resolveActivity(new Intent(action), PackageManager.MATCH_DEFAULT_ONLY) != null; 386 } 387 388 @Override requiresReportLog()389 public boolean requiresReportLog() { 390 return true; 391 } 392 393 /** 394 * Initializes and shows the {@link GLSurfaceView} available to tests. NOTE: initialization can 395 * be performed only once, usually inside {@link #activitySetUp()}. 396 */ initializeGlSurfaceView(final GLSurfaceView.Renderer renderer)397 protected void initializeGlSurfaceView(final GLSurfaceView.Renderer renderer) { 398 runOnUiThread( 399 new Runnable() { 400 @Override 401 public void run() { 402 mGLSurfaceView.setVisibility(View.VISIBLE); 403 mGLSurfaceView.setRenderer(renderer); 404 mUsingGlSurfaceView = true; 405 } 406 }); 407 } 408 409 /** Closes and hides the {@link GLSurfaceView}. */ closeGlSurfaceView()410 protected void closeGlSurfaceView() { 411 runOnUiThread( 412 new Runnable() { 413 @Override 414 public void run() { 415 if (!mUsingGlSurfaceView) { 416 return; 417 } 418 mGLSurfaceView.setVisibility(View.GONE); 419 mGLSurfaceView.onPause(); 420 mUsingGlSurfaceView = false; 421 } 422 }); 423 } 424 425 /** Plays a (default) sound as a notification for the operator. */ playSound()426 protected void playSound() throws InterruptedException { 427 MediaPlayer player = MediaPlayer.create(this, Settings.System.DEFAULT_NOTIFICATION_URI); 428 if (player == null) { 429 Log.e(LOG_TAG, "MediaPlayer unavailable."); 430 return; 431 } 432 player.start(); 433 try { 434 Thread.sleep(500); 435 } finally { 436 player.stop(); 437 } 438 } 439 440 /** Makes the device vibrate for the given amount of time. */ vibrate(int timeInMs)441 protected void vibrate(int timeInMs) { 442 Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); 443 vibrator.vibrate(timeInMs); 444 } 445 446 /** 447 * Makes the device vibrate following the given pattern. See {@link Vibrator#vibrate(long[], 448 * int)} for more information. 449 */ vibrate(long[] pattern)450 protected void vibrate(long[] pattern) { 451 Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); 452 vibrator.vibrate(pattern, -1); 453 } 454 455 // TODO: move to sensor assertions assertTimestampSynchronization( long eventTimestamp, long receivedTimestamp, long deltaThreshold, String sensorName)456 protected String assertTimestampSynchronization( 457 long eventTimestamp, long receivedTimestamp, long deltaThreshold, String sensorName) { 458 long timestampDelta = Math.abs(eventTimestamp - receivedTimestamp); 459 String timestampMessage = 460 getString( 461 R.string.snsr_event_time, 462 receivedTimestamp, 463 eventTimestamp, 464 timestampDelta, 465 deltaThreshold, 466 sensorName); 467 Assert.assertTrue(timestampMessage, timestampDelta < deltaThreshold); 468 return timestampMessage; 469 } 470 getTestClassName()471 protected String getTestClassName() { 472 if (mTestClass == null) { 473 return "<unknown>"; 474 } 475 return mTestClass.getName(); 476 } 477 setLogScrollViewListener(View.OnTouchListener listener)478 protected void setLogScrollViewListener(View.OnTouchListener listener) { 479 mLogScrollView.setOnTouchListener(listener); 480 } 481 setTestResult(SensorTestDetails testDetails)482 private void setTestResult(SensorTestDetails testDetails) { 483 // the name here, must be the Activity's name because it is what CtsVerifier expects 484 String name = 485 TestListAdapter.setTestNameSuffix( 486 TestListActivity.sCurrentDisplayMode, super.getClass().getName()); 487 String summary = mTestLogger.getOverallSummary(); 488 SensorTestDetails.ResultCode resultCode = testDetails.getResultCode(); 489 switch (resultCode) { 490 case SKIPPED: 491 TestResult.setPassedResult(this, name, summary); 492 break; 493 case PASS: 494 case WARNING: 495 TestResult.setPassedResult(this, name, summary); 496 break; 497 case FAIL: 498 TestResult.setFailedResult(this, name, summary); 499 break; 500 case INTERRUPTED: 501 // do not set a result, just return so the test can complete 502 break; 503 default: 504 throw new IllegalStateException("Unknown ResultCode: " + resultCode); 505 } 506 } 507 executeActivityTests(String testName)508 private SensorTestDetails executeActivityTests(String testName) { 509 SensorTestDetails testDetails; 510 try { 511 activitySetUp(); 512 testDetails = new SensorTestDetails(testName, SensorTestDetails.ResultCode.PASS); 513 } catch (Throwable e) { 514 testDetails = new SensorTestDetails(testName, "ActivitySetUp", e); 515 } 516 517 SensorTestDetails.ResultCode resultCode = testDetails.getResultCode(); 518 if (resultCode == SensorTestDetails.ResultCode.PASS) { 519 // TODO: implement execution filters: 520 // - execute all tests and report results officially 521 // - execute single test or failed tests only 522 try { 523 testDetails = executeTests(); 524 } catch (Throwable e) { 525 // we catch and continue because we have to guarantee a proper clean-up sequence 526 testDetails = new SensorTestDetails(testName, "TestExecution", e); 527 } 528 } 529 530 // clean-up executes for all states, even on SKIPPED and INTERRUPTED there might be some 531 // intermediate state that needs to be taken care of 532 try { 533 activityCleanUp(); 534 } catch (Throwable e) { 535 testDetails = new SensorTestDetails(testName, "ActivityCleanUp", e); 536 } 537 538 return testDetails; 539 } 540 promptUserToSetResult(SensorTestDetails testDetails)541 private void promptUserToSetResult(SensorTestDetails testDetails) { 542 SensorTestDetails.ResultCode resultCode = testDetails.getResultCode(); 543 if (resultCode == SensorTestDetails.ResultCode.FAIL) { 544 mTestLogger.logInstructions(R.string.snsr_test_complete_with_errors); 545 enableTestResultButton( 546 mFailButton, 547 R.string.fail_button_text, 548 testDetails.cloneAndChangeResultCode(SensorTestDetails.ResultCode.FAIL)); 549 } else if (resultCode != SensorTestDetails.ResultCode.INTERRUPTED) { 550 mTestLogger.logInstructions(R.string.snsr_test_complete); 551 enableTestResultButton( 552 mPassButton, 553 R.string.pass_button_text, 554 testDetails.cloneAndChangeResultCode(SensorTestDetails.ResultCode.PASS)); 555 } 556 } 557 updateNextButton(final boolean enabled)558 private void updateNextButton(final boolean enabled) { 559 runOnUiThread( 560 new Runnable() { 561 @Override 562 public void run() { 563 mNextButton.setEnabled(enabled); 564 } 565 }); 566 } 567 568 /** 569 * Set the text for next button by instruction message. During retry, next button text is 570 * changed to notify users. 571 * 572 * @param waitMessageResId The action requested to the operator. 573 */ setNextButtonText(int waitMessageResId)574 private void setNextButtonText(int waitMessageResId) { 575 int nextButtonText; 576 if (waitMessageResId == R.string.snsr_wait_to_retry) { 577 nextButtonText = R.string.fail_and_next_button_text; 578 } else if (waitMessageResId == R.string.snsr_wait_to_finish) { 579 nextButtonText = R.string.finish_button_text; 580 } else { 581 nextButtonText = R.string.next_button_text; 582 } 583 584 runOnUiThread( 585 new Runnable() { 586 @Override 587 public void run() { 588 mNextButton.setText(nextButtonText); 589 } 590 }); 591 } 592 593 /** 594 * Update the retry button status. During retry, show retry execution count. If not to retry, 595 * make retry button invisible. 596 * 597 * @param enabled The status of button. 598 */ updateRetryButton(final boolean enabled)599 private void updateRetryButton(final boolean enabled) { 600 runOnUiThread( 601 new Runnable() { 602 @Override 603 public void run() { 604 if (mShouldRetry) { 605 String showRetryCount = 606 String.format( 607 "%s (%d)", 608 getResources().getText(R.string.retry_button_text), 609 mRetryCount); 610 mRetryButton.setText(showRetryCount); 611 mRetryButton.setVisibility(View.VISIBLE); 612 mRetryButton.setEnabled(enabled); 613 } else { 614 mRetryButton.setVisibility(View.GONE); 615 mRetryCount = 0; 616 } 617 } 618 }); 619 } 620 enableTestResultButton( final Button button, final int textResId, final SensorTestDetails testDetails)621 private void enableTestResultButton( 622 final Button button, final int textResId, final SensorTestDetails testDetails) { 623 final View.OnClickListener listener = 624 new View.OnClickListener() { 625 @Override 626 public void onClick(View v) { 627 setTestResult(testDetails); 628 finish(); 629 } 630 }; 631 632 runOnUiThread( 633 new Runnable() { 634 @Override 635 public void run() { 636 mNextButton.setVisibility(View.GONE); 637 mRetryButton.setVisibility(View.GONE); 638 button.setText(textResId); 639 button.setOnClickListener(listener); 640 button.setVisibility(View.VISIBLE); 641 } 642 }); 643 } 644 645 // a logger available until sensor reporting is in place 646 public class SensorTestLogger { 647 private static final String SUMMARY_SEPARATOR = " | "; 648 649 private final StringBuilder mOverallSummaryBuilder = new StringBuilder("\n"); 650 logCustomView(View view)651 public void logCustomView(View view) { 652 new ViewAppender(view).append(); 653 } 654 logTestStart(String testName)655 void logTestStart(String testName) { 656 // TODO: log the sensor information and expected execution time of each test 657 TextAppender textAppender = new TextAppender(R.layout.snsr_test_title); 658 textAppender.setText(testName); 659 textAppender.append(); 660 } 661 662 /** Logs the instructions by the given ID and the params. */ logInstructions(int instructionsResId, Object... params)663 public void logInstructions(int instructionsResId, Object... params) { 664 TextAppender textAppender = new TextAppender(R.layout.snsr_instruction); 665 textAppender.setText(getString(instructionsResId, params)); 666 textAppender.append(); 667 } 668 669 /** Logs the specific message with given params. */ logMessage(int messageResId, Object... params)670 public void logMessage(int messageResId, Object... params) { 671 TextAppender textAppender = new TextAppender(R.layout.snsr_message); 672 textAppender.setText(getString(messageResId, params)); 673 textAppender.append(); 674 } 675 logWaitForSound()676 public void logWaitForSound() { 677 logInstructions(R.string.snsr_test_play_sound); 678 } 679 logTestDetails(SensorTestDetails testDetails)680 public void logTestDetails(SensorTestDetails testDetails) { 681 String name = testDetails.getName(); 682 String summary = testDetails.getSummary(); 683 SensorTestDetails.ResultCode resultCode = testDetails.getResultCode(); 684 switch (resultCode) { 685 case SKIPPED: 686 logTestSkip(name, summary); 687 break; 688 case PASS: 689 case WARNING: 690 mShouldRetry = false; 691 logTestPass(name, summary); 692 break; 693 case FAIL: 694 logTestFail(name, summary); 695 break; 696 case INTERRUPTED: 697 // do nothing, the test was interrupted so do we 698 break; 699 default: 700 throw new IllegalStateException("Unknown ResultCode: " + resultCode); 701 } 702 } 703 logTestPass(String testName, String testSummary)704 void logTestPass(String testName, String testSummary) { 705 testSummary = getValidTestSummary(testSummary, R.string.snsr_test_pass); 706 logTestEnd(R.layout.snsr_success, testSummary); 707 Log.d(LOG_TAG, testSummary); 708 saveResult(testName, SensorTestDetails.ResultCode.PASS, testSummary); 709 } 710 logTestFail(String testName, String testSummary)711 public void logTestFail(String testName, String testSummary) { 712 testSummary = getValidTestSummary(testSummary, R.string.snsr_test_fail); 713 logTestEnd(R.layout.snsr_error, testSummary); 714 Log.e(LOG_TAG, testSummary); 715 saveResult(testName, SensorTestDetails.ResultCode.FAIL, testSummary); 716 } 717 logTestSkip(String testName, String testSummary)718 void logTestSkip(String testName, String testSummary) { 719 testSummary = getValidTestSummary(testSummary, R.string.snsr_test_skipped); 720 logTestEnd(R.layout.snsr_warning, testSummary); 721 Log.i(LOG_TAG, testSummary); 722 saveResult(testName, SensorTestDetails.ResultCode.SKIPPED, testSummary); 723 } 724 getOverallSummary()725 String getOverallSummary() { 726 return mOverallSummaryBuilder.toString(); 727 } 728 logExecutionTime(long startTimeNs)729 void logExecutionTime(long startTimeNs) { 730 if (Thread.currentThread().isInterrupted()) { 731 return; 732 } 733 long executionTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs; 734 long executionTimeSec = TimeUnit.NANOSECONDS.toSeconds(executionTimeNs); 735 // TODO: find a way to format times with nanosecond accuracy and longer than 24hrs 736 String formattedElapsedTime = DateUtils.formatElapsedTime(executionTimeSec); 737 logMessage(R.string.snsr_execution_time, formattedElapsedTime); 738 } 739 logTestEnd(int textViewResId, String testSummary)740 private void logTestEnd(int textViewResId, String testSummary) { 741 TextAppender textAppender = new TextAppender(textViewResId); 742 textAppender.setText(testSummary); 743 textAppender.append(); 744 } 745 getValidTestSummary(String testSummary, int defaultSummaryResId)746 private String getValidTestSummary(String testSummary, int defaultSummaryResId) { 747 if (TextUtils.isEmpty(testSummary)) { 748 return getString(defaultSummaryResId); 749 } 750 return testSummary; 751 } 752 saveResult( String testName, SensorTestDetails.ResultCode resultCode, String summary)753 private void saveResult( 754 String testName, SensorTestDetails.ResultCode resultCode, String summary) { 755 mOverallSummaryBuilder.append(testName); 756 mOverallSummaryBuilder.append(SUMMARY_SEPARATOR); 757 mOverallSummaryBuilder.append(resultCode.name()); 758 mOverallSummaryBuilder.append(SUMMARY_SEPARATOR); 759 mOverallSummaryBuilder.append(summary); 760 mOverallSummaryBuilder.append("\n"); 761 } 762 } 763 764 private class ViewAppender { 765 protected final View mView; 766 ViewAppender(View view)767 public ViewAppender(View view) { 768 mView = view; 769 } 770 append()771 public void append() { 772 runOnUiThread( 773 new Runnable() { 774 @Override 775 public void run() { 776 mLogLayout.addView(mView); 777 mLogScrollView.post( 778 new Runnable() { 779 @Override 780 public void run() { 781 mLogScrollView.fullScroll(View.FOCUS_DOWN); 782 } 783 }); 784 } 785 }); 786 } 787 } 788 789 private class TextAppender extends ViewAppender { 790 private final TextView mTextView; 791 TextAppender(int textViewResId)792 public TextAppender(int textViewResId) { 793 super(getLayoutInflater().inflate(textViewResId, null /* viewGroup */)); 794 mTextView = (TextView) mView; 795 } 796 setText(String text)797 public void setText(String text) { 798 mTextView.setText(text); 799 } 800 setText(int textResId)801 public void setText(int textResId) { 802 mTextView.setText(textResId); 803 } 804 } 805 } 806