Bug 1249579 - part1 : request audio focus on Fennec. r=snorp.
authorAlastor Wu <alwu@mozilla.com>
Mon, 18 Apr 2016 18:48:19 +0800
changeset 293746 67672cbc496a8fd925eaf1b29cb6f88637c9723f
parent 293745 6f8f469a45c08371398d2a98bb7fb7b2a5d07355
child 293747 d615ba6b7fc7ecdb62f2797ebfcc0b4250dab01e
push idunknown
push userunknown
push dateunknown
reviewerssnorp
bugs1249579
milestone48.0a1
Bug 1249579 - part1 : request audio focus on Fennec. r=snorp. MozReview-Commit-ID: 45qW0Wjmnad
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
mobile/android/base/moz.build
widget/android/AndroidBridge.cpp
widget/android/AndroidBridge.h
widget/android/GeneratedJNIWrappers.cpp
widget/android/GeneratedJNIWrappers.h
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -43,16 +43,17 @@ import org.mozilla.gecko.home.HomeConfig
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.home.HomeConfigPrefsBackend;
 import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.home.SearchEngine;
 import org.mozilla.gecko.javaaddons.JavaAddonManager;
+import org.mozilla.gecko.media.AudioFocusAgent;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
 import org.mozilla.gecko.preferences.GeckoPreferences;
@@ -811,16 +812,17 @@ public class BrowserApp extends GeckoApp
                            public void run() {
                                showUpdaterPermissionSnackbar();
                            }
                        })
                       .run();
         }
 
         mAddToHomeScreenPromotion = new AddToHomeScreenPromotion(this);
