Compare commits

...

92 Commits

Author SHA1 Message Date
José Rebelo
bc940ceddb Update changelog 2024-03-21 23:46:28 +00:00
Ganblejs
de1faf6f00 Bangle.js:actTrk:replace RuntimeException with LOG 2024-03-21 23:28:29 +00:00
Ganblejs
ac4273fc1b Bangle.js:actTrk:dont apnd device mac str to files 2024-03-21 23:28:29 +00:00
Ganblejs
5c183da78c Bangle.js:actTrk: store files in device subfolder 2024-03-21 23:28:29 +00:00
Ganblejs
5540dc18a4 Bangle.js:actTrk: use last 4 chars of mac in names
... of files
2024-03-21 23:28:29 +00:00
Ganblejs
2803c62a7c Bangle.js:actTrk: add first timestamp to gpx name 2024-03-21 23:28:29 +00:00
Ganblejs
cd2535f49a Bangle.js:actTrk: fix activity point times 2024-03-21 23:28:29 +00:00
Ganblejs
f0826286dc Bangle.js:actTrk:add error msg re get file dir 2024-03-21 23:28:29 +00:00
Ganblejs
0ac6da3903 Bangle.js:ActTrk: fix requestTrackObj can be null 2024-03-21 23:28:29 +00:00
Ganblejs
9309ac8d38 Bangle.js:actTrk: comment out LAPS summary info 2024-03-21 23:28:29 +00:00
Ganblejs
1e2ea97391 Bangle.js:actTrk: curly brace to scope cases 2024-03-21 23:28:29 +00:00
Ganblejs
571410ff4b Bangle.js:actTrk: don't throw RuntimeException 2024-03-21 23:28:29 +00:00
Ganblejs
f3f9a75633 Bangle.js:actTrk: dataflow tweaks 2024-03-21 23:28:29 +00:00
Ganblejs
d2e61c5fe8 Bangle.js:actTrk: move tracksList inside class 2024-03-21 23:28:29 +00:00
Ganblejs
1660f4b7fa Bangle.js:actTrk: move package count inside class 2024-03-21 23:28:29 +00:00
Ganblejs
6eb97eeb15 Bangle.js:actTrk: see dataTypes as bitmask 2024-03-21 23:28:29 +00:00
Ganblejs
d94ac25cd1 Bangle.js:actTrk:refactor compileDateStringFromCal 2024-03-21 23:28:29 +00:00
Ganblejs
cd8fad29b1 Bangle.js:actTrk: reference ActivitySummaryEntries 2024-03-21 23:28:29 +00:00
Ganblejs
8715cc7e81 Bangle.js:actTrk:dont extend BangleJSDeviceSupport 2024-03-21 23:28:29 +00:00
Ganblejs
7182f4be67 Bangle.js:actTrk:cmnt out some faulty summary data 2024-03-21 23:28:29 +00:00
Ganblejs
35021f28fa Bangle.js:actTrk: fix 'Reset fetch date' function 2024-03-21 23:28:29 +00:00
Ganblejs
c2a4eed13a Bangle.js:actTrk: pckts out of order->stop timeout 2024-03-21 23:28:29 +00:00
Ganblejs
cd9fdca397 Bangle.js:actTrk: cmt re send "stop" on interrupt 2024-03-21 23:28:29 +00:00
Ganblejs
21882042c4 Bangle.js:actTrk: fix fastest/slowest pace 2024-03-21 23:28:29 +00:00
Ganblejs
9f2df0262f Bangle.js:actTrk: refactor LOG.info to debug/error 2024-03-21 23:28:29 +00:00
Ganblejs
8060911d42 Bangle.js:actTrk: move private methods down 2024-03-21 23:28:29 +00:00
Ganblejs
70a7d8f8da Bangle.js:actTrk: add private modifier 2024-03-21 23:28:29 +00:00
Ganblejs
546102f01f Bangle.js:actTrk: accommodate "erase" keyword 2024-03-21 23:28:29 +00:00
Ganblejs
e7f8c9e7a6 Bangle.js:actTrk:upd cmt re fetch all then parse 2024-03-21 23:28:29 +00:00
Ganblejs
7734fbe4b0 Revert "Bangle.js:actTrk:fetch all logs, then parse them"
This reverts commit 6b941f9277f70d642cfaa430d3d917ba6b559d17.
2024-03-21 23:28:29 +00:00
Ganblejs
6936053734 Revert "Bangle.js:actTrk: try fix parsing after interrupt"
This reverts commit 638f12e96bdb44445902b3bbd477c742e1d88299.
2024-03-21 23:28:29 +00:00
Ganblejs
d93ef074c3 Bangle.js:actTrk: try fix parsing after interrupt 2024-03-21 23:28:29 +00:00
Ganblejs
46be3c47f9 Bangle.js:actTrk:fetch all logs, then parse them 2024-03-21 23:28:29 +00:00
Ganblejs
d8172295cb Bangle.js:actTrk: note on postponing parsing 2024-03-21 23:28:29 +00:00
Ganblejs
ddca4ad90d Bangle.js:actTrk: remove unrelated comments 2024-03-21 23:28:29 +00:00
Ganblejs
a077e867b4 Bangle.js:actTrk:rm newline on latest fetch string 2024-03-21 23:28:29 +00:00
Ganblejs
6ce8f336b4 Bangle.js:actTrk: break out file operations 2024-03-21 23:28:29 +00:00
Ganblejs
f1053c5b80 Bangle.js:actTrk: add private modifier 2024-03-21 23:28:29 +00:00
Ganblejs
f97dda37c7 Bangle.js:actTrk: simplify timeout 2024-03-21 23:28:29 +00:00
Ganblejs
c250a70196 Bangle.js:actTrk: remove public modifier 2024-03-21 23:28:29 +00:00
Ganblejs
0c10edb9f2 Bangle.js:actTrk: small refactor of return 2024-03-21 23:28:29 +00:00
Ganblejs
49bd363d0f Bangle.js:actTrk: no timeout during data-parsing 2024-03-21 23:28:29 +00:00
Ganblejs
8ab77f148f Bangle.js:actTrk: speed determines activity type 2024-03-21 23:28:29 +00:00
Ganblejs
466349fbe7 Bangle.js:actTrk: refactor a name 2024-03-21 23:28:29 +00:00
Ganblejs
2683fd9495 Bangle.js:actTrk:add LOG.warn re interrupted fetch 2024-03-21 23:28:29 +00:00
Ganblejs
6a9c037926 Bangle.js:actTrk: notify on fetch interrupted 2024-03-21 23:28:29 +00:00
Ganblejs
af740d4dc3 Bangle.js:actTrk: fix timeout for interruptions 2024-03-21 23:28:29 +00:00
Ganblejs
4f04b61dbd Bangle.js:actTrk:unsuccessful try at fetch timeout 2024-03-21 23:28:29 +00:00
Ganblejs
49c4f34f42 Bangle.js:actTrk: refactor function names 2024-03-21 23:28:29 +00:00
Ganblejs
945a28c279 Bangle.js:actTrk: tweak start/stop messagaging 2024-03-21 23:28:29 +00:00
Ganblejs
40c3eade21 Bangle.js:actTrk:fix transfer notification 2024-03-21 23:28:29 +00:00
Ganblejs
bfa5d94587 Bangle.js:actTrk: Auto-reload when fetch done 2024-03-21 23:28:29 +00:00
Ganblejs
e492160531 Bangle.js:actTrk: fix packet counting 2024-03-21 23:28:29 +00:00
Ganblejs
8338b03b3b Bangle.js:actTrk:try at packet counting
... might not be necessary. Since I got the fetching to work with
intervals on the the Bangle.js side it's been stable.

