From 6fcccd7e5ca167daccad658756243455c110e6a7 Mon Sep 17 00:00:00 2001 From: Guilherme Chaguri Date: Mon, 20 Apr 2020 20:18:30 -0300 Subject: [PATCH 1/2] Removed the music service in favor of direct playback --- android/src/main/AndroidManifest.xml | 19 - .../guichaguri/trackplayer/TrackPlayer.java | 7 +- .../trackplayer/module/MusicEvents.java | 27 +- .../trackplayer/module/MusicModule.java | 437 ++++++------------ .../trackplayer/service/MusicBinder.java | 73 --- .../trackplayer/service/MusicManager.java | 116 ++--- .../trackplayer/service/MusicService.java | 130 ------ .../guichaguri/trackplayer/service/Utils.java | 116 +++-- .../service/metadata/ButtonEvents.java | 71 ++- .../service/metadata/ButtonReceiver.java | 21 + .../service/metadata/MetadataManager.java | 106 +++-- .../service/models/NowPlayingMetadata.java | 13 +- .../trackplayer/service/models/Track.java | 56 +-- .../service/models/TrackMetadata.java | 21 +- 14 files changed, 465 insertions(+), 748 deletions(-) delete mode 100644 android/src/main/java/com/guichaguri/trackplayer/service/MusicBinder.java delete mode 100644 android/src/main/java/com/guichaguri/trackplayer/service/MusicService.java create mode 100644 android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonReceiver.java diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index f400812b5..ccfd944e0 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -3,24 +3,5 @@ package="com.guichaguri.trackplayer"> - - - - - - - - - - - - - - - - - - - diff --git a/android/src/main/java/com/guichaguri/trackplayer/TrackPlayer.java b/android/src/main/java/com/guichaguri/trackplayer/TrackPlayer.java index ea26bd4fa..d5cfb39f6 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/TrackPlayer.java +++ b/android/src/main/java/com/guichaguri/trackplayer/TrackPlayer.java @@ -1,5 +1,6 @@ package com.guichaguri.trackplayer; +import androidx.annotation.NonNull; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; @@ -16,12 +17,14 @@ public class TrackPlayer implements ReactPackage { @Override - public List createNativeModules(ReactApplicationContext reactContext) { + @NonNull + public List createNativeModules(@NonNull ReactApplicationContext reactContext) { return Collections.singletonList(new MusicModule(reactContext)); } @Override - public List createViewManagers(ReactApplicationContext reactContext) { + @NonNull + public List createViewManagers(@NonNull ReactApplicationContext reactContext) { return Collections.emptyList(); } diff --git a/android/src/main/java/com/guichaguri/trackplayer/module/MusicEvents.java b/android/src/main/java/com/guichaguri/trackplayer/module/MusicEvents.java index 95db8b00e..7faed30ea 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/module/MusicEvents.java +++ b/android/src/main/java/com/guichaguri/trackplayer/module/MusicEvents.java @@ -1,18 +1,9 @@ package com.guichaguri.trackplayer.module; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; - /** * @author Guichaguri */ -public class MusicEvents extends BroadcastReceiver { +public final class MusicEvents { // Media Control Events public static final String BUTTON_PLAY = "remote-play"; @@ -36,20 +27,4 @@ public class MusicEvents extends BroadcastReceiver { public static final String PLAYBACK_METADATA = "playback-metadata-received"; public static final String PLAYBACK_ERROR = "playback-error"; - private final ReactContext reactContext; - - public MusicEvents(ReactContext reactContext) { - this.reactContext = reactContext; - } - - @Override - public void onReceive(Context context, Intent intent) { - String event = intent.getStringExtra("event"); - Bundle data = intent.getBundleExtra("data"); - - WritableMap map = data != null ? Arguments.fromBundle(data) : null; - - reactContext.getJSModule(RCTDeviceEventEmitter.class).emit(event, map); - } - } diff --git a/android/src/main/java/com/guichaguri/trackplayer/module/MusicModule.java b/android/src/main/java/com/guichaguri/trackplayer/module/MusicModule.java index 221bee0a9..e326839f8 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/module/MusicModule.java +++ b/android/src/main/java/com/guichaguri/trackplayer/module/MusicModule.java @@ -1,20 +1,13 @@ package com.guichaguri.trackplayer.module; -import android.content.ComponentName; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.os.Bundle; -import android.os.IBinder; import android.support.v4.media.RatingCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.facebook.react.bridge.*; import com.google.android.exoplayer2.C; -import com.guichaguri.trackplayer.service.MusicBinder; -import com.guichaguri.trackplayer.service.MusicService; +import com.guichaguri.trackplayer.service.MusicManager; import com.guichaguri.trackplayer.service.Utils; +import com.guichaguri.trackplayer.service.metadata.MetadataManager; import com.guichaguri.trackplayer.service.models.NowPlayingMetadata; import com.guichaguri.trackplayer.service.models.Track; import com.guichaguri.trackplayer.service.player.ExoPlayback; @@ -26,13 +19,9 @@ /** * @author Guichaguri */ -public class MusicModule extends ReactContextBaseJavaModule implements ServiceConnection { +public class MusicModule extends ReactContextBaseJavaModule { - private MusicBinder binder; - private MusicEvents eventHandler; - private ArrayDeque initCallbacks = new ArrayDeque<>(); - private boolean connecting = false; - private Bundle options; + private MusicManager manager; public MusicModule(ReactApplicationContext reactContext) { super(reactContext); @@ -44,71 +33,12 @@ public String getName() { return "TrackPlayerModule"; } - @Override - public void initialize() { - ReactContext context = getReactApplicationContext(); - LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context); - - eventHandler = new MusicEvents(context); - manager.registerReceiver(eventHandler, new IntentFilter(Utils.EVENT_INTENT)); - } - @Override public void onCatalystInstanceDestroy() { - ReactContext context = getReactApplicationContext(); - - if(eventHandler != null) { - LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context); - - manager.unregisterReceiver(eventHandler); - eventHandler = null; - } - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - binder = (MusicBinder)service; - connecting = false; - - // Reapply options that user set before with updateOptions - if (options != null) { - binder.updateOptions(options); + if(manager != null) { + manager.destroy(); + manager = null; } - - // Triggers all callbacks - while(!initCallbacks.isEmpty()) { - binder.post(initCallbacks.remove()); - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - binder = null; - connecting = false; - } - - /** - * Waits for a connection to the service and/or runs the {@link Runnable} in the player thread - */ - private void waitForConnection(Runnable r) { - if(binder != null) { - binder.post(r); - return; - } else { - initCallbacks.add(r); - } - - if(connecting) return; - - ReactApplicationContext context = getReactApplicationContext(); - - // Binds the service to get a MediaWrapper instance - Intent intent = new Intent(context, MusicService.class); - context.startService(intent); - intent.setAction(Utils.CONNECT_INTENT); - context.bindService(intent, this, 0); - - connecting = true; } /* ****************************** API ****************************** */ @@ -153,25 +83,20 @@ public Map getConstants() { } @ReactMethod - public void setupPlayer(ReadableMap data, final Promise promise) { - final Bundle options = Arguments.toBundle(data); - - waitForConnection(() -> binder.setupPlayer(options, promise)); + public void setupPlayer(ReadableMap data, Promise promise) { + if (manager == null) manager = new MusicManager(getReactApplicationContext()); + manager.switchPlayback(manager.createLocalPlayback(data)); + promise.resolve(null); } @ReactMethod public void destroy() { // Ignore if it was already destroyed - if (binder == null && !connecting) return; + if (manager == null) return; try { - if(binder != null) { - binder.destroy(); - binder = null; - } - - ReactContext context = getReactApplicationContext(); - if(context != null) context.unbindService(this); + manager.destroy(); + manager = null; } catch(Exception ex) { // This method shouldn't be throwing unhandled errors even if something goes wrong. Log.e(Utils.LOG, "An error occurred while destroying the service", ex); @@ -179,303 +104,249 @@ public void destroy() { } @ReactMethod - public void updateOptions(ReadableMap data, final Promise callback) { - // keep options as we may need them for correct MetadataManager reinitialization later - options = Arguments.toBundle(data); + public void updateOptions(ReadableMap data, Promise callback) { + if (manager == null) manager = new MusicManager(getReactApplicationContext()); - waitForConnection(() -> { - binder.updateOptions(options); - callback.resolve(null); - }); + manager.setStopWithApp(Utils.getBoolean(data, "stopWithApp", false)); + manager.setAlwaysPauseOnInterruption(Utils.getBoolean(data, "alwaysPauseOnInterruption", false)); + manager.getMetadata().updateOptions(data); + callback.resolve(null); } @ReactMethod - public void add(ReadableArray tracks, final String insertBeforeId, final Promise callback) { - final ArrayList bundleList = Arguments.toList(tracks); + public void add(ReadableArray tracks, String insertBeforeId, Promise callback) { - waitForConnection(() -> { - List trackList; + List trackList; - try { - trackList = Track.createTracks(getReactApplicationContext(), bundleList, binder.getRatingType()); - } catch(Exception ex) { - callback.reject("invalid_track_object", ex); - return; - } + try { + trackList = Track.createTracks(getReactApplicationContext(), tracks, manager.getMetadata().getRatingType()); + } catch(Exception ex) { + callback.reject("invalid_track_object", ex); + return; + } - List queue = binder.getPlayback().getQueue(); - int index = -1; + List queue = manager.getPlayback().getQueue(); + int index = -1; - if(insertBeforeId != null) { - for(int i = 0; i < queue.size(); i++) { - if(queue.get(i).id.equals(insertBeforeId)) { - index = i; - break; - } + if(insertBeforeId != null) { + for(int i = 0; i < queue.size(); i++) { + if(queue.get(i).id.equals(insertBeforeId)) { + index = i; + break; } - } else { - index = queue.size(); } + } else { + index = queue.size(); + } - if(index == -1) { - callback.reject("track_not_in_queue", "Given track ID was not found in queue"); - } else if(trackList == null || trackList.isEmpty()) { - callback.reject("invalid_track_object", "Track is missing a required key"); - } else if(trackList.size() == 1) { - binder.getPlayback().add(trackList.get(0), index, callback); - } else { - binder.getPlayback().add(trackList, index, callback); - } - }); + if(index == -1) { + callback.reject("track_not_in_queue", "Given track ID was not found in queue"); + } else if(trackList.isEmpty()) { + callback.reject("invalid_track_object", "Track is missing a required key"); + } else if(trackList.size() == 1) { + manager.getPlayback().add(trackList.get(0), index, callback); + } else { + manager.getPlayback().add(trackList, index, callback); + } } @ReactMethod public void remove(ReadableArray tracks, final Promise callback) { final ArrayList trackList = Arguments.toList(tracks); - waitForConnection(() -> { - List queue = binder.getPlayback().getQueue(); - List indexes = new ArrayList<>(); + List queue = manager.getPlayback().getQueue(); + List indexes = new ArrayList<>(); - for(Object o : trackList) { - String id = o.toString(); + for(Object o : trackList) { + String id = o.toString(); - for(int i = 0; i < queue.size(); i++) { - if(queue.get(i).id.equals(id)) { - indexes.add(i); - break; - } + for(int i = 0; i < queue.size(); i++) { + if(queue.get(i).id.equals(id)) { + indexes.add(i); + break; } } + } - if (!indexes.isEmpty()) { - binder.getPlayback().remove(indexes, callback); - } else { - callback.resolve(null); - } - }); + if (!indexes.isEmpty()) { + manager.getPlayback().remove(indexes, callback); + } else { + callback.resolve(null); + } } @ReactMethod - public void updateMetadataForTrack(String id, ReadableMap map, final Promise callback) { - waitForConnection(() -> { - ExoPlayback playback = binder.getPlayback(); - List queue = playback.getQueue(); - Track track = null; - int index = -1; - - for(int i = 0; i < queue.size(); i++) { - track = queue.get(i); - - if(track.id.equals(id)) { - index = i; - break; - } + public void updateMetadataForTrack(String id, ReadableMap map, Promise callback) { + ExoPlayback playback = manager.getPlayback(); + List queue = playback.getQueue(); + Track track = null; + int index = -1; + + for(int i = 0; i < queue.size(); i++) { + track = queue.get(i); + + if(track.id.equals(id)) { + index = i; + break; } + } - if(index == -1) { - callback.reject("track_not_in_queue", "No track found"); - } else { - track.setMetadata(getReactApplicationContext(), Arguments.toBundle(map), binder.getRatingType()); - playback.updateTrack(index, track); - callback.resolve(null); - } - }); + if(index == -1) { + callback.reject("track_not_in_queue", "No track found"); + } else { + track.setMetadata(getReactApplicationContext(), map, manager.getMetadata().getRatingType()); + playback.updateTrack(index, track); + callback.resolve(null); + } } @ReactMethod - public void updateNowPlayingMetadata(ReadableMap map, final Promise callback) { - final Bundle data = Arguments.toBundle(map); + public void updateNowPlayingMetadata(ReadableMap data, Promise callback) { + MetadataManager md = manager.getMetadata(); - waitForConnection(() -> { - NowPlayingMetadata metadata = new NowPlayingMetadata(getReactApplicationContext(), data, binder.getRatingType()); - binder.updateNowPlayingMetadata(metadata); - callback.resolve(null); - }); + // TODO elapsedTime + md.updateMetadata(new NowPlayingMetadata(getReactApplicationContext(), data, manager.getMetadata().getRatingType())); + md.setActive(true); + + callback.resolve(null); } @ReactMethod - public void clearNowPlayingMetadata(final Promise callback) { - waitForConnection(() -> { - binder.clearNowPlayingMetadata(); - callback.resolve(null); - }); + public void clearNowPlayingMetadata(Promise callback) { + manager.getMetadata().setActive(false); + callback.resolve(null); } @ReactMethod - public void removeUpcomingTracks(final Promise callback) { - waitForConnection(() -> { - binder.getPlayback().removeUpcomingTracks(); - callback.resolve(null); - }); + public void removeUpcomingTracks(Promise callback) { + manager.getPlayback().removeUpcomingTracks(); + callback.resolve(null); } @ReactMethod - public void skip(final String track, final Promise callback) { - waitForConnection(() -> binder.getPlayback().skip(track, callback)); + public void skip(String track, Promise callback) { + manager.getPlayback().skip(track, callback); } @ReactMethod - public void skipToNext(final Promise callback) { - waitForConnection(() -> binder.getPlayback().skipToNext(callback)); + public void skipToNext(Promise callback) { + manager.getPlayback().skipToNext(callback); } @ReactMethod - public void skipToPrevious(final Promise callback) { - waitForConnection(() -> binder.getPlayback().skipToPrevious(callback)); + public void skipToPrevious(Promise callback) { + manager.getPlayback().skipToPrevious(callback); } @ReactMethod - public void reset(final Promise callback) { - waitForConnection(() -> { - binder.getPlayback().reset(); - callback.resolve(null); - }); + public void reset(Promise callback) { + manager.getPlayback().reset(); + callback.resolve(null); } @ReactMethod - public void play(final Promise callback) { - waitForConnection(() -> { - binder.getPlayback().play(); - callback.resolve(null); - }); + public void play(Promise callback) { + manager.getPlayback().play(); + callback.resolve(null); } @ReactMethod - public void pause(final Promise callback) { - waitForConnection(() -> { - binder.getPlayback().pause(); - callback.resolve(null); - }); + public void pause(Promise callback) { + manager.getPlayback().pause(); + callback.resolve(null); } @ReactMethod - public void stop(final Promise callback) { - waitForConnection(() -> { - binder.getPlayback().stop(); - callback.resolve(null); - }); + public void stop(Promise callback) { + manager.getPlayback().stop(); + callback.resolve(null); } @ReactMethod - public void seekTo(final float seconds, final Promise callback) { - waitForConnection(() -> { - long secondsToSkip = Utils.toMillis(seconds); - binder.getPlayback().seekTo(secondsToSkip); - callback.resolve(null); - }); + public void seekTo(float seconds, Promise callback) { + manager.getPlayback().seekTo(Utils.toMillis(seconds)); + callback.resolve(null); } @ReactMethod - public void setVolume(final float volume, final Promise callback) { - waitForConnection(() -> { - binder.getPlayback().setVolume(volume); - callback.resolve(null); - }); + public void setVolume(float volume, Promise callback) { + manager.getPlayback().setVolume(volume); + callback.resolve(null); } @ReactMethod - public void getVolume(final Promise callback) { - waitForConnection(() -> callback.resolve(binder.getPlayback().getVolume())); + public void getVolume(Promise callback) { + callback.resolve(manager.getPlayback().getVolume()); } @ReactMethod - public void setRate(final float rate, final Promise callback) { - waitForConnection(() -> { - binder.getPlayback().setRate(rate); - callback.resolve(null); - }); + public void setRate(float rate, Promise callback) { + manager.getPlayback().setRate(rate); + callback.resolve(null); } @ReactMethod public void getRate(final Promise callback) { - waitForConnection(() -> callback.resolve(binder.getPlayback().getRate())); + callback.resolve(manager.getPlayback().getRate()); } @ReactMethod - public void getTrack(final String id, final Promise callback) { - waitForConnection(() -> { - List tracks = binder.getPlayback().getQueue(); - - for(Track track : tracks) { - if(track.id.equals(id)) { - callback.resolve(Arguments.fromBundle(track.originalItem)); - return; - } + public void getTrack(String id, Promise callback) { + List tracks = manager.getPlayback().getQueue(); + + for(Track track : tracks) { + if(track.id.equals(id)) { + callback.resolve(track.originalItem); + return; } + } - callback.resolve(null); - }); + callback.resolve(null); } @ReactMethod public void getQueue(Promise callback) { - waitForConnection(() -> { - List queue = new ArrayList(); - List tracks = binder.getPlayback().getQueue(); + WritableArray queue = Arguments.createArray(); + List tracks = manager.getPlayback().getQueue(); - for(Track track : tracks) { - queue.add(track.originalItem); - } + for(Track track : tracks) { + queue.pushMap(track.originalItem); + } - callback.resolve(Arguments.fromList(queue)); - }); + callback.resolve(queue); } @ReactMethod - public void getCurrentTrack(final Promise callback) { - waitForConnection(() -> { - Track track = binder.getPlayback().getCurrentTrack(); - - if(track == null) { - callback.resolve(null); - } else { - callback.resolve(track.id); - } - }); + public void getCurrentTrack(Promise callback) { + Track track = manager.getPlayback().getCurrentTrack(); + callback.resolve(track == null ? null : track.id); } @ReactMethod public void getDuration(final Promise callback) { - waitForConnection(() -> { - long duration = binder.getPlayback().getDuration(); - - if(duration == C.TIME_UNSET) { - callback.resolve(Utils.toSeconds(0)); - } else { - callback.resolve(Utils.toSeconds(duration)); - } - }); + long duration = manager.getPlayback().getDuration(); + callback.resolve(duration == C.TIME_UNSET ? 0 : Utils.toSeconds(duration)); } @ReactMethod public void getBufferedPosition(final Promise callback) { - waitForConnection(() -> { - long position = binder.getPlayback().getBufferedPosition(); - - if(position == C.POSITION_UNSET) { - callback.resolve(Utils.toSeconds(0)); - } else { - callback.resolve(Utils.toSeconds(position)); - } - }); + long position = manager.getPlayback().getBufferedPosition(); + callback.resolve(position == C.POSITION_UNSET ? 0 : Utils.toSeconds(position)); } @ReactMethod public void getPosition(final Promise callback) { - waitForConnection(() -> { - long position = binder.getPlayback().getPosition(); + long position = manager.getPlayback().getPosition(); - if(position == C.POSITION_UNSET) { - callback.reject("unknown", "Unknown position"); - } else { - callback.resolve(Utils.toSeconds(position)); - } - }); + if(position == C.POSITION_UNSET) { + callback.reject("unknown", "Unknown position"); + } else { + callback.resolve(Utils.toSeconds(position)); + } } @ReactMethod - public void getState(final Promise callback) { - waitForConnection(() -> callback.resolve(binder.getPlayback().getState())); + public void getState(Promise callback) { + callback.resolve(manager.getPlayback().getState()); } } diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/MusicBinder.java b/android/src/main/java/com/guichaguri/trackplayer/service/MusicBinder.java deleted file mode 100644 index 27794c857..000000000 --- a/android/src/main/java/com/guichaguri/trackplayer/service/MusicBinder.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.guichaguri.trackplayer.service; - -import android.os.Binder; -import android.os.Bundle; -import com.facebook.react.bridge.Promise; -import com.facebook.react.bridge.ReadableMap; -import com.guichaguri.trackplayer.service.metadata.MetadataManager; -import com.guichaguri.trackplayer.service.models.NowPlayingMetadata; -import com.guichaguri.trackplayer.service.models.Track; -import com.guichaguri.trackplayer.service.player.ExoPlayback; - -/** - * @author Guichaguri - */ -public class MusicBinder extends Binder { - - private final MusicService service; - private final MusicManager manager; - - public MusicBinder(MusicService service, MusicManager manager) { - this.service = service; - this.manager = manager; - } - - public void post(Runnable r) { - service.handler.post(r); - } - - public ExoPlayback getPlayback() { - ExoPlayback playback = manager.getPlayback(); - - // TODO remove? - if(playback == null) { - playback = manager.createLocalPlayback(new Bundle()); - manager.switchPlayback(playback); - } - - return playback; - } - - public void setupPlayer(Bundle bundle, Promise promise) { - manager.switchPlayback(manager.createLocalPlayback(bundle)); - promise.resolve(null); - } - - public void updateOptions(Bundle bundle) { - manager.setStopWithApp(bundle.getBoolean("stopWithApp", false)); - manager.setAlwaysPauseOnInterruption(bundle.getBoolean("alwaysPauseOnInterruption", false)); - manager.getMetadata().updateOptions(bundle); - } - - public void updateNowPlayingMetadata(NowPlayingMetadata nowPlaying) { - MetadataManager metadata = manager.getMetadata(); - - // TODO elapsedTime - metadata.updateMetadata(nowPlaying); - metadata.setActive(true); - } - - public void clearNowPlayingMetadata() { - manager.getMetadata().setActive(false); - } - - public int getRatingType() { - return manager.getMetadata().getRatingType(); - } - - public void destroy() { - service.destroy(); - service.stopSelf(); - } - -} diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/MusicManager.java b/android/src/main/java/com/guichaguri/trackplayer/service/MusicManager.java index f160170a8..8aef0e4ca 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/service/MusicManager.java +++ b/android/src/main/java/com/guichaguri/trackplayer/service/MusicManager.java @@ -18,6 +18,11 @@ import android.os.PowerManager.WakeLock; import androidx.annotation.RequiresApi; import android.util.Log; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; @@ -26,6 +31,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.guichaguri.trackplayer.module.MusicEvents; +import com.guichaguri.trackplayer.module.MusicModule; import com.guichaguri.trackplayer.service.metadata.MetadataManager; import com.guichaguri.trackplayer.service.models.Track; import com.guichaguri.trackplayer.service.player.ExoPlayback; @@ -38,11 +44,13 @@ */ public class MusicManager implements OnAudioFocusChangeListener { - private final MusicService service; + private final ReactApplicationContext context; private final WakeLock wakeLock; private final WifiLock wifiLock; + private final Handler handler = new Handler(); + private MetadataManager metadata; private ExoPlayback playback; @@ -54,7 +62,7 @@ public class MusicManager implements OnAudioFocusChangeListener { private BroadcastReceiver noisyReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - service.emit(MusicEvents.BUTTON_PAUSE, null); + emitEvent(MusicEvents.BUTTON_PAUSE, null); } }; private boolean receivingNoisyEvents = false; @@ -63,16 +71,16 @@ public void onReceive(Context context, Intent intent) { private boolean alwaysPauseOnInterruption = false; @SuppressLint("InvalidWakeLockTag") - public MusicManager(MusicService service) { - this.service = service; - this.metadata = new MetadataManager(service, this); + public MusicManager(ReactApplicationContext context) { + this.context = context; + this.metadata = new MetadataManager(context, this); - PowerManager powerManager = (PowerManager)service.getSystemService(Context.POWER_SERVICE); + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "track-player-wake-lock"); wakeLock.setReferenceCounted(false); // Android 7: Use the application context here to prevent any memory leaks - WifiManager wifiManager = (WifiManager)service.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "track-player-wifi-lock"); wifiLock.setReferenceCounted(false); } @@ -98,7 +106,11 @@ public MetadataManager getMetadata() { } public Handler getHandler() { - return service.handler; + return handler; + } + + public void emitEvent(String event, WritableMap data) { + context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(event, data); } public void switchPlayback(ExoPlayback playback) { @@ -114,13 +126,13 @@ public void switchPlayback(ExoPlayback playback) { } } - public LocalPlayback createLocalPlayback(Bundle options) { - boolean autoUpdateMetadata = options.getBoolean("autoUpdateMetadata", true); - int minBuffer = (int)Utils.toMillis(options.getDouble("minBuffer", Utils.toSeconds(DEFAULT_MIN_BUFFER_MS))); - int maxBuffer = (int)Utils.toMillis(options.getDouble("maxBuffer", Utils.toSeconds(DEFAULT_MAX_BUFFER_MS))); - int playBuffer = (int)Utils.toMillis(options.getDouble("playBuffer", Utils.toSeconds(DEFAULT_BUFFER_FOR_PLAYBACK_MS))); - int backBuffer = (int)Utils.toMillis(options.getDouble("backBuffer", Utils.toSeconds(DEFAULT_BACK_BUFFER_DURATION_MS))); - long cacheMaxSize = (long)(options.getDouble("maxCacheSize", 0) * 1024); + public LocalPlayback createLocalPlayback(ReadableMap options) { + boolean autoUpdateMetadata = Utils.getBoolean(options, "autoUpdateMetadata", true); + int minBuffer = (int)Utils.toMillis(Utils.getDouble(options, "minBuffer", Utils.toSeconds(DEFAULT_MIN_BUFFER_MS))); + int maxBuffer = (int)Utils.toMillis(Utils.getDouble(options, "maxBuffer", Utils.toSeconds(DEFAULT_MAX_BUFFER_MS))); + int playBuffer = (int)Utils.toMillis(Utils.getDouble(options, "playBuffer", Utils.toSeconds(DEFAULT_BUFFER_FOR_PLAYBACK_MS))); + int backBuffer = (int)Utils.toMillis(Utils.getDouble(options, "backBuffer", Utils.toSeconds(DEFAULT_BACK_BUFFER_DURATION_MS))); + long cacheMaxSize = (long)(Utils.getDouble(options, "maxCacheSize", 0) * 1024); int multiplier = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / DEFAULT_BUFFER_FOR_PLAYBACK_MS; LoadControl control = new DefaultLoadControl.Builder() @@ -128,12 +140,12 @@ public LocalPlayback createLocalPlayback(Bundle options) { .setBackBuffer(backBuffer, false) .createDefaultLoadControl(); - SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(service, new DefaultRenderersFactory(service), new DefaultTrackSelector(), control); + SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), control); player.setAudioAttributes(new com.google.android.exoplayer2.audio.AudioAttributes.Builder() .setContentType(C.CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA).build()); - return new LocalPlayback(service, this, player, cacheMaxSize, autoUpdateMetadata); + return new LocalPlayback(context, this, player, cacheMaxSize, autoUpdateMetadata); } @SuppressLint("WakelockTimeout") @@ -149,7 +161,7 @@ public void onPlay() { if(!receivingNoisyEvents) { receivingNoisyEvents = true; - service.registerReceiver(noisyReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + context.registerReceiver(noisyReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); } if(!wakeLock.isHeld()) wakeLock.acquire(); @@ -168,7 +180,7 @@ public void onPause() { // Unregisters the noisy receiver if(receivingNoisyEvents) { - service.unregisterReceiver(noisyReceiver); + context.unregisterReceiver(noisyReceiver); receivingNoisyEvents = false; } @@ -185,7 +197,7 @@ public void onStop() { // Unregisters the noisy receiver if(receivingNoisyEvents) { - service.unregisterReceiver(noisyReceiver); + context.unregisterReceiver(noisyReceiver); receivingNoisyEvents = false; } @@ -202,9 +214,9 @@ public void onStop() { public void onStateChange(int state) { Log.d(Utils.LOG, "onStateChange"); - Bundle bundle = new Bundle(); - bundle.putInt("state", state); - service.emit(MusicEvents.PLAYBACK_STATE, bundle); + WritableMap map = Arguments.createMap(); + map.putInt("state", state); + emitEvent(MusicEvents.PLAYBACK_STATE, map); if (playback.shouldAutoUpdateMetadata()) metadata.updatePlayback(playback); @@ -216,11 +228,11 @@ public void onTrackUpdate(Track previous, long prevPos, Track next) { if(playback.shouldAutoUpdateMetadata() && next != null) metadata.updateMetadata(next); - Bundle bundle = new Bundle(); - bundle.putString("track", previous != null ? previous.id : null); - bundle.putDouble("position", Utils.toSeconds(prevPos)); - bundle.putString("nextTrack", next != null ? next.id : null); - service.emit(MusicEvents.PLAYBACK_TRACK_CHANGED, bundle); + WritableMap map = Arguments.createMap(); + map.putString("track", previous != null ? previous.id : null); + map.putDouble("position", Utils.toSeconds(prevPos)); + map.putString("nextTrack", next != null ? next.id : null); + emitEvent(MusicEvents.PLAYBACK_TRACK_CHANGED, map); } public void onReset() { @@ -230,34 +242,34 @@ public void onReset() { public void onEnd(Track previous, long prevPos) { Log.d(Utils.LOG, "onEnd"); - Bundle bundle = new Bundle(); - bundle.putString("track", previous != null ? previous.id : null); - bundle.putDouble("position", Utils.toSeconds(prevPos)); - service.emit(MusicEvents.PLAYBACK_QUEUE_ENDED, bundle); + WritableMap map = Arguments.createMap(); + map.putString("track", previous != null ? previous.id : null); + map.putDouble("position", Utils.toSeconds(prevPos)); + emitEvent(MusicEvents.PLAYBACK_QUEUE_ENDED, map); } public void onMetadataReceived(String source, String title, String url, String artist, String album, String date, String genre) { Log.d(Utils.LOG, "onMetadataReceived: " + source); - Bundle bundle = new Bundle(); - bundle.putString("source", source); - bundle.putString("title", title); - bundle.putString("url", url); - bundle.putString("artist", artist); - bundle.putString("album", album); - bundle.putString("date", date); - bundle.putString("genre", genre); - service.emit(MusicEvents.PLAYBACK_METADATA, bundle); + WritableMap map = Arguments.createMap(); + map.putString("source", source); + map.putString("title", title); + map.putString("url", url); + map.putString("artist", artist); + map.putString("album", album); + map.putString("date", date); + map.putString("genre", genre); + emitEvent(MusicEvents.PLAYBACK_METADATA, map); } public void onError(String code, String error) { Log.d(Utils.LOG, "onError"); Log.e(Utils.LOG, "Playback error: " + code + " - " + error); - Bundle bundle = new Bundle(); - bundle.putString("code", code); - bundle.putString("message", error); - service.emit(MusicEvents.PLAYBACK_ERROR, bundle); + WritableMap map = Arguments.createMap(); + map.putString("code", code); + map.putString("message", error); + emitEvent(MusicEvents.PLAYBACK_ERROR, map); } @Override @@ -293,17 +305,17 @@ public void onAudioFocusChange(int focus) { wasDucking = false; } - Bundle bundle = new Bundle(); - bundle.putBoolean("permanent", permanent); - bundle.putBoolean("paused", paused); - service.emit(MusicEvents.BUTTON_DUCK, bundle); + WritableMap map = Arguments.createMap(); + map.putBoolean("permanent", permanent); + map.putBoolean("paused", paused); + emitEvent(MusicEvents.BUTTON_DUCK, map); } private void requestFocus() { if(hasAudioFocus) return; Log.d(Utils.LOG, "Requesting audio focus..."); - AudioManager manager = (AudioManager)service.getSystemService(Context.AUDIO_SERVICE); + AudioManager manager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); int r; if(manager == null) { @@ -331,7 +343,7 @@ private void abandonFocus() { if(!hasAudioFocus) return; Log.d(Utils.LOG, "Abandoning audio focus..."); - AudioManager manager = (AudioManager)service.getSystemService(Context.AUDIO_SERVICE); + AudioManager manager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); int r; if(manager == null) { @@ -354,7 +366,7 @@ public void destroy() { // Stop receiving audio becoming noisy events if(receivingNoisyEvents) { - service.unregisterReceiver(noisyReceiver); + context.unregisterReceiver(noisyReceiver); receivingNoisyEvents = false; } diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/MusicService.java b/android/src/main/java/com/guichaguri/trackplayer/service/MusicService.java deleted file mode 100644 index a48e4c91b..000000000 --- a/android/src/main/java/com/guichaguri/trackplayer/service/MusicService.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.guichaguri.trackplayer.service; - -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.media.session.MediaButtonReceiver; - -import com.facebook.react.HeadlessJsTaskService; -import com.facebook.react.ReactInstanceManager; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.jstasks.HeadlessJsTaskConfig; -import com.guichaguri.trackplayer.service.Utils; -import javax.annotation.Nullable; - -/** - * @author Guichaguri - */ -public class MusicService extends HeadlessJsTaskService { - - MusicManager manager; - Handler handler; - - @Nullable - @Override - protected HeadlessJsTaskConfig getTaskConfig(Intent intent) { - return new HeadlessJsTaskConfig("TrackPlayer", Arguments.createMap(), 0, true); - } - - @Override - public void onHeadlessJsTaskFinish(int taskId) { - // Overridden to prevent the service from being terminated - } - - public void emit(String event, Bundle data) { - Intent intent = new Intent(Utils.EVENT_INTENT); - - intent.putExtra("event", event); - if(data != null) intent.putExtra("data", data); - - LocalBroadcastManager.getInstance(this).sendBroadcast(intent); - } - - public void destroy() { - if(handler != null) { - handler.removeMessages(0); - handler = null; - } - - if(manager != null) { - manager.destroy(); - manager = null; - } - } - - private void onStartForeground() { - boolean serviceForeground = false; - - if(manager != null) { - // The session is only active when the service is on foreground - serviceForeground = manager.getMetadata().getSession().isActive(); - } - - if(!serviceForeground) { - ReactInstanceManager reactInstanceManager = getReactNativeHost().getReactInstanceManager(); - ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); - - // Checks whether there is a React activity - if(reactContext == null || !reactContext.hasCurrentActivity()) { - String channel = Utils.getNotificationChannel((Context) this); - - // Sets the service to foreground with an empty notification - startForeground(1, new NotificationCompat.Builder(this, channel).build()); - // Stops the service right after - stopSelf(); - } - } - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - if(Utils.CONNECT_INTENT.equals(intent.getAction())) { - return new MusicBinder(this, manager); - } - - return super.onBind(intent); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if(intent != null && Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { - // Check if the app is on background, then starts a foreground service and then ends it right after - onStartForeground(); - - if(manager != null) { - MediaButtonReceiver.handleIntent(manager.getMetadata().getSession(), intent); - } - - return START_NOT_STICKY; - } - - manager = new MusicManager(this); - handler = new Handler(); - - super.onStartCommand(intent, flags, startId); - return START_NOT_STICKY; - } - - @Override - public void onDestroy() { - super.onDestroy(); - - destroy(); - } - - @Override - public void onTaskRemoved(Intent rootIntent) { - super.onTaskRemoved(rootIntent); - - if (manager == null || manager.shouldStopWithApp()) { - stopSelf(); - } - } -} diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java b/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java index cc42c8737..7d8097279 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java +++ b/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java @@ -10,18 +10,20 @@ import android.os.Bundle; import android.support.v4.media.RatingCompat; import android.support.v4.media.session.PlaybackStateCompat; -import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.*; import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper; import com.google.android.exoplayer2.upstream.RawResourceDataSource; +import java.util.ArrayList; +import java.util.List; + /** * @author Guichaguri */ public class Utils { - public static final String EVENT_INTENT = "com.guichaguri.trackplayer.event"; - public static final String CONNECT_INTENT = "com.guichaguri.trackplayer.connect"; public static final String NOTIFICATION_CHANNEL = "com.guichaguri.trackplayer"; + public static final int NOTIFICATION_ID = 103; public static final String LOG = "RNTrackPlayer"; public static Runnable toRunnable(Promise promise) { @@ -54,22 +56,24 @@ public static boolean isLocal(Uri uri) { host.equals("[::1]"); } - public static Uri getUri(Context context, Bundle data, String key) { - if(!data.containsKey(key)) return null; - Object obj = data.get(key); + public static Uri getUri(Context context, ReadableMap data, String key) { + if (!data.hasKey(key)) return null; + ReadableType type = data.getType(key); - if(obj instanceof String) { + if (type == ReadableType.String) { // Remote or Local Uri - if(((String)obj).trim().isEmpty()) + String uri = data.getString(key); + + if(uri.trim().isEmpty()) throw new RuntimeException("The URL cannot be empty"); - return Uri.parse((String)obj); + return Uri.parse(uri); - } else if(obj instanceof Bundle) { + } else if (type == ReadableType.Map) { // require/import - String uri = ((Bundle)obj).getString("uri"); + String uri = data.getMap(key).getString("uri"); ResourceDrawableIdHelper helper = ResourceDrawableIdHelper.getInstance(); int id = helper.getResourceDrawableId(context, uri); @@ -94,12 +98,11 @@ public static Uri getUri(Context context, Bundle data, String key) { return null; } - public static int getRawResourceId(Context context, Bundle data, String key) { - if(!data.containsKey(key)) return 0; - Object obj = data.get(key); + public static int getRawResourceId(Context context, ReadableMap data, String key) { + if(!data.hasKey(key)) return 0; - if(!(obj instanceof Bundle)) return 0; - String name = ((Bundle)obj).getString("uri"); + if(data.getType(key) != ReadableType.Map) return 0; + String name = data.getMap(key).getString("uri"); if(name == null || name.isEmpty()) return 0; name = name.toLowerCase().replace("-", "_"); @@ -123,21 +126,21 @@ public static boolean isStopped(int state) { return state == PlaybackStateCompat.STATE_NONE || state == PlaybackStateCompat.STATE_STOPPED; } - public static RatingCompat getRating(Bundle data, String key, int ratingType) { - if(!data.containsKey(key) || ratingType == RatingCompat.RATING_NONE) { + public static RatingCompat getRating(ReadableMap data, String key, int ratingType) { + if (!data.hasKey(key) || ratingType == RatingCompat.RATING_NONE) { return RatingCompat.newUnratedRating(ratingType); - } else if(ratingType == RatingCompat.RATING_HEART) { - return RatingCompat.newHeartRating(data.getBoolean(key, true)); - } else if(ratingType == RatingCompat.RATING_THUMB_UP_DOWN) { - return RatingCompat.newThumbRating(data.getBoolean(key, true)); - } else if(ratingType == RatingCompat.RATING_PERCENTAGE) { - return RatingCompat.newPercentageRating(data.getFloat(key, 0)); + } else if (ratingType == RatingCompat.RATING_HEART) { + return RatingCompat.newHeartRating(getBoolean(data, key, true)); + } else if (ratingType == RatingCompat.RATING_THUMB_UP_DOWN) { + return RatingCompat.newThumbRating(getBoolean(data, key, true)); + } else if (ratingType == RatingCompat.RATING_PERCENTAGE) { + return RatingCompat.newPercentageRating((float) getDouble(data, key, 0)); } else { - return RatingCompat.newStarRating(ratingType, data.getFloat(key, 0)); + return RatingCompat.newStarRating(ratingType, (float) getDouble(data, key, 0)); } } - public static void setRating(Bundle data, String key, RatingCompat rating) { + public static void setRating(WritableMap data, String key, RatingCompat rating) { if(!rating.isRated()) return; int ratingType = rating.getRatingStyle(); @@ -152,25 +155,56 @@ public static void setRating(Bundle data, String key, RatingCompat rating) { } } - public static int getInt(Bundle data, String key, int defaultValue) { - Object value = data.get(key); - if (value instanceof Number) { - return ((Number) value).intValue(); + public static int getInt(ReadableMap data, String key, int defaultValue) { + if (!data.hasKey(key) || data.getType(key) != ReadableType.Number) { + return defaultValue; + } else { + return data.getInt(key); } - return defaultValue; } - public static String getNotificationChannel(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel( - Utils.NOTIFICATION_CHANNEL, - "MusicService", - NotificationManager.IMPORTANCE_DEFAULT - ); - channel.setShowBadge(false); - channel.setSound(null, null); - ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel); + public static double getDouble(ReadableMap data, String key, double defaultValue) { + if (!data.hasKey(key) || data.getType(key) != ReadableType.Number) { + return defaultValue; + } else { + return data.getDouble(key); + } + } + + public static boolean getBoolean(ReadableMap data, String key, boolean defaultValue) { + if (!data.hasKey(key) || data.getType(key) != ReadableType.Boolean) { + return defaultValue; + } else { + return data.getBoolean(key); + } + } + + public static String getString(ReadableMap data, String key, String defaultValue) { + if (!data.hasKey(key) || data.getType(key) != ReadableType.String) { + return defaultValue; + } else { + return data.getString(key); } + } + + public static List getIntegerList(ReadableMap data, String key, List defaultValue) { + if (!data.hasKey(key) || data.getType(key) != ReadableType.Array) { + return defaultValue; + } + + ReadableArray array = data.getArray(key); + List list = new ArrayList<>(); + + for(int i = 0; i < array.size(); i++) { + if (array.getType(i) == ReadableType.Number) + list.add(array.getInt(i)); + } + + return list; + } + + public static String getNotificationChannel(Context context) { + return Utils.NOTIFICATION_CHANNEL; } } diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonEvents.java b/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonEvents.java index df9468d06..1569e53b9 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonEvents.java +++ b/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonEvents.java @@ -5,9 +5,10 @@ import android.provider.MediaStore; import android.support.v4.media.RatingCompat; import android.support.v4.media.session.MediaSessionCompat; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; import com.guichaguri.trackplayer.module.MusicEvents; import com.guichaguri.trackplayer.service.MusicManager; -import com.guichaguri.trackplayer.service.MusicService; import com.guichaguri.trackplayer.service.Utils; import com.guichaguri.trackplayer.service.models.Track; import java.util.List; @@ -17,42 +18,40 @@ */ public class ButtonEvents extends MediaSessionCompat.Callback { - private final MusicService service; private final MusicManager manager; - public ButtonEvents(MusicService service, MusicManager manager) { - this.service = service; + public ButtonEvents(MusicManager manager) { this.manager = manager; } @Override public void onPlay() { - service.emit(MusicEvents.BUTTON_PLAY, null); + manager.emitEvent(MusicEvents.BUTTON_PLAY, null); } @Override public void onPause() { - service.emit(MusicEvents.BUTTON_PAUSE, null); + manager.emitEvent(MusicEvents.BUTTON_PAUSE, null); } @Override public void onStop() { - service.emit(MusicEvents.BUTTON_STOP, null); + manager.emitEvent(MusicEvents.BUTTON_STOP, null); } @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { - Bundle bundle = new Bundle(); - bundle.putString("id", mediaId); - service.emit(MusicEvents.BUTTON_PLAY_FROM_ID, bundle); + WritableMap map = Arguments.createMap(); + map.putString("id", mediaId); + manager.emitEvent(MusicEvents.BUTTON_PLAY_FROM_ID, map); } @SuppressLint("InlinedApi") @Override public void onPlayFromSearch(String query, Bundle extras) { - Bundle bundle = new Bundle(); - bundle.putString("query", query); + WritableMap map = Arguments.createMap(); + map.putString("query", query); if(extras.containsKey(MediaStore.EXTRA_MEDIA_FOCUS)) { String focus = extras.getString(MediaStore.EXTRA_MEDIA_FOCUS); @@ -69,21 +68,21 @@ public void onPlayFromSearch(String query, Bundle extras) { focus = "title"; } - bundle.putString("focus", focus); + map.putString("focus", focus); } if(extras.containsKey(MediaStore.EXTRA_MEDIA_TITLE)) - bundle.putString("title", extras.getString(MediaStore.EXTRA_MEDIA_TITLE)); + map.putString("title", extras.getString(MediaStore.EXTRA_MEDIA_TITLE)); if(extras.containsKey(MediaStore.EXTRA_MEDIA_ARTIST)) - bundle.putString("artist", extras.getString(MediaStore.EXTRA_MEDIA_ARTIST)); + map.putString("artist", extras.getString(MediaStore.EXTRA_MEDIA_ARTIST)); if(extras.containsKey(MediaStore.EXTRA_MEDIA_ALBUM)) - bundle.putString("album", extras.getString(MediaStore.EXTRA_MEDIA_ALBUM)); + map.putString("album", extras.getString(MediaStore.EXTRA_MEDIA_ALBUM)); if(extras.containsKey(MediaStore.EXTRA_MEDIA_GENRE)) - bundle.putString("genre", extras.getString(MediaStore.EXTRA_MEDIA_GENRE)); + map.putString("genre", extras.getString(MediaStore.EXTRA_MEDIA_GENRE)); if(extras.containsKey(MediaStore.EXTRA_MEDIA_PLAYLIST)) - bundle.putString("playlist", extras.getString(MediaStore.EXTRA_MEDIA_PLAYLIST)); + map.putString("playlist", extras.getString(MediaStore.EXTRA_MEDIA_PLAYLIST)); - service.emit(MusicEvents.BUTTON_PLAY_FROM_SEARCH, bundle); + manager.emitEvent(MusicEvents.BUTTON_PLAY_FROM_SEARCH, map); } @Override @@ -93,48 +92,48 @@ public void onSkipToQueueItem(long id) { for(Track track : tracks) { if(track.queueId != id) continue; - Bundle bundle = new Bundle(); - bundle.putString("id", track.id); - service.emit(MusicEvents.BUTTON_SKIP, bundle); + WritableMap map = Arguments.createMap(); + map.putString("id", track.id); + manager.emitEvent(MusicEvents.BUTTON_SKIP, map); break; } } @Override public void onSkipToPrevious() { - service.emit(MusicEvents.BUTTON_SKIP_PREVIOUS, null); + manager.emitEvent(MusicEvents.BUTTON_SKIP_PREVIOUS, null); } @Override public void onSkipToNext() { - service.emit(MusicEvents.BUTTON_SKIP_NEXT, null); + manager.emitEvent(MusicEvents.BUTTON_SKIP_NEXT, null); } @Override public void onRewind() { - Bundle bundle = new Bundle(); - bundle.putInt("interval", manager.getMetadata().getJumpInterval()); - service.emit(MusicEvents.BUTTON_JUMP_BACKWARD, bundle); + WritableMap map = Arguments.createMap(); + map.putInt("interval", manager.getMetadata().getJumpInterval()); + manager.emitEvent(MusicEvents.BUTTON_JUMP_BACKWARD, map); } @Override public void onFastForward() { - Bundle bundle = new Bundle(); - bundle.putInt("interval", manager.getMetadata().getJumpInterval()); - service.emit(MusicEvents.BUTTON_JUMP_FORWARD, bundle); + WritableMap map = Arguments.createMap(); + map.putInt("interval", manager.getMetadata().getJumpInterval()); + manager.emitEvent(MusicEvents.BUTTON_JUMP_FORWARD, map); } @Override public void onSeekTo(long pos) { - Bundle bundle = new Bundle(); - bundle.putDouble("position", Utils.toSeconds(pos)); - service.emit(MusicEvents.BUTTON_SEEK_TO, bundle); + WritableMap map = Arguments.createMap(); + map.putDouble("position", Utils.toSeconds(pos)); + manager.emitEvent(MusicEvents.BUTTON_SEEK_TO, map); } @Override public void onSetRating(RatingCompat rating) { - Bundle bundle = new Bundle(); - Utils.setRating(bundle, "rating", rating); - service.emit(MusicEvents.BUTTON_SET_RATING, bundle); + WritableMap map = Arguments.createMap(); + Utils.setRating(map, "rating", rating); + manager.emitEvent(MusicEvents.BUTTON_SET_RATING, map); } } diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonReceiver.java b/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonReceiver.java new file mode 100644 index 000000000..f6e718029 --- /dev/null +++ b/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonReceiver.java @@ -0,0 +1,21 @@ +package com.guichaguri.trackplayer.service.metadata; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import androidx.media.session.MediaButtonReceiver; + +public class ButtonReceiver extends BroadcastReceiver { + + private final MetadataManager manager; + + public ButtonReceiver(MetadataManager manager) { + this.manager = manager; + } + + @Override + public void onReceive(Context context, Intent intent) { + MediaButtonReceiver.handleIntent(manager.getSession(), intent); + } + +} diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/metadata/MetadataManager.java b/android/src/main/java/com/guichaguri/trackplayer/service/metadata/MetadataManager.java index 2c1282dac..2fb1d259c 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/service/metadata/MetadataManager.java +++ b/android/src/main/java/com/guichaguri/trackplayer/service/metadata/MetadataManager.java @@ -3,30 +3,30 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; -import android.os.Bundle; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationCompat.Action; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.RatingCompat; -import androidx.media.app.NotificationCompat.MediaStyle; -import androidx.media.session.MediaButtonReceiver; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationCompat.Action; +import androidx.core.app.NotificationManagerCompat; +import androidx.media.app.NotificationCompat.MediaStyle; +import androidx.media.session.MediaButtonReceiver; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.target.SimpleTarget; import com.bumptech.glide.request.transition.Transition; -import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper; import com.guichaguri.trackplayer.R; import com.guichaguri.trackplayer.service.MusicManager; -import com.guichaguri.trackplayer.service.MusicService; import com.guichaguri.trackplayer.service.Utils; import com.guichaguri.trackplayer.service.models.Track; import com.guichaguri.trackplayer.service.models.TrackMetadata; @@ -39,33 +39,46 @@ */ public class MetadataManager { - private final MusicService service; + private final Context context; private final MusicManager manager; private final MediaSessionCompat session; + private final NotificationCompat.Builder builder; + private final ButtonReceiver receiver; private int ratingType = RatingCompat.RATING_NONE; private int jumpInterval = 15; private long actions = 0; private long compactActions = 0; private SimpleTarget artworkTarget; - private NotificationCompat.Builder builder; + private boolean receiverRegistered = false; private Action previousAction, rewindAction, playAction, pauseAction, stopAction, forwardAction, nextAction; - public MetadataManager(MusicService service, MusicManager manager) { - this.service = service; + public MetadataManager(Context context, MusicManager manager) { + this.context = context; this.manager = manager; - String channel = Utils.getNotificationChannel((Context) service); - this.builder = new NotificationCompat.Builder(service, channel); - this.session = new MediaSessionCompat(service, "TrackPlayer", null, null); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + Utils.NOTIFICATION_CHANNEL, + "TrackPlayer", + NotificationManager.IMPORTANCE_DEFAULT + ); + channel.setShowBadge(false); + channel.setSound(null, null); + context.getSystemService(NotificationManager.class).createNotificationChannel(channel); + } + + this.builder = new NotificationCompat.Builder(context, Utils.NOTIFICATION_CHANNEL); + this.session = new MediaSessionCompat(context, "TrackPlayer", null, null); + this.receiver = new ButtonReceiver(this); session.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS); - session.setCallback(new ButtonEvents(service, manager)); + session.setCallback(new ButtonEvents(manager)); - Context context = service.getApplicationContext(); - String packageName = context.getPackageName(); - Intent openApp = context.getPackageManager().getLaunchIntentForPackage(packageName); + Context appContext = context.getApplicationContext(); + String packageName = appContext.getPackageName(); + Intent openApp = appContext.getPackageManager().getLaunchIntentForPackage(packageName); if (openApp == null) { openApp = new Intent(); @@ -80,13 +93,13 @@ public MetadataManager(MusicService service, MusicManager manager) { openApp.setAction(Intent.ACTION_VIEW); openApp.setData(Uri.parse("trackplayer://notification.click")); - builder.setContentIntent(PendingIntent.getActivity(context, 0, openApp, PendingIntent.FLAG_CANCEL_CURRENT)); + builder.setContentIntent(PendingIntent.getActivity(appContext, 0, openApp, PendingIntent.FLAG_CANCEL_CURRENT)); builder.setSmallIcon(R.drawable.play); builder.setCategory(NotificationCompat.CATEGORY_TRANSPORT); // Stops the playback when the notification is swiped away - builder.setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(service, PlaybackStateCompat.ACTION_STOP)); + builder.setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP)); // Make it visible in the lockscreen builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); @@ -100,10 +113,10 @@ public MediaSessionCompat getSession() { * Updates the metadata options * @param options The options */ - public void updateOptions(Bundle options) { - List capabilities = options.getIntegerArrayList("capabilities"); - List notification = options.getIntegerArrayList("notificationCapabilities"); - List compact = options.getIntegerArrayList("compactCapabilities"); + public void updateOptions(ReadableMap options) { + List capabilities = Utils.getIntegerList(options, "capabilities", null); + List notification = Utils.getIntegerList(options, "notificationCapabilities", capabilities); + List compact = Utils.getIntegerList(options, "compactCapabilities", null); actions = 0; compactActions = 0; @@ -112,9 +125,6 @@ public void updateOptions(Bundle options) { // Create the actions mask for(int cap : capabilities) actions |= cap; - // If there is no notification capabilities defined, we'll show all capabilities available - if(notification == null) notification = capabilities; - // Initialize all actions based on the options previousAction = createAction(notification, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS, "Previous", @@ -164,8 +174,7 @@ public int getJumpInterval() { public void removeNotifications() { String ns = Context.NOTIFICATION_SERVICE; - Context context = service.getApplicationContext(); - NotificationManager manager = (NotificationManager) context.getSystemService(ns); + NotificationManager manager = (NotificationManager) context.getApplicationContext().getSystemService(ns); manager.cancelAll(); } @@ -193,7 +202,7 @@ protected void updateArtwork(Bitmap bitmap) { public void updateMetadata(TrackMetadata track) { MediaMetadataCompat.Builder metadata = track.toMediaMetadata(); - RequestManager rm = Glide.with(service.getApplicationContext()); + RequestManager rm = Glide.with(context.getApplicationContext()); if(artworkTarget != null) rm.clear(artworkTarget); if(track.artwork != null) { @@ -228,6 +237,8 @@ public void updatePlayback(ExoPlayback playback) { int state = playback.getState(); boolean playing = Utils.isPlaying(state); List compact = new ArrayList<>(); + + builder.setOngoing(playing); builder.mActions.clear(); // Adds the media buttons to the notification @@ -255,7 +266,7 @@ public void updatePlayback(ExoPlayback playback) { } else { // Shows the cancel button on pre-lollipop versions due to a bug style.setShowCancelButton(true); - style.setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(service, + style.setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP)); } @@ -288,32 +299,43 @@ public void updatePlayback(ExoPlayback playback) { public void setActive(boolean active) { this.session.setActive(active); + if (active) { + if (!receiverRegistered) { + context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_MEDIA_BUTTON)); + receiverRegistered = true; + } + } else { + if (receiverRegistered) { + context.unregisterReceiver(receiver); + receiverRegistered = false; + } + } + updateNotification(); } public void destroy() { - service.stopForeground(true); + NotificationManagerCompat.from(context).cancel(Utils.NOTIFICATION_ID); session.setActive(false); session.release(); } private void updateNotification() { + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + if(session.isActive()) { - service.startForeground(1, builder.build()); + notificationManager.notify(Utils.NOTIFICATION_ID, builder.build()); } else { - service.stopForeground(true); + notificationManager.cancel(Utils.NOTIFICATION_ID); } } - private int getIcon(Bundle options, String propertyName, int defaultIcon) { - if(!options.containsKey(propertyName)) return defaultIcon; - - Bundle bundle = options.getBundle(propertyName); - if(bundle == null) return defaultIcon; + private int getIcon(ReadableMap options, String key, int defaultIcon) { + if(!options.hasKey(key) || options.getType(key) != ReadableType.Map) return defaultIcon; ResourceDrawableIdHelper helper = ResourceDrawableIdHelper.getInstance(); - int icon = helper.getResourceDrawableId(service, bundle.getString("uri")); + int icon = helper.getResourceDrawableId(context, options.getMap(key).getString("uri")); if(icon == 0) return defaultIcon; return icon; @@ -322,7 +344,7 @@ private int getIcon(Bundle options, String propertyName, int defaultIcon) { private Action createAction(List caps, long action, String title, int icon) { if(!caps.contains((int)action)) return null; - return new Action(icon, title, MediaButtonReceiver.buildMediaButtonPendingIntent(service, action)); + return new Action(icon, title, MediaButtonReceiver.buildMediaButtonPendingIntent(context, action)); } private void addAction(Action action, long id, List compact) { diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/models/NowPlayingMetadata.java b/android/src/main/java/com/guichaguri/trackplayer/service/models/NowPlayingMetadata.java index 71139c3d0..d2a01b04f 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/service/models/NowPlayingMetadata.java +++ b/android/src/main/java/com/guichaguri/trackplayer/service/models/NowPlayingMetadata.java @@ -1,21 +1,22 @@ package com.guichaguri.trackplayer.service.models; import android.content.Context; -import android.os.Bundle; +import com.facebook.react.bridge.ReadableMap; +import com.guichaguri.trackplayer.service.Utils; public class NowPlayingMetadata extends TrackMetadata { public double elapsedTime; - public NowPlayingMetadata(Context context, Bundle bundle, int ratingType) { - setMetadata(context, bundle, ratingType); + public NowPlayingMetadata(Context context, ReadableMap data, int ratingType) { + setMetadata(context, data, ratingType); } @Override - public void setMetadata(Context context, Bundle bundle, int ratingType) { - super.setMetadata(context, bundle, ratingType); + public void setMetadata(Context context, ReadableMap data, int ratingType) { + super.setMetadata(context, data, ratingType); - elapsedTime = bundle.getDouble("elapsedTime", 0); + elapsedTime = Utils.getDouble(data, "elapsedTime", 0); } } diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/models/Track.java b/android/src/main/java/com/guichaguri/trackplayer/service/models/Track.java index 9f7b73e31..1a6f60b65 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/service/models/Track.java +++ b/android/src/main/java/com/guichaguri/trackplayer/service/models/Track.java @@ -2,11 +2,10 @@ import android.content.Context; import android.net.Uri; -import android.os.Bundle; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; -import android.support.v4.media.RatingCompat; import android.support.v4.media.session.MediaSessionCompat.QueueItem; +import com.facebook.react.bridge.*; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; @@ -26,22 +25,22 @@ import java.util.List; import java.util.Map; -import static android.support.v4.media.MediaMetadataCompat.*; +import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_ID; +import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_URI; /** * @author Guichaguri */ public class Track extends TrackMetadata { - public static List createTracks(Context context, List objects, int ratingType) { + public static List createTracks(Context context, ReadableArray objects, int ratingType) { List tracks = new ArrayList<>(); - for(Object o : objects) { - if(o instanceof Bundle) { - tracks.add(new Track(context, (Bundle)o, ratingType)); - } else { - return null; + for (int i = 0; i < objects.size(); i++) { + if (objects.getType(i) != ReadableType.Map) { + throw new IllegalArgumentException("Expected the track to be an object"); } + tracks.add(new Track(context, objects.getMap(i), ratingType)); } return tracks; @@ -56,24 +55,24 @@ public static List createTracks(Context context, List objects, int rating public String contentType; public String userAgent; - public Bundle originalItem; + public WritableMap originalItem; public Map headers; public final long queueId; - public Track(Context context, Bundle bundle, int ratingType) { - id = bundle.getString("id"); + public Track(Context context, ReadableMap map, int ratingType) { + id = map.getString("id"); - resourceId = Utils.getRawResourceId(context, bundle, "url"); + resourceId = Utils.getRawResourceId(context, map, "url"); if(resourceId == 0) { - uri = Utils.getUri(context, bundle, "url"); + uri = Utils.getUri(context, map, "url"); } else { uri = RawResourceDataSource.buildRawResourceUri(resourceId); } - String trackType = bundle.getString("type", "default"); + String trackType = Utils.getString(map, "type", "default"); for(TrackType t : TrackType.values()) { if(t.name.equalsIgnoreCase(trackType)) { @@ -82,29 +81,32 @@ public Track(Context context, Bundle bundle, int ratingType) { } } - contentType = bundle.getString("contentType"); - userAgent = bundle.getString("userAgent"); + contentType = map.getString("contentType"); + userAgent = map.getString("userAgent"); - Bundle httpHeaders = bundle.getBundle("headers"); - if(httpHeaders != null) { + if (map.hasKey("headers")) { + ReadableMap httpHeaders = map.getMap("headers"); + ReadableMapKeySetIterator iterator = httpHeaders.keySetIterator(); headers = new HashMap<>(); - for(String header : httpHeaders.keySet()) { - headers.put(header, httpHeaders.getString(header)); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + headers.put(key, httpHeaders.getString(key)); } } - setMetadata(context, bundle, ratingType); + setMetadata(context, map, ratingType); queueId = System.currentTimeMillis(); - originalItem = bundle; + originalItem = new JavaOnlyMap(); + originalItem.merge(map); } @Override - public void setMetadata(Context context, Bundle bundle, int ratingType) { - super.setMetadata(context, bundle, ratingType); + public void setMetadata(Context context, ReadableMap data, int ratingType) { + super.setMetadata(context, data, ratingType); - if (originalItem != null && originalItem != bundle) - originalItem.putAll(bundle); + if (originalItem != null && originalItem != data) + originalItem.merge(data); } @Override diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/models/TrackMetadata.java b/android/src/main/java/com/guichaguri/trackplayer/service/models/TrackMetadata.java index 55f08c2f2..86607ef4f 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/service/models/TrackMetadata.java +++ b/android/src/main/java/com/guichaguri/trackplayer/service/models/TrackMetadata.java @@ -2,13 +2,12 @@ import android.content.Context; import android.net.Uri; -import android.os.Bundle; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.RatingCompat; +import com.facebook.react.bridge.ReadableMap; import com.guichaguri.trackplayer.service.Utils; import static android.support.v4.media.MediaMetadataCompat.*; -import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_RATING; public abstract class TrackMetadata { public Uri artwork; @@ -22,17 +21,17 @@ public abstract class TrackMetadata { public RatingCompat rating; - public void setMetadata(Context context, Bundle bundle, int ratingType) { - artwork = Utils.getUri(context, bundle, "artwork"); + public void setMetadata(Context context, ReadableMap data, int ratingType) { + artwork = Utils.getUri(context, data, "artwork"); - title = bundle.getString("title"); - artist = bundle.getString("artist"); - album = bundle.getString("album"); - date = bundle.getString("date"); - genre = bundle.getString("genre"); - duration = Utils.toMillis(bundle.getDouble("duration", 0)); + title = data.getString("title"); + artist = data.getString("artist"); + album = data.getString("album"); + date = data.getString("date"); + genre = data.getString("genre"); + duration = Utils.toMillis(Utils.getDouble(data, "duration", 0)); - rating = Utils.getRating(bundle, "rating", ratingType); + rating = Utils.getRating(data, "rating", ratingType); } public MediaMetadataCompat.Builder toMediaMetadata() { From c94d536fa73e701145286eee5c8cd813d17cee4b Mon Sep 17 00:00:00 2001 From: Guilherme Chaguri Date: Wed, 22 Apr 2020 00:54:20 -0300 Subject: [PATCH 2/2] Removed unused method --- .../main/java/com/guichaguri/trackplayer/service/Utils.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java b/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java index 7d8097279..8fa79afb2 100644 --- a/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java +++ b/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java @@ -202,9 +202,5 @@ public static List getIntegerList(ReadableMap data, String key, List