+        AudioFocusAgent.getInstance().attachToContext(this);
     }
 
     /**
      * Initializes the default Switchboard URLs the first time.
      * @param intent
      */
     private void initSwitchboard(Intent intent) {
         if (Experiments.isDisabled(new SafeIntent(intent)) || !AppConstants.MOZ_SWITCHBOARD) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
@@ -0,0 +1,121 @@
+package org.mozilla.gecko.media;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.util.Log;
+
+public class AudioFocusAgent {
+    private static final String LOGTAG = "AudioFocusAgent";
+
+    private static Context mContext;
+    private AudioManager mAudioManager;
+    private OnAudioFocusChangeListener mAfChangeListener;
+    private int mAudibleElementCounts;
+
+    @WrapForJNI
+    public static void notifyStartedPlaying() {
+        if (!isAttachedToContext()) {
+            return;
+        }
+        Log.d(LOGTAG, "NotifyStartedPlaying");
+        AudioFocusAgent.getInstance().requestAudioFocusIfNeeded();
+    }
+
+    @WrapForJNI
+    public static void notifyStoppedPlaying() {
+        if (!isAttachedToContext()) {
+            return;
+        }
+        Log.d(LOGTAG, "NotifyStoppedPlaying");
+        AudioFocusAgent.getInstance().abandonAudioFocusIfNeeded();
+    }
+
+    public synchronized void attachToContext(Context context) {
+        if (isAttachedToContext()) {
+            return;
+        }
+
+        mContext = context;
+        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+        mAfChangeListener = new OnAudioFocusChangeListener() {
+            public void onAudioFocusChange(int focusChange) {
+                switch (focusChange) {
+                    case AudioManager.AUDIOFOCUS_LOSS:
+                        Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS");
+                        notifyObservers("AudioFocusChanged", "Loss");
+                        // TODO : to dispatch audio-stop from gecko to trigger abandonAudioFocusIfNeeded
+                        break;
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                        Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS_TRANSIENT");
+                        notifyObservers("AudioFocusChanged", "LossTransient");
+                        break;
+                    case AudioManager.AUDIOFOCUS_GAIN:
+                        Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_GAIN");
+                        notifyObservers("AudioFocusChanged", "Gain");
+                        break;
+                    default:
+                }
+            }
+        };
+    }
+
+    @RobocopTarget
+    public static AudioFocusAgent getInstance() {
+        return AudioFocusAgent.SingletonHolder.INSTANCE;
+    }
+
+    private static class SingletonHolder {
+        private static final AudioFocusAgent INSTANCE = new AudioFocusAgent();
+    }
+
+    private static boolean isAttachedToContext() {
+        return (mContext != null);
+    }
+
+    private void notifyObservers(String topic, String data) {
+        GeckoAppShell.notifyObservers(topic, data);
+    }
+
+    private AudioFocusAgent() {
+        mAudibleElementCounts = 0;
+    }
+
+    private void requestAudioFocusIfNeeded() {
+        if (!isFirstAudibleElement()) {
+            return;
+        }
+
+        int result = mAudioManager.requestAudioFocus(mAfChangeListener,
+                                                     AudioManager.STREAM_MUSIC,
+                                                     AudioManager.AUDIOFOCUS_GAIN);
+
+        String focusMsg = (result == AudioManager.AUDIOFOCUS_GAIN) ?
+            "AudioFocus request granted" : "AudioFoucs request failed";
+        Log.d(LOGTAG, focusMsg);
+        // TODO : Enable media control when get the AudioFocus, see bug1240423.
+    }
+
+    private void abandonAudioFocusIfNeeded() {
+        if (!isLastAudibleElement()) {
+            return;
+        }
+
+        Log.d(LOGTAG, "Abandon AudioFocus");
+        mAudioManager.abandonAudioFocus(mAfChangeListener);
+    }
+
+    private boolean isFirstAudibleElement() {
+        return (++mAudibleElementCounts == 1);
+    }
+
+    private boolean isLastAudibleElement() {
+        return (--mAudibleElementCounts == 0);
+    }
+}
\ No newline at end of file
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -446,16 +446,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'InputConnectionListener.java',
     'InputMethods.java',
     'IntentHelper.java',
     'javaaddons/JavaAddonManager.java',
     'javaaddons/JavaAddonManagerV1.java',
     'lwt/LightweightTheme.java',
     'lwt/LightweightThemeDrawable.java',
     'mdns/MulticastDNSManager.java',
+    'media/AudioFocusAgent.java',
     'MediaCastingBar.java',
     'MemoryMonitor.java',
     'menu/GeckoMenu.java',
     'menu/GeckoMenuInflater.java',
     'menu/GeckoMenuItem.java',
     'menu/GeckoSubMenu.java',
     'menu/MenuItemActionBar.java',
     'menu/MenuItemDefault.java',
--- a/widget/android/AndroidBridge.cpp
+++ b/widget/android/AndroidBridge.cpp
@@ -46,16 +46,17 @@
 
 #include "MediaCodec.h"
 #include "SurfaceTexture.h"
 #include "GLContextProvider.h"
 
 #include "mozilla/TimeStamp.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/dom/ContentChild.h"
+#include "nsIObserverService.h"
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 using namespace mozilla::jni;
 using namespace mozilla::widget;
 
 AndroidBridge* AndroidBridge::sBridge = nullptr;
 pthread_t AndroidBridge::sJavaUiThread;
@@ -1573,20 +1574,22 @@ void AndroidBridge::SyncFrameMetrics(con
     aFixedLayerMargins.left = viewTransform->FixedLayerMarginLeft();
 }
 
 /* Implementation file */
 NS_IMPL_ISUPPORTS(nsAndroidBridge, nsIAndroidBridge)
 
 nsAndroidBridge::nsAndroidBridge()
 {
+  AddObservers();
 }
 
 nsAndroidBridge::~nsAndroidBridge()
 {
+  RemoveObservers();
 }
 
 NS_IMETHODIMP nsAndroidBridge::HandleGeckoMessage(JS::HandleValue val,
                                                   JSContext *cx)
 {
     if (val.isObject()) {
         JS::RootedObject object(cx, &val.toObject());
         AndroidBridge::Bridge()->HandleGeckoMessage(cx, object);
@@ -1630,16 +1633,55 @@ NS_IMETHODIMP nsAndroidBridge::ContentDo
 }
 
 NS_IMETHODIMP nsAndroidBridge::IsContentDocumentDisplayed(bool *aRet)
 {
     *aRet = AndroidBridge::Bridge()->IsContentDocumentDisplayed();
     return NS_OK;
 }
 
+NS_IMETHODIMP
+nsAndroidBridge::Observe(nsISupports* aSubject, const char* aTopic,
+                         const char16_t* aData)
+{
+  if (!strcmp(aTopic, "xpcom-shutdown")) {
+    RemoveObservers();
+  } else if (!strcmp(aTopic, "audio-playback")) {
+    ALOG_BRIDGE("nsAndroidBridge::Observe, get audio-playback event.");
+    nsAutoString activeStr(aData);
+    if (activeStr.EqualsLiteral("active")) {
+      AudioFocusAgent::NotifyStartedPlaying();
+    } else {
+      AudioFocusAgent::NotifyStoppedPlaying();
+    }
+  }
+
+  return NS_OK;
+}
+
+void
+nsAndroidBridge::AddObservers()
+{
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    obs->AddObserver(this, "xpcom-shutdown", false);
+    obs->AddObserver(this, "audio-playback", false);
+  }
+}
+
+void
+nsAndroidBridge::RemoveObservers()
+{
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    obs->RemoveObserver(this, "xpcom-shutdown");
+    obs->RemoveObserver(this, "audio-playback");
+  }
+}
+
 uint32_t
 AndroidBridge::GetScreenOrientation()
 {
     ALOG_BRIDGE("AndroidBridge::GetScreenOrientation");
 
     int16_t orientation = GeckoAppShell::GetScreenOrientationWrapper();
 
     if (!orientation)
--- a/widget/android/AndroidBridge.h
+++ b/widget/android/AndroidBridge.h
@@ -27,16 +27,17 @@
 #include "nsIMobileMessageCursorCallback.h"
 #include "nsIDOMDOMCursor.h"
 
 #include "mozilla/Likely.h"
 #include "mozilla/Mutex.h"
 #include "mozilla/Types.h"
 #include "mozilla/gfx/Point.h"
 #include "mozilla/jni/Utils.h"
+#include "nsIObserver.h"
 
 // Some debug #defines
 // #define DEBUG_ANDROID_EVENTS
 // #define DEBUG_ANDROID_WIDGET
 
 class nsIObserver;
 class Task;
 
@@ -577,23 +578,28 @@ private:
 };
 
 }
 
 #define NS_ANDROIDBRIDGE_CID \
 { 0x0FE2321D, 0xEBD9, 0x467D, \
     { 0xA7, 0x43, 0x03, 0xA6, 0x8D, 0x40, 0x59, 0x9E } }
 
