mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-01-12 18:57:36 +01:00
Amazfit GTR 4/GTS 4: Add AGPS Updates
This commit is contained in:
parent
f1e26aeb8b
commit
17c58d2947
@ -192,6 +192,8 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_GPS_SATELLITE_SEARCH = "pref_gps_satellite_search";
|
||||
public static final String PREF_AGPS_EXPIRY_REMINDER_ENABLED = "pref_agps_expiry_reminder_enabled";
|
||||
public static final String PREF_AGPS_EXPIRY_REMINDER_TIME = "pref_agps_expiry_reminder_time";
|
||||
public static final String PREF_AGPS_UPDATE_TIME = "pref_agps_update_time";
|
||||
public static final String PREF_AGPS_EXPIRE_TIME = "pref_agps_expire_time";
|
||||
|
||||
public static final String PREF_FIND_PHONE = "prefs_find_phone";
|
||||
public static final String PREF_FIND_PHONE_DURATION = "prefs_find_phone_duration";
|
||||
|
@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
@ -40,14 +41,13 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiVibrationPatternNotificationType;
|
||||
|
||||
public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
||||
@Override
|
||||
public abstract AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context);
|
||||
public abstract InstallHandler findInstallHandler(final Uri uri, final Context context);
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(final GBDevice device) {
|
||||
|
@ -25,8 +25,10 @@ import androidx.preference.Preference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -72,6 +74,8 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
|
||||
case SHORT:
|
||||
case INT:
|
||||
case DATETIME_HH_MM:
|
||||
case TIMESTAMP_MILLIS:
|
||||
default:
|
||||
// For other preferences, just hide them if they were not reported as supported by the device
|
||||
hidePrefIfNoConfigSupported(handler, prefs, config.getPrefKey(), config);
|
||||
break;
|
||||
@ -204,13 +208,15 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
|
||||
));
|
||||
hidePrefIfNoneVisible(handler, DeviceSettingsPreferenceConst.PREF_HEADER_AGPS, Arrays.asList(
|
||||
DeviceSettingsPreferenceConst.PREF_AGPS_EXPIRY_REMINDER_ENABLED,
|
||||
DeviceSettingsPreferenceConst.PREF_AGPS_EXPIRY_REMINDER_TIME
|
||||
DeviceSettingsPreferenceConst.PREF_AGPS_EXPIRY_REMINDER_TIME,
|
||||
DeviceSettingsPreferenceConst.PREF_AGPS_UPDATE_TIME,
|
||||
DeviceSettingsPreferenceConst.PREF_AGPS_EXPIRE_TIME
|
||||
));
|
||||
|
||||
setupGpsPreference(handler);
|
||||
setupGpsPreference(handler, prefs);
|
||||
}
|
||||
|
||||
private void setupGpsPreference(final DeviceSpecificSettingsHandler handler) {
|
||||
private void setupGpsPreference(final DeviceSpecificSettingsHandler handler, final Prefs prefs) {
|
||||
final ListPreference prefGpsPreset = handler.findPreference(DeviceSettingsPreferenceConst.PREF_GPS_MODE_PRESET);
|
||||
final ListPreference prefGpsBand = handler.findPreference(DeviceSettingsPreferenceConst.PREF_GPS_BAND);
|
||||
final ListPreference prefGpsCombination = handler.findPreference(DeviceSettingsPreferenceConst.PREF_GPS_COMBINATION);
|
||||
@ -288,6 +294,28 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
|
||||
onGpsBandUpdate.onPreferenceChange(prefGpsPreset, prefGpsBand.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
final Preference prefAgpsExpireTime = handler.findPreference(DeviceSettingsPreferenceConst.PREF_AGPS_EXPIRE_TIME);
|
||||
if (prefAgpsExpireTime != null) {
|
||||
final long ts = prefs.getLong(DeviceSettingsPreferenceConst.PREF_AGPS_EXPIRE_TIME, 0L);
|
||||
if (ts > 0) {
|
||||
prefAgpsExpireTime.setSummary(sdf.format(new Date(ts)));
|
||||
} else {
|
||||
prefAgpsExpireTime.setSummary(handler.getContext().getString(R.string.unknown));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,9 +28,9 @@ import org.slf4j.LoggerFactory;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsAgpsInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
|
||||
|
||||
public class AmazfitGTR4Coordinator extends Huami2021Coordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AmazfitGTR4Coordinator.class);
|
||||
@ -57,7 +57,11 @@ public class AmazfitGTR4Coordinator extends Huami2021Coordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) {
|
||||
public InstallHandler findInstallHandler(final Uri uri, final Context context) {
|
||||
final ZeppOsAgpsInstallHandler agpsInstallHandler = new ZeppOsAgpsInstallHandler(uri, context);
|
||||
if (agpsInstallHandler.isValid()) {
|
||||
return agpsInstallHandler;
|
||||
}
|
||||
final AmazfitGTR4FWInstallHandler handler = new AmazfitGTR4FWInstallHandler(uri, context);
|
||||
return handler.isValid() ? handler : null;
|
||||
}
|
||||
|
@ -25,11 +25,12 @@ import androidx.annotation.NonNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsAgpsInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
|
||||
|
||||
public class AmazfitGTS4Coordinator extends Huami2021Coordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AmazfitGTS4Coordinator.class);
|
||||
@ -56,7 +57,11 @@ public class AmazfitGTS4Coordinator extends Huami2021Coordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) {
|
||||
public InstallHandler findInstallHandler(final Uri uri, final Context context) {
|
||||
final ZeppOsAgpsInstallHandler agpsInstallHandler = new ZeppOsAgpsInstallHandler(uri, context);
|
||||
if (agpsInstallHandler.isValid()) {
|
||||
return agpsInstallHandler;
|
||||
}
|
||||
final AmazfitGTS4FWInstallHandler handler = new AmazfitGTS4FWInstallHandler(uri, context);
|
||||
return handler.isValid() ? handler : null;
|
||||
}
|
||||
|
@ -0,0 +1,122 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos;
|
||||
|
||||
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.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsAgpsFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
|
||||
|
||||
public class ZeppOsAgpsInstallHandler implements InstallHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsAgpsInstallHandler.class);
|
||||
|
||||
protected final Context mContext;
|
||||
private ZeppOsAgpsFile file;
|
||||
|
||||
public ZeppOsAgpsInstallHandler(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 ~128KB
|
||||
final ZeppOsAgpsFile agpsFile = new ZeppOsAgpsFile(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;
|
||||
}
|
||||
|
||||
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(device.getType().getIcon());
|
||||
|
||||
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 ZeppOsAgpsFile getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
private GenericItem createInstallItem(final GBDevice device) {
|
||||
final String firmwareName = mContext.getString(
|
||||
R.string.installhandler_firmware_name,
|
||||
mContext.getString(device.getType().getName()),
|
||||
mContext.getString(R.string.kind_agps_bundle),
|
||||
""
|
||||
);
|
||||
return new GenericItem(firmwareName);
|
||||
}
|
||||
}
|
@ -103,7 +103,6 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (helper.getFirmwareType() != WATCHFACE && helper.getFirmwareType() != AGPS_UIHH) {
|
||||
if (helper.isSingleFirmware()) {
|
||||
getFwUpgradeNotice();
|
||||
builder.append(getFwUpgradeNotice());
|
||||
} else {
|
||||
builder.append(mContext.getString(R.string.fw_multi_upgrade_notice, helper.getHumanFirmwareVersion(), helper.getHumanFirmwareVersion2()));
|
||||
|
@ -89,6 +89,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsAgpsInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
|
||||
@ -115,6 +116,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.Hua
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2021;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsAgpsFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsAgpsUpdateOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAgpsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileUploadService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
|
||||
@ -142,9 +146,11 @@ public abstract class Huami2021Support extends HuamiSupport {
|
||||
// Services
|
||||
private final ZeppOsFileUploadService fileUploadService = new ZeppOsFileUploadService(this);
|
||||
private final ZeppOsConfigService configService = new ZeppOsConfigService(this);
|
||||
private final ZeppOsAgpsService agpsService = new ZeppOsAgpsService(this);
|
||||
private final Map<Short, AbstractZeppOsService> mServiceMap = new HashMap<Short, AbstractZeppOsService>() {{
|
||||
put(fileUploadService.getEndpoint(), fileUploadService);
|
||||
put(configService.getEndpoint(), configService);
|
||||
put(agpsService.getEndpoint(), agpsService);
|
||||
}};
|
||||
|
||||
public Huami2021Support() {
|
||||
@ -898,6 +904,26 @@ public abstract class Huami2021Support extends HuamiSupport {
|
||||
writeToChunked2021("toggle realtime steps", CHUNKED2021_ENDPOINT_STEPS, cmd, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(final Uri uri) {
|
||||
final ZeppOsAgpsInstallHandler agpsHandler = new ZeppOsAgpsInstallHandler(uri, getContext());
|
||||
if (agpsHandler.isValid()) {
|
||||
try {
|
||||
new ZeppOsAgpsUpdateOperation(
|
||||
this,
|
||||
agpsHandler.getFile(),
|
||||
agpsService,
|
||||
fileUploadService,
|
||||
configService
|
||||
).perform();
|
||||
} catch (final Exception e) {
|
||||
GB.toast(getContext(), "AGPS File cannot be installed: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
} else {
|
||||
super.onInstallApp(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Huami2021Support setHeartrateSleepSupport(final TransactionBuilder builder) {
|
||||
final boolean enableHrSleepSupport = MiBandCoordinator.getHeartrateSleepSupport(gbDevice.getAddress());
|
||||
@ -2151,7 +2177,7 @@ public abstract class Huami2021Support extends HuamiSupport {
|
||||
tga565,
|
||||
new ZeppOsFileUploadService.Callback() {
|
||||
@Override
|
||||
public void onFinish(final boolean success) {
|
||||
public void onFileUploadFinish(final boolean success) {
|
||||
LOG.info("Finished sending icon, success={}", success);
|
||||
if (success) {
|
||||
ackNotificationAfterIconSent(packageName);
|
||||
@ -2159,7 +2185,7 @@ public abstract class Huami2021Support extends HuamiSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(final int progress) {
|
||||
public void onFileUploadProgress(final int progress) {
|
||||
LOG.trace("Icon send progress: {}", progress);
|
||||
}
|
||||
}
|
||||
|
@ -174,6 +174,9 @@ public class UIHHContainer {
|
||||
LLE_GLO_LLE(0x88, "lle_glo.lle"),
|
||||
LLE_GAL_LLE(0x89, "lle_gal.lle"),
|
||||
LLE_QZSS_LLE(0x8a, "lle_qzss.lle"),
|
||||
AGPS_EPO_GR_3(-116, "EPO_GR_3.DAT"),
|
||||
AGPS_EPO_GAL_7(-115, "EPO_GAL_7.DAT"),
|
||||
AGPS_EPO_BDS_3(-114, "EPO_BDS_3.DAT"),
|
||||
;
|
||||
|
||||
private final byte value;
|
||||
|
@ -27,13 +27,19 @@ public abstract class AbstractZeppOsService {
|
||||
}
|
||||
|
||||
public abstract short getEndpoint();
|
||||
|
||||
public abstract boolean isEncrypted();
|
||||
|
||||
public abstract void handlePayload(final byte[] payload);
|
||||
|
||||
protected Huami2021Support getSupport() {
|
||||
return mSupport;
|
||||
}
|
||||
|
||||
protected void write(final String taskName, final byte b) {
|
||||
this.write(taskName, new byte[]{b});
|
||||
}
|
||||
|
||||
protected void write(final String taskName, final byte[] data) {
|
||||
this.mSupport.writeToChunked2021(taskName, getEndpoint(), data, isEncrypted());
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.UIHHContainer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ZipFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException;
|
||||
|
||||
public class ZeppOsAgpsFile {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsAgpsFile.class);
|
||||
|
||||
private final byte[] zipBytes;
|
||||
|
||||
public ZeppOsAgpsFile(final byte[] zipBytes) {
|
||||
this.zipBytes = zipBytes;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
if (!ZipFile.isZipFile(zipBytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ZipFile zipFile = new ZipFile(zipBytes);
|
||||
|
||||
try {
|
||||
final byte[] manifestBin = zipFile.getFileFromZip("META-INF/MANIFEST.MF");
|
||||
if (manifestBin == null) {
|
||||
LOG.warn("Failed to get MANIFEST from zip");
|
||||
return false;
|
||||
}
|
||||
|
||||
final String appJsonString = new String(manifestBin, StandardCharsets.UTF_8)
|
||||
// Remove UTF-8 BOM if present
|
||||
.replace("\uFEFF", "");
|
||||
final JSONObject jsonObject = new JSONObject(appJsonString);
|
||||
return jsonObject.getString("manifestVersion").equals("2.0") &&
|
||||
zipFile.fileExists("EPO_BDS_3.DAT") &&
|
||||
zipFile.fileExists("EPO_GAL_7.DAT") &&
|
||||
zipFile.fileExists("EPO_GR_3.DAT");
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to parse read MANIFEST or check file", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public byte[] getUihhBytes() {
|
||||
final UIHHContainer uihh = new UIHHContainer();
|
||||
|
||||
final ZipFile zipFile = new ZipFile(zipBytes);
|
||||
|
||||
try {
|
||||
uihh.addFile(UIHHContainer.FileType.AGPS_EPO_GR_3, zipFile.getFileFromZip("EPO_GR_3.DAT"));
|
||||
uihh.addFile(UIHHContainer.FileType.AGPS_EPO_GAL_7, zipFile.getFileFromZip("EPO_GAL_7.DAT"));
|
||||
uihh.addFile(UIHHContainer.FileType.AGPS_EPO_BDS_3, zipFile.getFileFromZip("EPO_BDS_3.DAT"));
|
||||
} catch (final ZipFileException e) {
|
||||
throw new IllegalStateException("Failed to read file from zip", e);
|
||||
}
|
||||
|
||||
return uihh.toRawBytes();
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAgpsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileUploadService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
/**
|
||||
* Updates the AGPS EPO on a Zepp OS device. Update goes as follows:
|
||||
* 1. Request an upload start from {@link ZeppOsAgpsService}
|
||||
* 2. After successful ack from 1, upload the file to agps://upgrade using {@link ZeppOsFileUploadService}
|
||||
* 3. After successful ack from 2, trigger the actual update with {@link ZeppOsAgpsService}
|
||||
* 4. After successful ack from 3, update is finished. Trigger an AGPS config request from {@link ZeppOsConfigService}
|
||||
* to reload the AGPS update and expiration timestamps.
|
||||
*/
|
||||
public class ZeppOsAgpsUpdateOperation extends AbstractBTLEOperation<Huami2021Support>
|
||||
implements ZeppOsFileUploadService.Callback, ZeppOsAgpsService.Callback {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsAgpsUpdateOperation.class);
|
||||
|
||||
private static final String AGPS_UPDATE_URL = "agps://upgrade";
|
||||
private static final String AGPS_UPDATE_FILE = "uih.bin";
|
||||
|
||||
private final ZeppOsAgpsFile file;
|
||||
private final byte[] fileBytes;
|
||||
|
||||
private final ZeppOsAgpsService agpsService;
|
||||
private final ZeppOsFileUploadService fileUploadService;
|
||||
private final ZeppOsConfigService configService;
|
||||
|
||||
public ZeppOsAgpsUpdateOperation(final Huami2021Support support,
|
||||
final ZeppOsAgpsFile file,
|
||||
final ZeppOsAgpsService agpsService,
|
||||
final ZeppOsFileUploadService fileUploadService,
|
||||
final ZeppOsConfigService configService) {
|
||||
super(support);
|
||||
this.file = file;
|
||||
this.fileBytes = file.getUihhBytes();
|
||||
this.agpsService = agpsService;
|
||||
this.fileUploadService = fileUploadService;
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPerform() throws IOException {
|
||||
agpsService.setCallback(this);
|
||||
agpsService.startUpload(file.getUihhBytes().length);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void operationFinished() {
|
||||
operationStatus = OperationStatus.FINISHED;
|
||||
if (getDevice() != null && getDevice().isConnected()) {
|
||||
unsetBusy();
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileUploadFinish(final boolean success) {
|
||||
LOG.info("Finished file upload operation, success={}", success);
|
||||
|
||||
agpsService.startUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileUploadProgress(final int progress) {
|
||||
LOG.trace("File upload operation progress: {}", progress);
|
||||
|
||||
// This makes the progress go from 0% to 50%, during file upload the other 50% are incremented
|
||||
// by the update process on the watch
|
||||
final int progressPercent = (int) ((((float) (progress)) / (fileBytes.length * 2)) * 100);
|
||||
updateProgress(progressPercent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgpsUploadStartResponse(final boolean success) {
|
||||
if (!success) {
|
||||
onFinish(false);
|
||||
return;
|
||||
}
|
||||
|
||||
fileUploadService.sendFile(AGPS_UPDATE_URL, AGPS_UPDATE_FILE, fileBytes, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgpsProgressResponse(final int size, final int progress) {
|
||||
// First 50% are from file upload, so this one starts at 50%
|
||||
final int progressPercent = (int) ((((float) (size + progress)) / (size * 2)) * 100);
|
||||
updateProgress(progressPercent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgpsUpdateFinishResponse(final boolean success) {
|
||||
if (success) {
|
||||
try {
|
||||
final TransactionBuilder builder = performInitialized("request agps config");
|
||||
configService.requestConfig(builder, ZeppOsConfigService.ConfigGroup.AGPS);
|
||||
builder.queue(getQueue());
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to request agps config", e);
|
||||
}
|
||||
}
|
||||
|
||||
onFinish(success);
|
||||
}
|
||||
|
||||
private void updateProgress(final int progressPercent) {
|
||||
try {
|
||||
final TransactionBuilder builder = performInitialized("send agps update progress");
|
||||
builder.add(new SetProgressAction(getContext().getString(R.string.updatefirmwareoperation_update_in_progress), true, progressPercent, getContext()));
|
||||
builder.queue(getQueue());
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to update progress notification", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFinish(final boolean success) {
|
||||
LOG.info("Finished agps update operation, success={}", success);
|
||||
|
||||
agpsService.setCallback(null);
|
||||
|
||||
final String notificationMessage = success ?
|
||||
getContext().getString(R.string.updatefirmwareoperation_update_complete) :
|
||||
getContext().getString(R.string.updatefirmwareoperation_write_failed);
|
||||
|
||||
GB.updateInstallNotification(notificationMessage, false, 100, getContext());
|
||||
|
||||
operationFinished();
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/* Copyright (C) 2022 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||
|
||||
public class ZeppOsAgpsService extends AbstractZeppOsService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsAgpsService.class);
|
||||
|
||||
private static final short ENDPOINT = 0x0042;
|
||||
|
||||
private static final byte CMD_UPDATE_START_UPLOAD_REQUEST = 0x03;
|
||||
private static final byte CMD_UPDATE_START_UPLOAD_RESPONSE = 0x04;
|
||||
private static final byte CMD_UPDATE_START_REQUEST = 0x05;
|
||||
private static final byte CMD_UPDATE_PROGRESS_RESPONSE = 0x06;
|
||||
private static final byte CMD_UPDATE_FINISH_RESPONSE = 0x07;
|
||||
|
||||
private Callback mCallback = null;
|
||||
|
||||
public ZeppOsAgpsService(final Huami2021Support support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getEndpoint() {
|
||||
return ENDPOINT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case CMD_UPDATE_START_UPLOAD_RESPONSE:
|
||||
final byte uploadStatus = payload[1];
|
||||
LOG.info("Got agps start upload status = {}", uploadStatus);
|
||||
if (mCallback != null) {
|
||||
mCallback.onAgpsUploadStartResponse(uploadStatus == 0x01);
|
||||
}
|
||||
return;
|
||||
case CMD_UPDATE_PROGRESS_RESPONSE:
|
||||
final int size = BLETypeConversions.toUint32(payload, 1);
|
||||
final int progress = BLETypeConversions.toUint32(payload, 5);
|
||||
|
||||
LOG.info("Got agps progress = {}/{}", progress, size);
|
||||
if (mCallback != null) {
|
||||
mCallback.onAgpsProgressResponse(size, progress);
|
||||
}
|
||||
return;
|
||||
case CMD_UPDATE_FINISH_RESPONSE:
|
||||
final byte finishStatus = payload[1];
|
||||
LOG.info("Got agps update finish status = {}", finishStatus);
|
||||
if (mCallback != null) {
|
||||
mCallback.onAgpsUpdateFinishResponse(finishStatus == 0x01);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unexpected agps byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
public void startUpload(final int size) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(5)
|
||||
.order(ByteOrder.LITTLE_ENDIAN)
|
||||
.put(CMD_UPDATE_START_UPLOAD_REQUEST)
|
||||
.putInt(size);
|
||||
|
||||
write("start upload request", buf.array());
|
||||
}
|
||||
|
||||
public void startUpdate() {
|
||||
write("start update request", CMD_UPDATE_START_REQUEST);
|
||||
}
|
||||
|
||||
public void setCallback(@Nullable final Callback callback) {
|
||||
this.mCallback = callback;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onAgpsUploadStartResponse(boolean success);
|
||||
|
||||
void onAgpsProgressResponse(int size, int progress);
|
||||
|
||||
void onAgpsUpdateFinishResponse(boolean success);
|
||||
}
|
||||
}
|
@ -205,6 +205,7 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
|
||||
}
|
||||
|
||||
public enum ConfigGroup {
|
||||
AGPS(0x00, 0x01),
|
||||
DISPLAY(0x01, 0x02),
|
||||
// TODO 0x02
|
||||
SOUND_AND_VIBRATION(0x03, 0x02),
|
||||
@ -254,6 +255,7 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
|
||||
BYTE(0x10),
|
||||
BYTE_LIST(0x11),
|
||||
DATETIME_HH_MM(0x30),
|
||||
TIMESTAMP_MILLIS(0x40),
|
||||
;
|
||||
|
||||
private final byte value;
|
||||
@ -278,6 +280,10 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
|
||||
}
|
||||
|
||||
public enum ConfigArg {
|
||||
// AGPS
|
||||
AGPS_UPDATE_TIME(ConfigGroup.AGPS, ConfigType.TIMESTAMP_MILLIS, 0x09, PREF_AGPS_UPDATE_TIME),
|
||||
AGPS_EXPIRE_TIME(ConfigGroup.AGPS, ConfigType.TIMESTAMP_MILLIS, 0x0a, PREF_AGPS_EXPIRE_TIME),
|
||||
|
||||
// Display
|
||||
SCREEN_AUTO_BRIGHTNESS(ConfigGroup.DISPLAY, ConfigType.BOOL, 0x01, PREF_SCREEN_AUTO_BRIGHTNESS),
|
||||
SCREEN_BRIGHTNESS(ConfigGroup.DISPLAY, ConfigType.SHORT, 0x02, PREF_SCREEN_BRIGHTNESS),
|
||||
@ -908,6 +914,17 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
|
||||
argPrefs = convertDatetimeHhMmToPrefs(configArg, valHhMm);
|
||||
}
|
||||
break;
|
||||
case TIMESTAMP_MILLIS:
|
||||
final ConfigTimestamp valTimestamp = ConfigTimestamp.consume(buf);
|
||||
if (valTimestamp == null) {
|
||||
LOG.error("Failed to parse {} for {}", configType, configArg);
|
||||
return prefs;
|
||||
}
|
||||
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valTimestamp);
|
||||
if (configArg != null) {
|
||||
argPrefs = convertTimestampToPrefs(configArg, valTimestamp);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LOG.error("No parser for {}", configArg);
|
||||
// Abort, since we don't know how to parse this type or how many bytes it is
|
||||
@ -1050,6 +1067,15 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, Object> convertTimestampToPrefs(final ConfigArg configArg, final ConfigTimestamp value) {
|
||||
if (configArg.getPrefKey() != null) {
|
||||
// The arg maps to a number pref directly
|
||||
return singletonMap(configArg.getPrefKey(), value.getValue());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, Object> convertDatetimeHhMmToPrefs(final ConfigArg configArg, final ConfigDatetimeHhMm hhmm) {
|
||||
if (configArg.getPrefKey() != null) {
|
||||
// The arg maps to a hhmm pref directly
|
||||
@ -1446,6 +1472,29 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConfigTimestamp {
|
||||
private final long value;
|
||||
|
||||
public ConfigTimestamp(final long value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public long getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
private static ConfigTimestamp consume(final ByteBuffer buf) {
|
||||
final long value = buf.getLong();
|
||||
|
||||
return new ConfigTimestamp(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(Locale.ROOT, "ConfigTimestamp{value=%s}", new Date(value));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConfigByte {
|
||||
private final byte value;
|
||||
private final byte[] possibleValues;
|
||||
|
@ -124,7 +124,7 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
|
||||
public void sendFile(final String url, final String filename, final byte[] bytes, final Callback callback) {
|
||||
if (mChunkSize < 0) {
|
||||
LOG.error("Service not initialized, refusing to send {}", url);
|
||||
callback.onFinish(false);
|
||||
callback.onFileUploadFinish(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -198,7 +198,7 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
|
||||
|
||||
request.setProgress(request.getProgress() + payload.length);
|
||||
request.setIndex((byte) (request.getIndex() + 1));
|
||||
request.getCallback().onProgress(request.getProgress());
|
||||
request.getCallback().onFileUploadProgress(request.getProgress());
|
||||
|
||||
write("send file data", buf.array());
|
||||
}
|
||||
@ -212,7 +212,7 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
|
||||
|
||||
mSessionRequests.remove(session);
|
||||
|
||||
request.getCallback().onFinish(success);
|
||||
request.getCallback().onFileUploadFinish(success);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -271,8 +271,8 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onFinish(boolean success);
|
||||
void onFileUploadFinish(boolean success);
|
||||
|
||||
void onProgress(int progress);
|
||||
void onFileUploadProgress(int progress);
|
||||
}
|
||||
}
|
||||
|
@ -73,9 +73,32 @@ public class ZipFile {
|
||||
throw new ZipFileException(String.format("Path in ZIP file was not found: %s", path));
|
||||
|
||||
} catch (ZipException e) {
|
||||
throw new ZipFileException("The ZIP file might be corrupted");
|
||||
throw new ZipFileException("The ZIP file might be corrupted", e);
|
||||
} catch (IOException e) {
|
||||
throw new ZipFileException("General IO error");
|
||||
throw new ZipFileException("General IO error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean fileExists(final String path) throws ZipFileException {
|
||||
try (InputStream is = new ByteArrayInputStream(zipBytes); ZipInputStream zipInputStream = new ZipInputStream(is)) {
|
||||
ZipEntry zipEntry;
|
||||
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
|
||||
if (!zipEntry.getName().equals(path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (zipEntry.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (final ZipException e) {
|
||||
throw new ZipFileException("The ZIP file might be corrupted", e);
|
||||
} catch (final IOException e) {
|
||||
throw new ZipFileException("General IO error", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
public class ZipFileException extends Exception {
|
||||
public ZipFileException(String message) {
|
||||
public ZipFileException(final String message) {
|
||||
super(String.format("Error while reading ZIP file: %s", message));
|
||||
}
|
||||
|
||||
public ZipFileException(final String message, final Throwable cause) {
|
||||
super(String.format("Error while reading ZIP file: %s", message), cause);
|
||||
}
|
||||
}
|
||||
|
@ -456,6 +456,8 @@
|
||||
<string name="pref_agps_header">AGPS</string>
|
||||
<string name="pref_agps_expiry_reminder_enabled">AGPS Expiry Reminder</string>
|
||||
<string name="pref_agps_expiry_reminder_time">AGPS Expiry Reminder Time</string>
|
||||
<string name="pref_agps_update_time">AGPS Update Time</string>
|
||||
<string name="pref_agps_expire_time">AGPS Expire Time</string>
|
||||
<string name="pref_workout_start_on_phone_title">Fitness app tracking</string>
|
||||
<string name="pref_workout_start_on_phone_summary">Start/stop fitness app tracking on phone when a GPS workout is started on the band</string>
|
||||
<string name="pref_workout_send_gps_title">Send GPS during workout</string>
|
||||
|
@ -57,6 +57,14 @@
|
||||
android:dependency="pref_agps_expiry_reminder_enabled"
|
||||
android:key="pref_agps_expiry_reminder_time"
|
||||
android:title="@string/pref_agps_expiry_reminder_time" />
|
||||
|
||||
<Preference
|
||||
android:key="pref_agps_update_time"
|
||||
android:title="@string/pref_agps_update_time" />
|
||||
|
||||
<Preference
|
||||
android:key="pref_agps_expire_time"
|
||||
android:title="@string/pref_agps_expire_time" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
@ -122,6 +122,24 @@ public class ZipFileTest extends TestBase {
|
||||
Assert.assertEquals(contents3, readContents3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testZipFilesFileExists() throws IOException, ZipFileException {
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
final ZipOutputStream zipWriteStream = new ZipOutputStream(baos);
|
||||
|
||||
writeFileToZip(TEST_FILE_CONTENTS_1, "file1", zipWriteStream);
|
||||
writeFileToZip(TEST_FILE_CONTENTS_2, "file2", zipWriteStream);
|
||||
writeFileToZip("Hello, World!", "folder1/file3", zipWriteStream);
|
||||
zipWriteStream.close();
|
||||
|
||||
final ZipFile zipFile = new ZipFile(baos.toByteArray());
|
||||
Assert.assertTrue(zipFile.fileExists("file2"));
|
||||
Assert.assertTrue(zipFile.fileExists("file1"));
|
||||
Assert.assertTrue(zipFile.fileExists("folder1/file3"));
|
||||
Assert.assertFalse(zipFile.fileExists("folder1"));
|
||||
Assert.assertFalse(zipFile.fileExists("file4"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ZIP archive with a single text file.
|
||||
* The archive will not be saved to a file, it is kept in memory.
|
||||
|
Loading…
x
Reference in New Issue
Block a user