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      */
BaseSensorTestActivity(Class testClass)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      */
BaseSensorTestActivity(Class testClass, int layoutId)116     protected BaseSensorTestActivity(Class testClass, int layoutId) {
117         mTestClass = testClass;
118         mLayoutId = layoutId;
119         mSensorFeaturesDeactivator = new SensorFeaturesDeactivator(this);
120     }
121 
122     @Override
onCreate(Bundle savedInstanceState)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
onDestroy()140     protected void onDestroy() {
141         super.onDestroy();
142         mExecutorService.shutdownNow();
143     }
144 
145     @Override
onPause()146     protected void onPause() {
147         super.onPause();
148         if (mUsingGlSurfaceView) {
149             mGLSurfaceView.onPause();
150         }
151     }
152 
153     @Override
onResume()154     protected void onResume() {
155         super.onResume();
156         if (mUsingGlSurfaceView) {
157             mGLSurfaceView.onResume();
158         }
159     }
160 
161     @Override
onClick(View target)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
onActivityResult(int requestCode, int resultCode, Intent data)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
run()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      */
activitySetUp()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      */
activityCleanUp()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      */
executeTests()252     protected abstract SensorTestDetails executeTests() throws InterruptedException;
253 
254     @Override
getTestLogger()255     public SensorTestLogger getTestLogger() {
256         return mTestLogger;
257     }
258 
259     @Deprecated
appendText(int resId)260     protected void appendText(int resId) {
261         mTestLogger.logInstructions(resId);
262     }
263 
264     @Deprecated
appendText(String text)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
clearText()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      */
waitForUser(int waitMessageResId)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      */
waitForUserToBegin()301     protected void waitForUserToBegin() throws InterruptedException {
302         waitForUser(R.string.snsr_wait_to_begin);
303     }
304 
305     /**
306      * {@inheritDoc}
307      */
308     @Override
waitForUserToContinue()309     public void waitForUserToContinue() throws InterruptedException {
310         waitForUser(R.string.snsr_wait_for_user);
311     }
312 
313     /**
314      * {@inheritDoc}
315      */
316     @Override
executeActivity(String action)317     public int executeActivity(String action) throws InterruptedException {
318         return executeActivity(new Intent(action));
319     }
320 
321     /**
322      * {@inheritDoc}
323      */
324     @Override
executeActivity(Intent intent)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      */
initializeGlSurfaceView(final GLSurfaceView.Renderer renderer)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      */
closeGlSurfaceView()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      */
playSound()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      */
vibrate(int timeInMs)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      */
vibrate(long[] pattern)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
assertTimestampSynchronization( long eventTimestamp, long receivedTimestamp, long deltaThreshold, String sensorName)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 
getTestClassName()415     protected String getTestClassName() {
416         if (mTestClass == null) {
417             return "<unknown>";
418         }
419         return mTestClass.getName();
420     }
421 
setLogScrollViewListener(View.OnTouchListener listener)422     protected void setLogScrollViewListener(View.OnTouchListener listener) {
423         mLogScrollView.setOnTouchListener(listener);
424     }
425 
setTestResult(SensorTestDetails testDetails)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 
executeActivityTests(String testName)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 
promptUserToSetResult(SensorTestDetails testDetails)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 
updateNextButton(final boolean enabled)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 
enableTestResultButton( final Button button, final int textResId, final SensorTestDetails testDetails)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 
logTestStart(String testName)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 
logInstructions(int instructionsResId, Object ... params)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 
logMessage(int messageResId, Object ... params)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 
logWaitForSound()560         public void logWaitForSound() {
561             logInstructions(R.string.snsr_test_play_sound);
562         }
563 
logTestDetails(SensorTestDetails testDetails)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 
logTestPass(String testName, String testSummary)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 
logTestFail(String testName, String testSummary)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 
logTestSkip(String testName, String testSummary)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 
getOverallSummary()607         String getOverallSummary() {
608             return mOverallSummaryBuilder.toString();
609         }
610 
logExecutionTime(long startTimeNs)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 
logTestEnd(int textViewResId, String testSummary)622         private void logTestEnd(int textViewResId, String testSummary) {
623             TextAppender textAppender = new TextAppender(textViewResId);
624             textAppender.setText(testSummary);
625             textAppender.append();
626         }
627 
getValidTestSummary(String testSummary, int defaultSummaryResId)628         private String getValidTestSummary(String testSummary, int defaultSummaryResId) {
629             if (TextUtils.isEmpty(testSummary)) {
630                 return getString(defaultSummaryResId);
631             }
632             return testSummary;
633         }
634 
saveResult( String testName, SensorTestDetails.ResultCode resultCode, String summary)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 
TextAppender(int textViewResId)651         public TextAppender(int textViewResId) {
652             mTextView = (TextView) getLayoutInflater().inflate(textViewResId, null /* viewGroup */);
653         }
654 
setText(String text)655         public void setText(String text) {
656             mTextView.setText(text);
657         }
658 
setText(int textResId)659         public void setText(int textResId) {
660             mTextView.setText(textResId);
661         }
662 
append()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