From da5f91f05b0c24b2f29dea5415ff586b760dce38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sun, 9 Jul 2023 15:16:37 +0100 Subject: [PATCH] Huami/Zepp OS: Improve music info stability --- .../devices/huami/Huami2021Service.java | 16 -- .../devices/huami/Huami2021Support.java | 81 +-------- .../service/devices/huami/HuamiSupport.java | 86 +++++----- .../huami/amazfitcor/AmazfitCorSupport.java | 5 +- .../zeppos/services/ZeppOsMusicService.java | 142 ++++++++++++++++ .../gadgetbridge/util/MediaManager.java | 156 ++++++++++++++++++ 6 files changed, 347 insertions(+), 139 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsMusicService.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/MediaManager.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Service.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Service.java index 7f5efdbe9..c4ba5ee26 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Service.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Service.java @@ -27,7 +27,6 @@ public class Huami2021Service { public static final short CHUNKED2021_ENDPOINT_VIBRATION_PATTERNS = 0x0018; public static final short CHUNKED2021_ENDPOINT_WORKOUT = 0x0019; public static final short CHUNKED2021_ENDPOINT_FIND_DEVICE = 0x001a; - public static final short CHUNKED2021_ENDPOINT_MUSIC = 0x001b; public static final short CHUNKED2021_ENDPOINT_HEARTRATE = 0x001d; public static final short CHUNKED2021_ENDPOINT_BATTERY = 0x0029; public static final short CHUNKED2021_ENDPOINT_SILENT_MODE = 0x003b; @@ -115,21 +114,6 @@ public class Huami2021Service { public static final byte WORKOUT_STATUS_START = 0x01; public static final byte WORKOUT_STATUS_END = 0x04; - /** - * Music, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_MUSIC}. - */ - public static final byte MUSIC_CMD_MEDIA_INFO = 0x03; - public static final byte MUSIC_CMD_APP_STATE = 0x04; - public static final byte MUSIC_CMD_BUTTON_PRESS = 0x05; - public static final byte MUSIC_APP_OPEN = 0x01; - public static final byte MUSIC_APP_CLOSE = 0x02; - public static final byte MUSIC_BUTTON_PLAY = 0x00; - public static final byte MUSIC_BUTTON_PAUSE = 0x01; - public static final byte MUSIC_BUTTON_NEXT = 0x03; - public static final byte MUSIC_BUTTON_PREVIOUS = 0x04; - public static final byte MUSIC_BUTTON_VOLUME_UP = 0x05; - public static final byte MUSIC_BUTTON_VOLUME_DOWN = 0x06; - /** * Weather, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_WEATHER}. */ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java index 2416a0f7b..8b9f81eee 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java @@ -43,7 +43,6 @@ import android.net.Uri; import android.os.Handler; import android.widget.Toast; -import org.apache.commons.lang3.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,36 +50,27 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLDecoder; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; -import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; -import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; @@ -99,7 +89,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.Contact; -import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; @@ -123,6 +112,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsHttpService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLoyaltyCardService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsMusicService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsNotificationService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsServicesService; @@ -139,9 +129,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; -import nodomain.freeyourgadget.gadgetbridge.util.MapUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFileTransferService.Callback { private static final Logger LOG = LoggerFactory.getLogger(Huami2021Support.class); @@ -176,6 +164,7 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil private final ZeppOsHttpService httpService = new ZeppOsHttpService(this); private final ZeppOsRemindersService remindersService = new ZeppOsRemindersService(this); private final ZeppOsLoyaltyCardService loyaltyCardService = new ZeppOsLoyaltyCardService(this); + private final ZeppOsMusicService musicService = new ZeppOsMusicService(this); private final Map mServiceMap = new LinkedHashMap() {{ put(servicesService.getEndpoint(), servicesService); @@ -200,6 +189,7 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil put(httpService.getEndpoint(), httpService); put(remindersService.getEndpoint(), remindersService); put(loyaltyCardService.getEndpoint(), loyaltyCardService); + put(musicService.getEndpoint(), musicService); }}; public Huami2021Support() { @@ -555,17 +545,12 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil @Override public void onSetPhoneVolume(final float volume) { - // FIXME: we need to send the music info and state as well, or it breaks the info - sendMusicStateToDevice(bufferMusicSpec, bufferMusicStateSpec); + musicService.sendVolume(volume); } - protected void sendMusicStateToDevice(final MusicSpec musicSpec, - final MusicStateSpec musicStateSpec) { - byte[] cmd = ArrayUtils.addAll(new byte[]{MUSIC_CMD_MEDIA_INFO}, encodeMusicState(musicSpec, musicStateSpec, true)); - - LOG.info("sendMusicStateToDevice: {}, {}", musicSpec, musicStateSpec); - - writeToChunked2021("send playback info", CHUNKED2021_ENDPOINT_MUSIC, cmd, false); + @Override + protected void sendMusicStateToDevice(final MusicSpec musicSpec, final MusicStateSpec musicStateSpec) { + musicService.sendMusicState(musicSpec, musicStateSpec); } @Override @@ -1091,9 +1076,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil case CHUNKED2021_ENDPOINT_SILENT_MODE: handle2021SilentMode(payload); return; - case CHUNKED2021_ENDPOINT_MUSIC: - handle2021Music(payload); - return; default: LOG.warn("Unhandled 2021 payload {}", String.format("0x%04x", type)); } @@ -1320,55 +1302,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil } } - protected void handle2021Music(final byte[] payload) { - switch (payload[0]) { - case MUSIC_CMD_APP_STATE: - switch (payload[1]) { - case MUSIC_APP_OPEN: - onMusicAppOpen(); - break; - case MUSIC_APP_CLOSE: - onMusicAppClosed(); - break; - default: - LOG.warn("Unexpected music app state {}", String.format("0x%02x", payload[1])); - break; - } - return; - - case MUSIC_CMD_BUTTON_PRESS: - LOG.info("Got music button press"); - final GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl(); - switch (payload[1]) { - case MUSIC_BUTTON_PLAY: - deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PLAY; - break; - case MUSIC_BUTTON_PAUSE: - deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PAUSE; - break; - case MUSIC_BUTTON_NEXT: - deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.NEXT; - break; - case MUSIC_BUTTON_PREVIOUS: - deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PREVIOUS; - break; - case MUSIC_BUTTON_VOLUME_UP: - deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEUP; - break; - case MUSIC_BUTTON_VOLUME_DOWN: - deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN; - break; - default: - LOG.warn("Unexpected music button {}", String.format("0x%02x", payload[1])); - return; - } - evaluateGBDeviceEvent(deviceEventMusicControl); - return; - default: - LOG.warn("Unexpected music byte {}", String.format("0x%02x", payload[0])); - } - } - @Override public void onFileUploadFinish(final boolean success) { LOG.warn("Unexpected file upload finish: {}", success); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index 53adc1f09..f4e97760b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -19,6 +19,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.Context; @@ -27,8 +28,9 @@ import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.location.Location; -import android.media.AudioManager; import android.net.Uri; +import android.os.Handler; +import android.os.Looper; import android.widget.Toast; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -112,6 +114,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager; import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; @@ -130,6 +133,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.Fet import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchStressManualOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.HuamiFetchDebugLogsOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCannedMessagesService; +import nodomain.freeyourgadget.gadgetbridge.util.MediaManager; import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent; import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; @@ -327,8 +331,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements private RealtimeSamplesSupport realtimeSamplesSupport; protected boolean isMusicAppStarted = false; - protected MusicSpec bufferMusicSpec = null; - protected MusicStateSpec bufferMusicStateSpec = null; + protected MediaManager mediaManager; private boolean heartRateNotifyEnabled; private int mMTU = 23; protected int mActivitySampleSize = 4; @@ -360,6 +363,12 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements addSupportedProfile(deviceInfoProfile); } + @Override + public void setContext(final GBDevice gbDevice, final BluetoothAdapter btAdapter, final Context context) { + super.setContext(gbDevice, btAdapter, context); + this.mediaManager = new MediaManager(context); + } + @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { try { @@ -1337,53 +1346,49 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements } @Override - public void onSetMusicState(MusicStateSpec stateSpec) { - DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); - if (!coordinator.supportsMusicInfo()) { + public void onSetMusicState(final MusicStateSpec stateSpec) { + if (!getCoordinator().supportsMusicInfo()) { return; } - if (stateSpec != null && !stateSpec.equals(bufferMusicStateSpec)) { - bufferMusicStateSpec = stateSpec; - if (isMusicAppStarted) { - sendMusicStateToDevice(null, bufferMusicStateSpec); - } + if (mediaManager.onSetMusicState(stateSpec) && isMusicAppStarted) { + sendMusicStateToDevice(null, mediaManager.getBufferMusicStateSpec()); } } @Override - public void onSetMusicInfo(MusicSpec musicSpec) { - DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); - if (!coordinator.supportsMusicInfo()) { + public void onSetMusicInfo(final MusicSpec musicSpec) { + if (!getCoordinator().supportsMusicInfo()) { return; } - if (musicSpec != null && !musicSpec.equals(bufferMusicSpec)) { - bufferMusicSpec = musicSpec; - if (bufferMusicStateSpec != null) { - bufferMusicStateSpec.state = 0; - bufferMusicStateSpec.position = 0; - } - if (isMusicAppStarted) { - sendMusicStateToDevice(bufferMusicSpec, bufferMusicStateSpec); - } + if (mediaManager.onSetMusicInfo(musicSpec) && isMusicAppStarted) { + sendMusicStateToDevice(mediaManager.getBufferMusicSpec(), mediaManager.getBufferMusicStateSpec()); } } - protected void onMusicAppOpen() { + public void onMusicAppOpen() { LOG.info("Music app started"); isMusicAppStarted = true; - sendMusicStateToDevice(); - sendVolumeStateToDevice(); + sendMusicStateDelayed(); } - protected void onMusicAppClosed() { + public void onMusicAppClosed() { LOG.info("Music app terminated"); isMusicAppStarted = false; } - private void sendMusicStateToDevice() { - sendMusicStateToDevice(bufferMusicSpec, bufferMusicStateSpec); + /** + * Send the music state after a small delay. If we send it right as the app notifies us that it opened, + * it won't be recognized. + */ + private void sendMusicStateDelayed() { + final Looper mainLooper = Looper.getMainLooper(); + new Handler(mainLooper).postDelayed(() -> { + mediaManager.refresh(); + sendMusicStateToDevice(mediaManager.getBufferMusicSpec(), mediaManager.getBufferMusicStateSpec()); + onSetPhoneVolume(mediaManager.getPhoneVolume()); + }, 100); } @Override @@ -1406,20 +1411,6 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements LOG.info("sendVolumeStateToDevice: {}", volume); } - private void sendVolumeStateToDevice() { - onSetPhoneVolume(getPhoneVolume()); - } - - protected int getPhoneVolume() { - final AudioManager audioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); - - final int volumeLevel = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); - final int volumeMax = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); - final int volumePercentage = (byte) Math.round(100 * (volumeLevel / (float) volumeMax)); - - return volumePercentage; - } - protected void sendMusicStateToDevice(final MusicSpec musicSpec, final MusicStateSpec musicStateSpec) { if (characteristicChunked == null) { return; @@ -1431,7 +1422,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements try { TransactionBuilder builder = performInitialized("send playback info"); - writeToChunked(builder, 3, encodeMusicState(musicSpec, musicStateSpec, false)); + writeToChunked(builder, 3, encodeMusicState(getContext(), musicSpec, musicStateSpec, false)); builder.queue(getQueue()); } catch (IOException e) { LOG.error("Unable to send playback state"); @@ -1439,7 +1430,10 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements LOG.info("sendMusicStateToDevice: {}, {}", musicSpec, musicStateSpec); } - protected byte[] encodeMusicState(final MusicSpec musicSpec, final MusicStateSpec musicStateSpec, final boolean includeVolume) { + public static byte[] encodeMusicState(final Context context, + final MusicSpec musicSpec, + final MusicStateSpec musicStateSpec, + final boolean includeVolume) { String artist = ""; String album = ""; String track = ""; @@ -1518,7 +1512,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements } if (includeVolume) { - buf.put((byte) getPhoneVolume()); + buf.put((byte) MediaManager.getPhoneVolume(context)); } return buf.array(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorSupport.java index 725839a52..e4e4f4ad2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorSupport.java @@ -51,9 +51,8 @@ public class AmazfitCorSupport extends AmazfitBipSupport { @Override public void onSetMusicState(MusicStateSpec stateSpec) { - if (stateSpec != null && !stateSpec.equals(bufferMusicStateSpec)) { - sendMusicStateToDevice(null, stateSpec); - bufferMusicStateSpec = stateSpec; + if (mediaManager.onSetMusicState(stateSpec)) { + sendMusicStateToDevice(null, mediaManager.getBufferMusicStateSpec()); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsMusicService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsMusicService.java new file mode 100644 index 000000000..69abc6f14 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsMusicService.java @@ -0,0 +1,142 @@ +/* Copyright (C) 2023 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services; + +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService; + +public class ZeppOsMusicService extends AbstractZeppOsService { + private static final Logger LOG = LoggerFactory.getLogger(ZeppOsMusicService.class); + + private static final short ENDPOINT = 0x001b; + + private static final byte CMD_MEDIA_INFO = 0x03; + private static final byte CMD_APP_STATE = 0x04; + private static final byte CMD_BUTTON_PRESS = 0x05; + private static final byte MUSIC_APP_OPEN = 0x01; + private static final byte MUSIC_APP_CLOSE = 0x02; + private static final byte BUTTON_PLAY = 0x00; + private static final byte BUTTON_PAUSE = 0x01; + private static final byte BUTTON_NEXT = 0x03; + private static final byte BUTTON_PREVIOUS = 0x04; + private static final byte BUTTON_VOLUME_UP = 0x05; + private static final byte BUTTON_VOLUME_DOWN = 0x06; + + public ZeppOsMusicService(final Huami2021Support support) { + super(support); + } + + @Override + public short getEndpoint() { + return ENDPOINT; + } + + @Override + public boolean isEncrypted() { + return false; + } + + @Override + public void handlePayload(final byte[] payload) { + switch (payload[0]) { + case CMD_APP_STATE: + switch (payload[1]) { + case MUSIC_APP_OPEN: + onMusicAppOpen(); + break; + case MUSIC_APP_CLOSE: + onMusicAppClosed(); + break; + default: + LOG.warn("Unexpected music app state {}", String.format("0x%02x", payload[1])); + break; + } + return; + + case CMD_BUTTON_PRESS: + LOG.info("Got music button press"); + final GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl(); + switch (payload[1]) { + case BUTTON_PLAY: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PLAY; + break; + case BUTTON_PAUSE: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PAUSE; + break; + case BUTTON_NEXT: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.NEXT; + break; + case BUTTON_PREVIOUS: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PREVIOUS; + break; + case BUTTON_VOLUME_UP: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEUP; + break; + case BUTTON_VOLUME_DOWN: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN; + break; + default: + LOG.warn("Unexpected music button {}", String.format("0x%02x", payload[1])); + return; + } + evaluateGBDeviceEvent(deviceEventMusicControl); + return; + default: + LOG.warn("Unexpected music byte {}", String.format("0x%02x", payload[0])); + } + } + + private void onMusicAppOpen() { + getSupport().onMusicAppOpen(); + } + + private void onMusicAppClosed() { + getSupport().onMusicAppClosed(); + } + + public void sendMusicState(final MusicSpec musicSpec, + final MusicStateSpec musicStateSpec) { + LOG.info("Sending music: {}, {}", musicSpec, musicStateSpec); + + // TODO: Encode not playing state (flag 0x20, single 0x01 byte before volume) + final byte[] cmd = ArrayUtils.addAll( + new byte[]{CMD_MEDIA_INFO}, + HuamiSupport.encodeMusicState(getContext(), musicSpec, musicStateSpec, false) + ); + + write("send music state", cmd); + } + + public void sendVolume(final float volume) { + LOG.info("Sending volume: {}", volume); + + final byte[] cmd = ArrayUtils.addAll( + new byte[]{CMD_MEDIA_INFO}, + HuamiSupport.encodeMusicState(getContext(), null, null, true) + ); + + write("send volume", cmd); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/MediaManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/MediaManager.java new file mode 100644 index 000000000..a11ebf32a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/MediaManager.java @@ -0,0 +1,156 @@ +/* Copyright (C) 2023 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.util; + +import android.content.ComponentName; +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; + +public class MediaManager { + private static final Logger LOG = LoggerFactory.getLogger(MediaManager.class); + + private final Context context; + + private MusicSpec bufferMusicSpec = null; + private MusicStateSpec bufferMusicStateSpec = null; + + public MediaManager(final Context context) { + this.context = context; + } + + public MusicSpec getBufferMusicSpec() { + return bufferMusicSpec; + } + + public MusicStateSpec getBufferMusicStateSpec() { + return bufferMusicStateSpec; + } + + /** + * Returns true if the spec changed, so the device should be updated. + */ + public boolean onSetMusicState(final MusicStateSpec stateSpec) { + if (stateSpec != null && !stateSpec.equals(bufferMusicStateSpec)) { + bufferMusicStateSpec = stateSpec; + return true; + } + + return false; + } + + /** + * Returns true if the spec changed, so the device should be updated. + */ + public boolean onSetMusicInfo(MusicSpec musicSpec) { + if (musicSpec != null && !musicSpec.equals(bufferMusicSpec)) { + bufferMusicSpec = musicSpec; + if (bufferMusicStateSpec != null) { + bufferMusicStateSpec.state = 0; + bufferMusicStateSpec.position = 0; + } + return true; + } + return false; + } + + public void refresh() { + LOG.info("Refreshing media state"); + + final MediaSessionManager mediaSessionManager = + (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); + + try { + final List controllers = mediaSessionManager.getActiveSessions( + new ComponentName(context, NotificationListener.class) + ); + if (controllers.isEmpty()) { + LOG.debug("No media controller available"); + return; + } + final MediaController controller = controllers.get(0); + + final MediaMetadata metadata = controller.getMetadata(); + final PlaybackState playbackState = controller.getPlaybackState(); + + final MusicSpec musicSpec = new MusicSpec(); + musicSpec.artist = StringUtils.ensureNotNull(metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)); + musicSpec.album = StringUtils.ensureNotNull(metadata.getString(MediaMetadata.METADATA_KEY_ALBUM)); + musicSpec.track = StringUtils.ensureNotNull(metadata.getString(MediaMetadata.METADATA_KEY_TITLE)); + musicSpec.trackNr = (int) metadata.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER); + musicSpec.trackCount = (int) metadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS); + musicSpec.duration = (int) metadata.getLong(MediaMetadata.METADATA_KEY_DURATION) / 1000; + + final MusicStateSpec stateSpec = new MusicStateSpec(); + switch (playbackState.getState()) { + case PlaybackState.STATE_PLAYING: + case PlaybackState.STATE_FAST_FORWARDING: + case PlaybackState.STATE_REWINDING: + case PlaybackState.STATE_BUFFERING: + case PlaybackState.STATE_CONNECTING: + case PlaybackState.STATE_SKIPPING_TO_PREVIOUS: + case PlaybackState.STATE_SKIPPING_TO_NEXT: + case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM: + stateSpec.state = MusicStateSpec.STATE_PLAYING; + break; + case PlaybackState.STATE_PAUSED: + stateSpec.state = MusicStateSpec.STATE_PAUSED; + break; + case PlaybackState.STATE_STOPPED: + case PlaybackState.STATE_ERROR: + stateSpec.state = MusicStateSpec.STATE_STOPPED; + break; + case PlaybackState.STATE_NONE: + default: + stateSpec.state = MusicStateSpec.STATE_UNKNOWN; + } + stateSpec.position = (int) playbackState.getPosition() / 1000; + stateSpec.playRate = (int) (playbackState.getPlaybackSpeed() * 100); + stateSpec.repeat = MusicStateSpec.STATE_UNKNOWN; + stateSpec.shuffle = MusicStateSpec.STATE_UNKNOWN; + + bufferMusicStateSpec = stateSpec; + bufferMusicSpec = musicSpec; + } catch (final SecurityException e) { + LOG.warn("No permission to get media sessions - did not grant notification access?", e); + } + } + + public int getPhoneVolume() { + return getPhoneVolume(context); + } + + public static int getPhoneVolume(final Context context) { + final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + + final int volumeLevel = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + final int volumeMax = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + return Math.round(100 * (volumeLevel / (float) volumeMax)); + } +}