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