From da6ed3413eaeeea934f6e34c455cf9364c8c3a01 Mon Sep 17 00:00:00 2001 From: "wjia@chromium.org" Date: Thu, 5 Dec 2013 19:09:18 +0000 Subject: [PATCH] Mute audio when volume is zero on Android. We are using Communication mode for audio backend on Android. By design, the platform voice volume never goes to zero. This patch uses ContentObserver to listen to volume change. When volume is set to zero, AudioManagerAndroid will mute all its output streams. BUG=263399 R=henrika@chromium.org, qinmin@chromium.org, tommi@chromium.org Review URL: https://codereview.chromium.org/93233003 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@239007 0039d316-1c4b-4281-b951-d872f2087c98 --- .../findbugs_filter/findbugs_known_bugs.txt | 2 + media/audio/android/audio_manager_android.cc | 28 ++++++- media/audio/android/audio_manager_android.h | 16 ++++ media/audio/android/opensles_output.cc | 9 ++- media/audio/android/opensles_output.h | 10 +++ .../chromium/media/AudioManagerAndroid.java | 78 ++++++++++++++++++- 6 files changed, 138 insertions(+), 5 deletions(-) diff --git a/build/android/findbugs_filter/findbugs_known_bugs.txt b/build/android/findbugs_filter/findbugs_known_bugs.txt index 09e403fe63934d..5ac2ae017a00ad 100644 --- a/build/android/findbugs_filter/findbugs_known_bugs.txt +++ b/build/android/findbugs_filter/findbugs_known_bugs.txt @@ -26,6 +26,8 @@ M D SF: Switch statement found in org.chromium.chrome.browser.database.SQLiteCur M D SF: Switch statement found in org.chromium.content.browser.third_party.GestureDetector.onTouchEvent(MotionEvent) where default case is missing At GestureDetector.java M M UG: org.chromium.content.browser.JavaBridgeReturnValuesTest$TestObject.getBooleanValue() is unsynchronized, org.chromium.content.browser.JavaBridgeReturnValuesTest$TestObject.setBooleanValue(boolean) is synchronized At JavaBridgeReturnValuesTest.java M M UG: org.chromium.content.browser.JavaBridgeReturnValuesTest$TestObject.getStringValue() is unsynchronized, org.chromium.content.browser.JavaBridgeReturnValuesTest$TestObject.setStringValue(String) is synchronized At JavaBridgeReturnValuesTest.java +M M UW: Unconditional wait in org.chromium.media.AudioManagerAndroid.init() At AudioManagerAndroid.java +M M Wa: Wait not in loop in org.chromium.media.AudioManagerAndroid.init() At AudioManagerAndroid.java M V EI2: new org.chromium.chrome.browser.FindMatchRectsDetails(int, RectF[], RectF) may expose internal representation by storing an externally mutable object into FindMatchRectsDetails.rects At FindMatchRectsDetails.java M V EI2: org.chromium.chrome.browser.ChromeBrowserProvider$BookmarkNode.setFavicon(byte[]) may expose internal representation by storing an externally mutable object into ChromeBrowserProvider$BookmarkNode.mFavicon At ChromeBrowserProvider.java M V EI2: org.chromium.chrome.browser.ChromeBrowserProvider$BookmarkNode.setThumbnail(byte[]) may expose internal representation by storing an externally mutable object into ChromeBrowserProvider$BookmarkNode.mThumbnail At ChromeBrowserProvider.java diff --git a/media/audio/android/audio_manager_android.cc b/media/audio/android/audio_manager_android.cc index 2e16a1f974ea09..b588768bbd08c1 100644 --- a/media/audio/android/audio_manager_android.cc +++ b/media/audio/android/audio_manager_android.cc @@ -53,7 +53,8 @@ AudioManagerAndroid::AudioManagerAndroid(AudioLogFactory* audio_log_factory) j_audio_manager_.Reset( Java_AudioManagerAndroid_createAudioManagerAndroid( base::android::AttachCurrentThread(), - base::android::GetApplicationContext())); + base::android::GetApplicationContext(), + reinterpret_cast(this))); Init(); } @@ -126,6 +127,12 @@ AudioOutputStream* AudioManagerAndroid::MakeAudioOutputStream( if (stream && output_stream_count() == 1) { SetAudioMode(kAudioModeInCommunication); } + + { + base::AutoLock lock(streams_lock_); + streams_.insert(static_cast(stream)); + } + return stream; } @@ -141,6 +148,8 @@ void AudioManagerAndroid::ReleaseOutputStream(AudioOutputStream* stream) { if (!output_stream_count()) { SetAudioMode(kAudioModeNormal); } + base::AutoLock lock(streams_lock_); + streams_.erase(static_cast(stream)); } void AudioManagerAndroid::ReleaseInputStream(AudioInputStream* stream) { @@ -241,6 +250,23 @@ void AudioManagerAndroid::Close() { j_audio_manager_.obj()); } +void AudioManagerAndroid::SetMute(JNIEnv* env, jobject obj, jboolean muted) { + GetMessageLoop()->PostTask( + FROM_HERE, + base::Bind( + &AudioManagerAndroid::DoSetMuteOnAudioThread, + base::Unretained(this), + muted)); +} + +void AudioManagerAndroid::DoSetMuteOnAudioThread(bool muted) { + base::AutoLock lock(streams_lock_); + for (OutputStreams::iterator it = streams_.begin(); + it != streams_.end(); ++it) { + (*it)->SetMute(muted); + } +} + void AudioManagerAndroid::SetAudioMode(int mode) { Java_AudioManagerAndroid_setMode( base::android::AttachCurrentThread(), diff --git a/media/audio/android/audio_manager_android.h b/media/audio/android/audio_manager_android.h index 59c830d61cd199..d9744ff0999f54 100644 --- a/media/audio/android/audio_manager_android.h +++ b/media/audio/android/audio_manager_android.h @@ -5,12 +5,17 @@ #ifndef MEDIA_AUDIO_ANDROID_AUDIO_MANAGER_ANDROID_H_ #define MEDIA_AUDIO_ANDROID_AUDIO_MANAGER_ANDROID_H_ +#include + #include "base/android/jni_android.h" #include "base/gtest_prod_util.h" +#include "base/synchronization/lock.h" #include "media/audio/audio_manager_base.h" namespace media { +class OpenSLESOutputStream; + // Android implemention of AudioManager. class MEDIA_EXPORT AudioManagerAndroid : public AudioManagerBase { public: @@ -52,6 +57,8 @@ class MEDIA_EXPORT AudioManagerAndroid : public AudioManagerBase { static bool RegisterAudioManager(JNIEnv* env); + void SetMute(JNIEnv* env, jobject obj, jboolean muted); + protected: virtual ~AudioManagerAndroid(); @@ -69,12 +76,21 @@ class MEDIA_EXPORT AudioManagerAndroid : public AudioManagerBase { int GetAudioLowLatencyOutputFrameSize(); int GetOptimalOutputFrameSize(int sample_rate, int channels); + void DoSetMuteOnAudioThread(bool muted); + // Allow the AudioAndroidTest to access private methods. FRIEND_TEST_ALL_PREFIXES(AudioAndroidTest, IsAudioLowLatencySupported); // Java AudioManager instance. base::android::ScopedJavaGlobalRef j_audio_manager_; + typedef std::set OutputStreams; + OutputStreams streams_; + // TODO(wjia): remove this lock once unit test modules are fixed to call + // AudioManager::MakeAudioOutputStream on the audio thread. For now, this + // lock is used to guard access to |streams_|. + base::Lock streams_lock_; + DISALLOW_COPY_AND_ASSIGN(AudioManagerAndroid); }; diff --git a/media/audio/android/opensles_output.cc b/media/audio/android/opensles_output.cc index 5643f833c3dd98..77e6778dbcf618 100644 --- a/media/audio/android/opensles_output.cc +++ b/media/audio/android/opensles_output.cc @@ -28,6 +28,7 @@ OpenSLESOutputStream::OpenSLESOutputStream(AudioManagerAndroid* manager, active_buffer_index_(0), buffer_size_bytes_(0), started_(false), + muted_(false), volume_(1.0) { DVLOG(2) << "OpenSLESOutputStream::OpenSLESOutputStream()"; format_.formatType = SL_DATAFORMAT_PCM; @@ -172,6 +173,12 @@ void OpenSLESOutputStream::GetVolume(double* volume) { *volume = static_cast(volume_); } +void OpenSLESOutputStream::SetMute(bool muted) { + DVLOG(2) << "OpenSLESOutputStream::SetMute(" << muted << ")"; + DCHECK(thread_checker_.CalledOnValidThread()); + muted_ = muted; +} + bool OpenSLESOutputStream::CreatePlayer() { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(!engine_object_.Get()); @@ -324,7 +331,7 @@ void OpenSLESOutputStream::FillBufferQueueNoLock() { // Note: If the internal representation ever changes from 16-bit PCM to // raw float, the data must be clipped and sanitized since it may come // from an untrusted source such as NaCl. - audio_bus_->Scale(volume_); + audio_bus_->Scale(muted_ ? 0.0f : volume_); audio_bus_->ToInterleaved(frames_filled, format_.bitsPerSample / 8, audio_data_[active_buffer_index_]); diff --git a/media/audio/android/opensles_output.h b/media/audio/android/opensles_output.h index 7232d5da5f7621..623b0193894ba8 100644 --- a/media/audio/android/opensles_output.h +++ b/media/audio/android/opensles_output.h @@ -40,6 +40,10 @@ class OpenSLESOutputStream : public AudioOutputStream { virtual void SetVolume(double volume) OVERRIDE; virtual void GetVolume(double* volume) OVERRIDE; + // Set the value of |muted_|. It does not affect |volume_| which can be + // got by calling GetVolume(). See comments for |muted_| below. + void SetMute(bool muted); + private: bool CreatePlayer(); @@ -96,6 +100,12 @@ class OpenSLESOutputStream : public AudioOutputStream { bool started_; + // Volume control coming from hardware. It overrides |volume_| when it's + // true. Otherwise, use |volume_| for scaling. + // This is needed because platform voice volume never goes to zero in + // COMMUNICATION mode on Android. + bool muted_; + // Volume level from 0 to 1. float volume_; 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 1a9cb0a383687a..0c37c0a9e3a51c 100644 --- a/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java +++ b/media/base/android/java/src/org/chromium/media/AudioManagerAndroid.java @@ -7,16 +7,23 @@ 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.net.Uri; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.os.Process; +import android.provider.Settings; +import android.provider.Settings.System; import android.util.Log; import java.util.ArrayList; @@ -106,6 +113,7 @@ private AudioDeviceName(int id, String name) { private final AudioManager mAudioManager; private final Context mContext; + private final long mNativeAudioManagerAndroid; private boolean mHasBluetoothPermission = false; private boolean mIsInitialized = false; @@ -121,18 +129,28 @@ private AudioDeviceName(int id, String name) { // 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) { - return new AudioManagerAndroid(context); + private static AudioManagerAndroid createAudioManagerAndroid( + Context context, + long nativeAudioManagerAndroid) { + return new AudioManagerAndroid(context, nativeAudioManagerAndroid); } - private AudioManagerAndroid(Context context) { + private AudioManagerAndroid(Context context, long nativeAudioManagerAndroid) { mContext = context; + mNativeAudioManagerAndroid = nativeAudioManagerAndroid; mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE); + mContentResolver = mContext.getContentResolver(); } /** @@ -182,6 +200,16 @@ public void init() { initBluetooth(); mIsInitialized = true; + + mSettingsObserverThread = new SettingsObserverThread(); + mSettingsObserverThread.start(); + synchronized(mSettingsObserverLock) { + try { + mSettingsObserverLock.wait(); + } catch (InterruptedException e) { + Log.e(TAG, "unregisterHeadsetReceiver exception: " + e.getMessage()); + } + } } /** @@ -193,6 +221,14 @@ 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. @@ -575,4 +611,40 @@ private void logd(String msg) { private 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(); + } + } }