Chromium Code Reviews| Index: media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java |
| diff --git a/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java b/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java |
| index 0f0cfb61e08428eb477c9de493d4d60fff342293..9d0cdf282ddabd25ebc311b88adee7e8b859b961 100644 |
| --- a/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java |
| +++ b/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java |
| @@ -4,6 +4,8 @@ |
| package org.chromium.media; |
| +import android.bluetooth.BluetoothAdapter; |
| +import android.bluetooth.BluetoothManager; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| @@ -14,8 +16,15 @@ import android.media.AudioManager; |
| import android.media.AudioRecord; |
| import android.media.AudioTrack; |
| import android.os.Build; |
| +import android.os.Process; |
| import android.util.Log; |
| +import java.util.Arrays; |
| +import java.util.ArrayList; |
| +import java.util.HashSet; |
| +import java.util.List; |
| +import java.util.Set; |
| + |
| import org.chromium.base.CalledByNative; |
| import org.chromium.base.JNINamespace; |
| @@ -23,30 +32,90 @@ import org.chromium.base.JNINamespace; |
| class AudioManagerAndroid { |
| private static final String TAG = "AudioManagerAndroid"; |
| - // Most of Google lead devices use 44.1K as the default sampling rate, 44.1K |
| - // is also widely used on other android devices. |
| + /** Simple container for device information. */ |
| + private static class AudioDeviceName { |
| + private final int mId; |
| + private final String mName; |
| + |
| + private AudioDeviceName(int id, String name) { |
| + mId = id; |
| + mName = name; |
| + } |
| + |
| + @CalledByNative("AudioDeviceName") |
| + private String id() { return String.valueOf(mId); } |
| + |
| + @CalledByNative("AudioDeviceName") |
| + private String name() { return mName; } |
| + } |
| + |
| + // Supported audio device types. |
| + private static final int DEVICE_INVALID = 0; |
| + private static final int DEVICE_SPEAKERPHONE = 1; |
| + private static final int DEVICE_WIRED_HEADSET = 2; |
| + private static final int DEVICE_EARPIECE = 3; |
| + private static final int DEVICE_BLUETOOTH_HEADSET = 4; |
| + |
| + // List of valid device types. |
| + private static Integer[] VALID_DEVICES = new Integer[] { |
| + DEVICE_SPEAKERPHONE, |
| + DEVICE_WIRED_HEADSET, |
| + DEVICE_EARPIECE, |
| + DEVICE_BLUETOOTH_HEADSET, |
| + }; |
| + |
| + // Maps audio device types to string values. |
|
Jói
2013/11/29 14:40:46
Maybe it would be better to have this right after
henrika (OOO until Aug 14)
2013/11/29 15:31:49
Done.
|
| + // TODO(henrika): add support for proper detection of device names and |
| + // localize the name strings. |
| + private static final String[] DEVICE_NAMES = new String[] { |
| + "Invalid device", |
| + "Speakerphone", |
| + "Wired headset", // With or without microphone |
| + "Headset earpiece", // Only available on mobile phones |
| + "Bluetooth headset", |
| + }; |
| + |
| + // The device does not have any audio device. |
|
Jói
2013/11/29 14:40:46
It would be cool to document the valid state trans
henrika (OOO until Aug 14)
2013/11/29 15:31:49
Added TODO.
|
| + static final int STATE_NO_DEVICE_SELECTED = 0; |
| + // The speakerphone is on and an associated microphone is used. |
| + static final int STATE_SPEAKERPHONE_ON = 1; |
| + // The phone's earpiece is on and an associated microphone is used. |
| + static final int STATE_EARPIECE_ON = 2; |
| + // A wired headset (with or without a microphone) is plugged in. |
| + static final int STATE_WIRED_HEADSET_ON = 3; |
| + // The audio stream is being directed to a Bluetooth headset. |
| + static final int STATE_BLUETOOTH_ON = 4; |
| + // We've requested that the audio stream be directed to Bluetooth, but |
| + // have not yet received a response from the framework. |
| + static final int STATE_BLUETOOTH_TURNING_ON = 5; |
| + // We've requested that the audio stream stop being directed to |
| + // Bluetooth, but have not yet received a response from the framework. |
| + static final int STATE_BLUETOOTH_TURNING_OFF = 6; |
| + |
| + // Use 44.1kHz as the default sampling rate. |
| private static final int DEFAULT_SAMPLING_RATE = 44100; |
| // Randomly picked up frame size which is close to return value on N4. |
| - // Return this default value when |
| - // getProperty(PROPERTY_OUTPUT_FRAMES_PER_BUFFER) fails. |
| + // Return this value when getProperty(PROPERTY_OUTPUT_FRAMES_PER_BUFFER) |
| + // fails. |
| private static final int DEFAULT_FRAME_PER_BUFFER = 256; |
| private final AudioManager mAudioManager; |
| private final Context mContext; |
| - private BroadcastReceiver mReceiver; |
| - private boolean mOriginalSpeakerStatus; |
| + private boolean mHasBluetoothPermission = false; |
| + private boolean mIsInitialized = false; |
| + private boolean mSavedSpeakerPhoneState; |
| + private boolean mSavedMicrophoneMuteState; |
| - @CalledByNative |
| - public void setMode(int mode) { |
| - try { |
| - mAudioManager.setMode(mode); |
| - } catch (SecurityException e) { |
| - Log.e(TAG, "setMode exception: " + e.getMessage()); |
| - logDeviceInfo(); |
| - } |
| - } |
| + private Integer mAudioDeviceState = STATE_NO_DEVICE_SELECTED; |
| + |
| + // Contains a list of currently available audio devices. |
| + private Set<Integer> mAudioDevices = new HashSet<Integer>(); |
| + // Broadcast receiver for wired headset intent broadcasts. |
| + private BroadcastReceiver mWiredHeadsetReceiver; |
| + |
| + /** Construction */ |
| @CalledByNative |
| private static AudioManagerAndroid createAudioManagerAndroid(Context context) { |
| return new AudioManagerAndroid(context); |
| @@ -57,46 +126,129 @@ class AudioManagerAndroid { |
| mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE); |
| } |
| + /** |
| + * Saves the initial speakerphone and microphone state. |
| + * Populates the list of available audio devices and registers receivers |
| + * for broadcasted intents related to wired headset and bluetooth devices. |
| + */ |
| @CalledByNative |
| - public void registerHeadsetReceiver() { |
| - if (mReceiver != null) { |
| + public void init() { |
| + if (mIsInitialized) |
| return; |
| - } |
| - mOriginalSpeakerStatus = mAudioManager.isSpeakerphoneOn(); |
| - if (!mOriginalSpeakerStatus) { |
| - mAudioManager.setSpeakerphoneOn(true); |
| + // Store microphone mute state and speakerphone state so it can |
| + // be restored when closing. |
| + mSavedSpeakerPhoneState = mAudioManager.isSpeakerphoneOn(); |
|
Jói
2013/11/29 14:40:46
Naming suggestion: mSavedIsSpeakerphoneOn and mSav
henrika (OOO until Aug 14)
2013/11/29 15:31:49
thx
|
| + mSavedMicrophoneMuteState = mAudioManager.isMicrophoneMute(); |
| + |
| + // Always enable speaker phone by default. This state might be reset |
| + // by the wired headset receiver when it gets its initial sticky |
| + // intent, if any. |
| + setSpeakerphoneOn(true); |
| + mAudioDeviceState = STATE_SPEAKERPHONE_ON; |
| + |
| + // Initialize audio device list with things we know is always available. |
| + if (hasEarpiece()) { |
| + mAudioDevices.add(DEVICE_EARPIECE); |
| } |
| - IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); |
| + mAudioDevices.add(DEVICE_SPEAKERPHONE); |
| - mReceiver = new BroadcastReceiver() { |
| - @Override |
| - public void onReceive(Context context, Intent intent) { |
| - if (Intent.ACTION_HEADSET_PLUG.equals(intent.getAction())) { |
| - try { |
| - mAudioManager.setSpeakerphoneOn( |
| - intent.getIntExtra("state", 0) == 0); |
| - } catch (SecurityException e) { |
| - Log.e(TAG, "setMode exception: " + e.getMessage()); |
| - logDeviceInfo(); |
| - } |
| - } |
| - } |
| - }; |
| - mContext.registerReceiver(mReceiver, filter); |
| + // Register receiver for broadcasted intents related to adding/ |
| + // removing a wired headset (Intent.ACTION_HEADSET_PLUG). |
| + // Also starts routing to the wired headset/headphone if one is |
| + // already attached (can be overridden by a Bluetooth headset). |
| + registerForWiredHeadsetIntentBroadcast(); |
| + |
| + // Start routing to Bluetooth if there's a connected device. |
| + // TODO(henrika): the actual routing part is not implemented yet. |
| + // All we do currently is to detect if BT headset is attached or not. |
| + initBluetooth(); |
| + |
| + mIsInitialized = true; |
| } |
| + /** |
| + * Unregister all previously registered intent receivers and restore |
| + * the stored state (stored in {@link #init()}). |
| + */ |
| @CalledByNative |
| - public void unregisterHeadsetReceiver() { |
| - mContext.unregisterReceiver(mReceiver); |
| - mReceiver = null; |
| - mAudioManager.setSpeakerphoneOn(mOriginalSpeakerStatus); |
| + public void close() { |
| + if (!mIsInitialized) |
| + return; |
| + |
| + unregisterForWiredHeadsetIntentBroadcast(); |
| + |
| + // Restore previously stored audio states. |
| + setMicrophoneMute(mSavedMicrophoneMuteState); |
| + setSpeakerphoneOn(mSavedSpeakerPhoneState); |
| + |
| + mIsInitialized = false; |
| } |
| - private void logDeviceInfo() { |
| - Log.i(TAG, "Manufacturer:" + Build.MANUFACTURER + |
| - " Board: " + Build.BOARD + " Device: " + Build.DEVICE + |
| - " Model: " + Build.MODEL + " PRODUCT: " + Build.PRODUCT); |
| + @CalledByNative |
| + public void setMode(int mode) { |
| + try { |
| + mAudioManager.setMode(mode); |
| + } catch (SecurityException e) { |
| + Log.e(TAG, "setMode exception: " + e.getMessage()); |
| + logDeviceInfo(); |
| + } |
| + } |
| + |
| + /** |
| + * Activates, i.e., starts routing audio to, the specified audio device. |
| + * |
| + * @param deviceId Unique device ID (integer converted to string) |
| + * representing the selected device. This string is empty if the so-called |
| + * default device is selected. |
| + */ |
| + @CalledByNative |
| + public void setDevice(String deviceId) { |
| + if (deviceId.isEmpty()) { |
| + logd("setDevice: default"); |
| + // Use a special selection scheme if the default device is selected. |
| + // The "most unique" device will be selected; Bluetooth first, then |
| + // wired headset and last the speaker phone. |
| + if (mAudioDevices.contains(DEVICE_BLUETOOTH_HEADSET)) { |
| + // TODO(henrika): possibly need improvements here if we are |
| + // in a STATE_BLUETOOTH_TURNING_OFF state. |
| + setAudioDevice(DEVICE_BLUETOOTH_HEADSET); |
| + } else if (mAudioDevices.contains(DEVICE_WIRED_HEADSET)) { |
| + setAudioDevice(DEVICE_WIRED_HEADSET); |
| + } else { |
| + setAudioDevice(DEVICE_SPEAKERPHONE); |
| + } |
| + } else { |
| + logd("setDevice: " + deviceId); |
| + // A non-default device is specified. Verify that it is valid |
| + // device, and if so, start using it. |
| + List<Integer> validIds = Arrays.asList(VALID_DEVICES); |
| + Integer id = Integer.valueOf(deviceId); |
| + if (validIds.contains(id)) { |
| + setAudioDevice(id.intValue()); |
| + } else { |
| + loge("Invalid device ID!"); |
| + } |
| + } |
| + } |
| + |
| + /** |
| + * @return the current list of available audio devices. |
| + * Note that this call does not trigger any update of the list of devices, |
| + * it only copies the current state in to the output array. |
| + */ |
| + @CalledByNative |
| + public AudioDeviceName[] getAudioInputDeviceNames() { |
| + List<String> devices = new ArrayList<String>(); |
| + AudioDeviceName[] array = new AudioDeviceName[mAudioDevices.size()]; |
| + int i = 0; |
| + for (Integer dev : mAudioDevices) { |
| + array[i] = new AudioDeviceName(dev, DEVICE_NAMES[dev]); |
| + devices.add(DEVICE_NAMES[dev]); |
| + i++; |
| + } |
| + logd("getAudioInputDeviceNames: " + devices); |
| + return array; |
| } |
| @CalledByNative |
| @@ -165,4 +317,218 @@ class AudioManagerAndroid { |
| DEFAULT_FRAME_PER_BUFFER : Integer.parseInt(framesPerBuffer)); |
| } |
| + /** Sets the speaker phone mode. */ |
| + public void setSpeakerphoneOn(boolean on) { |
| + boolean wasOn = mAudioManager.isSpeakerphoneOn(); |
| + if (wasOn == on) { |
| + return; |
| + } |
| + mAudioManager.setSpeakerphoneOn(on); |
| + } |
| + |
| + /** Sets the microphone mute state. */ |
| + public void setMicrophoneMute(boolean on) { |
| + boolean wasOn = mAudioManager.isMicrophoneMute(); |
|
Jói
2013/11/29 14:40:46
nit: wasMuted maybe instead of wasOn?
henrika (OOO until Aug 14)
2013/11/29 15:31:49
Done.
|
| + if (wasOn == on) { |
| + return; |
| + } |
| + mAudioManager.setMicrophoneMute(on); |
| + } |
| + |
| + /** Gets the current microphone mute state. */ |
| + public boolean isMicrophoneMute() { |
| + return mAudioManager.isMicrophoneMute(); |
| + } |
| + |
| + /** Gets the current earpice state. */ |
| + private boolean hasEarpiece() { |
| + boolean hasFeature = mContext.getPackageManager().hasSystemFeature( |
|
Jói
2013/11/29 14:40:46
could just return directly
henrika (OOO until Aug 14)
2013/11/29 15:31:49
Done.
|
| + PackageManager.FEATURE_TELEPHONY); |
| + return hasFeature; |
| + } |
| + |
| + /** |
| + * Registers receiver for the broadcasted intent when a wired headset is |
| + * plugged in or unplugged. The received intent will have an extra |
| + * 'state' value where 0 means unplugged, and 1 means plugged. |
| + */ |
| + private void registerForWiredHeadsetIntentBroadcast() { |
| + IntentFilter filter = new IntentFilter(); |
| + filter.addAction(Intent.ACTION_HEADSET_PLUG); |
| + |
| + /** |
| + * Receiver which handles changes in wired headset availablilty. |
| + */ |
| + mWiredHeadsetReceiver = new BroadcastReceiver() { |
| + private static final int STATE_UNPLUGGED = 0; |
| + private static final int STATE_PLUGGED = 1; |
| + private static final int HAS_NO_MIC = 0; |
| + private static final int HAS_MIC = 1; |
| + |
| + @Override |
| + public void onReceive(Context context, Intent intent) { |
| + String action = intent.getAction(); |
| + if (action.equals(Intent.ACTION_HEADSET_PLUG)) { |
|
Jói
2013/11/29 14:40:46
Will it not always equal this, since you have a fi
tommi (sloooow) - chröme
2013/11/29 15:04:33
+1 - and if it isn't then it might be better to sa
henrika (OOO until Aug 14)
2013/11/29 15:31:49
You guys ;-)
|
| + int state = intent.getIntExtra("state", STATE_UNPLUGGED); |
| + int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); |
| + String name = intent.getStringExtra("name"); |
| + logd("==> onReceive: s=" + state |
| + + ", m=" + microphone |
| + + ", n=" + name |
| + + ", s=" + isInitialStickyBroadcast()); |
| + |
| + switch (state) { |
| + case STATE_UNPLUGGED: |
| + // Wired headset and earpiece are mutually exclusive. |
| + mAudioDevices.remove(DEVICE_WIRED_HEADSET); |
| + if (hasEarpiece()) { |
| + mAudioDevices.add(DEVICE_EARPIECE); |
| + } |
| + // If wired headset was used before it was unplugged, |
| + // switch to speaker phone. If it was not in use; just |
| + // log the change. |
| + if (mAudioDeviceState == STATE_WIRED_HEADSET_ON) { |
| + setAudioDevice(DEVICE_SPEAKERPHONE); |
| + } else { |
| + reportUpdate(); |
| + } |
| + break; |
| + case STATE_PLUGGED: |
| + // Wired headset and earpiece are mutually exclusive. |
| + mAudioDevices.add(DEVICE_WIRED_HEADSET); |
| + mAudioDevices.remove(DEVICE_EARPIECE); |
| + setAudioDevice(DEVICE_WIRED_HEADSET); |
| + break; |
| + } |
| + } |
| + } |
| + }; |
| + |
| + // Note: the intent we register for here is sticky, so it'll tell us |
| + // immediately what the last action was (plugged or unplugged). |
| + // It will enable us to set the speakerphone correctly. |
| + mContext.registerReceiver(mWiredHeadsetReceiver, filter); |
| + } |
| + |
| + /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */ |
| + private void unregisterForWiredHeadsetIntentBroadcast() { |
| + mContext.unregisterReceiver(mWiredHeadsetReceiver); |
| + mWiredHeadsetReceiver = null; |
| + } |
| + |
| + |
|
Jói
2013/11/29 14:40:46
nit: just one blank line?
henrika (OOO until Aug 14)
2013/11/29 15:31:49
Done.
|
| + /** |
| + * Check if Bluetooth device is connected, register Bluetooth receiver |
| + * and start routing to Bluetooth if a device is connected. |
| + * TODO(henrika): currently only supports the detecion part at startup. |
| + */ |
| + private void initBluetooth() { |
| + // Bail out if we don't have the required permission. |
| + mHasBluetoothPermission = mContext.checkPermission( |
| + android.Manifest.permission.BLUETOOTH, |
| + Process.myPid(), |
| + Process.myUid()) == PackageManager.PERMISSION_GRANTED; |
| + if (!mHasBluetoothPermission) { |
| + loge("BLUETOOTH permission is missing!"); |
| + return; |
| + } |
| + |
| + // To get a BluetoothAdapter representing the local Bluetooth adapter, |
| + // when running on JELLY_BEAN_MR1 (4.2) and below, call the static |
| + // getDefaultAdapter() method; when running on JELLY_BEAN_MR2 (4.3) and |
| + // higher, retrieve it through getSystemService(String) with |
| + // BLUETOOTH_SERVICE. |
| + // Note: Most methods require the BLUETOOTH permission. |
| + BluetoothAdapter btAdapter = null; |
| + if (android.os.Build.VERSION.SDK_INT <= |
| + android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { |
| + // Use static method for Android 4.2 and below to get the |
| + // BluetoothAdapter. |
| + btAdapter = BluetoothAdapter.getDefaultAdapter(); |
| + } else { |
| + // Use BluetoothManager to get the BluetoothAdapter for |
| + // Android 4.3 and above. |
| + BluetoothManager btManager = |
| + (BluetoothManager)mContext.getSystemService( |
| + Context.BLUETOOTH_SERVICE); |
| + btAdapter = btManager.getAdapter(); |
| + } |
| + |
| + if (btAdapter != null && |
| + // android.bluetooth.BluetoothAdapter.getProfileConnectionState |
| + // requires BLUETOOTH permission. |
| + android.bluetooth.BluetoothProfile.STATE_CONNECTED == |
| + btAdapter.getProfileConnectionState( |
| + android.bluetooth.BluetoothProfile.HEADSET)) { |
| + mAudioDevices.add(DEVICE_BLUETOOTH_HEADSET); |
| + // TODO(henrika): ensure that we set the active audio |
| + // device to Bluetooth (not trivial). |
| + setAudioDevice(DEVICE_BLUETOOTH_HEADSET); |
| + } |
| + } |
| + |
| + /** |
| + * Changes selection of the currently active audio device. |
| + * |
| + * @param device Specifies the selected audio device. |
| + */ |
| + public void setAudioDevice(int device) { |
| + switch (device) { |
| + case DEVICE_BLUETOOTH_HEADSET: |
| + // TODO(henrika): add support for turning on an routing to |
| + // BT here. |
| + logd("--- TO BE IMPLEMENTED ---"); |
|
tommi (sloooow) - chröme
2013/11/29 15:04:33
use |if (DEBUG)| for debug logging?
henrika (OOO until Aug 14)
2013/11/29 15:31:49
Forgot, will do.
|
| + break; |
| + case DEVICE_SPEAKERPHONE: |
| + // TODO(henrika): turn off BT if required. |
| + mAudioDeviceState = STATE_SPEAKERPHONE_ON; |
| + setSpeakerphoneOn(true); |
| + break; |
| + case DEVICE_WIRED_HEADSET: |
| + // TODO(henrika): turn off BT if required. |
| + mAudioDeviceState = STATE_WIRED_HEADSET_ON; |
| + setSpeakerphoneOn(false); |
| + break; |
| + case DEVICE_EARPIECE: |
| + // TODO(henrika): turn off BT if required. |
| + mAudioDeviceState = STATE_EARPIECE_ON; |
| + setSpeakerphoneOn(false); |
| + break; |
| + default: |
| + loge("Invalid audio device selection!"); |
| + break; |
| + } |
| + reportUpdate(); |
| + } |
| + |
| + /** |
| + * For now, just log the state change but the idea is that we should |
| + * notifies a registered state change listener (if any) that there has |
| + * been a change in the state. |
| + * TODO(henrika): add support for state change listener. |
| + */ |
| + private void reportUpdate() { |
| + List<String> devices = new ArrayList<String>(); |
| + for (Integer dev : mAudioDevices) { |
| + devices.add(DEVICE_NAMES[dev]); |
| + } |
| + logd("reportUpdate: state=" + mAudioDeviceState |
| + + ", devices=" + devices); |
| + } |
| + |
| + private void logDeviceInfo() { |
| + Log.i(TAG, "Manufacturer:" + Build.MANUFACTURER + |
| + " Board: " + Build.BOARD + " Device: " + Build.DEVICE + |
| + " Model: " + Build.MODEL + " PRODUCT: " + Build.PRODUCT); |
| + } |
| + |
| + /** Trivial helper method for debug logging */ |
| + private void logd(String msg) { |
| + Log.d(TAG, msg); |
| + } |
| + |
| + /** Trivial helper method for error logging */ |
| + private void loge(String msg) { |
| + Log.e(TAG, msg); |
| + } |
| } |