Home | History | Annotate | Download | only in base
      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      */
    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      */
    117     protected BaseSensorTestActivity(Class testClass, int layoutId) {
    118         mTestClass = testClass;
    119         mLayoutId = layoutId;
    120         mSensorFeaturesDeactivator = new SensorFeaturesDeactivator(this);
    121     }
    122 
    123     @Override
    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
    141     protected void onDestroy() {
    142         super.onDestroy();
    143         mExecutorService.shutdownNow();
    144     }
    145 
    146     @Override
    147     protected void onPause() {
    148         super.onPause();
    149         if (mUsingGlSurfaceView) {
    150             mGLSurfaceView.onPause();
    151         }
    152     }
    153 
    154     @Override
    155     protected void onResume() {
    156         super.onResume();
    157         if (mUsingGlSurfaceView) {
    158             mGLSurfaceView.onResume();
    159         }
    160     }
    161 
    162     @Override
    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
    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
    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      */
    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      */
    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      */
    253     protected abstract SensorTestDetails executeTests() throws InterruptedException;
    254 
    255     @Override
    256     public SensorTestLogger getTestLogger() {
    257         return mTestLogger;
    258     }
    259 
    260     @Deprecated
    261     protected void appendText(int resId) {
    262         mTestLogger.logInstructions(resId);
    263     }
    264 
    265     @Deprecated
    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
    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      */
    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      */
    302     protected void waitForUserToBegin() throws InterruptedException {
    303         waitForUser(R.string.snsr_wait_to_begin);
    304     }
    305 
    306     /**
    307      * {@inheritDoc}
    308      */
    309     @Override
    310     public void waitForUserToContinue() throws InterruptedException {
    311         waitForUser(R.string.snsr_wait_for_user);
    312     }
    313 
    314     /**
    315      * {@inheritDoc}
    316      */
    317     @Override
    318     public int executeActivity(String action) throws InterruptedException {
    319         return executeActivity(new Intent(action));
    320     }
    321 
    322     /**
    323      * {@inheritDoc}
    324      */
    325     @Override
    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
    343     public boolean hasSystemFeature(String feature) {
    344         PackageManager pm = getPackageManager();
    345         return pm.hasSystemFeature(feature);
    346     }
    347 
    348     /**
    349      * {@inheritDoc}
    350      */
    351     @Override
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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 
    441     protected String getTestClassName() {
    442         if (mTestClass == null) {
    443             return "<unknown>";
    444         }
    445         return mTestClass.getName();
    446     }
    447 
    448     protected void setLogScrollViewListener(View.OnTouchListener listener) {
    449         mLogScrollView.setOnTouchListener(listener);
    450     }
    451 
    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 
    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 
    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 
    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 
    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 
    563         public void logCustomView(View view) {
    564             new ViewAppender(view).append();
    565         }
    566 
    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 
    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 
    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 
    586         public void logWaitForSound() {
    587             logInstructions(R.string.snsr_test_play_sound);
    588         }
    589 
    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 
    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 
    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 
    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 
    633         String getOverallSummary() {
    634             return mOverallSummaryBuilder.toString();
    635         }
    636 
    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 
    648         private void logTestEnd(int textViewResId, String testSummary) {
    649             TextAppender textAppender = new TextAppender(textViewResId);
    650             textAppender.setText(testSummary);
    651             textAppender.append();
    652         }
    653 
    654         private String getValidTestSummary(String testSummary, int defaultSummaryResId) {
    655             if (TextUtils.isEmpty(testSummary)) {
    656                 return getString(defaultSummaryResId);
    657             }
    658             return testSummary;
    659         }
    660 
    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 
    677         public ViewAppender(View view) {
    678             mView = view;
    679         }
    680 
    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 
    700         public TextAppender(int textViewResId) {
    701             super(getLayoutInflater().inflate(textViewResId, null /* viewGroup */));
    702             mTextView = (TextView) mView;
    703         }
    704 
    705         public void setText(String text) {
    706             mTextView.setText(text);
    707         }
    708 
    709         public void setText(int textResId) {
    710             mTextView.setText(textResId);
    711         }
    712     }
    713 }
    714