mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Garmin: PRG upload and basic app management
This commit is contained in:
+18
@@ -65,6 +65,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
|
||||
private TextView fwAppInstallTextView;
|
||||
private ImageView previewImage;
|
||||
private Button installButton;
|
||||
private Button closeButton;
|
||||
private Uri uri;
|
||||
private GBDevice device;
|
||||
private InstallHandler installHandler;
|
||||
@@ -121,6 +122,10 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
|
||||
String message = intent.getStringExtra(GB.DISPLAY_MESSAGE_MESSAGE);
|
||||
int severity = intent.getIntExtra(GB.DISPLAY_MESSAGE_SEVERITY, GB.INFO);
|
||||
addMessage(message, severity);
|
||||
} else if (GB.ACTION_SET_FINISHED.equals(action)) {
|
||||
setProgressBarVisibility(false);
|
||||
setInstallEnabled(false);
|
||||
setCloseEnabled(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -185,6 +190,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
|
||||
fwAppInstallTextView = findViewById(R.id.infoTextView);
|
||||
previewImage = findViewById(R.id.previewImage);
|
||||
installButton = findViewById(R.id.installButton);
|
||||
closeButton = findViewById(R.id.closeButton);
|
||||
progressBar = findViewById(R.id.installProgressBar);
|
||||
progressText = findViewById(R.id.installProgressText);
|
||||
detailsListView = findViewById(R.id.detailsListView);
|
||||
@@ -199,6 +205,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
|
||||
filter.addAction(GB.ACTION_SET_PROGRESS_BAR);
|
||||
filter.addAction(GB.ACTION_SET_PROGRESS_TEXT);
|
||||
filter.addAction(GB.ACTION_SET_INFO_TEXT);
|
||||
filter.addAction(GB.ACTION_SET_FINISHED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter);
|
||||
|
||||
installButton.setOnClickListener(new View.OnClickListener() {
|
||||
@@ -210,6 +217,8 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
|
||||
}
|
||||
});
|
||||
|
||||
closeButton.setOnClickListener(v -> finish());
|
||||
|
||||
uri = getIntent().getData();
|
||||
if (uri == null) { // For "share" intent
|
||||
uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
@@ -329,6 +338,15 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
|
||||
boolean enabled = device != null && device.isConnected() && enable;
|
||||
installButton.setEnabled(enabled);
|
||||
installButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
||||
if (enabled) {
|
||||
setCloseEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCloseEnabled(boolean enable) {
|
||||
closeButton.setEnabled(enable);
|
||||
closeButton.setVisibility(enable ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,6 +31,8 @@ public interface InstallActivity {
|
||||
|
||||
void setInstallEnabled(boolean enable);
|
||||
|
||||
void setCloseEnabled(boolean enable);
|
||||
|
||||
void clearInstallItems();
|
||||
|
||||
void setInstallItem(ItemWithDetails item);
|
||||
|
||||
+8
-8
@@ -92,20 +92,20 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
|
||||
enabledTabsList.add("watchfaces");
|
||||
}
|
||||
|
||||
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
|
||||
assert fab != null;
|
||||
fab.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
FloatingActionButton fab = findViewById(R.id.fab);
|
||||
if (coordinator.supportsFlashing()) {
|
||||
fab.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, READ_REQUEST_CODE);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
fab.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
ViewPager viewPager = (ViewPager) findViewById(R.id.appmanager_pager);
|
||||
ViewPager viewPager = findViewById(R.id.appmanager_pager);
|
||||
if (viewPager != null) {
|
||||
viewPager.setAdapter(getPagerAdapter());
|
||||
}
|
||||
|
||||
+29
-1
@@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -16,6 +17,7 @@ import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
|
||||
@@ -66,7 +68,7 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
deleteAllActivityData(device, session);
|
||||
}
|
||||
|
||||
public void deleteAllActivityData(@NonNull final Device device, @NonNull final DaoSession session) throws GBException {
|
||||
public void deleteAllActivityData(@NonNull final Device device, @NonNull final DaoSession session) {
|
||||
final Long deviceId = device.getId();
|
||||
|
||||
final Map<AbstractDao<?, ?>, Property> daoMap = new HashMap<AbstractDao<?, ?>, Property>() {{
|
||||
@@ -404,6 +406,28 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement(final GBDevice device) {
|
||||
// FIXME: disabled until better polished
|
||||
//return supports(device, GarminCapability.CONNECTIQ_APP_MANAGEMENT);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCachedAppManagement(final GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
return AppManagerActivity.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppListFetching() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean supportsAgpsUpdates(final GBDevice device) {
|
||||
return !getPrefs(device).getString(GarminPreferences.PREF_AGPS_KNOWN_URLS, "").isEmpty();
|
||||
}
|
||||
@@ -429,6 +453,10 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
if (garminGpxRouteInstallHandler.isValid())
|
||||
return garminGpxRouteInstallHandler;
|
||||
|
||||
final GarminPrgFileInstallHandler prgFileInstallHandler = new GarminPrgFileInstallHandler(uri, context);
|
||||
if (prgFileInstallHandler.isValid())
|
||||
return prgFileInstallHandler;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminPrgFile;
|
||||
|
||||
public class GarminPrgFileInstallHandler implements InstallHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GarminPrgFileInstallHandler.class);
|
||||
|
||||
private final Context mContext;
|
||||
private final GarminPrgFile prgFile;
|
||||
|
||||
public GarminPrgFileInstallHandler(final Uri uri, final Context context) {
|
||||
this.mContext = context;
|
||||
this.prgFile = new GarminPrgFile(uri, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return prgFile.isValid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateInstallation(final InstallActivity installActivity, final GBDevice device) {
|
||||
if (device.isBusy()) {
|
||||
installActivity.setInfoText(device.getBusyTask());
|
||||
installActivity.setInstallEnabled(false);
|
||||
installActivity.setCloseEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
if (!(coordinator instanceof GarminCoordinator)) {
|
||||
LOG.warn("Coordinator is not a GarminCoordinator: {}", coordinator.getClass());
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
|
||||
installActivity.setInstallEnabled(false);
|
||||
installActivity.setCloseEnabled(true);
|
||||
return;
|
||||
}
|
||||
final GarminCoordinator garminCoordinator = (GarminCoordinator) coordinator;
|
||||
|
||||
// FIXME this might not be the correct capability
|
||||
if (!garminCoordinator.supports(device, GarminCapability.CONNECTIQ_APP_MANAGEMENT)) {
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
|
||||
installActivity.setInstallEnabled(false);
|
||||
installActivity.setCloseEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO parse prg and identify type and name
|
||||
final GenericItem fwItem = new GenericItem(mContext.getString(
|
||||
R.string.installhandler_firmware_name,
|
||||
mContext.getString(coordinator.getDeviceNameResource()),
|
||||
"PRG",
|
||||
prgFile.getName()
|
||||
));
|
||||
fwItem.setIcon(coordinator.getDefaultIconResource());
|
||||
|
||||
installActivity.setInfoText(mContext.getString(R.string.fw_upgrade_notice, "PRG"));
|
||||
installActivity.setInstallItem(fwItem);
|
||||
|
||||
if (!device.isInitialized()) {
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready));
|
||||
installActivity.setInstallEnabled(false);
|
||||
installActivity.setCloseEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
installActivity.setInstallEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartInstall(final GBDevice device) {
|
||||
}
|
||||
|
||||
public byte[] getRawBytes() {
|
||||
return prgFile.getBytes();
|
||||
}
|
||||
}
|
||||
+40
-1
@@ -1,6 +1,9 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -16,6 +19,8 @@ import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.FileDownloadedDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.CreateFileMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.DownloadRequestMessage;
|
||||
@@ -28,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.stat
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.FileTransferDataStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.UploadRequestStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class FileTransferHandler implements MessageHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileTransferHandler.class);
|
||||
@@ -188,12 +194,41 @@ public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYP
|
||||
}
|
||||
}
|
||||
|
||||
public static class Upload {
|
||||
private void updateUploadProgress(final int percentage) {
|
||||
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(GBApplication.getContext());
|
||||
|
||||
if (percentage < 0) {
|
||||
// Failure
|
||||
GB.updateInstallNotification(GBApplication.getContext().getString(R.string.installation_failed_), false, 100, GBApplication.getContext());
|
||||
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_INFO_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, ""));
|
||||
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, GBApplication.getContext().getString(R.string.installation_failed_)));
|
||||
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_FINISHED));
|
||||
} else if (percentage >= 100) {
|
||||
// Success
|
||||
GB.updateInstallNotification(GBApplication.getContext().getString(R.string.installation_successful), false, 100, GBApplication.getContext());
|
||||
|
||||
deviceSupport.getDevice().sendDeviceUpdateIntent(deviceSupport.getContext());
|
||||
|
||||
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_INFO_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, ""));
|
||||
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, GBApplication.getContext().getString(R.string.installation_successful)));
|
||||
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_FINISHED));
|
||||
} else {
|
||||
// In Progress
|
||||
GB.updateInstallNotification(GBApplication.getContext().getString(R.string.uploading), true, percentage, GBApplication.getContext());
|
||||
|
||||
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_INFO_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, ""));
|
||||
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, GBApplication.getContext().getString(R.string.uploading)));
|
||||
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_BAR).putExtra(GB.PROGRESS_BAR_PROGRESS, percentage));
|
||||
}
|
||||
}
|
||||
|
||||
public class Upload {
|
||||
private FileFragment currentlyUploading;
|
||||
|
||||
private UploadRequestMessage setCreateFileStatusMessage(CreateFileStatusMessage createFileStatusMessage) {
|
||||
if (createFileStatusMessage.canProceed()) {
|
||||
LOG.info("SENDING UPLOAD FILE");
|
||||
updateUploadProgress(0);
|
||||
return new UploadRequestMessage(createFileStatusMessage.getFileIndex(), currentlyUploading.getDataSize());
|
||||
} else {
|
||||
LOG.warn("Cannot proceed with upload");
|
||||
@@ -211,6 +246,7 @@ public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYP
|
||||
return currentlyUploading.take();
|
||||
} else {
|
||||
LOG.warn("Cannot proceed with upload");
|
||||
updateUploadProgress(-1);
|
||||
this.currentlyUploading = null;
|
||||
}
|
||||
return null;
|
||||
@@ -220,16 +256,19 @@ public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYP
|
||||
if (currentlyUploading.getDataSize() <= fileTransferDataStatusMessage.getDataOffset()) {
|
||||
this.currentlyUploading = null;
|
||||
LOG.info("SENDING SYNC COMPLETE!!!");
|
||||
updateUploadProgress(100);
|
||||
|
||||
return new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_COMPLETE, 0);
|
||||
} else {
|
||||
if (fileTransferDataStatusMessage.canProceed()) {
|
||||
LOG.info("SENDING NEXT CHUNK!!!");
|
||||
updateUploadProgress((100 * currentlyUploading.dataHolder.position()) / currentlyUploading.dataHolder.limit());
|
||||
if (fileTransferDataStatusMessage.getDataOffset() != currentlyUploading.dataHolder.position())
|
||||
throw new IllegalStateException("Received file transfer status with unaligned offset");
|
||||
return currentlyUploading.take();
|
||||
} else {
|
||||
LOG.warn("Cannot proceed with upload");
|
||||
updateUploadProgress(-1);
|
||||
this.currentlyUploading = null;
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -80,6 +80,7 @@ public class FileType {
|
||||
|
||||
// Other files
|
||||
DOWNLOAD_COURSE(255, 4),
|
||||
PRG(255, 17),
|
||||
ERROR_SHUTDOWN_REPORTS(255, 245),
|
||||
IQ_ERROR_REPORTS(255, 244),
|
||||
ULF_LOGS(255, 247),
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
|
||||
|
||||
public class GarminPrgFile {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GarminPrgFile.class);
|
||||
|
||||
// This does not match the known 4-byte headers for older files
|
||||
private static final byte[] MAGIC = new byte[]{(byte) 0xd0, 0x00, (byte) 0xd0};
|
||||
|
||||
private final Uri uri;
|
||||
private byte[] fw;
|
||||
private String name;
|
||||
private boolean valid;
|
||||
|
||||
public GarminPrgFile(final Uri uri, final Context context) {
|
||||
this.uri = uri;
|
||||
|
||||
final UriHelper uriHelper;
|
||||
try {
|
||||
uriHelper = UriHelper.get(uri, context);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to get uri helper for {}", uri, e);
|
||||
return;
|
||||
}
|
||||
|
||||
name = uriHelper.getFileName();
|
||||
|
||||
// Quick 3-byte check to avoid reading the entire file
|
||||
try (final InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
|
||||
final byte[] magic = new byte[3];
|
||||
if (in.read(magic) != magic.length) {
|
||||
LOG.error("Failed to read magic");
|
||||
return;
|
||||
}
|
||||
valid = Arrays.equals(magic, MAGIC);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to read bytes from {}", uri, e);
|
||||
return;
|
||||
}
|
||||
|
||||
final int maxExpectedFileSize = 1024 * 1024; // 1MB
|
||||
if (uriHelper.getFileSize() > maxExpectedFileSize) {
|
||||
LOG.warn("File size is larger than the maximum expected file size of {}", maxExpectedFileSize);
|
||||
return;
|
||||
}
|
||||
|
||||
try (final InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
|
||||
this.fw = FileUtils.readAll(in, maxExpectedFileSize);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to read bytes from {}", uri, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO parse bytes
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return fw;
|
||||
}
|
||||
}
|
||||
+99
@@ -20,6 +20,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
@@ -36,15 +37,18 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.PendingFileProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminFitFileInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminGpxRouteInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPrgFileInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
@@ -55,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiCore;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiDeviceStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiFindMyWatch;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiInstalledAppsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSettingsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmartProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
@@ -103,6 +108,8 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
private boolean mFirstConnect = false;
|
||||
private boolean isBusyFetching;
|
||||
|
||||
final Map<UUID, GdiInstalledAppsService.InstalledAppsService.InstalledApp> installedApps = new HashMap<>();
|
||||
|
||||
public GarminSupport() {
|
||||
super(LOG);
|
||||
addSupportedService(CommunicatorV1.UUID_SERVICE_GARMIN_GFDI_V0);
|
||||
@@ -358,6 +365,87 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
sendOutgoingMessage("delete notification " + id, notificationsHandler.onDeleteNotification(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInfoReq() {
|
||||
sendOutgoingMessage(
|
||||
"request apps",
|
||||
protocolBufferHandler.prepareProtobufRequest(
|
||||
GdiSmartProto.Smart.newBuilder().setInstalledAppsService(
|
||||
GdiInstalledAppsService.InstalledAppsService.newBuilder().setGetInstalledAppsRequest(
|
||||
GdiInstalledAppsService.InstalledAppsService.GetInstalledAppsRequest.newBuilder()
|
||||
.setAppType(GdiInstalledAppsService.InstalledAppsService.AppType.ALL)
|
||||
)
|
||||
).build()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppStart(final UUID uuid, final boolean start) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppDelete(final UUID uuid) {
|
||||
final GdiInstalledAppsService.InstalledAppsService.InstalledApp app = installedApps.get(uuid);
|
||||
|
||||
if (app == null) {
|
||||
LOG.warn("Unknown app {}", uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
sendOutgoingMessage(
|
||||
"delete app",
|
||||
protocolBufferHandler.prepareProtobufRequest(
|
||||
GdiSmartProto.Smart.newBuilder().setInstalledAppsService(
|
||||
GdiInstalledAppsService.InstalledAppsService.newBuilder().setDeleteAppRequest(
|
||||
GdiInstalledAppsService.InstalledAppsService.DeleteAppRequest.newBuilder()
|
||||
.setStoreAppId(app.getStoreAppId())
|
||||
.setAppType(app.getType())
|
||||
)
|
||||
).build()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void onAppListReceived(final List<GdiInstalledAppsService.InstalledAppsService.InstalledApp> apps) {
|
||||
installedApps.clear();
|
||||
|
||||
final List<GBDeviceApp> gbApps = new ArrayList<>(apps.size());
|
||||
|
||||
for (final GdiInstalledAppsService.InstalledAppsService.InstalledApp installedApp : apps) {
|
||||
GBDeviceApp.Type type;
|
||||
|
||||
switch (installedApp.getType()) {
|
||||
case WATCH_FACE:
|
||||
type = GBDeviceApp.Type.WATCHFACE;
|
||||
break;
|
||||
case DATA_FIELD:
|
||||
case ACTIVITY:
|
||||
type = GBDeviceApp.Type.APP_ACTIVITYTRACKER;
|
||||
break;
|
||||
default:
|
||||
// FIXME we set everything else as app generic otherwise they get filtered, add new types
|
||||
type = GBDeviceApp.Type.APP_GENERIC;
|
||||
}
|
||||
|
||||
final UUID uuid = UUID.nameUUIDFromBytes(installedApp.getStoreAppId().toByteArray());
|
||||
installedApps.put(uuid, installedApp);
|
||||
gbApps.add(new GBDeviceApp(
|
||||
uuid,
|
||||
installedApp.getName() + " (" + installedApp.getType() + ")",
|
||||
"",
|
||||
String.valueOf(installedApp.getVersion()),
|
||||
type
|
||||
));
|
||||
gbApps.sort(Comparator.comparing(GBDeviceApp::getName));
|
||||
}
|
||||
|
||||
final GBDeviceEventAppInfo appInfoCmd = new GBDeviceEventAppInfo();
|
||||
appInfoCmd.apps = gbApps.toArray(new GBDeviceApp[0]);
|
||||
evaluateGBDeviceEvent(appInfoCmd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(final ArrayList<WeatherSpec> weatherSpecs) { //todo: find the closest one relative to the requested lat/long
|
||||
sendWeatherConditions(weatherSpecs.get(0));
|
||||
@@ -857,6 +945,17 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
if (garminGpxRouteInstallHandler.isValid()) {
|
||||
communicator.sendMessage("upload course file", fileTransferHandler.initiateUpload(garminGpxRouteInstallHandler.getGpxRouteFileConverter().getConvertedFile().getOutgoingMessage(), FileType.FILETYPE.DOWNLOAD_COURSE).getOutgoingMessage());
|
||||
}
|
||||
|
||||
final GarminPrgFileInstallHandler prgFileInstallHandler = new GarminPrgFileInstallHandler(uri, getContext());
|
||||
if (prgFileInstallHandler.isValid()) {
|
||||
communicator.sendMessage(
|
||||
"upload prg file",
|
||||
fileTransferHandler.initiateUpload(
|
||||
prgFileInstallHandler.getRawBytes(),
|
||||
FileType.FILETYPE.PRG
|
||||
).getOutgoingMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+27
@@ -40,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiFindMyWatch;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiHttpService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiNotificationsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSettingsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiInstalledAppsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmartProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmsNotification;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.DataTransferHandler;
|
||||
@@ -139,6 +140,32 @@ public class ProtocolBufferHandler implements MessageHandler {
|
||||
if (smart.hasNotificationsService()) {
|
||||
return prepareProtobufResponse(processProtobufNotificationsServiceMessage(smart.getNotificationsService()), message.getRequestId());
|
||||
}
|
||||
if (smart.hasInstalledAppsService()) {
|
||||
final GdiInstalledAppsService.InstalledAppsService installedAppsService = smart.getInstalledAppsService();
|
||||
|
||||
if (installedAppsService.hasGetInstalledAppsResponse()) {
|
||||
processed = true;
|
||||
|
||||
final List<GdiInstalledAppsService.InstalledAppsService.InstalledApp> installedAppsList = installedAppsService
|
||||
.getGetInstalledAppsResponse()
|
||||
.getInstalledAppsList();
|
||||
|
||||
LOG.info("Got app list with {} apps", installedAppsList.size());
|
||||
|
||||
deviceSupport.onAppListReceived(installedAppsList);
|
||||
} else if (installedAppsService.hasDeleteAppResponse()) {
|
||||
processed = true;
|
||||
|
||||
final GdiInstalledAppsService.InstalledAppsService.DeleteAppResponse.Status status = installedAppsService.getDeleteAppResponse().getStatus();
|
||||
|
||||
LOG.info("Got app delete response, status = {}", status);
|
||||
|
||||
// Refresh app list
|
||||
if (status == GdiInstalledAppsService.InstalledAppsService.DeleteAppResponse.Status.OK) {
|
||||
deviceSupport.onAppInfoReq();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (processed) {
|
||||
message.setStatusMessage(new ProtobufStatusMessage(
|
||||
message.getMessageType(),
|
||||
|
||||
@@ -106,6 +106,7 @@ public class GB {
|
||||
public static final String PROGRESS_BAR_PROGRESS = "progress";
|
||||
public static final String ACTION_SET_PROGRESS_TEXT = "GB_Set_Progress_Text";
|
||||
public static final String ACTION_SET_INFO_TEXT = "GB_Set_Info_Text";
|
||||
public static final String ACTION_SET_FINISHED = "GB_Set_Finished";
|
||||
|
||||
private static boolean notificationChannelsCreated;
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
syntax = "proto2";
|
||||
|
||||
package garmin_vivomovehr;
|
||||
|
||||
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.garmin";
|
||||
|
||||
message InstalledAppsService {
|
||||
enum AppType {
|
||||
UNKNOWN_APP_TYPE = 0;
|
||||
WATCH_APP = 1;
|
||||
WIDGET = 2;
|
||||
WATCH_FACE = 3;
|
||||
DATA_FIELD = 4;
|
||||
ALL = 5;
|
||||
NONE = 6;
|
||||
AUDIO_CONTENT_PROVIDER = 7;
|
||||
ACTIVITY = 8;
|
||||
}
|
||||
|
||||
message InstalledApp {
|
||||
required bytes storeAppId = 1;
|
||||
required AppType type = 2;
|
||||
required string name = 3;
|
||||
required bool disabled = 4;
|
||||
optional uint32 version = 5;
|
||||
optional string fileName = 6;
|
||||
optional uint64 fileSize = 7;
|
||||
optional uint32 nativeAppId = 8;
|
||||
optional bool favorite = 9;
|
||||
}
|
||||
|
||||
optional GetInstalledAppsRequest getInstalledAppsRequest = 1;
|
||||
optional GetInstalledAppsResponse getInstalledAppsResponse = 2;
|
||||
optional DeleteAppRequest deleteAppRequest = 3;
|
||||
optional DeleteAppResponse deleteAppResponse = 4;
|
||||
|
||||
message GetInstalledAppsRequest {
|
||||
required AppType appType = 1;
|
||||
}
|
||||
|
||||
message GetInstalledAppsResponse {
|
||||
required uint64 availableSpace = 1;
|
||||
required uint64 availableSlots = 2;
|
||||
repeated InstalledApp installedApps = 3;
|
||||
}
|
||||
|
||||
message DeleteAppRequest {
|
||||
required bytes storeAppId = 1;
|
||||
required AppType appType = 2;
|
||||
}
|
||||
|
||||
message DeleteAppResponse {
|
||||
enum Status {
|
||||
UNKNOWN_STATUS = 0;
|
||||
OK = 1;
|
||||
FAILED_TO_DELETE = 2;
|
||||
}
|
||||
|
||||
required Status status = 1;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import "garmin/gdi_device_status.proto";
|
||||
import "garmin/gdi_find_my_watch.proto";
|
||||
import "garmin/gdi_core.proto";
|
||||
import "garmin/gdi_http_service.proto";
|
||||
import "garmin/gdi_installed_apps_service.proto";
|
||||
import "garmin/gdi_data_transfer_service.proto";
|
||||
import "garmin/gdi_sms_notification.proto";
|
||||
import "garmin/gdi_calendar_service.proto";
|
||||
@@ -17,6 +18,7 @@ import "garmin/gdi_notifications_service.proto";
|
||||
message Smart {
|
||||
optional CalendarService calendar_service = 1;
|
||||
optional HttpService http_service = 2;
|
||||
optional InstalledAppsService installed_apps_service = 3;
|
||||
optional DataTransferService data_transfer_service = 7;
|
||||
optional DeviceStatusService device_status_service = 8;
|
||||
optional FindMyWatchService find_my_watch_service = 12;
|
||||
@@ -25,39 +27,3 @@ message Smart {
|
||||
optional SettingsService settings_service = 42;
|
||||
optional NotificationsService notifications_service = 49;
|
||||
}
|
||||
|
||||
/*
|
||||
1: CALENDAR_EVENTS_SERVICE_FIELD_NUMBER
|
||||
2: CONNECT_IQ_HTTP_SERVICE_FIELD_NUMBER
|
||||
3: CONNECT_IQ_INSTALLED_APPS_SERVICE_FIELD_NUMBER
|
||||
4: CONNECT_IQ_APP_SETTINGS_SERVICE_FIELD_NUMBER
|
||||
5: INTERNATIONAL_GOLF_SERVICE_FIELD_NUMBER
|
||||
6: SWING_SENSOR_SERVICE_FIELD_NUMBER
|
||||
7: DATA_TRANSFER_SERVICE_FIELD_NUMBER
|
||||
8: DEVICE_STATUS_SERVICE_FIELD_NUMBER
|
||||
9: INCIDENT_DETECTION_SERVICE_FIELD_NUMBER
|
||||
10: AUDIO_PROMPTS_SERVICE_FIELD_NUMBER
|
||||
11: WIFI_SETUP_SERVICE_FIELD_NUMBER
|
||||
12: FIND_MY_WATCH_SERVICE_FIELD_NUMBER
|
||||
13: CORE_SERVICE_FIELD_NUMBER
|
||||
14: GROUP_LIVE_TRACK_SERVICE_FIELD_NUMBER
|
||||
15: EXPRESSPAY_COMMAND_SERVICE_FIELD_NUMBER
|
||||
16: SMS_NOTIFICATION_SERVICE_FIELD_NUMBER
|
||||
17: LIVE_TRACK_MESSAGING_SERVICE_FIELD_NUMBER
|
||||
18: INSTANT_INPUT_SERVICE_FIELD_NUMBER
|
||||
19: SPORT_PROFILE_SETUP_SERVICE_FIELD_NUMBER
|
||||
20: HSA_DATA_SERVICE_FIELD_NUMBER
|
||||
21: LIVE_TRACK_SERVICE_FIELD_NUMBER
|
||||
22: EXPLORE_SYNC_SERVICE_FIELD_NUMBER
|
||||
23: WAY_POINT_TRANSFER_SERVICE_FIELD_NUMBER
|
||||
24: DEVICE_MESSAGE_SERVICE_FIELD_NUMBER
|
||||
25: LTE_SERVICE_FIELD_NUMBER
|
||||
26: ANTI_THEFT_ALARM_SERVICE_FIELD_NUMBER
|
||||
27: CREDENTIALS_SERVICE_FIELD_NUMBER
|
||||
28: INREACH_TRACKING_SERVICE_FIELD_NUMBER
|
||||
29: INREACH_MESSAGING_SERVICE_FIELD_NUMBER
|
||||
30: EVENT_SHARING_FIELD_NUMBER
|
||||
31: GENERIC_ITEM_TRANSFER_SERVICE_FIELD_NUMBER
|
||||
32: INREACH_CONTACT_SYNC_SERVICE_FIELD_NUMBER
|
||||
33: HAND_CALIBRATION_SERVICE_FIELD_NUMBER
|
||||
*/
|
||||
|
||||
@@ -76,6 +76,22 @@
|
||||
android:enabled="false"
|
||||
android:text="@string/appinstaller_install" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/closeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="false"
|
||||
android:layout_alignParentStart="false"
|
||||
android:layout_alignParentTop="false"
|
||||
android:layout_alignWithParentIfMissing="false"
|
||||
android:layout_below="@+id/installProgressBar"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"
|
||||
android:enabled="false"
|
||||
android:visibility="gone"
|
||||
android:text="@string/ok" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/installProgressText"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -3290,6 +3290,7 @@
|
||||
<string name="pref_title_fossil_hr_nav_vibrate">Vibrate on new instruction</string>
|
||||
<string name="pref_summary_fossil_hr_nav_vibrate">Whether the watch should vibrate on every new or changed navigation instruction (only when the app is in the foreground)</string>
|
||||
<string name="notification_channel_connection_status_name">Connection Status</string>
|
||||
<string name="uploading">Uploading…</string>
|
||||
<string name="uploading_watchface">Uploading watchface…</string>
|
||||
<string name="uploadwatchfaceoperation_in_progress">Uploading watchface</string>
|
||||
<string name="uploadwatchfaceoperation_complete">Watchface installation completed</string>
|
||||
|
||||
Reference in New Issue
Block a user