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