mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-29 13:26:50 +01:00
Huami/Zepp OS: Improve music info stability
This commit is contained in:
parent
ac89b1df9d
commit
da5f91f05b
@ -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}.
|
||||
*/
|
||||
|
@ -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<Short, AbstractZeppOsService> mServiceMap = new LinkedHashMap<Short, AbstractZeppOsService>() {{
|
||||
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);
|
||||
|
@ -19,6 +19,7 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
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();
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user