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