Didn't manage to make packet counting work yet.
2024-03-21 23:28:29 +00:00
Ganblejs
ddc2d7a34a Bangle.js: remove imports marked as unused 2024-03-21 23:28:29 +00:00
Ganblejs
3ac3519f3e Bangle.js:break out to class BangleJSActivityTrack 2024-03-21 23:28:29 +00:00
Ganblejs
9919bb2d6f Bangle.js: actTrk- fix logic re requesting logs 2024-03-21 23:28:29 +00:00
Ganblejs
7146fce683 Bangle.js: actTrk- sync one log at a time 2024-03-21 23:28:29 +00:00
Ganblejs
ee274510ac Bangle.js:actTrk- check if HRM could be exported
... to GPX track file. But commented out to avoid error.
2024-03-21 23:28:29 +00:00
Ganblejs
64b3468671 Bangle.js:actTrk- don't export GPX if no such data 2024-03-21 23:28:29 +00:00
Ganblejs
19ec7344ca Bangle.js: comment out some LOG.info 2024-03-21 23:28:29 +00:00
Ganblejs
2c918f6a7c Bangle.js: actTrk receive multiple lines per UARTRX 2024-03-21 23:28:29 +00:00
Ganblejs
d3229dea96 Bangle.js: clear fetched csv if refetching 2024-03-21 23:28:29 +00:00
Ganblejs
6255ff615d Bangle.js: activity track logic tweak
... to make string sent from Bangle.js shorter. And some other changes.
2024-03-21 23:28:29 +00:00
Ganblejs
c572cae161 Bangle.js: use FileUtils to read/write files 2024-03-21 23:28:29 +00:00
Ganblejs
cba2cf38f6 Bangle.js:actTrck change stride unit to only meter 2024-03-21 23:28:29 +00:00
Ganblejs
2429f64412 Bangle.js: avoid infinities, cardiac arrest, etc. 2024-03-21 23:28:29 +00:00
Ganblejs
c2c53d5495 Bangle.js: actTrack reorder activity details 2024-03-21 23:28:29 +00:00
Ganblejs
e23c5f2dd4 Bangle.js: actTrack handle empty lines on analyze 2024-03-21 23:28:29 +00:00
Ganblejs
acdde68fab Bangle.js: handle if time was given with decimals 2024-03-21 23:28:29 +00:00
Ganblejs
e39e67100e Bangle.js: actTrk WIP 2024-03-21 23:28:29 +00:00
Ganblejs
bcd1fc8681 Bangle.js: more work on summary data 2024-03-21 23:28:29 +00:00
Ganblejs
e10fae00cf Bangle.js: activity tracks summary entries 2024-03-21 23:28:29 +00:00
Ganblejs
eec3d2b89a Bangle.js:don't show stride if not sufficient data 2024-03-21 23:28:29 +00:00
Ganblejs
6d82980cc3 Bangle.js: Activity tracks analytics 2024-03-21 23:28:29 +00:00
Ganblejs
70b9911153 Bangle.js: WIP adding summary data
Calculate distances from lat/long coordinates.
2024-03-21 23:28:29 +00:00
Ganblejs
b4879a92d9 Bangle.js: Activity tracks, addSummaryData 2024-03-21 23:28:29 +00:00
Ganblejs
517f9ac214 Bangle.js:Activity traks attempt (un-)setBusyTask 2024-03-21 23:28:29 +00:00
Ganblejs
6faa4452e2 Bangle.js:Act Tracks follow refactored code style 2024-03-21 23:28:29 +00:00
Ganblejs
c6cec7a0f8 Bangle.js:WIP add activity tracks support
Bangle.js: WIP add supportsActivityTracks

