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 a60b69018cf97e598308ce0ab34b0eb989ba217f..02bdf2778cfe5b6b73dbdb96a1f8069e114f0cf4 100644 |
--- a/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java |
+++ b/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java |
@@ -38,7 +38,41 @@ class AudioManagerAndroid { |
// Set to true to enable debug logs. Avoid in production builds. |
// NOTE: always check in as false. |
- private static final boolean DEBUG = false; |
+ private static final boolean DEBUG = true; |
+ |
+ /** |
+ * NonThreadSafe is a helper class used to help verify that methods of a |
+ * class are called from the same thread. |
+ * Inspired by class in package com.google.android.apps.chrome.utilities. |
+ * Is only utilized when DEBUG is set to true. |
+ */ |
+ private static class NonThreadSafe { |
+ private Long mThreadId = 0L; |
+ |
+ public NonThreadSafe() { |
+ if (DEBUG) { |
+ ensureThreadIdAssigned(); |
+ } |
+ } |
+ |
+ /** |
+ * Checks if the method is called on the valid thread. |
+ * Assigns the current thread if no thread was assigned. |
+ */ |
+ public boolean calledOnValidThread() { |
+ if (DEBUG) { |
+ ensureThreadIdAssigned(); |
+ return mThreadId.equals(Thread.currentThread().getId()); |
+ } |
+ return true; |
+ } |
+ |
+ private void ensureThreadIdAssigned() { |
+ if (DEBUG) { |
+ if (mThreadId == 0L) mThreadId = Thread.currentThread().getId(); |
+ } |
+ } |
+ } |
/** Simple container for device information. */ |
private static class AudioDeviceName { |
@@ -123,6 +157,12 @@ class AudioManagerAndroid { |
// call to setDevice(). |
private int mRequestedAudioDevice = DEVICE_INVALID; |
+ // This class should be created, initialized and closed on the main |
+ // Java thread. In addition, BroadcastReceiver.onReceive should also |
+ // happen on this thread. We use |mNonThreadSafe| to ensure that this is |
+ // the case. Only active when |DEBUG| is set to true. |
+ private final NonThreadSafe mNonThreadSafe = new NonThreadSafe(); |
+ |
// Lock to protect |mAudioDevices| and |mRequestedAudioDevice| which can |
// be accessed from the main thread and the audio manager thread. |
private final Object mLock = new Object(); |
@@ -155,6 +195,7 @@ class AudioManagerAndroid { |
} |
private AudioManagerAndroid(Context context, long nativeAudioManagerAndroid) { |
+ logd("@AudioManagerAndroid: " + Thread.currentThread()); |
mContext = context; |
mNativeAudioManagerAndroid = nativeAudioManagerAndroid; |
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); |
@@ -165,21 +206,20 @@ class AudioManagerAndroid { |
* Saves the initial speakerphone and microphone state. |
* Populates the list of available audio devices and registers receivers |
* for broadcast intents related to wired headset and Bluetooth devices. |
+ * TODO(henrika): investigate if it would be possible to move code in |
+ * init() to the constructor and code in close() to finalize(). |
*/ |
@CalledByNative |
private void init() { |
+ logd("@init: " + Thread.currentThread()); |
+ checkIfCalledOnValidThread(); |
if (DEBUG) logd("init"); |
if (mIsInitialized) |
return; |
- for (int i = 0; i < DEVICE_COUNT; ++i) { |
- mAudioDevices[i] = false; |
- } |
- |
// Initialize audio device list with things we know is always available. |
- if (hasEarpiece()) { |
- mAudioDevices[DEVICE_EARPIECE] = true; |
- } |
+ mAudioDevices[DEVICE_EARPIECE] = hasEarpiece(); |
+ mAudioDevices[DEVICE_WIRED_HEADSET] = hasWiredHeadset(); |
mAudioDevices[DEVICE_SPEAKERPHONE] = true; |
// Register receivers for broadcast intents related to Bluetooth device |
@@ -196,6 +236,7 @@ class AudioManagerAndroid { |
new Handler(mSettingsObserverThread.getLooper())); |
mIsInitialized = true; |
+ |
if (DEBUG) reportUpdate(); |
} |
@@ -205,10 +246,15 @@ class AudioManagerAndroid { |
*/ |
@CalledByNative |
private void close() { |
+ logd("@close: " + Thread.currentThread()); |
+ checkIfCalledOnValidThread(); |
if (DEBUG) logd("close"); |
if (!mIsInitialized) |
return; |
+ unregisterForWiredHeadsetIntentBroadcast(); |
+ unregisterBluetoothIntentsIfNeeded(); |
+ |
mSettingsObserverThread.quit(); |
try { |
mSettingsObserverThread.join(); |
@@ -219,9 +265,6 @@ class AudioManagerAndroid { |
mContentResolver.unregisterContentObserver(mSettingsObserver); |
mSettingsObserver = null; |
- unregisterForWiredHeadsetIntentBroadcast(); |
- unregisterBluetoothIntentsIfNeeded(); |
- |
mIsInitialized = false; |
} |
@@ -232,6 +275,7 @@ class AudioManagerAndroid { |
*/ |
@CalledByNative |
private void setCommunicationAudioModeOn(boolean on) { |
+ logd("@setCommunicationAudioModeOn: " + Thread.currentThread()); |
if (DEBUG) logd("setCommunicationAudioModeOn(" + on + ")"); |
if (on) { |
@@ -291,7 +335,9 @@ class AudioManagerAndroid { |
*/ |
@CalledByNative |
private boolean setDevice(String deviceId) { |
+ logd("@setDevice: " + Thread.currentThread()); |
if (DEBUG) logd("setDevice: " + deviceId); |
+ |
int intDeviceId = deviceId.isEmpty() ? |
DEVICE_DEFAULT : Integer.parseInt(deviceId); |
@@ -326,6 +372,8 @@ class AudioManagerAndroid { |
*/ |
@CalledByNative |
private AudioDeviceName[] getAudioInputDeviceNames() { |
+ logd("@getAudioInputDeviceNames: " + Thread.currentThread()); |
+ if (DEBUG) logd("getAudioInputDeviceNames"); |
boolean devices[] = null; |
synchronized (mLock) { |
devices = mAudioDevices.clone(); |
@@ -347,6 +395,7 @@ class AudioManagerAndroid { |
@CalledByNative |
private int getNativeOutputSampleRate() { |
+ logd("@getNativeOutputSampleRate: " + Thread.currentThread()); |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { |
String sampleRateString = mAudioManager.getProperty( |
AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); |
@@ -365,6 +414,7 @@ class AudioManagerAndroid { |
*/ |
@CalledByNative |
private static int getMinInputFrameSize(int sampleRate, int channels) { |
+ logd("@getMinInputFrameSize: " + Thread.currentThread()); |
int channelConfig; |
if (channels == 1) { |
channelConfig = AudioFormat.CHANNEL_IN_MONO; |
@@ -385,6 +435,7 @@ class AudioManagerAndroid { |
*/ |
@CalledByNative |
private static int getMinOutputFrameSize(int sampleRate, int channels) { |
+ logd("@getMinOutputFrameSize: " + Thread.currentThread()); |
int channelConfig; |
if (channels == 1) { |
channelConfig = AudioFormat.CHANNEL_OUT_MONO; |
@@ -399,12 +450,14 @@ class AudioManagerAndroid { |
@CalledByNative |
private boolean isAudioLowLatencySupported() { |
+ logd("@isAudioLowLatencySupported: " + Thread.currentThread()); |
return mContext.getPackageManager().hasSystemFeature( |
PackageManager.FEATURE_AUDIO_LOW_LATENCY); |
} |
@CalledByNative |
private int getAudioLowLatencyOutputFrameSize() { |
+ logd("@getAudioLowLatencyOutputFrameSize: " + Thread.currentThread()); |
String framesPerBuffer = |
mAudioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); |
return (framesPerBuffer == null ? |
@@ -412,7 +465,8 @@ class AudioManagerAndroid { |
} |
@CalledByNative |
- public static boolean shouldUseAcousticEchoCanceler() { |
+ private static boolean shouldUseAcousticEchoCanceler() { |
+ logd("@shouldUseAcousticEchoCanceler: " + Thread.currentThread()); |
// 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. |
@@ -429,6 +483,16 @@ class AudioManagerAndroid { |
} |
/** |
+ * Helper method for debugging purposes. Logs message if method is not |
+ * called on same thread as this object was created on. |
+ */ |
+ private void checkIfCalledOnValidThread() { |
+ if (DEBUG && !mNonThreadSafe.calledOnValidThread()) { |
+ logwtf("close is not called on valid thread!"); |
+ } |
+ } |
+ |
+ /** |
* Register for BT intents if we have the BLUETOOTH permission. |
* Also extends the list of available devices with a BT device if one exists. |
*/ |
@@ -445,9 +509,7 @@ class AudioManagerAndroid { |
if (!mHasBluetoothPermission) { |
return; |
} |
- if (hasBluetoothHeadset()) { |
- mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = true; |
- } |
+ mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = hasBluetoothHeadset(); |
// Register receivers for broadcast intents related to changes in |
// Bluetooth headset availability and usage of the SCO channel. |
@@ -487,12 +549,24 @@ class AudioManagerAndroid { |
return mAudioManager.isMicrophoneMute(); |
} |
- /** Gets the current earpice state. */ |
+ /** Gets the current earpiece state. */ |
private boolean hasEarpiece() { |
return mContext.getPackageManager().hasSystemFeature( |
PackageManager.FEATURE_TELEPHONY); |
} |
+ /** |
+ * Checks whether a wired headset is connected or not. |
+ * This is not a valid indication that audio playback is actually over |
+ * the wired headset as audio routing depends on other conditions. We |
+ * only use it as an early indicator (during initialization) of an attached |
+ * wired headset. |
+ */ |
+ @Deprecated |
+ private boolean hasWiredHeadset() { |
+ return mAudioManager.isWiredHeadsetOn(); |
+ } |
+ |
/** Checks if the process has BLUETOOTH permission or not. */ |
private boolean hasBluetoothPermission() { |
boolean hasBluetooth = mContext.checkPermission( |
@@ -559,6 +633,8 @@ class AudioManagerAndroid { |
@Override |
public void onReceive(Context context, Intent intent) { |
+ logd("@onReceive: " + Thread.currentThread()); |
+ checkIfCalledOnValidThread(); |
int state = intent.getIntExtra("state", STATE_UNPLUGGED); |
if (DEBUG) { |
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); |
@@ -627,6 +703,7 @@ class AudioManagerAndroid { |
mBluetoothHeadsetReceiver = new BroadcastReceiver() { |
@Override |
public void onReceive(Context context, Intent intent) { |
+ checkIfCalledOnValidThread(); |
// A change in connection state of the Headset profile has |
// been detected, e.g. BT headset has been connected or |
// disconnected. This broadcast is *not* sticky. |