blob: cacaa8e468f0127637ab35f22b831fba2f0ba63d [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cts.verifier.sensors.base;
import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R;
import com.android.cts.verifier.TestResult;
import com.android.cts.verifier.sensors.helpers.SensorFeaturesDeactivator;
import com.android.cts.verifier.sensors.reporting.SensorTestDetails;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.cts.helpers.ActivityResultMultiplexedLatch;
import android.media.MediaPlayer;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.os.SystemClock;
import android.os.Vibrator;
import android.provider.Settings;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import junit.framework.Assert;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* A base Activity that is used to build different methods to execute tests inside CtsVerifier.
* i.e. CTS tests, and semi-automated CtsVerifier tests.
*
* This class provides access to the following flow:
* Activity set up
* Execute tests (implemented by sub-classes)
* Activity clean up
*
* Currently the following class structure is available:
* - BaseSensorTestActivity : provides the platform to execute Sensor tests inside
* | CtsVerifier, and logging support
* |
* -- SensorCtsTestActivity : an activity that can be inherited from to wrap a CTS
* | sensor test, and execute it inside CtsVerifier
* | these tests do not require any operator interaction
* |
* -- SensorCtsVerifierTestActivity : an activity that can be inherited to write sensor
* tests that require operator interaction
*/
public abstract class BaseSensorTestActivity
extends PassFailButtons.Activity
implements View.OnClickListener, Runnable, ISensorTestStateContainer {
@Deprecated
protected static final String LOG_TAG = "SensorTest";
protected final Class mTestClass;
private final int mLayoutId;
private final SensorFeaturesDeactivator mSensorFeaturesDeactivator;
private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
private final SensorTestLogger mTestLogger = new SensorTestLogger();
private final ActivityResultMultiplexedLatch mActivityResultMultiplexedLatch =
new ActivityResultMultiplexedLatch();
private final ArrayList<CountDownLatch> mWaitForUserLatches = new ArrayList<CountDownLatch>();
private ScrollView mLogScrollView;
private LinearLayout mLogLayout;
private Button mNextButton;
private Button mPassButton;
private Button mFailButton;
private Button mRetryButton;
private GLSurfaceView mGLSurfaceView;
private boolean mUsingGlSurfaceView;
// Flag for sensor tests with retry.
protected boolean mEnableRetry = false;
// Flag for Retry button appearance.
protected boolean mShouldRetry = false;
// Flag for the last sub-test to show Finish button.
protected boolean mIsLastSubtest = false;
protected int mRetryCount = 0;
/**
* Constructor to be used by subclasses.
*
* @param testClass The class that contains the tests. It is dependant on test executor
* implemented by subclasses.
*/
protected BaseSensorTestActivity(Class testClass) {
this(testClass, R.layout.sensor_test);
}
/**
* Constructor to be used by subclasses. It allows to provide a custom layout for the test UI.
*
* @param testClass The class that contains the tests. It is dependant on test executor
* implemented by subclasses.
* @param layoutId The Id of the layout to use for the test UI. The layout must contain all the
* elements in the base layout {@code R.layout.sensor_test}.
*/
protected BaseSensorTestActivity(Class testClass, int layoutId) {
mTestClass = testClass;
mLayoutId = layoutId;
mSensorFeaturesDeactivator = new SensorFeaturesDeactivator(this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(mLayoutId);
mLogScrollView = (ScrollView) findViewById(R.id.log_scroll_view);
mLogLayout = (LinearLayout) findViewById(R.id.log_layout);
mNextButton = (Button) findViewById(R.id.next_button);
mNextButton.setOnClickListener(this);
mPassButton = (Button) findViewById(R.id.pass_button);
mFailButton = (Button) findViewById(R.id.fail_button);
mGLSurfaceView = (GLSurfaceView) findViewById(R.id.gl_surface_view);
mRetryButton = (Button) findViewById(R.id.retry_button);
mRetryButton.setOnClickListener(this);
updateNextButton(false /*enabled*/);
mExecutorService.execute(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
mExecutorService.shutdownNow();
}
@Override
protected void onPause() {
super.onPause();
if (mUsingGlSurfaceView) {
mGLSurfaceView.onPause();
}
}
@Override
protected void onResume() {
super.onResume();
if (mUsingGlSurfaceView) {
mGLSurfaceView.onResume();
}
}
@Override
public void onClick(View target) {
switch (target.getId()) {
case R.id.next_button:
mShouldRetry = false;
break;
case R.id.retry_button:
mShouldRetry = true;
break;
}
synchronized (mWaitForUserLatches) {
for (CountDownLatch latch : mWaitForUserLatches) {
latch.countDown();
}
mWaitForUserLatches.clear();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
mActivityResultMultiplexedLatch.onActivityResult(requestCode, resultCode);
}
/**
* The main execution {@link Thread}.
*
* This function executes in a background thread, allowing the test run freely behind the
* scenes. It provides the following execution hooks:
* - Activity SetUp/CleanUp (not available in JUnit)
* - executeTests: to implement several execution engines
*/
@Override
public void run() {
long startTimeNs = SystemClock.elapsedRealtimeNanos();
String testName = getTestClassName();
SensorTestDetails testDetails;
try {
mSensorFeaturesDeactivator.requestDeactivationOfFeatures();
testDetails = new SensorTestDetails(testName, SensorTestDetails.ResultCode.PASS);
} catch (Throwable e) {
testDetails = new SensorTestDetails(testName, "DeactivateSensorFeatures", e);
}
SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
if (resultCode == SensorTestDetails.ResultCode.SKIPPED) {
// this is an invalid state at this point of the test setup
throw new IllegalStateException("Deactivation of features cannot skip the test.");
}
if (resultCode == SensorTestDetails.ResultCode.PASS) {
testDetails = executeActivityTests(testName);
}
// we consider all remaining states at this point, because we could have been half way
// deactivating features
try {
mSensorFeaturesDeactivator.requestToRestoreFeatures();
} catch (Throwable e) {
testDetails = new SensorTestDetails(testName, "RestoreSensorFeatures", e);
}
mTestLogger.logTestDetails(testDetails);
mTestLogger.logExecutionTime(startTimeNs);
// because we cannot enforce test failures in several devices, set the test UI so the
// operator can report the result of the test
promptUserToSetResult(testDetails);
}
/**
* A general set up routine. It executes only once before the first test case.
*
* NOTE: implementers must be aware of the interrupted status of the worker thread, and let
* {@link InterruptedException} propagate.
*
* @throws Throwable An exception that denotes the failure of set up. No tests will be executed.
*/
protected void activitySetUp() throws Throwable {}
/**
* A general clean up routine. It executes upon successful execution of {@link #activitySetUp()}
* and after all the test cases.
*
* NOTE: implementers must be aware of the interrupted status of the worker thread, and handle
* it in two cases:
* - let {@link InterruptedException} propagate
* - if it is invoked with the interrupted status, prevent from showing any UI
* @throws Throwable An exception that will be logged and ignored, for ease of implementation
* by subclasses.
*/
protected void activityCleanUp() throws Throwable {}
/**
* Performs the work of executing the tests.
* Sub-classes implementing different execution methods implement this method.
*
* @return A {@link SensorTestDetails} object containing information about the executed tests.
*/
protected abstract SensorTestDetails executeTests() throws InterruptedException;
@Override
public SensorTestLogger getTestLogger() {
return mTestLogger;
}
@Deprecated
protected void appendText(int resId) {
mTestLogger.logInstructions(resId);
}
@Deprecated
protected void appendText(String text) {
TextAppender textAppender = new TextAppender(R.layout.snsr_instruction);
textAppender.setText(text);
textAppender.append();
}
@Deprecated
protected void clearText() {
this.runOnUiThread(new Runnable() {
@Override
public void run() {
mLogLayout.removeAllViews();
}
});
}
/**
* Waits for the operator to acknowledge a requested action.
*
* @param waitMessageResId The action requested to the operator.
*/
protected void waitForUser(int waitMessageResId) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
synchronized (mWaitForUserLatches) {
mWaitForUserLatches.add(latch);
}
mTestLogger.logInstructions(waitMessageResId);
updateNextButton(true);
latch.await();
updateNextButton(false);
}
/**
* Waits for the operator to acknowledge to begin execution.
*/
protected void waitForUserToBegin() throws InterruptedException {
waitForUser(R.string.snsr_wait_to_begin);
}
/**
* Waits for the operator to acknowledge to retry execution.
* If the execution is for the last subtest, will notify user by Finish button.
*/
protected void waitForUserToRetry() throws InterruptedException {
if (mIsLastSubtest) {
waitForUser(R.string.snsr_wait_to_finish);
} else {
waitForUser(R.string.snsr_wait_to_retry);
}
}
/**
* {@inheritDoc}
*/
@Override
public void waitForUserToContinue() throws InterruptedException {
waitForUser(R.string.snsr_wait_for_user);
}
/**
* {@inheritDoc}
*/
@Override
public int executeActivity(String action) throws InterruptedException {
return executeActivity(new Intent(action));
}
/**
* {@inheritDoc}
*/
@Override
public int executeActivity(Intent intent) throws InterruptedException {
ActivityResultMultiplexedLatch.Latch latch = mActivityResultMultiplexedLatch.bindThread();
try {
startActivityForResult(intent, latch.getRequestCode());
} catch (ActivityNotFoundException e) {
// handle exception gracefully
// Among all defined activity results, RESULT_CANCELED offers the semantic closest to
// represent absent setting activity.
return RESULT_CANCELED;
}
return latch.await();
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasSystemFeature(String feature) {
PackageManager pm = getPackageManager();
return pm.hasSystemFeature(feature);
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasActivity(String action) {
PackageManager pm = getPackageManager();
return pm.resolveActivity(new Intent(action), PackageManager.MATCH_DEFAULT_ONLY) != null;
}
/**
* Initializes and shows the {@link GLSurfaceView} available to tests.
* NOTE: initialization can be performed only once, usually inside {@link #activitySetUp()}.
*/
protected void initializeGlSurfaceView(final GLSurfaceView.Renderer renderer) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mGLSurfaceView.setVisibility(View.VISIBLE);
mGLSurfaceView.setRenderer(renderer);
mUsingGlSurfaceView = true;
}
});
}
/**
* Closes and hides the {@link GLSurfaceView}.
*/
protected void closeGlSurfaceView() {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!mUsingGlSurfaceView) {
return;
}
mGLSurfaceView.setVisibility(View.GONE);
mGLSurfaceView.onPause();
mUsingGlSurfaceView = false;
}
});
}
/**
* Plays a (default) sound as a notification for the operator.
*/
protected void playSound() throws InterruptedException {
MediaPlayer player = MediaPlayer.create(this, Settings.System.DEFAULT_NOTIFICATION_URI);
if (player == null) {
Log.e(LOG_TAG, "MediaPlayer unavailable.");
return;
}
player.start();
try {
Thread.sleep(500);
} finally {
player.stop();
}
}
/**
* Makes the device vibrate for the given amount of time.
*/
protected void vibrate(int timeInMs) {
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
vibrator.vibrate(timeInMs);
}
/**
* Makes the device vibrate following the given pattern.
* See {@link Vibrator#vibrate(long[], int)} for more information.
*/
protected void vibrate(long[] pattern) {
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
vibrator.vibrate(pattern, -1);
}
// TODO: move to sensor assertions
protected String assertTimestampSynchronization(
long eventTimestamp,
long receivedTimestamp,
long deltaThreshold,
String sensorName) {
long timestampDelta = Math.abs(eventTimestamp - receivedTimestamp);
String timestampMessage = getString(
R.string.snsr_event_time,
receivedTimestamp,
eventTimestamp,
timestampDelta,
deltaThreshold,
sensorName);
Assert.assertTrue(timestampMessage, timestampDelta < deltaThreshold);
return timestampMessage;
}
protected String getTestClassName() {
if (mTestClass == null) {
return "<unknown>";
}
return mTestClass.getName();
}
protected void setLogScrollViewListener(View.OnTouchListener listener) {
mLogScrollView.setOnTouchListener(listener);
}
private void setTestResult(SensorTestDetails testDetails) {
// the name here, must be the Activity's name because it is what CtsVerifier expects
String name = super.getClass().getName();
String summary = mTestLogger.getOverallSummary();
SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
switch(resultCode) {
case SKIPPED:
TestResult.setPassedResult(this, name, summary);
break;
case PASS:
TestResult.setPassedResult(this, name, summary);
break;
case FAIL:
TestResult.setFailedResult(this, name, summary);
break;
case INTERRUPTED:
// do not set a result, just return so the test can complete
break;
default:
throw new IllegalStateException("Unknown ResultCode: " + resultCode);
}
}
private SensorTestDetails executeActivityTests(String testName) {
SensorTestDetails testDetails;
try {
activitySetUp();
testDetails = new SensorTestDetails(testName, SensorTestDetails.ResultCode.PASS);
} catch (Throwable e) {
testDetails = new SensorTestDetails(testName, "ActivitySetUp", e);
}
SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
if (resultCode == SensorTestDetails.ResultCode.PASS) {
// TODO: implement execution filters:
// - execute all tests and report results officially
// - execute single test or failed tests only
try {
testDetails = executeTests();
} catch (Throwable e) {
// we catch and continue because we have to guarantee a proper clean-up sequence
testDetails = new SensorTestDetails(testName, "TestExecution", e);
}
}
// clean-up executes for all states, even on SKIPPED and INTERRUPTED there might be some
// intermediate state that needs to be taken care of
try {
activityCleanUp();
} catch (Throwable e) {
testDetails = new SensorTestDetails(testName, "ActivityCleanUp", e);
}
return testDetails;
}
private void promptUserToSetResult(SensorTestDetails testDetails) {
SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
if (resultCode == SensorTestDetails.ResultCode.FAIL) {
mTestLogger.logInstructions(R.string.snsr_test_complete_with_errors);
enableTestResultButton(
mFailButton,
R.string.fail_button_text,
testDetails.cloneAndChangeResultCode(SensorTestDetails.ResultCode.FAIL));
} else if (resultCode != SensorTestDetails.ResultCode.INTERRUPTED) {
mTestLogger.logInstructions(R.string.snsr_test_complete);
enableTestResultButton(
mPassButton,
R.string.pass_button_text,
testDetails.cloneAndChangeResultCode(SensorTestDetails.ResultCode.PASS));
}
}
private void updateNextButton(final boolean enabled) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mNextButton.setText(getNextButtonText());
updateRetryButton(enabled);
mNextButton.setEnabled(enabled);
}
});
}
/**
* Get the text for next button.
* During retry, next button text is changed to notify users.
*/
private int getNextButtonText() {
int nextButtonText = R.string.next_button_text;
if (mShouldRetry) {
if (mIsLastSubtest){
nextButtonText = R.string.finish_button_text;
} else {
nextButtonText = R.string.fail_and_next_button_text;
}
}
return nextButtonText;
}
/**
* Update the retry button status.
* During retry, show retry execution count. If not to retry, make retry button invisible.
*
* @param enabled The status of button.
*/
private void updateRetryButton(boolean enabled) {
String showRetryCount = String.format(
"%s (%d)", getResources().getText(R.string.retry_button_text), mRetryCount);
if (mShouldRetry) {
mRetryButton.setText(showRetryCount);
mRetryButton.setVisibility(View.VISIBLE);
mRetryButton.setEnabled(enabled);
} else {
mRetryButton.setVisibility(View.GONE);
}
}
private void enableTestResultButton(
final Button button,
final int textResId,
final SensorTestDetails testDetails) {
final View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
setTestResult(testDetails);
finish();
}
};
runOnUiThread(new Runnable() {
@Override
public void run() {
mNextButton.setVisibility(View.GONE);
button.setText(textResId);
button.setOnClickListener(listener);
button.setVisibility(View.VISIBLE);
}
});
}
// a logger available until sensor reporting is in place
public class SensorTestLogger {
private static final String SUMMARY_SEPARATOR = " | ";
private final StringBuilder mOverallSummaryBuilder = new StringBuilder("\n");
public void logCustomView(View view) {
new ViewAppender(view).append();
}
void logTestStart(String testName) {
// TODO: log the sensor information and expected execution time of each test
TextAppender textAppender = new TextAppender(R.layout.snsr_test_title);
textAppender.setText(testName);
textAppender.append();
}
public void logInstructions(int instructionsResId, Object ... params) {
TextAppender textAppender = new TextAppender(R.layout.snsr_instruction);
textAppender.setText(getString(instructionsResId, params));
textAppender.append();
}
public void logMessage(int messageResId, Object ... params) {
TextAppender textAppender = new TextAppender(R.layout.snsr_message);
textAppender.setText(getString(messageResId, params));
textAppender.append();
}
public void logWaitForSound() {
logInstructions(R.string.snsr_test_play_sound);
}
public void logTestDetails(SensorTestDetails testDetails) {
String name = testDetails.getName();
String summary = testDetails.getSummary();
SensorTestDetails.ResultCode resultCode = testDetails.getResultCode();
switch (resultCode) {
case SKIPPED:
logTestSkip(name, summary);
break;
case PASS:
mShouldRetry = false;
logTestPass(name, summary);
break;
case FAIL:
logTestFail(name, summary);
break;
case INTERRUPTED:
// do nothing, the test was interrupted so do we
break;
default:
throw new IllegalStateException("Unknown ResultCode: " + resultCode);
}
}
void logTestPass(String testName, String testSummary) {
testSummary = getValidTestSummary(testSummary, R.string.snsr_test_pass);
logTestEnd(R.layout.snsr_success, testSummary);
Log.d(LOG_TAG, testSummary);
saveResult(testName, SensorTestDetails.ResultCode.PASS, testSummary);
}
public void logTestFail(String testName, String testSummary) {
testSummary = getValidTestSummary(testSummary, R.string.snsr_test_fail);
logTestEnd(R.layout.snsr_error, testSummary);
Log.e(LOG_TAG, testSummary);
saveResult(testName, SensorTestDetails.ResultCode.FAIL, testSummary);
}
void logTestSkip(String testName, String testSummary) {
testSummary = getValidTestSummary(testSummary, R.string.snsr_test_skipped);
logTestEnd(R.layout.snsr_warning, testSummary);
Log.i(LOG_TAG, testSummary);
saveResult(testName, SensorTestDetails.ResultCode.SKIPPED, testSummary);
}
String getOverallSummary() {
return mOverallSummaryBuilder.toString();
}
void logExecutionTime(long startTimeNs) {
if (Thread.currentThread().isInterrupted()) {
return;
}
long executionTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
long executionTimeSec = TimeUnit.NANOSECONDS.toSeconds(executionTimeNs);
// TODO: find a way to format times with nanosecond accuracy and longer than 24hrs
String formattedElapsedTime = DateUtils.formatElapsedTime(executionTimeSec);
logMessage(R.string.snsr_execution_time, formattedElapsedTime);
}
private void logTestEnd(int textViewResId, String testSummary) {
TextAppender textAppender = new TextAppender(textViewResId);
textAppender.setText(testSummary);
textAppender.append();
}
private String getValidTestSummary(String testSummary, int defaultSummaryResId) {
if (TextUtils.isEmpty(testSummary)) {
return getString(defaultSummaryResId);
}
return testSummary;
}
private void saveResult(
String testName,
SensorTestDetails.ResultCode resultCode,
String summary) {
mOverallSummaryBuilder.append(testName);
mOverallSummaryBuilder.append(SUMMARY_SEPARATOR);
mOverallSummaryBuilder.append(resultCode.name());
mOverallSummaryBuilder.append(SUMMARY_SEPARATOR);
mOverallSummaryBuilder.append(summary);
mOverallSummaryBuilder.append("\n");
}
}
private class ViewAppender {
protected final View mView;
public ViewAppender(View view) {
mView = view;
}
public void append() {
runOnUiThread(new Runnable() {
@Override
public void run() {
mLogLayout.addView(mView);
mLogScrollView.post(new Runnable() {
@Override
public void run() {
mLogScrollView.fullScroll(View.FOCUS_DOWN);
}
});
}
});
}
}
private class TextAppender extends ViewAppender{
private final TextView mTextView;
public TextAppender(int textViewResId) {
super(getLayoutInflater().inflate(textViewResId, null /* viewGroup */));
mTextView = (TextView) mView;
}
public void setText(String text) {
mTextView.setText(text);
}
public void setText(int textResId) {
mTextView.setText(textResId);
}
}
}