blob: f61873fa9d25e1a6e9cef780810c55460b2dcd91 [file] [log] [blame]
/*
* Copyright 2017 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.mobileer.oboetester;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
/**
* Base class for other Activities.
*/
abstract class TestAudioActivity extends Activity {
public static final String TAG = "OboeTester";
protected static final int FADER_PROGRESS_MAX = 1000;
private static final int INTENT_TEST_DELAY_MILLIS = 1100;
public static final int AUDIO_STATE_OPEN = 0;
public static final int AUDIO_STATE_STARTED = 1;
public static final int AUDIO_STATE_PAUSED = 2;
public static final int AUDIO_STATE_STOPPED = 3;
public static final int AUDIO_STATE_CLOSING = 4;
public static final int AUDIO_STATE_CLOSED = 5;
public static final int COLOR_ACTIVE = 0xFFD0D0A0;
public static final int COLOR_IDLE = 0xFFD0D0D0;
// Pass the activity index to native so it can know how to respond to the start and stop calls.
// WARNING - must match definitions in NativeAudioContext.h ActivityType
public static final int ACTIVITY_TEST_OUTPUT = 0;
public static final int ACTIVITY_TEST_INPUT = 1;
public static final int ACTIVITY_TAP_TO_TONE = 2;
public static final int ACTIVITY_RECORD_PLAY = 3;
public static final int ACTIVITY_ECHO = 4;
public static final int ACTIVITY_RT_LATENCY = 5;
public static final int ACTIVITY_GLITCHES = 6;
public static final int ACTIVITY_TEST_DISCONNECT = 7;
public static final int ACTIVITY_DATA_PATHS = 8;
private static final int MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE = 1001;
private int mAudioState = AUDIO_STATE_CLOSED;
protected ArrayList<StreamContext> mStreamContexts;
private Button mOpenButton;
private Button mStartButton;
private Button mPauseButton;
private Button mStopButton;
private Button mCloseButton;
private MyStreamSniffer mStreamSniffer;
private CheckBox mCallbackReturnStopBox;
private int mSampleRate;
private int mSingleTestIndex = -1;
private static boolean mBackgroundEnabled;
protected Bundle mBundleFromIntent;
protected boolean mTestRunningByIntent;
protected String mResultFileName;
private String mTestResults;
public String getTestName() {
return "TestAudio";
}
public static class StreamContext {
StreamConfigurationView configurationView;
AudioStreamTester tester;
boolean isInput() {
return tester.getCurrentAudioStream().isInput();
}
}
// Periodically query the status of the streams.
protected class MyStreamSniffer {
public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150;
public static final int SNIFFER_UPDATE_DELAY_MSEC = 300;
private Handler mHandler;
// Display status info for the stream.
private Runnable runnableCode = new Runnable() {
@Override
public void run() {
boolean streamClosed = false;
boolean gotViews = false;
for (StreamContext streamContext : mStreamContexts) {
AudioStreamBase.StreamStatus status = streamContext.tester.getCurrentAudioStream().getStreamStatus();
AudioStreamBase.DoubleStatistics latencyStatistics =
streamContext.tester.getCurrentAudioStream().getLatencyStatistics();
if (streamContext.configurationView != null) {
// Handler runs this on the main UI thread.
int framesPerBurst = streamContext.tester.getCurrentAudioStream().getFramesPerBurst();
status.framesPerCallback = getFramesPerCallback();
String msg = "";
msg += "timestamp.latency = " + latencyStatistics.dump() + "\n";
msg += status.dump(framesPerBurst);
streamContext.configurationView.setStatusText(msg);
updateStreamDisplay();
gotViews = true;
}
streamClosed = streamClosed || (status.state >= 12);
}
if (streamClosed) {
onStreamClosed();
} else {
// Repeat this runnable code block again.
if (gotViews) {
mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC);
}
}
}
};
private void startStreamSniffer() {
stopStreamSniffer();
mHandler = new Handler(Looper.getMainLooper());
// Start the initial runnable task by posting through the handler
mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC);
}
private void stopStreamSniffer() {
if (mHandler != null) {
mHandler.removeCallbacks(runnableCode);
}
}
}
public static void setBackgroundEnabled(boolean enabled) {
mBackgroundEnabled = enabled;
}
public static boolean isBackgroundEnabled() {
return mBackgroundEnabled;
}
public void onStreamClosed() {
}
protected abstract void inflateActivity();
void updateStreamDisplay() {
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
inflateActivity();
findAudioCommon();
mBundleFromIntent = getIntent().getExtras();
}
@Override
public void onNewIntent(Intent intent) {
mBundleFromIntent = intent.getExtras();
}
public boolean isTestConfiguredUsingBundle() {
return mBundleFromIntent != null;
}
public void hideSettingsViews() {
for (StreamContext streamContext : mStreamContexts) {
if (streamContext.configurationView != null) {
streamContext.configurationView.hideSettingsView();
}
}
}
abstract int getActivityType();
public void setSingleTestIndex(int testIndex) {
mSingleTestIndex = testIndex;
}
public int getSingleTestIndex() {
return mSingleTestIndex;
}
@Override
protected void onStart() {
super.onStart();
resetConfiguration();
setActivityType(getActivityType());
}
protected void resetConfiguration() {
}
@Override
public void onResume() {
super.onResume();
if (mBundleFromIntent != null) {
processBundleFromIntent();
}
}
private void setVolumeFromIntent() {
float normalizedVolume = IntentBasedTestSupport.getNormalizedVolumeFromBundle(mBundleFromIntent);
if (normalizedVolume >= 0.0) {
int streamType = IntentBasedTestSupport.getVolumeStreamTypeFromBundle(mBundleFromIntent);
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int maxVolume = audioManager.getStreamMaxVolume(streamType);
int requestedVolume = (int) (maxVolume * normalizedVolume);
audioManager.setStreamVolume(streamType, requestedVolume, 0);
}
}
private void processBundleFromIntent() {
if (mTestRunningByIntent) {
return;
}
// Delay the test start to avoid race conditions. See Oboe Issue #1533
mTestRunningByIntent = true;
Handler handler = new Handler(Looper.getMainLooper()); // UI thread
handler.postDelayed(new DelayedTestByIntentRunnable(),
INTENT_TEST_DELAY_MILLIS); // Delay long enough to get past the onStop() call!
}
private class DelayedTestByIntentRunnable implements Runnable {
@Override
public void run() {
try {
mResultFileName = mBundleFromIntent.getString(IntentBasedTestSupport.KEY_FILE_NAME);
setVolumeFromIntent();
startTestUsingBundle();
} catch( Exception e) {
showErrorToast(e.getMessage());
}
}
}
public void startTestUsingBundle() {
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onStop() {
if (!isBackgroundEnabled()) {
Log.i(TAG, "onStop() called so stop the test =========================");
onStopTest();
}
super.onStop();
}
@Override
protected void onDestroy() {
if (isBackgroundEnabled()) {
Log.i(TAG, "onDestroy() called so stop the test =========================");
onStopTest();
}
mAudioState = AUDIO_STATE_CLOSED;
super.onDestroy();
}
protected void updateEnabledWidgets() {
if (mOpenButton != null) {
mOpenButton.setBackgroundColor(mAudioState == AUDIO_STATE_OPEN ? COLOR_ACTIVE : COLOR_IDLE);
mStartButton.setBackgroundColor(mAudioState == AUDIO_STATE_STARTED ? COLOR_ACTIVE : COLOR_IDLE);
mPauseButton.setBackgroundColor(mAudioState == AUDIO_STATE_PAUSED ? COLOR_ACTIVE : COLOR_IDLE);
mStopButton.setBackgroundColor(mAudioState == AUDIO_STATE_STOPPED ? COLOR_ACTIVE : COLOR_IDLE);
mCloseButton.setBackgroundColor(mAudioState == AUDIO_STATE_CLOSED ? COLOR_ACTIVE : COLOR_IDLE);
}
setConfigViewsEnabled(mAudioState == AUDIO_STATE_CLOSED);
}
private void setConfigViewsEnabled(boolean b) {
for (StreamContext streamContext : mStreamContexts) {
if (streamContext.configurationView != null) {
streamContext.configurationView.setChildrenEnabled(b);
}
}
}
private void applyConfigurationViewsToModels() {
for (StreamContext streamContext : mStreamContexts) {
if (streamContext.configurationView != null) {
streamContext.configurationView.applyToModel(streamContext.tester.requestedConfiguration);
}
}
}
abstract boolean isOutput();
public void clearStreamContexts() {
mStreamContexts.clear();
}
public StreamContext addOutputStreamContext() {
StreamContext streamContext = new StreamContext();
streamContext.tester = AudioOutputTester.getInstance();
streamContext.configurationView = (StreamConfigurationView)
findViewById(R.id.outputStreamConfiguration);
if (streamContext.configurationView == null) {
streamContext.configurationView = (StreamConfigurationView)
findViewById(R.id.streamConfiguration);
}
if (streamContext.configurationView != null) {
streamContext.configurationView.setOutput(true);
}
mStreamContexts.add(streamContext);
return streamContext;
}
public AudioOutputTester addAudioOutputTester() {
StreamContext streamContext = addOutputStreamContext();
return (AudioOutputTester) streamContext.tester;
}
public StreamContext addInputStreamContext() {
StreamContext streamContext = new StreamContext();
streamContext.tester = AudioInputTester.getInstance();
streamContext.configurationView = (StreamConfigurationView)
findViewById(R.id.inputStreamConfiguration);
if (streamContext.configurationView == null) {
streamContext.configurationView = (StreamConfigurationView)
findViewById(R.id.streamConfiguration);
}
if (streamContext.configurationView != null) {
streamContext.configurationView.setOutput(false);
}
streamContext.tester = AudioInputTester.getInstance();
mStreamContexts.add(streamContext);
return streamContext;
}
public AudioInputTester addAudioInputTester() {
StreamContext streamContext = addInputStreamContext();
return (AudioInputTester) streamContext.tester;
}
void updateStreamConfigurationViews() {
for (StreamContext streamContext : mStreamContexts) {
if (streamContext.configurationView != null) {
streamContext.configurationView.updateDisplay(streamContext.tester.actualConfiguration);
}
}
}
StreamContext getFirstInputStreamContext() {
for (StreamContext streamContext : mStreamContexts) {
if (streamContext.isInput())
return streamContext;
}
return null;
}
StreamContext getFirstOutputStreamContext() {
for (StreamContext streamContext : mStreamContexts) {
if (!streamContext.isInput())
return streamContext;
}
return null;
}
protected void findAudioCommon() {
mOpenButton = (Button) findViewById(R.id.button_open);
if (mOpenButton != null) {
mStartButton = (Button) findViewById(R.id.button_start);
mPauseButton = (Button) findViewById(R.id.button_pause);
mStopButton = (Button) findViewById(R.id.button_stop);
mCloseButton = (Button) findViewById(R.id.button_close);
}
mStreamContexts = new ArrayList<StreamContext>();
mCallbackReturnStopBox = (CheckBox) findViewById(R.id.callbackReturnStop);
if (mCallbackReturnStopBox != null) {
mCallbackReturnStopBox.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
OboeAudioStream.setCallbackReturnStop(mCallbackReturnStopBox.isChecked());
}
});
}
OboeAudioStream.setCallbackReturnStop(false);
mStreamSniffer = new MyStreamSniffer();
}
private void updateNativeAudioParameters() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
String text = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
int audioManagerSampleRate = Integer.parseInt(text);
text = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
int audioManagerFramesPerBurst = Integer.parseInt(text);
setDefaultAudioValues(audioManagerSampleRate, audioManagerFramesPerBurst);
}
}
protected void showErrorToast(String message) {
String text = "Error: " + message;
Log.e(TAG, text);
showToast(text);
}
protected void showToast(final String message) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(TestAudioActivity.this,
message,
Toast.LENGTH_SHORT).show();
}
});
}
public void openAudio(View view) {
try {
openAudio();
} catch (Exception e) {
showErrorToast(e.getMessage());
}
}
public void startAudio(View view) {
Log.i(TAG, "startAudio() called =======================================");
try {
startAudio();
} catch (Exception e) {
showErrorToast(e.getMessage());
}
keepScreenOn(true);
}
protected void keepScreenOn(boolean on) {
if (on) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
public void stopAudio(View view) {
stopAudio();
keepScreenOn(false);
}
public void pauseAudio(View view) {
pauseAudio();
keepScreenOn(false);
}
public void closeAudio(View view) {
closeAudio();
}
public int getSampleRate() {
return mSampleRate;
}
public void openAudio() throws IOException {
closeAudio();
updateNativeAudioParameters();
if (!isTestConfiguredUsingBundle()) {
applyConfigurationViewsToModels();
}
int sampleRate = 0;
// Open output streams then open input streams.
// This is so that the capacity of input stream can be expanded to
// match the burst size of the output for full duplex.
for (StreamContext streamContext : mStreamContexts) {
if (!streamContext.isInput()) {
openStreamContext(streamContext);
int streamSampleRate = streamContext.tester.actualConfiguration.getSampleRate();
if (sampleRate == 0) {
sampleRate = streamSampleRate;
}
}
}
for (StreamContext streamContext : mStreamContexts) {
if (streamContext.isInput()) {
if (sampleRate != 0) {
streamContext.tester.requestedConfiguration.setSampleRate(sampleRate);
}
openStreamContext(streamContext);
}
}
updateEnabledWidgets();
mStreamSniffer.startStreamSniffer();
}
/**
* @param deviceId
* @return true if the device is TYPE_BLUETOOTH_SCO
*/
boolean isScoDevice(int deviceId) {
if (deviceId == 0) return false; // Unspecified
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
final AudioDeviceInfo[] devices = audioManager.getDevices(
AudioManager.GET_DEVICES_INPUTS | AudioManager.GET_DEVICES_OUTPUTS);
for (AudioDeviceInfo device : devices) {
if (device.getId() == deviceId) {
return device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO;
}
}
return false;
}
private void openStreamContext(StreamContext streamContext) throws IOException {
StreamConfiguration requestedConfig = streamContext.tester.requestedConfiguration;
StreamConfiguration actualConfig = streamContext.tester.actualConfiguration;
streamContext.tester.open(); // OPEN the stream
mSampleRate = actualConfig.getSampleRate();
mAudioState = AUDIO_STATE_OPEN;
int sessionId = actualConfig.getSessionId();
if (streamContext.configurationView != null) {
if (sessionId > 0) {
try {
streamContext.configurationView.setupEffects(sessionId);
} catch (Exception e) {
showErrorToast(e.getMessage());
}
}
streamContext.configurationView.updateDisplay(streamContext.tester.actualConfiguration);
}
}
// Native methods
private native int startNative();
private native int pauseNative();
private native int stopNative();
protected native void setActivityType(int activityType);
private native int getFramesPerCallback();
private static native void setDefaultAudioValues(int audioManagerSampleRate, int audioManagerFramesPerBurst);
public void startAudio() throws IOException {
Log.i(TAG, "startAudio() called =========================");
int result = startNative();
if (result < 0) {
showErrorToast("Start failed with " + result);
throw new IOException("startNative returned " + result);
} else {
for (StreamContext streamContext : mStreamContexts) {
StreamConfigurationView configView = streamContext.configurationView;
if (configView != null) {
configView.updateDisplay(streamContext.tester.actualConfiguration);
}
}
mAudioState = AUDIO_STATE_STARTED;
updateEnabledWidgets();
}
}
protected void toastPauseError(int result) {
showErrorToast("Pause failed with " + result);
}
public void pauseAudio() {
int result = pauseNative();
if (result < 0) {
toastPauseError(result);
} else {
mAudioState = AUDIO_STATE_PAUSED;
updateEnabledWidgets();
}
}
public void stopAudio() {
int result = stopNative();
if (result < 0) {
showErrorToast("Stop failed with " + result);
} else {
mAudioState = AUDIO_STATE_STOPPED;
updateEnabledWidgets();
}
}
public void runTest() {
}
public void saveIntentLog() {
}
// This should only be called from UI events such as onStop or a button press.
public void onStopTest() {
stopTest();
}
public void stopTest() {
stopAudio();
closeAudio();
}
public void stopAudioQuiet() {
stopNative();
mAudioState = AUDIO_STATE_STOPPED;
updateEnabledWidgets();
}
// Make synchronized so we don't close from two streams at the same time.
public synchronized void closeAudio() {
if (mAudioState >= AUDIO_STATE_CLOSING) {
Log.d(TAG, "closeAudio() already closing");
return;
}
mAudioState = AUDIO_STATE_CLOSING;
mStreamSniffer.stopStreamSniffer();
// Close output streams first because legacy callbacks may still be active
// and an output stream may be calling the input stream.
for (StreamContext streamContext : mStreamContexts) {
if (!streamContext.isInput()) {
streamContext.tester.close();
}
}
for (StreamContext streamContext : mStreamContexts) {
if (streamContext.isInput()) {
streamContext.tester.close();
}
}
mAudioState = AUDIO_STATE_CLOSED;
updateEnabledWidgets();
}
void startBluetoothSco() {
AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
myAudioMgr.startBluetoothSco();
}
void stopBluetoothSco() {
AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
myAudioMgr.stopBluetoothSco();
}
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions,
int[] grantResults) {
if (MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE != requestCode) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
return;
}
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
writeTestResult(mTestResults);
} else {
showToast("Writing external storage needed for test results.");
}
}
@NonNull
protected String getCommonTestReport() {
StringBuffer report = new StringBuffer();
// Add some extra information for the remote tester.
report.append("build.fingerprint = " + Build.FINGERPRINT + "\n");
try {
PackageInfo pinfo = getPackageManager().getPackageInfo(getPackageName(), 0);
report.append(String.format("test.version = %s\n", pinfo.versionName));
report.append(String.format("test.version.code = %d\n", pinfo.versionCode));
} catch (PackageManager.NameNotFoundException e) {
}
report.append("time.millis = " + System.currentTimeMillis() + "\n");
if (mStreamContexts.size() == 0) {
report.append("ERROR: no active streams" + "\n");
} else {
StreamContext streamContext = mStreamContexts.get(0);
AudioStreamTester streamTester = streamContext.tester;
report.append(streamTester.actualConfiguration.dump());
AudioStreamBase.StreamStatus status = streamTester.getCurrentAudioStream().getStreamStatus();
AudioStreamBase.DoubleStatistics latencyStatistics =
streamTester.getCurrentAudioStream().getLatencyStatistics();
int framesPerBurst = streamTester.getCurrentAudioStream().getFramesPerBurst();
status.framesPerCallback = getFramesPerCallback();
report.append("timestamp.latency = " + latencyStatistics.dump() + "\n");
report.append(status.dump(framesPerBurst));
}
return report.toString();
}
void writeTestResultIfPermitted(String resultString) {
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
mTestResults = resultString;
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE);
} else {
// Permission has already been granted
writeTestResult(resultString);
}
}
void maybeWriteTestResult(String resultString) {
if (mResultFileName != null) {
writeTestResultIfPermitted(resultString);
};
}
// Run this in a background thread.
void writeTestResult(String resultString) {
File resultFile = new File(mResultFileName);
Writer writer = null;
try {
writer = new OutputStreamWriter(new FileOutputStream(resultFile));
writer.write(resultString);
} catch (
IOException e) {
e.printStackTrace();
showErrorToast(" writing result file. " + e.getMessage());
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
mResultFileName = null;
}
}