diff --git a/plugin.xml b/plugin.xml
index de207dd..7c5f98e 100644
--- a/plugin.xml
+++ b/plugin.xml
@@ -6,7 +6,6 @@
-
RemoteControls
lockscreen,media,now,playing
https://github.com/shi11/RemoteControls.git
@@ -17,11 +16,8 @@
-
- Seth Hillinger, François LASSERRE, Michael GAUTHIER
-
+ Seth Hillinger, François LASSERRE, Michael GAUTHIER, Guilherme Chaguri
MIT License
-
@@ -34,5 +30,26 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/android/RemoteControls.java b/src/android/RemoteControls.java
new file mode 100644
index 0000000..a1effed
--- /dev/null
+++ b/src/android/RemoteControls.java
@@ -0,0 +1,277 @@
+package guichaguri.remotecontrols;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.PluginResult;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * RemoteControls for Android
+ * @author Guilherme Chaguri
+ */
+public class RemoteControls extends CordovaPlugin {
+
+ static RemoteControls instance;
+
+ MediaSessionCompat session;
+ PlaybackStateCompat.Builder pb;
+ MediaMetadataCompat.Builder md;
+
+ RemoteEventHandler eventHandler;
+ RemoteEventHandler.RemoteVolumeHandler volumeHandler;
+ RemoteNotification notification;
+
+ private void init(CordovaInterface cordova) {
+ instance = this;
+
+ Activity activity = cordova.getActivity();
+
+ ComponentName name = new ComponentName(activity, RemoteNotification.class);
+ session = new MediaSessionCompat(activity, "RemoteControls", name, null);
+
+ session.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
+ MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+
+ pb = new PlaybackStateCompat.Builder();
+ md = new MediaMetadataCompat.Builder();
+
+ eventHandler = new RemoteEventHandler(this);
+ volumeHandler = eventHandler.new RemoteVolumeHandler(true, 100);
+ notification = new RemoteNotification(this);
+
+ session.setCallback(eventHandler);
+ session.setPlaybackToRemote(volumeHandler);
+
+ activity.registerReceiver(notification, new IntentFilter(RemoteNotification.ACTION_BUTTON));
+
+ activity.startService(new Intent(activity.getBaseContext(), RemoteNotification.NotificationService.class));
+ }
+
+ private void destroy() {
+ notification.remove();
+ session.release();
+ cordova.getActivity().unregisterReceiver(notification);
+
+ if(instance == this) instance = null;
+ }
+
+ @Override
+ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+ init(cordova);
+ }
+
+ @Override
+ public void onDestroy() {
+ destroy();
+ }
+
+ @Override
+ public void onReset() {
+ destroy();
+ init(cordova);
+ }
+
+ @Override
+ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+ if(action.equals("updatePlayback")) {
+ JSONObject obj = args.length() > 0 ? args.getJSONObject(0) : null;
+ updatePlayback(obj, callbackContext);
+ return true;
+ } else if(action.equals("updateMetadata")) {
+ JSONObject obj = args.length() > 0 ? args.getJSONObject(0) : null;
+ updateMetadata(obj, callbackContext);
+ return true;
+ } else if(action.equals("updateActions")) {
+ JSONObject obj = args.length() > 0 ? args.getJSONObject(0) : null;
+ updateActions(obj, callbackContext);
+ return true;
+ } else if(action.equals("handleEvents")) {
+ eventHandler.eventCallback = callbackContext;
+ PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT);
+ pluginResult.setKeepCallback(true);
+ callbackContext.sendPluginResult(pluginResult);
+ return true;
+ }
+ return false;
+ }
+
+ private void updateActions(JSONObject obj, CallbackContext callbackContext) throws JSONException {
+ if(obj != null) {
+ long actions = 0;
+
+ boolean stop = obj.has("stop") && obj.getBoolean("stop");
+ boolean pause = obj.has("pause") && obj.getBoolean("pause");
+ boolean play = obj.has("play") && obj.getBoolean("play");
+ boolean previous = obj.has("skipToPrevious") && obj.getBoolean("skipToPrevious");
+ boolean next = obj.has("skipToNext") && obj.getBoolean("skipToNext");
+
+ notification.stopButton = stop;
+ notification.pauseButton = pause;
+ notification.playButton = play;
+ notification.previousButton = previous;
+ notification.nextButton = next;
+
+ if(stop)
+ actions |= PlaybackStateCompat.ACTION_STOP;
+ if(pause)
+ actions |= PlaybackStateCompat.ACTION_PAUSE;
+ if(play)
+ actions |= PlaybackStateCompat.ACTION_PLAY;
+ if(previous)
+ actions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+ if(next)
+ actions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
+ if(obj.has("rate") && obj.getBoolean("rate"))
+ actions |= PlaybackStateCompat.ACTION_SET_RATING;
+ if(obj.has("seekTo") && obj.getBoolean("seekTo"))
+ actions |= PlaybackStateCompat.ACTION_SEEK_TO;
+
+ boolean volumeChangeable = obj.has("volume") && obj.getBoolean("volume");
+ if(volumeChangeable != volumeHandler.isChangeable()) {
+ volumeHandler = eventHandler.new RemoteVolumeHandler(volumeChangeable, volumeHandler.getCurrentVolume());
+ session.setPlaybackToRemote(volumeHandler);
+ }
+
+ pb.setActions(actions);
+ session.setPlaybackState(pb.build());
+ notification.update();
+ callbackContext.success("Actions updated");
+ } else {
+ callbackContext.error("Missing properties");
+ }
+ }
+
+ private void updatePlayback(JSONObject obj, CallbackContext callbackContext) throws JSONException {
+ if(obj != null) {
+ int state = PlaybackStateCompat.STATE_NONE;
+ notification.isPlaying = false;
+ switch(obj.getInt("state")) {
+ case -1:
+ state = PlaybackStateCompat.STATE_ERROR;
+ break;
+ case 0:
+ state = PlaybackStateCompat.STATE_STOPPED;
+ break;
+ case 1:
+ state = PlaybackStateCompat.STATE_PLAYING;
+ notification.isPlaying = true;
+ break;
+ case 2:
+ state = PlaybackStateCompat.STATE_PAUSED;
+ break;
+ case 3:
+ state = PlaybackStateCompat.STATE_BUFFERING;
+ notification.isPlaying = true;
+ break;
+ }
+
+ float speed = obj.has("speed") ? (float)obj.getDouble("speed") : 1;
+ int elapsedTime = obj.has("elapsedTime") ? obj.getInt("elapsedTime") : 0;
+ pb.setState(state, elapsedTime, speed);
+
+ if(obj.has("bufferedTime"))
+ pb.setBufferedPosition(obj.getInt("bufferedTime"));
+ if(obj.has("volume"))
+ volumeHandler.setCurrentVolume(obj.getInt("volume"));
+
+ session.setPlaybackState(pb.build());
+ notification.update();
+ callbackContext.success("Playback updated");
+ } else {
+ callbackContext.error("Missing properties");
+ }
+ }
+
+ private void updateMetadata(final JSONObject obj, final CallbackContext callbackContext) {
+ if(obj != null) {
+ cordova.getThreadPool().execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ String artist = obj.getString("artist");
+ String title = obj.getString("title");
+
+ md.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist);
+ md.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
+ notification.artist = artist;
+ notification.title = title;
+ notification.album = null;
+ notification.cover = null;
+ notification.color = null;
+
+ if(obj.has("album")) {
+ String album = obj.getString("album");
+ md.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album);
+ notification.album = album;
+ }
+ if(obj.has("genre"))
+ md.putString(MediaMetadataCompat.METADATA_KEY_GENRE, obj.getString("genre"));
+ if(obj.has("rating"))
+ md.putRating(MediaMetadataCompat.METADATA_KEY_RATING, RatingCompat.newPercentageRating(obj.getInt("rating")));
+ if(obj.has("description"))
+ md.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, obj.getString("description"));
+ if(obj.has("date"))
+ md.putString(MediaMetadataCompat.METADATA_KEY_DATE, obj.getString("date"));
+ if(obj.has("color"))
+ notification.color = obj.getInt("color");
+ if(obj.has("duration"))
+ md.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, obj.getLong("duration"));
+ if(obj.has("cover"))
+ loadCover(obj.getString("cover"));
+
+ session.setMetadata(md.build());
+ session.setActive(true);
+ notification.update();
+ callbackContext.success("Metadata updated");
+ } catch(JSONException e) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
+ } catch(IOException e) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION));
+ }
+ }
+ });
+ } else {
+ session.setActive(false);
+ notification.remove();
+ callbackContext.success("Metadata removed");
+ }
+ }
+
+ private void loadCover(final String coverUri) throws IOException {
+ Bitmap cover;
+ InputStream input;
+
+ if(coverUri.matches("https?:\\/\\/.*")) { // HTTP URL
+ URLConnection connection = new URL(coverUri).openConnection();
+ connection.setDoInput(true);
+ connection.connect();
+ input = connection.getInputStream();
+ cover = BitmapFactory.decodeStream(input);
+ } else { // ASSET FILE
+ input = cordova.getActivity().getAssets().open("www/" + coverUri);
+ cover = BitmapFactory.decodeStream(input);
+ }
+
+ md.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, cover);
+ notification.cover = cover;
+ input.close();
+ }
+}
diff --git a/src/android/RemoteEventHandler.java b/src/android/RemoteEventHandler.java
new file mode 100644
index 0000000..9376b8c
--- /dev/null
+++ b/src/android/RemoteEventHandler.java
@@ -0,0 +1,86 @@
+package guichaguri.remotecontrols;
+
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.PluginResult;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * The event handler
+ * @author Guilherme Chaguri
+ */
+public class RemoteEventHandler extends MediaSessionCompat.Callback {
+ private final RemoteControls rc;
+ CallbackContext eventCallback;
+
+ RemoteEventHandler(RemoteControls rc) {
+ this.rc = rc;
+ }
+
+ private void sendEvent(String action, String key, Object value) {
+ if(eventCallback == null) return;
+ try {
+ JSONObject obj = new JSONObject();
+
+ obj.put("action", action);
+ if(key != null) obj.put(key, value);
+
+ PluginResult result = new PluginResult(PluginResult.Status.OK, obj);
+ result.setKeepCallback(true);
+ eventCallback.sendPluginResult(result);
+ } catch(JSONException ex) {
+ ex.printStackTrace();
+ }
+ }
+ private void sendEvent(String action) {
+ sendEvent(action, null, null);
+ }
+
+ @Override
+ public void onPlay() {
+ sendEvent("play");
+ }
+ @Override
+ public void onPause() {
+ sendEvent("pause");
+ }
+ @Override
+ public void onStop() {
+ sendEvent("stop");
+ }
+ @Override
+ public void onSeekTo(long pos) {
+ sendEvent("seek", "position", pos);
+ }
+ @Override
+ public void onSkipToPrevious() {
+ sendEvent("previous");
+ }
+ @Override
+ public void onSkipToNext() {
+ sendEvent("next");
+ }
+ @Override
+ public void onSetRating(RatingCompat rating) {
+ sendEvent("rate", "rating", rating.getPercentRating());
+ }
+
+ class RemoteVolumeHandler extends VolumeProviderCompat {
+ RemoteVolumeHandler(boolean changeable, int currentVolume) {
+ super(changeable ? VOLUME_CONTROL_ABSOLUTE : VOLUME_CONTROL_FIXED, 100, currentVolume);
+ }
+
+ boolean isChangeable() {
+ return VOLUME_CONTROL_FIXED != getVolumeControl();
+ }
+
+ @Override
+ public void onSetVolumeTo(int volume) {
+ sendEvent("volume", "volume", volume);
+ }
+ }
+
+}
diff --git a/src/android/RemoteNotification.java b/src/android/RemoteNotification.java
new file mode 100644
index 0000000..8fa08e1
--- /dev/null
+++ b/src/android/RemoteNotification.java
@@ -0,0 +1,158 @@
+package guichaguri.remotecontrols;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.os.IBinder;
+import android.support.v4.app.NotificationManagerCompat;
+import android.support.v4.media.session.MediaButtonReceiver;
+import android.support.v7.app.NotificationCompat;
+
+/**
+ * The Notification Updater
+ * @author Guilherme Chaguri
+ */
+public class RemoteNotification extends BroadcastReceiver {
+ static final String ACTION_BUTTON = "remote-controls-button";
+
+ private static final int BUTTON_PLAY = 0;
+ private static final int BUTTON_PAUSE = 1;
+ private static final int BUTTON_STOP = 2;
+ private static final int BUTTON_NEXT = 3;
+ private static final int BUTTON_PREVIOUS = 4;
+ private static final int BUTTON_REMOVE = 5;
+
+ private final RemoteControls rc;
+
+ String title, album, artist;
+ Bitmap cover;
+ Integer color = null;
+ boolean playButton, pauseButton, stopButton, nextButton, previousButton;
+ boolean isPlaying;
+ boolean showWhenPaused = true;
+
+ RemoteNotification(RemoteControls rc) {
+ this.rc = rc;
+ }
+
+ void update() {
+ if(!rc.session.isActive() || (!isPlaying && !showWhenPaused)) {
+ remove();
+ return;
+ }
+ Activity activity = rc.cordova.getActivity();
+
+ NotificationCompat.Builder nc = new NotificationCompat.Builder(activity);
+ nc.setStyle(new NotificationCompat.MediaStyle().setMediaSession(rc.session.getSessionToken()));
+ nc.setLargeIcon(cover);
+ nc.setContentTitle(title);
+ nc.setContentText(artist);
+ nc.setContentInfo(album);
+ nc.setOngoing(isPlaying);
+ nc.setColor(color == null ? NotificationCompat.COLOR_DEFAULT : color);
+
+ Resources r = activity.getResources();
+ String packageName = activity.getPackageName();
+ int previous = r.getIdentifier("previous", "drawable", packageName);
+ int pause = r.getIdentifier("pause", "drawable", packageName);
+ int play = r.getIdentifier("play", "drawable", packageName);
+ int stop = r.getIdentifier("stop", "drawable", packageName);
+ int next = r.getIdentifier("next", "drawable", packageName);
+
+ nc.setSmallIcon(play);
+
+ if(previousButton)
+ nc.addAction(createAction(activity, previous, "Previous", BUTTON_PREVIOUS));
+ if(pauseButton && isPlaying)
+ nc.addAction(createAction(activity, pause, "Pause", BUTTON_PAUSE));
+ if(playButton && !isPlaying)
+ nc.addAction(createAction(activity, play, "Play", BUTTON_PLAY));
+ if(stopButton)
+ nc.addAction(createAction(activity, stop, "Stop", BUTTON_STOP));
+ if(nextButton)
+ nc.addAction(createAction(activity, next, "Next", BUTTON_NEXT));
+
+ showWhenPaused = true;
+
+ // Open the app when the notification is clicked
+ Intent openApp = new Intent(activity, activity.getClass());
+ openApp.setAction(Intent.ACTION_MAIN);
+ openApp.addCategory(Intent.CATEGORY_LAUNCHER);
+ nc.setContentIntent(PendingIntent.getActivity(activity, 0, openApp, 0));
+
+ if(!isPlaying) {
+ // Remove notification
+ Intent remove = new Intent(ACTION_BUTTON).putExtra("button", BUTTON_REMOVE);
+ nc.setDeleteIntent(PendingIntent.getBroadcast(activity, BUTTON_REMOVE, remove, 0));
+ }
+
+ NotificationManagerCompat.from(activity).notify(0, nc.build());
+ }
+
+ void remove() {
+ NotificationManagerCompat.from(rc.cordova.getActivity()).cancel(0);
+ }
+
+ private NotificationCompat.Action createAction(Activity activity, int icon, String title, int buttonId) {
+ Intent intent = new Intent(ACTION_BUTTON).putExtra("button-action", buttonId);
+ PendingIntent i = PendingIntent.getBroadcast(activity, buttonId, intent, 0);
+ return new NotificationCompat.Action(icon, title, i);
+ }
+
+ @Override
+ public void onReceive(Context context, final Intent intent) {
+ String action = intent.getAction();
+ boolean isButton = action.equals(ACTION_BUTTON) && intent.hasExtra("button-action");
+ boolean isMedia = action.equals(Intent.ACTION_MEDIA_BUTTON) && intent.hasExtra(Intent.EXTRA_KEY_EVENT);
+ if(!isButton && !isMedia) return;
+
+ final int button = isButton ? intent.getIntExtra("button-action", -1) : -1;
+
+ rc.cordova.getThreadPool().execute(new Runnable() {
+ @Override
+ public void run() {
+ if(button == -1) {
+ MediaButtonReceiver.handleIntent(rc.session, intent);
+ } else if(button == BUTTON_PLAY) {
+ rc.eventHandler.onPlay();
+ } else if(button == BUTTON_PAUSE) {
+ rc.eventHandler.onPause();
+ } else if(button == BUTTON_STOP) {
+ rc.eventHandler.onStop();
+ } else if(button == BUTTON_NEXT) {
+ rc.eventHandler.onSkipToNext();
+ } else if(button == BUTTON_PREVIOUS) {
+ rc.eventHandler.onSkipToPrevious();
+ } else if(button == BUTTON_REMOVE) {
+ showWhenPaused = false;
+ }
+ }
+ });
+ }
+
+ public static class NotificationService extends Service {
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onTaskRemoved(Intent rootIntent) {
+ if(RemoteControls.instance != null && RemoteControls.instance.notification != null) {
+ RemoteControls.instance.notification.remove();
+ }
+ stopSelf();
+ }
+
+ }
+}
diff --git a/src/android/res/next.png b/src/android/res/next.png
new file mode 100644
index 0000000..3c475d0
Binary files /dev/null and b/src/android/res/next.png differ
diff --git a/src/android/res/pause.png b/src/android/res/pause.png
new file mode 100644
index 0000000..d631813
Binary files /dev/null and b/src/android/res/pause.png differ
diff --git a/src/android/res/play.png b/src/android/res/play.png
new file mode 100644
index 0000000..3cf83d7
Binary files /dev/null and b/src/android/res/play.png differ
diff --git a/src/android/res/previous.png b/src/android/res/previous.png
new file mode 100644
index 0000000..9e841cb
Binary files /dev/null and b/src/android/res/previous.png differ
diff --git a/src/android/res/stop.png b/src/android/res/stop.png
new file mode 100644
index 0000000..3bacfbc
Binary files /dev/null and b/src/android/res/stop.png differ
diff --git a/www/RemoteControls.js b/www/RemoteControls.js
index 0ed7ba0..76b9bf1 100644
--- a/www/RemoteControls.js
+++ b/www/RemoteControls.js
@@ -10,20 +10,88 @@
// MIT Licensed
//
-
//------------------------------------------------------------------------------
-// object that we're exporting
+// Cordova Objects
//------------------------------------------------------------------------------
+var cordova = require('cordova');
+var exec = require('cordova/exec');
var remoteControls = module.exports;
+//------------------------------------------------------------------------------
+// Update Functions
+//------------------------------------------------------------------------------
+
//params = [artist, title, album, cover, duration]
+// DEPRECATED! Use updateMetadata instead
remoteControls.updateMetas = function(success, fail, params) {
- cordova.exec(success, fail, 'RemoteControls', 'updateMetas', params);
+ remoteControls.updateMetadata({
+ artist: params[0],
+ title: params[1],
+ album: params[2],
+ cover: params[3],
+ duration: params[4]
+ }, success, fail);
};
+remoteControls.updateMetadata = function(data, success, fail) {
+ exec(success, fail, 'RemoteControls', 'updateMetadata', [data]);
+};
+
+remoteControls.updatePlayback = function(data, success, fail) {
+ exec(success, fail, 'RemoteControls', 'updatePlayback', [data]);
+};
+
+remoteControls.updateActions = function(data, success, fail) {
+ exec(success, fail, 'RemoteControls', 'updateActions', [data]);
+};
+
+//------------------------------------------------------------------------------
+// Event Handling
+//------------------------------------------------------------------------------
+
+// DEPRECATED (remove it as soon as possible)
remoteControls.receiveRemoteEvent = function(event) {
var ev = document.createEvent('HTMLEvents');
ev.remoteEvent = event;
ev.initEvent('remote-event', true, true, arguments);
document.dispatchEvent(ev);
-}
\ No newline at end of file
+
+ var action = event.subtype;
+ if(action == 'prevTrack') {
+ action = 'previous';
+ } else if(action == 'nextTrack') {
+ action = 'next';
+ }
+ cordova.fireWindowEvent('remotecontrols', {action: action});
+};
+
+remoteControls._eventHandler = cordova.addWindowEventHandler('remotecontrols');
+remoteControls._eventHandlerRegistered = false;
+
+remoteControls._fireOldEvent = function(obj) {
+ var subtype = obj.action;
+ if(subtype == 'previous') {
+ subtype = 'prevTrack';
+ } else if(subtype == 'next') {
+ subtype = 'nextTrack';
+ }
+
+ var ev = document.createEvent('HTMLEvents');
+ ev.remoteEvent = {subtype: subtype};
+ ev.initEvent('remote-event', true, true);
+ document.dispatchEvent(ev);
+};
+
+remoteControls._fireEvent = function(obj) {
+ // Fire the event
+ cordova.fireWindowEvent('remotecontrols', obj);
+
+ remoteControls._fireOldEvent(obj);
+};
+
+remoteControls._eventHandler.onHasSubscribersChange = function() {
+ if(remoteControls._eventHandler.numHandlers > 0 && !remoteControls._eventHandlerRegistered) {
+ exec(remoteControls._fireEvent, null, 'RemoteControls', 'handleEvents', []);
+ remoteControls._eventHandlerRegistered = true;
+ }
+};