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     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