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