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.oboe.samples.hellooboe; 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.Build; 24 import android.os.Bundle; 25 26 import androidx.annotation.RequiresApi; 27 28 import android.os.Message; 29 import android.util.Log; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.widget.AdapterView; 33 import android.widget.ArrayAdapter; 34 import android.widget.SimpleAdapter; 35 import android.widget.Spinner; 36 import android.widget.TextView; 37 import android.widget.Toast; 38 39 import com.google.oboe.samples.audio_device.AudioDeviceListEntry; 40 import com.google.oboe.samples.audio_device.AudioDeviceSpinner; 41 42 import java.util.ArrayList; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Locale; 46 import java.util.Objects; 47 import java.util.Timer; 48 import java.util.TimerTask; 49 50 public class MainActivity extends Activity { 51 private static final String TAG = "HelloOboe"; 52 private static final long UPDATE_LATENCY_EVERY_MILLIS = 1000; 53 private static final Integer[] CHANNEL_COUNT_OPTIONS = {1, 2, 3, 4, 5, 6, 7, 8}; 54 // Default to Stereo (OPTIONS is zero-based array so index 1 = 2 channels) 55 private static final int CHANNEL_COUNT_DEFAULT_OPTION_INDEX = 1; 56 private static final int[] BUFFER_SIZE_OPTIONS = {0, 1, 2, 4, 8}; 57 private static final String[] AUDIO_API_OPTIONS = {"Unspecified", "OpenSL ES", "AAudio"}; 58 private static final int OBOE_API_OPENSL_ES = 1; 59 // Default all other spinners to the first option on the list 60 private static final int SPINNER_DEFAULT_OPTION_INDEX = 0; 61 62 private Spinner mAudioApiSpinner; 63 private AudioDeviceSpinner mPlaybackDeviceSpinner; 64 private Spinner mChannelCountSpinner; 65 private Spinner mBufferSizeSpinner; 66 private TextView mLatencyText; 67 private Timer mLatencyUpdater; 68 private boolean mScoStarted = false; 69 70 /** Commands for background thread. */ 71 private static final int WHAT_START = 100; 72 private static final int WHAT_STOP = 101; 73 private static final int WHAT_SET_DEVICE_ID = 102; 74 private static final int WHAT_SET_AUDIO_API = 103; 75 private static final int WHAT_SET_CHANNEL_COUNT = 104; 76 private BackgroundRunner mRunner = new MyBackgroundRunner(); 77 78 private class MyBackgroundRunner extends BackgroundRunner { 79 // These are initialized to zero by Java. 80 // Zero matches the oboe::Unspecified value. 81 int audioApi; 82 int deviceId; 83 int channelCount; 84 85 @Override 86 /* Execute this in a background thread to avoid ANRs. */ handleMessageInBackground(Message message)87 void handleMessageInBackground(Message message) { 88 int what = message.what; 89 int arg1 = message.arg1; 90 Log.i(MainActivity.TAG, "got background message, what = " + what + ", arg1 = " + arg1); 91 int result = 0; 92 boolean restart = false; 93 switch (what) { 94 case WHAT_START: 95 result = PlaybackEngine.startEngine(audioApi, deviceId, channelCount); 96 break; 97 case WHAT_STOP: 98 result = PlaybackEngine.stopEngine(); 99 break; 100 case WHAT_SET_AUDIO_API: 101 if (audioApi != arg1) { 102 audioApi = arg1; 103 restart = true; 104 } 105 break; 106 case WHAT_SET_DEVICE_ID: 107 if (deviceId != arg1) { 108 deviceId = arg1; 109 restart = true; 110 } 111 break; 112 case WHAT_SET_CHANNEL_COUNT: 113 if (channelCount != arg1) { 114 channelCount = arg1; 115 restart = true; 116 } 117 break; 118 } 119 if (restart) { 120 int result1 = PlaybackEngine.stopEngine(); 121 int result2 = PlaybackEngine.startEngine(audioApi, deviceId, channelCount); 122 result = (result2 != 0) ? result2 : result1; 123 } 124 if (result != 0) { 125 Log.e(TAG, "audio error " + result); 126 showToast("Error in audio =" + result); 127 } 128 } 129 } 130 131 /* 132 * Hook to user control to start / stop audio playback: 133 * touch-down: start, and keeps on playing 134 * touch-up: stop. 135 * simply pass the events to native side. 136 */ 137 @Override onTouchEvent(MotionEvent event)138 public boolean onTouchEvent(MotionEvent event) { 139 switch (event.getAction()) { 140 case (MotionEvent.ACTION_DOWN): 141 PlaybackEngine.setToneOn(true); 142 break; 143 case (MotionEvent.ACTION_UP): 144 PlaybackEngine.setToneOn(false); 145 break; 146 } 147 return super.onTouchEvent(event); 148 } 149 150 @Override onCreate(Bundle savedInstanceState)151 protected void onCreate(Bundle savedInstanceState) { 152 super.onCreate(savedInstanceState); 153 setContentView(R.layout.activity_main); 154 mLatencyText = findViewById(R.id.latencyText); 155 setupAudioApiSpinner(); 156 setupPlaybackDeviceSpinner(); 157 setupChannelCountSpinner(); 158 setupBufferSizeSpinner(); 159 } 160 161 /* 162 * Creating engine in onResume() and destroying in onPause() so the stream retains exclusive 163 * mode only while in focus. This allows other apps to reclaim exclusive stream mode. 164 */ 165 @Override onResume()166 protected void onResume() { 167 super.onResume(); 168 PlaybackEngine.setDefaultStreamValues(this); 169 setupLatencyUpdater(); 170 171 // Return the spinner states to their default value 172 mChannelCountSpinner.setSelection(CHANNEL_COUNT_DEFAULT_OPTION_INDEX); 173 mPlaybackDeviceSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX); 174 mBufferSizeSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX); 175 if (PlaybackEngine.isAAudioRecommended()) { 176 mAudioApiSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX); 177 } else { 178 mAudioApiSpinner.setSelection(OBOE_API_OPENSL_ES); 179 mAudioApiSpinner.setEnabled(false); 180 } 181 182 startAudioAsync(); 183 } 184 185 @Override onPause()186 protected void onPause() { 187 if (mLatencyUpdater != null) mLatencyUpdater.cancel(); 188 stopAudioAsync(); 189 190 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 191 clearCommunicationDevice(); 192 } else { 193 if (mScoStarted) { 194 stopBluetoothSco(); 195 mScoStarted = false; 196 } 197 } 198 super.onPause(); 199 } 200 setupChannelCountSpinner()201 private void setupChannelCountSpinner() { 202 mChannelCountSpinner = findViewById(R.id.channelCountSpinner); 203 204 ArrayAdapter<Integer> channelCountAdapter = new ArrayAdapter<>(this, R.layout.channel_counts_spinner, CHANNEL_COUNT_OPTIONS); 205 mChannelCountSpinner.setAdapter(channelCountAdapter); 206 mChannelCountSpinner.setSelection(CHANNEL_COUNT_DEFAULT_OPTION_INDEX); 207 208 mChannelCountSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 209 @Override 210 public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { 211 setChannelCountAsync(CHANNEL_COUNT_OPTIONS[mChannelCountSpinner.getSelectedItemPosition()]); 212 } 213 214 @Override 215 public void onNothingSelected(AdapterView<?> adapterView) { 216 } 217 }); 218 } 219 setupBufferSizeSpinner()220 private void setupBufferSizeSpinner() { 221 mBufferSizeSpinner = findViewById(R.id.bufferSizeSpinner); 222 mBufferSizeSpinner.setAdapter(new SimpleAdapter( 223 this, 224 createBufferSizeOptionsList(), // list of buffer size options 225 R.layout.buffer_sizes_spinner, // the xml layout 226 new String[]{getString(R.string.buffer_size_description_key)}, // field to display 227 new int[]{R.id.bufferSizeOption} // View to show field in 228 )); 229 230 mBufferSizeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 231 @Override 232 public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { 233 PlaybackEngine.setBufferSizeInBursts(getBufferSizeInBursts()); 234 } 235 236 @Override 237 public void onNothingSelected(AdapterView<?> adapterView) { 238 } 239 }); 240 } 241 setupPlaybackDeviceSpinner()242 private void setupPlaybackDeviceSpinner() { 243 mPlaybackDeviceSpinner = findViewById(R.id.playbackDevicesSpinner); 244 245 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 246 mPlaybackDeviceSpinner.setDirectionType(AudioManager.GET_DEVICES_OUTPUTS); 247 mPlaybackDeviceSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 248 @Override 249 public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { 250 // To use Bluetooth SCO, setCommunicationDevice() or startBluetoothSco() must 251 // be called. The AudioManager.startBluetoothSco() is deprecated in Android T. 252 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 253 if (isScoDevice(getPlaybackDeviceId())){ 254 setCommunicationDevice(getPlaybackDeviceId()); 255 } else { 256 clearCommunicationDevice(); 257 } 258 } else { 259 // Start Bluetooth SCO if needed. 260 if (isScoDevice(getPlaybackDeviceId()) && !mScoStarted) { 261 startBluetoothSco(); 262 mScoStarted = true; 263 } else if (!isScoDevice(getPlaybackDeviceId()) && mScoStarted) { 264 stopBluetoothSco(); 265 mScoStarted = false; 266 } 267 } 268 setAudioDeviceIdAsync(getPlaybackDeviceId()); 269 } 270 271 @Override 272 public void onNothingSelected(AdapterView<?> adapterView) { 273 } 274 }); 275 } else { 276 mPlaybackDeviceSpinner.setEnabled(false); 277 } 278 } 279 setupAudioApiSpinner()280 private void setupAudioApiSpinner() { 281 mAudioApiSpinner = findViewById(R.id.audioApiSpinner); 282 mAudioApiSpinner.setAdapter(new SimpleAdapter( 283 this, 284 createAudioApisOptionsList(), 285 R.layout.audio_apis_spinner, // the xml layout 286 new String[]{getString(R.string.audio_api_description_key)}, // field to display 287 new int[]{R.id.audioApiOption} // View to show field in 288 )); 289 290 mAudioApiSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 291 @Override 292 public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { 293 setAudioApiAsync(i); 294 if (i == OBOE_API_OPENSL_ES) { 295 mPlaybackDeviceSpinner.setSelection(SPINNER_DEFAULT_OPTION_INDEX); 296 mPlaybackDeviceSpinner.setEnabled(false); 297 } else { 298 mPlaybackDeviceSpinner.setEnabled(true); 299 } 300 } 301 302 @Override 303 public void onNothingSelected(AdapterView<?> adapterView) { 304 } 305 }); 306 } 307 getPlaybackDeviceId()308 private int getPlaybackDeviceId() { 309 return ((AudioDeviceListEntry) mPlaybackDeviceSpinner.getSelectedItem()).getId(); 310 } 311 getBufferSizeInBursts()312 private int getBufferSizeInBursts() { 313 @SuppressWarnings("unchecked") 314 HashMap<String, String> selectedOption = (HashMap<String, String>) 315 mBufferSizeSpinner.getSelectedItem(); 316 317 String valueKey = getString(R.string.buffer_size_value_key); 318 319 // parseInt will throw a NumberFormatException if the string doesn't contain a valid integer 320 // representation. We don't need to worry about this because the values are derived from 321 // the BUFFER_SIZE_OPTIONS int array. 322 return Integer.parseInt(Objects.requireNonNull(selectedOption.get(valueKey))); 323 } 324 setupLatencyUpdater()325 private void setupLatencyUpdater() { 326 //Update the latency every 1s 327 TimerTask latencyUpdateTask = new TimerTask() { 328 @Override 329 public void run() { 330 final String latencyStr; 331 if (PlaybackEngine.isLatencyDetectionSupported()) { 332 double latency = PlaybackEngine.getCurrentOutputLatencyMillis(); 333 if (latency >= 0) { 334 latencyStr = String.format(Locale.getDefault(), "%.2fms", latency); 335 } else { 336 latencyStr = "Unknown"; 337 } 338 } else { 339 latencyStr = getString(R.string.only_supported_on_api_26); 340 } 341 342 runOnUiThread(() -> mLatencyText.setText(getString(R.string.latency, latencyStr))); 343 } 344 }; 345 mLatencyUpdater = new Timer(); 346 mLatencyUpdater.schedule(latencyUpdateTask, 0, UPDATE_LATENCY_EVERY_MILLIS); 347 } 348 349 /** 350 * Creates a list of buffer size options which can be used to populate a SimpleAdapter. 351 * Each option has a description and a value. The description is always equal to the value, 352 * except when the value is zero as this indicates that the buffer size should be set 353 * automatically by the audio engine 354 * 355 * @return list of buffer size options 356 */ createBufferSizeOptionsList()357 private List<HashMap<String, String>> createBufferSizeOptionsList() { 358 359 ArrayList<HashMap<String, String>> bufferSizeOptions = new ArrayList<>(); 360 361 for (int i : BUFFER_SIZE_OPTIONS) { 362 HashMap<String, String> option = new HashMap<>(); 363 String strValue = String.valueOf(i); 364 String description = (i == 0) ? getString(R.string.automatic) : strValue; 365 option.put(getString(R.string.buffer_size_description_key), description); 366 option.put(getString(R.string.buffer_size_value_key), strValue); 367 368 bufferSizeOptions.add(option); 369 } 370 371 return bufferSizeOptions; 372 } 373 createAudioApisOptionsList()374 private List<HashMap<String, String>> createAudioApisOptionsList() { 375 376 ArrayList<HashMap<String, String>> audioApiOptions = new ArrayList<>(); 377 378 for (int i = 0; i < AUDIO_API_OPTIONS.length; i++) { 379 HashMap<String, String> option = new HashMap<>(); 380 option.put(getString(R.string.buffer_size_description_key), AUDIO_API_OPTIONS[i]); 381 option.put(getString(R.string.buffer_size_value_key), String.valueOf(i)); 382 audioApiOptions.add(option); 383 } 384 return audioApiOptions; 385 } 386 showToast(final String message)387 protected void showToast(final String message) { 388 runOnUiThread(() -> Toast.makeText(MainActivity.this, 389 message, 390 Toast.LENGTH_SHORT).show()); 391 } 392 393 @RequiresApi(api = Build.VERSION_CODES.S) setCommunicationDevice(int deviceId)394 private void setCommunicationDevice(int deviceId) { 395 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 396 final AudioDeviceInfo[] devices; 397 devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); 398 for (AudioDeviceInfo device : devices) { 399 if (device.getId() == deviceId) { 400 audioManager.setCommunicationDevice(device); 401 return; 402 } 403 } 404 } 405 406 @RequiresApi(api = Build.VERSION_CODES.S) clearCommunicationDevice()407 private void clearCommunicationDevice() { 408 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 409 audioManager.clearCommunicationDevice(); 410 } 411 412 @RequiresApi(api = Build.VERSION_CODES.M) isScoDevice(int deviceId)413 private boolean isScoDevice(int deviceId) { 414 if (deviceId == 0) return false; // Unspecified 415 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 416 final AudioDeviceInfo[] devices; 417 devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); 418 for (AudioDeviceInfo device : devices) { 419 if (device.getId() == deviceId) { 420 return device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO; 421 } 422 } 423 return false; 424 } 425 startBluetoothSco()426 private void startBluetoothSco() { 427 AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 428 myAudioMgr.startBluetoothSco(); 429 } 430 stopBluetoothSco()431 private void stopBluetoothSco() { 432 AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 433 myAudioMgr.stopBluetoothSco(); 434 } 435 436 startAudioAsync()437 void startAudioAsync() { 438 mRunner.sendMessage(WHAT_START); 439 } 440 stopAudioAsync()441 void stopAudioAsync() { 442 mRunner.sendMessage(WHAT_STOP); 443 } 444 setAudioApiAsync(int audioApi)445 void setAudioApiAsync(int audioApi){ 446 mRunner.sendMessage(WHAT_SET_AUDIO_API, audioApi); 447 } 448 setAudioDeviceIdAsync(int deviceId)449 void setAudioDeviceIdAsync(int deviceId){ 450 mRunner.sendMessage(WHAT_SET_DEVICE_ID, deviceId); 451 } 452 setChannelCountAsync(int channelCount)453 void setChannelCountAsync(int channelCount) { 454 mRunner.sendMessage(WHAT_SET_CHANNEL_COUNT, channelCount); 455 } 456 } 457