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