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