1 /* 2 * Copyright 2017 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.mobileer.oboetester; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.media.AudioDeviceInfo; 22 import android.media.AudioManager; 23 import android.os.Bundle; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.util.Log; 27 import android.view.View; 28 import android.view.WindowManager; 29 import android.widget.Button; 30 import android.widget.CheckBox; 31 import android.widget.Toast; 32 33 import java.io.IOException; 34 import java.util.ArrayList; 35 36 /** 37 * Base class for other Activities. 38 */ 39 abstract class TestAudioActivity extends Activity { 40 public static final String TAG = "OboeTester"; 41 42 protected static final int FADER_PROGRESS_MAX = 1000; 43 44 public static final int AUDIO_STATE_OPEN = 0; 45 public static final int AUDIO_STATE_STARTED = 1; 46 public static final int AUDIO_STATE_PAUSED = 2; 47 public static final int AUDIO_STATE_STOPPED = 3; 48 public static final int AUDIO_STATE_CLOSING = 4; 49 public static final int AUDIO_STATE_CLOSED = 5; 50 51 public static final int COLOR_ACTIVE = 0xFFD0D0A0; 52 public static final int COLOR_IDLE = 0xFFD0D0D0; 53 54 // Pass the activity index to native so it can know how to respond to the start and stop calls. 55 // WARNING - must match definitions in NativeAudioContext.h ActivityType 56 public static final int ACTIVITY_TEST_OUTPUT = 0; 57 public static final int ACTIVITY_TEST_INPUT = 1; 58 public static final int ACTIVITY_TAP_TO_TONE = 2; 59 public static final int ACTIVITY_RECORD_PLAY = 3; 60 public static final int ACTIVITY_ECHO = 4; 61 public static final int ACTIVITY_RT_LATENCY = 5; 62 public static final int ACTIVITY_GLITCHES = 6; 63 public static final int ACTIVITY_TEST_DISCONNECT = 7; 64 public static final int ACTIVITY_DATA_PATHS = 8; 65 66 private int mAudioState = AUDIO_STATE_CLOSED; 67 protected String audioManagerSampleRate; 68 protected int audioManagerFramesPerBurst; 69 protected ArrayList<StreamContext> mStreamContexts; 70 private Button mOpenButton; 71 private Button mStartButton; 72 private Button mPauseButton; 73 private Button mStopButton; 74 private Button mCloseButton; 75 private MyStreamSniffer mStreamSniffer; 76 private CheckBox mCallbackReturnStopBox; 77 private int mSampleRate; 78 private boolean mScoStarted; 79 private int mSingleTestIndex = -1; 80 private static boolean mBackgroundEnabled; 81 getTestName()82 public String getTestName() { 83 return "TestAudio"; 84 } 85 86 public static class StreamContext { 87 StreamConfigurationView configurationView; 88 AudioStreamTester tester; 89 isInput()90 boolean isInput() { 91 return tester.getCurrentAudioStream().isInput(); 92 } 93 } 94 95 // Periodically query the status of the streams. 96 protected class MyStreamSniffer { 97 public static final int SNIFFER_UPDATE_PERIOD_MSEC = 150; 98 public static final int SNIFFER_UPDATE_DELAY_MSEC = 300; 99 100 private Handler mHandler; 101 102 // Display status info for the stream. 103 private Runnable runnableCode = new Runnable() { 104 @Override 105 public void run() { 106 boolean streamClosed = false; 107 boolean gotViews = false; 108 for (StreamContext streamContext : mStreamContexts) { 109 AudioStreamBase.StreamStatus status = streamContext.tester.getCurrentAudioStream().getStreamStatus(); 110 AudioStreamBase.DoubleStatistics latencyStatistics = 111 streamContext.tester.getCurrentAudioStream().getLatencyStatistics(); 112 if (streamContext.configurationView != null) { 113 // Handler runs this on the main UI thread. 114 int framesPerBurst = streamContext.tester.getCurrentAudioStream().getFramesPerBurst(); 115 status.framesPerCallback = getFramesPerCallback(); 116 String msg = ""; 117 msg += "timestamp.latency = " + latencyStatistics.dump() + "\n"; 118 msg += status.dump(framesPerBurst); 119 streamContext.configurationView.setStatusText(msg); 120 updateStreamDisplay(); 121 gotViews = true; 122 } 123 124 streamClosed = streamClosed || (status.state >= 12); 125 } 126 127 if (streamClosed) { 128 onStreamClosed(); 129 } else { 130 // Repeat this runnable code block again. 131 if (gotViews) { 132 mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_PERIOD_MSEC); 133 } 134 } 135 } 136 }; 137 startStreamSniffer()138 private void startStreamSniffer() { 139 stopStreamSniffer(); 140 mHandler = new Handler(Looper.getMainLooper()); 141 // Start the initial runnable task by posting through the handler 142 mHandler.postDelayed(runnableCode, SNIFFER_UPDATE_DELAY_MSEC); 143 } 144 stopStreamSniffer()145 private void stopStreamSniffer() { 146 if (mHandler != null) { 147 mHandler.removeCallbacks(runnableCode); 148 } 149 } 150 151 } 152 setBackgroundEnabled(boolean enabled)153 public static void setBackgroundEnabled(boolean enabled) { 154 mBackgroundEnabled = enabled; 155 } isBackgroundEnabled()156 public static boolean isBackgroundEnabled() { 157 return mBackgroundEnabled; 158 } 159 onStreamClosed()160 public void onStreamClosed() { 161 } 162 inflateActivity()163 protected abstract void inflateActivity(); 164 updateStreamDisplay()165 void updateStreamDisplay() { 166 } 167 168 @Override onCreate(Bundle savedInstanceState)169 protected void onCreate(Bundle savedInstanceState) { 170 super.onCreate(savedInstanceState); 171 inflateActivity(); 172 findAudioCommon(); 173 } 174 hideSettingsViews()175 public void hideSettingsViews() { 176 for (StreamContext streamContext : mStreamContexts) { 177 if (streamContext.configurationView != null) { 178 streamContext.configurationView.hideSettingsView(); 179 } 180 } 181 } 182 getActivityType()183 abstract int getActivityType(); 184 setSingleTestIndex(int testIndex)185 public void setSingleTestIndex(int testIndex) { 186 mSingleTestIndex = testIndex; 187 } getSingleTestIndex()188 public int getSingleTestIndex() { 189 return mSingleTestIndex; 190 } 191 192 @Override onStart()193 protected void onStart() { 194 super.onStart(); 195 resetConfiguration(); 196 setActivityType(getActivityType()); 197 } 198 resetConfiguration()199 protected void resetConfiguration() { 200 } 201 202 @Override onStop()203 protected void onStop() { 204 if (!isBackgroundEnabled()) { 205 Log.i(TAG, "onStop() called so stop the test ========================="); 206 onStopTest(); 207 } 208 super.onStop(); 209 } 210 211 @Override onDestroy()212 protected void onDestroy() { 213 if (isBackgroundEnabled()) { 214 Log.i(TAG, "onDestroy() called so stop the test ========================="); 215 onStopTest(); 216 } 217 mAudioState = AUDIO_STATE_CLOSED; 218 super.onDestroy(); 219 } 220 updateEnabledWidgets()221 protected void updateEnabledWidgets() { 222 if (mOpenButton != null) { 223 mOpenButton.setBackgroundColor(mAudioState == AUDIO_STATE_OPEN ? COLOR_ACTIVE : COLOR_IDLE); 224 mStartButton.setBackgroundColor(mAudioState == AUDIO_STATE_STARTED ? COLOR_ACTIVE : COLOR_IDLE); 225 mPauseButton.setBackgroundColor(mAudioState == AUDIO_STATE_PAUSED ? COLOR_ACTIVE : COLOR_IDLE); 226 mStopButton.setBackgroundColor(mAudioState == AUDIO_STATE_STOPPED ? COLOR_ACTIVE : COLOR_IDLE); 227 mCloseButton.setBackgroundColor(mAudioState == AUDIO_STATE_CLOSED ? COLOR_ACTIVE : COLOR_IDLE); 228 } 229 setConfigViewsEnabled(mAudioState == AUDIO_STATE_CLOSED); 230 } 231 setConfigViewsEnabled(boolean b)232 private void setConfigViewsEnabled(boolean b) { 233 for (StreamContext streamContext : mStreamContexts) { 234 if (streamContext.configurationView != null) { 235 streamContext.configurationView.setChildrenEnabled(b); 236 } 237 } 238 } 239 isOutput()240 abstract boolean isOutput(); 241 clearStreamContexts()242 public void clearStreamContexts() { 243 mStreamContexts.clear(); 244 } 245 addOutputStreamContext()246 public StreamContext addOutputStreamContext() { 247 StreamContext streamContext = new StreamContext(); 248 streamContext.tester = AudioOutputTester.getInstance(); 249 streamContext.configurationView = (StreamConfigurationView) 250 findViewById(R.id.outputStreamConfiguration); 251 if (streamContext.configurationView == null) { 252 streamContext.configurationView = (StreamConfigurationView) 253 findViewById(R.id.streamConfiguration); 254 } 255 if (streamContext.configurationView != null) { 256 streamContext.configurationView.setOutput(true); 257 streamContext.configurationView.setRequestedConfiguration(streamContext.tester.requestedConfiguration); 258 streamContext.configurationView.setActualConfiguration(streamContext.tester.actualConfiguration); 259 } 260 mStreamContexts.add(streamContext); 261 return streamContext; 262 } 263 264 addAudioOutputTester()265 public AudioOutputTester addAudioOutputTester() { 266 StreamContext streamContext = addOutputStreamContext(); 267 return (AudioOutputTester) streamContext.tester; 268 } 269 addInputStreamContext()270 public StreamContext addInputStreamContext() { 271 StreamContext streamContext = new StreamContext(); 272 streamContext.tester = AudioInputTester.getInstance(); 273 streamContext.configurationView = (StreamConfigurationView) 274 findViewById(R.id.inputStreamConfiguration); 275 if (streamContext.configurationView == null) { 276 streamContext.configurationView = (StreamConfigurationView) 277 findViewById(R.id.streamConfiguration); 278 } 279 if (streamContext.configurationView != null) { 280 streamContext.configurationView.setOutput(false); 281 streamContext.configurationView.setRequestedConfiguration(streamContext.tester.requestedConfiguration); 282 streamContext.configurationView.setActualConfiguration(streamContext.tester.actualConfiguration); 283 } 284 streamContext.tester = AudioInputTester.getInstance(); 285 mStreamContexts.add(streamContext); 286 return streamContext; 287 } 288 addAudioInputTester()289 public AudioInputTester addAudioInputTester() { 290 StreamContext streamContext = addInputStreamContext(); 291 return (AudioInputTester) streamContext.tester; 292 } 293 updateStreamConfigurationViews()294 void updateStreamConfigurationViews() { 295 for (StreamContext streamContext : mStreamContexts) { 296 if (streamContext.configurationView != null) { 297 streamContext.configurationView.updateDisplay(); 298 } 299 } 300 } 301 getFirstInputStreamContext()302 StreamContext getFirstInputStreamContext() { 303 for (StreamContext streamContext : mStreamContexts) { 304 if (streamContext.isInput()) 305 return streamContext; 306 } 307 return null; 308 } 309 getFirstOutputStreamContext()310 StreamContext getFirstOutputStreamContext() { 311 for (StreamContext streamContext : mStreamContexts) { 312 if (!streamContext.isInput()) 313 return streamContext; 314 } 315 return null; 316 } 317 findAudioCommon()318 protected void findAudioCommon() { 319 mOpenButton = (Button) findViewById(R.id.button_open); 320 if (mOpenButton != null) { 321 mStartButton = (Button) findViewById(R.id.button_start); 322 mPauseButton = (Button) findViewById(R.id.button_pause); 323 mStopButton = (Button) findViewById(R.id.button_stop); 324 mCloseButton = (Button) findViewById(R.id.button_close); 325 } 326 mStreamContexts = new ArrayList<StreamContext>(); 327 328 queryNativeAudioParameters(); 329 330 mCallbackReturnStopBox = (CheckBox) findViewById(R.id.callbackReturnStop); 331 if (mCallbackReturnStopBox != null) { 332 mCallbackReturnStopBox.setOnClickListener(new View.OnClickListener() { 333 @Override 334 public void onClick(View v) { 335 OboeAudioStream.setCallbackReturnStop(mCallbackReturnStopBox.isChecked()); 336 } 337 }); 338 } 339 OboeAudioStream.setCallbackReturnStop(false); 340 341 mStreamSniffer = new MyStreamSniffer(); 342 } 343 queryNativeAudioParameters()344 private void queryNativeAudioParameters() { 345 AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 346 audioManagerSampleRate = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); 347 String audioManagerFramesPerBurstText = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); 348 audioManagerFramesPerBurst = Integer.parseInt(audioManagerFramesPerBurstText); 349 } 350 setupEffects(int sessionId)351 abstract public void setupEffects(int sessionId); 352 showErrorToast(String message)353 protected void showErrorToast(String message) { 354 showToast("Error: " + message); 355 } 356 showToast(final String message)357 protected void showToast(final String message) { 358 runOnUiThread(new Runnable() { 359 @Override 360 public void run() { 361 Toast.makeText(TestAudioActivity.this, 362 message, 363 Toast.LENGTH_SHORT).show(); 364 } 365 }); 366 } 367 openAudio(View view)368 public void openAudio(View view) { 369 try { 370 openAudio(); 371 } catch (Exception e) { 372 showErrorToast(e.getMessage()); 373 } 374 } 375 startAudio(View view)376 public void startAudio(View view) { 377 Log.i(TAG, "startAudio() called ======================================="); 378 try { 379 startAudio(); 380 } catch (Exception e) { 381 showErrorToast(e.getMessage()); 382 } 383 keepScreenOn(true); 384 } 385 keepScreenOn(boolean on)386 protected void keepScreenOn(boolean on) { 387 if (on) { 388 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 389 } else { 390 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 391 } 392 } 393 stopAudio(View view)394 public void stopAudio(View view) { 395 stopAudio(); 396 keepScreenOn(false); 397 } 398 pauseAudio(View view)399 public void pauseAudio(View view) { 400 pauseAudio(); 401 keepScreenOn(false); 402 } 403 closeAudio(View view)404 public void closeAudio(View view) { 405 closeAudio(); 406 } 407 getSampleRate()408 public int getSampleRate() { 409 return mSampleRate; 410 } 411 openAudio()412 public void openAudio() throws IOException { 413 closeAudio(); 414 415 int sampleRate = 0; 416 417 // Open output streams then open input streams. 418 // This is so that the capacity of input stream can be expanded to 419 // match the burst size of the output for full duplex. 420 for (StreamContext streamContext : mStreamContexts) { 421 if (!streamContext.isInput()) { 422 openStreamContext(streamContext); 423 int streamSampleRate = streamContext.tester.actualConfiguration.getSampleRate(); 424 if (sampleRate == 0) { 425 sampleRate = streamSampleRate; 426 } 427 } 428 } 429 for (StreamContext streamContext : mStreamContexts) { 430 if (streamContext.isInput()) { 431 if (sampleRate != 0) { 432 streamContext.tester.requestedConfiguration.setSampleRate(sampleRate); 433 } 434 openStreamContext(streamContext); 435 } 436 } 437 updateEnabledWidgets(); 438 mStreamSniffer.startStreamSniffer(); 439 } 440 441 /** 442 * @param deviceId 443 * @return true if the device is TYPE_BLUETOOTH_SCO 444 */ isScoDevice(int deviceId)445 boolean isScoDevice(int deviceId) { 446 if (deviceId == 0) return false; // Unspecified 447 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 448 final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL); 449 for (AudioDeviceInfo device : devices) { 450 if (device.getId() == deviceId) { 451 return device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO; 452 } 453 } 454 return false; 455 } 456 openStreamContext(StreamContext streamContext)457 private void openStreamContext(StreamContext streamContext) throws IOException { 458 StreamConfiguration requestedConfig = streamContext.tester.requestedConfiguration; 459 StreamConfiguration actualConfig = streamContext.tester.actualConfiguration; 460 requestedConfig.setFramesPerBurst(audioManagerFramesPerBurst); 461 462 // Start Bluetooth SCO if needed. 463 if (isScoDevice(requestedConfig.getDeviceId()) && !mScoStarted) { 464 startBluetoothSco(); 465 mScoStarted = true; 466 } 467 468 streamContext.tester.open(); // OPEN the stream 469 470 mSampleRate = actualConfig.getSampleRate(); 471 mAudioState = AUDIO_STATE_OPEN; 472 int sessionId = actualConfig.getSessionId(); 473 if (sessionId > 0) { 474 setupEffects(sessionId); 475 } 476 if (streamContext.configurationView != null) { 477 streamContext.configurationView.updateDisplay(); 478 } 479 } 480 481 // Native methods startNative()482 private native int startNative(); pauseNative()483 private native int pauseNative(); stopNative()484 private native int stopNative(); setActivityType(int activityType)485 protected native void setActivityType(int activityType); getFramesPerCallback()486 private native int getFramesPerCallback(); 487 startAudio()488 public void startAudio() throws IOException { 489 int result = startNative(); 490 if (result < 0) { 491 showErrorToast("Start failed with " + result); 492 throw new IOException("startNative returned " + result); 493 } else { 494 for (StreamContext streamContext : mStreamContexts) { 495 StreamConfigurationView configView = streamContext.configurationView; 496 if (configView != null) { 497 configView.updateDisplay(); 498 } 499 } 500 mAudioState = AUDIO_STATE_STARTED; 501 updateEnabledWidgets(); 502 } 503 } 504 toastPauseError(int result)505 protected void toastPauseError(int result) { 506 showErrorToast("Pause failed with " + result); 507 } 508 pauseAudio()509 public void pauseAudio() { 510 int result = pauseNative(); 511 if (result < 0) { 512 toastPauseError(result); 513 } else { 514 mAudioState = AUDIO_STATE_PAUSED; 515 updateEnabledWidgets(); 516 } 517 } 518 stopAudio()519 public void stopAudio() { 520 int result = stopNative(); 521 if (result < 0) { 522 showErrorToast("Stop failed with " + result); 523 } else { 524 mAudioState = AUDIO_STATE_STOPPED; 525 updateEnabledWidgets(); 526 } 527 } 528 runTest()529 public void runTest() {} 530 531 // This should only be called from UI events such as onStop or a button press. onStopTest()532 public void onStopTest() { 533 stopTest(); 534 } 535 stopTest()536 public void stopTest() { 537 stopAudio(); 538 closeAudio(); 539 } 540 stopAudioQuiet()541 public void stopAudioQuiet() { 542 stopNative(); 543 mAudioState = AUDIO_STATE_STOPPED; 544 updateEnabledWidgets(); 545 } 546 547 // Make synchronized so we don't close from two streams at the same time. closeAudio()548 public synchronized void closeAudio() { 549 if (mAudioState >= AUDIO_STATE_CLOSING) { 550 Log.d(TAG, "closeAudio() already closing"); 551 return; 552 } 553 mAudioState = AUDIO_STATE_CLOSING; 554 555 mStreamSniffer.stopStreamSniffer(); 556 // Close output streams first because legacy callbacks may still be active 557 // and an output stream may be calling the input stream. 558 for (StreamContext streamContext : mStreamContexts) { 559 if (!streamContext.isInput()) { 560 streamContext.tester.close(); 561 } 562 } 563 for (StreamContext streamContext : mStreamContexts) { 564 if (streamContext.isInput()) { 565 streamContext.tester.close(); 566 } 567 } 568 569 if (mScoStarted) { 570 stopBluetoothSco(); 571 mScoStarted = false; 572 } 573 574 mAudioState = AUDIO_STATE_CLOSED; 575 updateEnabledWidgets(); 576 } 577 startBluetoothSco()578 void startBluetoothSco() { 579 AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 580 myAudioMgr.startBluetoothSco(); 581 } 582 stopBluetoothSco()583 void stopBluetoothSco() { 584 AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 585 myAudioMgr.stopBluetoothSco(); 586 } 587 588 } 589