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.
5 package org.chromium.media;
7 import android.bluetooth.BluetoothAdapter;
8 import android.bluetooth.BluetoothManager;
9 import android.content.BroadcastReceiver;
10 import android.content.ContentResolver;
11 import android.content.Context;
12 import android.content.Intent;
13 import android.content.IntentFilter;
14 import android.content.pm.PackageManager;
15 import android.database.ContentObserver;
16 import android.media.AudioFormat;
17 import android.media.AudioManager;
18 import android.media.AudioRecord;
19 import android.media.AudioTrack;
20 import android.media.audiofx.AcousticEchoCanceler;
21 import android.os.Build;
22 import android.os.Handler;
23 import android.os.HandlerThread;
24 import android.os.Process;
25 import android.provider.Settings;
26 import android.util.Log;
28 import org.chromium.base.CalledByNative;
29 import org.chromium.base.JNINamespace;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.List;
35 @JNINamespace("media")
36 class AudioManagerAndroid {
37 private static final String TAG = "AudioManagerAndroid";
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;
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.
49 private static class NonThreadSafe {
50 private final Long mThreadId;
52 public NonThreadSafe() {
54 mThreadId = Thread.currentThread().getId();
56 // Avoids "Unread field" issue reported by findbugs.
62 * Checks if the method is called on the valid thread.
63 * Assigns the current thread if no thread was assigned.
65 public boolean calledOnValidThread() {
67 return mThreadId.equals(Thread.currentThread().getId());
73 private static boolean runningOnJellyBeanOrHigher() {
74 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
77 private static boolean runningOnJellyBeanMR1OrHigher() {
78 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
81 private static boolean runningOnJellyBeanMR2OrHigher() {
82 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
85 /** Simple container for device information. */
86 private static class AudioDeviceName {
87 private final int mId;
88 private final String mName;
90 private AudioDeviceName(int id, String name) {
95 @CalledByNative("AudioDeviceName")
96 private String id() { return String.valueOf(mId); }
98 @CalledByNative("AudioDeviceName")
99 private String name() { return mName; }
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)
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;
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[] {
134 "Wired headset", // With or without microphone.
135 "Headset earpiece", // Only available on mobile phones.
136 "Bluetooth headset", // Requires BLUETOOTH permission.
139 // List of valid device types.
140 private static final Integer[] VALID_DEVICES = new Integer[] {
142 DEVICE_WIRED_HEADSET,
144 DEVICE_BLUETOOTH_HEADSET,
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;
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)
160 private static final int DEFAULT_FRAME_PER_BUFFER = 256;
162 private final AudioManager mAudioManager;
163 private final Context mContext;
164 private final long mNativeAudioManagerAndroid;
166 // Enabled during initialization if MODIFY_AUDIO_SETTINGS permission is
167 // granted. Required to shift system-wide audio settings.
168 private boolean mHasModifyAudioSettingsPermission = false;
170 // Enabled during initialization if RECORD_AUDIO permission is granted.
171 private boolean mHasRecordAudioPermission = false;
173 // Enabled during initialization if BLUETOOTH permission is granted.
174 private boolean mHasBluetoothPermission = false;
176 private int mSavedAudioMode = AudioManager.MODE_INVALID;
178 // Stores the audio states related to Bluetooth SCO audio, where some
179 // states are needed to keep track of intermediate states while the SCO
180 // channel is enabled or disabled (switching state can take a few seconds).
181 private int mBluetoothScoState = STATE_BLUETOOTH_SCO_INVALID;
183 private boolean mIsInitialized = false;
184 private boolean mSavedIsSpeakerphoneOn;
185 private boolean mSavedIsMicrophoneMute;
187 // Id of the requested audio device. Can only be modified by
188 // call to setDevice().
189 private int mRequestedAudioDevice = DEVICE_INVALID;
191 // This class should be created, initialized and closed on the audio thread
192 // in the audio manager. We use |mNonThreadSafe| to ensure that this is
193 // the case. Only active when |DEBUG| is set to true.
194 private final NonThreadSafe mNonThreadSafe = new NonThreadSafe();
196 // Lock to protect |mAudioDevices| and |mRequestedAudioDevice| which can
197 // be accessed from the main thread and the audio manager thread.
198 private final Object mLock = new Object();
200 // Contains a list of currently available audio devices.
201 private boolean[] mAudioDevices = new boolean[DEVICE_COUNT];
203 private final ContentResolver mContentResolver;
204 private ContentObserver mSettingsObserver = null;
205 private HandlerThread mSettingsObserverThread = null;
206 private int mCurrentVolume;
208 // Broadcast receiver for wired headset intent broadcasts.
209 private BroadcastReceiver mWiredHeadsetReceiver;
211 // Broadcast receiver for Bluetooth headset intent broadcasts.
212 // Utilized to detect changes in Bluetooth headset availability.
213 private BroadcastReceiver mBluetoothHeadsetReceiver;
215 // Broadcast receiver for Bluetooth SCO broadcasts.
216 // Utilized to detect if BT SCO streaming is on or off.
217 private BroadcastReceiver mBluetoothScoReceiver;
221 private static AudioManagerAndroid createAudioManagerAndroid(
223 long nativeAudioManagerAndroid) {
224 return new AudioManagerAndroid(context, nativeAudioManagerAndroid);
227 private AudioManagerAndroid(Context context, long nativeAudioManagerAndroid) {
229 mNativeAudioManagerAndroid = nativeAudioManagerAndroid;
230 mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
231 mContentResolver = mContext.getContentResolver();
235 * Saves the initial speakerphone and microphone state.
236 * Populates the list of available audio devices and registers receivers
237 * for broadcast intents related to wired headset and Bluetooth devices.
240 private void init() {
241 checkIfCalledOnValidThread();
242 if (DEBUG) logd("init");
243 if (DEBUG) logDeviceInfo();
247 // Check if process has MODIFY_AUDIO_SETTINGS and RECORD_AUDIO
248 // permissions. Both are required for full functionality.
249 mHasModifyAudioSettingsPermission = hasPermission(
250 android.Manifest.permission.MODIFY_AUDIO_SETTINGS);
251 if (DEBUG && !mHasModifyAudioSettingsPermission) {
252 logd("MODIFY_AUDIO_SETTINGS permission is missing");
254 mHasRecordAudioPermission = hasPermission(
255 android.Manifest.permission.RECORD_AUDIO);
256 if (DEBUG && !mHasRecordAudioPermission) {
257 logd("RECORD_AUDIO permission is missing");
260 // Initialize audio device list with things we know is always available.
261 mAudioDevices[DEVICE_EARPIECE] = hasEarpiece();
262 mAudioDevices[DEVICE_WIRED_HEADSET] = hasWiredHeadset();
263 mAudioDevices[DEVICE_SPEAKERPHONE] = true;
265 // Register receivers for broadcast intents related to Bluetooth device
266 // and Bluetooth SCO notifications. Requires BLUETOOTH permission.
267 registerBluetoothIntentsIfNeeded();
269 // Register receiver for broadcast intents related to adding/
270 // removing a wired headset (Intent.ACTION_HEADSET_PLUG).
271 registerForWiredHeadsetIntentBroadcast();
273 mIsInitialized = true;
275 if (DEBUG) reportUpdate();
279 * Unregister all previously registered intent receivers and restore
280 * the stored state (stored in {@link #init()}).
283 private void close() {
284 checkIfCalledOnValidThread();
285 if (DEBUG) logd("close");
289 stopObservingVolumeChanges();
290 unregisterForWiredHeadsetIntentBroadcast();
291 unregisterBluetoothIntentsIfNeeded();
293 mIsInitialized = false;
297 * Saves current audio mode and sets audio mode to MODE_IN_COMMUNICATION
298 * if input parameter is true. Restores saved audio mode if input parameter
300 * Required permission: android.Manifest.permission.MODIFY_AUDIO_SETTINGS.
303 private void setCommunicationAudioModeOn(boolean on) {
304 if (DEBUG) logd("setCommunicationAudioModeOn(" + on + ")");
306 // The MODIFY_AUDIO_SETTINGS permission is required to allow an
307 // application to modify global audio settings.
308 if (!mHasModifyAudioSettingsPermission) {
309 Log.w(TAG, "MODIFY_AUDIO_SETTINGS is missing => client will run " +
310 "with reduced functionality");
315 if (mSavedAudioMode != AudioManager.MODE_INVALID) {
316 throw new IllegalStateException("Audio mode has already been set");
319 // Store the current audio mode the first time we try to
320 // switch to communication mode.
322 mSavedAudioMode = mAudioManager.getMode();
323 } catch (SecurityException e) {
329 // Store microphone mute state and speakerphone state so it can
330 // be restored when closing.
331 mSavedIsSpeakerphoneOn = mAudioManager.isSpeakerphoneOn();
332 mSavedIsMicrophoneMute = mAudioManager.isMicrophoneMute();
335 mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
336 } catch (SecurityException e) {
341 // Start observing volume changes to detect when the
342 // voice/communication stream volume is at its lowest level.
343 // It is only possible to pull down the volume slider to about 20%
344 // of the absolute minimum (slider at far left) in communication
345 // mode but we want to be able to mute it completely.
346 startObservingVolumeChanges();
349 if (mSavedAudioMode == AudioManager.MODE_INVALID) {
350 throw new IllegalStateException("Audio mode has not yet been set");
353 stopObservingVolumeChanges();
355 // Restore previously stored audio states.
356 setMicrophoneMute(mSavedIsMicrophoneMute);
357 setSpeakerphoneOn(mSavedIsSpeakerphoneOn);
359 // Restore the mode that was used before we switched to
360 // communication mode.
362 mAudioManager.setMode(mSavedAudioMode);
363 } catch (SecurityException e) {
367 mSavedAudioMode = AudioManager.MODE_INVALID;
372 * Activates, i.e., starts routing audio to, the specified audio device.
374 * @param deviceId Unique device ID (integer converted to string)
375 * representing the selected device. This string is empty if the so-called
376 * default device is requested.
377 * Required permissions: android.Manifest.permission.MODIFY_AUDIO_SETTINGS
378 * and android.Manifest.permission.RECORD_AUDIO.
381 private boolean setDevice(String deviceId) {
382 if (DEBUG) logd("setDevice: " + deviceId);
385 if (!mHasModifyAudioSettingsPermission || !mHasRecordAudioPermission) {
386 Log.w(TAG, "Requires MODIFY_AUDIO_SETTINGS and RECORD_AUDIO");
387 Log.w(TAG, "Selected device will not be available for recording");
391 int intDeviceId = deviceId.isEmpty() ?
392 DEVICE_DEFAULT : Integer.parseInt(deviceId);
394 if (intDeviceId == DEVICE_DEFAULT) {
395 boolean devices[] = null;
396 synchronized (mLock) {
397 devices = mAudioDevices.clone();
398 mRequestedAudioDevice = DEVICE_DEFAULT;
400 int defaultDevice = selectDefaultDevice(devices);
401 setAudioDevice(defaultDevice);
405 // A non-default device is specified. Verify that it is valid
406 // device, and if so, start using it.
407 List<Integer> validIds = Arrays.asList(VALID_DEVICES);
408 if (!validIds.contains(intDeviceId) || !mAudioDevices[intDeviceId]) {
411 synchronized (mLock) {
412 mRequestedAudioDevice = intDeviceId;
414 setAudioDevice(intDeviceId);
419 * @return the current list of available audio devices.
420 * Note that this call does not trigger any update of the list of devices,
421 * it only copies the current state in to the output array.
422 * Required permissions: android.Manifest.permission.MODIFY_AUDIO_SETTINGS
423 * and android.Manifest.permission.RECORD_AUDIO.
426 private AudioDeviceName[] getAudioInputDeviceNames() {
427 if (DEBUG) logd("getAudioInputDeviceNames");
430 if (!mHasModifyAudioSettingsPermission || !mHasRecordAudioPermission) {
431 Log.w(TAG, "Requires MODIFY_AUDIO_SETTINGS and RECORD_AUDIO");
432 Log.w(TAG, "No audio device will be available for recording");
436 boolean devices[] = null;
437 synchronized (mLock) {
438 devices = mAudioDevices.clone();
440 List<String> list = new ArrayList<String>();
441 AudioDeviceName[] array =
442 new AudioDeviceName[getNumOfAudioDevices(devices)];
444 for (int id = 0; id < DEVICE_COUNT; ++id) {
446 array[i] = new AudioDeviceName(id, DEVICE_NAMES[id]);
447 list.add(DEVICE_NAMES[id]);
451 if (DEBUG) logd("getAudioInputDeviceNames: " + list);
456 private int getNativeOutputSampleRate() {
457 if (runningOnJellyBeanMR1OrHigher()) {
458 String sampleRateString = mAudioManager.getProperty(
459 AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
460 return (sampleRateString == null ?
461 DEFAULT_SAMPLING_RATE : Integer.parseInt(sampleRateString));
463 return DEFAULT_SAMPLING_RATE;
468 * Returns the minimum frame size required for audio input.
470 * @param sampleRate sampling rate
471 * @param channels number of channels
474 private static int getMinInputFrameSize(int sampleRate, int channels) {
477 channelConfig = AudioFormat.CHANNEL_IN_MONO;
478 } else if (channels == 2) {
479 channelConfig = AudioFormat.CHANNEL_IN_STEREO;
483 return AudioRecord.getMinBufferSize(
484 sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) / 2 / channels;
488 * Returns the minimum frame size required for audio output.
490 * @param sampleRate sampling rate
491 * @param channels number of channels
494 private static int getMinOutputFrameSize(int sampleRate, int channels) {
497 channelConfig = AudioFormat.CHANNEL_OUT_MONO;
498 } else if (channels == 2) {
499 channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
503 return AudioTrack.getMinBufferSize(
504 sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) / 2 / channels;
508 private boolean isAudioLowLatencySupported() {
509 return mContext.getPackageManager().hasSystemFeature(
510 PackageManager.FEATURE_AUDIO_LOW_LATENCY);
514 private int getAudioLowLatencyOutputFrameSize() {
515 String framesPerBuffer =
516 mAudioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
517 return (framesPerBuffer == null ?
518 DEFAULT_FRAME_PER_BUFFER : Integer.parseInt(framesPerBuffer));
522 private static boolean shouldUseAcousticEchoCanceler() {
523 // AcousticEchoCanceler was added in API level 16 (Jelly Bean).
524 if (!runningOnJellyBeanOrHigher()) {
528 // Verify that this device is among the supported/tested models.
529 List<String> supportedModels = Arrays.asList(SUPPORTED_AEC_MODELS);
530 if (!supportedModels.contains(Build.MODEL)) {
533 if (DEBUG && AcousticEchoCanceler.isAvailable()) {
534 logd("Approved for use of hardware acoustic echo canceler.");
537 // As a final check, verify that the device supports acoustic echo
539 return AcousticEchoCanceler.isAvailable();
543 * Helper method for debugging purposes. Ensures that method is
544 * called on same thread as this object was created on.
546 private void checkIfCalledOnValidThread() {
547 if (DEBUG && !mNonThreadSafe.calledOnValidThread()) {
548 throw new IllegalStateException("Method is not called on valid thread");
553 * Register for BT intents if we have the BLUETOOTH permission.
554 * Also extends the list of available devices with a BT device if one exists.
556 private void registerBluetoothIntentsIfNeeded() {
557 // Check if this process has the BLUETOOTH permission or not.
558 mHasBluetoothPermission = hasPermission(
559 android.Manifest.permission.BLUETOOTH);
561 // Add a Bluetooth headset to the list of available devices if a BT
562 // headset is detected and if we have the BLUETOOTH permission.
563 // We must do this initial check using a dedicated method since the
564 // broadcasted intent BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED
565 // is not sticky and will only be received if a BT headset is connected
566 // after this method has been called.
567 if (!mHasBluetoothPermission) {
568 Log.w(TAG, "Requires BLUETOOTH permission");
571 mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = hasBluetoothHeadset();
573 // Register receivers for broadcast intents related to changes in
574 // Bluetooth headset availability and usage of the SCO channel.
575 registerForBluetoothHeadsetIntentBroadcast();
576 registerForBluetoothScoIntentBroadcast();
579 /** Unregister for BT intents if a registration has been made. */
580 private void unregisterBluetoothIntentsIfNeeded() {
581 if (mHasBluetoothPermission) {
582 mAudioManager.stopBluetoothSco();
583 unregisterForBluetoothHeadsetIntentBroadcast();
584 unregisterForBluetoothScoIntentBroadcast();
588 /** Sets the speaker phone mode. */
589 private void setSpeakerphoneOn(boolean on) {
590 boolean wasOn = mAudioManager.isSpeakerphoneOn();
594 mAudioManager.setSpeakerphoneOn(on);
597 /** Sets the microphone mute state. */
598 private void setMicrophoneMute(boolean on) {
599 boolean wasMuted = mAudioManager.isMicrophoneMute();
600 if (wasMuted == on) {
603 mAudioManager.setMicrophoneMute(on);
606 /** Gets the current microphone mute state. */
607 private boolean isMicrophoneMute() {
608 return mAudioManager.isMicrophoneMute();
611 /** Gets the current earpiece state. */
612 private boolean hasEarpiece() {
613 return mContext.getPackageManager().hasSystemFeature(
614 PackageManager.FEATURE_TELEPHONY);
618 * Checks whether a wired headset is connected or not.
619 * This is not a valid indication that audio playback is actually over
620 * the wired headset as audio routing depends on other conditions. We
621 * only use it as an early indicator (during initialization) of an attached
625 private boolean hasWiredHeadset() {
626 return mAudioManager.isWiredHeadsetOn();
629 /** Checks if the process has as specified permission or not. */
630 private boolean hasPermission(String permission) {
631 return mContext.checkPermission(
634 Process.myUid()) == PackageManager.PERMISSION_GRANTED;
638 * Gets the current Bluetooth headset state.
639 * android.bluetooth.BluetoothAdapter.getProfileConnectionState() requires
640 * the BLUETOOTH permission.
642 private boolean hasBluetoothHeadset() {
643 if (!mHasBluetoothPermission) {
644 Log.w(TAG, "hasBluetoothHeadset() requires BLUETOOTH permission");
648 // To get a BluetoothAdapter representing the local Bluetooth adapter,
649 // when running on JELLY_BEAN_MR1 (4.2) and below, call the static
650 // getDefaultAdapter() method; when running on JELLY_BEAN_MR2 (4.3) and
651 // higher, retrieve it through getSystemService(String) with
652 // BLUETOOTH_SERVICE.
653 BluetoothAdapter btAdapter = null;
654 if (runningOnJellyBeanMR2OrHigher()) {
655 // Use BluetoothManager to get the BluetoothAdapter for
656 // Android 4.3 and above.
657 BluetoothManager btManager =
658 (BluetoothManager) mContext.getSystemService(
659 Context.BLUETOOTH_SERVICE);
660 btAdapter = btManager.getAdapter();
662 // Use static method for Android 4.2 and below to get the
664 btAdapter = BluetoothAdapter.getDefaultAdapter();
667 if (btAdapter == null) {
668 // Bluetooth not supported on this platform.
672 int profileConnectionState;
673 profileConnectionState = btAdapter.getProfileConnectionState(
674 android.bluetooth.BluetoothProfile.HEADSET);
676 // Ensure that Bluetooth is enabled and that a device which supports the
677 // headset and handsfree profile is connected.
678 // TODO(henrika): it is possible that btAdapter.isEnabled() is
679 // redundant. It might be sufficient to only check the profile state.
680 return btAdapter.isEnabled() && profileConnectionState ==
681 android.bluetooth.BluetoothProfile.STATE_CONNECTED;
685 * Registers receiver for the broadcasted intent when a wired headset is
686 * plugged in or unplugged. The received intent will have an extra
687 * 'state' value where 0 means unplugged, and 1 means plugged.
689 private void registerForWiredHeadsetIntentBroadcast() {
690 IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
692 /** Receiver which handles changes in wired headset availability. */
693 mWiredHeadsetReceiver = new BroadcastReceiver() {
694 private static final int STATE_UNPLUGGED = 0;
695 private static final int STATE_PLUGGED = 1;
696 private static final int HAS_NO_MIC = 0;
697 private static final int HAS_MIC = 1;
700 public void onReceive(Context context, Intent intent) {
701 int state = intent.getIntExtra("state", STATE_UNPLUGGED);
703 int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
704 String name = intent.getStringExtra("name");
705 logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
707 ", m=" + microphone +
709 ", sb=" + isInitialStickyBroadcast());
712 case STATE_UNPLUGGED:
713 synchronized (mLock) {
714 // Wired headset and earpiece are mutually exclusive.
715 mAudioDevices[DEVICE_WIRED_HEADSET] = false;
717 mAudioDevices[DEVICE_EARPIECE] = true;
722 synchronized (mLock) {
723 // Wired headset and earpiece are mutually exclusive.
724 mAudioDevices[DEVICE_WIRED_HEADSET] = true;
725 mAudioDevices[DEVICE_EARPIECE] = false;
729 loge("Invalid state");
733 // Update the existing device selection, but only if a specific
734 // device has already been selected explicitly.
735 if (deviceHasBeenRequested()) {
736 updateDeviceActivation();
743 // Note: the intent we register for here is sticky, so it'll tell us
744 // immediately what the last action was (plugged or unplugged).
745 // It will enable us to set the speakerphone correctly.
746 mContext.registerReceiver(mWiredHeadsetReceiver, filter);
749 /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */
750 private void unregisterForWiredHeadsetIntentBroadcast() {
751 mContext.unregisterReceiver(mWiredHeadsetReceiver);
752 mWiredHeadsetReceiver = null;
756 * Registers receiver for the broadcasted intent related to BT headset
757 * availability or a change in connection state of the local Bluetooth
758 * adapter. Example: triggers when the BT device is turned on or off.
759 * BLUETOOTH permission is required to receive this one.
761 private void registerForBluetoothHeadsetIntentBroadcast() {
762 IntentFilter filter = new IntentFilter(
763 android.bluetooth.BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
765 /** Receiver which handles changes in BT headset availability. */
766 mBluetoothHeadsetReceiver = new BroadcastReceiver() {
768 public void onReceive(Context context, Intent intent) {
769 // A change in connection state of the Headset profile has
770 // been detected, e.g. BT headset has been connected or
771 // disconnected. This broadcast is *not* sticky.
772 int profileState = intent.getIntExtra(
773 android.bluetooth.BluetoothHeadset.EXTRA_STATE,
774 android.bluetooth.BluetoothHeadset.STATE_DISCONNECTED);
776 logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
777 ", s=" + profileState +
778 ", sb=" + isInitialStickyBroadcast());
781 switch (profileState) {
782 case android.bluetooth.BluetoothProfile.STATE_DISCONNECTED:
783 // We do not have to explicitly call stopBluetoothSco()
784 // since BT SCO will be disconnected automatically when
785 // the BT headset is disabled.
786 synchronized (mLock) {
787 // Remove the BT device from the list of devices.
788 mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = false;
791 case android.bluetooth.BluetoothProfile.STATE_CONNECTED:
792 synchronized (mLock) {
793 // Add the BT device to the list of devices.
794 mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = true;
797 case android.bluetooth.BluetoothProfile.STATE_CONNECTING:
798 // Bluetooth service is switching from off to on.
800 case android.bluetooth.BluetoothProfile.STATE_DISCONNECTING:
801 // Bluetooth service is switching from on to off.
804 loge("Invalid state");
808 // Update the existing device selection, but only if a specific
809 // device has already been selected explicitly.
810 if (deviceHasBeenRequested()) {
811 updateDeviceActivation();
818 mContext.registerReceiver(mBluetoothHeadsetReceiver, filter);
821 private void unregisterForBluetoothHeadsetIntentBroadcast() {
822 mContext.unregisterReceiver(mBluetoothHeadsetReceiver);
823 mBluetoothHeadsetReceiver = null;
827 * Registers receiver for the broadcasted intent related the existence
828 * of a BT SCO channel. Indicates if BT SCO streaming is on or off.
830 private void registerForBluetoothScoIntentBroadcast() {
831 IntentFilter filter = new IntentFilter(
832 AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
834 /** BroadcastReceiver implementation which handles changes in BT SCO. */
835 mBluetoothScoReceiver = new BroadcastReceiver() {
837 public void onReceive(Context context, Intent intent) {
838 int state = intent.getIntExtra(
839 AudioManager.EXTRA_SCO_AUDIO_STATE,
840 AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
842 logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
844 ", sb=" + isInitialStickyBroadcast());
848 case AudioManager.SCO_AUDIO_STATE_CONNECTED:
849 mBluetoothScoState = STATE_BLUETOOTH_SCO_ON;
851 case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
852 mBluetoothScoState = STATE_BLUETOOTH_SCO_OFF;
854 case AudioManager.SCO_AUDIO_STATE_CONNECTING:
858 loge("Invalid state");
866 mContext.registerReceiver(mBluetoothScoReceiver, filter);
869 private void unregisterForBluetoothScoIntentBroadcast() {
870 mContext.unregisterReceiver(mBluetoothScoReceiver);
871 mBluetoothScoReceiver = null;
874 /** Enables BT audio using the SCO audio channel. */
875 private void startBluetoothSco() {
876 if (!mHasBluetoothPermission) {
879 if (mBluetoothScoState == STATE_BLUETOOTH_SCO_ON ||
880 mBluetoothScoState == STATE_BLUETOOTH_SCO_TURNING_ON) {
881 // Unable to turn on BT in this state.
885 // Check if audio is already routed to BT SCO; if so, just update
886 // states but don't try to enable it again.
887 if (mAudioManager.isBluetoothScoOn()) {
888 mBluetoothScoState = STATE_BLUETOOTH_SCO_ON;
892 if (DEBUG) logd("startBluetoothSco: turning BT SCO on...");
893 mBluetoothScoState = STATE_BLUETOOTH_SCO_TURNING_ON;
894 mAudioManager.startBluetoothSco();
897 /** Disables BT audio using the SCO audio channel. */
898 private void stopBluetoothSco() {
899 if (!mHasBluetoothPermission) {
902 if (mBluetoothScoState != STATE_BLUETOOTH_SCO_ON &&
903 mBluetoothScoState != STATE_BLUETOOTH_SCO_TURNING_ON) {
904 // No need to turn off BT in this state.
907 if (!mAudioManager.isBluetoothScoOn()) {
908 // TODO(henrika): can we do anything else than logging here?
909 loge("Unable to stop BT SCO since it is already disabled");
913 if (DEBUG) logd("stopBluetoothSco: turning BT SCO off...");
914 mBluetoothScoState = STATE_BLUETOOTH_SCO_TURNING_OFF;
915 mAudioManager.stopBluetoothSco();
919 * Changes selection of the currently active audio device.
921 * @param device Specifies the selected audio device.
923 private void setAudioDevice(int device) {
924 if (DEBUG) logd("setAudioDevice(device=" + device + ")");
926 // Ensure that the Bluetooth SCO audio channel is always disabled
927 // unless the BT headset device is selected.
928 if (device == DEVICE_BLUETOOTH_HEADSET) {
935 case DEVICE_BLUETOOTH_HEADSET:
937 case DEVICE_SPEAKERPHONE:
938 setSpeakerphoneOn(true);
940 case DEVICE_WIRED_HEADSET:
941 setSpeakerphoneOn(false);
943 case DEVICE_EARPIECE:
944 setSpeakerphoneOn(false);
947 loge("Invalid audio device selection");
954 * Use a special selection scheme if the default device is selected.
955 * The "most unique" device will be selected; Wired headset first,
956 * then Bluetooth and last the speaker phone.
958 private static int selectDefaultDevice(boolean[] devices) {
959 if (devices[DEVICE_WIRED_HEADSET]) {
960 return DEVICE_WIRED_HEADSET;
961 } else if (devices[DEVICE_BLUETOOTH_HEADSET]) {
962 // TODO(henrika): possibly need improvements here if we are
963 // in a state where Bluetooth is turning off.
964 return DEVICE_BLUETOOTH_HEADSET;
966 return DEVICE_SPEAKERPHONE;
969 /** Returns true if setDevice() has been called with a valid device id. */
970 private boolean deviceHasBeenRequested() {
971 synchronized (mLock) {
972 return (mRequestedAudioDevice != DEVICE_INVALID);
977 * Updates the active device given the current list of devices and
978 * information about if a specific device has been selected or if
979 * the default device is selected.
981 private void updateDeviceActivation() {
982 boolean devices[] = null;
983 int requested = DEVICE_INVALID;
984 synchronized (mLock) {
985 requested = mRequestedAudioDevice;
986 devices = mAudioDevices.clone();
988 if (requested == DEVICE_INVALID) {
989 loge("Unable to activate device since no device is selected");
993 // Update default device if it has been selected explicitly, or
994 // the selected device has been removed from the list.
995 if (requested == DEVICE_DEFAULT || !devices[requested]) {
996 // Get default device given current list and activate the device.
997 int defaultDevice = selectDefaultDevice(devices);
998 setAudioDevice(defaultDevice);
1000 // Activate the selected device since we know that it exists in
1002 setAudioDevice(requested);
1006 /** Returns number of available devices */
1007 private static int getNumOfAudioDevices(boolean[] devices) {
1009 for (int i = 0; i < DEVICE_COUNT; ++i) {
1017 * For now, just log the state change but the idea is that we should
1018 * notify a registered state change listener (if any) that there has
1019 * been a change in the state.
1020 * TODO(henrika): add support for state change listener.
1022 private void reportUpdate() {
1023 synchronized (mLock) {
1024 List<String> devices = new ArrayList<String>();
1025 for (int i = 0; i < DEVICE_COUNT; ++i) {
1026 if (mAudioDevices[i])
1027 devices.add(DEVICE_NAMES[i]);
1030 logd("reportUpdate: requested=" + mRequestedAudioDevice +
1031 ", btSco=" + mBluetoothScoState +
1032 ", devices=" + devices);
1037 /** Information about the current build, taken from system properties. */
1038 private void logDeviceInfo() {
1039 logd("Android SDK: " + Build.VERSION.SDK_INT + ", " +
1040 "Release: " + Build.VERSION.RELEASE + ", " +
1041 "Brand: " + Build.BRAND + ", " +
1042 "Device: " + Build.DEVICE + ", " +
1043 "Id: " + Build.ID + ", " +
1044 "Hardware: " + Build.HARDWARE + ", " +
1045 "Manufacturer: " + Build.MANUFACTURER + ", " +
1046 "Model: " + Build.MODEL + ", " +
1047 "Product: " + Build.PRODUCT);
1050 /** Trivial helper method for debug logging */
1051 private static void logd(String msg) {
1055 /** Trivial helper method for error logging */
1056 private static void loge(String msg) {
1060 /** Start thread which observes volume changes on the voice stream. */
1061 private void startObservingVolumeChanges() {
1062 if (DEBUG) logd("startObservingVolumeChanges");
1063 if (mSettingsObserverThread != null)
1065 mSettingsObserverThread = new HandlerThread("SettingsObserver");
1066 mSettingsObserverThread.start();
1068 mSettingsObserver = new ContentObserver(
1069 new Handler(mSettingsObserverThread.getLooper())) {
1072 public void onChange(boolean selfChange) {
1073 if (DEBUG) logd("SettingsObserver.onChange: " + selfChange);
1074 super.onChange(selfChange);
1076 // Ensure that the observer is activated during communication mode.
1077 if (mAudioManager.getMode() != AudioManager.MODE_IN_COMMUNICATION) {
1078 throw new IllegalStateException(
1079 "Only enable SettingsObserver in COMM mode");
1082 // Get stream volume for the voice stream and deliver callback if
1083 // the volume index is zero. It is not possible to move the volume
1084 // slider all the way down in communication mode but the callback
1085 // implementation can ensure that the volume is completely muted.
1086 int volume = mAudioManager.getStreamVolume(
1087 AudioManager.STREAM_VOICE_CALL);
1088 if (DEBUG) logd("nativeSetMute: " + (volume == 0));
1089 nativeSetMute(mNativeAudioManagerAndroid, (volume == 0));
1093 mContentResolver.registerContentObserver(
1094 Settings.System.CONTENT_URI, true, mSettingsObserver);
1097 /** Quit observer thread and stop listening for volume changes. */
1098 private void stopObservingVolumeChanges() {
1099 if (DEBUG) logd("stopObservingVolumeChanges");
1100 if (mSettingsObserverThread == null)
1103 mContentResolver.unregisterContentObserver(mSettingsObserver);
1104 mSettingsObserver = null;
1106 mSettingsObserverThread.quit();
1108 mSettingsObserverThread.join();
1109 } catch (InterruptedException e) {
1110 Log.e(TAG, "Thread.join() exception: ", e);
1112 mSettingsObserverThread = null;
1115 private native void nativeSetMute(long nativeAudioManagerAndroid, boolean muted);