diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java index 8b819e773..7c51ca12f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java @@ -20,6 +20,8 @@ package nodomain.freeyourgadget.gadgetbridge.model; import java.util.Objects; public class MusicSpec { + public static final int MUSIC_UNKNOWN = -1; + public static final int MUSIC_UNDEFINED = 0; public static final int MUSIC_PLAY = 1; public static final int MUSIC_PAUSE = 2; @@ -27,12 +29,12 @@ public class MusicSpec { public static final int MUSIC_NEXT = 4; public static final int MUSIC_PREVIOUS = 5; - public String artist; - public String album; - public String track; - public int duration; - public int trackCount; - public int trackNr; + public String artist = null; + public String album = null; + public String track = null; + public int duration = MUSIC_UNKNOWN; + public int trackCount = MUSIC_UNKNOWN; + public int trackNr = MUSIC_UNKNOWN; public MusicSpec() { 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 23aa23593..ef0c44f8a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java @@ -17,20 +17,26 @@ along with this program. If not, see . */ 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 static final int STATE_UNKNOWN = -1; - public byte state; - public int position; // Position of the current media in seconds - public int playRate; // Speed of playback, usually 0 or 100 (full speed) - public byte shuffle; - public byte repeat; + 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_SHUFFLE_ENABLED = 1; + + public byte state = STATE_UNKNOWN; + /** + * Position of the current media in seconds + */ + public int position = STATE_UNKNOWN; + /** + * Speed of playback, usually 0 or 100 (full speed) + */ + public int playRate = STATE_UNKNOWN; + public byte shuffle = STATE_UNKNOWN; + public byte repeat = STATE_UNKNOWN; public MusicStateSpec() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java index 7f1a89f65..b48e2c83f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Andreas Shimokawa +/* Copyright (C) 2020 Andreas Shimokawa, Taavi Eomäe This file is part of Gadgetbridge. @@ -24,13 +24,14 @@ import android.net.Uri; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.GregorianCalendar; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; +import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFConstants; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; @@ -59,46 +60,14 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); private final DeviceInfoProfile deviceInfoProfile; - private static final UUID UUID_SERVICE_MUSICCONTROL = UUID.fromString("c7e50001-00fc-48fe-8e23-433b3a1942d0"); - private static final UUID UUID_CHARACTERISTICS_MUSIC_EVENT = UUID.fromString("c7e50002-00fc-48fe-8e23-433b3a1942d0"); - private static final UUID UUID_CHARACTERISTICS_MUSIC_STATUS = UUID.fromString("c7e50003-00fc-48fe-8e23-433b3a1942d0"); - private static final UUID UUID_CHARACTERISTICS_MUSIC_TRACK = UUID.fromString("c7e50004-00fc-48fe-8e23-433b3a1942d0"); - private static final UUID UUID_CHARACTERISTICS_MUSIC_ARTIST = UUID.fromString("c7e50005-00fc-48fe-8e23-433b3a1942d0"); - private static final UUID UUID_CHARACTERISTICS_MUSIC_ALBUM = UUID.fromString("c7e50006-00fc-48fe-8e23-433b3a1942d0"); - - public PineTimeJFSupport() { - super(LOG); - addSupportedService(GattService.UUID_SERVICE_ALERT_NOTIFICATION); - addSupportedService(GattService.UUID_SERVICE_CURRENT_TIME); - addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); - addSupportedService(UUID_SERVICE_MUSICCONTROL); - deviceInfoProfile = new DeviceInfoProfile<>(this); - IntentListener mListener = new IntentListener() { - @Override - public void notify(Intent intent) { - String action = intent.getAction(); - if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) { - handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); - } - } - }; - deviceInfoProfile.addListener(mListener); - AlertNotificationProfile alertNotificationProfile = new AlertNotificationProfile<>(this); - addSupportedProfile(alertNotificationProfile); - addSupportedProfile(deviceInfoProfile); - } - - - @Override - protected TransactionBuilder initializeDevice(TransactionBuilder builder) { - builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); - requestDeviceInfo(builder); - onSetTime(); - builder.notify(getCharacteristic(UUID_CHARACTERISTICS_MUSIC_EVENT), true); - setInitialized(builder); - return builder; - } - + /** + * These are used to keep track when long strings haven't changed, + * thus avoiding unnecessary transfers that are (potentially) very slow. + *

+ * Makes the device's UI more responsive. + */ + String lastAlbum; + String lastTrack; private void setInitialized(TransactionBuilder builder) { builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); @@ -163,7 +132,6 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { } - @Override public void onEnableRealtimeSteps(boolean enable) { @@ -253,19 +221,73 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { } + String lastArtist; + + public PineTimeJFSupport() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_ALERT_NOTIFICATION); + addSupportedService(GattService.UUID_SERVICE_CURRENT_TIME); + addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); + addSupportedService(PineTimeJFConstants.UUID_SERVICE_MUSIC_CONTROL); + deviceInfoProfile = new DeviceInfoProfile<>(this); + IntentListener mListener = new IntentListener() { + @Override + public void notify(Intent intent) { + String action = intent.getAction(); + if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) { + handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); + } + } + }; + deviceInfoProfile.addListener(mListener); + AlertNotificationProfile alertNotificationProfile = new AlertNotificationProfile<>(this); + addSupportedProfile(alertNotificationProfile); + addSupportedProfile(deviceInfoProfile); + } + + /** + * Helper function that ust converts an integer into a byte array + */ + private static byte[] intToBytes(int source) { + return ByteBuffer.allocate(4).putInt(source).array(); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + requestDeviceInfo(builder); + onSetTime(); + builder.notify(getCharacteristic(PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_EVENT), true); + setInitialized(builder); + return builder; + } + @Override public void onSetMusicInfo(MusicSpec musicSpec) { try { TransactionBuilder builder = performInitialized("send playback info"); - if (musicSpec.album != null) { - builder.write(getCharacteristic(UUID_CHARACTERISTICS_MUSIC_TRACK), musicSpec.track.getBytes()); + if (musicSpec.album != null && !musicSpec.album.equals(lastAlbum)) { + lastAlbum = musicSpec.album; + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_ALBUM, musicSpec.album.getBytes()); } - if (musicSpec.artist != null) { - builder.write(getCharacteristic(UUID_CHARACTERISTICS_MUSIC_ARTIST), musicSpec.artist.getBytes()); + if (musicSpec.track != null && !musicSpec.track.equals(lastTrack)) { + lastTrack = musicSpec.track; + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_TRACK, musicSpec.track.getBytes()); } - if (musicSpec.album != null) { - builder.write(getCharacteristic(UUID_CHARACTERISTICS_MUSIC_ALBUM), musicSpec.album.getBytes()); + if (musicSpec.artist != null && !musicSpec.artist.equals(lastArtist)) { + lastArtist = musicSpec.artist; + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_ARTIST, musicSpec.artist.getBytes()); + } + + if (musicSpec.duration != MusicSpec.MUSIC_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_LENGTH_TOTAL, intToBytes(musicSpec.duration)); + } + if (musicSpec.trackNr != MusicSpec.MUSIC_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_TRACK_NUMBER, intToBytes(musicSpec.trackNr)); + } + if (musicSpec.trackCount != MusicSpec.MUSIC_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_TRACK_TOTAL, intToBytes(musicSpec.trackCount)); } builder.queue(getQueue()); @@ -279,11 +301,30 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { try { TransactionBuilder builder = performInitialized("send playback state"); - byte[] state = new byte[]{0}; - if (stateSpec.state == MusicStateSpec.STATE_PLAYING) { - state[0] = 1; + if (stateSpec.state != MusicStateSpec.STATE_UNKNOWN) { + byte[] state = new byte[1]; + if (stateSpec.state == MusicStateSpec.STATE_PLAYING) { + state[0] = 0x01; + } + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_STATUS, state); } - builder.write(getCharacteristic(UUID_CHARACTERISTICS_MUSIC_STATUS), state); + + if (stateSpec.playRate != MusicStateSpec.STATE_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_PLAYBACK_SPEED, intToBytes(stateSpec.playRate)); + } + + if (stateSpec.position != MusicStateSpec.STATE_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_POSITION, intToBytes(stateSpec.position)); + } + + if (stateSpec.repeat != MusicStateSpec.STATE_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_REPEAT, intToBytes(stateSpec.repeat)); + } + + if (stateSpec.shuffle != MusicStateSpec.STATE_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_SHUFFLE, intToBytes(stateSpec.repeat)); + } + builder.queue(getQueue()); } catch (Exception e) { @@ -292,6 +333,33 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { } + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + if (super.onCharacteristicRead(gatt, characteristic, status)) { + return true; + } + UUID characteristicUUID = characteristic.getUuid(); + + LOG.info("Unhandled characteristic read: " + characteristicUUID); + return false; + } + + @Override + public void onSendConfiguration(String config) { + + } + + @Override + public void onReadConfiguration(String config) { + + } + + @Override + public void onTestNewFunction() { + + } + @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { @@ -300,7 +368,7 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { } UUID characteristicUUID = characteristic.getUuid(); - if (characteristicUUID.equals(UUID_CHARACTERISTICS_MUSIC_EVENT)) { + if (characteristicUUID.equals(PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_EVENT)) { byte[] value = characteristic.getValue(); GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl(); @@ -333,35 +401,21 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { return false; } - @Override - public boolean onCharacteristicRead(BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic, int status) { - if (super.onCharacteristicRead(gatt, characteristic, status)) { - return true; - } - UUID characteristicUUID = characteristic.getUuid(); - - LOG.info("Unhandled characteristic read: " + characteristicUUID); - return false; - } - - @Override - public void onSendConfiguration(String config) { - - } - - @Override - public void onReadConfiguration(String config) { - - } - - @Override - public void onTestNewFunction() { - - } - @Override public void onSendWeather(WeatherSpec weatherSpec) { } + + /** + * This will check if the characteristic exists and can be written + *

+ * Keeps backwards compatibility with firmware that can't take all the information + */ + private void safeWriteToCharacteristic(TransactionBuilder builder, UUID uuid, byte[] data) { + BluetoothGattCharacteristic characteristic = getCharacteristic(uuid); + if (characteristic != null && + (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) { + builder.write(characteristic, data); + } + } }