AudioManagerAndroid.java revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.media; 6 7import android.bluetooth.BluetoothAdapter; 8import android.bluetooth.BluetoothManager; 9import android.content.BroadcastReceiver; 10import android.content.ContentResolver; 11import android.content.Context; 12import android.content.Intent; 13import android.content.IntentFilter; 14import android.content.pm.PackageManager; 15import android.database.ContentObserver; 16import android.media.AudioFormat; 17import android.media.AudioManager; 18import android.media.AudioRecord; 19import android.media.AudioTrack; 20import android.media.audiofx.AcousticEchoCanceler; 21import android.os.Build; 22import android.os.Handler; 23import android.os.HandlerThread; 24import android.os.Process; 25import android.provider.Settings; 26import android.util.Log; 27 28import org.chromium.base.CalledByNative; 29import org.chromium.base.JNINamespace; 30 31import java.util.ArrayList; 32import java.util.Arrays; 33import java.util.List; 34 35@JNINamespace("media") 36class AudioManagerAndroid { 37 private static final String TAG = "AudioManagerAndroid"; 38 39 // Set to true to enable debug logs. Avoid in production builds. 40 // NOTE: always check in as false. 41 private static final boolean DEBUG = false; 42 43 /** 44 * NonThreadSafe is a helper class used to help verify that methods of a 45 * class are called from the same thread. 46 * Inspired by class in package com.google.android.apps.chrome.utilities. 47 * Is only utilized when DEBUG is set to true. 48 */ 49 private static class NonThreadSafe { 50 private final Long mThreadId; 51 52 public NonThreadSafe() { 53 if (DEBUG) { 54 mThreadId = Thread.currentThread().getId(); 55 } else { 56 // Avoids "Unread field" issue reported by findbugs. 57 mThreadId = 0L; 58 } 59 } 60 61 /** 62 * Checks if the method is called on the valid thread. 63 * Assigns the current thread if no thread was assigned. 64 */ 65 public boolean calledOnValidThread() { 66 if (DEBUG) { 67 return mThreadId.equals(Thread.currentThread().getId()); 68 } 69 return true; 70 } 71 } 72 73 private static boolean runningOnJellyBeanOrHigher() { 74 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; 75 } 76 77 private static boolean runningOnJellyBeanMR1OrHigher() { 78 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; 79 } 80 81 private static boolean runningOnJellyBeanMR2OrHigher() { 82 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; 83 } 84 85 /** Simple container for device information. */ 86 private static class AudioDeviceName { 87 private final int mId; 88 private final String mName; 89 90 private AudioDeviceName(int id, String name) { 91 mId = id; 92 mName = name; 93 } 94 95 @CalledByNative("AudioDeviceName") 96 private String id() { return String.valueOf(mId); } 97 98 @CalledByNative("AudioDeviceName") 99 private String name() { return mName; } 100 } 101 102 // List if device models which have been vetted for good quality platform 103 // echo cancellation. 104 // NOTE: only add new devices to this list if manual tests have been 105 // performed where the AEC performance is evaluated using e.g. a WebRTC 106 // audio client such as https://apprtc.appspot.com/?r=<ROOM NAME>. 107 private static final String[] SUPPORTED_AEC_MODELS = new String[] { 108 "GT-I9300", // Galaxy S3 109 "GT-I9500", // Galaxy S4 110 "GT-N7105", // Galaxy Note 2 111 "Nexus 4", // Nexus 4 112 "Nexus 5", // Nexus 5 113 "Nexus 7", // Nexus 7 114 "SM-N9005", // Galaxy Note 3 115 "SM-T310", // Galaxy Tab 3 8.0 (WiFi) 116 }; 117 118 // Supported audio device types. 119 private static final int DEVICE_DEFAULT = -2; 120 private static final int DEVICE_INVALID = -1; 121 private static final int DEVICE_SPEAKERPHONE = 0; 122 private static final int DEVICE_WIRED_HEADSET = 1; 123 private static final int DEVICE_EARPIECE = 2; 124 private static final int DEVICE_BLUETOOTH_HEADSET = 3; 125 private static final int DEVICE_COUNT = 4; 126 127 // Maps audio device types to string values. This map must be in sync 128 // with the device types above. 129 // TODO(henrika): add support for proper detection of device names and 130 // localize the name strings by using resource strings. 131 // See http://crbug.com/333208 for details. 132 private static final String[] DEVICE_NAMES = new String[] { 133 "Speakerphone", 134 "Wired headset", // With or without microphone. 135 "Headset earpiece", // Only available on mobile phones. 136 "Bluetooth headset", // Requires BLUETOOTH permission. 137 }; 138 139 // List of valid device types. 140 private static final Integer[] VALID_DEVICES = new Integer[] { 141 DEVICE_SPEAKERPHONE, 142 DEVICE_WIRED_HEADSET, 143 DEVICE_EARPIECE, 144 DEVICE_BLUETOOTH_HEADSET, 145 }; 146 147 // Bluetooth audio SCO states. Example of valid state sequence: 148 // SCO_INVALID -> SCO_TURNING_ON -> SCO_ON -> SCO_TURNING_OFF -> SCO_OFF. 149 private static final int STATE_BLUETOOTH_SCO_INVALID = -1; 150 private static final int STATE_BLUETOOTH_SCO_OFF = 0; 151 private static final int STATE_BLUETOOTH_SCO_ON = 1; 152 private static final int STATE_BLUETOOTH_SCO_TURNING_ON = 2; 153 private static final int STATE_BLUETOOTH_SCO_TURNING_OFF = 3; 154 155 // Use 44.1kHz as the default sampling rate. 156 private static final int DEFAULT_SAMPLING_RATE = 44100; 157 // Randomly picked up frame size which is close to return value on N4. 158 // Return this value when getProperty(PROPERTY_OUTPUT_FRAMES_PER_BUFFER) 159 // fails. 160 private static final int DEFAULT_FRAME_PER_BUFFER = 256; 161 162 private final AudioManager mAudioManager; 163 private final Context mContext; 164 private final long mNativeAudioManagerAndroid; 165 166 // Enabled during initialization if BLUETOOTH permission is granted. 167 private boolean mHasBluetoothPermission = false; 168 169 private int mSavedAudioMode = AudioManager.MODE_INVALID; 170 171 // Stores the audio states related to Bluetooth SCO audio, where some 172 // states are needed to keep track of intermediate states while the SCO 173 // channel is enabled or disabled (switching state can take a few seconds). 174 private int mBluetoothScoState = STATE_BLUETOOTH_SCO_INVALID; 175 176 private boolean mIsInitialized = false; 177 private boolean mSavedIsSpeakerphoneOn; 178 private boolean mSavedIsMicrophoneMute; 179 180 // Id of the requested audio device. Can only be modified by 181 // call to setDevice(). 182 private int mRequestedAudioDevice = DEVICE_INVALID; 183 184 // This class should be created, initialized and closed on the audio thread 185 // in the audio manager. We use |mNonThreadSafe| to ensure that this is 186 // the case. Only active when |DEBUG| is set to true. 187 private final NonThreadSafe mNonThreadSafe = new NonThreadSafe(); 188 189 // Lock to protect |mAudioDevices| and |mRequestedAudioDevice| which can 190 // be accessed from the main thread and the audio manager thread. 191 private final Object mLock = new Object(); 192 193 // Contains a list of currently available audio devices. 194 private boolean[] mAudioDevices = new boolean[DEVICE_COUNT]; 195 196 private final ContentResolver mContentResolver; 197 private ContentObserver mSettingsObserver = null; 198 private HandlerThread mSettingsObserverThread = null; 199 private int mCurrentVolume; 200 201 // Broadcast receiver for wired headset intent broadcasts. 202 private BroadcastReceiver mWiredHeadsetReceiver; 203 204 // Broadcast receiver for Bluetooth headset intent broadcasts. 205 // Utilized to detect changes in Bluetooth headset availability. 206 private BroadcastReceiver mBluetoothHeadsetReceiver; 207 208 // Broadcast receiver for Bluetooth SCO broadcasts. 209 // Utilized to detect if BT SCO streaming is on or off. 210 private BroadcastReceiver mBluetoothScoReceiver; 211 212 /** Construction */ 213 @CalledByNative 214 private static AudioManagerAndroid createAudioManagerAndroid( 215 Context context, 216 long nativeAudioManagerAndroid) { 217 return new AudioManagerAndroid(context, nativeAudioManagerAndroid); 218 } 219 220 private AudioManagerAndroid(Context context, long nativeAudioManagerAndroid) { 221 mContext = context; 222 mNativeAudioManagerAndroid = nativeAudioManagerAndroid; 223 mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 224 mContentResolver = mContext.getContentResolver(); 225 } 226 227 /** 228 * Saves the initial speakerphone and microphone state. 229 * Populates the list of available audio devices and registers receivers 230 * for broadcast intents related to wired headset and Bluetooth devices. 231 */ 232 @CalledByNative 233 private void init() { 234 checkIfCalledOnValidThread(); 235 if (DEBUG) logd("init"); 236 if (DEBUG) logDeviceInfo(); 237 if (mIsInitialized) 238 return; 239 240 // Initialize audio device list with things we know is always available. 241 mAudioDevices[DEVICE_EARPIECE] = hasEarpiece(); 242 mAudioDevices[DEVICE_WIRED_HEADSET] = hasWiredHeadset(); 243 mAudioDevices[DEVICE_SPEAKERPHONE] = true; 244 245 // Register receivers for broadcast intents related to Bluetooth device 246 // and Bluetooth SCO notifications. Requires BLUETOOTH permission. 247 registerBluetoothIntentsIfNeeded(); 248 249 // Register receiver for broadcast intents related to adding/ 250 // removing a wired headset (Intent.ACTION_HEADSET_PLUG). 251 registerForWiredHeadsetIntentBroadcast(); 252 253 mIsInitialized = true; 254 255 if (DEBUG) reportUpdate(); 256 } 257 258 /** 259 * Unregister all previously registered intent receivers and restore 260 * the stored state (stored in {@link #init()}). 261 */ 262 @CalledByNative 263 private void close() { 264 checkIfCalledOnValidThread(); 265 if (DEBUG) logd("close"); 266 if (!mIsInitialized) 267 return; 268 269 stopObservingVolumeChanges(); 270 unregisterForWiredHeadsetIntentBroadcast(); 271 unregisterBluetoothIntentsIfNeeded(); 272 273 mIsInitialized = false; 274 } 275 276 /** 277 * Saves current audio mode and sets audio mode to MODE_IN_COMMUNICATION 278 * if input parameter is true. Restores saved audio mode if input parameter 279 * is false. 280 */ 281 @CalledByNative 282 private void setCommunicationAudioModeOn(boolean on) { 283 if (DEBUG) logd("setCommunicationAudioModeOn(" + on + ")"); 284 285 if (on) { 286 if (mSavedAudioMode != AudioManager.MODE_INVALID) { 287 Log.wtf(TAG, "Audio mode has already been set!"); 288 return; 289 } 290 291 // Store the current audio mode the first time we try to 292 // switch to communication mode. 293 try { 294 mSavedAudioMode = mAudioManager.getMode(); 295 } catch (SecurityException e) { 296 Log.wtf(TAG, "getMode exception: ", e); 297 logDeviceInfo(); 298 } 299 300 // Store microphone mute state and speakerphone state so it can 301 // be restored when closing. 302 mSavedIsSpeakerphoneOn = mAudioManager.isSpeakerphoneOn(); 303 mSavedIsMicrophoneMute = mAudioManager.isMicrophoneMute(); 304 305 try { 306 mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 307 } catch (SecurityException e) { 308 Log.wtf(TAG, "setMode exception: ", e); 309 logDeviceInfo(); 310 } 311 312 // Start observing volume changes to detect when the 313 // voice/communication stream volume is at its lowest level. 314 // It is only possible to pull down the volume slider to about 20% 315 // of the absolute minimum (slider at far left) in communication 316 // mode but we want to be able to mute it completely. 317 startObservingVolumeChanges(); 318 319 } else { 320 if (mSavedAudioMode == AudioManager.MODE_INVALID) { 321 Log.wtf(TAG, "Audio mode has not yet been set!"); 322 return; 323 } 324 325 stopObservingVolumeChanges(); 326 327 // Restore previously stored audio states. 328 setMicrophoneMute(mSavedIsMicrophoneMute); 329 setSpeakerphoneOn(mSavedIsSpeakerphoneOn); 330 331 // Restore the mode that was used before we switched to 332 // communication mode. 333 try { 334 mAudioManager.setMode(mSavedAudioMode); 335 } catch (SecurityException e) { 336 Log.wtf(TAG, "setMode exception: ", e); 337 logDeviceInfo(); 338 } 339 mSavedAudioMode = AudioManager.MODE_INVALID; 340 } 341 } 342 343 /** 344 * Activates, i.e., starts routing audio to, the specified audio device. 345 * 346 * @param deviceId Unique device ID (integer converted to string) 347 * representing the selected device. This string is empty if the so-called 348 * default device is requested. 349 */ 350 @CalledByNative 351 private boolean setDevice(String deviceId) { 352 if (DEBUG) logd("setDevice: " + deviceId); 353 if (!mIsInitialized) 354 return false; 355 int intDeviceId = deviceId.isEmpty() ? 356 DEVICE_DEFAULT : Integer.parseInt(deviceId); 357 358 if (intDeviceId == DEVICE_DEFAULT) { 359 boolean devices[] = null; 360 synchronized (mLock) { 361 devices = mAudioDevices.clone(); 362 mRequestedAudioDevice = DEVICE_DEFAULT; 363 } 364 int defaultDevice = selectDefaultDevice(devices); 365 setAudioDevice(defaultDevice); 366 return true; 367 } 368 369 // A non-default device is specified. Verify that it is valid 370 // device, and if so, start using it. 371 List<Integer> validIds = Arrays.asList(VALID_DEVICES); 372 if (!validIds.contains(intDeviceId) || !mAudioDevices[intDeviceId]) { 373 return false; 374 } 375 synchronized (mLock) { 376 mRequestedAudioDevice = intDeviceId; 377 } 378 setAudioDevice(intDeviceId); 379 return true; 380 } 381 382 /** 383 * @return the current list of available audio devices. 384 * Note that this call does not trigger any update of the list of devices, 385 * it only copies the current state in to the output array. 386 */ 387 @CalledByNative 388 private AudioDeviceName[] getAudioInputDeviceNames() { 389 if (DEBUG) logd("getAudioInputDeviceNames"); 390 if (!mIsInitialized) 391 return null; 392 boolean devices[] = null; 393 synchronized (mLock) { 394 devices = mAudioDevices.clone(); 395 } 396 List<String> list = new ArrayList<String>(); 397 AudioDeviceName[] array = 398 new AudioDeviceName[getNumOfAudioDevices(devices)]; 399 int i = 0; 400 for (int id = 0; id < DEVICE_COUNT; ++id) { 401 if (devices[id]) { 402 array[i] = new AudioDeviceName(id, DEVICE_NAMES[id]); 403 list.add(DEVICE_NAMES[id]); 404 i++; 405 } 406 } 407 if (DEBUG) logd("getAudioInputDeviceNames: " + list); 408 return array; 409 } 410 411 @CalledByNative 412 private int getNativeOutputSampleRate() { 413 if (runningOnJellyBeanMR1OrHigher()) { 414 String sampleRateString = mAudioManager.getProperty( 415 AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); 416 return (sampleRateString == null ? 417 DEFAULT_SAMPLING_RATE : Integer.parseInt(sampleRateString)); 418 } else { 419 return DEFAULT_SAMPLING_RATE; 420 } 421 } 422 423 /** 424 * Returns the minimum frame size required for audio input. 425 * 426 * @param sampleRate sampling rate 427 * @param channels number of channels 428 */ 429 @CalledByNative 430 private static int getMinInputFrameSize(int sampleRate, int channels) { 431 int channelConfig; 432 if (channels == 1) { 433 channelConfig = AudioFormat.CHANNEL_IN_MONO; 434 } else if (channels == 2) { 435 channelConfig = AudioFormat.CHANNEL_IN_STEREO; 436 } else { 437 return -1; 438 } 439 return AudioRecord.getMinBufferSize( 440 sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) / 2 / channels; 441 } 442 443 /** 444 * Returns the minimum frame size required for audio output. 445 * 446 * @param sampleRate sampling rate 447 * @param channels number of channels 448 */ 449 @CalledByNative 450 private static int getMinOutputFrameSize(int sampleRate, int channels) { 451 int channelConfig; 452 if (channels == 1) { 453 channelConfig = AudioFormat.CHANNEL_OUT_MONO; 454 } else if (channels == 2) { 455 channelConfig = AudioFormat.CHANNEL_OUT_STEREO; 456 } else { 457 return -1; 458 } 459 return AudioTrack.getMinBufferSize( 460 sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) / 2 / channels; 461 } 462 463 @CalledByNative 464 private boolean isAudioLowLatencySupported() { 465 return mContext.getPackageManager().hasSystemFeature( 466 PackageManager.FEATURE_AUDIO_LOW_LATENCY); 467 } 468 469 @CalledByNative 470 private int getAudioLowLatencyOutputFrameSize() { 471 String framesPerBuffer = 472 mAudioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); 473 return (framesPerBuffer == null ? 474 DEFAULT_FRAME_PER_BUFFER : Integer.parseInt(framesPerBuffer)); 475 } 476 477 @CalledByNative 478 private static boolean shouldUseAcousticEchoCanceler() { 479 // AcousticEchoCanceler was added in API level 16 (Jelly Bean). 480 if (!runningOnJellyBeanOrHigher()) { 481 return false; 482 } 483 484 // Verify that this device is among the supported/tested models. 485 List<String> supportedModels = Arrays.asList(SUPPORTED_AEC_MODELS); 486 if (!supportedModels.contains(Build.MODEL)) { 487 return false; 488 } 489 if (DEBUG && AcousticEchoCanceler.isAvailable()) { 490 logd("Approved for use of hardware acoustic echo canceler."); 491 } 492 493 // As a final check, verify that the device supports acoustic echo 494 // cancellation. 495 return AcousticEchoCanceler.isAvailable(); 496 } 497 498 /** 499 * Helper method for debugging purposes. Logs message if method is not 500 * called on same thread as this object was created on. 501 */ 502 private void checkIfCalledOnValidThread() { 503 if (DEBUG && !mNonThreadSafe.calledOnValidThread()) { 504 Log.wtf(TAG, "Method is not called on valid thread!"); 505 } 506 } 507 508 /** 509 * Register for BT intents if we have the BLUETOOTH permission. 510 * Also extends the list of available devices with a BT device if one exists. 511 */ 512 private void registerBluetoothIntentsIfNeeded() { 513 // Check if this process has the BLUETOOTH permission or not. 514 mHasBluetoothPermission = hasBluetoothPermission(); 515 516 // Add a Bluetooth headset to the list of available devices if a BT 517 // headset is detected and if we have the BLUETOOTH permission. 518 // We must do this initial check using a dedicated method since the 519 // broadcasted intent BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED 520 // is not sticky and will only be received if a BT headset is connected 521 // after this method has been called. 522 if (!mHasBluetoothPermission) { 523 return; 524 } 525 mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = hasBluetoothHeadset(); 526 527 // Register receivers for broadcast intents related to changes in 528 // Bluetooth headset availability and usage of the SCO channel. 529 registerForBluetoothHeadsetIntentBroadcast(); 530 registerForBluetoothScoIntentBroadcast(); 531 } 532 533 /** Unregister for BT intents if a registration has been made. */ 534 private void unregisterBluetoothIntentsIfNeeded() { 535 if (mHasBluetoothPermission) { 536 mAudioManager.stopBluetoothSco(); 537 unregisterForBluetoothHeadsetIntentBroadcast(); 538 unregisterForBluetoothScoIntentBroadcast(); 539 } 540 } 541 542 /** Sets the speaker phone mode. */ 543 private void setSpeakerphoneOn(boolean on) { 544 boolean wasOn = mAudioManager.isSpeakerphoneOn(); 545 if (wasOn == on) { 546 return; 547 } 548 mAudioManager.setSpeakerphoneOn(on); 549 } 550 551 /** Sets the microphone mute state. */ 552 private void setMicrophoneMute(boolean on) { 553 boolean wasMuted = mAudioManager.isMicrophoneMute(); 554 if (wasMuted == on) { 555 return; 556 } 557 mAudioManager.setMicrophoneMute(on); 558 } 559 560 /** Gets the current microphone mute state. */ 561 private boolean isMicrophoneMute() { 562 return mAudioManager.isMicrophoneMute(); 563 } 564 565 /** Gets the current earpiece state. */ 566 private boolean hasEarpiece() { 567 return mContext.getPackageManager().hasSystemFeature( 568 PackageManager.FEATURE_TELEPHONY); 569 } 570 571 /** 572 * Checks whether a wired headset is connected or not. 573 * This is not a valid indication that audio playback is actually over 574 * the wired headset as audio routing depends on other conditions. We 575 * only use it as an early indicator (during initialization) of an attached 576 * wired headset. 577 */ 578 @Deprecated 579 private boolean hasWiredHeadset() { 580 return mAudioManager.isWiredHeadsetOn(); 581 } 582 583 /** Checks if the process has BLUETOOTH permission or not. */ 584 private boolean hasBluetoothPermission() { 585 boolean hasBluetooth = mContext.checkPermission( 586 android.Manifest.permission.BLUETOOTH, 587 Process.myPid(), 588 Process.myUid()) == PackageManager.PERMISSION_GRANTED; 589 if (DEBUG && !hasBluetooth) { 590 logd("BLUETOOTH permission is missing!"); 591 } 592 return hasBluetooth; 593 } 594 595 /** 596 * Gets the current Bluetooth headset state. 597 * android.bluetooth.BluetoothAdapter.getProfileConnectionState() requires 598 * the BLUETOOTH permission. 599 */ 600 private boolean hasBluetoothHeadset() { 601 if (!mHasBluetoothPermission) { 602 Log.wtf(TAG, "hasBluetoothHeadset() requires BLUETOOTH permission!"); 603 return false; 604 } 605 606 // To get a BluetoothAdapter representing the local Bluetooth adapter, 607 // when running on JELLY_BEAN_MR1 (4.2) and below, call the static 608 // getDefaultAdapter() method; when running on JELLY_BEAN_MR2 (4.3) and 609 // higher, retrieve it through getSystemService(String) with 610 // BLUETOOTH_SERVICE. 611 BluetoothAdapter btAdapter = null; 612 if (runningOnJellyBeanMR2OrHigher()) { 613 // Use BluetoothManager to get the BluetoothAdapter for 614 // Android 4.3 and above. 615 try { 616 BluetoothManager btManager = 617 (BluetoothManager)mContext.getSystemService( 618 Context.BLUETOOTH_SERVICE); 619 btAdapter = btManager.getAdapter(); 620 } catch (Exception e) { 621 Log.wtf(TAG, "BluetoothManager.getAdapter exception", e); 622 return false; 623 } 624 } else { 625 // Use static method for Android 4.2 and below to get the 626 // BluetoothAdapter. 627 try { 628 btAdapter = BluetoothAdapter.getDefaultAdapter(); 629 } catch (Exception e) { 630 Log.wtf(TAG, "BluetoothAdapter.getDefaultAdapter exception", e); 631 return false; 632 } 633 } 634 635 int profileConnectionState; 636 try { 637 profileConnectionState = btAdapter.getProfileConnectionState( 638 android.bluetooth.BluetoothProfile.HEADSET); 639 } catch (Exception e) { 640 Log.wtf(TAG, "BluetoothAdapter.getProfileConnectionState exception", e); 641 profileConnectionState = 642 android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; 643 } 644 645 // Ensure that Bluetooth is enabled and that a device which supports the 646 // headset and handsfree profile is connected. 647 // TODO(henrika): it is possible that btAdapter.isEnabled() is 648 // redundant. It might be sufficient to only check the profile state. 649 return btAdapter.isEnabled() && profileConnectionState == 650 android.bluetooth.BluetoothProfile.STATE_CONNECTED; 651 } 652 653 /** 654 * Registers receiver for the broadcasted intent when a wired headset is 655 * plugged in or unplugged. The received intent will have an extra 656 * 'state' value where 0 means unplugged, and 1 means plugged. 657 */ 658 private void registerForWiredHeadsetIntentBroadcast() { 659 IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); 660 661 /** Receiver which handles changes in wired headset availability. */ 662 mWiredHeadsetReceiver = new BroadcastReceiver() { 663 private static final int STATE_UNPLUGGED = 0; 664 private static final int STATE_PLUGGED = 1; 665 private static final int HAS_NO_MIC = 0; 666 private static final int HAS_MIC = 1; 667 668 @Override 669 public void onReceive(Context context, Intent intent) { 670 int state = intent.getIntExtra("state", STATE_UNPLUGGED); 671 if (DEBUG) { 672 int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); 673 String name = intent.getStringExtra("name"); 674 logd("BroadcastReceiver.onReceive: a=" + intent.getAction() + 675 ", s=" + state + 676 ", m=" + microphone + 677 ", n=" + name + 678 ", sb=" + isInitialStickyBroadcast()); 679 } 680 switch (state) { 681 case STATE_UNPLUGGED: 682 synchronized (mLock) { 683 // Wired headset and earpiece are mutually exclusive. 684 mAudioDevices[DEVICE_WIRED_HEADSET] = false; 685 if (hasEarpiece()) { 686 mAudioDevices[DEVICE_EARPIECE] = true; 687 } 688 } 689 break; 690 case STATE_PLUGGED: 691 synchronized (mLock) { 692 // Wired headset and earpiece are mutually exclusive. 693 mAudioDevices[DEVICE_WIRED_HEADSET] = true; 694 mAudioDevices[DEVICE_EARPIECE] = false; 695 } 696 break; 697 default: 698 loge("Invalid state!"); 699 break; 700 } 701 702 // Update the existing device selection, but only if a specific 703 // device has already been selected explicitly. 704 if (deviceHasBeenRequested()) { 705 updateDeviceActivation(); 706 } else if (DEBUG) { 707 reportUpdate(); 708 } 709 } 710 }; 711 712 // Note: the intent we register for here is sticky, so it'll tell us 713 // immediately what the last action was (plugged or unplugged). 714 // It will enable us to set the speakerphone correctly. 715 mContext.registerReceiver(mWiredHeadsetReceiver, filter); 716 } 717 718 /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */ 719 private void unregisterForWiredHeadsetIntentBroadcast() { 720 mContext.unregisterReceiver(mWiredHeadsetReceiver); 721 mWiredHeadsetReceiver = null; 722 } 723 724 /** 725 * Registers receiver for the broadcasted intent related to BT headset 726 * availability or a change in connection state of the local Bluetooth 727 * adapter. Example: triggers when the BT device is turned on or off. 728 * BLUETOOTH permission is required to receive this one. 729 */ 730 private void registerForBluetoothHeadsetIntentBroadcast() { 731 IntentFilter filter = new IntentFilter( 732 android.bluetooth.BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 733 734 /** Receiver which handles changes in BT headset availability. */ 735 mBluetoothHeadsetReceiver = new BroadcastReceiver() { 736 @Override 737 public void onReceive(Context context, Intent intent) { 738 // A change in connection state of the Headset profile has 739 // been detected, e.g. BT headset has been connected or 740 // disconnected. This broadcast is *not* sticky. 741 int profileState = intent.getIntExtra( 742 android.bluetooth.BluetoothHeadset.EXTRA_STATE, 743 android.bluetooth.BluetoothHeadset.STATE_DISCONNECTED); 744 if (DEBUG) { 745 logd("BroadcastReceiver.onReceive: a=" + intent.getAction() + 746 ", s=" + profileState + 747 ", sb=" + isInitialStickyBroadcast()); 748 } 749 750 switch (profileState) { 751 case android.bluetooth.BluetoothProfile.STATE_DISCONNECTED: 752 // We do not have to explicitly call stopBluetoothSco() 753 // since BT SCO will be disconnected automatically when 754 // the BT headset is disabled. 755 synchronized (mLock) { 756 // Remove the BT device from the list of devices. 757 mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = false; 758 } 759 break; 760 case android.bluetooth.BluetoothProfile.STATE_CONNECTED: 761 synchronized (mLock) { 762 // Add the BT device to the list of devices. 763 mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = true; 764 } 765 break; 766 case android.bluetooth.BluetoothProfile.STATE_CONNECTING: 767 // Bluetooth service is switching from off to on. 768 break; 769 case android.bluetooth.BluetoothProfile.STATE_DISCONNECTING: 770 // Bluetooth service is switching from on to off. 771 break; 772 default: 773 loge("Invalid state!"); 774 break; 775 } 776 777 // Update the existing device selection, but only if a specific 778 // device has already been selected explicitly. 779 if (deviceHasBeenRequested()) { 780 updateDeviceActivation(); 781 } else if (DEBUG) { 782 reportUpdate(); 783 } 784 } 785 }; 786 787 mContext.registerReceiver(mBluetoothHeadsetReceiver, filter); 788 } 789 790 private void unregisterForBluetoothHeadsetIntentBroadcast() { 791 mContext.unregisterReceiver(mBluetoothHeadsetReceiver); 792 mBluetoothHeadsetReceiver = null; 793 } 794 795 /** 796 * Registers receiver for the broadcasted intent related the existence 797 * of a BT SCO channel. Indicates if BT SCO streaming is on or off. 798 */ 799 private void registerForBluetoothScoIntentBroadcast() { 800 IntentFilter filter = new IntentFilter( 801 AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); 802 803 /** BroadcastReceiver implementation which handles changes in BT SCO. */ 804 mBluetoothScoReceiver = new BroadcastReceiver() { 805 @Override 806 public void onReceive(Context context, Intent intent) { 807 int state = intent.getIntExtra( 808 AudioManager.EXTRA_SCO_AUDIO_STATE, 809 AudioManager.SCO_AUDIO_STATE_DISCONNECTED); 810 if (DEBUG) { 811 logd("BroadcastReceiver.onReceive: a=" + intent.getAction() + 812 ", s=" + state + 813 ", sb=" + isInitialStickyBroadcast()); 814 } 815 816 switch (state) { 817 case AudioManager.SCO_AUDIO_STATE_CONNECTED: 818 mBluetoothScoState = STATE_BLUETOOTH_SCO_ON; 819 break; 820 case AudioManager.SCO_AUDIO_STATE_DISCONNECTED: 821 mBluetoothScoState = STATE_BLUETOOTH_SCO_OFF; 822 break; 823 case AudioManager.SCO_AUDIO_STATE_CONNECTING: 824 // do nothing 825 break; 826 default: 827 loge("Invalid state!"); 828 } 829 if (DEBUG) { 830 reportUpdate(); 831 } 832 } 833 }; 834 835 mContext.registerReceiver(mBluetoothScoReceiver, filter); 836 } 837 838 private void unregisterForBluetoothScoIntentBroadcast() { 839 mContext.unregisterReceiver(mBluetoothScoReceiver); 840 mBluetoothScoReceiver = null; 841 } 842 843 /** Enables BT audio using the SCO audio channel. */ 844 private void startBluetoothSco() { 845 if (!mHasBluetoothPermission) { 846 return; 847 } 848 if (mBluetoothScoState == STATE_BLUETOOTH_SCO_ON || 849 mBluetoothScoState == STATE_BLUETOOTH_SCO_TURNING_ON) { 850 // Unable to turn on BT in this state. 851 return; 852 } 853 854 // Check if audio is already routed to BT SCO; if so, just update 855 // states but don't try to enable it again. 856 if (mAudioManager.isBluetoothScoOn()) { 857 mBluetoothScoState = STATE_BLUETOOTH_SCO_ON; 858 return; 859 } 860 861 if (DEBUG) logd("startBluetoothSco: turning BT SCO on..."); 862 mBluetoothScoState = STATE_BLUETOOTH_SCO_TURNING_ON; 863 mAudioManager.startBluetoothSco(); 864 } 865 866 /** Disables BT audio using the SCO audio channel. */ 867 private void stopBluetoothSco() { 868 if (!mHasBluetoothPermission) { 869 return; 870 } 871 if (mBluetoothScoState != STATE_BLUETOOTH_SCO_ON && 872 mBluetoothScoState != STATE_BLUETOOTH_SCO_TURNING_ON) { 873 // No need to turn off BT in this state. 874 return; 875 } 876 if (!mAudioManager.isBluetoothScoOn()) { 877 // TODO(henrika): can we do anything else than logging here? 878 loge("Unable to stop BT SCO since it is already disabled!"); 879 return; 880 } 881 882 if (DEBUG) logd("stopBluetoothSco: turning BT SCO off..."); 883 mBluetoothScoState = STATE_BLUETOOTH_SCO_TURNING_OFF; 884 mAudioManager.stopBluetoothSco(); 885 } 886 887 /** 888 * Changes selection of the currently active audio device. 889 * 890 * @param device Specifies the selected audio device. 891 */ 892 private void setAudioDevice(int device) { 893 if (DEBUG) logd("setAudioDevice(device=" + device + ")"); 894 895 // Ensure that the Bluetooth SCO audio channel is always disabled 896 // unless the BT headset device is selected. 897 if (device == DEVICE_BLUETOOTH_HEADSET) { 898 startBluetoothSco(); 899 } else { 900 stopBluetoothSco(); 901 } 902 903 switch (device) { 904 case DEVICE_BLUETOOTH_HEADSET: 905 break; 906 case DEVICE_SPEAKERPHONE: 907 setSpeakerphoneOn(true); 908 break; 909 case DEVICE_WIRED_HEADSET: 910 setSpeakerphoneOn(false); 911 break; 912 case DEVICE_EARPIECE: 913 setSpeakerphoneOn(false); 914 break; 915 default: 916 loge("Invalid audio device selection!"); 917 break; 918 } 919 reportUpdate(); 920 } 921 922 /** 923 * Use a special selection scheme if the default device is selected. 924 * The "most unique" device will be selected; Wired headset first, 925 * then Bluetooth and last the speaker phone. 926 */ 927 private static int selectDefaultDevice(boolean[] devices) { 928 if (devices[DEVICE_WIRED_HEADSET]) { 929 return DEVICE_WIRED_HEADSET; 930 } else if (devices[DEVICE_BLUETOOTH_HEADSET]) { 931 // TODO(henrika): possibly need improvements here if we are 932 // in a state where Bluetooth is turning off. 933 return DEVICE_BLUETOOTH_HEADSET; 934 } 935 return DEVICE_SPEAKERPHONE; 936 } 937 938 /** Returns true if setDevice() has been called with a valid device id. */ 939 private boolean deviceHasBeenRequested() { 940 synchronized (mLock) { 941 return (mRequestedAudioDevice != DEVICE_INVALID); 942 } 943 } 944 945 /** 946 * Updates the active device given the current list of devices and 947 * information about if a specific device has been selected or if 948 * the default device is selected. 949 */ 950 private void updateDeviceActivation() { 951 boolean devices[] = null; 952 int requested = DEVICE_INVALID; 953 synchronized (mLock) { 954 requested = mRequestedAudioDevice; 955 devices = mAudioDevices.clone(); 956 } 957 if (requested == DEVICE_INVALID) { 958 loge("Unable to activate device since no device is selected!"); 959 return; 960 } 961 962 // Update default device if it has been selected explicitly, or 963 // the selected device has been removed from the list. 964 if (requested == DEVICE_DEFAULT || !devices[requested]) { 965 // Get default device given current list and activate the device. 966 int defaultDevice = selectDefaultDevice(devices); 967 setAudioDevice(defaultDevice); 968 } else { 969 // Activate the selected device since we know that it exists in 970 // the list. 971 setAudioDevice(requested); 972 } 973 } 974 975 /** Returns number of available devices */ 976 private static int getNumOfAudioDevices(boolean[] devices) { 977 int count = 0; 978 for (int i = 0; i < DEVICE_COUNT; ++i) { 979 if (devices[i]) 980 ++count; 981 } 982 return count; 983 } 984 985 /** 986 * For now, just log the state change but the idea is that we should 987 * notify a registered state change listener (if any) that there has 988 * been a change in the state. 989 * TODO(henrika): add support for state change listener. 990 */ 991 private void reportUpdate() { 992 synchronized (mLock) { 993 List<String> devices = new ArrayList<String>(); 994 for (int i = 0; i < DEVICE_COUNT; ++i) { 995 if (mAudioDevices[i]) 996 devices.add(DEVICE_NAMES[i]); 997 } 998 if (DEBUG) { 999 logd("reportUpdate: requested=" + mRequestedAudioDevice + 1000 ", btSco=" + mBluetoothScoState + 1001 ", devices=" + devices); 1002 } 1003 } 1004 } 1005 1006 /** Information about the current build, taken from system properties. */ 1007 private void logDeviceInfo() { 1008 logd("Android SDK: " + Build.VERSION.SDK_INT + ", " + 1009 "Release: " + Build.VERSION.RELEASE + ", " + 1010 "Brand: " + Build.BRAND + ", " + 1011 "CPU_ABI: " + Build.CPU_ABI + ", " + 1012 "Device: " + Build.DEVICE + ", " + 1013 "Id: " + Build.ID + ", " + 1014 "Hardware: " + Build.HARDWARE + ", " + 1015 "Manufacturer: " + Build.MANUFACTURER + ", " + 1016 "Model: " + Build.MODEL + ", " + 1017 "Product: " + Build.PRODUCT); 1018 } 1019 1020 /** Trivial helper method for debug logging */ 1021 private static void logd(String msg) { 1022 Log.d(TAG, msg); 1023 } 1024 1025 /** Trivial helper method for error logging */ 1026 private static void loge(String msg) { 1027 Log.e(TAG, msg); 1028 } 1029 1030 /** Start thread which observes volume changes on the voice stream. */ 1031 private void startObservingVolumeChanges() { 1032 if (DEBUG) logd("startObservingVolumeChanges"); 1033 if (mSettingsObserverThread != null) 1034 return; 1035 mSettingsObserverThread = new HandlerThread("SettingsObserver"); 1036 mSettingsObserverThread.start(); 1037 1038 mSettingsObserver = new ContentObserver( 1039 new Handler(mSettingsObserverThread.getLooper())) { 1040 1041 @Override 1042 public void onChange(boolean selfChange) { 1043 if (DEBUG) logd("SettingsObserver.onChange: " + selfChange); 1044 super.onChange(selfChange); 1045 1046 // Ensure that the observer is activated during communication mode. 1047 if (mAudioManager.getMode() != AudioManager.MODE_IN_COMMUNICATION) { 1048 Log.wtf(TAG, "Only enable SettingsObserver in COMM mode!"); 1049 return; 1050 } 1051 1052 // Get stream volume for the voice stream and deliver callback if 1053 // the volume index is zero. It is not possible to move the volume 1054 // slider all the way down in communication mode but the callback 1055 // implementation can ensure that the volume is completely muted. 1056 int volume = mAudioManager.getStreamVolume( 1057 AudioManager.STREAM_VOICE_CALL); 1058 if (DEBUG) logd("nativeSetMute: " + (volume == 0)); 1059 nativeSetMute(mNativeAudioManagerAndroid, (volume == 0)); 1060 } 1061 }; 1062 1063 mContentResolver.registerContentObserver( 1064 Settings.System.CONTENT_URI, true, mSettingsObserver); 1065 } 1066 1067 /** Quit observer thread and stop listening for volume changes. */ 1068 private void stopObservingVolumeChanges() { 1069 if (DEBUG) logd("stopObservingVolumeChanges"); 1070 if (mSettingsObserverThread == null) 1071 return; 1072 1073 mContentResolver.unregisterContentObserver(mSettingsObserver); 1074 mSettingsObserver = null; 1075 1076 mSettingsObserverThread.quit(); 1077 try { 1078 mSettingsObserverThread.join(); 1079 } catch (InterruptedException e) { 1080 Log.wtf(TAG, "Thread.join() exception: ", e); 1081 } 1082 mSettingsObserverThread = null; 1083 } 1084 1085 private native void nativeSetMute(long nativeAudioManagerAndroid, boolean muted); 1086} 1087