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 int profileConnectionState;
668 profileConnectionState = btAdapter.getProfileConnectionState(
669 android.bluetooth.BluetoothProfile.HEADSET);
671 // Ensure that Bluetooth is enabled and that a device which supports the
672 // headset and handsfree profile is connected.
673 // TODO(henrika): it is possible that btAdapter.isEnabled() is
674 // redundant. It might be sufficient to only check the profile state.
675 return btAdapter.isEnabled() && profileConnectionState ==
676 android.bluetooth.BluetoothProfile.STATE_CONNECTED;
680 * Registers receiver for the broadcasted intent when a wired headset is
681 * plugged in or unplugged. The received intent will have an extra
682 * 'state' value where 0 means unplugged, and 1 means plugged.
684 private void registerForWiredHeadsetIntentBroadcast() {
685 IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
687 /** Receiver which handles changes in wired headset availability. */
688 mWiredHeadsetReceiver = new BroadcastReceiver() {
689 private static final int STATE_UNPLUGGED = 0;
690 private static final int STATE_PLUGGED = 1;
691 private static final int HAS_NO_MIC = 0;
692 private static final int HAS_MIC = 1;
695 public void onReceive(Context context, Intent intent) {
696 int state = intent.getIntExtra("state", STATE_UNPLUGGED);
698 int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
699 String name = intent.getStringExtra("name");
700 logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
702 ", m=" + microphone +
704 ", sb=" + isInitialStickyBroadcast());
707 case STATE_UNPLUGGED:
708 synchronized (mLock) {
709 // Wired headset and earpiece are mutually exclusive.
710 mAudioDevices[DEVICE_WIRED_HEADSET] = false;
712 mAudioDevices[DEVICE_EARPIECE] = true;
717 synchronized (mLock) {
718 // Wired headset and earpiece are mutually exclusive.
719 mAudioDevices[DEVICE_WIRED_HEADSET] = true;
720 mAudioDevices[DEVICE_EARPIECE] = false;
724 loge("Invalid state");
728 // Update the existing device selection, but only if a specific
729 // device has already been selected explicitly.
730 if (deviceHasBeenRequested()) {
731 updateDeviceActivation();
738 // Note: the intent we register for here is sticky, so it'll tell us
739 // immediately what the last action was (plugged or unplugged).
740 // It will enable us to set the speakerphone correctly.
741 mContext.registerReceiver(mWiredHeadsetReceiver, filter);
744 /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */
745 private void unregisterForWiredHeadsetIntentBroadcast() {
746 mContext.unregisterReceiver(mWiredHeadsetReceiver);
747 mWiredHeadsetReceiver = null;
751 * Registers receiver for the broadcasted intent related to BT headset
752 * availability or a change in connection state of the local Bluetooth
753 * adapter. Example: triggers when the BT device is turned on or off.
754 * BLUETOOTH permission is required to receive this one.
756 private void registerForBluetoothHeadsetIntentBroadcast() {
757 IntentFilter filter = new IntentFilter(
758 android.bluetooth.BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
760 /** Receiver which handles changes in BT headset availability. */
761 mBluetoothHeadsetReceiver = new BroadcastReceiver() {
763 public void onReceive(Context context, Intent intent) {
764 // A change in connection state of the Headset profile has
765 // been detected, e.g. BT headset has been connected or
766 // disconnected. This broadcast is *not* sticky.
767 int profileState = intent.getIntExtra(
768 android.bluetooth.BluetoothHeadset.EXTRA_STATE,
769 android.bluetooth.BluetoothHeadset.STATE_DISCONNECTED);
771 logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
772 ", s=" + profileState +
773 ", sb=" + isInitialStickyBroadcast());
776 switch (profileState) {
777 case android.bluetooth.BluetoothProfile.STATE_DISCONNECTED:
778 // We do not have to explicitly call stopBluetoothSco()
779 // since BT SCO will be disconnected automatically when
780 // the BT headset is disabled.
781 synchronized (mLock) {
782 // Remove the BT device from the list of devices.
783 mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = false;
786 case android.bluetooth.BluetoothProfile.STATE_CONNECTED:
787 synchronized (mLock) {
788 // Add the BT device to the list of devices.
789 mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = true;
792 case android.bluetooth.BluetoothProfile.STATE_CONNECTING:
793 // Bluetooth service is switching from off to on.
795 case android.bluetooth.BluetoothProfile.STATE_DISCONNECTING:
796 // Bluetooth service is switching from on to off.
799 loge("Invalid state");
803 // Update the existing device selection, but only if a specific
804 // device has already been selected explicitly.
805 if (deviceHasBeenRequested()) {
806 updateDeviceActivation();
813 mContext.registerReceiver(mBluetoothHeadsetReceiver, filter);
816 private void unregisterForBluetoothHeadsetIntentBroadcast() {
817 mContext.unregisterReceiver(mBluetoothHeadsetReceiver);
818 mBluetoothHeadsetReceiver = null;
822 * Registers receiver for the broadcasted intent related the existence
823 * of a BT SCO channel. Indicates if BT SCO streaming is on or off.
825 private void registerForBluetoothScoIntentBroadcast() {
826 IntentFilter filter = new IntentFilter(
827 AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
829 /** BroadcastReceiver implementation which handles changes in BT SCO. */
830 mBluetoothScoReceiver = new BroadcastReceiver() {
832 public void onReceive(Context context, Intent intent) {
833 int state = intent.getIntExtra(
834 AudioManager.EXTRA_SCO_AUDIO_STATE,
835 AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
837 logd("BroadcastReceiver.onReceive: a=" + intent.getAction() +
839 ", sb=" + isInitialStickyBroadcast());
843 case AudioManager.SCO_AUDIO_STATE_CONNECTED:
844 mBluetoothScoState = STATE_BLUETOOTH_SCO_ON;
846 case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
847 mBluetoothScoState = STATE_BLUETOOTH_SCO_OFF;
849 case AudioManager.SCO_AUDIO_STATE_CONNECTING:
853 loge("Invalid state");
861 mContext.registerReceiver(mBluetoothScoReceiver, filter);
864 private void unregisterForBluetoothScoIntentBroadcast() {
865 mContext.unregisterReceiver(mBluetoothScoReceiver);
866 mBluetoothScoReceiver = null;
869 /** Enables BT audio using the SCO audio channel. */
870 private void startBluetoothSco() {
871 if (!mHasBluetoothPermission) {
874 if (mBluetoothScoState == STATE_BLUETOOTH_SCO_ON ||
875 mBluetoothScoState == STATE_BLUETOOTH_SCO_TURNING_ON) {
876 // Unable to turn on BT in this state.
880 // Check if audio is already routed to BT SCO; if so, just update
881 // states but don't try to enable it again.
882 if (mAudioManager.isBluetoothScoOn()) {
883 mBluetoothScoState = STATE_BLUETOOTH_SCO_ON;
887 if (DEBUG) logd("startBluetoothSco: turning BT SCO on...");
888 mBluetoothScoState = STATE_BLUETOOTH_SCO_TURNING_ON;
889 mAudioManager.startBluetoothSco();
892 /** Disables BT audio using the SCO audio channel. */
893 private void stopBluetoothSco() {
894 if (!mHasBluetoothPermission) {
897 if (mBluetoothScoState != STATE_BLUETOOTH_SCO_ON &&
898 mBluetoothScoState != STATE_BLUETOOTH_SCO_TURNING_ON) {
899 // No need to turn off BT in this state.
902 if (!mAudioManager.isBluetoothScoOn()) {
903 // TODO(henrika): can we do anything else than logging here?
904 loge("Unable to stop BT SCO since it is already disabled");
908 if (DEBUG) logd("stopBluetoothSco: turning BT SCO off...");
909 mBluetoothScoState = STATE_BLUETOOTH_SCO_TURNING_OFF;
910 mAudioManager.stopBluetoothSco();
914 * Changes selection of the currently active audio device.
916 * @param device Specifies the selected audio device.
918 private void setAudioDevice(int device) {
919 if (DEBUG) logd("setAudioDevice(device=" + device + ")");
921 // Ensure that the Bluetooth SCO audio channel is always disabled
922 // unless the BT headset device is selected.
923 if (device == DEVICE_BLUETOOTH_HEADSET) {
930 case DEVICE_BLUETOOTH_HEADSET:
932 case DEVICE_SPEAKERPHONE:
933 setSpeakerphoneOn(true);
935 case DEVICE_WIRED_HEADSET:
936 setSpeakerphoneOn(false);
938 case DEVICE_EARPIECE:
939 setSpeakerphoneOn(false);
942 loge("Invalid audio device selection");
949 * Use a special selection scheme if the default device is selected.
950 * The "most unique" device will be selected; Wired headset first,
951 * then Bluetooth and last the speaker phone.
953 private static int selectDefaultDevice(boolean[] devices) {
954 if (devices[DEVICE_WIRED_HEADSET]) {
955 return DEVICE_WIRED_HEADSET;
956 } else if (devices[DEVICE_BLUETOOTH_HEADSET]) {
957 // TODO(henrika): possibly need improvements here if we are
958 // in a state where Bluetooth is turning off.
959 return DEVICE_BLUETOOTH_HEADSET;
961 return DEVICE_SPEAKERPHONE;
964 /** Returns true if setDevice() has been called with a valid device id. */
965 private boolean deviceHasBeenRequested() {
966 synchronized (mLock) {
967 return (mRequestedAudioDevice != DEVICE_INVALID);
972 * Updates the active device given the current list of devices and
973 * information about if a specific device has been selected or if
974 * the default device is selected.
976 private void updateDeviceActivation() {
977 boolean devices[] = null;
978 int requested = DEVICE_INVALID;
979 synchronized (mLock) {
980 requested = mRequestedAudioDevice;
981 devices = mAudioDevices.clone();
983 if (requested == DEVICE_INVALID) {
984 loge("Unable to activate device since no device is selected");
988 // Update default device if it has been selected explicitly, or
989 // the selected device has been removed from the list.
990 if (requested == DEVICE_DEFAULT || !devices[requested]) {
991 // Get default device given current list and activate the device.
992 int defaultDevice = selectDefaultDevice(devices);
993 setAudioDevice(defaultDevice);
995 // Activate the selected device since we know that it exists in
997 setAudioDevice(requested);
1001 /** Returns number of available devices */
1002 private static int getNumOfAudioDevices(boolean[] devices) {
1004 for (int i = 0; i < DEVICE_COUNT; ++i) {
1012 * For now, just log the state change but the idea is that we should
1013 * notify a registered state change listener (if any) that there has
1014 * been a change in the state.
1015 * TODO(henrika): add support for state change listener.
1017 private void reportUpdate() {
1018 synchronized (mLock) {
1019 List<String> devices = new ArrayList<String>();
1020 for (int i = 0; i < DEVICE_COUNT; ++i) {
1021 if (mAudioDevices[i])
1022 devices.add(DEVICE_NAMES[i]);
1025 logd("reportUpdate: requested=" + mRequestedAudioDevice +
1026 ", btSco=" + mBluetoothScoState +
1027 ", devices=" + devices);
1032 /** Information about the current build, taken from system properties. */
1033 private void logDeviceInfo() {
1034 logd("Android SDK: " + Build.VERSION.SDK_INT + ", " +
1035 "Release: " + Build.VERSION.RELEASE + ", " +
1036 "Brand: " + Build.BRAND + ", " +
1037 "CPU_ABI: " + Build.CPU_ABI + ", " +
1038 "Device: " + Build.DEVICE + ", " +
1039 "Id: " + Build.ID + ", " +
1040 "Hardware: " + Build.HARDWARE + ", " +
1041 "Manufacturer: " + Build.MANUFACTURER + ", " +
1042 "Model: " + Build.MODEL + ", " +
1043 "Product: " + Build.PRODUCT);
1046 /** Trivial helper method for debug logging */
1047 private static void logd(String msg) {
1051 /** Trivial helper method for error logging */
1052 private static void loge(String msg) {
1056 /** Start thread which observes volume changes on the voice stream. */
1057 private void startObservingVolumeChanges() {
1058 if (DEBUG) logd("startObservingVolumeChanges");
1059 if (mSettingsObserverThread != null)
1061 mSettingsObserverThread = new HandlerThread("SettingsObserver");
1062 mSettingsObserverThread.start();
1064 mSettingsObserver = new ContentObserver(
1065 new Handler(mSettingsObserverThread.getLooper())) {
1068 public void onChange(boolean selfChange) {
1069 if (DEBUG) logd("SettingsObserver.onChange: " + selfChange);
1070 super.onChange(selfChange);
1072 // Ensure that the observer is activated during communication mode.
1073 if (mAudioManager.getMode() != AudioManager.MODE_IN_COMMUNICATION) {
1074 throw new IllegalStateException(
1075 "Only enable SettingsObserver in COMM mode");
1078 // Get stream volume for the voice stream and deliver callback if
1079 // the volume index is zero. It is not possible to move the volume
1080 // slider all the way down in communication mode but the callback
1081 // implementation can ensure that the volume is completely muted.
1082 int volume = mAudioManager.getStreamVolume(
1083 AudioManager.STREAM_VOICE_CALL);
1084 if (DEBUG) logd("nativeSetMute: " + (volume == 0));
1085 nativeSetMute(mNativeAudioManagerAndroid, (volume == 0));
1089 mContentResolver.registerContentObserver(
1090 Settings.System.CONTENT_URI, true, mSettingsObserver);
1093 /** Quit observer thread and stop listening for volume changes. */
1094 private void stopObservingVolumeChanges() {
1095 if (DEBUG) logd("stopObservingVolumeChanges");
1096 if (mSettingsObserverThread == null)
1099 mContentResolver.unregisterContentObserver(mSettingsObserver);
1100 mSettingsObserver = null;
1102 mSettingsObserverThread.quit();
1104 mSettingsObserverThread.join();
1105 } catch (InterruptedException e) {
1106 Log.e(TAG, "Thread.join() exception: ", e);
1108 mSettingsObserverThread = null;
1111 private native void nativeSetMute(long nativeAudioManagerAndroid, boolean muted);