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