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