Bug 656101 - support for audio ducking; r?ihsiao draft
authorAdam Velebil <adamtomasv@gmail.com>
Sun, 29 Jan 2017 10:03:37 +0100
changeset 467772 09cbbc30f25c6caad2527c096fb37d50b9941bcf
parent 467601 045d8fe30f546ab08466c9586ce298e6459c2069
child 543768 325f3876b5a3d616005bf431a62ea1bd71825cc3
push id43265
push useradamtomasv@gmail.com
push dateSun, 29 Jan 2017 09:07:25 +0000
reviewersihsiao
bugs656101
milestone54.0a1
Bug 656101 - support for audio ducking; r?ihsiao MozReview-Commit-ID: 1OumuSeEIEi
mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
--- a/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/AudioFocusAgent.java
@@ -16,16 +16,17 @@ public class AudioFocusAgent {
 
     private static Context mContext;
     private AudioManager mAudioManager;
     private OnAudioFocusChangeListener mAfChangeListener;
 
     public static final String OWN_FOCUS = "own_focus";
     public static final String LOST_FOCUS = "lost_focus";
     public static final String LOST_FOCUS_TRANSIENT = "lost_focus_transient";
+    public static final String LOST_FOCUS_TRANSIENT_CAN_DUCK = "lost_focus_transient_can_duck";
 
     private String mAudioFocusState = LOST_FOCUS;
 
     @WrapForJNI(calledFrom = "gecko")
     public static void notifyStartedPlaying() {
         if (!isAttachedToContext()) {
             return;
         }
@@ -60,23 +61,30 @@ public class AudioFocusAgent {
                         mAudioFocusState = LOST_FOCUS;
                         break;
                     case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                         Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS_TRANSIENT");
                         notifyObservers("AudioFocusChanged", "lostAudioFocusTransiently");
                         notifyMediaControlService(MediaControlService.ACTION_PAUSE_BY_AUDIO_FOCUS);
                         mAudioFocusState = LOST_FOCUS_TRANSIENT;
                         break;
+                    case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                        Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK");
+                        notifyMediaControlService(MediaControlService.ACTION_START_AUDIO_DUCK);
+                        mAudioFocusState = LOST_FOCUS_TRANSIENT_CAN_DUCK;
+                        break;
                     case AudioManager.AUDIOFOCUS_GAIN:
-                        if (!mAudioFocusState.equals(LOST_FOCUS_TRANSIENT)) {
-                            return;
+                        if (mAudioFocusState.equals(LOST_FOCUS_TRANSIENT_CAN_DUCK)) {
+                            Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_GAIN (from DUCKING)");
+                            notifyMediaControlService(MediaControlService.ACTION_STOP_AUDIO_DUCK);
+                        } else if (mAudioFocusState.equals(LOST_FOCUS_TRANSIENT)) {
+                            Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_GAIN");
+                            notifyObservers("AudioFocusChanged", "gainAudioFocus");
+                            notifyMediaControlService(MediaControlService.ACTION_RESUME_BY_AUDIO_FOCUS);
                         }
-                        Log.d(LOGTAG, "onAudioFocusChange, AUDIOFOCUS_GAIN");
-                        notifyObservers("AudioFocusChanged", "gainAudioFocus");
-                        notifyMediaControlService(MediaControlService.ACTION_RESUME_BY_AUDIO_FOCUS);
                         mAudioFocusState = OWN_FOCUS;
                         break;
                     default:
                 }
             }
         };
         notifyMediaControlService(MediaControlService.ACTION_INIT);
     }
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
@@ -8,16 +8,17 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.media.AudioManager;
+import android.media.VolumeProvider;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.support.annotation.CheckResult;
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.app.NotificationManagerCompat;
@@ -38,20 +39,27 @@ public class MediaControlService extends
     private static final String LOGTAG = "MediaControlService";
 
     public static final String ACTION_INIT           = "action_init";
     public static final String ACTION_RESUME         = "action_resume";
     public static final String ACTION_PAUSE          = "action_pause";
     public static final String ACTION_STOP           = "action_stop";
     public static final String ACTION_RESUME_BY_AUDIO_FOCUS = "action_resume_audio_focus";
     public static final String ACTION_PAUSE_BY_AUDIO_FOCUS  = "action_pause_audio_focus";
