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