mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-25 16:15:55 +01:00
Compare commits
92 Commits
d1eb6ae740
...
bc940ceddb
Author | SHA1 | Date | |
---|---|---|---|
|
bc940ceddb | ||
|
de1faf6f00 | ||
|
ac4273fc1b | ||
|
5c183da78c | ||
|
5540dc18a4 | ||
|
2803c62a7c | ||
|
cd2535f49a | ||
|
f0826286dc | ||
|
0ac6da3903 | ||
|
9309ac8d38 | ||
|
1e2ea97391 | ||
|
571410ff4b | ||
|
f3f9a75633 | ||
|
d2e61c5fe8 | ||
|
1660f4b7fa | ||
|
6eb97eeb15 | ||
|
d94ac25cd1 | ||
|
cd8fad29b1 | ||
|
8715cc7e81 | ||
|
7182f4be67 | ||
|
35021f28fa | ||
|
c2a4eed13a | ||
|
cd9fdca397 | ||
|
21882042c4 | ||
|
9f2df0262f | ||
|
8060911d42 | ||
|
70a7d8f8da | ||
|
546102f01f | ||
|
e7f8c9e7a6 | ||
|
7734fbe4b0 | ||
|
6936053734 | ||
|
d93ef074c3 | ||
|
46be3c47f9 | ||
|
d8172295cb | ||
|
ddca4ad90d | ||
|
a077e867b4 | ||
|
6ce8f336b4 | ||
|
f1053c5b80 | ||
|
f97dda37c7 | ||
|
c250a70196 | ||
|
0c10edb9f2 | ||
|
49bd363d0f | ||
|
8ab77f148f | ||
|
466349fbe7 | ||
|
2683fd9495 | ||
|
6a9c037926 | ||
|
af740d4dc3 | ||
|
4f04b61dbd | ||
|
49c4f34f42 | ||
|
945a28c279 | ||
|
40c3eade21 | ||
|
bfa5d94587 | ||
|
e492160531 | ||
|
8338b03b3b | ||
|
ddc2d7a34a | ||
|
3ac3519f3e | ||
|
9919bb2d6f | ||
|
7146fce683 | ||
|
ee274510ac | ||
|
64b3468671 | ||
|
19ec7344ca | ||
|
2c918f6a7c | ||
|
d3229dea96 | ||
|
6255ff615d | ||
|
c572cae161 | ||
|
cba2cf38f6 | ||
|
2429f64412 | ||
|
c2c53d5495 | ||
|
e23c5f2dd4 | ||
|
acdde68fab | ||
|
e39e67100e | ||
|
bcd1fc8681 | ||
|
e10fae00cf | ||
|
eec3d2b89a | ||
|
6d82980cc3 | ||
|
70b9911153 | ||
|
b4879a92d9 | ||
|
517f9ac214 | ||
|
6faa4452e2 | ||
|
c6cec7a0f8 | ||
|
fc9d18100d | ||
|
a59b5de343 | ||
|
57c0c4dfcb | ||
|
758e9223d1 | ||
|
8907757674 | ||
|
3c32636089 | ||
|
717bc3035d | ||
|
6167f3f781 | ||
|
924088e5a3 | ||
|
88043aa813 | ||
|
014b453693 | ||
|
d59b7c7bfa |
25
CHANGELOG.md
25
CHANGELOG.md
@ -1,5 +1,30 @@
|
||||
### Changelog
|
||||
|
||||
#### Next release (WIP)
|
||||
* Initial support for Amazfit Bip 3
|
||||
* Initial support for Huawei Band 8
|
||||
* Initial support for Huawei Watch GT 4
|
||||
* Initial support for Huawei Watch Ultimate
|
||||
* Initial support for Sony LinkBuds
|
||||
* Initial support for Xiaomi Smart Band 8 Active
|
||||
* Bangle.js: Fix crash when file save is cancelled
|
||||
* Bangle.js: Set filename on save file dialogs
|
||||
* Bangle.js: Improve communication stability
|
||||
* Bangle.js: Sync activity tracks
|
||||
* Femometer Vinca II: Add temperature charts
|
||||
* Fossil/Skagen Hybrids: Remove activity fetching toasts and add finished signal
|
||||
* Fossil/Skagen Hybrids: Use steps instead of calories for activity intensity
|
||||
* Fossil/Skagen Hybrids: Mark device busy and show transfer notification while syncing
|
||||
* Huami/Zepp OS: Fix activity sync getting stuck sometimes
|
||||
* Huawei: Add smart wakeup interval
|
||||
* Pebble: Fix pairing with LE counterpart
|
||||
* Xiaomi Watch S1 Pro: Add temperature charts
|
||||
* Xiaomi: Fix sleep sync failing when sleep stages are not found
|
||||
* Xiaomi: Improve activity sync
|
||||
* Allow for device settings sub-screens (#3620)
|
||||
* Device connection: Add support for scan before BLE connection
|
||||
* Misc UI improvements (alarms, chart settings)
|
||||
|
||||
#### 0.79.1
|
||||
* Initial support for Huawei Watch Fit
|
||||
* Initial support for Xiaomi Redmi Watch 3
|
||||
|
@ -383,6 +383,7 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
|
||||
|
||||
private void fetchTrackData() {
|
||||
if (mGBDevice.isInitialized() && !mGBDevice.isBusy()) {
|
||||
swipeLayout.setRefreshing(true);
|
||||
GBApplication.deviceService(mGBDevice).onFetchRecordedData(RecordedDataTypes.TYPE_GPS_TRACKS);
|
||||
} else {
|
||||
swipeLayout.setRefreshing(false);
|
||||
|
@ -107,6 +107,11 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2024 Damien Gaignon
|
||||
/* Copyright (C) 2024 Damien Gaignon, Vitalii Tomin
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@ -64,7 +64,9 @@ public final class HuaweiConstants {
|
||||
public static final String HU_BAND8_NAME = "huawei band 8-";
|
||||
public static final String HU_WATCHGT3_NAME = "huawei watch gt 3-";
|
||||
public static final String HU_WATCHGT3PRO_NAME = "huawei watch gt 3 pro-";
|
||||
public static final String HU_WATCHGT4_NAME = "huawei watch gt 4-";
|
||||
public static final String HU_WATCHFIT_NAME = "huawei watch fit-";
|
||||
public static final String HU_WATCHULTIMATE_NAME = "huawei watch ultimate-";
|
||||
|
||||
public static final String PREF_HUAWEI_ADDRESS = "huawei_address";
|
||||
public static final String PREF_HUAWEI_WORKMODE = "workmode";
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2024 Damien Gaignon, Martin.JM
|
||||
/* Copyright (C) 2024 Damien Gaignon, Martin.JM, Vitalii Tomin
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@ -243,6 +243,10 @@ public class HuaweiCoordinator {
|
||||
return supportsCommandForService(0x01, 0x1d);
|
||||
}
|
||||
|
||||
public boolean supportsAcceptAgreement() {
|
||||
return supportsCommandForService(0x01, 0x30);
|
||||
}
|
||||
|
||||
public boolean supportsSettingRelated() {
|
||||
return supportsCommandForService(0x01, 0x31);
|
||||
}
|
||||
@ -369,7 +373,21 @@ public class HuaweiCoordinator {
|
||||
}
|
||||
|
||||
public boolean supportsAccount() {
|
||||
return supportsCommandForService(0x1A, 0x05) || supportsCommandForService(0x1A, 0x06);
|
||||
return supportsCommandForService(0x1A, 0x01);
|
||||
}
|
||||
|
||||
public boolean supportsAccountJudgment() {
|
||||
return supportsCommandForService(0x1A, 0x05);
|
||||
}
|
||||
|
||||
public boolean supportsAccountSwitch() {
|
||||
return supportsCommandForService(0x1A, 0x06);
|
||||
}
|
||||
|
||||
public boolean supportsDiffAccountPairingOptimization() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(0xac);
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsMusic() {
|
||||
|
@ -520,6 +520,8 @@ public class HuaweiPacket {
|
||||
switch(this.commandId) {
|
||||
case AccountRelated.SendAccountToDevice.id:
|
||||
return new AccountRelated.SendAccountToDevice.Response(paramsProvider).fromPacket(this);
|
||||
case AccountRelated.SendExtendedAccountToDevice.id:
|
||||
return new AccountRelated.SendExtendedAccountToDevice.Response(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
|
@ -48,7 +48,12 @@ public class HuaweiWatchGT3Coordinator extends HuaweiBRCoordinator {
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(null);
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,63 @@
|
||||
/* Copyright (C) 2024 Vitalii Tomin
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt4;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiBRCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class HuaweiWatchGT4Coordinator extends HuaweiBRCoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWatchGT4Coordinator.class);
|
||||
|
||||
public HuaweiWatchGT4Coordinator() {
|
||||
super();
|
||||
getHuaweiCoordinator().setTransactionCrypted(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIWATCHGT4;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("(" + HuaweiConstants.HU_WATCHGT4_NAME + ").*", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_watchgt4;
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/* Copyright (C) 2024 Damien Gaignon
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchultimate;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiBRCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class HuaweiWatchUltimateCoordinator extends HuaweiBRCoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWatchUltimateCoordinator.class);
|
||||
|
||||
public HuaweiWatchUltimateCoordinator() {
|
||||
super();
|
||||
getHuaweiCoordinator().setTransactionCrypted(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIWATCHULTIMATE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("(" + HuaweiConstants.HU_WATCHULTIMATE_NAME + ").*", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return getHuaweiCoordinator().genericHuaweiSupportedDeviceSpecificSettings(new int[]{
|
||||
R.xml.devicesettings_heartrate_automatic_enable,
|
||||
R.xml.devicesettings_spo_automatic_enable,
|
||||
R.xml.devicesettings_find_phone,
|
||||
R.xml.devicesettings_disable_find_phone_with_dnd,
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_watchultimate;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2024 Damien Gaignon
|
||||
/* Copyright (C) 2024 Damien Gaignon, Vitalii Tomin
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@ -45,4 +45,30 @@ public class AccountRelated {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SendExtendedAccountToDevice {
|
||||
public static final byte id = 0x05;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request (ParamsProvider paramsProvider, boolean accountPairingOptimization) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = AccountRelated.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01);
|
||||
if (accountPairingOptimization) {
|
||||
this.tlv.put(0x03, (byte)0x01);
|
||||
}
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public Response (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1410,6 +1410,46 @@ public class DeviceConfig {
|
||||
// TODO: implement parsing this request for the log parser support
|
||||
}
|
||||
|
||||
public static class AcceptAgreement {
|
||||
public static final int id = 0x30;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = id;
|
||||
|
||||
int timestamp = (int) (System.currentTimeMillis() / 1000);
|
||||
|
||||
HuaweiTLV software = new HuaweiTLV()
|
||||
.put(0x03, "software_update_service_statement")
|
||||
.put(0x04, 0x01)
|
||||
.put(0x05, "20230508-20230508-0-0")
|
||||
.put(0x06, timestamp);
|
||||
HuaweiTLV device_information = new HuaweiTLV()
|
||||
.put(0x03, "device_information_management")
|
||||
.put(0x04,0x01)
|
||||
.put(0x05, "20230508-20230508-0-0")
|
||||
.put(0x06,timestamp);
|
||||
|
||||
HuaweiTLV user_license = new HuaweiTLV()
|
||||
.put(0x03, "user_license_agreement")
|
||||
.put(0x04,0x01)
|
||||
.put(0x05, "20230508-20230508-0-0")
|
||||
.put(0x06,timestamp);
|
||||
HuaweiTLV tlvList = new HuaweiTLV()
|
||||
.put(0x82, software)
|
||||
.put(0x82,device_information)
|
||||
.put(0x82,user_license);
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x81, tlvList);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static class SettingRelated {
|
||||
public static final int id = 0x31;
|
||||
|
||||
|
@ -123,6 +123,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt.HuaweiW
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt2.HuaweiWatchGT2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt2e.HuaweiWatchGT2eCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt3.HuaweiWatchGT3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt4.HuaweiWatchGT4Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchultimate.HuaweiWatchUltimateCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.itag.ITagCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.BFH16DeviceCoordinator;
|
||||
@ -358,8 +360,10 @@ public enum DeviceType {
|
||||
HONORBAND7(HonorBand7Coordinator.class),
|
||||
HONORMAGICWATCH2(HonorMagicWatch2Coordinator.class),
|
||||
HUAWEIWATCHGT3(HuaweiWatchGT3Coordinator.class),
|
||||
HUAWEIWATCHGT4(HuaweiWatchGT4Coordinator.class),
|
||||
HUAWEIBAND8(HuaweiBand8Coordinator.class),
|
||||
HUAWEIWATCHFIT(HuaweiWatchFitCoordinator.class),
|
||||
HUAWEIWATCHULTIMATE(HuaweiWatchUltimateCoordinator.class),
|
||||
VESC(VescCoordinator.class),
|
||||
BINARY_SENSOR(BinarySensorCoordinator.class),
|
||||
FLIPPER_ZERO(FlipperZeroCoordinator.class),
|
||||
|
@ -0,0 +1,901 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs;
|
||||
|
||||
import static java.lang.Integer.parseInt;
|
||||
import static java.lang.Long.parseLong;
|
||||
import static java.lang.Math.cos;
|
||||
import static java.lang.Math.sqrt;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
class BangleJSActivityTrack {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BangleJSActivityTrack.class);
|
||||
|
||||
static JSONObject compileTracksListRequest(GBDevice device, Context context) {
|
||||
stopAndRestartTimeout(device, context);
|
||||
signalFetchingStarted(device, context);
|
||||
//GB.toast("TYPE_GPS_TRACKS says hi!", Toast.LENGTH_LONG, GB.INFO);
|
||||
|
||||
String lastSyncedID = getLatestFetchedRecorderLog(device);
|
||||
|
||||
JSONObject o = new JSONObject();
|
||||
try {
|
||||
o.put("t", "listRecs");
|
||||
o.put("id", lastSyncedID);
|
||||
//uartTxJSON("requestActivityTracksList", o);
|
||||
} catch (JSONException e) {
|
||||
LOG.error("JSONException: " + e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
private static JSONArray tracksList;
|
||||
static JSONObject handleActTrksList(JSONObject json, GBDevice device, Context context) throws JSONException {
|
||||
stopAndRestartTimeout(device, context);
|
||||
tracksList = json.getJSONArray("list");
|
||||
LOG.debug("trksList says hi!");
|
||||
//GB.toast(getContext(), "trksList says hi!", Toast.LENGTH_LONG, GB.INFO);
|
||||
LOG.info("New recorder logs since last fetch: " + String.valueOf(tracksList));
|
||||
if (tracksList.length()==0) {
|
||||
signalFetchingEnded(device, context);
|
||||
return null;
|
||||
} else {
|
||||
JSONObject requestTrackObj = BangleJSActivityTrack.compileTrackRequest(tracksList.getString(0), 1==tracksList.length());
|
||||
tracksList.remove(0);
|
||||
return requestTrackObj;
|
||||
}
|
||||
}
|
||||
|
||||
private static int lastPacketCount = -1;
|
||||
static JSONObject handleActTrk(JSONObject json, GBDevice device, Context context) throws JSONException {
|
||||
stopAndRestartTimeout(device, context);
|
||||
|
||||
JSONObject returnObj;
|
||||
|
||||
JSONObject stopObj = new JSONObject().put("t","fetchRec").put("id","stop");
|
||||
int currPacketCount;
|
||||
if (!json.has("cnt")) {
|
||||
currPacketCount = 0;
|
||||
} else {
|
||||
currPacketCount = json.getInt("cnt");
|
||||
}
|
||||
if (currPacketCount != lastPacketCount+1) {
|
||||
LOG.error("Activity Track Packets came out of order - aborting.");
|
||||
LOG.debug("packetCount Aborting: " + lastPacketCount);
|
||||
signalFetchingEnded(device, context);
|
||||
stopTimeoutTask();
|
||||
return stopObj;
|
||||
}
|
||||
|
||||
LOG.debug("actTrk says hi!");
|
||||
//GB.toast(context, "actTrk says hi!", Toast.LENGTH_LONG, GB.INFO);
|
||||
String log = json.getString("log");
|
||||
LOG.debug(log);
|
||||
String filename = "recorder.log" + log + ".csv";
|
||||
File dir;
|
||||
try {
|
||||
dir = new File(FileUtils.getExternalFilesDir() + "/" + FileUtils.makeValidFileName(device.getName()));
|
||||
if (!dir.isDirectory()) {
|
||||
if (!dir.mkdir()) {
|
||||
throw new IOException("Cannot create device specific directory for " + device.getName());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed at getting external files directory with error: " + e);
|
||||
resetPacketCount();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!json.has("lines")) { // if no lines were sent with this json object, it signifies that the whole recorder log has been transmitted.
|
||||
setLatestFetchedRecorderLog(log, device);
|
||||
parseFetchedRecorderCSV(dir, filename, log, device, context); // I tried refactoring to parse all fetched logs in one go at the end instead. But that only gave me more troubles. This seems like a more stable approach at least in the Bangle.js case.
|
||||
if (tracksList.length()==0) {
|
||||
signalFetchingEnded(device, context);
|
||||
LOG.debug("packetCount reset1: " + lastPacketCount);
|
||||
returnObj = null;
|
||||
} else {
|
||||
JSONObject requestTrackObj = BangleJSActivityTrack.compileTrackRequest(tracksList.getString(0), 1==tracksList.length());
|
||||
tracksList.remove(0);
|
||||
resetPacketCount();
|
||||
LOG.debug("packetCount reset2: " + lastPacketCount);
|
||||
returnObj = requestTrackObj;
|
||||
}
|
||||
} else { // We received a lines of the csv, now we append it to the file in storage.
|
||||
|
||||
String lines = json.getString("lines");
|
||||
LOG.debug(lines);
|
||||
|
||||
writeToRecorderCSV(lines, dir, filename);
|
||||
|
||||
lastPacketCount += 1;
|
||||
LOG.debug("packetCount continue: " + lastPacketCount);
|
||||
returnObj = null;
|
||||
}
|
||||
|
||||
return returnObj;
|
||||
}
|
||||
|
||||
private static void parseFetchedRecorderCSV(File dir, String filename, String log, GBDevice device, Context context) {
|
||||
stopTimeoutTask(); // Parsing can take a while if there are many data. Restart at end of parsing.
|
||||
|
||||
File inputFile = new File(dir, filename);
|
||||
try { // FIXME: There is maybe code inside this try-statement that should be outside of it.
|
||||
|
||||
// Read from the previously stored log into a string.
|
||||
BufferedReader reader = new BufferedReader(new FileReader(inputFile));
|
||||
StringBuilder storedLogBuilder = new StringBuilder(reader.readLine() + "\n");
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
storedLogBuilder.append(line).append("\n");
|
||||
}
|
||||
reader.close();
|
||||
String storedLog = String.valueOf(storedLogBuilder);
|
||||
storedLog = storedLog.replace(",",", "); // So all rows (internal arrays) in storedLogArray2 get the same number of entries.
|
||||
LOG.debug("Contents of log read from GB storage:\n" + storedLog);
|
||||
|
||||
// Turn the string log into a 2d array in two steps.
|
||||
String[] storedLogArray = storedLog.split("\n") ;
|
||||
String[][] storedLogArray2 = new String[storedLogArray.length][1];
|
||||
|
||||
for (int i = 0; i < storedLogArray.length; i++) {
|
||||
storedLogArray2[i] = storedLogArray[i].split(",");
|
||||
for (int j = 0; j < storedLogArray2[i].length;j++) {
|
||||
storedLogArray2[i][j] = storedLogArray2[i][j].trim(); // Remove the extra spaces we introduced above for getting the same number of entries on all rows.
|
||||
}
|
||||
}
|
||||
|
||||
LOG.debug("Contents of storedLogArray2:\n" + Arrays.deepToString(storedLogArray2));
|
||||
|
||||
// Turn the 2d array into an object for easier access later on.
|
||||
JSONObject storedLogObject = new JSONObject();
|
||||
JSONArray valueArray = new JSONArray();
|
||||
for (int i = 0; i < storedLogArray2[0].length; i++){
|
||||
for (int j = 1; j < storedLogArray2.length; j++) {
|
||||
valueArray.put(storedLogArray2[j][i]);
|
||||
}
|
||||
storedLogObject.put(storedLogArray2[0][i], valueArray);
|
||||
valueArray = new JSONArray();
|
||||
}
|
||||
|
||||
// Clean out heartrate==0...
|
||||
if (storedLogObject.has("Heartrate")) {
|
||||
JSONArray heartrateArray = storedLogObject.getJSONArray("Heartrate");
|
||||
for (int i = 0; i < heartrateArray.length(); i++){
|
||||
if (Objects.equals(heartrateArray.getString(i), "0") ||
|
||||
Objects.equals(heartrateArray.getString(i), "0.0")) {
|
||||
heartrateArray.put(i,"");
|
||||
}
|
||||
}
|
||||
//storedLogObject.remove("Heartrate");
|
||||
storedLogObject.put("Heartrate", heartrateArray);
|
||||
|
||||
}
|
||||
|
||||
LOG.debug("storedLogObject:\n" + storedLogObject);
|
||||
|
||||
// Calculate and store analytical data (distance, speed, cadence, etc.).
|
||||
JSONObject analyticsObject = new JSONObject();
|
||||
JSONArray calculationsArray = new JSONArray();
|
||||
int logLength = storedLogObject.getJSONArray("Time").length();
|
||||
|
||||
// Add elapsed time since first reading (seconds).
|
||||
valueArray = storedLogObject.getJSONArray("Time");
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
calculationsArray.put(valueArray.getDouble(i)-valueArray.getDouble(0));
|
||||
}
|
||||
analyticsObject.put("Elapsed Time", calculationsArray);
|
||||
|
||||
valueArray = new JSONArray();
|
||||
calculationsArray = new JSONArray();
|
||||
|
||||
JSONArray valueArray2 = new JSONArray();
|
||||
|
||||
//LOG.debug("check here 0");
|
||||
// Add analytics based on GPS coordinates.
|
||||
if (storedLogObject.has("Latitude")) {
|
||||
// Add distance between last and current reading.
|
||||
valueArray = storedLogObject.getJSONArray("Latitude");
|
||||
valueArray2 = storedLogObject.getJSONArray("Longitude");
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (i == 0) {
|
||||
calculationsArray.put("0");
|
||||
} else {
|
||||
String distance;
|
||||
if (Objects.equals(valueArray.getString(i), "") ||
|
||||
Objects.equals(valueArray.getString(i - 1), "")) {
|
||||
// FIXME: GPS data can be missing for some entries which is handled here.
|
||||
// Should use more complex logic to be more accurate. Use interpolation.
|
||||
// Should distances be done via the GPX file we generate instead?
|
||||
distance = "0";
|
||||
} else {
|
||||
distance = distanceFromCoordinatePairs(
|
||||
(String) valueArray.get(i - 1),
|
||||
(String) valueArray2.get(i - 1),
|
||||
(String) valueArray.get(i),
|
||||
(String) valueArray2.get(i)
|
||||
);
|
||||
}
|
||||
calculationsArray.put(distance);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Intermediate Distance", calculationsArray);
|
||||
|
||||
valueArray = new JSONArray();
|
||||
valueArray2 = new JSONArray();
|
||||
calculationsArray = new JSONArray();
|
||||
|
||||
//LOG.debug("check here 1");
|
||||
// Add stride lengths between consecutive readings.
|
||||
if (storedLogObject.has("Steps")) {
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (Objects.equals(storedLogObject.getJSONArray("Steps").getString(i), "0") ||
|
||||
Objects.equals(storedLogObject.getJSONArray("Steps").getString(i), "")) {
|
||||
calculationsArray.put("");
|
||||
} else if (Objects.equals(analyticsObject.getJSONArray("Intermediate Distance").getString(i), "0")) {
|
||||
calculationsArray.put("0");
|
||||
} else {
|
||||
double steps = storedLogObject.getJSONArray("Steps").getDouble(i);
|
||||
double calculation =
|
||||
analyticsObject.getJSONArray("Intermediate Distance").getDouble(i) / steps;
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Stride", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
}
|
||||
|
||||
//LOG.debug("check here 2");
|
||||
} else if (storedLogObject.has("Steps")) {
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (i==0 ||
|
||||
Objects.equals(storedLogObject.getJSONArray("Steps").getString(i), "0") ||
|
||||
Objects.equals(storedLogObject.getJSONArray("Steps").getString(i), "")) {
|
||||
calculationsArray.put(0);
|
||||
} else {
|
||||
double avgStep = (0.67+0.762)/2; // https://marathonhandbook.com/average-stride-length/ (female+male)/2
|
||||
double stride = 2*avgStep; // TODO: Depend on user defined stride length?
|
||||
double calculation = stride * (storedLogObject.getJSONArray("Steps").getDouble(i));
|
||||
//if (calculation == 0) calculation = 0.001; // To avoid potential division by zero later on.
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Intermediate Distance", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
|
||||
}
|
||||
|
||||
//LOG.debug("check here 3");
|
||||
if (analyticsObject.has("Intermediate Distance")) {
|
||||
// Add total distance from start of activity up to each reading.
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (i==0) {
|
||||
calculationsArray.put(0);
|
||||
} else {
|
||||
double calculation = calculationsArray.getDouble(i-1) + analyticsObject.getJSONArray("Intermediate Distance").getDouble(i);
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Total Distance", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
|
||||
//LOG.debug("check here 4");
|
||||
// Add average speed between last and current reading (m/s).
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (i==0) {
|
||||
calculationsArray.put("");
|
||||
} else {
|
||||
double timeDiff =
|
||||
(analyticsObject.getJSONArray("Elapsed Time").getDouble(i) -
|
||||
analyticsObject.getJSONArray("Elapsed Time").getDouble(i-1));
|
||||
if (timeDiff==0) timeDiff = 1; // On older versions of the Recorder Bangle.js app the time reporting could be the same for two data points due to rounding.
|
||||
double calculation =
|
||||
analyticsObject.getJSONArray("Intermediate Distance").getDouble(i) / timeDiff;
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
//LOG.debug("check " + calculationsArray);
|
||||
analyticsObject.put("Speed", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
|
||||
//LOG.debug("check here 5");
|
||||
// Add average pace between last and current reading (s/km). (Was gonna do this as min/km but summary seems to expect s/km).
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
String speed = analyticsObject.getJSONArray("Speed").getString(i);
|
||||
//LOG.debug("check: " + speed);
|
||||
if (i==0 || Objects.equals(speed, "0") || Objects.equals(speed, "0.0") || Objects.equals(speed, "")) {
|
||||
calculationsArray.put("");
|
||||
} else {
|
||||
double calculation = (1000.0) * 1/ analyticsObject.getJSONArray("Speed").getDouble(i);
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Pace", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
}
|
||||
|
||||
//LOG.debug("check here 6");
|
||||
if (storedLogObject.has("Steps")) {
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (i==0 || Objects.equals(storedLogObject.getJSONArray("Steps").getString(i), "")) {
|
||||
calculationsArray.put(0);
|
||||
} else {
|
||||
// FIXME: Should cadence be steps/min or half that? https://www.polar.com/blog/what-is-running-cadence/
|
||||
// The Bangle.js App Loader has Cadence = (steps/min)/2, https://github.com/espruino/BangleApps/blob/master/apps/recorder/interface.html#L103,
|
||||
// as discussed here: https://github.com/espruino/BangleApps/pull/3068#issuecomment-1790293879 .
|
||||
double timeDiff =
|
||||
(storedLogObject.getJSONArray("Time").getDouble(i) -
|
||||
storedLogObject.getJSONArray("Time").getDouble(i-1));
|
||||
if (timeDiff==0) timeDiff = 1;
|
||||
double calculation = 0.5 * 60 *
|
||||
(storedLogObject.getJSONArray("Steps").getDouble(i) / timeDiff);
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Cadence", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
}
|
||||
//LOG.debug("check here AnalyticsObject:\n" + analyticsObject.toString());
|
||||
|
||||
//LOG.debug("check here 7");
|
||||
BaseActivitySummary summary = null;
|
||||
|
||||
Date startTime = new Date(parseLong(storedLogArray2[1][0].split("\\.\\d")[0])*1000L);
|
||||
Date endTime = new Date(parseLong(storedLogArray2[storedLogArray2.length-1][0].split("\\.\\d")[0])*1000L);
|
||||
summary = new BaseActivitySummary();
|
||||
summary.setName(log);
|
||||
summary.setStartTime(startTime);
|
||||
summary.setEndTime(endTime);
|
||||
int activityKind;
|
||||
if (analyticsObject.has("Speed")) {
|
||||
if ((float) 3 > averageOfJSONArray(analyticsObject.getJSONArray("Speed"))) {
|
||||
activityKind = ActivityKind.TYPE_WALKING;
|
||||
} else {
|
||||
activityKind = ActivityKind.TYPE_RUNNING;
|
||||
}
|
||||
} else {
|
||||
activityKind = ActivityKind.TYPE_ACTIVITY;
|
||||
}
|
||||
summary.setActivityKind(activityKind); // TODO: Make this depend on info from watch (currently this info isn't supplied in Bangle.js recorder logs).
|
||||
summary.setRawDetailsPath(String.valueOf(inputFile));
|
||||
|
||||
|
||||
// FIXME: Many summaryData entries are commented out below. They currently don't report feasible results. Logic and calculation inside this function needs to be fixed.
|
||||
JSONObject summaryData = new JSONObject();
|
||||
// put("Activity", Arrays.asList(
|
||||
// "distanceMeters", "steps", "activeSeconds", "caloriesBurnt", "totalStride",
|
||||
// "averageHR", "maxHR", "minHR", "averageStride", "maxStride", "minStride"
|
||||
// ));
|
||||
if (analyticsObject.has("Intermediate Distance")) summaryData =
|
||||
addSummaryData(summaryData, ActivitySummaryEntries.DISTANCE_METERS,
|
||||
(float) analyticsObject.getJSONArray("Total Distance").getDouble(logLength - 1),
|
||||
"m");
|
||||
if (storedLogObject.has("Steps"))
|
||||
summaryData = addSummaryData(summaryData, "steps", sumOfJSONArray(storedLogObject.getJSONArray("Steps")), "steps");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.ACTIVE_SECONDS,3,"mm"); // FIXME: Is this suppose to exclude the time of inactivity in a workout?
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.CALORIES_BURNT,3,"mm"); // TODO: Should this be calculated on Gadgetbridge side or be reported by Bangle.js?
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.STRIDE_TOTAL,3,"mm"); // FIXME: What is this?
|
||||
if (storedLogObject.has("Heartrate")) {
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.HR_AVG, averageOfJSONArray(storedLogObject.getJSONArray("Heartrate")), "bpm");
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.HR_MAX, maxOfJSONArray(storedLogObject.getJSONArray("Heartrate")), "bpm");
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.HR_MIN, minOfJSONArray(storedLogObject.getJSONArray("Heartrate")), "bpm");
|
||||
}
|
||||
if (analyticsObject.has("Stride")) {
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.STRIDE_AVG,
|
||||
(float) (analyticsObject.getJSONArray("Total Distance").getDouble(logLength - 1) /
|
||||
(0.5 * sumOfJSONArray(storedLogObject.getJSONArray("Steps")))),
|
||||
"m"); // FIXME: Is this meant to be stride length as I've assumed?
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.STRIDE_MAX, maxOfJSONArray(analyticsObject.getJSONArray("Stride")), "m");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.STRIDE_MIN, minOfJSONArray(analyticsObject.getJSONArray("Stride")), "m");
|
||||
}
|
||||
|
||||
// put("Speed", Arrays.asList(
|
||||
// "averageSpeed", "maxSpeed", "minSpeed", "averageKMPaceSeconds", "minPace",
|
||||
// "maxPace", "averageSpeed2", "averageCadence", "maxCadence", "minCadence"
|
||||
// ));
|
||||
try {
|
||||
if (analyticsObject.has("Speed")) {
|
||||
summaryData = addSummaryData(summaryData,ActivitySummaryEntries.SPEED_AVG, averageOfJSONArray(analyticsObject.getJSONArray("Speed")),"m/s"); // This seems to be calculated somewhere else automatically.
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.SPEED_MAX, maxOfJSONArray(analyticsObject.getJSONArray("Speed")), "m/s");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.SPEED_MIN, minOfJSONArray(analyticsObject.getJSONArray("Speed")), "m/s");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.PACE_AVG_SECONDS_KM, averageOfJSONArray(analyticsObject.getJSONArray("Pace")), "s/km"); // Is this also calculated automatically then?
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.PACE_AVG_SECONDS_KM,
|
||||
// (float) (1000.0 * analyticsObject.getJSONArray("Elapsed Time").getDouble(logLength-1) /
|
||||
// analyticsObject.getJSONArray("Total Distance").getDouble(logLength-1)),
|
||||
// "s/km"
|
||||
//);
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.PACE_MIN, maxOfJSONArray(analyticsObject.getJSONArray("Pace")), "s/km");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.PACE_MAX, minOfJSONArray(analyticsObject.getJSONArray("Pace")), "s/km");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.averageSpeed2,3,"mm");
|
||||
}
|
||||
if (analyticsObject.has("Cadence")) {
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.averageCadence, averageOfJSONArray(analyticsObject.getJSONArray("Cadence")), "cycles/min"); // Is this also calculated automatically then?
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.CADENCE_AVG,
|
||||
(float) 0.5 * 60 * sumOfJSONArray(storedLogObject.getJSONArray("Steps")) /
|
||||
(float) analyticsObject.getJSONArray("Elapsed Time").getDouble(logLength - 1),
|
||||
"cycles/min"
|
||||
);
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.CADENCE_MAX, maxOfJSONArray(analyticsObject.getJSONArray("Cadence")), "cycles/min");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.CADENCE_MIN, minOfJSONArray(analyticsObject.getJSONArray("Cadence")), "cycles/min");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error(e + ". (thrown when trying to add summary data");
|
||||
}
|
||||
// private JSONObject createActivitySummaryGroups(){
|
||||
// final Map<String, List<String>> groupDefinitions = new HashMap<String, List<String>>() {{
|
||||
// put("Strokes", Arrays.asList(
|
||||
// "averageStrokeDistance", "averageStrokesPerSecond", "strokes"
|
||||
// ));
|
||||
|
||||
// put("Swimming", Arrays.asList(
|
||||
// "swolfIndex", "swimStyle"
|
||||
// ));
|
||||
|
||||
// put("Elevation", Arrays.asList(
|
||||
// "ascentMeters", "descentMeters", "maxAltitude", "minAltitude", "averageAltitude",
|
||||
// "baseAltitude", "ascentSeconds", "descentSeconds", "flatSeconds", "ascentDistance",
|
||||
// "descentDistance", "flatDistance", "elevationGain", "elevationLoss"
|
||||
// ));
|
||||
//}
|
||||
if (storedLogObject.has("Altitude") || storedLogObject.has("Barometer Altitude")) {
|
||||
String altitudeToUseKey = null;
|
||||
if (storedLogObject.has("Altitude")) {
|
||||
altitudeToUseKey = "Altitude";
|
||||
} else if (storedLogObject.has("Barometer Altitude")) {
|
||||
altitudeToUseKey = "Barometer Altitude";
|
||||
}
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ASCENT_METERS, 3, "m");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ASCENT_DISTANCE, 3, "m");
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ALTITUDE_MAX, maxOfJSONArray(storedLogObject.getJSONArray(altitudeToUseKey)), "m");
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ALTITUDE_MIN, minOfJSONArray(storedLogObject.getJSONArray(altitudeToUseKey)), "m");
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ALTITUDE_AVG, averageOfJSONArray(storedLogObject.getJSONArray(altitudeToUseKey)), "m");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ALTITUDE_BASE, 3, "m");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ASCENT_SECONDS, 3, "s");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.DESCENT_SECONDS, 3, "s");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.FLAT_SECONDS, 3, "s");
|
||||
//if (analyticsObject.has("Intermittent Distance")) {
|
||||
// summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ASCENT_DISTANCE, 3, "m");
|
||||
// summaryData = addSummaryData(summaryData, ActivitySummaryEntries.DESCENT_DISTANCE, 3, "m");
|
||||
// summaryData = addSummaryData(summaryData, ActivitySummaryEntries.FLAT_DISTANCE, 3, "m");
|
||||
//}
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ELEVATION_GAIN, 3, "mm");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ELEVATION_LOGG, 3, "mm");
|
||||
}
|
||||
// put("HeartRateZones", Arrays.asList(
|
||||
// "hrZoneNa", "hrZoneWarmUp", "hrZoneFatBurn", "hrZoneAerobic", "hrZoneAnaerobic",
|
||||
// "hrZoneExtreme"
|
||||
// ));
|
||||
// TODO: Implement hrZones by doing calculations on Gadgetbridge side or make Bangle.js report this (Karvonen method implemented to a degree in watch app "Run+")?
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_NA,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_WARM_UP,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_FAT_BURN,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_AEROBIC,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_ANAEROBIC,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_EXTREME,3,"mm");
|
||||
// put("TrainingEffect", Arrays.asList(
|
||||
// "aerobicTrainingEffect", "anaerobicTrainingEffect", "currentWorkoutLoad",
|
||||
// "maximumOxygenUptake"
|
||||
// ));
|
||||
|
||||
// put("Laps", Arrays.asList(
|
||||
// "averageLapPace", "laps"
|
||||
// ));
|
||||
// TODO: Does Bangle.js report laps in recorder logs?
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.LAP_PACE_AVERAGE,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.LAPS,3,"mm");
|
||||
// }};
|
||||
summary.setSummaryData(summaryData.toString());
|
||||
|
||||
ActivityTrack track = new ActivityTrack(); // detailsParser.parse(buffer.toByteArray());
|
||||
track.startNewSegment();
|
||||
track.setBaseTime(startTime);
|
||||
track.setName(log);
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
DaoSession session = dbHandler.getDaoSession();
|
||||
Device deviceDB = DBHelper.getDevice(device, session);
|
||||
User user = DBHelper.getUser(session);
|
||||
track.setDevice(deviceDB);
|
||||
track.setUser(user);
|
||||
} catch (Exception ex) {
|
||||
GB.toast(context, "Error setting user for activity track.", Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
}
|
||||
ActivityPoint point = new ActivityPoint();
|
||||
Date timeOfPoint = new Date();
|
||||
boolean hasGPXReading = false;
|
||||
boolean hasHRMReading = false;
|
||||
for (int i = 0; i < storedLogObject.getJSONArray("Time").length(); i++) {
|
||||
timeOfPoint.setTime(storedLogObject.getJSONArray("Time").getLong(i)*1000L);
|
||||
point.setTime((Date) timeOfPoint.clone());
|
||||
if (storedLogObject.has("Longitude")) {
|
||||
if (!Objects.equals(storedLogObject.getJSONArray("Longitude").getString(i), "")
|
||||
&& !Objects.equals(storedLogObject.getJSONArray("Latitude").getString(i), "")
|
||||
&& !Objects.equals(storedLogObject.getJSONArray("Altitude").getString(i), "")) {
|
||||
|
||||
point.setLocation(new GPSCoordinate(
|
||||
storedLogObject.getJSONArray("Longitude").getDouble(i),
|
||||
storedLogObject.getJSONArray("Latitude").getDouble(i),
|
||||
storedLogObject.getJSONArray("Altitude").getDouble(i)
|
||||
)
|
||||
);
|
||||
|
||||
if (!hasGPXReading) hasGPXReading = true;
|
||||
}
|
||||
}
|
||||
if (storedLogObject.has("Heartrate") && !Objects.equals(storedLogObject.getJSONArray("Heartrate").getString(i), "")) {
|
||||
point.setHeartRate(storedLogObject.getJSONArray("Heartrate").getInt(i));
|
||||
|
||||
if (!hasHRMReading) hasHRMReading = true;
|
||||
}
|
||||
track.addTrackPoint(point);
|
||||
LOG.debug("Activity Point:\n" + point.getHeartRate());
|
||||
point = new ActivityPoint();
|
||||
}
|
||||
|
||||
ActivityTrackExporter exporter = createExporter();
|
||||
String trackType = "track";
|
||||
switch (summary.getActivityKind()) {
|
||||
case ActivityKind.TYPE_CYCLING:
|
||||
trackType = context.getString(R.string.activity_type_biking);
|
||||
break;
|
||||
case ActivityKind.TYPE_RUNNING:
|
||||
trackType = context.getString(R.string.activity_type_running);
|
||||
break;
|
||||
case ActivityKind.TYPE_WALKING:
|
||||
trackType = context.getString(R.string.activity_type_walking);
|
||||
break;
|
||||
case ActivityKind.TYPE_HIKING:
|
||||
trackType = context.getString(R.string.activity_type_hiking);
|
||||
break;
|
||||
case ActivityKind.TYPE_CLIMBING:
|
||||
trackType = context.getString(R.string.activity_type_climbing);
|
||||
break;
|
||||
case ActivityKind.TYPE_SWIMMING:
|
||||
trackType = context.getString(R.string.activity_type_swimming);
|
||||
break;
|
||||
}
|
||||
|
||||
String fileName = FileUtils.makeValidFileName("gadgetbridge-" + trackType.toLowerCase() + "-" + summary.getName() + ".gpx");
|
||||
dir = new File(FileUtils.getExternalFilesDir() + "/" + FileUtils.makeValidFileName(device.getName()));
|
||||
File targetFile = new File(dir, fileName);
|
||||
|
||||
if (hasGPXReading /*|| hasHRMReading*/) {
|
||||
try {
|
||||
exporter.performExport(track, targetFile);
|
||||
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
summary.setGpxTrack(targetFile.getAbsolutePath());
|
||||
//dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Could not add gpx track to summary:" + e);
|
||||
}
|
||||
} catch (ActivityTrackExporter.GPXTrackEmptyException ex) {
|
||||
GB.toast(context, "This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
}
|
||||
}
|
||||
|
||||
//summary.setSummaryData(null); // remove json before saving to database,
|
||||
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
DaoSession session = dbHandler.getDaoSession();
|
||||
Device deviceDB = DBHelper.getDevice(device, session);
|
||||
User user = DBHelper.getUser(session);
|
||||
summary.setDevice(deviceDB);
|
||||
summary.setUser(user);
|
||||
session.getBaseActivitySummaryDao().insertOrReplace(summary);
|
||||
} catch (Exception ex) {
|
||||
GB.toast(context, "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
}
|
||||
|
||||
LOG.debug("Activity track:\n" + track.getSegments());
|
||||
|
||||
} catch (IOException e) {
|
||||
LOG.error("IOException when parsing fetched CSV: " + e);
|
||||
} catch (JSONException e) {
|
||||
LOG.error("JSONException when parsing fetched CSV: " + e);
|
||||
}
|
||||
|
||||
stopAndRestartTimeout(device,context);
|
||||
}
|
||||
|
||||
private static void resetPacketCount() {
|
||||
lastPacketCount = -1;
|
||||
}
|
||||
|
||||
private static JSONObject compileTrackRequest(String id, Boolean isLastId) {
|
||||
JSONObject o = new JSONObject();
|
||||
try {
|
||||
o.put("t", "fetchRec");
|
||||
o.put("id", id);
|
||||
o.put("last", String.valueOf(isLastId));
|
||||
} catch (JSONException e) {
|
||||
LOG.error("JSONException: " + e.getLocalizedMessage());
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
private static void signalFetchingStarted(GBDevice device, Context context) {
|
||||
GB.updateTransferNotification(context.getString(R.string.activity_detail_start_label) + " : " + context.getString(R.string.busy_task_fetch_sports_details),"", true, 0, context);
|
||||
device.setBusyTask(context.getString(R.string.busy_task_fetch_sports_details));
|
||||
GB.toast(context.getString(R.string.activity_detail_start_label) + " : " + context.getString(R.string.busy_task_fetch_sports_details), Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
|
||||
private static void signalFetchingEnded(GBDevice device, Context context) {
|
||||
stopTimeoutTask();
|
||||
resetPacketCount();
|
||||
device.unsetBusyTask();
|
||||
device.sendDeviceUpdateIntent(context);
|
||||
GB.updateTransferNotification(null, "", false, 100, context);
|
||||
GB.toast(context.getString(R.string.activity_detail_end_label) + " : " + context.getString(R.string.busy_task_fetch_sports_details), Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
|
||||
private static Timer timeout;
|
||||
private static TimerTask timeoutTask;
|
||||
|
||||
private static void startTimeout(GBDevice device, Context context) {
|
||||
timeout = new Timer();
|
||||
|
||||
initializeTimeoutTask(device, context);
|
||||
|
||||
timeout.schedule(timeoutTask, 5000);
|
||||
}
|
||||
|
||||
private static void stopTimeoutTask() {
|
||||
if (timeout != null) {
|
||||
timeout.cancel();
|
||||
timeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void initializeTimeoutTask(GBDevice device, Context context) {
|
||||
|
||||
timeoutTask = new TimerTask() {
|
||||
public void run() {
|
||||
signalFetchingEnded(device, context);
|
||||
LOG.warn(context.getString(R.string.busy_task_fetch_sports_details_interrupted));
|
||||
GB.toast(context.getString(R.string.busy_task_fetch_sports_details_interrupted), Toast.LENGTH_LONG, GB.INFO);
|
||||
// TODO: We could send a stop message back to Bangle.js here if we want to hinder
|
||||
// the potential event it would start sending lines again after Gadgetbridge
|
||||
// determined the fetch had been interrupted. On the other hand I think if we
|
||||
// started receiving lines again, they would only be appended to the file in
|
||||
// storage and the fetch would continue as if nothing happend.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void stopAndRestartTimeout(GBDevice device, Context context) {
|
||||
stopTimeoutTask();
|
||||
startTimeout(device, context);
|
||||
}
|
||||
|
||||
private static String getLatestFetchedRecorderLog(GBDevice device) {
|
||||
// "lastSportsActivityTimeMillis" is what
|
||||
// `ActivitySummaryActivity.resetFetchTimestampToChosenDate()` uses, so we have to
|
||||
// control if the user changed that value, and if so recompile a new sync id from that info.
|
||||
|
||||
String lastSyncedId = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).
|
||||
getString("lastSportsActivityIdBangleJS","19700101a");
|
||||
LOG.debug("lastSyncedId: " + lastSyncedId);
|
||||
|
||||
long lastSportsActivityTimeMillis = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).
|
||||
getLong("lastSportsActivityTimeMillis",0);
|
||||
LOG.debug("lastSportsActivityTimeMillis: " + lastSportsActivityTimeMillis);
|
||||
|
||||
Calendar dateFromId = parseCalendarFromBangleJSLogId(lastSyncedId);
|
||||
Calendar dateFromMillis = parseCalendarFromMillis(lastSportsActivityTimeMillis);
|
||||
|
||||
LOG.debug("lastSyncedIdMillis: " + dateFromId.getTimeInMillis());
|
||||
|
||||
int intMillis = parseInt(compileDateStringFromCalendar(dateFromMillis));
|
||||
int intBangle = parseInt(lastSyncedId.substring(0,lastSyncedId.length()-1));
|
||||
|
||||
LOG.debug("intMillis: " + intMillis);
|
||||
LOG.debug("intBangle: " + intBangle);
|
||||
|
||||
//if (dateFromMillis.before(dateFromId)) { // This would not work b/c the millis didn't ever become the same in my testing, even if compiled from the same sync id string.
|
||||
if (intMillis < intBangle) {
|
||||
dateFromMillis.add(Calendar.DATE, -1); // We want the day before so we fetch from the day the user reset to.
|
||||
int year = dateFromMillis.get(Calendar.YEAR);
|
||||
int month = dateFromMillis.get(Calendar.MONTH);
|
||||
int dayBefore = dateFromMillis.get(Calendar.DATE);
|
||||
|
||||
|
||||
String yearString = String.valueOf(year);
|
||||
String monthString = String.valueOf(month);
|
||||
if (month<10) monthString = "0" + monthString;
|
||||
String dayBeforeString = String.valueOf(dayBefore);
|
||||
if (dayBefore<10) dayBeforeString = "0" + dayBeforeString;
|
||||
String letter = "z";
|
||||
|
||||
return yearString + monthString + dayBeforeString + letter;
|
||||
} else {
|
||||
return lastSyncedId;
|
||||
}
|
||||
}
|
||||
|
||||
private static void setLatestFetchedRecorderLog(String log, GBDevice device) {
|
||||
Calendar date = parseCalendarFromBangleJSLogId(log);
|
||||
|
||||
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).edit();
|
||||
editor.remove("lastSportsActivityIdBangleJS"); //FIXME: key reconstruction is BAD (FIXME inherited from `ActivitySummaryActivity`.
|
||||
editor.remove("lastSportsActivityTimeMillis"); //FIXME: key reconstruction is BAD
|
||||
editor.putString("lastSportsActivityIdBangleJS", log);
|
||||
editor.putLong("lastSportsActivityTimeMillis", date.getTimeInMillis());
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private static Calendar parseCalendarFromBangleJSLogId(String log) {
|
||||
int year = parseInt(log.substring(0,4));
|
||||
int month = parseInt(log.substring(4,6));
|
||||
int day = parseInt(log.substring(6,8));
|
||||
|
||||
LOG.debug("DateFromId: " + year+ "|"+ month + "|" +day);
|
||||
|
||||
Calendar date = Calendar.getInstance();
|
||||
date.set(year,month,day);
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
private static Calendar parseCalendarFromMillis(long millis) {
|
||||
Calendar date = Calendar.getInstance();
|
||||
date.setTimeInMillis(millis);
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
private static String compileDateStringFromCalendar(Calendar date) {
|
||||
int year = date.get(Calendar.YEAR);
|
||||
int month = date.get(Calendar.MONTH);
|
||||
int day = date.get(Calendar.DATE);
|
||||
|
||||
return String.format("%d%02d%02d", year, month, day);
|
||||
}
|
||||
|
||||
private static void writeToRecorderCSV(String lines, File dir, String filename) {
|
||||
String mode = "append";
|
||||
if (lines.equals("erase")) {
|
||||
mode = "write";
|
||||
lines = "";
|
||||
}
|
||||
|
||||
File outputFile = new File(dir, filename);
|
||||
try {
|
||||
FileUtils.copyStringToFile(lines,outputFile,mode);
|
||||
//GB.toast(context, "Log written to " + filename, Toast.LENGTH_LONG, GB.INFO);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not write to file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static ActivityTrackExporter createExporter() {
|
||||
GPXExporter exporter = new GPXExporter();
|
||||
exporter.setCreator(GBApplication.app().getNameAndVersion());
|
||||
return exporter;
|
||||
}
|
||||
|
||||
private static JSONObject addSummaryData(JSONObject summaryData, String key, float value, String unit) {
|
||||
if (value > 0) {
|
||||
try {
|
||||
JSONObject innerData = new JSONObject();
|
||||
innerData.put("value", value);
|
||||
innerData.put("unit", unit);
|
||||
summaryData.put(key, innerData);
|
||||
} catch (JSONException ignore) {
|
||||
}
|
||||
}
|
||||
return summaryData;
|
||||
}
|
||||
|
||||
// protected JSONObject addSummaryData(JSONObject summaryData, String key, String value) {
|
||||
// if (key != null && !key.equals("") && value != null && !value.equals("")) {
|
||||
// try {
|
||||
// JSONObject innerData = new JSONObject();
|
||||
// innerData.put("value", value);
|
||||
// innerData.put("unit", "string");
|
||||
// summaryData.put(key, innerData);
|
||||
// } catch (JSONException ignore) {
|
||||
// }
|
||||
// }
|
||||
// return summaryData;
|
||||
// }
|
||||
|
||||
private static String distanceFromCoordinatePairs(String latA, String lonA, String latB, String lonB) {
|
||||
// https://en.wikipedia.org/wiki/Geographic_coordinate_system#Length_of_a_degree
|
||||
//phi = latitude
|
||||
//lambda = longitude
|
||||
//length of 1 degree lat:
|
||||
//111132.92 - 559.82*cos(2*phi) + 1.175*cos(4*phi) - 0.0023*cos(6*phi)
|
||||
//length of 1 degree lon:
|
||||
//111412.84*cos(phi) - 93.5*cos(3*phi) + 0.118*cos(5*phi)
|
||||
double latADouble = Double.parseDouble(latA);
|
||||
double latBDouble = Double.parseDouble(latB);
|
||||
double lonADouble = Double.parseDouble(lonA);
|
||||
double lonBDouble = Double.parseDouble(lonB);
|
||||
|
||||
double lengthPerDegreeLat = 111132.92 - 559.82*cos(2*latADouble) + 1.175*cos(4*latADouble) - 0.0023*cos(6*latADouble);
|
||||
double lengthPerDegreeLon = 111412.84*cos(latADouble) - 93.5*cos(3*latADouble) + 0.118*cos(5*latADouble);
|
||||
|
||||
double latDist = (latBDouble-latADouble)*lengthPerDegreeLat;
|
||||
double lonDist = (lonBDouble-lonADouble)*lengthPerDegreeLon;
|
||||
|
||||
return String.valueOf(sqrt(latDist*latDist+lonDist*lonDist));
|
||||
}
|
||||
|
||||
private static float sumOfJSONArray(JSONArray a) throws JSONException {
|
||||
double sum = 0;
|
||||
for (int i=0; i<a.length(); i++) {
|
||||
if (!Objects.equals(a.getString(i), "")) sum += a.getDouble(i);
|
||||
}
|
||||
return (float) sum;
|
||||
}
|
||||
|
||||
private static float averageOfJSONArray(JSONArray a) throws JSONException {
|
||||
JSONArray b = new JSONArray();
|
||||
// Disregard empty lines.
|
||||
for (int i=0; i<a.length(); i++) {
|
||||
if (!Objects.equals(a.getString(i), "")) b.put(a.getString(i));
|
||||
}
|
||||
return sumOfJSONArray(b) / b.length();
|
||||
}
|
||||
|
||||
private static float minOfJSONArray(JSONArray a) throws JSONException {
|
||||
double min = 999999999;
|
||||
for (int i=0; i<a.length(); i++) {
|
||||
if (!Objects.equals(a.getString(i), "")) min = Math.min(min, a.getDouble(i));
|
||||
}
|
||||
return (float) min;
|
||||
}
|
||||
|
||||
private static float maxOfJSONArray(JSONArray a) throws JSONException {
|
||||
double max = -999999999;
|
||||
for (int i=0; i<a.length(); i++) {
|
||||
if (!Objects.equals(a.getString(i), "")) max = Math.max(max, a.getDouble(i));
|
||||
}
|
||||
return (float) max;
|
||||
}
|
||||
|
||||
}
|
@ -505,7 +505,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
} else
|
||||
LOG.warn("UART RX JSON parsed but doesn't contain 't' - ignoring");
|
||||
} catch (JSONException e) {
|
||||
LOG.info("UART RX JSON parse failure: "+ e.getLocalizedMessage());
|
||||
LOG.error("UART RX JSON parse failure: "+ e.getLocalizedMessage());
|
||||
GB.toast(getContext(), "Malformed JSON from Bangle.js: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
|
||||
@ -562,6 +562,14 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
case "act":
|
||||
handleActivity(json);
|
||||
break;
|
||||
case "actTrksList": {
|
||||
JSONObject requestTrackObj = BangleJSActivityTrack.handleActTrksList(json, getDevice(), getContext());
|
||||
if (requestTrackObj!=null) uartTxJSON("requestActivityTrackLog", requestTrackObj);
|
||||
} break;
|
||||
case "actTrk": {
|
||||
JSONObject requestTrackObj = BangleJSActivityTrack.handleActTrk(json, getDevice(), getContext());
|
||||
if (requestTrackObj!=null) uartTxJSON("requestActivityTrackLog", requestTrackObj);
|
||||
} break;
|
||||
case "http":
|
||||
handleHttp(json);
|
||||
break;
|
||||
@ -1388,7 +1396,11 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
fetchActivityData(getLastSuccessfulSyncTime());
|
||||
}
|
||||
|
||||
if ((dataTypes & RecordedDataTypes.TYPE_DEBUGLOGS) != 0) {
|
||||
if ((dataTypes & RecordedDataTypes.TYPE_GPS_TRACKS) !=0) {
|
||||
JSONObject requestTracksListObj = BangleJSActivityTrack.compileTracksListRequest(getDevice(), getContext());
|
||||
uartTxJSON("requestActivityTracksList", requestTracksListObj);
|
||||
}
|
||||
if ((dataTypes & RecordedDataTypes.TYPE_DEBUGLOGS) !=0) {
|
||||
File dir;
|
||||
try {
|
||||
dir = FileUtils.getExternalFilesDir();
|
||||
@ -1397,7 +1409,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US);
|
||||
String filename = "banglejs_debug_" + dateFormat.format(new Date()) + ".log";
|
||||
File outputFile = new File(dir, filename );
|
||||
File outputFile = new File(dir, filename);
|
||||
LOG.warn("Writing log to "+outputFile.toString());
|
||||
try {
|
||||
BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile));
|
||||
|
@ -75,9 +75,11 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.AcceptAgreementsRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetEventAlarmList;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetNotificationConstraintsRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSmartAlarmList;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendExtendedAccountRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsAndTimeToDeviceRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherCurrentRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyHeartRateCapabilityRequest;
|
||||
@ -245,7 +247,7 @@ public class HuaweiSupportProvider {
|
||||
return builder;
|
||||
}
|
||||
|
||||
protected void initializeDevice(Request linkParamsReq) {
|
||||
protected void initializeDevice(final Request linkParamsReq) {
|
||||
deviceMac = this.gbDevice.getAddress();
|
||||
createRandomMacAddress();
|
||||
createAndroidID();
|
||||
@ -275,9 +277,9 @@ public class HuaweiSupportProvider {
|
||||
}
|
||||
}
|
||||
|
||||
protected void initializeDeviceCheckStatus(Request linkParamsReq) {
|
||||
protected void initializeDeviceCheckStatus(final Request linkParamsReq) {
|
||||
try {
|
||||
GetDeviceStatusRequest deviceStatusReq = new GetDeviceStatusRequest(this, true);
|
||||
final GetDeviceStatusRequest deviceStatusReq = new GetDeviceStatusRequest(this, true);
|
||||
RequestCallback finalizeReq = new RequestCallback() {
|
||||
@Override
|
||||
public void call() {
|
||||
@ -329,16 +331,16 @@ public class HuaweiSupportProvider {
|
||||
return (authType ^ 0x01) == 0x04 || (authType ^ 0x02) == 0x04;
|
||||
}
|
||||
|
||||
protected void initializeDeviceDealHiChain(Request linkParamsReq) {
|
||||
protected void initializeDeviceDealHiChain(final Request linkParamsReq) {
|
||||
try {
|
||||
if (isHiChain()) {
|
||||
GetSecurityNegotiationRequest securityNegoReq = new GetSecurityNegotiationRequest(this);
|
||||
final GetSecurityNegotiationRequest securityNegoReq = new GetSecurityNegotiationRequest(this);
|
||||
RequestCallback securityFinalizeReq = new RequestCallback(this) {
|
||||
@Override
|
||||
public void call() {
|
||||
if (securityNegoReq.authType == 0x0186A0 || isHiChain3(securityNegoReq.authType)) {
|
||||
LOG.debug("HiChain mode");
|
||||
initializeDeviceHiChainMode(linkParamsReq);
|
||||
initializeDeviceHiChainMode(securityNegoReq.authType);
|
||||
} else if (securityNegoReq.authType == 0x01 || securityNegoReq.authType == 0x02) {
|
||||
LOG.debug("HiChain Lite mode");
|
||||
// Keep track the gadget is connected
|
||||
@ -368,11 +370,11 @@ public class HuaweiSupportProvider {
|
||||
}
|
||||
};
|
||||
|
||||
protected void initializeDeviceHiChainMode(Request linkParamsReq) {
|
||||
protected void initializeDeviceHiChainMode(int authType) {
|
||||
try {
|
||||
GetHiChainRequest hiChainReq = new GetHiChainRequest(this, needsAuth);
|
||||
hiChainReq.setFinalizeReq(configureReq);
|
||||
if (((GetLinkParamsRequest)linkParamsReq).bondState == 0x00 || ((GetLinkParamsRequest)linkParamsReq).bondState == 0x02) {
|
||||
if (paramsProvider.getPinCode() == null & ((authType ^ 0x04) == 0x01) ) {
|
||||
GetPincodeRequest pincodeReq = new GetPincodeRequest(this);
|
||||
pincodeReq.nextRequest(hiChainReq);
|
||||
pincodeReq.doPerform();
|
||||
@ -595,7 +597,19 @@ public class HuaweiSupportProvider {
|
||||
GetExpandCapabilityRequest expandCapabilityReq = new GetExpandCapabilityRequest(this);
|
||||
expandCapabilityReq.doPerform();
|
||||
}
|
||||
if (getHuaweiCoordinator().supportsAccount()) { // GetAccountJudgment
|
||||
if (getHuaweiCoordinator().supportsAccountJudgment() && getHuaweiCoordinator().supportsAccountSwitch()) {
|
||||
SendExtendedAccountRequest sendExtendedAccountRequest = new SendExtendedAccountRequest(this);
|
||||
sendExtendedAccountRequest.doPerform();
|
||||
}
|
||||
if (getHuaweiCoordinator().supportsSettingRelated()) { // GetSettingRelated
|
||||
GetSettingRelatedRequest getSettingRelatedReq = new GetSettingRelatedRequest(this);
|
||||
getSettingRelatedReq.doPerform();
|
||||
}
|
||||
if (getHuaweiCoordinator().supportsAcceptAgreement()) {
|
||||
AcceptAgreementsRequest acceptAgreementsRequest = new AcceptAgreementsRequest(this);
|
||||
acceptAgreementsRequest.doPerform();
|
||||
}
|
||||
if (getHuaweiCoordinator().supportsAccount()) {
|
||||
SendAccountRequest sendAccountReq = new SendAccountRequest(this);
|
||||
sendAccountReq.doPerform();
|
||||
}
|
||||
@ -603,10 +617,7 @@ public class HuaweiSupportProvider {
|
||||
GetActivityTypeRequest activityTypeReq = new GetActivityTypeRequest(this);
|
||||
activityTypeReq.doPerform();
|
||||
}
|
||||
if (getHuaweiCoordinator().supportsSettingRelated()) { // GetSettingRelated
|
||||
GetSettingRelatedRequest getSettingRelatedReq = new GetSettingRelatedRequest(this);
|
||||
getSettingRelatedReq.doPerform();
|
||||
}
|
||||
|
||||
if (getHuaweiCoordinator().supportsConnectStatus()) {
|
||||
GetConnectStatusRequest getConnectStatusReq = new GetConnectStatusRequest(this);
|
||||
getConnectStatusReq.doPerform();
|
||||
|
@ -0,0 +1,54 @@
|
||||
/* Copyright (C) 2024 Vitalii Tomin
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig.AcceptAgreement;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms.SmartAlarmRequest;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class AcceptAgreementsRequest extends Request {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AcceptAgreementsRequest.class);
|
||||
|
||||
|
||||
public AcceptAgreementsRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.AcceptAgreement.id;
|
||||
}
|
||||
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.AcceptAgreement.Request(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/* Copyright (C) 2024 Vitalii Tomin
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendExtendedAccountRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendExtendedAccountRequest.class);
|
||||
|
||||
public SendExtendedAccountRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = AccountRelated.id;
|
||||
this.commandId = AccountRelated.SendExtendedAccountToDevice.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new AccountRelated.SendExtendedAccountToDevice.Request(
|
||||
paramsProvider,
|
||||
supportProvider.getHuaweiCoordinator().supportsDiffAccountPairingOptimization())
|
||||
.serialize();
|
||||
} catch (CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws Request.ResponseParseException {
|
||||
LOG.debug("handle Send Extended Account to Device");
|
||||
}
|
||||
}
|
@ -22,17 +22,20 @@ import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@ -41,6 +44,7 @@ import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBEnvironment;
|
||||
@ -87,6 +91,21 @@ public class FileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the contents of the given string to the destination file.
|
||||
* @param string the contents to write.
|
||||
* @param dst the file to write to
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void copyStringToFile(String string, File dst, String mode) throws IOException {
|
||||
boolean append = true;
|
||||
if (!Objects.equals(mode, "append")) append = false;
|
||||
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(dst, append))) {
|
||||
writer.write(string);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the contents of the given file to the destination output stream.
|
||||
* @param src the file from which to read.
|
||||
|
@ -649,6 +649,7 @@
|
||||
<string name="busy_task_fetch_activity_data">Fetching activity data</string>
|
||||
<string name="busy_task_fetch_sports_summaries">Fetching sports summaries</string>
|
||||
<string name="busy_task_fetch_sports_details">Fetching sports details</string>
|
||||
<string name="busy_task_fetch_sports_details_interrupted">Fetching sports details was interrupted</string>
|
||||
<string name="busy_task_fetch_debug_logs">Fetching debug logs</string>
|
||||
<string name="busy_task_fetch_stress_data">Fetching stress data</string>
|
||||
<string name="busy_task_fetch_pai_data">Fetching PAI data</string>
|
||||
@ -1543,7 +1544,9 @@
|
||||
<string name="devicetype_huawei_watchgt2e">Huawei Watch GT 2e</string>
|
||||
<string name="devicetype_huawei_talk_band_b6">Huawei Talk Band B6</string>
|
||||
<string name="devicetype_huawei_watchgt3">Huawei Watch GT 3 (Pro)</string>
|
||||
<string name="devicetype_huawei_watchgt4">Huawei Watch GT 4</string>
|
||||
<string name="devicetype_huawei_watchfit">Huawei Watch Fit</string>
|
||||
<string name="devicetype_huawei_watchultimate">Huawei Watch Ultimate</string>
|
||||
<string name="devicetype_femometer_vinca2">Femometer Vinca II</string>
|
||||
<string name="devicetype_xiaomi_watch_lite">Xiaomi Watch Lite</string>
|
||||
<string name="devicetype_redmiwatch3active">Redmi Watch 3 Active</string>
|
||||
|
Loading…
Reference in New Issue
Block a user