1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.cts.verifier.location.base;
18 
19 import com.android.cts.verifier.R;
20 import com.android.cts.verifier.TestResult;
21 import com.android.cts.verifier.location.reporting.GnssTestDetails;
22 
23 import junit.framework.Assert;
24 
25 import com.android.cts.verifier.PassFailButtons;
26 
27 import android.content.Context;
28 import android.content.Intent;
29 import android.hardware.cts.helpers.ActivityResultMultiplexedLatch;
30 import android.media.MediaPlayer;
31 import android.os.Bundle;
32 import android.os.SystemClock;
33 import android.os.Vibrator;
34 import android.provider.Settings;
35 import android.text.TextUtils;
36 import android.text.format.DateUtils;
37 import android.util.Log;
38 import android.view.View;
39 import android.widget.Button;
40 import android.widget.LinearLayout;
41 import android.widget.ScrollView;
42 import android.widget.TextView;
43 
44 import java.util.ArrayList;
45 import java.util.concurrent.CountDownLatch;
46 import java.util.concurrent.ExecutorService;
47 import java.util.concurrent.Executors;
48 import java.util.concurrent.TimeUnit;
49 
50 import android.test.AndroidTestCase;
51 
52 /**
53  * A base Activity that is used to build different methods to execute tests inside CtsVerifier.
54  * i.e. CTS tests, and semi-automated CtsVerifier tests.
55  *
56  * This class provides access to the following flow:
57  *      Activity set up
58  *          Execute tests (implemented by sub-classes)
59  *      Activity clean up
60  *
61  * Currently the following class structure is available:
62  * - BaseGnssTestActivity                 : provides the platform to execute Gnss tests inside
63  *      |                                     CtsVerifier.
64  *      |
65  *      -- GnssCtsTestActivity            : an activity that can be inherited from to wrap a CTS
66  *      |                                     Gnss test, and execute it inside CtsVerifier
67  *      |                                     these tests do not require any operator interaction
68  */
69 public abstract class BaseGnssTestActivity extends PassFailButtons.Activity
70         implements View.OnClickListener, Runnable, IGnssTestStateContainer {
71     @Deprecated
72     protected static final String LOG_TAG = "GnssTest";
73 
74     protected final Class mTestClass;
75 
76     private final int mLayoutId;
77 
78     private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
79     private final ActivityResultMultiplexedLatch mActivityResultMultiplexedLatch =
80             new ActivityResultMultiplexedLatch();
81     private final ArrayList<CountDownLatch> mWaitForUserLatches = new ArrayList<CountDownLatch>();
82 
83     private ScrollView mLogScrollView;
84     private LinearLayout mLogLayout;
85     private Button mNextButton;
86     protected TextView mTextView;
87 
88     /**
89      * Constructor to be used by subclasses.
90      *
91      * @param testClass The class that contains the tests. It is dependant on test executor
92      *                  implemented by subclasses.
93      */
BaseGnssTestActivity(Class<? extends AndroidTestCase> testClass)94     protected BaseGnssTestActivity(Class<? extends AndroidTestCase> testClass) {
95         this(testClass, R.layout.gnss_test);
96     }
97 
98     /**
99      * Constructor to be used by subclasses. It allows to provide a custom layout for the test UI.
100      *
101      * @param testClass The class that contains the tests. It is dependant on test executor
102      *                  implemented by subclasses.
103      * @param layoutId The Id of the layout to use for the test UI. The layout must contain all the
104      *                 elements in the base layout {@code R.layout.gnss_test}.
105      */
BaseGnssTestActivity(Class testClass, int layoutId)106     protected BaseGnssTestActivity(Class testClass, int layoutId) {
107         mTestClass = testClass;
108         mLayoutId = layoutId;
109     }
110 
111     @Override
onCreate(Bundle savedInstanceState)112     protected void onCreate(Bundle savedInstanceState) {
113         super.onCreate(savedInstanceState);
114         setContentView(mLayoutId);
115 
116         mLogScrollView = (ScrollView) findViewById(R.id.log_scroll_view);
117         mLogLayout = (LinearLayout) findViewById(R.id.log_layout);
118         mNextButton = (Button) findViewById(R.id.next_button);
119         mNextButton.setOnClickListener(this);
120         mTextView = (TextView) findViewById(R.id.text);
121 
122         mTextView.setText(R.string.location_gnss_test_info);
123 
124         updateNextButton(false /*not enabled*/);
125         mExecutorService.execute(this);
126     }
127 
128     @Override
onDestroy()129     protected void onDestroy() {
130         super.onDestroy();
131         mExecutorService.shutdownNow();
132     }
133 
134     @Override
onPause()135     protected void onPause() {
136         super.onPause();
137     }
138 
139     @Override
onResume()140     protected void onResume() {
141         super.onResume();
142     }
143 
144     @Override
onClick(View target)145     public void onClick(View target) {
146         synchronized (mWaitForUserLatches) {
147             for (CountDownLatch latch : mWaitForUserLatches) {
148                 latch.countDown();
149             }
150             mWaitForUserLatches.clear();
151         }
152     }
153 
154     @Override
onActivityResult(int requestCode, int resultCode, Intent data)155     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
156         mActivityResultMultiplexedLatch.onActivityResult(requestCode, resultCode);
157     }
158 
159     /**
160      * The main execution {@link Thread}.
161      *
162      * This function executes in a background thread, allowing the test run freely behind the
163      * scenes. It provides the following execution hooks:
164      *  - Activity SetUp/CleanUp (not available in JUnit)
165      *  - executeTests: to implement several execution engines
166      */
167     @Override
run()168     public void run() {
169         long startTimeNs = SystemClock.elapsedRealtimeNanos();
170         String testName = getTestClassName();
171 
172         GnssTestDetails testDetails;
173         try {
174             testDetails = new GnssTestDetails(testName, GnssTestDetails.ResultCode.PASS);
175         } catch (Throwable e) {
176             testDetails = new GnssTestDetails(testName, "DeactivateFeatures", e);
177         }
178 
179         GnssTestDetails.ResultCode resultCode = testDetails.getResultCode();
180         if (resultCode == GnssTestDetails.ResultCode.SKIPPED) {
181             // this is an invalid state at this point of the test setup
182             throw new IllegalStateException("Deactivation of features cannot skip the test.");
183         }
184         if (resultCode == GnssTestDetails.ResultCode.PASS) {
185             testDetails = executeActivityTests(testName);
186         }
187 
188         // This set the test UI so the operator can report the result of the test
189         updateResult(testDetails);
190     }
191 
192     /**
193      * A general set up routine. It executes only once before the first test case.
194      *
195      * NOTE: implementers must be aware of the interrupted status of the worker thread, and let
196      * {@link InterruptedException} propagate.
197      *
198      * @throws Throwable An exception that denotes the failure of set up. No tests will be executed.
199      */
activitySetUp()200     protected void activitySetUp() throws Throwable {}
201 
202     /**
203      * A general clean up routine. It executes upon successful execution of {@link #activitySetUp()}
204      * and after all the test cases.
205      *
206      * NOTE: implementers must be aware of the interrupted status of the worker thread, and handle
207      * it in two cases:
208      * - let {@link InterruptedException} propagate
209      * - if it is invoked with the interrupted status, prevent from showing any UI
210 
211      * @throws Throwable An exception that will be logged and ignored, for ease of implementation
212      *                   by subclasses.
213      */
activityCleanUp()214     protected void activityCleanUp() throws Throwable {}
215 
216     /**
217      * Performs the work of executing the tests.
218      * Sub-classes implementing different execution methods implement this method.
219      *
220      * @return A {@link GnssTestDetails} object containing information about the executed tests.
221      */
executeTests()222     protected abstract GnssTestDetails executeTests() throws InterruptedException;
223 
224     @Deprecated
appendText(String text)225     protected void appendText(String text) {
226         TextAppender textAppender = new TextAppender(R.layout.snsr_instruction);
227         textAppender.setText(text);
228         textAppender.append();
229     }
230 
231     @Deprecated
clearText()232     protected void clearText() {
233         this.runOnUiThread(new Runnable() {
234             @Override
235             public void run() {
236                 mLogLayout.removeAllViews();
237             }
238         });
239     }
240 
241     /**
242      * Waits for the operator to acknowledge a requested action.
243      *
244      * @param waitMessageResId The action requested to the operator.
245      */
waitForUser(int waitMessageResId)246     protected void waitForUser(int waitMessageResId) throws InterruptedException {
247         CountDownLatch latch = new CountDownLatch(1);
248         synchronized (mWaitForUserLatches) {
249             mWaitForUserLatches.add(latch);
250         }
251 
252         updateNextButton(true);
253         latch.await();
254         updateNextButton(false);
255     }
256 
257     /**
258      * Waits for the operator to acknowledge to begin execution.
259      */
waitForUserToBegin()260     protected void waitForUserToBegin() throws InterruptedException {
261         waitForUser(R.string.snsr_wait_to_begin);
262     }
263 
264     /**
265      * {@inheritDoc}
266      */
267     @Override
waitForUserToContinue()268     public void waitForUserToContinue() throws InterruptedException {
269         waitForUser(R.string.snsr_wait_for_user);
270     }
271 
272     /**
273      * {@inheritDoc}
274      */
275     @Override
executeActivity(String action)276     public int executeActivity(String action) throws InterruptedException {
277         return executeActivity(new Intent(action));
278     }
279 
280     /**
281      * {@inheritDoc}
282      */
283     @Override
executeActivity(Intent intent)284     public int executeActivity(Intent intent) throws InterruptedException {
285         ActivityResultMultiplexedLatch.Latch latch = mActivityResultMultiplexedLatch.bindThread();
286         startActivityForResult(intent, latch.getRequestCode());
287         return latch.await();
288     }
289 
290     /**
291      * Plays a (default) sound as a notification for the operator.
292      */
playSound()293     protected void playSound() throws InterruptedException {
294         MediaPlayer player = MediaPlayer.create(this, Settings.System.DEFAULT_NOTIFICATION_URI);
295         if (player == null) {
296             Log.e(LOG_TAG, "MediaPlayer unavailable.");
297             return;
298         }
299         player.start();
300         try {
301             Thread.sleep(500);
302         } finally {
303             player.stop();
304         }
305     }
306 
307     /**
308      * Makes the device vibrate for the given amount of time.
309      */
vibrate(int timeInMs)310     protected void vibrate(int timeInMs) {
311         Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
312         vibrator.vibrate(timeInMs);
313     }
314 
315     /**
316      * Makes the device vibrate following the given pattern.
317      * See {@link Vibrator#vibrate(long[], int)} for more information.
318      */
vibrate(long[] pattern)319     protected void vibrate(long[] pattern) {
320         Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
321         vibrator.vibrate(pattern, -1);
322     }
323 
getTestClassName()324     protected String getTestClassName() {
325         if (mTestClass == null) {
326             return "<unknown>";
327         }
328         return mTestClass.getName();
329     }
330 
setLogScrollViewListener(View.OnTouchListener listener)331     protected void setLogScrollViewListener(View.OnTouchListener listener) {
332         mLogScrollView.setOnTouchListener(listener);
333     }
334 
setTestResult(GnssTestDetails testDetails)335     private void setTestResult(GnssTestDetails testDetails) {
336         // the name here, must be the Activity's name because it is what CtsVerifier expects
337         String name = super.getClass().getName();
338         GnssTestDetails.ResultCode resultCode = testDetails.getResultCode();
339         switch(resultCode) {
340             case SKIPPED:
341                 TestResult.setPassedResult(this, name, "");
342                 break;
343             case PASS:
344                 TestResult.setPassedResult(this, name, "");
345                 break;
346             case FAIL:
347                 TestResult.setFailedResult(this, name, "");
348                 break;
349             case INTERRUPTED:
350                 // do not set a result, just return so the test can complete
351                 break;
352             default:
353                 throw new IllegalStateException("Unknown ResultCode: " + resultCode);
354         }
355     }
356 
executeActivityTests(String testName)357     private GnssTestDetails executeActivityTests(String testName) {
358         GnssTestDetails testDetails;
359         try {
360             activitySetUp();
361             testDetails = new GnssTestDetails(testName, GnssTestDetails.ResultCode.PASS);
362         } catch (Throwable e) {
363             testDetails = new GnssTestDetails(testName, "ActivitySetUp", e);
364         }
365 
366         GnssTestDetails.ResultCode resultCode = testDetails.getResultCode();
367         if (resultCode == GnssTestDetails.ResultCode.PASS) {
368             // TODO: implement execution filters:
369             //      - execute all tests and report results officially
370             //      - execute single test or failed tests only
371             try {
372                 testDetails = executeTests();
373             } catch (Throwable e) {
374                 // we catch and continue because we have to guarantee a proper clean-up sequence
375                 testDetails = new GnssTestDetails(testName, "TestExecution", e);
376             }
377         }
378 
379         // clean-up executes for all states, even on SKIPPED and INTERRUPTED there might be some
380         // intermediate state that needs to be taken care of
381         try {
382             activityCleanUp();
383         } catch (Throwable e) {
384             testDetails = new GnssTestDetails(testName, "ActivityCleanUp", e);
385         }
386 
387         return testDetails;
388     }
389 
updateResult(final GnssTestDetails testDetails)390     private void updateResult(final GnssTestDetails testDetails) {
391         runOnUiThread(new Runnable() {
392             @Override
393             public void run() {
394                 setTestResult(testDetails);
395             }
396         });
397     }
398 
updateNextButton(final boolean enabled)399     private void updateNextButton(final boolean enabled) {
400         runOnUiThread(new Runnable() {
401             @Override
402             public void run() {
403                 mNextButton.setEnabled(enabled);
404             }
405         });
406     }
407 
408     private class ViewAppender {
409         protected final View mView;
410 
ViewAppender(View view)411         public ViewAppender(View view) {
412             mView = view;
413         }
414 
append()415         public void append() {
416             runOnUiThread(new Runnable() {
417                 @Override
418                 public void run() {
419                     mLogLayout.addView(mView);
420                     mLogScrollView.post(new Runnable() {
421                         @Override
422                         public void run() {
423                             mLogScrollView.fullScroll(View.FOCUS_DOWN);
424                         }
425                     });
426                 }
427             });
428         }
429     }
430 
431     private class TextAppender extends ViewAppender{
432         private final TextView mTextView;
433 
TextAppender(int textViewResId)434         public TextAppender(int textViewResId) {
435             super(getLayoutInflater().inflate(textViewResId, null /* viewGroup */));
436             mTextView = (TextView) mView;
437         }
438 
setText(String text)439         public void setText(String text) {
440             mTextView.setText(text);
441         }
442 
setText(int textResId)443         public void setText(int textResId) {
444             mTextView.setText(textResId);
445         }
446     }
447 }