From 7c5fe927c503c1d342c3d1ecebab9edbec9a9162 Mon Sep 17 00:00:00 2001
From: Gustav Haglund <gustav.haglund@gmail.com>
Date: Thu, 25 Apr 2019 17:18:43 +0200
Subject: [PATCH] feat: Android interuption (#240)

* Add AudioFocusListener, refactor VolumeListener

* Update js and readme

* Cleanup

* Set volume levels correct
---
 README.md                                     |   1 +
 .../react/MediaSessionCallback.java           |  68 +++++++++
 .../react/MusicControlAudioFocusListener.java |  59 ++++++++
 .../react/MusicControlEventEmitter.java       |  73 ++++++++++
 .../react/MusicControlListener.java           | 135 ------------------
 .../react/MusicControlModule.java             |  47 +++---
 .../react/MusicControlVolumeListener.java     |  49 +++++++
 src/index.ts                                  |   8 +-
 8 files changed, 279 insertions(+), 161 deletions(-)
 create mode 100644 android/src/main/java/com/tanguyantoine/react/MediaSessionCallback.java
 create mode 100644 android/src/main/java/com/tanguyantoine/react/MusicControlAudioFocusListener.java
 create mode 100644 android/src/main/java/com/tanguyantoine/react/MusicControlEventEmitter.java
 delete mode 100644 android/src/main/java/com/tanguyantoine/react/MusicControlListener.java
 create mode 100644 android/src/main/java/com/tanguyantoine/react/MusicControlVolumeListener.java

diff --git a/README.md b/README.md
index 7f4e1868..1f2f4e67 100644
--- a/README.md
+++ b/README.md
@@ -308,6 +308,7 @@ componentDidMount() {
     MusicControl.enableBackgroundMode(true);
 
     // on iOS, pause playback during audio interruptions (incoming calls) and resume afterwards.
+    // As of {{ INSERT NEXT VERSION HERE}} works for android aswell.
     MusicControl.handleAudioInterruptions(true);
 
     MusicControl.on('play', ()=> {
diff --git a/android/src/main/java/com/tanguyantoine/react/MediaSessionCallback.java b/android/src/main/java/com/tanguyantoine/react/MediaSessionCallback.java
new file mode 100644
index 00000000..c86c54f6
--- /dev/null
+++ b/android/src/main/java/com/tanguyantoine/react/MediaSessionCallback.java
@@ -0,0 +1,68 @@
+package com.tanguyantoine.react;
+
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+
+public class MediaSessionCallback extends MediaSessionCompat.Callback {
+    private final MusicControlEventEmitter emitter;
+
+    MediaSessionCallback(MusicControlEventEmitter emitter) {
+        this.emitter = emitter;
+    }
+
+    @Override
+    public void onPlay() {
+        emitter.onPlay();
+    }
+
+    @Override
+    public void onPause() {
+        emitter.onPause();
+    }
+
+    @Override
+    public void onStop() {
+        emitter.onStop();
+    }
+
+    @Override
+    public void onSkipToNext() {
+        emitter.onSkipToNext();
+    }
+
+    @Override
+    public void onSkipToPrevious() {
+        emitter.onSkipToPrevious();
+    }
+
+    @Override
+    public void onSeekTo(long pos) {
+        emitter.onSeekTo(pos);
+    }
+
+    @Override
+    public void onFastForward() {
+        emitter.onFastForward();
+    }
+
+    @Override
+    public void onRewind() {
+        emitter.onRewind();
+    }
+
+    @Override
+    public void onSetRating(RatingCompat rating) {
+        if(MusicControlModule.INSTANCE == null) return;
+        int type = MusicControlModule.INSTANCE.ratingType;
+
+        if(type == RatingCompat.RATING_PERCENTAGE) {
+            emitter.onSetRating(rating.getPercentRating());
+        } else if(type == RatingCompat.RATING_HEART) {
+            emitter.onSetRating(rating.hasHeart());
+        } else if(type == RatingCompat.RATING_THUMB_UP_DOWN) {
+            emitter.onSetRating(rating.isThumbUp());
+        } else {
+            emitter.onSetRating(rating.getStarRating());
+        }
+    }
+}
diff --git a/android/src/main/java/com/tanguyantoine/react/MusicControlAudioFocusListener.java b/android/src/main/java/com/tanguyantoine/react/MusicControlAudioFocusListener.java
new file mode 100644
index 00000000..949451fb
--- /dev/null
+++ b/android/src/main/java/com/tanguyantoine/react/MusicControlAudioFocusListener.java
@@ -0,0 +1,59 @@
+package com.tanguyantoine.react;
+
+import android.content.Context;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.os.Build;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+
+public class MusicControlAudioFocusListener implements AudioManager.OnAudioFocusChangeListener {
+    private final MusicControlEventEmitter emitter;
+    private final MusicControlVolumeListener volume;
+
+    private AudioManager mAudioManager;
+    private AudioFocusRequest mFocusRequest;
+
+    MusicControlAudioFocusListener(ReactApplicationContext context, MusicControlEventEmitter emitter,
+                                   MusicControlVolumeListener volume) {
+        this.emitter = emitter;
+        this.volume = volume;
+
+        this.mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+    }
+
+    @Override
+    public void onAudioFocusChange(int focusChange) {
+        if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
+            emitter.onStop();
+        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
+            emitter.onPause();
+        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+            volume.setCurrentVolume(40);
+        } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+            if (volume.getCurrentVolume() != 100) {
+                volume.setCurrentVolume(100);
+            }
+            emitter.onPlay();
+        }
+    }
+
+    public void requestAudioFocus() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            mFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+                    .setOnAudioFocusChangeListener(this).build();
+
+            mAudioManager.requestAudioFocus(mFocusRequest);
+        } else {
+            mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+        }
+    }
+
+    public void abandonAudioFocus() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            mAudioManager.abandonAudioFocusRequest(mFocusRequest);
+        } else {
+            mAudioManager.abandonAudioFocus(this);
+        }
+    }
+}
diff --git a/android/src/main/java/com/tanguyantoine/react/MusicControlEventEmitter.java b/android/src/main/java/com/tanguyantoine/react/MusicControlEventEmitter.java
new file mode 100644
index 00000000..bf867c9f
--- /dev/null
+++ b/android/src/main/java/com/tanguyantoine/react/MusicControlEventEmitter.java
@@ -0,0 +1,73 @@
+package com.tanguyantoine.react;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.modules.core.DeviceEventManagerModule;
+
+public class MusicControlEventEmitter {
+    private static void sendEvent(ReactApplicationContext context, String type, Object value) {
+        WritableMap data = Arguments.createMap();
+        data.putString("name", type);
+
+        if(value != null) {
+            if(value instanceof Double || value instanceof Float) {
+                data.putDouble("value", (double)value);
+            } else if(value instanceof Boolean) {
+                data.putBoolean("value", (boolean)value);
+            } else if(value instanceof Integer) {
+                data.putInt("value", (int)value);
+            }
+        }
+
+        context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("RNMusicControlEvent", data);
+    }
+
+    private final ReactApplicationContext context;
+
+    MusicControlEventEmitter(ReactApplicationContext context) {
+        this.context = context;
+    }
+
+    public void onPlay() {
+        sendEvent(context, "play", null);
+    }
+
+    public void onPause() {
+        sendEvent(context, "pause", null);
+    }
+
+    public void onStop() {
+        sendEvent(context, "stop", null);
+    }
+
+    public void onSkipToNext() {
+        sendEvent(context, "nextTrack", null);
+    }
+
+    public void onSkipToPrevious() {
+        sendEvent(context, "previousTrack", null);
+    }
+
+    public void onSeekTo(long pos) {
+        sendEvent(context, "seek", pos / 1000D);
+    }
+
+    public void onFastForward() {
+        sendEvent(context, "skipForward", null);
+    }
+
+    public void onRewind() {
+        sendEvent(context, "skipBackward", null);
+    }
+
+    public void onSetRating(float rating) {
+        sendEvent(context,"setRating", rating);
+    }
+    public void onSetRating(boolean hasHeartOrThumb) {
+        sendEvent(context,"setRating", hasHeartOrThumb);
+    }
+
+    public void onVolumeChange(int volume) { sendEvent(context, "volume", volume); }
+}
diff --git a/android/src/main/java/com/tanguyantoine/react/MusicControlListener.java b/android/src/main/java/com/tanguyantoine/react/MusicControlListener.java
deleted file mode 100644
index f12df845..00000000
--- a/android/src/main/java/com/tanguyantoine/react/MusicControlListener.java
+++ /dev/null
@@ -1,135 +0,0 @@
-package com.tanguyantoine.react;
-
-import android.support.v4.media.RatingCompat;
-import android.support.v4.media.VolumeProviderCompat;
-import android.support.v4.media.session.MediaSessionCompat;
-import com.facebook.react.bridge.Arguments;
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.WritableMap;
-import com.facebook.react.modules.core.DeviceEventManagerModule;
-
-public class MusicControlListener extends MediaSessionCompat.Callback {
-
-    private static void sendEvent(ReactApplicationContext context, String type, Object value) {
-        WritableMap data = Arguments.createMap();
-        data.putString("name", type);
-
-        if(value == null) {
-            // NOOP
-        } else if(value instanceof Double || value instanceof Float) {
-            data.putDouble("value", (double)value);
-        } else if(value instanceof Boolean) {
-            data.putBoolean("value", (boolean)value);
-        } else if(value instanceof Integer) {
-            data.putInt("value", (int)value);
-        }
-
-        context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("RNMusicControlEvent", data);
-    }
-
-    private final ReactApplicationContext context;
-
-    MusicControlListener(ReactApplicationContext context) {
-        this.context = context;
-    }
-
-    @Override
-    public void onPlay() {
-        sendEvent(context, "play", null);
-    }
-
-    @Override
-    public void onPause() {
-        sendEvent(context, "pause", null);
-    }
-
-    @Override
-    public void onStop() {
-        sendEvent(context, "stop", null);
-    }
-
-    @Override
-    public void onSkipToNext() {
-        sendEvent(context, "nextTrack", null);
-    }
-
-    @Override
-    public void onSkipToPrevious() {
-        sendEvent(context, "previousTrack", null);
-    }
-
-    @Override
-    public void onSeekTo(long pos) {
-        sendEvent(context, "seek", pos / 1000D);
-    }
-
-    @Override
-    public void onFastForward() {
-        sendEvent(context, "skipForward", null);
-    }
-
-    @Override
-    public void onRewind() {
-        sendEvent(context, "skipBackward", null);
-    }
-
-    @Override
-    public void onSetRating(RatingCompat rating) {
-        if(MusicControlModule.INSTANCE == null) return;
-        int type = MusicControlModule.INSTANCE.ratingType;
-
-        if(type == RatingCompat.RATING_PERCENTAGE) {
-            sendEvent(context, "setRating", rating.getPercentRating());
-        } else if(type == RatingCompat.RATING_HEART) {
-            sendEvent(context, "setRating", rating.hasHeart());
-        } else if(type == RatingCompat.RATING_THUMB_UP_DOWN) {
-            sendEvent(context, "setRating", rating.isThumbUp());
-        } else {
-            sendEvent(context, "setRating", rating.getStarRating());
-        }
-    }
-
-    public static class VolumeListener extends VolumeProviderCompat {
-
-        private final ReactApplicationContext context;
-        public VolumeListener(ReactApplicationContext context, boolean changeable, int maxVolume, int currentVolume) {
-            super(changeable ? VOLUME_CONTROL_ABSOLUTE : VOLUME_CONTROL_FIXED, maxVolume, currentVolume);
-            this.context = context;
-        }
-
-        public boolean isChangeable() {
-            return getVolumeControl() != VolumeProviderCompat.VOLUME_CONTROL_FIXED;
-        }
-
-        @Override
-        public void onSetVolumeTo(int volume) {
-            setCurrentVolume(volume);
-            sendEvent(context, "volume", volume);
-        }
-
-        @Override
-        public void onAdjustVolume(int direction) {
-            int maxVolume = getMaxVolume();
-            int tick = direction * (maxVolume / 10);
-            int volume = Math.max(Math.min(getCurrentVolume() + tick, maxVolume), 0);
-
-            setCurrentVolume(volume);
-            sendEvent(context, "volume", volume);
-        }
-
-        public VolumeListener create(Boolean changeable, Integer maxVolume, Integer currentVolume) {
-            if(currentVolume == null) {
-                currentVolume = getCurrentVolume();
-            } else {
-                setCurrentVolume(currentVolume);
-            }
-
-            if(changeable == null) changeable = isChangeable();
-            if(maxVolume == null) maxVolume = getMaxVolume();
-
-            if(changeable == isChangeable() && maxVolume == getMaxVolume()) return this;
-            return new VolumeListener(context, changeable, maxVolume, currentVolume);
-        }
-    }
-
-}
diff --git a/android/src/main/java/com/tanguyantoine/react/MusicControlModule.java b/android/src/main/java/com/tanguyantoine/react/MusicControlModule.java
index ac3c78c5..a5fa9faf 100644
--- a/android/src/main/java/com/tanguyantoine/react/MusicControlModule.java
+++ b/android/src/main/java/com/tanguyantoine/react/MusicControlModule.java
@@ -1,6 +1,5 @@
 package com.tanguyantoine.react;
 
