diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsHandler.java index 8d3812fe9..ae6d31ac2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsHandler.java @@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings; import android.content.Context; +import androidx.activity.result.ActivityResultCaller; import androidx.annotation.NonNull; import androidx.preference.Preference; @@ -27,7 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; * A device-specific preference handler, that allows for {@link nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator}s to register * their own preferences dynamically. */ -public interface DeviceSpecificSettingsHandler { +public interface DeviceSpecificSettingsHandler extends ActivityResultCaller { /** * Finds a preference with the given key. Returns null if the preference is not found. * diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminAgpsInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminAgpsInstallHandler.java deleted file mode 100644 index add459584..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminAgpsInstallHandler.java +++ /dev/null @@ -1,122 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.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 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.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps.GarminAgpsFile; -import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; -import nodomain.freeyourgadget.gadgetbridge.util.UriHelper; - -public class GarminAgpsInstallHandler implements InstallHandler { - private static final Logger LOG = LoggerFactory.getLogger(GarminAgpsInstallHandler.class); - - protected final Context mContext; - private GarminAgpsFile file; - - public GarminAgpsInstallHandler(final Uri uri, final Context context) { - this.mContext = context; - - final UriHelper uriHelper; - try { - uriHelper = UriHelper.get(uri, context); - } catch (final IOException e) { - LOG.error("Failed to get uri", e); - return; - } - - try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) { - final byte[] rawBytes = FileUtils.readAll(in, 1024 * 1024); // 1MB, they're usually ~60KB - final GarminAgpsFile agpsFile = new GarminAgpsFile(rawBytes); - if (agpsFile.isValid()) { - this.file = agpsFile; - } - } catch (final Exception e) { - LOG.error("Failed to read file", e); - } - } - - @Override - public boolean isValid() { - return file != null; - } - - @Override - public void validateInstallation(final InstallActivity installActivity, final GBDevice device) { - if (device.isBusy()) { - installActivity.setInfoText(device.getBusyTask()); - installActivity.setInstallEnabled(false); - 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); - return; - } - final GarminCoordinator garminCoordinator = (GarminCoordinator) coordinator; - if (!garminCoordinator.supportsAgpsUpdates()) { - installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported)); - installActivity.setInstallEnabled(false); - return; - } - - if (!device.isInitialized()) { - installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready)); - installActivity.setInstallEnabled(false); - return; - } - - final GenericItem fwItem = createInstallItem(device); - fwItem.setIcon(coordinator.getDefaultIconResource()); - - if (file == null) { - fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version)); - installActivity.setInfoText(mContext.getString(R.string.fwinstaller_firmware_not_compatible_to_device)); - installActivity.setInstallEnabled(false); - return; - } - - final StringBuilder builder = new StringBuilder(); - final String agpsBundle = mContext.getString(R.string.kind_agps_bundle); - builder.append(mContext.getString(R.string.fw_upgrade_notice, agpsBundle)); - builder.append("\n\n").append(mContext.getString(R.string.miband_firmware_unknown_warning)); - fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_untested_version)); - installActivity.setInfoText(builder.toString()); - installActivity.setInstallItem(fwItem); - installActivity.setInstallEnabled(true); - } - - @Override - public void onStartInstall(final GBDevice device) { - } - - public GarminAgpsFile getFile() { - return file; - } - - private GenericItem createInstallItem(final GBDevice device) { - DeviceCoordinator coordinator = device.getDeviceCoordinator(); - final String firmwareName = mContext.getString( - R.string.installhandler_firmware_name, - mContext.getString(coordinator.getDeviceNameResource()), - mContext.getString(R.string.kind_agps_bundle), - "" - ); - return new GenericItem(firmwareName); - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java index f6e4cebf7..ba5a87d85 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java @@ -1,8 +1,5 @@ package nodomain.freeyourgadget.gadgetbridge.devices.garmin; -import android.content.Context; -import android.net.Uri; - import androidx.annotation.NonNull; import java.util.List; @@ -14,7 +11,6 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; -import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao; @@ -114,7 +110,7 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { final List location = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.LOCATION); location.add(R.xml.devicesettings_workout_send_gps_to_band); - if (supportsAgpsUpdates()) { + if (supportsAgpsUpdates(device)) { location.add(R.xml.devicesettings_garmin_agps); } @@ -210,19 +206,7 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return true; } - @Override - public InstallHandler findInstallHandler(final Uri uri, final Context context) { - if (supportsAgpsUpdates()) { - final GarminAgpsInstallHandler agpsInstallHandler = new GarminAgpsInstallHandler(uri, context); - if (agpsInstallHandler.isValid()) { - return agpsInstallHandler; - } - } - - return null; - } - - public boolean supportsAgpsUpdates() { - return false; + public boolean supportsAgpsUpdates(final GBDevice device) { + return !getPrefs(device).getString(GarminPreferences.PREF_AGPS_KNOWN_URLS, "").isEmpty(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java index bc98d4f14..2fb53f5cb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java @@ -1,6 +1,25 @@ package nodomain.freeyourgadget.gadgetbridge.devices.garmin; +import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; + public class GarminPreferences { public static final String PREF_GARMIN_CAPABILITIES = "garmin_capabilities"; public static final String PREF_FEAT_CANNED_MESSAGES = "feat_canned_messages"; + public static final String PREF_AGPS_KNOWN_URLS = "garmin_agps_known_urls"; + public static final String PREF_GARMIN_AGPS_STATUS = "garmin_agps_status_%s"; + public static final String PREF_GARMIN_AGPS_UPDATE_TIME = "garmin_agps_update_time_%s"; + public static final String PREF_GARMIN_AGPS_FOLDER = "garmin_agps_folder"; + public static final String PREF_GARMIN_AGPS_FILENAME = "garmin_agps_filename_%s"; + + public static String agpsStatus(final String url) { + return String.format(GarminPreferences.PREF_GARMIN_AGPS_STATUS, CheckSums.md5(url)); + } + + public static String agpsUpdateTime(final String url) { + return String.format(GarminPreferences.PREF_GARMIN_AGPS_UPDATE_TIME, CheckSums.md5(url)); + } + + public static String agpsFilename(final String url) { + return String.format(GarminPreferences.PREF_GARMIN_AGPS_FILENAME, CheckSums.md5(url)); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSettingsCustomizer.java index 60fb01583..a0b69005e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSettingsCustomizer.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSettingsCustomizer.java @@ -1,47 +1,221 @@ package nodomain.freeyourgadget.gadgetbridge.devices.garmin; -import android.os.Parcel; +import static nodomain.freeyourgadget.gadgetbridge.util.GB.toast; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Parcel; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; +import androidx.documentfile.provider.DocumentFile; import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps.GarminAgpsStatus; +import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -public class GarminSettingsCustomizer implements DeviceSpecificSettingsCustomizer { +public class GarminSettingsCustomizer implements DeviceSpecificSettingsCustomizer { + private static final Logger LOG = LoggerFactory.getLogger(GarminSettingsCustomizer.class); + + private final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); @Override - public void onPreferenceChange(Preference preference, DeviceSpecificSettingsHandler handler) { + public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) { } @Override - public void customizeSettings(DeviceSpecificSettingsHandler handler, Prefs prefs) { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); - final Preference prefAgpsUpdateTime = handler.findPreference(DeviceSettingsPreferenceConst.PREF_AGPS_UPDATE_TIME); - if (prefAgpsUpdateTime != null) { - final long ts = prefs.getLong(DeviceSettingsPreferenceConst.PREF_AGPS_UPDATE_TIME, 0L); - if (ts > 0) { - prefAgpsUpdateTime.setSummary(sdf.format(new Date(ts))); - } else { - prefAgpsUpdateTime.setSummary(handler.getContext().getString(R.string.unknown)); + public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs) { + final PreferenceCategory prefAgpsHeader = handler.findPreference(DeviceSettingsPreferenceConst.PREF_HEADER_AGPS); + if (prefAgpsHeader != null) { + final List urls = prefs.getList(GarminPreferences.PREF_AGPS_KNOWN_URLS, Collections.emptyList(), "\n"); + if (urls.isEmpty()) { + return; + } + + final String currentFolder = prefs.getString(GarminPreferences.PREF_GARMIN_AGPS_FOLDER, ""); + + final Preference prefFolder = handler.findPreference(GarminPreferences.PREF_GARMIN_AGPS_FOLDER); + final ActivityResultLauncher agpsFolderChooser = handler.registerForActivityResult( + new ActivityResultContracts.OpenDocumentTree(), + localUri -> { + LOG.info("Garmin agps folder: {}", localUri); + if (localUri != null) { + handler.getContext().getContentResolver().takePersistableUriPermission(localUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + prefs.getPreferences().edit() + .putString(GarminPreferences.PREF_GARMIN_AGPS_FOLDER, localUri.toString()) + .apply(); + prefFolder.setSummary(localUri.toString()); + + for (final String url : urls) { + updateAgpsStatus(handler, prefs, url); + } + } + } + ); + prefFolder.setOnPreferenceClickListener(preference -> { + agpsFolderChooser.launch(null); + return true; + }); + prefFolder.setSummary(currentFolder); + prefAgpsHeader.addPreference(prefFolder); + + int i = 0; + for (final String url : urls) { + i++; + + final Preference prefHeader = new PreferenceCategory(handler.getContext()); + prefHeader.setKey("pref_agps_url_header_" + i); + prefHeader.setIconSpaceReserved(false); + prefHeader.setTitle(handler.getContext().getString(R.string.garmin_agps_url_i, i)); + prefAgpsHeader.addPreference(prefHeader); + + final Preference prefUrl = new Preference(handler.getContext()); + prefUrl.setOnPreferenceClickListener(preference -> { + final ClipboardManager clipboard = (ClipboardManager) handler.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + final ClipData clip = ClipData.newPlainText(handler.getContext().getString(R.string.url), url); + clipboard.setPrimaryClip(clip); + toast(handler.getContext(), handler.getContext().getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT, GB.INFO); + return true; + }); + prefUrl.setKey("pref_garmin_agps_url_" + i); + prefUrl.setIcon(R.drawable.ic_link); + prefUrl.setTitle(R.string.url); + prefUrl.setSummary(url); + prefAgpsHeader.addPreference(prefUrl); + + final Preference prefLocalFile = new Preference(handler.getContext()); + prefLocalFile.setOnPreferenceClickListener(preference -> { + selectAgpsFile(handler, prefs, url, prefLocalFile); + return true; + }); + prefLocalFile.setKey(GarminPreferences.agpsFilename(url)); + prefLocalFile.setIcon(R.drawable.ic_file_open); + prefLocalFile.setTitle(R.string.garmin_agps_local_file); + prefLocalFile.setSummary(prefs.getString(GarminPreferences.agpsFilename(url), "")); + prefAgpsHeader.addPreference(prefLocalFile); + + final Preference prefStatus = new Preference(handler.getContext()); + prefStatus.setKey(GarminPreferences.agpsStatus(url)); + prefStatus.setIcon(R.drawable.ic_health); + prefStatus.setTitle(R.string.status); + prefAgpsHeader.addPreference(prefStatus); + updateAgpsStatus(handler, prefs, url); + + final Preference prefUpdateTime = new Preference(handler.getContext()); + prefUpdateTime.setKey(GarminPreferences.agpsUpdateTime(url)); + prefUpdateTime.setIcon(R.drawable.ic_calendar_today); + prefUpdateTime.setTitle(R.string.pref_agps_update_time); + final long ts = prefs.getLong(GarminPreferences.agpsUpdateTime(url), 0L); + if (ts > 0) { + prefUpdateTime.setSummary(SDF.format(new Date(ts))); + } else { + prefUpdateTime.setSummary(handler.getContext().getString(R.string.unknown)); + } + prefAgpsHeader.addPreference(prefUpdateTime); + } + } + } + + private void selectAgpsFile(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final String url, final Preference prefLocalFile) { + final String currentFolder = prefs.getString(GarminPreferences.PREF_GARMIN_AGPS_FOLDER, ""); + + final String folderUri = prefs.getString(GarminPreferences.PREF_GARMIN_AGPS_FOLDER, ""); + if (folderUri.isEmpty()) { + GB.toast(handler.getContext().getString(R.string.no_folder_selected), Toast.LENGTH_SHORT, GB.INFO); + return; + } + + final DocumentFile folder = DocumentFile.fromTreeUri(handler.getContext(), Uri.parse(currentFolder)); + if (folder == null || folder.listFiles().length == 0) { + GB.toast(handler.getContext().getString(R.string.folder_is_empty), Toast.LENGTH_SHORT, GB.INFO); + return; + } + + final DocumentFile[] documentFiles = folder.listFiles(); + final String[] files = new String[documentFiles.length + 1]; + files[0] = handler.getContext().getString(R.string.none); + final String selectedFile = prefs.getString(GarminPreferences.agpsFilename(url), ""); + int checkedItem = 0; + for (int j = 0; j < documentFiles.length; j++) { + files[j + 1] = documentFiles[j].getName(); + if (selectedFile.equals(files[j + 1])) { + checkedItem = j + 1; } } - final Preference prefAgpsStatus = handler.findPreference(DeviceSettingsPreferenceConst.PREF_AGPS_STATUS); - if (prefAgpsStatus != null) { - final GarminAgpsStatus agpsStatus = GarminAgpsStatus.valueOf(prefs.getString(DeviceSettingsPreferenceConst.PREF_AGPS_STATUS, GarminAgpsStatus.MISSING.name())); - prefAgpsStatus.setSummary(handler.getContext().getString(agpsStatus.getText())); + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(handler.getContext()); + builder.setTitle(R.string.garmin_agps_local_file); + + final AtomicInteger selectedIdx = new AtomicInteger(0); + builder.setSingleChoiceItems(files, checkedItem, (dialog, which) -> { + selectedIdx.set(which); + }); + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + final String selectedFilename = selectedIdx.get() > 0 ? files[selectedIdx.get()] : null; + prefs.getPreferences().edit() + .putString(GarminPreferences.agpsFilename(url), selectedFilename) + .apply(); + prefLocalFile.setSummary(selectedFilename); + updateAgpsStatus(handler, prefs, url); + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.create().show(); + } + + private void updateAgpsStatus(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final String url) { + final Preference prefStatus = handler.findPreference(GarminPreferences.agpsStatus(url)); + + final String filename = prefs.getString(GarminPreferences.agpsFilename(url), ""); + if (filename.isEmpty()) { + prefStatus.setSummary(""); + return; } + final String folderUri = prefs.getString(GarminPreferences.PREF_GARMIN_AGPS_FOLDER, ""); + if (folderUri.isEmpty()) { + prefStatus.setSummary(""); + return; + } + final DocumentFile folder = DocumentFile.fromTreeUri(handler.getContext(), Uri.parse(folderUri)); + if (folder == null) { + prefStatus.setSummary(""); + return; + } + final GarminAgpsStatus agpsStatus; + final DocumentFile localFile = folder.findFile(filename); + if (localFile != null && localFile.isFile() && localFile.canRead()) { + if (localFile.lastModified() < prefs.getLong(GarminPreferences.agpsUpdateTime(url), 0L)) { + agpsStatus = GarminAgpsStatus.CURRENT; + } else { + agpsStatus = GarminAgpsStatus.PENDING; + } + } else { + agpsStatus = GarminAgpsStatus.MISSING; + } + prefStatus.setSummary(handler.getContext().getString(agpsStatus.getText())); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/forerunner245/GarminForerunner245Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/forerunner245/GarminForerunner245Coordinator.java index 2f96d12bc..499b14879 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/forerunner245/GarminForerunner245Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/forerunner245/GarminForerunner245Coordinator.java @@ -15,14 +15,4 @@ public class GarminForerunner245Coordinator extends GarminCoordinator { public int getDeviceNameResource() { return R.string.devicetype_garmin_forerunner_245; } - - @Override - public boolean supportsFlashing() { - return true; - } - - @Override - public boolean supportsAgpsUpdates() { - return true; - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2s/GarminInstinct2SCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2s/GarminInstinct2SCoordinator.java index 91564d795..ee656d92a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2s/GarminInstinct2SCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2s/GarminInstinct2SCoordinator.java @@ -15,14 +15,4 @@ public class GarminInstinct2SCoordinator extends GarminCoordinator { public int getDeviceNameResource() { return R.string.devicetype_garmin_instinct_2s; } - - @Override - public boolean supportsFlashing() { - return true; - } - - @Override - public boolean supportsAgpsUpdates() { - return true; - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2solar/GarminInstinct2SolarCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2solar/GarminInstinct2SolarCoordinator.java index 65a384741..fa754aa21 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2solar/GarminInstinct2SolarCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2solar/GarminInstinct2SolarCoordinator.java @@ -16,14 +16,4 @@ public class GarminInstinct2SolarCoordinator extends GarminCoordinator { public int getDeviceNameResource() { return R.string.devicetype_garmin_instinct_2_solar; } - - @Override - public boolean supportsFlashing() { - return true; - } - - @Override - public boolean supportsAgpsUpdates() { - return true; - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2soltac/GarminInstinct2SolTacCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2soltac/GarminInstinct2SolTacCoordinator.java index e75044c4f..15973ab05 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2soltac/GarminInstinct2SolTacCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2soltac/GarminInstinct2SolTacCoordinator.java @@ -15,14 +15,4 @@ public class GarminInstinct2SolTacCoordinator extends GarminCoordinator { public int getDeviceNameResource() { return R.string.devicetype_garmin_instinct_2_soltac; } - - @Override - public boolean supportsFlashing() { - return true; - } - - @Override - public boolean supportsAgpsUpdates() { - return true; - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivoactive4/GarminVivoActive4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivoactive4/GarminVivoActive4Coordinator.java index 607badc93..52c13612d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivoactive4/GarminVivoActive4Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivoactive4/GarminVivoActive4Coordinator.java @@ -15,14 +15,4 @@ public class GarminVivoActive4Coordinator extends GarminCoordinator { public int getDeviceNameResource() { return R.string.devicetype_garmin_vivoactive_4; } - - @Override - public boolean supportsFlashing() { - return true; - } - - @Override - public boolean supportsAgpsUpdates() { - return true; - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivoactive4s/GarminVivoActive4SCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivoactive4s/GarminVivoActive4SCoordinator.java index 2765db1f5..0e5a69ca8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivoactive4s/GarminVivoActive4SCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivoactive4s/GarminVivoActive4SCoordinator.java @@ -15,14 +15,4 @@ public class GarminVivoActive4SCoordinator extends GarminCoordinator { public int getDeviceNameResource() { return R.string.devicetype_garmin_vivoactive_4s; } - - @Override - public boolean supportsFlashing() { - return true; - } - - @Override - public boolean supportsAgpsUpdates() { - return true; - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java index 834108f2a..ae297bf34 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java @@ -6,11 +6,13 @@ import android.location.Location; import android.net.Uri; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.text.DecimalFormat; import java.util.ArrayList; @@ -29,12 +31,9 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; -import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; -import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; -import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminAgpsInstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability; @@ -57,7 +56,6 @@ 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.agps.GarminAgpsStatus; 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; @@ -79,11 +77,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetF 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.Optional; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; 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; @@ -652,28 +649,6 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni } } - @Override - public void onInstallApp(final Uri uri) { - final GarminAgpsInstallHandler agpsHandler = new GarminAgpsInstallHandler(uri, getContext()); - if (agpsHandler.isValid()) { - try { - // Write the AGPS update to a temporary file in cache, so we can load it when requested - final File agpsFile = getAgpsFile(); - try (FileOutputStream outputStream = new FileOutputStream(agpsFile)) { - outputStream.write(agpsHandler.getFile().getBytes()); - evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences( - DeviceSettingsPreferenceConst.PREF_AGPS_STATUS, GarminAgpsStatus.PENDING.name() - )); - LOG.info("AGPS file successfully written to the cache directory."); - } catch (final IOException e) { - LOG.error("Failed to write AGPS bytes to temporary directory", e); - } - } catch (final Exception e) { - GB.toast(getContext(), "AGPS install error: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e); - } - } - } - private boolean alreadyDownloaded(final FileTransferHandler.DirectoryEntry entry) { final Optional file = getFile(entry.getFileName()); if (file.isPresent()) { @@ -728,19 +703,36 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni sendOutgoingMessage(locationUpdatedNotificationRequest); } - public File getAgpsFile() throws IOException { - return new File(getAgpsCacheDirectory(), "CPE.BIN"); - } - - private File getAgpsCacheDirectory() throws IOException { - final File cacheDir = getContext().getCacheDir(); - final File agpsCacheDir = new File(cacheDir, "garmin-agps"); - if (agpsCacheDir.mkdir()) { - LOG.info("AGPS cache directory for Garmin devices successfully created."); - } else if (!agpsCacheDir.exists() || !agpsCacheDir.isDirectory()) { - throw new IOException("Cannot create/locate AGPS directory for Garmin devices."); + @Nullable + public DocumentFile getAgpsFile(final String url) { + final Prefs prefs = getDevicePrefs(); + final String filename = prefs.getString(GarminPreferences.agpsFilename(url), ""); + if (filename.isEmpty()) { + LOG.debug("agps file not configured for {}", url); + return null; } - return agpsCacheDir; + + final String folderUri = prefs.getString(GarminPreferences.PREF_GARMIN_AGPS_FOLDER, ""); + if (folderUri.isEmpty()) { + LOG.debug("agps folder not set"); + return null; + } + final DocumentFile folder = DocumentFile.fromTreeUri(getContext(), Uri.parse(folderUri)); + if (folder == null) { + LOG.warn("Failed to find agps folder on {}", folderUri); + return null; + } + + final DocumentFile localFile = folder.findFile(filename); + if (localFile == null) { + LOG.warn("Failed to find agps file '{}' for '{}' on '{}'", filename, url, folderUri); + return null; + } + if (!localFile.isFile()) { + LOG.warn("Local agps file {} for {} can't be read: isFile={} canRead={}", folderUri, url, localFile.isFile(), localFile.canRead()); + return null; + } + return localFile; } public GarminCoordinator getCoordinator() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/agps/AgpsHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/agps/AgpsHandler.java index 02fb58539..e9375711f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/agps/AgpsHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/agps/AgpsHandler.java @@ -1,22 +1,25 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps; +import androidx.documentfile.provider.DocumentFile; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.threeten.bp.Instant; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.Callable; -import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; +import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; -import nodomain.freeyourgadget.gadgetbridge.util.GBTarFile; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class AgpsHandler { private static final Logger LOG = LoggerFactory.getLogger(AgpsHandler.class); @@ -27,63 +30,74 @@ public class AgpsHandler { this.deviceSupport = deviceSupport; } - public byte[] handleAgpsRequest(final String path, final Map query) { + public byte[] handleAgpsRequest(final String url, final String path, final Map query) { + saveKnownUrl(url); + try { - if (!query.containsKey(QUERY_CONSTELLATIONS)) { - LOG.debug("Query does not contain information about constellations; skipping request."); + final DocumentFile agpsFile = deviceSupport.getAgpsFile(url); + if (agpsFile == null) { + LOG.warn("File with AGPS data for {} does not exist.", url); return null; } - final File agpsFile = deviceSupport.getAgpsFile(); - if (!agpsFile.exists() || !agpsFile.isFile()) { - LOG.info("File with AGPS data does not exist."); - return null; - } - try(InputStream agpsIn = new FileInputStream(agpsFile)) { + try (InputStream agpsIn = deviceSupport.getContext().getContentResolver().openInputStream(agpsFile.getUri())) { + if (agpsIn == null) { + LOG.error("Failed to open input stream for agps file {}", agpsFile.getUri()); + return null; + } + + // Run some sanity checks on known agps file formats final byte[] rawBytes = FileUtils.readAll(agpsIn, 1024 * 1024); // 1MB, they're usually ~60KB - final GBTarFile tarFile = new GBTarFile(rawBytes); - final String[] requestedConstellations = Objects.requireNonNull(query.get(QUERY_CONSTELLATIONS)).split(","); - for (final String constellation: requestedConstellations) { - try { - final GarminAgpsDataType garminAgpsDataType = GarminAgpsDataType.valueOf(constellation); - if (!tarFile.containsFile(garminAgpsDataType.getFileName())) { - LOG.error("AGPS archive is missing requested file: {}", garminAgpsDataType.getFileName()); - deviceSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences( - DeviceSettingsPreferenceConst.PREF_AGPS_STATUS, GarminAgpsStatus.ERROR.name() - )); - return null; - } - } catch (IllegalArgumentException e) { - LOG.error("Device requested unsupported AGPS data type: {}", constellation); - deviceSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences( - DeviceSettingsPreferenceConst.PREF_AGPS_STATUS, GarminAgpsStatus.ERROR.name() - )); + final GarminAgpsFile garminAgpsFile = new GarminAgpsFile(rawBytes); + if (query.containsKey(QUERY_CONSTELLATIONS)) { + final String[] requestedConstellations = Objects.requireNonNull(query.get(QUERY_CONSTELLATIONS)).split(","); + if (!garminAgpsFile.isValidTar(requestedConstellations)) { + reportError(url); return null; } + } else if (path.contains(("/rxnetworks/"))) { + if (!garminAgpsFile.isValidRxNetworks()) { + reportError(url); + return null; + } + } else { + LOG.warn("Refusing to send agps for unknown url"); + return null; } - LOG.info("Sending new AGPS data to the device."); + + LOG.info("Sending new AGPS data to the device from {}", agpsFile.getUri()); return rawBytes; } - } catch (IOException e) { - LOG.error("Unable to obtain AGPS data.", e); - deviceSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences( - DeviceSettingsPreferenceConst.PREF_AGPS_STATUS, GarminAgpsStatus.ERROR.name() - )); + } catch (final IOException e) { + LOG.error("Unable to obtain AGPS data", e); + reportError(url); return null; } } - public Callable getOnDataSuccessfullySentListener() { + public void saveKnownUrl(final String url) { + final Prefs devicePrefs = deviceSupport.getDevicePrefs(); + final List knownAgpsUrls = new ArrayList<>(devicePrefs.getList(GarminPreferences.PREF_AGPS_KNOWN_URLS, Collections.emptyList(), "\n")); + if (!knownAgpsUrls.contains(url)) { + knownAgpsUrls.add(url); + devicePrefs.getPreferences().edit() + .putString(GarminPreferences.PREF_AGPS_KNOWN_URLS, String.join("\n", knownAgpsUrls)) + .apply(); + } + } + + private void reportError(final String url) { + deviceSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences( + GarminPreferences.agpsStatus(url), GarminAgpsStatus.ERROR.name() + )); + } + + public Callable getOnDataSuccessfullySentListener(final String urlString) { return () -> { LOG.info("AGPS data successfully sent to the device."); - deviceSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences( - DeviceSettingsPreferenceConst.PREF_AGPS_UPDATE_TIME, Instant.now().toEpochMilli() - )); - deviceSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences( - DeviceSettingsPreferenceConst.PREF_AGPS_STATUS, GarminAgpsStatus.CURRENT.name() - )); - if (deviceSupport.getAgpsFile().delete()) { - LOG.info("AGPS data was deleted from the cache folder."); - } + deviceSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences() + .withPreference(GarminPreferences.agpsStatus(urlString), GarminAgpsStatus.CURRENT.name()) + .withPreference(GarminPreferences.agpsUpdateTime(urlString), Instant.now().toEpochMilli()) + ); return null; }; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/agps/GarminAgpsFile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/agps/GarminAgpsFile.java index 67923895f..48a8a5291 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/agps/GarminAgpsFile.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/agps/GarminAgpsFile.java @@ -3,27 +3,42 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.zip.GZIPInputStream; + +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GBTarFile; public class GarminAgpsFile { private static final Logger LOG = LoggerFactory.getLogger(GarminAgpsFile.class); - private final byte[] tarBytes; + private final byte[] bytes; - public GarminAgpsFile(final byte[] tarBytes) { - this.tarBytes = tarBytes; + public static final byte[] GZ_HEADER = new byte[]{(byte) 0x1f, (byte) 0x8b}; + public static final byte[] CPE_RXNETWORKS_HEADER = new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x66}; + + public GarminAgpsFile(final byte[] bytes) { + this.bytes = bytes; } - public boolean isValid() { - if (!GBTarFile.isTarFile(tarBytes)) { - LOG.debug("Is not TAR file!"); + public boolean isValidTar(final String[] constellations) { + if (!GBTarFile.isTarFile(bytes)) { return false; } - final GBTarFile tarFile = new GBTarFile(tarBytes); - for (final String fileName: tarFile.listFileNames()) { - if (!GarminAgpsDataType.isValidAgpsDataFileName(fileName)) { - LOG.error("Unknown file in TAR archive: {}", fileName); + final GBTarFile tarFile = new GBTarFile(bytes); + for (final String constellation : constellations) { + try { + final GarminAgpsDataType garminAgpsDataType = GarminAgpsDataType.valueOf(constellation); + if (!tarFile.containsFile(garminAgpsDataType.getFileName())) { + LOG.error("AGPS archive is missing requested file: {}", garminAgpsDataType.getFileName()); + return false; + } + } catch (final IllegalArgumentException e) { + LOG.error("Device requested unsupported AGPS data type: {}", constellation); return false; } } @@ -31,7 +46,27 @@ public class GarminAgpsFile { return true; } + public boolean isValidRxNetworks() { + if (!ArrayUtils.startsWith(bytes, GZ_HEADER)) { + return false; + } + + try (GZIPInputStream gzis = new GZIPInputStream(new ByteArrayInputStream(bytes))) { + final byte[] header = new byte[CPE_RXNETWORKS_HEADER.length]; + int read = gzis.read(header); + if (read != CPE_RXNETWORKS_HEADER.length || !Arrays.equals(header, CPE_RXNETWORKS_HEADER)) { + LOG.error("Header in gz file is not agps rxnetworks: {}", GB.hexdump(header)); + return false; + } + return true; + } catch (final IOException e) { + LOG.error("Failed to decompress file as gzip", e); + } + + return false; + } + public byte[] getBytes() { - return tarBytes.clone(); + return bytes.clone(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java index 502274173..c2f2cef5b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java @@ -68,6 +68,7 @@ public class HttpHandler { final String path = url.getPath(); final Map query = HttpUtils.urlQueryParameters(url); + final Map requestHeaders = headersToMap(rawRequest.getHeaderList()); if (path.startsWith("/weather/")) { LOG.info("Got weather request for {}", path); @@ -80,12 +81,13 @@ public class HttpHandler { return createRawResponse(rawRequest, json.getBytes(StandardCharsets.UTF_8), "application/json", null); } else if (path.startsWith("/ephemeris/")) { LOG.info("Got AGPS request for {}", path); - final byte[] agpsData = agpsHandler.handleAgpsRequest(path, query); + final byte[] agpsData = agpsHandler.handleAgpsRequest(urlString, path, query); if (agpsData == null) { return null; } LOG.debug("Successfully obtained AGPS data (length: {})", agpsData.length); - return createRawResponse(rawRequest, agpsData, "application/x-tar", agpsHandler.getOnDataSuccessfullySentListener()); + final String contentType = requestHeaders.containsKey("accept") ? requestHeaders.get("accept") : "application/octet-stream"; + return createRawResponse(rawRequest, agpsData, contentType, agpsHandler.getOnDataSuccessfullySentListener(urlString)); } else { LOG.warn("Unhandled path {}", urlString); return null; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java index 0931571a2..d6e42705c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java @@ -26,8 +26,10 @@ import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Locale; import java.util.zip.CRC32; public class CheckSums { @@ -175,4 +177,17 @@ public class CheckSums { md.update(data); return md.digest(); } + + @Nullable + public static String md5(final String str) { + final MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } catch (final NoSuchAlgorithmException e) { + LOG.error("Failed to get md5 digest", e); + return null; + } + md.update(str.getBytes(StandardCharsets.UTF_8)); + return GB.hexdump(md.digest()).toLowerCase(Locale.ROOT); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/HttpUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/HttpUtils.java index 67dbf3be0..4c804f8c0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/HttpUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/HttpUtils.java @@ -7,7 +7,7 @@ import org.slf4j.LoggerFactory; import java.net.URL; import java.net.URLDecoder; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; public class HttpUtils { @@ -22,7 +22,7 @@ public class HttpUtils { if (StringUtils.isBlank(query)) { return Collections.emptyMap(); } - final Map queryParameters = new HashMap<>(); + final Map queryParameters = new LinkedHashMap<>(); final String[] pairs = query.split("&"); for (final String pair : pairs) { final String[] parts = pair.split("=", 2); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java index 4e8a2f217..4769fba7e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Prefs.java @@ -163,14 +163,19 @@ public class Prefs { * The preference is assumed to be a string, with each value separated by a comma. * @param key the preference key * @param defaultValue the default value to return if the preference value is unset + * @param separator the separator to use to split the string * @return the saved preference value or the given defaultValue */ - public List getList(final String key, final List defaultValue) { + public List getList(final String key, final List defaultValue, final String separatorRegex) { final String stringValue = preferences.getString(key, null); if (stringValue == null) { return defaultValue; } - return Arrays.asList(stringValue.split(",")); + return Arrays.asList(stringValue.split(separatorRegex)); + } + + public List getList(final String key, final List defaultValue) { + return getList(key, defaultValue, ","); } public Date getTimePreference(final String key, final String defaultValue) { diff --git a/app/src/main/res/drawable/ic_file_open.xml b/app/src/main/res/drawable/ic_file_open.xml new file mode 100644 index 000000000..bd1899ce9 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_open.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e6f414e0b..e2dcfbbf9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2872,4 +2872,14 @@ Camera permission is required for this function. Camera support is required for this function. Photo has been taken and saved at: %s + + None + AGPS %1$d URL + No folder selected + Folder is empty + Folder + URL + Local file + The list below contains all URLs requested by the watch for AGPS updates. You can select a file from the phone\'s storage that will be sent to the watch when it requests an update. + Copied to clipboard diff --git a/app/src/main/res/xml/devicesettings_garmin_agps.xml b/app/src/main/res/xml/devicesettings_garmin_agps.xml index 562ad2840..2c93e06d9 100644 --- a/app/src/main/res/xml/devicesettings_garmin_agps.xml +++ b/app/src/main/res/xml/devicesettings_garmin_agps.xml @@ -1,18 +1,20 @@ - + - - - - - + + + + - \ No newline at end of file +