blob: 8ee34464a2b12c9e8257ba09c816ffa51cc3ce4b [file] [log] [blame]
/*
* Copyright (C) 2015 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.audio;
import android.app.AlertDialog;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import com.android.compatibility.common.util.ResultType;
import com.android.compatibility.common.util.ResultUnit;
import com.android.cts.verifier.audio.audiolib.AudioSystemFlags;
import com.android.cts.verifier.audio.audiolib.StatUtils;
import com.android.cts.verifier.audio.audiolib.AudioUtils;
import com.android.cts.verifier.CtsVerifierReportLog;
import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R;
import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
import static com.android.cts.verifier.TestListAdapter.setTestNameSuffix;
/**
* CtsVerifier Audio Loopback Latency Test
*/
public class AudioLoopbackLatencyActivity extends PassFailButtons.Activity {
private static final String TAG = "AudioLoopbackLatencyActivity";
// JNI load
static {
try {
System.loadLibrary("audioloopback_jni");
} catch (UnsatisfiedLinkError e) {
Log.e(TAG, "Error loading Audio Loopback JNI library");
Log.e(TAG, "e: " + e);
e.printStackTrace();
}
/* TODO: gracefully fail/notify if the library can't be loaded */
}
protected AudioManager mAudioManager;
// UI
TextView mInputDeviceTxt;
TextView mOutputDeviceTxt;
TextView mAudioLevelText;
SeekBar mAudioLevelSeekbar;
TextView mTestPathTxt;
TextView mResultText;
ProgressBar mProgressBar;
int mMaxLevel;
OnBtnClickListener mBtnClickListener = new OnBtnClickListener();
protected Button mTestButton;
String mYesString;
String mNoString;
// These flags determine the maximum allowed latency
private boolean mClaimsProAudio;
private boolean mClaimsOutput;
private boolean mClaimsInput;
// Useful info
private boolean mSupportsMMAP = AudioUtils.isMMapSupported();
private boolean mSupportsMMAPExclusive = AudioUtils.isMMapExclusiveSupported();
// Peripheral(s)
boolean mIsPeripheralAttached; // CDD ProAudio section C-1-3
AudioDeviceInfo mOutputDevInfo;
AudioDeviceInfo mInputDevInfo;
protected static final int TESTPERIPHERAL_INVALID = -1;
protected static final int TESTPERIPHERAL_NONE = 0;
protected static final int TESTPERIPHERAL_ANALOG_JACK = 1;
protected static final int TESTPERIPHERAL_USB = 2;
protected static final int TESTPERIPHERAL_DEVICE = 3; // device speaker + mic
protected int mTestPeripheral = TESTPERIPHERAL_NONE;
// Loopback Logic
NativeAnalyzerThread mNativeAnalyzerThread = null;
protected static final int NUM_TEST_PHASES = 5;
protected int mTestPhase = 0;
protected double[] mLatencyMillis = new double[NUM_TEST_PHASES];
protected double[] mConfidence = new double[NUM_TEST_PHASES];
protected double mMeanLatencyMillis;
protected double mMeanAbsoluteDeviation;
protected double mMeanConfidence;
protected static final double CONFIDENCE_THRESHOLD = 0.6;
// impossibly low latencies (indicating something in the test went wrong).
protected static final float EPSILON = 1.0f;
protected static final double PROAUDIO_RECOMMENDED_LATENCY_MS = 20.0;
protected static final double PROAUDIO_RECOMMENDED_USB_LATENCY_MS = 25.0;
protected static final double PROAUDIO_MUST_LATENCY_MS = 20.0;
protected static final double BASIC_RECOMMENDED_LATENCY_MS = 50.0;
protected static final double BASIC_MUST_LATENCY_MS = 800.0;
protected double mMustLatency;
protected double mRecommendedLatency;
// The audio stream callback threads should stop and close
// in less than a few hundred msec. This is a generous timeout value.
private static final int STOP_TEST_TIMEOUT_MSEC = 2 * 1000;
//
// Common UI Handling
//
private void connectLoopbackUI() {
// Connected Device
mInputDeviceTxt = ((TextView)findViewById(R.id.audioLoopbackInputLbl));
mOutputDeviceTxt = ((TextView)findViewById(R.id.audioLoopbackOutputLbl));
mAudioLevelText = (TextView)findViewById(R.id.audio_loopback_level_text);
mAudioLevelSeekbar = (SeekBar)findViewById(R.id.audio_loopback_level_seekbar);
mMaxLevel = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
mAudioLevelSeekbar.setMax(mMaxLevel);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (int)(0.7 * mMaxLevel), 0);
refreshLevel();
mAudioLevelSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
progress, 0);
Log.i(TAG,"Level set to: " + progress);
refreshLevel();
}
});
mResultText = (TextView)findViewById(R.id.audio_loopback_results_text);
mProgressBar = (ProgressBar)findViewById(R.id.audio_loopback_progress_bar);
showWait(false);
}
//
// Peripheral Connection Logic
//
protected void scanPeripheralList(AudioDeviceInfo[] devices) {
// CDD Section C-1-3: USB port, host-mode support
// Can't just use the first record because then we will only get
// Source OR sink, not both even on devices that are both.
mOutputDevInfo = null;
mInputDevInfo = null;
// Any valid peripherals
// Do we leave in the Headset test to support a USB-Dongle?
for (AudioDeviceInfo devInfo : devices) {
if (devInfo.getType() == AudioDeviceInfo.TYPE_USB_DEVICE || // USB Peripheral
devInfo.getType() == AudioDeviceInfo.TYPE_USB_HEADSET || // USB dongle+LBPlug
devInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET || // Loopback Plug?
devInfo.getType() == AudioDeviceInfo.TYPE_AUX_LINE) { // Aux-cable loopback?
if (devInfo.isSink()) {
mOutputDevInfo = devInfo;
}
if (devInfo.isSource()) {
mInputDevInfo = devInfo;
}
} else {
handleDeviceConnection(devInfo);
}
}
// need BOTH input and output to test
mIsPeripheralAttached = mOutputDevInfo != null && mInputDevInfo != null;
calculateTestPeripheral();
showConnectedAudioPeripheral();
calculateLatencyThresholds();
displayLatencyThresholds();
}
protected void handleDeviceConnection(AudioDeviceInfo deviceInfo) {
// NOP
}
private class ConnectListener extends AudioDeviceCallback {
/*package*/ ConnectListener() {}
//
// AudioDevicesManager.OnDeviceConnectionListener
//
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
scanPeripheralList(mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL));
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
scanPeripheralList(mAudioManager.getDevices(AudioManager.GET_DEVICES_ALL));
}
}
private void calculateTestPeripheral() {
if (!mIsPeripheralAttached) {
if ((mOutputDevInfo != null && mInputDevInfo == null)
|| (mOutputDevInfo == null && mInputDevInfo != null)) {
mTestPeripheral = TESTPERIPHERAL_INVALID;
} else {
mTestPeripheral = TESTPERIPHERAL_DEVICE;
}
} else if (!areIODevicesOnePeripheral()) {
mTestPeripheral = TESTPERIPHERAL_INVALID;
} else if (mInputDevInfo.getType() == AudioDeviceInfo.TYPE_USB_DEVICE ||
mInputDevInfo.getType() == AudioDeviceInfo.TYPE_USB_HEADSET) {
mTestPeripheral = TESTPERIPHERAL_USB;
} else if (mInputDevInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
mInputDevInfo.getType() == AudioDeviceInfo.TYPE_AUX_LINE) {
mTestPeripheral = TESTPERIPHERAL_ANALOG_JACK;
} else {
// Huh?
Log.e(TAG, "No valid peripheral found!?");
mTestPeripheral = TESTPERIPHERAL_NONE;
}
}
private boolean isPeripheralValidForTest() {
return mTestPeripheral == TESTPERIPHERAL_ANALOG_JACK
|| mTestPeripheral == TESTPERIPHERAL_USB;
}
private void showConnectedAudioPeripheral() {
mInputDeviceTxt.setText(
mInputDevInfo != null ? mInputDevInfo.getProductName().toString()
: "Not connected");
mOutputDeviceTxt.setText(
mOutputDevInfo != null ? mOutputDevInfo.getProductName().toString()
: "Not connected");
String pathName;
switch (mTestPeripheral) {
case TESTPERIPHERAL_INVALID:
pathName = "Invalid Test Peripheral";
break;
case TESTPERIPHERAL_ANALOG_JACK:
pathName = "Headset Jack";
break;
case TESTPERIPHERAL_USB:
pathName = "USB";
break;
case TESTPERIPHERAL_DEVICE:
pathName = "Device Speaker + Microphone";
break;
case TESTPERIPHERAL_NONE:
default:
pathName = "Error. Unknown Test Path";
break;
}
mTestPathTxt.setText(pathName);
mTestButton.setEnabled(
mTestPeripheral != TESTPERIPHERAL_INVALID && mTestPeripheral != TESTPERIPHERAL_NONE);
}
private boolean areIODevicesOnePeripheral() {
if (mOutputDevInfo == null || mInputDevInfo == null) {
return false;
}
return mOutputDevInfo.getProductName().toString().equals(
mInputDevInfo.getProductName().toString());
}
private void calculateLatencyThresholds() {
switch (mTestPeripheral) {
case TESTPERIPHERAL_ANALOG_JACK:
mRecommendedLatency = mClaimsProAudio
? PROAUDIO_RECOMMENDED_LATENCY_MS : BASIC_RECOMMENDED_LATENCY_MS;
mMustLatency = mClaimsProAudio
? PROAUDIO_RECOMMENDED_LATENCY_MS : BASIC_MUST_LATENCY_MS;
break;
case TESTPERIPHERAL_USB:
mRecommendedLatency = mClaimsProAudio
? PROAUDIO_RECOMMENDED_USB_LATENCY_MS : BASIC_RECOMMENDED_LATENCY_MS;
mMustLatency = mClaimsProAudio
? PROAUDIO_RECOMMENDED_USB_LATENCY_MS : BASIC_MUST_LATENCY_MS;
break;
case TESTPERIPHERAL_DEVICE:
// This isn't a valid case so we won't pass it, but it can be run
mRecommendedLatency = mClaimsProAudio
? PROAUDIO_RECOMMENDED_LATENCY_MS : BASIC_RECOMMENDED_LATENCY_MS;
mMustLatency = mClaimsProAudio
? PROAUDIO_RECOMMENDED_LATENCY_MS :BASIC_MUST_LATENCY_MS;
break;
case TESTPERIPHERAL_NONE:
default:
mRecommendedLatency = BASIC_RECOMMENDED_LATENCY_MS;
mMustLatency = BASIC_MUST_LATENCY_MS;
break;
}
}
private void displayLatencyThresholds() {
if (isPeripheralValidForTest()) {
((TextView) findViewById(R.id.audio_loopback_must_latency)).setText("" + mMustLatency);
((TextView) findViewById(R.id.audio_loopback_recommended_latency)).setText(
"" + mRecommendedLatency);
} else {
String naStr = getResources().getString(R.string.audio_proaudio_NA);
((TextView) findViewById(R.id.audio_loopback_must_latency)).setText(naStr);
((TextView) findViewById(R.id.audio_loopback_recommended_latency)).setText(naStr);
}
}
/**
* refresh Audio Level seekbar and text
*/
private void refreshLevel() {
int currentLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
mAudioLevelSeekbar.setProgress(currentLevel);
String levelText = String.format("%s: %d/%d",
getResources().getString(R.string.audio_loopback_level_text),
currentLevel, mMaxLevel);
mAudioLevelText.setText(levelText);
}
//
// show active progress bar
//
protected void showWait(boolean show) {
mProgressBar.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
}
//
// Common logging
//
// Schema
private static final String KEY_LATENCY = "latency";
private static final String KEY_CONFIDENCE = "confidence";
private static final String KEY_SAMPLE_RATE = "sample_rate";
private static final String KEY_IS_PRO_AUDIO = "is_pro_audio";
private static final String KEY_IS_LOW_LATENCY = "is_low_latency";
private static final String KEY_IS_PERIPHERAL_ATTACHED = "is_peripheral_attached";
private static final String KEY_INPUT_PERIPHERAL_NAME = "input_peripheral";
private static final String KEY_OUTPUT_PERIPHERAL_NAME = "output_peripheral";
private static final String KEY_TEST_PERIPHERAL = "test_peripheral";
private static final String KEY_TEST_MMAP = "supports_mmap";
private static final String KEY_TEST_MMAPEXCLUSIVE = "supports_mmap_exclusive";
private static final String KEY_LEVEL = "level";
private static final String KEY_BUFFER_SIZE = "buffer_size_in_frames";
@Override
public String getTestId() {
return setTestNameSuffix(sCurrentDisplayMode, getClass().getName());
}
@Override
public String getReportFileName() { return PassFailButtons.AUDIO_TESTS_REPORT_LOG_NAME; }
@Override
public final String getReportSectionName() {
return setTestNameSuffix(sCurrentDisplayMode, "audio_loopback_latency_activity");
}
//
// Subclasses should call this explicitly. SubClasses should call submit() after their logs
//
@Override
public void recordTestResults() {
if (mNativeAnalyzerThread == null) {
return; // no results to report
}
CtsVerifierReportLog reportLog = getReportLog();
reportLog.addValue(
KEY_LATENCY,
mMeanLatencyMillis,
ResultType.LOWER_BETTER,
ResultUnit.MS);
reportLog.addValue(
KEY_CONFIDENCE,
mMeanConfidence,
ResultType.HIGHER_BETTER,
ResultUnit.NONE);
reportLog.addValue(
KEY_SAMPLE_RATE,
mNativeAnalyzerThread.getSampleRate(),
ResultType.NEUTRAL,
ResultUnit.NONE);
reportLog.addValue(
KEY_IS_LOW_LATENCY,
mNativeAnalyzerThread.isLowLatencyStream(),
ResultType.NEUTRAL,
ResultUnit.NONE);
reportLog.addValue(
KEY_IS_PERIPHERAL_ATTACHED,
mIsPeripheralAttached,
ResultType.NEUTRAL,
ResultUnit.NONE);
reportLog.addValue(
KEY_IS_PRO_AUDIO,
mClaimsProAudio,
ResultType.NEUTRAL,
ResultUnit.NONE);
reportLog.addValue(
KEY_TEST_PERIPHERAL,
mTestPeripheral,
ResultType.NEUTRAL,
ResultUnit.NONE);
reportLog.addValue(
KEY_TEST_MMAP,
mSupportsMMAP,
ResultType.NEUTRAL,
ResultUnit.NONE);
reportLog.addValue(
KEY_TEST_MMAPEXCLUSIVE ,
mSupportsMMAPExclusive,
ResultType.NEUTRAL,
ResultUnit.NONE);
if (mIsPeripheralAttached) {
reportLog.addValue(
KEY_INPUT_PERIPHERAL_NAME,
mInputDevInfo != null ? mInputDevInfo.getProductName().toString() : "None",
ResultType.NEUTRAL,
ResultUnit.NONE);
reportLog.addValue(
KEY_OUTPUT_PERIPHERAL_NAME,
mOutputDevInfo != null ? mOutputDevInfo.getProductName().toString() : "None",
ResultType.NEUTRAL,
ResultUnit.NONE);
}
int audioLevel = mAudioLevelSeekbar.getProgress();
reportLog.addValue(
KEY_LEVEL,
audioLevel,
ResultType.NEUTRAL,
ResultUnit.NONE);
reportLog.submit();
}
private void startAudioTest(Handler messageHandler) {
getPassButton().setEnabled(false);
mTestPhase = 0;
java.util.Arrays.fill(mLatencyMillis, 0.0);
java.util.Arrays.fill(mConfidence, 0.0);
mNativeAnalyzerThread = new NativeAnalyzerThread(this);
if (mNativeAnalyzerThread != null) {
mNativeAnalyzerThread.setMessageHandler(messageHandler);
// This value matches AAUDIO_INPUT_PRESET_VOICE_RECOGNITION
mNativeAnalyzerThread.setInputPreset(MediaRecorder.AudioSource.VOICE_RECOGNITION);
startTestPhase();
} else {
Log.e(TAG, "Couldn't allocate native analyzer thread");
mResultText.setText(getResources().getString(R.string.audio_loopback_failure));
}
}
private void startTestPhase() {
if (mNativeAnalyzerThread != null) {
mNativeAnalyzerThread.startTest();
// what is this for?
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void handleTestCompletion() {
mMeanLatencyMillis = StatUtils.calculateMean(mLatencyMillis);
mMeanAbsoluteDeviation =
StatUtils.calculateMeanAbsoluteDeviation(mMeanLatencyMillis, mLatencyMillis);
mMeanConfidence = StatUtils.calculateMean(mConfidence);
boolean pass = isPeripheralValidForTest()
&& mMeanConfidence >= CONFIDENCE_THRESHOLD
&& mMeanLatencyMillis > EPSILON
&& mMeanLatencyMillis < mMustLatency;
getPassButton().setEnabled(pass);
String result;
if (mMeanConfidence < CONFIDENCE_THRESHOLD) {
result = String.format(
"Test Finished\nInsufficient Confidence (%.2f < %.2f). No Results.",
mMeanConfidence, CONFIDENCE_THRESHOLD);
} else {
result = String.format(
"Test Finished - %s\nMean Latency:%.2f ms (required:%.2f)\n" +
"Mean Absolute Deviation: %.2f\n" +
" Confidence: %.2f\n" +
" Low Latency Path: %s",
(pass ? "PASS" : "FAIL"),
mMeanLatencyMillis,
mMustLatency,
mMeanAbsoluteDeviation,
mMeanConfidence,
mNativeAnalyzerThread.isLowLatencyStream() ? mYesString : mNoString);
}
// Make sure the test thread is finished. It should already be done.
if (mNativeAnalyzerThread != null) {
try {
mNativeAnalyzerThread.stopTest(STOP_TEST_TIMEOUT_MSEC);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mResultText.setText(result);
recordTestResults();
showWait(false);
mTestButton.setEnabled(true);
}
private void handleTestPhaseCompletion() {
if (mNativeAnalyzerThread != null && mTestPhase < NUM_TEST_PHASES) {
mLatencyMillis[mTestPhase] = mNativeAnalyzerThread.getLatencyMillis();
mConfidence[mTestPhase] = mNativeAnalyzerThread.getConfidence();
String result = String.format(
"Test %d Finished\nLatency: %.2f ms\nConfidence: %.2f\n",
mTestPhase,
mLatencyMillis[mTestPhase],
mConfidence[mTestPhase]);
mResultText.setText(result);
try {
mNativeAnalyzerThread.stopTest(STOP_TEST_TIMEOUT_MSEC);
// Thread.sleep(/*STOP_TEST_TIMEOUT_MSEC*/500);
} catch (InterruptedException e) {
e.printStackTrace();
}
mTestPhase++;
if (mTestPhase >= NUM_TEST_PHASES) {
handleTestCompletion();
} else {
startTestPhase();
}
}
}
/**
* handler for messages from audio thread
*/
private Handler mMessageHandler = new Handler() {
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch(msg.what) {
case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_STARTED:
Log.v(TAG,"got message native rec started!!");
showWait(true);
mResultText.setText(String.format("[phase: %d] - Test Running...",
(mTestPhase + 1)));
break;
case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_OPEN_ERROR:
Log.v(TAG,"got message native rec can't start!!");
mResultText.setText("Test Error opening streams.");
handleTestCompletion();
break;
case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_ERROR:
Log.v(TAG,"got message native rec can't start!!");
mResultText.setText("Test Error while recording.");
handleTestCompletion();
break;
case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE_ERRORS:
mResultText.setText("Test FAILED due to errors.");
handleTestCompletion();
break;
case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_ANALYZING:
Log.i(TAG, "NATIVE_AUDIO_THREAD_MESSAGE_ANALYZING");
mResultText.setText(String.format("[phase: %d] - Analyzing ...",
mTestPhase + 1));
break;
case NativeAnalyzerThread.NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE:
Log.i(TAG, "NATIVE_AUDIO_THREAD_MESSAGE_REC_COMPLETE");
handleTestPhaseCompletion();
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.audio_loopback_latency_activity);
setPassFailButtonClickListeners();
getPassButton().setEnabled(false);
setInfoResources(R.string.audio_loopback_latency_test, R.string.audio_loopback_info, -1);
mClaimsOutput = AudioSystemFlags.claimsOutput(this);
mClaimsInput = AudioSystemFlags.claimsInput(this);
mClaimsProAudio = AudioSystemFlags.claimsProAudio(this);
mYesString = getResources().getString(R.string.audio_general_yes);
mNoString = getResources().getString(R.string.audio_general_no);
// Pro Audio
((TextView)findViewById(R.id.audio_loopback_pro_audio)).setText(
"" + (mClaimsProAudio ? mYesString : mNoString));
// MMAP
((TextView)findViewById(R.id.audio_loopback_mmap)).setText(
"" + (mSupportsMMAP ? mYesString : mNoString));
((TextView)findViewById(R.id.audio_loopback_mmap_exclusive)).setText(
"" + (mSupportsMMAPExclusive ? mYesString : mNoString));
// Low Latency
((TextView)findViewById(R.id.audio_loopback_low_latency)).setText(
"" + (AudioSystemFlags.claimsLowLatencyAudio(this) ? mYesString : mNoString));
mTestPathTxt = ((TextView)findViewById(R.id.audio_loopback_testpath));
mTestButton = (Button)findViewById(R.id.audio_loopback_test_btn);
mTestButton.setOnClickListener(mBtnClickListener);
mAudioManager = getSystemService(AudioManager.class);
mAudioManager.registerAudioDeviceCallback(new ConnectListener(), new Handler());
connectLoopbackUI();
calculateLatencyThresholds();
displayLatencyThresholds();
}
private class OnBtnClickListener implements OnClickListener {
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.audio_loopback_test_btn:
Log.i(TAG, "audio loopback test");
startAudioTest(mMessageHandler);
break;
}
}
}
}