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