1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-07-22 14:52:25 +02:00

Amazfit GTR 4/GTS 4: Add AGPS Updates

This commit is contained in:
José Rebelo 2023-01-05 00:28:50 +00:00
parent f1e26aeb8b
commit 17c58d2947
20 changed files with 671 additions and 20 deletions

View File

@ -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";

View File

@ -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) {

View File

@ -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));
}
}
}
/**

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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()));

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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());
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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.