1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-07 18:57:02 +01:00

Huami/Zepp OS: Improve music info stability

This commit is contained in:
José Rebelo 2023-07-09 15:16:37 +01:00
parent ac89b1df9d
commit da5f91f05b
6 changed files with 347 additions and 139 deletions

View File

@ -27,7 +27,6 @@ public class Huami2021Service {
public static final short CHUNKED2021_ENDPOINT_VIBRATION_PATTERNS = 0x0018; public static final short CHUNKED2021_ENDPOINT_VIBRATION_PATTERNS = 0x0018;
public static final short CHUNKED2021_ENDPOINT_WORKOUT = 0x0019; public static final short CHUNKED2021_ENDPOINT_WORKOUT = 0x0019;
public static final short CHUNKED2021_ENDPOINT_FIND_DEVICE = 0x001a; 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_HEARTRATE = 0x001d;
public static final short CHUNKED2021_ENDPOINT_BATTERY = 0x0029; public static final short CHUNKED2021_ENDPOINT_BATTERY = 0x0029;
public static final short CHUNKED2021_ENDPOINT_SILENT_MODE = 0x003b; 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_START = 0x01;
public static final byte WORKOUT_STATUS_END = 0x04; 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}. * Weather, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_WEATHER}.
*/ */

View File

@ -43,7 +43,6 @@ import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.widget.Toast; import android.widget.Toast;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -51,36 +50,27 @@ import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard; import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; 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.Huami2021Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service; import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; 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.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Contact; import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; 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.ZeppOsHttpService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService; 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.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.ZeppOsNotificationService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsServicesService; 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.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.MapUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFileTransferService.Callback { public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFileTransferService.Callback {
private static final Logger LOG = LoggerFactory.getLogger(Huami2021Support.class); 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 ZeppOsHttpService httpService = new ZeppOsHttpService(this);
private final ZeppOsRemindersService remindersService = new ZeppOsRemindersService(this); private final ZeppOsRemindersService remindersService = new ZeppOsRemindersService(this);
private final ZeppOsLoyaltyCardService loyaltyCardService = new ZeppOsLoyaltyCardService(this); private final ZeppOsLoyaltyCardService loyaltyCardService = new ZeppOsLoyaltyCardService(this);
private final ZeppOsMusicService musicService = new ZeppOsMusicService(this);
private final Map<Short, AbstractZeppOsService> mServiceMap = new LinkedHashMap<Short, AbstractZeppOsService>() {{ private final Map<Short, AbstractZeppOsService> mServiceMap = new LinkedHashMap<Short, AbstractZeppOsService>() {{
put(servicesService.getEndpoint(), servicesService); put(servicesService.getEndpoint(), servicesService);
@ -200,6 +189,7 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
put(httpService.getEndpoint(), httpService); put(httpService.getEndpoint(), httpService);
put(remindersService.getEndpoint(), remindersService); put(remindersService.getEndpoint(), remindersService);
put(loyaltyCardService.getEndpoint(), loyaltyCardService); put(loyaltyCardService.getEndpoint(), loyaltyCardService);
put(musicService.getEndpoint(), musicService);
}}; }};
public Huami2021Support() { public Huami2021Support() {
@ -555,17 +545,12 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
@Override @Override
public void onSetPhoneVolume(final float volume) { public void onSetPhoneVolume(final float volume) {
// FIXME: we need to send the music info and state as well, or it breaks the info musicService.sendVolume(volume);
sendMusicStateToDevice(bufferMusicSpec, bufferMusicStateSpec);
} }
protected void sendMusicStateToDevice(final MusicSpec musicSpec, @Override
final MusicStateSpec musicStateSpec) { protected void sendMusicStateToDevice(final MusicSpec musicSpec, final MusicStateSpec musicStateSpec) {
byte[] cmd = ArrayUtils.addAll(new byte[]{MUSIC_CMD_MEDIA_INFO}, encodeMusicState(musicSpec, musicStateSpec, true)); musicService.sendMusicState(musicSpec, musicStateSpec);
LOG.info("sendMusicStateToDevice: {}, {}", musicSpec, musicStateSpec);
writeToChunked2021("send playback info", CHUNKED2021_ENDPOINT_MUSIC, cmd, false);
} }
@Override @Override
@ -1091,9 +1076,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
case CHUNKED2021_ENDPOINT_SILENT_MODE: case CHUNKED2021_ENDPOINT_SILENT_MODE:
handle2021SilentMode(payload); handle2021SilentMode(payload);
return; return;
case CHUNKED2021_ENDPOINT_MUSIC:
handle2021Music(payload);
return;
default: default:
LOG.warn("Unhandled 2021 payload {}", String.format("0x%04x", type)); 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 @Override
public void onFileUploadFinish(final boolean success) { public void onFileUploadFinish(final boolean success) {
LOG.warn("Unexpected file upload finish: {}", success); LOG.warn("Unexpected file upload finish: {}", success);

View File

@ -19,6 +19,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context; import android.content.Context;
@ -27,8 +28,9 @@ import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.location.Location; import android.location.Location;
import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast; import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; 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.entities.User;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager; import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController; import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; 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.FetchStressManualOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.HuamiFetchDebugLogsOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.HuamiFetchDebugLogsOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCannedMessagesService; 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.CalendarEvent;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager; import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
@ -327,8 +331,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
private RealtimeSamplesSupport realtimeSamplesSupport; private RealtimeSamplesSupport realtimeSamplesSupport;
protected boolean isMusicAppStarted = false; protected boolean isMusicAppStarted = false;
protected MusicSpec bufferMusicSpec = null; protected MediaManager mediaManager;
protected MusicStateSpec bufferMusicStateSpec = null;
private boolean heartRateNotifyEnabled; private boolean heartRateNotifyEnabled;
private int mMTU = 23; private int mMTU = 23;
protected int mActivitySampleSize = 4; protected int mActivitySampleSize = 4;
@ -360,6 +363,12 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
addSupportedProfile(deviceInfoProfile); 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 @Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) { protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
try { try {
@ -1337,53 +1346,49 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
} }
@Override @Override
public void onSetMusicState(MusicStateSpec stateSpec) { public void onSetMusicState(final MusicStateSpec stateSpec) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); if (!getCoordinator().supportsMusicInfo()) {
if (!coordinator.supportsMusicInfo()) {
return; return;
} }
if (stateSpec != null && !stateSpec.equals(bufferMusicStateSpec)) { if (mediaManager.onSetMusicState(stateSpec) && isMusicAppStarted) {
bufferMusicStateSpec = stateSpec; sendMusicStateToDevice(null, mediaManager.getBufferMusicStateSpec());
if (isMusicAppStarted) {
sendMusicStateToDevice(null, bufferMusicStateSpec);
}
} }
} }
@Override @Override
public void onSetMusicInfo(MusicSpec musicSpec) { public void onSetMusicInfo(final MusicSpec musicSpec) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); if (!getCoordinator().supportsMusicInfo()) {
if (!coordinator.supportsMusicInfo()) {
return; return;
} }
if (musicSpec != null && !musicSpec.equals(bufferMusicSpec)) { if (mediaManager.onSetMusicInfo(musicSpec) && isMusicAppStarted) {
bufferMusicSpec = musicSpec; sendMusicStateToDevice(mediaManager.getBufferMusicSpec(), mediaManager.getBufferMusicStateSpec());
if (bufferMusicStateSpec != null) {
bufferMusicStateSpec.state = 0;
bufferMusicStateSpec.position = 0;
}
if (isMusicAppStarted) {
sendMusicStateToDevice(bufferMusicSpec, bufferMusicStateSpec);
}
} }
} }
protected void onMusicAppOpen() { public void onMusicAppOpen() {
LOG.info("Music app started"); LOG.info("Music app started");
isMusicAppStarted = true; isMusicAppStarted = true;
sendMusicStateToDevice(); sendMusicStateDelayed();
sendVolumeStateToDevice();
} }
protected void onMusicAppClosed() { public void onMusicAppClosed() {
LOG.info("Music app terminated"); LOG.info("Music app terminated");
isMusicAppStarted = false; 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 @Override
@ -1406,20 +1411,6 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
LOG.info("sendVolumeStateToDevice: {}", volume); 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) { protected void sendMusicStateToDevice(final MusicSpec musicSpec, final MusicStateSpec musicStateSpec) {
if (characteristicChunked == null) { if (characteristicChunked == null) {
return; return;
@ -1431,7 +1422,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
try { try {
TransactionBuilder builder = performInitialized("send playback info"); TransactionBuilder builder = performInitialized("send playback info");
writeToChunked(builder, 3, encodeMusicState(musicSpec, musicStateSpec, false)); writeToChunked(builder, 3, encodeMusicState(getContext(), musicSpec, musicStateSpec, false));
builder.queue(getQueue()); builder.queue(getQueue());
} catch (IOException e) { } catch (IOException e) {
LOG.error("Unable to send playback state"); LOG.error("Unable to send playback state");
@ -1439,7 +1430,10 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
LOG.info("sendMusicStateToDevice: {}, {}", musicSpec, musicStateSpec); 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 artist = "";
String album = ""; String album = "";
String track = ""; String track = "";
@ -1518,7 +1512,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
} }
if (includeVolume) { if (includeVolume) {
buf.put((byte) getPhoneVolume()); buf.put((byte) MediaManager.getPhoneVolume(context));
} }
return buf.array(); return buf.array();

View File

@ -51,9 +51,8 @@ public class AmazfitCorSupport extends AmazfitBipSupport {
@Override @Override
public void onSetMusicState(MusicStateSpec stateSpec) { public void onSetMusicState(MusicStateSpec stateSpec) {
if (stateSpec != null && !stateSpec.equals(bufferMusicStateSpec)) { if (mediaManager.onSetMusicState(stateSpec)) {
sendMusicStateToDevice(null, stateSpec); sendMusicStateToDevice(null, mediaManager.getBufferMusicStateSpec());
bufferMusicStateSpec = stateSpec;
} }
} }

View File

@ -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 <http://www.gnu.org/licenses/>. */
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<MediaController> 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));
}
}