-import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.NotificationChannel;
 import android.content.ComponentCallbacks2;
@@ -8,7 +7,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.Context;
-import android.content.res.Resources;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -23,7 +21,6 @@
 import android.support.v4.media.session.MediaSessionCompat;
 import android.support.v4.media.session.PlaybackStateCompat;
 import android.support.v4.app.NotificationCompat;
-import android.support.v4.content.ContextCompat;
 import android.support.v4.media.app.NotificationCompat.MediaStyle;
 import android.util.Log;
 import com.facebook.react.bridge.ReactApplicationContext;
@@ -31,7 +28,6 @@
 import com.facebook.react.bridge.ReactMethod;
 import com.facebook.react.bridge.ReadableMap;
 import com.facebook.react.bridge.ReadableType;
-import com.facebook.react.packagerconnection.NotificationOnlyHandler;
 import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
 import java.io.IOException;
 import java.io.InputStream;
@@ -53,11 +49,14 @@ public class MusicControlModule extends ReactContextBaseJavaModule implements Co
     private PlaybackStateCompat.Builder pb;
     public NotificationCompat.Builder nb;
 
+
     private PlaybackStateCompat state;
 
     public MusicControlNotification notification;
-    private MusicControlListener.VolumeListener volume;
+    private MusicControlVolumeListener volume;
     private MusicControlReceiver receiver;