-
+    public static final String ACTION_START_AUDIO_DUCK      = "action_start_audio_duck";
+    public static final String ACTION_STOP_AUDIO_DUCK       = "action_stop_audio_duck";
     private static final int MEDIA_CONTROL_ID = 1;
     private static final String MEDIA_CONTROL_PREF = "dom.audiochannel.mediaControl";
 
+    // This is maximum volume level difference when audio ducking. The number is arbitrary.
+    private static final int AUDIO_DUCK_MAX_STEPS = 3;
+    private enum AudioDucking { START, STOP };
+    private boolean mSupportsDucking = false;
+    private int mAudioDuckCurrentSteps = 0;
+
     private MediaSession mSession;
     private MediaController mController;
     private HeadSetStateReceiver mHeadSetStateReceiver;
 
     private PrefsHelper.PrefHandler mPrefsObserver;
     private final String[] mPrefs = { MEDIA_CONTROL_PREF };
 
     private boolean mInitialize = false;
@@ -208,16 +216,44 @@ public class MediaControlService extends
                 mController.getTransportControls().stop();
                 break;
             case ACTION_PAUSE_BY_AUDIO_FOCUS :
                 mController.getTransportControls().sendCustomAction(ACTION_PAUSE_BY_AUDIO_FOCUS, null);
                 break;
             case ACTION_RESUME_BY_AUDIO_FOCUS :
                 mController.getTransportControls().sendCustomAction(ACTION_RESUME_BY_AUDIO_FOCUS, null);
                 break;
+            case ACTION_START_AUDIO_DUCK:
+                handleAudioDucking(AudioDucking.START);
+                break;
+            case ACTION_STOP_AUDIO_DUCK :
+                handleAudioDucking(AudioDucking.STOP);
+                break;
+        }
+    }
+
+    private void handleAudioDucking(AudioDucking audioDucking) {
+        if (!mInitialize || !mSupportsDucking) {
+            return;
+        }
+
+        int currentVolume = mController.getPlaybackInfo().getCurrentVolume();
+        int maxVolume = mController.getPlaybackInfo().getMaxVolume();
+
+        int adjustDirection;
+        if (audioDucking == AudioDucking.START) {
+            mAudioDuckCurrentSteps = Math.min(AUDIO_DUCK_MAX_STEPS, currentVolume);
+            adjustDirection = AudioManager.ADJUST_LOWER;
+        } else {
+            mAudioDuckCurrentSteps = Math.min(mAudioDuckCurrentSteps, maxVolume - currentVolume);
+            adjustDirection = AudioManager.ADJUST_RAISE;
+        }
+
+        for (int i = 0; i < mAudioDuckCurrentSteps; i++) {
+            mController.adjustVolume(adjustDirection, 0);
         }
     }
 
     private void getGeckoPreference() {
         mPrefsObserver = new PrefsHelper.PrefHandlerBase() {
             @Override
             public void prefValue(String pref, boolean value) {
                 if (pref.equals(MEDIA_CONTROL_PREF)) {
@@ -242,19 +278,27 @@ public class MediaControlService extends
             }
         };
         PrefsHelper.addObserver(mPrefs, mPrefsObserver);
     }
 
     private void initMediaSession() {
         // Android MediaSession is introduced since version L.
         mSession = new MediaSession(getApplicationContext(),
-                                    "fennec media session");
+                "fennec media session");
         mController = new MediaController(getApplicationContext(),
-                                          mSession.getSessionToken());
+                mSession.getSessionToken());
+
+        int volumeControl = mController.getPlaybackInfo().getVolumeControl();
+        if (volumeControl == VolumeProvider.VOLUME_CONTROL_ABSOLUTE ||
+                volumeControl == VolumeProvider.VOLUME_CONTROL_RELATIVE) {
+            mSupportsDucking = true;
+        } else {
+            Log.w(LOGTAG, "initMediaSession, Session does not support volume absolute or relative volume control");
+        }
 
         mSession.setCallback(new MediaSession.Callback() {
             @Override
             public void onCustomAction(String action, Bundle extras) {
                 if (action.equals(ACTION_PAUSE_BY_AUDIO_FOCUS)) {
                     Log.d(LOGTAG, "Controller, pause by audio focus changed");
                     setState(State.PAUSED);
                 } else if (action.equals(ACTION_RESUME_BY_AUDIO_FOCUS)) {