blob: afaa0358ec184f956b2616f0037b33f699454f1e [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.media;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.audiofx.AcousticEchoCanceler;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.provider.Settings;
import android.util.Log;
import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@JNINamespace("media")
class AudioManagerAndroid {
private static final String TAG = "AudioManagerAndroid";
// Set to true to enable debug logs. Always check in as false.
private static final boolean DEBUG = false;
/** 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 = -1;
private static final int DEVICE_SPEAKERPHONE = 0;
private static final int DEVICE_WIRED_HEADSET = 1;
private static final int DEVICE_EARPIECE = 2;
private static final int DEVICE_BLUETOOTH_HEADSET = 3;
private static final int DEVICE_COUNT = 4;
// Maps audio device types to string values. This map must be in sync
// with the device types above.
// TODO(henrika): add support for proper detection of device names and
// localize the name strings by using resource strings.
private static final String[] DEVICE_NAMES = new String[] {
"Speakerphone",
"Wired headset", // With or without microphone
"Headset earpiece", // Only available on mobile phones
"Bluetooth headset",
};
// List of valid device types.
private static final Integer[] VALID_DEVICES = new Integer[] {
DEVICE_SPEAKERPHONE,
DEVICE_WIRED_HEADSET,
DEVICE_EARPIECE,
DEVICE_BLUETOOTH_HEADSET,
};
// The device does not have any audio device.
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;
// TODO(henrika): document the valid state transitions.
// 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 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 final long mNativeAudioManagerAndroid;
private boolean mHasBluetoothPermission = false;
private boolean mIsInitialized = false;
private boolean mSavedIsSpeakerphoneOn;
private boolean mSavedIsMicrophoneMute;
private Integer mAudioDeviceState = STATE_NO_DEVICE_SELECTED;
// Lock to protect |mAudioDevices| which can be accessed from the main
// thread and the audio manager thread.
private final Object mLock = new Object();
// Contains a list of currently available audio devices.
private boolean[] mAudioDevices = new boolean[DEVICE_COUNT];
private final ContentResolver mContentResolver;
private SettingsObserver mSettingsObserver = null;
private SettingsObserverThread mSettingsObserverThread = null;
private int mCurrentVolume;
private final Object mSettingsObserverLock = new Object();
// Broadcast receiver for wired headset intent broadcasts.
private BroadcastReceiver mWiredHeadsetReceiver;
/** Construction */
@CalledByNative
private static AudioManagerAndroid createAudioManagerAndroid(
Context context,
long nativeAudioManagerAndroid) {
return new AudioManagerAndroid(context, nativeAudioManagerAndroid);
}
private AudioManagerAndroid(Context context, long nativeAudioManagerAndroid) {
mContext = context;
mNativeAudioManagerAndroid = nativeAudioManagerAndroid;
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mContentResolver = mContext.getContentResolver();
}
/**
* 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 init() {
if (mIsInitialized)
return;
synchronized (mLock) {
for (int i = 0; i < DEVICE_COUNT; ++i) {
mAudioDevices[i] = false;
}
}
// Store microphone mute state and speakerphone state so it can
// be restored when closing.
mSavedIsSpeakerphoneOn = mAudioManager.isSpeakerphoneOn();
mSavedIsMicrophoneMute = 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.
synchronized (mLock) {
if (hasEarpiece()) {
mAudioDevices[DEVICE_EARPIECE] = true;
}
mAudioDevices[DEVICE_SPEAKERPHONE] = true;
}
// 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;
mSettingsObserverThread = new SettingsObserverThread();
mSettingsObserverThread.start();
synchronized (mSettingsObserverLock) {
try {
mSettingsObserverLock.wait();
} catch (InterruptedException e) {
Log.e(TAG, "unregisterHeadsetReceiver exception: " + e.getMessage());
}
}
}
/**
* Unregister all previously registered intent receivers and restore
* the stored state (stored in {@link #init()}).
*/
@CalledByNative
public void close() {
if (!mIsInitialized)
return;
if (mSettingsObserverThread != null) {
mSettingsObserverThread = null;
}
if (mSettingsObserver != null) {
mContentResolver.unregisterContentObserver(mSettingsObserver);
mSettingsObserver = null;
}
unregisterForWiredHeadsetIntentBroadcast();
// Restore previously stored audio states.
setMicrophoneMute(mSavedIsMicrophoneMute);
setSpeakerphoneOn(mSavedIsSpeakerphoneOn);
mIsInitialized = false;
}
@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) {
boolean devices[] = null;
synchronized (mLock) {
devices = mAudioDevices.clone();
}
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 (devices[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 (devices[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() {
synchronized (mLock) {
List<String> devices = new ArrayList<String>();
AudioDeviceName[] array = new AudioDeviceName[getNumOfAudioDevicesWithLock()];
int i = 0;
for (int id = 0; id < DEVICE_COUNT; ++id) {
if (mAudioDevices[id]) {
array[i] = new AudioDeviceName(id, DEVICE_NAMES[id]);
devices.add(DEVICE_NAMES[id]);
i++;
}
}
logd("getAudioInputDeviceNames: " + devices);
return array;
}
}
@CalledByNative
private int getNativeOutputSampleRate() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
String sampleRateString = mAudioManager.getProperty(
AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
return (sampleRateString == null ?
DEFAULT_SAMPLING_RATE : Integer.parseInt(sampleRateString));
} else {
return DEFAULT_SAMPLING_RATE;
}
}
/**
* Returns the minimum frame size required for audio input.
*
* @param sampleRate sampling rate
* @param channels number of channels
*/
@CalledByNative
private static int getMinInputFrameSize(int sampleRate, int channels) {
int channelConfig;
if (channels == 1) {
channelConfig = AudioFormat.CHANNEL_IN_MONO;
} else if (channels == 2) {
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
} else {
return -1;
}
return AudioRecord.getMinBufferSize(
sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) / 2 / channels;
}
/**
* Returns the minimum frame size required for audio output.
*
* @param sampleRate sampling rate
* @param channels number of channels
*/
@CalledByNative
private static int getMinOutputFrameSize(int sampleRate, int channels) {
int channelConfig;
if (channels == 1) {
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
} else if (channels == 2) {
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
} else {
return -1;
}
return AudioTrack.getMinBufferSize(
sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) / 2 / channels;
}
@CalledByNative
private boolean isAudioLowLatencySupported() {
return mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_AUDIO_LOW_LATENCY);
}
@CalledByNative
private int getAudioLowLatencyOutputFrameSize() {
String framesPerBuffer =
mAudioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
return (framesPerBuffer == null ?
DEFAULT_FRAME_PER_BUFFER : Integer.parseInt(framesPerBuffer));
}
@CalledByNative
public static boolean shouldUseAcousticEchoCanceler() {
// AcousticEchoCanceler was added in API level 16 (Jelly Bean).
// Next is a list of device models which have been vetted for good
// quality platform echo cancellation.
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
AcousticEchoCanceler.isAvailable() &&
(Build.MODEL.equals("Nexus 5") ||
Build.MODEL.equals("Nexus 7"));
}
/** 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 wasMuted = mAudioManager.isMicrophoneMute();
if (wasMuted == 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() {
return mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_TELEPHONY);
}
/**
* 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(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)) {
return;
}
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
+ ", sb=" + isInitialStickyBroadcast());
switch (state) {
case STATE_UNPLUGGED:
synchronized (mLock) {
// Wired headset and earpiece are mutually exclusive.
mAudioDevices[DEVICE_WIRED_HEADSET] = false;
if (hasEarpiece()) {
mAudioDevices[DEVICE_EARPIECE] = true;
}
}
// 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:
synchronized (mLock) {
// Wired headset and earpiece are mutually exclusive.
mAudioDevices[DEVICE_WIRED_HEADSET] = true;
mAudioDevices[DEVICE_EARPIECE] = false;
setAudioDevice(DEVICE_WIRED_HEADSET);
}
break;
default:
loge("Invalid state!");
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;
}
/**
* 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)) {
synchronized (mLock) {
mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = true;
}
// 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.
if (DEBUG) logd("--- TO BE IMPLEMENTED ---");
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();
}
private int getNumOfAudioDevicesWithLock() {
int count = 0;
for (int i = 0; i < DEVICE_COUNT; ++i) {
if (mAudioDevices[i])
count++;
}
return count;
}
/**
* For now, just log the state change but the idea is that we should
* notify 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() {
synchronized (mLock) {
List<String> devices = new ArrayList<String>();
for (int i = 0; i < DEVICE_COUNT; ++i) {
if (mAudioDevices[i])
devices.add(DEVICE_NAMES[i]);
}
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 static void logd(String msg) {
Log.d(TAG, msg);
}
/** Trivial helper method for error logging */
private static void loge(String msg) {
Log.e(TAG, msg);
}
private class SettingsObserver extends ContentObserver {
SettingsObserver() {
super(new Handler());
mContentResolver.registerContentObserver(Settings.System.CONTENT_URI, true, this);
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
int volume = mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
nativeSetMute(mNativeAudioManagerAndroid, (volume == 0));
}
}
private native void nativeSetMute(long nativeAudioManagerAndroid, boolean muted);
private class SettingsObserverThread extends Thread {
SettingsObserverThread() {
super("SettinsObserver");
}
@Override
public void run() {
// Set this thread up so the handler will work on it.
Looper.prepare();
synchronized (mSettingsObserverLock) {
mSettingsObserver = new SettingsObserver();
mSettingsObserverLock.notify();
}
// Listen for volume change.
Looper.loop();
}
}
}