+    private MusicControlEventEmitter emitter;
+    private MusicControlAudioFocusListener afListener;
 
     private Thread artworkThread;
 
@@ -121,12 +120,15 @@ public void init() {
 
         ComponentName compName = new ComponentName(context, MusicControlReceiver.class);
 
+        emitter = new MusicControlEventEmitter(context);
+
         session = new MediaSessionCompat(context, "MusicControl", compName, null);
         session.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
                 MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
-        session.setCallback(new MusicControlListener(context));
 
-        volume = new MusicControlListener.VolumeListener(context, true, 100, 100);
+        session.setCallback(new MediaSessionCallback(emitter));
+
+        volume = new MusicControlVolumeListener(context, emitter, true, 100, 100);
         if(remoteVolume) {
             session.setPlaybackToRemote(volume);
         } else {
@@ -161,6 +163,8 @@ public void init() {
 
         Intent myIntent = new Intent(context, MusicControlNotification.NotificationService.class);
 
+        afListener = new MusicControlAudioFocusListener(context, emitter, volume);
+
         if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
             context.startForegroundService(myIntent);
 
@@ -213,6 +217,15 @@ public void enableBackgroundMode(boolean enable) {
         // Nothing?
     }
 
+    @ReactMethod
+    public void observeAudioInterruptions(boolean enable) {
+        if (enable) {
+            afListener.requestAudioFocus();
+        } else {
+            afListener.abandonAudioFocus();
+        }
+    }
+
     @ReactMethod
     synchronized public void setNowPlaying(ReadableMap metadata) {
         init();
@@ -243,8 +256,6 @@ synchronized public void setNowPlaying(ReadableMap metadata) {
             rating = RatingCompat.newUnratedRating(ratingType);
         }
 
-
-
         md.putText(MediaMetadataCompat.METADATA_KEY_TITLE, title);
         md.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist);
         md.putText(MediaMetadataCompat.METADATA_KEY_ALBUM, album);
@@ -414,13 +425,13 @@ synchronized public void enableControl(String control, boolean enable, ReadableM
             case "closeNotification":
                 if(enable) {
                     if (options.hasKey("when")) {
-                       if ("always".equals(options.getString("when"))) {
-                           this.notificationClose = notificationClose.ALWAYS;
-                       }else if ("paused".equals(options.getString("when"))) {
-                           this.notificationClose = notificationClose.PAUSED;
-                       }else {
-                           this.notificationClose = notificationClose.NEVER;
-                       }
+                        if ("always".equals(options.getString("when"))) {
+                            this.notificationClose = notificationClose.ALWAYS;
+                        }else if ("paused".equals(options.getString("when"))) {
+                            this.notificationClose = notificationClose.PAUSED;
+                        }else {
+                            this.notificationClose = notificationClose.NEVER;
+                        }
                     }
                     return;
                 }
@@ -448,7 +459,6 @@ private Bitmap loadArtwork(String url, boolean local) {
         try {
             // If we are running the app in debug mode, the "local" image will be served from htt://localhost:8080, so we need to check for this case and load those images from URL
             if(local && !url.startsWith("http")) {
-
                 // Gets the drawable from the RN's helper for local resources
                 ResourceDrawableIdHelper helper = ResourceDrawableIdHelper.getInstance();
                 Drawable image = helper.getResourceDrawable(getReactApplicationContext(), url);
@@ -458,16 +468,13 @@ private Bitmap loadArtwork(String url, boolean local) {
                 } else {
                     bitmap = BitmapFactory.decodeFile(url);
                 }
-
             } else {
-
                 // Open connection to the URL and decodes the image
                 URLConnection con = new URL(url).openConnection();
                 con.connect();
                 InputStream input = con.getInputStream();
                 bitmap = BitmapFactory.decodeStream(input);
                 input.close();
-
             }
         } catch(IOException ex) {
             Log.w(TAG, "Could not load the artwork", ex);
diff --git a/android/src/main/java/com/tanguyantoine/react/MusicControlVolumeListener.java b/android/src/main/java/com/tanguyantoine/react/MusicControlVolumeListener.java
new file mode 100644
index 00000000..95281b75
--- /dev/null
+++ b/android/src/main/java/com/tanguyantoine/react/MusicControlVolumeListener.java
@@ -0,0 +1,49 @@
+package com.tanguyantoine.react;
+
+import android.support.v4.media.VolumeProviderCompat;
+import com.facebook.react.bridge.ReactApplicationContext;
+
+public class MusicControlVolumeListener extends VolumeProviderCompat {
+    private final ReactApplicationContext context;
+    private final MusicControlEventEmitter emitter;
+
+    MusicControlVolumeListener(ReactApplicationContext context, MusicControlEventEmitter emitter, boolean changeable, int maxVolume, int currentVolume) {
+        super(changeable ? VOLUME_CONTROL_ABSOLUTE : VOLUME_CONTROL_FIXED, maxVolume, currentVolume);
+        this.context = context;
+        this.emitter = emitter;
+    }
+
+    public boolean isChangeable() {
+        return getVolumeControl() != VolumeProviderCompat.VOLUME_CONTROL_FIXED;
+    }
+
+    @Override
+    public void onSetVolumeTo(int volume) {
+        setCurrentVolume(volume);
+        emitter.onVolumeChange(volume);
+    }
+
+    @Override
+    public void onAdjustVolume(int direction) {
+        int maxVolume = getMaxVolume();
+        int tick = direction * (maxVolume / 10);
+        int volume = Math.max(Math.min(getCurrentVolume() + tick, maxVolume), 0);
+
+        setCurrentVolume(volume);
+        emitter.onVolumeChange(volume);
+    }
+
+    public MusicControlVolumeListener create(Boolean changeable, Integer maxVolume, Integer currentVolume) {
+        if(currentVolume == null) {
+            currentVolume = getCurrentVolume();
+        } else {
+            setCurrentVolume(currentVolume);
+        }
+
+        if(changeable == null) changeable = isChangeable();
+        if(maxVolume == null) maxVolume = getMaxVolume();
+
+        if(changeable == isChangeable() && maxVolume == getMaxVolume()) return this;
+        return new MusicControlVolumeListener(context, emitter, changeable, maxVolume, currentVolume);
+    }
+}
diff --git a/src/index.ts b/src/index.ts
index ef05e56c..18fb69d8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -88,12 +88,8 @@ const MusicControl = {
     })
     NativeMusicControl.stopControl()
   },
-  handleAudioInterruptions: function(enable: boolean): void {
-    if (IS_ANDROID) {
-      console.log('Audio interruption handling not implemented for Android')
-    } else {
-      NativeMusicControl.observeAudioInterruptions(enable)
-    }
+  handleAudioInterruptions: function(enable: boolean): void {    
+    NativeMusicControl.observeAudioInterruptions(enable)
   }
 }