Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java

621 lines
30 KiB
Java

package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.location.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCore;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDeviceStatus;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiFindMyWatch;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSettingsService;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmartProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.ICommunicator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v1.CommunicatorV1;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v2.CommunicatorV2;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.FileDownloadedDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.PredefinedLocalMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ConfigurationMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.DownloadRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MusicControlEntityUpdateMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetDeviceSettingsMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetFileFlagsMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SupportedFileTypesMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SystemEventMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationSubscriptionStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALLOW_HIGH_MTU;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_GARMIN_DEFAULT_REPLY_SUFFIX;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SEND_APP_NOTIFICATIONS;
public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommunicator.Callback {
private static final Logger LOG = LoggerFactory.getLogger(GarminSupport.class);
private final ProtocolBufferHandler protocolBufferHandler;
private final NotificationsHandler notificationsHandler;
private final FileTransferHandler fileTransferHandler;
private final Queue<FileTransferHandler.DirectoryEntry> filesToDownload;
private final List<MessageHandler> messageHandlers;
private ICommunicator communicator;
private MusicStateSpec musicStateSpec;
private Timer musicStateTimer;
private final List<FileType> supportedFileTypeList = new ArrayList<>();
public GarminSupport() {
super(LOG);
addSupportedService(CommunicatorV1.UUID_SERVICE_GARMIN_GFDI);
addSupportedService(CommunicatorV2.UUID_SERVICE_GARMIN_ML_GFDI);
protocolBufferHandler = new ProtocolBufferHandler(this);
fileTransferHandler = new FileTransferHandler(this);
filesToDownload = new LinkedList<>();
messageHandlers = new ArrayList<>();
notificationsHandler = new NotificationsHandler();
messageHandlers.add(fileTransferHandler);
messageHandlers.add(protocolBufferHandler);
messageHandlers.add(notificationsHandler);
}
@Override
public void dispose() {
LOG.info("Garmin dispose()");
GBLocationService.stop(getContext(), getDevice());
stopMusicTimer();
super.dispose();
}
private void stopMusicTimer() {
if (musicStateTimer != null) {
musicStateTimer.cancel();
musicStateTimer.purge();
musicStateTimer = null;
}
}
public void addFileToDownloadList(FileTransferHandler.DirectoryEntry directoryEntry) {
filesToDownload.add(directoryEntry);
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
if (getSupportedServices().contains(CommunicatorV2.UUID_SERVICE_GARMIN_ML_GFDI)) {
communicator = new CommunicatorV2(this);
} else if (getSupportedServices().contains(CommunicatorV1.UUID_SERVICE_GARMIN_GFDI)) {
communicator = new CommunicatorV1(this);
} else {
LOG.warn("Failed to find a known Garmin service");
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.NOT_CONNECTED, getContext()));
return builder;
}
if (getDevicePrefs().getBoolean(PREF_ALLOW_HIGH_MTU, true)) {
builder.requestMtu(515);
}
communicator.initializeDevice(builder);
return builder;
}
@Override
public void onMtuChanged(final BluetoothGatt gatt, final int mtu, final int status) {
if (mtu < 23) {
LOG.warn("Ignoring mtu of {}, too low", mtu);
return;
}
if (!getDevicePrefs().getBoolean(PREF_ALLOW_HIGH_MTU, true)) {
LOG.warn("Ignoring mtu change to {} - high mtu is disabled", mtu);
return;
}
communicator.onMtuChanged(mtu);
}
@Override
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
final UUID characteristicUUID = characteristic.getUuid();
if (super.onCharacteristicChanged(gatt, characteristic)) {
LOG.debug("Change of characteristic {} handled by parent", characteristicUUID);
return true;
}
return communicator.onCharacteristicChanged(gatt, characteristic);
}
@Override
public void onMessage(final byte[] message) {
if (null == message) {
return; //message is not complete yet TODO check before calling
}
// LOG.debug("COBS decoded MESSAGE: {}", GB.hexdump(message));
GFDIMessage parsedMessage = GFDIMessage.parseIncoming(message);
if (null == parsedMessage) {
return; //message cannot be handled
}
/*
the handler elaborates the followup message but might change the status message since it does
check the integrity of the incoming message payload. Hence we let the handlers elaborate the
incoming message, then we send the status message of the incoming message, then the response
and finally we send the followup.
*/
GFDIMessage followup = null;
for (MessageHandler han : messageHandlers) {
followup = han.handle(parsedMessage);
if (followup != null) {
break;
}
}
final List<GBDeviceEvent> events = parsedMessage.getGBDeviceEvent();
for (final GBDeviceEvent event : events) {
evaluateGBDeviceEvent(event);
}
communicator.sendMessage(parsedMessage.getAckBytestream()); //send status message
sendOutgoingMessage(parsedMessage); //send reply if any
sendOutgoingMessage(followup); //send followup message if any
if (parsedMessage instanceof ConfigurationMessage) { //the last forced message exchange
completeInitialization();
}
processDownloadQueue();
}
@Override
public void onSetCallState(CallSpec callSpec) {
LOG.info("INCOMING CALLSPEC: {}", callSpec.command);
sendOutgoingMessage(notificationsHandler.onSetCallState(callSpec));
}
@Override
public void evaluateGBDeviceEvent(GBDeviceEvent deviceEvent) {
if (deviceEvent instanceof WeatherRequestDeviceEvent) {
WeatherSpec weather = Weather.getInstance().getWeatherSpec();
if (weather != null) {
sendWeatherConditions(weather);
}
} else if (deviceEvent instanceof NotificationSubscriptionDeviceEvent) {
final boolean enable = ((NotificationSubscriptionDeviceEvent) deviceEvent).enable;
notificationsHandler.setEnabled(enable);
final NotificationSubscriptionStatusMessage.NotificationStatus finalStatus;
if (getDevicePrefs().getBoolean(PREF_SEND_APP_NOTIFICATIONS, true)) {
finalStatus = NotificationSubscriptionStatusMessage.NotificationStatus.ENABLED;
} else {
finalStatus = NotificationSubscriptionStatusMessage.NotificationStatus.DISABLED;
}
LOG.info("NOTIFICATIONS ARE NOW enabled={}, status={}", enable, finalStatus);
sendOutgoingMessage(new NotificationSubscriptionStatusMessage(
GFDIMessage.Status.ACK,
finalStatus,
enable,
0
));
} else if (deviceEvent instanceof SupportedFileTypesDeviceEvent) {
this.supportedFileTypeList.clear();
this.supportedFileTypeList.addAll(((SupportedFileTypesDeviceEvent) deviceEvent).getSupportedFileTypes());
} else if (deviceEvent instanceof FileDownloadedDeviceEvent) {
LOG.debug("FILE DOWNLOAD COMPLETE {}", ((FileDownloadedDeviceEvent) deviceEvent).directoryEntry.getFileName());
if (!getKeepActivityDataOnDevice()) // delete file from watch upon successful download
sendOutgoingMessage(new SetFileFlagsMessage(((FileDownloadedDeviceEvent) deviceEvent).directoryEntry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE));
}
super.evaluateGBDeviceEvent(deviceEvent);
}
private boolean getKeepActivityDataOnDevice() {
return getDevicePrefs().getBoolean("keep_activity_data_on_device", true); // TODO: change to default false once we are sure of the consequences
}
@Override
public void onFetchRecordedData(final int dataTypes) {
if (this.supportedFileTypeList.isEmpty()) {
LOG.warn("No known supported file types");
return;
}
// FIXME respect dataTypes?
sendOutgoingMessage(fileTransferHandler.initiateDownload());
}
@Override
public void onNotification(final NotificationSpec notificationSpec) {
sendOutgoingMessage(notificationsHandler.onNotification(notificationSpec));
}
@Override
public void onDeleteNotification(int id) {
sendOutgoingMessage(notificationsHandler.onDeleteNotification(id));
}
@Override
public void onSendWeather(final ArrayList<WeatherSpec> weatherSpecs) { //todo: find the closest one relative to the requested lat/long
sendWeatherConditions(weatherSpecs.get(0));
}
private void sendOutgoingMessage(GFDIMessage message) {
if (message == null)
return;
communicator.sendMessage(message.getOutgoingMessage());
}
private boolean supports(final GarminCapability capability) {
return getDevicePrefs().getStringSet(GarminPreferences.PREF_GARMIN_CAPABILITIES, Collections.emptySet())
.contains(capability.name());
}
private void sendWeatherConditions(WeatherSpec weather) {
if (!supports(GarminCapability.WEATHER_CONDITIONS)) {
// Device does not support sending weather as fit
return;
}
List<RecordData> weatherData = new ArrayList<>();
List<RecordDefinition> weatherDefinitions = new ArrayList<>(3);
weatherDefinitions.add(PredefinedLocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
weatherDefinitions.add(PredefinedLocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition());
weatherDefinitions.add(PredefinedLocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
sendOutgoingMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDefinitionMessage(weatherDefinitions));
try {
RecordData today = new RecordData(PredefinedLocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
today.setFieldByName("weather_report", 0); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
today.setFieldByName("timestamp", weather.timestamp);
today.setFieldByName("observed_at_time", weather.timestamp);
today.setFieldByName("temperature", weather.currentTemp);
today.setFieldByName("low_temperature", weather.todayMinTemp);
today.setFieldByName("high_temperature", weather.todayMaxTemp);
today.setFieldByName("condition", weather.currentConditionCode);
today.setFieldByName("wind_direction", weather.windDirection);
today.setFieldByName("precipitation_probability", weather.precipProbability);
today.setFieldByName("wind_speed", Math.round(weather.windSpeed));
today.setFieldByName("temperature_feels_like", weather.feelsLikeTemp);
today.setFieldByName("relative_humidity", weather.currentHumidity);
today.setFieldByName("observed_location_lat", weather.latitude);
today.setFieldByName("observed_location_long", weather.longitude);
today.setFieldByName("location", weather.location);
weatherData.add(today);
for (int hour = 0; hour <= 11; hour++) {
if (hour < weather.hourly.size()) {
WeatherSpec.Hourly hourly = weather.hourly.get(hour);
RecordData weatherHourlyForecast = new RecordData(PredefinedLocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition());
weatherHourlyForecast.setFieldByName("weather_report", 1); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
weatherHourlyForecast.setFieldByName("timestamp", hourly.timestamp);
weatherHourlyForecast.setFieldByName("temperature", hourly.temp);
weatherHourlyForecast.setFieldByName("condition", hourly.conditionCode);
weatherHourlyForecast.setFieldByName("wind_direction", hourly.windDirection);
weatherHourlyForecast.setFieldByName("wind_speed", Math.round(hourly.windSpeed));
weatherHourlyForecast.setFieldByName("precipitation_probability", hourly.precipProbability);
weatherHourlyForecast.setFieldByName("relative_humidity", hourly.humidity);
// weatherHourlyForecast.setFieldByName("dew_point", 0); // dew_point sint8
weatherHourlyForecast.setFieldByName("uv_index", hourly.uvIndex);
// weatherHourlyForecast.setFieldByName("air_quality", 0); // air_quality enum
weatherData.add(weatherHourlyForecast);
}
}
//
RecordData todayDailyForecast = new RecordData(PredefinedLocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
todayDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
todayDailyForecast.setFieldByName("timestamp", weather.timestamp);
todayDailyForecast.setFieldByName("low_temperature", weather.todayMinTemp);
todayDailyForecast.setFieldByName("high_temperature", weather.todayMaxTemp);
todayDailyForecast.setFieldByName("condition", weather.currentConditionCode);
todayDailyForecast.setFieldByName("precipitation_probability", weather.precipProbability);
todayDailyForecast.setFieldByName("day_of_week", weather.timestamp);
weatherData.add(todayDailyForecast);
for (int day = 0; day < 4; day++) {
if (day < weather.forecasts.size()) {
WeatherSpec.Daily daily = weather.forecasts.get(day);
int ts = weather.timestamp + (day + 1) * 24 * 60 * 60; //TODO: is this needed?
RecordData weatherDailyForecast = new RecordData(PredefinedLocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
weatherDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
weatherDailyForecast.setFieldByName("timestamp", weather.timestamp);
weatherDailyForecast.setFieldByName("low_temperature", daily.minTemp);
weatherDailyForecast.setFieldByName("high_temperature", daily.maxTemp);
weatherDailyForecast.setFieldByName("condition", daily.conditionCode);
weatherDailyForecast.setFieldByName("precipitation_probability", daily.precipProbability);
weatherDailyForecast.setFieldByName("day_of_week", ts);
weatherData.add(weatherDailyForecast);
}
}
sendOutgoingMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDataMessage(weatherData));
} catch (Exception e) {
LOG.error(e.getMessage());
}
}
private void completeInitialization() {
onSetTime();
enableWeather();
//following is needed for vivomove style
sendOutgoingMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_READY, 0));
enableBatteryLevelUpdate();
gbDevice.setState(GBDevice.State.INITIALIZED);
gbDevice.sendDeviceUpdateIntent(getContext());
sendOutgoingMessage(new SupportedFileTypesMessage());
sendOutgoingMessage(toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
}
private ProtobufMessage toggleDefaultReplySuffix(boolean value) {
final GdiSettingsService.SettingsService.Builder enableSignature = GdiSettingsService.SettingsService.newBuilder()
.setChangeRequest(
GdiSettingsService.ChangeRequest.newBuilder()
.setPointer1(65566) //TODO: this might be device specific, tested on Instinct 2s
.setPointer2(3) //TODO: this might be device specific, tested on Instinct 2s
.setEnable(GdiSettingsService.ChangeRequest.Switch.newBuilder().setValue(value)));
return protocolBufferHandler.prepareProtobufRequest(
GdiSmartProto.Smart.newBuilder()
.setSettingsService(enableSignature).build());
}
@Override
public void onSendConfiguration(String config) {
switch (config) {
case PREF_GARMIN_DEFAULT_REPLY_SUFFIX:
sendOutgoingMessage(toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
break;
case PREF_SEND_APP_NOTIFICATIONS:
NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent();
notificationSubscriptionDeviceEvent.enable = true; // actual status is fetched from preferences
evaluateGBDeviceEvent(notificationSubscriptionDeviceEvent);
break;
}
}
private void processDownloadQueue() {
if (!filesToDownload.isEmpty() && !fileTransferHandler.isDownloading()) {
if (!gbDevice.isBusy()) {
GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data), "", true, 0, getContext());
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data));
getDevice().sendDeviceUpdateIntent(getContext());
}
try {
FileTransferHandler.DirectoryEntry directoryEntry = filesToDownload.remove();
while (checkFileExists(directoryEntry.getFileName())) {
LOG.debug("File: {} already downloaded, not downloading again.", directoryEntry.getFileName());
if (!getKeepActivityDataOnDevice()) // delete file from watch if already downloaded
sendOutgoingMessage(new SetFileFlagsMessage(directoryEntry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE));
directoryEntry = filesToDownload.remove();
}
DownloadRequestMessage downloadRequestMessage = fileTransferHandler.downloadDirectoryEntry(directoryEntry);
if (downloadRequestMessage != null) {
sendOutgoingMessage(downloadRequestMessage);
} else {
LOG.debug("File: {} already downloaded, not downloading again, from inside.", directoryEntry.getFileName());
}
} catch (NoSuchElementException e) {
// we ran out of files to download
// FIXME this is ugly
if (gbDevice.isBusy() && gbDevice.getBusyTask().equals(getContext().getString(R.string.busy_task_fetch_activity_data))) {
getDevice().unsetBusyTask();
GB.updateTransferNotification(null, "", false, 100, getContext());
getDevice().sendDeviceUpdateIntent(getContext());
}
}
} else if (filesToDownload.isEmpty() && !fileTransferHandler.isDownloading()) {
if (gbDevice.isBusy() && gbDevice.getBusyTask().equals(getContext().getString(R.string.busy_task_fetch_activity_data))) {
getDevice().unsetBusyTask();
GB.updateTransferNotification(null, "", false, 100, getContext());
getDevice().sendDeviceUpdateIntent(getContext());
}
}
}
private void enableBatteryLevelUpdate() {
final ProtobufMessage batteryLevelProtobufRequest = protocolBufferHandler.prepareProtobufRequest(GdiSmartProto.Smart.newBuilder()
.setDeviceStatusService(
GdiDeviceStatus.DeviceStatusService.newBuilder()
.setRemoteDeviceBatteryStatusRequest(
GdiDeviceStatus.DeviceStatusService.RemoteDeviceBatteryStatusRequest.newBuilder()
)
)
.build());
sendOutgoingMessage(batteryLevelProtobufRequest);
}
private void enableWeather() {
final Map<SetDeviceSettingsMessage.GarminDeviceSetting, Object> settings = new LinkedHashMap<>(3);
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.AUTO_UPLOAD_ENABLED, false);
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_CONDITIONS_ENABLED, true);
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_ALERTS_ENABLED, false);
sendOutgoingMessage(new SetDeviceSettingsMessage(settings));
}
@Override
public void onSetTime() {
sendOutgoingMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.TIME_UPDATED, 0));
}
@Override
public void onFindDevice(boolean start) {
final GdiFindMyWatch.FindMyWatchService.Builder a = GdiFindMyWatch.FindMyWatchService.newBuilder();
if (start) {
a.setFindRequest(
GdiFindMyWatch.FindMyWatchService.FindMyWatchRequest.newBuilder()
.setTimeout(60)
);
} else {
a.setCancelRequest(
GdiFindMyWatch.FindMyWatchService.FindMyWatchCancelRequest.newBuilder()
);
}
final ProtobufMessage findMyWatch = protocolBufferHandler.prepareProtobufRequest(
GdiSmartProto.Smart.newBuilder()
.setFindMyWatchService(a).build());
sendOutgoingMessage(findMyWatch);
}
@Override
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
sendOutgoingMessage(protocolBufferHandler.setCannedMessages(cannedMessagesSpec));
}
@Override
public void onSetMusicInfo(MusicSpec musicSpec) {
Map<MusicControlEntityUpdateMessage.MusicEntity, String> attributes = new HashMap<>();
attributes.put(MusicControlEntityUpdateMessage.TRACK.ARTIST, musicSpec.artist);
attributes.put(MusicControlEntityUpdateMessage.TRACK.ALBUM, musicSpec.album);
attributes.put(MusicControlEntityUpdateMessage.TRACK.TITLE, musicSpec.track);
attributes.put(MusicControlEntityUpdateMessage.TRACK.DURATION, String.valueOf(musicSpec.duration));
sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
}
@Override
public void onSetMusicState(MusicStateSpec stateSpec) {
musicStateSpec = stateSpec;
stopMusicTimer();
musicStateTimer = new Timer();
int updatePeriod = 4000; //milliseconds
LOG.debug("onSetMusicState: {}", stateSpec.toString());
if (stateSpec.state == MusicStateSpec.STATE_PLAYING) {
musicStateTimer.schedule(new TimerTask() {
@Override
public void run() {
String playing = "1";
String playRate = "1.0";
String position = new DecimalFormat("#.000").format(musicStateSpec.position);
musicStateSpec.position += updatePeriod / 1000;
Map<MusicControlEntityUpdateMessage.MusicEntity, String> attributes = new HashMap<>();
attributes.put(MusicControlEntityUpdateMessage.PLAYER.PLAYBACK_INFO, StringUtils.join(",", playing, playRate, position).toString());
sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
}
}, 0, updatePeriod);
} else {
String playing = "0";
String playRate = "0.0";
String position = new DecimalFormat("#.###").format(stateSpec.position);
Map<MusicControlEntityUpdateMessage.MusicEntity, String> attributes = new HashMap<>();
attributes.put(MusicControlEntityUpdateMessage.PLAYER.PLAYBACK_INFO, StringUtils.join(",", playing, playRate, position).toString());
sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
}
}
private boolean checkFileExists(String fileName) {
File dir;
try {
dir = getWritableExportDirectory();
File outputFile = new File(dir, fileName);
if (outputFile.exists()) //do not download again already downloaded file
return true;
} catch (IOException e) {
LOG.error("IOException: " + e);
}
return false;
}
public File getWritableExportDirectory() throws IOException {
File dir;
dir = new File(FileUtils.getExternalFilesDir() + "/" + FileUtils.makeValidFileName(getDevice().getName() + "_" + getDevice().getAddress()));
if (!dir.isDirectory()) {
if (!dir.mkdir()) {
throw new IOException("Cannot create device specific directory for " + getDevice().getName());
}
}
return dir;
}
@Override
public void onSetGpsLocation(final Location location) {
final GdiCore.CoreService.LocationUpdatedNotification.Builder locationUpdatedNotification = GdiCore.CoreService.LocationUpdatedNotification.newBuilder()
.addLocationData(
GarminUtils.toLocationData(location, GdiCore.CoreService.DataType.REALTIME_TRACKING)
);
final ProtobufMessage locationUpdatedNotificationRequest = protocolBufferHandler.prepareProtobufRequest(
GdiSmartProto.Smart.newBuilder().setCoreService(
GdiCore.CoreService.newBuilder().setLocationUpdatedNotification(locationUpdatedNotification)
).build()
);
sendOutgoingMessage(locationUpdatedNotificationRequest);
}
}