From 389a143bdbcb7064c50af8e09459f3bf3d8a95a9 Mon Sep 17 00:00:00 2001 From: Steffen Liebergeld Date: Tue, 7 Jun 2016 19:34:37 +0200 Subject: [PATCH 01/10] Set music info for PocketCasts PocketCasts tells about its current media state via notifications. This patch tries to parse incoming notifications from PocketCasts and if successful tells the device about it. Currently supported are track and artist. --- .../externalevents/NotificationListener.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index a3c3d37c..c12b20c6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; @@ -182,6 +183,11 @@ public class NotificationListener extends NotificationListenerService { String source = sbn.getPackageName(); Notification notification = sbn.getNotification(); + if (source.equals("au.com.shiftyjelly.pocketcasts")) { + if (handlePocketCastNotification(notification)) + return; + } + if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) { return; } @@ -311,6 +317,36 @@ public class NotificationListener extends NotificationListenerService { return false; } + /** + * Try to handle pocket cast notifications that tell info about the current play state. + * @param notification The notification to handle. + * @return true if notification was handled, false otherwise + */ + public boolean handlePocketCastNotification(Notification notification) { + MusicSpec musicSpec = new MusicSpec(); + + Bundle extras = notification.extras; + if (extras == null) + return false; + + CharSequence title = extras.getCharSequence(Notification.EXTRA_TITLE); + if (title != null) + musicSpec.artist = title.toString(); + if (extras.containsKey(Notification.EXTRA_TEXT)) { + CharSequence contentCS = extras.getCharSequence(Notification.EXTRA_TEXT); + if (contentCS != null) + musicSpec.track = contentCS.toString(); + } + + musicSpec.album = "unknown"; + + LOG.info("handlePocketCastsNotification: artist " + musicSpec.artist + ", track " + musicSpec.track); + + // finally, tell the device about it + GBApplication.deviceService().onSetMusicInfo(musicSpec); + return true; + } + @Override public void onNotificationRemoved(StatusBarNotification sbn) { From 0470731e4bbd2737e0f5a3dfd1562ea4efa4e199 Mon Sep 17 00:00:00 2001 From: Steffen Liebergeld Date: Wed, 8 Jun 2016 20:16:28 +0200 Subject: [PATCH 02/10] PebbleProtocol: Do not call encodeSetMusicState in encodeSetMusicInfo encodeSetMusicState will be accessible on its own. If it was used to set the music state, a call to encodeSetMusicInfo must not reset this info arbitrarily. --- .../gadgetbridge/service/devices/pebble/PebbleProtocol.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index 6938c45c..33397145 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -1126,7 +1126,6 @@ public class PebbleProtocol extends GBDeviceProtocol { if (duration == 0) { return encodeMessage(ENDPOINT_MUSICCONTROL, MUSICCONTROL_SETMUSICINFO, 0, parts); } else { - byte[] stateMessage = encodeSetMusicState(MUSICCONTROL_STATE_PLAYING, 0, 100, (byte) 1, (byte) 1); // Calculate length first int length = LENGTH_PREFIX + 9; if (parts != null) { @@ -1140,7 +1139,7 @@ public class PebbleProtocol extends GBDeviceProtocol { } // Encode Prefix - ByteBuffer buf = ByteBuffer.allocate(length + stateMessage.length); + ByteBuffer buf = ByteBuffer.allocate(length); buf.order(ByteOrder.BIG_ENDIAN); buf.putShort((short) (length - LENGTH_PREFIX)); buf.putShort(ENDPOINT_MUSICCONTROL); @@ -1164,8 +1163,6 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.putShort((short) (trackCount & 0xffff)); buf.putShort((short) (trackNr & 0xffff)); - buf.put(stateMessage); - return buf.array(); } } From 1d5c8bae9d7a0070185fbcb1026a04d3350a9dc7 Mon Sep 17 00:00:00 2001 From: Steffen Liebergeld Date: Wed, 8 Jun 2016 20:22:05 +0200 Subject: [PATCH 03/10] MusicStateSpec: introduce new class describing the music state Contains: - state - position - playRate - shuffle - repeat This is close to what PebbleProtocol currently supports. --- .../gadgetbridge/model/MusicStateSpec.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java new file mode 100644 index 00000000..59ec114f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java @@ -0,0 +1,12 @@ +package nodomain.freeyourgadget.gadgetbridge.model; + +/** + * Created by steffen on 07.06.16. + */ +public class MusicStateSpec { + public byte state; + public int position; + public int playRate; + public byte shuffle; + public byte repeat; +} From e386d6da439c38340818ef11922349f9fbb1396d Mon Sep 17 00:00:00 2001 From: Steffen Liebergeld Date: Wed, 8 Jun 2016 20:27:25 +0200 Subject: [PATCH 04/10] Add onSetMusicState(MusicStateSpec stateSpec) This commit contains the infrastructure needed for the NotificationHandler to send music state information to the device. That is, it introduces a call onSetMusicState(MusicStateSpec stateSpec), that in turn sets up an intent to the service, which will then call the encodeSetMusicState() function of the device. encodeSetMusicState is available for pebble only. There are empty stubs for other devices. --- .../gadgetbridge/devices/EventHandler.java | 3 +++ .../gadgetbridge/impl/GBDeviceService.java | 12 ++++++++++++ .../gadgetbridge/model/DeviceService.java | 6 ++++++ .../service/DeviceCommunicationService.java | 16 ++++++++++++++++ .../service/ServiceDeviceSupport.java | 9 +++++++++ .../service/devices/miband/MiBandSupport.java | 6 ++++++ .../service/devices/pebble/PebbleSupport.java | 8 ++++++++ .../serial/AbstractSerialDeviceSupport.java | 7 +++++++ .../service/serial/GBDeviceProtocol.java | 4 ++++ .../gadgetbridge/service/TestDeviceSupport.java | 6 ++++++ 10 files changed, 77 insertions(+) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java index 602ca5c8..a275ace5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java @@ -9,6 +9,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; /** @@ -25,6 +26,8 @@ public interface EventHandler { void onSetCallState(CallSpec callSpec); + void onSetMusicState(MusicStateSpec stateSpec); + void onSetMusicInfo(MusicSpec musicSpec); void onEnableRealtimeSteps(boolean enable); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java index 2e598eac..60ff7b52 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -14,6 +14,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; @@ -125,6 +126,17 @@ public class GBDeviceService implements DeviceService { invokeService(intent); } + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + Intent intent = createIntent().setAction(ACTION_SETMUSICSTATE) + .putExtra(EXTRA_MUSIC_REPEAT, stateSpec.repeat) + .putExtra(EXTRA_MUSIC_RATE, stateSpec.playRate) + .putExtra(EXTRA_MUSIC_STATE, stateSpec.state) + .putExtra(EXTRA_MUSIC_SHUFFLE, stateSpec.shuffle) + .putExtra(EXTRA_MUSIC_POSITION, stateSpec.position); + invokeService(intent); + } + @Override public void onSetMusicInfo(MusicSpec musicSpec) { Intent intent = createIntent().setAction(ACTION_SETMUSICINFO) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index aaf02798..b3921b1b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -18,6 +18,7 @@ public interface DeviceService extends EventHandler { String ACTION_CALLSTATE = PREFIX + ".action.callstate"; String ACTION_SETTIME = PREFIX + ".action.settime"; String ACTION_SETMUSICINFO = PREFIX + ".action.setmusicinfo"; + String ACTION_SETMUSICSTATE = PREFIX + ".action.setmusicstate"; String ACTION_REQUEST_DEVICEINFO = PREFIX + ".action.request_deviceinfo"; String ACTION_REQUEST_APPINFO = PREFIX + ".action.request_appinfo"; String ACTION_REQUEST_SCREENSHOT = PREFIX + ".action.request_screenshot"; @@ -57,6 +58,11 @@ public interface DeviceService extends EventHandler { String EXTRA_MUSIC_DURATION = "music_duration"; String EXTRA_MUSIC_TRACKNR = "music_tracknr"; String EXTRA_MUSIC_TRACKCOUNT = "music_trackcount"; + String EXTRA_MUSIC_STATE = "music_state"; + String EXTRA_MUSIC_SHUFFLE = "music_shuffle"; + String EXTRA_MUSIC_REPEAT = "music_repeat"; + String EXTRA_MUSIC_POSITION = "music_position"; + String EXTRA_MUSIC_RATE = "music_rate"; String EXTRA_APP_UUID = "app_uuid"; String EXTRA_APP_START = "app_start"; String EXTRA_APP_CONFIG = "app_config"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index fdafa526..7903ff22 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; @@ -65,6 +66,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_RE import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_DEVICEINFO; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_SCREENSHOT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICINFO; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICSTATE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETTIME; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_ALARMS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START; @@ -87,6 +89,11 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIN import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_POSITION; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_RATE; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_REPEAT; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_SHUFFLE; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_STATE; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACK; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACKCOUNT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACKNR; @@ -351,6 +358,15 @@ public class DeviceCommunicationService extends Service implements SharedPrefere musicSpec.trackNr = intent.getIntExtra(EXTRA_MUSIC_TRACKNR, 0); mDeviceSupport.onSetMusicInfo(musicSpec); break; + case ACTION_SETMUSICSTATE: + MusicStateSpec stateSpec = new MusicStateSpec(); + stateSpec.shuffle = intent.getByteExtra(EXTRA_MUSIC_SHUFFLE, (byte)0); + stateSpec.repeat = intent.getByteExtra(EXTRA_MUSIC_REPEAT, (byte)0); + stateSpec.position = intent.getIntExtra(EXTRA_MUSIC_POSITION, 0); + stateSpec.playRate = intent.getIntExtra(EXTRA_MUSIC_RATE, 0); + stateSpec.state = intent.getByteExtra(EXTRA_MUSIC_STATE, (byte)0); + mDeviceSupport.onSetMusicState(stateSpec); + break; case ACTION_REQUEST_APPINFO: mDeviceSupport.onAppInfoReq(); break; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index 019722b5..e284f83d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -16,6 +16,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; /** @@ -150,6 +151,14 @@ public class ServiceDeviceSupport implements DeviceSupport { delegate.onSetCallState(callSpec); } + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + if (checkBusy("set music state")) { + return; + } + delegate.onSetMusicState(stateSpec); + } + @Override public void onSetMusicInfo(MusicSpec musicSpec) { if (checkBusy("set music info")) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java index fd00d02a..165e72e5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java @@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; @@ -599,6 +600,11 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { return telephoneRinging; } + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + // not supported + } + @Override public void onSetMusicInfo(MusicSpec musicSpec) { // not supported diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java index b0bc3b47..7233a2be 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java @@ -15,6 +15,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; @@ -108,6 +109,13 @@ public class PebbleSupport extends AbstractSerialDeviceSupport { } } + @Override + public void onSetMusicState(MusicStateSpec musicStateSpec) { + if (reconnect()) { + super.onSetMusicState(musicStateSpec); + } + } + @Override public void onSetMusicInfo(MusicSpec musicSpec) { if (reconnect()) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java index 78e15667..e963155e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java @@ -11,6 +11,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; @@ -123,6 +124,12 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport sendToDevice(bytes); } + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + byte[] bytes = gbDeviceProtocol.encodeSetMusicState(stateSpec.state, stateSpec.position, stateSpec.playRate, stateSpec.shuffle, stateSpec.repeat); + sendToDevice(bytes); + } + @Override public void onSetMusicInfo(MusicSpec musicSpec) { byte[] bytes = gbDeviceProtocol.encodeSetMusicInfo(musicSpec.artist, musicSpec.album, musicSpec.track, musicSpec.duration, musicSpec.trackCount, musicSpec.trackNr); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java index a226c094..2492f277 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java @@ -24,6 +24,10 @@ public abstract class GBDeviceProtocol { return null; } + public byte[] encodeSetMusicState(byte state, int position, int playRate, byte shuffle, byte repeat) { + return null; + } + public byte[] encodeFirmwareVersionReq() { return null; } diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java index 8070cb1e..42b4dd5c 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java @@ -12,6 +12,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; public class TestDeviceSupport extends AbstractDeviceSupport { @@ -66,6 +67,11 @@ public class TestDeviceSupport extends AbstractDeviceSupport { } + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + + } + @Override public void onSetMusicInfo(MusicSpec musicSpec) { From 73fbaf0a54f7ea817e406c7cc6e4ff2c0b1fabbe Mon Sep 17 00:00:00 2001 From: Steffen Liebergeld Date: Wed, 8 Jun 2016 20:32:32 +0200 Subject: [PATCH 05/10] Restore previous working of the debug activity The previous commits broke the debug activity's setting of the music info. This commit restores this functionality. --- .../gadgetbridge/activities/DebugActivity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java index c832148f..3922505e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java @@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; @@ -222,6 +223,15 @@ public class DebugActivity extends GBActivity { musicSpec.trackNr = 2; GBApplication.deviceService().onSetMusicInfo(musicSpec); + + MusicStateSpec stateSpec = new MusicStateSpec(); + stateSpec.position = 0; + stateSpec.state = 0x01; // playing + stateSpec.playRate = 100; + stateSpec.repeat = 1; + stateSpec.shuffle = 1; + + GBApplication.deviceService().onSetMusicState(stateSpec); } }); From fb71cdf55b2ab539b9c13ca7595565279f98ff25 Mon Sep 17 00:00:00 2001 From: Steffen Liebergeld Date: Wed, 8 Jun 2016 20:33:20 +0200 Subject: [PATCH 06/10] Add handling for media session notifications Since Android 5.0, media players can have interactive notifications that reside in the notification area, and offer up to 5 control buttons (play/pause, next, previous, etc), and information about the currentlu playing media file. We use these notifications to get information about the currently playing media file such as: - artist - track (title) - album - duration (length of the media file) - play state (playing, paused, stopped) - position - play rate (how fast is the media file being played) We then send this information up to the device. On Pebble, the music app will display the title and the artist, as well as a progress bar showing the current position. The progress bar is animated when the media file is being played, and if it is being paused, it displays a pause symbol. This code will be skipped when GadgetBridge is run on a device with Android version older than 5.0 (lollipop). --- .../externalevents/NotificationListener.java | 72 +++++++++++++++---- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index c12b20c6..a6abd06f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -11,8 +11,14 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Build; import android.os.Bundle; import android.os.PowerManager; +import android.provider.MediaStore; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.support.v4.app.NotificationCompat; @@ -26,6 +32,7 @@ import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; @@ -183,10 +190,8 @@ public class NotificationListener extends NotificationListenerService { String source = sbn.getPackageName(); Notification notification = sbn.getNotification(); - if (source.equals("au.com.shiftyjelly.pocketcasts")) { - if (handlePocketCastNotification(notification)) - return; - } + if (handleMediaSessionNotification(notification)) + return; if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) { return; @@ -318,32 +323,69 @@ public class NotificationListener extends NotificationListenerService { } /** - * Try to handle pocket cast notifications that tell info about the current play state. + * Try to handle media session notifications that tell info about the current play state. + * * @param notification The notification to handle. * @return true if notification was handled, false otherwise */ - public boolean handlePocketCastNotification(Notification notification) { + public boolean handleMediaSessionNotification(Notification notification) { + + // this code requires Android 5.0 or newer + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return false; + } + MusicSpec musicSpec = new MusicSpec(); + MusicStateSpec stateSpec = new MusicStateSpec(); Bundle extras = notification.extras; if (extras == null) return false; - CharSequence title = extras.getCharSequence(Notification.EXTRA_TITLE); - if (title != null) - musicSpec.artist = title.toString(); - if (extras.containsKey(Notification.EXTRA_TEXT)) { - CharSequence contentCS = extras.getCharSequence(Notification.EXTRA_TEXT); - if (contentCS != null) - musicSpec.track = contentCS.toString(); + if (extras.get(Notification.EXTRA_MEDIA_SESSION) == null) + return false; + + MediaController c; + try { + c = new MediaController(getApplicationContext(), (MediaSession.Token) extras.get(Notification.EXTRA_MEDIA_SESSION)); + } catch (NullPointerException e) { + return false; } - musicSpec.album = "unknown"; + PlaybackState s = c.getPlaybackState(); + stateSpec.position = (int)s.getPosition(); + stateSpec.playRate = Math.round(100 * s.getPlaybackSpeed()); + stateSpec.repeat = 1; + stateSpec.shuffle = 1; + switch (s.getState()) { + case PlaybackState.STATE_PLAYING: + stateSpec.state = 0x01; + break; + case PlaybackState.STATE_STOPPED: + case PlaybackState.STATE_PAUSED: + stateSpec.state = 0x00; + break; + default: + stateSpec.state = 0x04; + break; + } - LOG.info("handlePocketCastsNotification: artist " + musicSpec.artist + ", track " + musicSpec.track); + MediaMetadata d = c.getMetadata(); + if (d == null) + return false; + if (d.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) + musicSpec.artist = d.getString(MediaMetadata.METADATA_KEY_ARTIST); + if (d.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) + musicSpec.album = d.getString(MediaMetadata.METADATA_KEY_ALBUM); + if (d.containsKey(MediaMetadata.METADATA_KEY_TITLE)) + musicSpec.track = d.getString(MediaMetadata.METADATA_KEY_TITLE); + if (d.containsKey(MediaMetadata.METADATA_KEY_DURATION)) + musicSpec.duration = (int)d.getLong(MediaMetadata.METADATA_KEY_DURATION) / 1000; // finally, tell the device about it GBApplication.deviceService().onSetMusicInfo(musicSpec); + GBApplication.deviceService().onSetMusicState(stateSpec); + return true; } From 204748c518c0477689c05730c30ab57859ef89fd Mon Sep 17 00:00:00 2001 From: Steffen Liebergeld Date: Wed, 8 Jun 2016 20:43:46 +0200 Subject: [PATCH 07/10] "duration" parameter in onSetMusicInfo uses microseconds This is in line with Android, and saves some calculations (and thereby a tiny little bit of battery life). --- .../freeyourgadget/gadgetbridge/activities/DebugActivity.java | 2 +- .../gadgetbridge/externalevents/NotificationListener.java | 2 +- .../gadgetbridge/service/devices/pebble/PebbleProtocol.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java index 3922505e..feea0e95 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java @@ -218,7 +218,7 @@ public class DebugActivity extends GBActivity { musicSpec.artist = editContent.getText().toString() + "(artist)"; musicSpec.album = editContent.getText().toString() + "(album)"; musicSpec.track = editContent.getText().toString() + "(track)"; - musicSpec.duration = 10; + musicSpec.duration = 10 * 1000; musicSpec.trackCount = 5; musicSpec.trackNr = 2; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index a6abd06f..369d18ba 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -380,7 +380,7 @@ public class NotificationListener extends NotificationListenerService { if (d.containsKey(MediaMetadata.METADATA_KEY_TITLE)) musicSpec.track = d.getString(MediaMetadata.METADATA_KEY_TITLE); if (d.containsKey(MediaMetadata.METADATA_KEY_DURATION)) - musicSpec.duration = (int)d.getLong(MediaMetadata.METADATA_KEY_DURATION) / 1000; + musicSpec.duration = (int)d.getLong(MediaMetadata.METADATA_KEY_DURATION); // finally, tell the device about it GBApplication.deviceService().onSetMusicInfo(musicSpec); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index 33397145..ffa2ce87 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -1159,7 +1159,7 @@ public class PebbleProtocol extends GBDeviceProtocol { } buf.order(ByteOrder.LITTLE_ENDIAN); - buf.putInt(duration * 1000); + buf.putInt(duration); buf.putShort((short) (trackCount & 0xffff)); buf.putShort((short) (trackNr & 0xffff)); From 91f374edec601c001288f9018830f2173a11a06c Mon Sep 17 00:00:00 2001 From: Steffen Liebergeld Date: Thu, 9 Jun 2016 20:02:01 +0200 Subject: [PATCH 08/10] Revert ""duration" parameter in onSetMusicInfo uses microseconds" The decision on granularity of APIs is up to the maintainers. This reverts commit 204748c518c0477689c05730c30ab57859ef89fd. --- .../freeyourgadget/gadgetbridge/activities/DebugActivity.java | 2 +- .../gadgetbridge/externalevents/NotificationListener.java | 2 +- .../gadgetbridge/service/devices/pebble/PebbleProtocol.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java index feea0e95..3922505e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java @@ -218,7 +218,7 @@ public class DebugActivity extends GBActivity { musicSpec.artist = editContent.getText().toString() + "(artist)"; musicSpec.album = editContent.getText().toString() + "(album)"; musicSpec.track = editContent.getText().toString() + "(track)"; - musicSpec.duration = 10 * 1000; + musicSpec.duration = 10; musicSpec.trackCount = 5; musicSpec.trackNr = 2; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index 369d18ba..a6abd06f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -380,7 +380,7 @@ public class NotificationListener extends NotificationListenerService { if (d.containsKey(MediaMetadata.METADATA_KEY_TITLE)) musicSpec.track = d.getString(MediaMetadata.METADATA_KEY_TITLE); if (d.containsKey(MediaMetadata.METADATA_KEY_DURATION)) - musicSpec.duration = (int)d.getLong(MediaMetadata.METADATA_KEY_DURATION); + musicSpec.duration = (int)d.getLong(MediaMetadata.METADATA_KEY_DURATION) / 1000; // finally, tell the device about it GBApplication.deviceService().onSetMusicInfo(musicSpec); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index ffa2ce87..33397145 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -1159,7 +1159,7 @@ public class PebbleProtocol extends GBDeviceProtocol { } buf.order(ByteOrder.LITTLE_ENDIAN); - buf.putInt(duration); + buf.putInt(duration * 1000); buf.putShort((short) (trackCount & 0xffff)); buf.putShort((short) (trackNr & 0xffff)); From c5262869d94922123ee88776c9105fb781b22a9a Mon Sep 17 00:00:00 2001 From: Steffen Liebergeld Date: Thu, 9 Jun 2016 20:00:14 +0200 Subject: [PATCH 09/10] Use names for playstates These names need to be mapped to device specific constants in the device code. --- .../externalevents/NotificationListener.java | 8 +++++--- .../gadgetbridge/model/MusicStateSpec.java | 5 +++++ .../service/devices/pebble/PebbleProtocol.java | 17 ++++++++++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index a6abd06f..16647ff6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -359,14 +359,16 @@ public class NotificationListener extends NotificationListenerService { stateSpec.shuffle = 1; switch (s.getState()) { case PlaybackState.STATE_PLAYING: - stateSpec.state = 0x01; + stateSpec.state = MusicStateSpec.STATE_PLAYING; break; case PlaybackState.STATE_STOPPED: + stateSpec.state = MusicStateSpec.STATE_STOPPED; + break; case PlaybackState.STATE_PAUSED: - stateSpec.state = 0x00; + stateSpec.state = MusicStateSpec.STATE_PAUSED; break; default: - stateSpec.state = 0x04; + stateSpec.state = MusicStateSpec.STATE_UNKNOWN; break; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java index 59ec114f..ee5510fb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java @@ -4,6 +4,11 @@ package nodomain.freeyourgadget.gadgetbridge.model; * Created by steffen on 07.06.16. */ public class MusicStateSpec { + public static final int STATE_PLAYING = 0; + public static final int STATE_PAUSED = 1; + public static final int STATE_STOPPED = 2; + public static final int STATE_UNKNOWN = 3; + public byte state; public int position; public int playRate; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index 33397145..11decdc9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -35,6 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; @@ -1102,6 +1103,20 @@ public class PebbleProtocol extends GBDeviceProtocol { } public byte[] encodeSetMusicState(byte state, int position, int playRate, byte shuffle, byte repeat) { + byte playState; + + switch (state) { + case MusicStateSpec.STATE_PLAYING: + playState = MUSICCONTROL_STATE_PLAYING; + break; + case MusicStateSpec.STATE_PAUSED: + playState = MUSICCONTROL_STATE_PAUSED; + break; + default: + playState = MUSICCONTROL_STATE_UNKNOWN; + break; + } + int length = LENGTH_PREFIX + 12; // Encode Prefix ByteBuffer buf = ByteBuffer.allocate(length); @@ -1111,7 +1126,7 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.order(ByteOrder.LITTLE_ENDIAN); buf.put(MUSICCONTROL_SETPLAYSTATE); - buf.put(state); + buf.put(playState); buf.putInt(position); buf.putInt(playRate); buf.put(shuffle); From b76619bb5bd4e9dde1c00e0c68177d73e288bacc Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Thu, 9 Jun 2016 19:55:36 +0200 Subject: [PATCH 10/10] Pebble: implement app reordering in PebbleProtocol Not yet used. --- .../devices/pebble/PebbleProtocol.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index 11decdc9..7672c5fb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -70,6 +70,7 @@ public class PebbleProtocol extends GBDeviceProtocol { static final short ENDPOINT_RUNKEEPER = 7000; static final short ENDPOINT_SCREENSHOT = 8000; static final short ENDPOINT_NOTIFICATIONACTION = 11440; // 3.x only, TODO: find a better name + static final short ENDPOINT_APPREORDER = (short) 0xabcd; // 3.x only static final short ENDPOINT_BLOBDB = (short) 45531; // 3.x only static final short ENDPOINT_PUTBYTES = (short) 48879; @@ -1292,6 +1293,22 @@ public class PebbleProtocol extends GBDeviceProtocol { return encodeSimpleMessage(ENDPOINT_SCREENSHOT, SCREENSHOT_TAKE); } + public byte[] encodeAppReoder(UUID[] uuids) { + int length = 2 + uuids.length * LENGTH_UUID; + ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length); + buf.order(ByteOrder.BIG_ENDIAN); + buf.putShort((short) length); + buf.putShort(ENDPOINT_APPREORDER); + buf.put((byte) 0x01); + buf.put((byte) uuids.length); + for (UUID uuid : uuids) { + buf.putLong(uuid.getMostSignificantBits()); + buf.putLong(uuid.getLeastSignificantBits()); + } + + return buf.array(); + } + /* pebble specific install methods */ public byte[] encodeUploadStart(byte type, int app_id, int size, String filename) { short length; @@ -1947,6 +1964,16 @@ public class PebbleProtocol extends GBDeviceProtocol { return sendBytes; } + private GBDeviceEvent decodeAppReorder(ByteBuffer buf) { + byte status = buf.get(); + if (status == 1) { + LOG.info("app reordering successful"); + } else { + LOG.info("app reordering returned status " + status); + } + return null; + } + @Override public GBDeviceEvent[] decodeResponse(byte[] responseData) { ByteBuffer buf = ByteBuffer.wrap(responseData); @@ -2197,6 +2224,8 @@ public class PebbleProtocol extends GBDeviceProtocol { case ENDPOINT_BLOBDB: devEvts = new GBDeviceEvent[]{decodeBlobDb(buf)}; break; + case ENDPOINT_APPREORDER: + devEvts = new GBDeviceEvent[]{decodeAppReorder(buf)}; default: break; }