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