-class nsAndroidBridge final : public nsIAndroidBridge
+class nsAndroidBridge final : public nsIAndroidBridge,
+                              public nsIObserver
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIANDROIDBRIDGE
+  NS_DECL_NSIOBSERVER
 
   nsAndroidBridge();
 
 private:
   ~nsAndroidBridge();
 
+  void AddObservers();
+  void RemoveObservers();
+
 protected:
 };
 
 #endif /* AndroidBridge_h__ */
--- a/widget/android/GeneratedJNIWrappers.cpp
+++ b/widget/android/GeneratedJNIWrappers.cpp
@@ -1770,16 +1770,35 @@ auto ViewTransform::Y() const -> float
     return mozilla::jni::Field<Y_t>::Get(ViewTransform::mCtx, nullptr);
 }
 
 auto ViewTransform::Y(float a0) const -> void
 {
     return mozilla::jni::Field<Y_t>::Set(ViewTransform::mCtx, nullptr, a0);
 }
 
+template<> const char mozilla::jni::Context<AudioFocusAgent, jobject>::name[] =
+        "org/mozilla/gecko/media/AudioFocusAgent";
+
+constexpr char AudioFocusAgent::NotifyStartedPlaying_t::name[];
+constexpr char AudioFocusAgent::NotifyStartedPlaying_t::signature[];
+
+auto AudioFocusAgent::NotifyStartedPlaying() -> void
+{
+    return mozilla::jni::Method<NotifyStartedPlaying_t>::Call(AudioFocusAgent::Context(), nullptr);
+}
+
+constexpr char AudioFocusAgent::NotifyStoppedPlaying_t::name[];
+constexpr char AudioFocusAgent::NotifyStoppedPlaying_t::signature[];
+
+auto AudioFocusAgent::NotifyStoppedPlaying() -> void
+{
+    return mozilla::jni::Method<NotifyStoppedPlaying_t>::Call(AudioFocusAgent::Context(), nullptr);
+}
+
 template<> const char mozilla::jni::Context<Restrictions, jobject>::name[] =
         "org/mozilla/gecko/restrictions/Restrictions";
 
 constexpr char Restrictions::IsAllowed_t::name[];
 constexpr char Restrictions::IsAllowed_t::signature[];
 
 auto Restrictions::IsAllowed(int32_t a0, mozilla::jni::String::Param a1) -> bool
 {
--- a/widget/android/GeneratedJNIWrappers.h
+++ b/widget/android/GeneratedJNIWrappers.h
@@ -4102,16 +4102,55 @@ public:
     auto Y() const -> float;
 
     auto Y(float) const -> void;
 
     static const bool isMultithreaded = true;
 
 };
 
+class AudioFocusAgent : public mozilla::jni::ObjectBase<AudioFocusAgent, jobject>
+{
+public:
+    explicit AudioFocusAgent(const Context& ctx) : ObjectBase<AudioFocusAgent, jobject>(ctx) {}
+
+    struct NotifyStartedPlaying_t {
+        typedef AudioFocusAgent Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<> Args;
+        static constexpr char name[] = "notifyStartedPlaying";
+        static constexpr char signature[] =
+                "()V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+    };
+
+    static auto NotifyStartedPlaying() -> void;
+
+    struct NotifyStoppedPlaying_t {
+        typedef AudioFocusAgent Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<> Args;
+        static constexpr char name[] = "notifyStoppedPlaying";
+        static constexpr char signature[] =
+                "()V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+    };
+
+    static auto NotifyStoppedPlaying() -> void;
+
+    static const bool isMultithreaded = false;
+
+};
+
 class Restrictions : public mozilla::jni::ObjectBase<Restrictions, jobject>
 {
 public:
     explicit Restrictions(const Context& ctx) : ObjectBase<Restrictions, jobject>(ctx) {}
 
     struct IsAllowed_t {
         typedef Restrictions Owner;
         typedef bool ReturnType;