Bangle.js: testing flow of info

Bangle.js:WIP receive and store csv from Bangle.js

Bangle.js:store and transmit ID of last synced log

bangle.js:activity tracks, act on completed fetch

... of the recorder csv file.

Bangle.js: Activity tracks, now in database

... but not all data is persisted correctly I think. It's presented as
'Unknown activity'.

Bangle.js:Activity tracks, try to add gps info

I haven't tested with recordings where I have gps values, so far only
empty values. With empty values I currently get "This activity does not
contain GPX tracks" when trying to use the GPXExporter.

Bangle.js: Activity tracks, now adds GPS points

... to the activity to be shown when on the "Sport Activity Detail"
screen.
2024-03-21 23:28:29 +00:00
Ganblejs
fc9d18100d ActivitySummariesActivity:fix no fetch btn spinner 2024-03-21 23:28:29 +00:00
Ganblejs
a59b5de343 ActivitySummariesActivity:FIXME unreachable reload 2024-03-21 23:28:29 +00:00
Ganblejs
57c0c4dfcb FileUtils: refactor copyStringToFile
With the goal to hinder potential leak of file.
2024-03-21 23:28:29 +00:00
Ganblejs
758e9223d1 FileUtils:copyStringToFile: Try not to leak file. 2024-03-21 23:28:29 +00:00
Ganblejs
8907757674 FileUtils: add description for copyStringtoFile
... and remove commented out code
2024-03-21 23:28:29 +00:00
Ganblejs
3c32636089 FileUtils: Add copyStringToFile method 2024-03-21 23:28:29 +00:00
Damien 'Psolyca' Gaignon
717bc3035d
[Huawei] Add settings to new gadgets 2024-03-21 19:06:03 +01:00
Vitaliy Tomin
6167f3f781
[Huawei] Initial AcceptAgreement packet and request 2024-03-21 19:06:03 +01:00
Damien 'Psolyca' Gaignon
924088e5a3
[Huawei] Check pinCode and authType not BondState 2024-03-21 19:06:03 +01:00
Damien 'Psolyca' Gaignon
88043aa813
[Huawei] Add Huawei Watch Ultimate support 2024-03-21 19:06:02 +01:00
Vitaliy Tomin
014b453693
[Huawei] Add Huawei Watch GT4 support 2024-03-20 13:41:49 +01:00
Damien 'Psolyca' Gaignon
d59b7c7bfa
[Huawei] Add SendExtendedAccountRequest 2024-03-20 13:41:49 +01:00
19 changed files with 1328 additions and 21 deletions

View File

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

View File

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

View File

@ -107,6 +107,11 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
return true;
}
@Override
public boolean supportsActivityTracks() {
return true;
}
@Override
public boolean supportsScreenshots() {
return false;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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