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