mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-02-02 21:17:32 +01:00
Zepp OS: Implement workout fetching
This commit is contained in:
parent
7f4bd16914
commit
d1ae6cf225
@ -43,7 +43,7 @@ public class GBDaoGenerator {
|
|||||||
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
final Schema schema = new Schema(43, MAIN_PACKAGE + ".entities");
|
final Schema schema = new Schema(44, MAIN_PACKAGE + ".entities");
|
||||||
|
|
||||||
Entity userAttributes = addUserAttributes(schema);
|
Entity userAttributes = addUserAttributes(schema);
|
||||||
Entity user = addUserInfo(schema, userAttributes);
|
Entity user = addUserInfo(schema, userAttributes);
|
||||||
@ -638,6 +638,7 @@ public class GBDaoGenerator {
|
|||||||
summary.addIntProperty("baseAltitude").javaDocGetterAndSetter("Temporary, bip-specific");
|
summary.addIntProperty("baseAltitude").javaDocGetterAndSetter("Temporary, bip-specific");
|
||||||
|
|
||||||
summary.addStringProperty("gpxTrack").codeBeforeGetter(OVERRIDE);
|
summary.addStringProperty("gpxTrack").codeBeforeGetter(OVERRIDE);
|
||||||
|
summary.addStringProperty("rawDetailsPath");
|
||||||
|
|
||||||
Property deviceId = summary.addLongProperty("deviceId").notNull().codeBeforeGetter(OVERRIDE).getProperty();
|
Property deviceId = summary.addLongProperty("deviceId").notNull().codeBeforeGetter(OVERRIDE).getProperty();
|
||||||
summary.addToOne(device, deviceId);
|
summary.addToOne(device, deviceId);
|
||||||
|
@ -336,6 +336,17 @@ tasks.withType(SpotBugsTask) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task cleanGenerated(type: Delete) {
|
||||||
|
delete fileTree('src/main/java/nodomain/freeyourgadget/gadgetbridge/proto') {
|
||||||
|
include '**/*.java'
|
||||||
|
}
|
||||||
|
delete fileTree('src/main/java/nodomain/freeyourgadget/gadgetbridge/entities') {
|
||||||
|
include '**/*.java'
|
||||||
|
exclude '**/Abstract*.java'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.clean.dependsOn(tasks.cleanGenerated)
|
||||||
|
|
||||||
protobuf {
|
protobuf {
|
||||||
protoc {
|
protoc {
|
||||||
@ -344,7 +355,12 @@ protobuf {
|
|||||||
generateProtoTasks {
|
generateProtoTasks {
|
||||||
all().each { task ->
|
all().each { task ->
|
||||||
task.builtins {
|
task.builtins {
|
||||||
java { option 'lite' }
|
java {
|
||||||
|
option 'lite'
|
||||||
|
// Uncomment this to get Android Studio to recognize the generated files
|
||||||
|
// this makes it think that all src files are generated though...
|
||||||
|
//outputSubDir = '../../../../../src/main/java/'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,11 @@
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.app.DatePickerDialog;
|
import android.app.DatePickerDialog;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
@ -232,13 +234,25 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
|
|||||||
SparseBooleanArray checked = getItemListView().getCheckedItemPositions();
|
SparseBooleanArray checked = getItemListView().getCheckedItemPositions();
|
||||||
switch (menuItem.getItemId()) {
|
switch (menuItem.getItemId()) {
|
||||||
case R.id.activity_action_delete:
|
case R.id.activity_action_delete:
|
||||||
List<BaseActivitySummary> toDelete = new ArrayList<>();
|
final List<BaseActivitySummary> toDelete = new ArrayList<>();
|
||||||
for (int i = 0; i < checked.size(); i++) {
|
for (int i = 0; i < checked.size(); i++) {
|
||||||
if (checked.valueAt(i)) {
|
if (checked.valueAt(i)) {
|
||||||
toDelete.add(getItemAdapter().getItem(checked.keyAt(i)));
|
toDelete.add(getItemAdapter().getItem(checked.keyAt(i)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deleteItems(toDelete);
|
|
||||||
|
new AlertDialog.Builder(ActivitySummariesActivity.this)
|
||||||
|
.setTitle(ActivitySummariesActivity.this.getString(R.string.sports_activity_confirm_delete_title, toDelete.size()))
|
||||||
|
.setMessage(ActivitySummariesActivity.this.getString(R.string.sports_activity_confirm_delete_description, toDelete.size()))
|
||||||
|
.setIcon(R.drawable.ic_delete_forever)
|
||||||
|
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
|
||||||
|
public void onClick(final DialogInterface dialog, final int whichButton) {
|
||||||
|
deleteItems(toDelete);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.no, null)
|
||||||
|
.show();
|
||||||
|
|
||||||
processed = true;
|
processed = true;
|
||||||
break;
|
break;
|
||||||
case R.id.activity_action_export:
|
case R.id.activity_action_export:
|
||||||
|
@ -73,6 +73,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
@ -80,8 +81,10 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryItems;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryItems;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.SwipeEvents;
|
import nodomain.freeyourgadget.gadgetbridge.util.SwipeEvents;
|
||||||
@ -414,13 +417,16 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void makeSummaryContent(BaseActivitySummary item) {
|
private void makeSummaryContent(BaseActivitySummary item) {
|
||||||
|
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
|
||||||
|
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(gbDevice);
|
||||||
|
|
||||||
//make view of data from summaryData of item
|
//make view of data from summaryData of item
|
||||||
String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
|
String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
|
||||||
String UNIT_IMPERIAL = GBApplication.getContext().getString(R.string.p_unit_imperial);
|
String UNIT_IMPERIAL = GBApplication.getContext().getString(R.string.p_unit_imperial);
|
||||||
|
|
||||||
TableLayout fieldLayout = findViewById(R.id.summaryDetails);
|
TableLayout fieldLayout = findViewById(R.id.summaryDetails);
|
||||||
fieldLayout.removeAllViews(); //remove old widgets
|
fieldLayout.removeAllViews(); //remove old widgets
|
||||||
ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(item);
|
ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, item);
|
||||||
JSONObject data = activitySummaryJsonSummary.getSummaryGroupedList(); //get list, grouped by groups
|
JSONObject data = activitySummaryJsonSummary.getSummaryGroupedList(); //get list, grouped by groups
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
@ -874,7 +874,11 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
|||||||
if (supportedLanguages != null) {
|
if (supportedLanguages != null) {
|
||||||
supportedSettings = ArrayUtils.insert(0, supportedSettings, R.xml.devicesettings_language_generic);
|
supportedSettings = ArrayUtils.insert(0, supportedSettings, R.xml.devicesettings_language_generic);
|
||||||
}
|
}
|
||||||
supportedSettings = ArrayUtils.addAll(supportedSettings, coordinator.getSupportedDeviceSpecificAuthenticationSettings());
|
final int[] supportedAuthSettings = coordinator.getSupportedDeviceSpecificAuthenticationSettings();
|
||||||
|
if (supportedAuthSettings != null && supportedAuthSettings.length > 0) {
|
||||||
|
supportedSettings = ArrayUtils.add(supportedSettings, R.xml.devicesettings_header_authentication);
|
||||||
|
supportedSettings = ArrayUtils.addAll(supportedSettings, supportedAuthSettings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer = coordinator.getDeviceSpecificSettingsCustomizer(device);
|
final DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer = coordinator.getDeviceSpecificSettingsCustomizer(device);
|
||||||
|
@ -42,13 +42,16 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
@ -184,6 +187,8 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
|
|||||||
ImageView activityIconView = view.findViewById(R.id.summary_dashboard_layout_activity_icon);
|
ImageView activityIconView = view.findViewById(R.id.summary_dashboard_layout_activity_icon);
|
||||||
ImageView activityIconBigView = view.findViewById(R.id.summary_dashboard_layout_big_activity_icon);
|
ImageView activityIconBigView = view.findViewById(R.id.summary_dashboard_layout_big_activity_icon);
|
||||||
|
|
||||||
|
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||||
|
|
||||||
for (BaseActivitySummary sportitem : getItems()) {
|
for (BaseActivitySummary sportitem : getItems()) {
|
||||||
if (sportitem.getStartTime() == null) continue; //first item is empty, for dashboard
|
if (sportitem.getStartTime() == null) continue; //first item is empty, for dashboard
|
||||||
|
|
||||||
@ -199,8 +204,8 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(device);
|
||||||
ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(sportitem);
|
final ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, sportitem);
|
||||||
JSONObject summarySubdata = activitySummaryJsonSummary.getSummaryData();
|
JSONObject summarySubdata = activitySummaryJsonSummary.getSummaryData();
|
||||||
|
|
||||||
if (summarySubdata != null) {
|
if (summarySubdata != null) {
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
/* Copyright (C) 2022 José Rebelo
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.database.schema;
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||||
|
|
||||||
|
public class GadgetbridgeUpdate_44 implements DBUpdateScript {
|
||||||
|
@Override
|
||||||
|
public void upgradeSchema(final SQLiteDatabase db) {
|
||||||
|
if (!DBHelper.existsColumn(BaseActivitySummaryDao.TABLENAME, BaseActivitySummaryDao.Properties.RawDetailsPath.columnName, db)) {
|
||||||
|
final String statement = "ALTER TABLE " + BaseActivitySummaryDao.TABLENAME + " ADD COLUMN "
|
||||||
|
+ BaseActivitySummaryDao.Properties.RawDetailsPath.columnName + " TEXT";
|
||||||
|
db.execSQL(statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void downgradeSchema(final SQLiteDatabase db) {
|
||||||
|
}
|
||||||
|
}
|
@ -46,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.AlarmDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.AlarmDao;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.BatteryLevelDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.BatteryLevelDao;
|
||||||
@ -54,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
@ -150,6 +152,12 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
|||||||
return device.isInitialized() && !device.isBusy() && supportsActivityDataFetching();
|
return device.isInitialized() && !device.isBusy() && supportsActivityDataFetching();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isHealthWearable(BluetoothDevice device) {
|
public boolean isHealthWearable(BluetoothDevice device) {
|
||||||
BluetoothClass bluetoothClass = device.getBluetoothClass();
|
BluetoothClass bluetoothClass = device.getBluetoothClass();
|
||||||
if (bluetoothClass == null) {
|
if (bluetoothClass == null) {
|
||||||
|
@ -40,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||||
|
|
||||||
@ -215,6 +216,13 @@ public interface DeviceCoordinator {
|
|||||||
*/
|
*/
|
||||||
SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session);
|
SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link ActivitySummaryParser} for the device being supported.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
ActivitySummaryParser getActivitySummaryParser(final GBDevice device);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this device/coordinator supports installing files like firmware,
|
* Returns true if this device/coordinator supports installing files like firmware,
|
||||||
* watchfaces, gps, resources, fonts...
|
* watchfaces, gps, resources, fonts...
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
/* Copyright (C) 2022 José Rebelo
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.huami;
|
||||||
|
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.proto.HuamiProtos;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021ActivityDetailsParser;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021WorkoutTrackActivityType;
|
||||||
|
|
||||||
|
public class Huami2021ActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Huami2021ActivitySummaryParser.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractHuamiActivityDetailsParser getDetailsParser(final BaseActivitySummary summary) {
|
||||||
|
return new Huami2021ActivityDetailsParser(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void parseBinaryData(final BaseActivitySummary summary, final Date startTime) {
|
||||||
|
final byte[] rawData = summary.getRawSummaryData();
|
||||||
|
final int version = (rawData[0] & 0xff) | ((rawData[1] & 0xff) << 8);
|
||||||
|
if (version != 0x8000) {
|
||||||
|
LOG.warn("Unexpected binary data version {}, parsing might fail", version);
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] protobufData = ArrayUtils.subarray(rawData, 2, rawData.length);
|
||||||
|
final HuamiProtos.WorkoutSummary summaryProto;
|
||||||
|
try {
|
||||||
|
summaryProto = HuamiProtos.WorkoutSummary.parseFrom(protobufData);
|
||||||
|
} catch (final InvalidProtocolBufferException e) {
|
||||||
|
LOG.error("Failed to parse summary protobuf data", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryProto.hasType()) {
|
||||||
|
final Huami2021WorkoutTrackActivityType workoutTrackActivityType = Huami2021WorkoutTrackActivityType
|
||||||
|
.fromCode((byte) summaryProto.getType().getType());
|
||||||
|
|
||||||
|
final int activityKind;
|
||||||
|
if (workoutTrackActivityType != null) {
|
||||||
|
activityKind = workoutTrackActivityType.toActivityKind();
|
||||||
|
} else {
|
||||||
|
LOG.warn("Unknown workout activity type code {}", String.format("0x%X", summaryProto.getType().getType()));
|
||||||
|
activityKind = ActivityKind.TYPE_UNKNOWN;
|
||||||
|
}
|
||||||
|
summary.setActivityKind(activityKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryProto.hasTime()) {
|
||||||
|
int totalDuration = summaryProto.getTime().getTotalDuration();
|
||||||
|
summary.setEndTime(new Date(startTime.getTime() + totalDuration * 1000L));
|
||||||
|
addSummaryData("activeSeconds", summaryProto.getTime().getWorkoutDuration(), "seconds");
|
||||||
|
// TODO pause durations
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryProto.hasLocation()) {
|
||||||
|
summary.setBaseLongitude(summaryProto.getLocation().getBaseLongitude());
|
||||||
|
summary.setBaseLatitude(summaryProto.getLocation().getBaseLatitude());
|
||||||
|
summary.setBaseAltitude(summaryProto.getLocation().getBaseAltitude() / 2);
|
||||||
|
// TODO: Min/Max Latitude/Longitude
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryProto.hasHeartRate()) {
|
||||||
|
addSummaryData("averageHR", summaryProto.getHeartRate().getAvg(), "bpm");
|
||||||
|
addSummaryData("maxHR", summaryProto.getHeartRate().getMax(), "bpm");
|
||||||
|
addSummaryData("minHR", summaryProto.getHeartRate().getMin(), "bpm");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryProto.hasSteps()) {
|
||||||
|
addSummaryData("maxCadence", summaryProto.getSteps().getMaxCadence() * 60, "spm");
|
||||||
|
addSummaryData("averageCadence", summaryProto.getSteps().getAvgCadence() * 60, "spm");
|
||||||
|
addSummaryData("averageStride", summaryProto.getSteps().getAvgStride(), "cm");
|
||||||
|
addSummaryData("steps", summaryProto.getSteps().getSteps(), "steps_unit");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryProto.hasDistance()) {
|
||||||
|
addSummaryData("distanceMeters", summaryProto.getDistance().getDistance(), "meters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryProto.hasPace()) {
|
||||||
|
addSummaryData("maxPace", summaryProto.getPace().getBest(), "seconds_m");
|
||||||
|
addSummaryData("averageKMPaceSeconds", summaryProto.getPace().getAvg() * 1000, "seconds_km");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryProto.hasCalories()) {
|
||||||
|
addSummaryData("caloriesBurnt", summaryProto.getCalories().getCalories(), "calories_unit");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryProto.hasHeartRateZones()) {
|
||||||
|
// TODO HR zones
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryProto.hasTrainingEffect()) {
|
||||||
|
// TODO training effect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||||
|
|
||||||
public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
||||||
@Override
|
@Override
|
||||||
@ -68,8 +69,7 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsActivityTracks() {
|
public boolean supportsActivityTracks() {
|
||||||
// TODO: It's supported by the devices, but not yet implemented
|
return true;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -102,6 +102,11 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
|||||||
return new HuamiExtendedSampleProvider(device, session);
|
return new HuamiExtendedSampleProvider(device, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
|
||||||
|
return new Huami2021ActivitySummaryParser();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsAlarmSnoozing() {
|
public boolean supportsAlarmSnoozing() {
|
||||||
// All alarms snooze by default, there doesn't seem to be a flag that disables it
|
// All alarms snooze by default, there doesn't seem to be a flag that disables it
|
||||||
@ -194,6 +199,9 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
|||||||
R.xml.devicesettings_expose_hr_thirdparty,
|
R.xml.devicesettings_expose_hr_thirdparty,
|
||||||
R.xml.devicesettings_bt_connected_advertisement,
|
R.xml.devicesettings_bt_connected_advertisement,
|
||||||
R.xml.devicesettings_high_mtu,
|
R.xml.devicesettings_high_mtu,
|
||||||
|
|
||||||
|
R.xml.devicesettings_header_developer,
|
||||||
|
R.xml.devicesettings_keep_activity_data_on_device,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSportsActivityType;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSportsActivityType;
|
||||||
|
|
||||||
@ -45,11 +46,17 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
|
|||||||
LOG.error("Due to a bug, we can only parse the summary when startTime is already set");
|
LOG.error("Due to a bug, we can only parse the summary when startTime is already set");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return parseBinaryData(summary, startTime);
|
summaryData = new JSONObject();
|
||||||
|
parseBinaryData(summary, startTime);
|
||||||
|
summary.setSummaryData(summaryData.toString());
|
||||||
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseActivitySummary parseBinaryData(BaseActivitySummary summary, Date startTime) {
|
public AbstractHuamiActivityDetailsParser getDetailsParser(final BaseActivitySummary summary) {
|
||||||
summaryData = new JSONObject();
|
return new HuamiActivityDetailsParser(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void parseBinaryData(BaseActivitySummary summary, Date startTime) {
|
||||||
ByteBuffer buffer = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN);
|
ByteBuffer buffer = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
short version = buffer.getShort(); // version
|
short version = buffer.getShort(); // version
|
||||||
@ -372,13 +379,9 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
|
|||||||
addSummaryData("swimStyle", swimStyleName);
|
addSummaryData("swimStyle", swimStyleName);
|
||||||
addSummaryData("laps", laps, "laps");
|
addSummaryData("laps", laps, "laps");
|
||||||
}
|
}
|
||||||
|
|
||||||
summary.setSummaryData(summaryData.toString());
|
|
||||||
return summary;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void addSummaryData(String key, float value, String unit) {
|
||||||
private void addSummaryData(String key, float value, String unit) {
|
|
||||||
if (value > 0) {
|
if (value > 0) {
|
||||||
try {
|
try {
|
||||||
JSONObject innerData = new JSONObject();
|
JSONObject innerData = new JSONObject();
|
||||||
@ -390,7 +393,7 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addSummaryData(String key, String value) {
|
protected void addSummaryData(String key, String value) {
|
||||||
if (key != null && !key.equals("") && value != null && !value.equals("")) {
|
if (key != null && !key.equals("") && value != null && !value.equals("")) {
|
||||||
try {
|
try {
|
||||||
JSONObject innerData = new JSONObject();
|
JSONObject innerData = new JSONObject();
|
||||||
|
@ -59,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiVibrationPatternNotificationType;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiVibrationPatternNotificationType;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
@ -140,6 +141,11 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
|
|||||||
return new MiBand2SampleProvider(device, session);
|
return new MiBand2SampleProvider(device, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
|
||||||
|
return new HuamiActivitySummaryParser();
|
||||||
|
}
|
||||||
|
|
||||||
public static DateTimeDisplay getDateDisplay(Context context, String deviceAddress) throws IllegalArgumentException {
|
public static DateTimeDisplay getDateDisplay(Context context, String deviceAddress) throws IllegalArgumentException {
|
||||||
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
|
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
|
||||||
String dateFormatTime = context.getString(R.string.p_dateformat_time);
|
String dateFormatTime = context.getString(R.string.p_dateformat_time);
|
||||||
@ -352,6 +358,11 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
|
|||||||
return prefs.getBoolean("overwrite_settings_on_connection", true);
|
return prefs.getBoolean("overwrite_settings_on_connection", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean getKeepActivityDataOnDevice(String deviceAddress) {
|
||||||
|
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||||
|
return prefs.getBoolean("keep_activity_data_on_device", false);
|
||||||
|
}
|
||||||
|
|
||||||
public static VibrationProfile getVibrationProfile(String deviceAddress, HuamiVibrationPatternNotificationType notificationType) {
|
public static VibrationProfile getVibrationProfile(String deviceAddress, HuamiVibrationPatternNotificationType notificationType) {
|
||||||
final String defaultVibrationProfileId;
|
final String defaultVibrationProfileId;
|
||||||
final int defaultVibrationCount;
|
final int defaultVibrationCount;
|
||||||
|
@ -115,8 +115,9 @@ public class HuamiService {
|
|||||||
|
|
||||||
// maybe not really activity data, but steps?
|
// maybe not really activity data, but steps?
|
||||||
public static final byte COMMAND_FETCH_DATA = 0x02;
|
public static final byte COMMAND_FETCH_DATA = 0x02;
|
||||||
// maybe delete/drop activity data?
|
// delete/drop activity data
|
||||||
// on Huami it's just 03 / on Huami 2021 it's 03:09
|
// on Huami it's just the single 03 byte
|
||||||
|
// on Huami 2021 it's followed by 09 to keep, 01 to drop from device
|
||||||
public static final byte COMMAND_ACK_ACTIVITY_DATA = 0x03;
|
public static final byte COMMAND_ACK_ACTIVITY_DATA = 0x03;
|
||||||
|
|
||||||
public static final byte[] COMMAND_SET_FITNESS_GOAL_START = new byte[] { 0x10, 0x0, 0x0 };
|
public static final byte[] COMMAND_SET_FITNESS_GOAL_START = new byte[] { 0x10, 0x0, 0x0 };
|
||||||
@ -230,13 +231,6 @@ public class HuamiService {
|
|||||||
public static final byte COMMAND_FIRMWARE_CHECKSUM = 0x04; // to UUID_CHARACTERISTIC_FIRMWARE
|
public static final byte COMMAND_FIRMWARE_CHECKSUM = 0x04; // to UUID_CHARACTERISTIC_FIRMWARE
|
||||||
public static final byte COMMAND_FIRMWARE_REBOOT = 0x05; // to UUID_CHARACTERISTIC_FIRMWARE
|
public static final byte COMMAND_FIRMWARE_REBOOT = 0x05; // to UUID_CHARACTERISTIC_FIRMWARE
|
||||||
|
|
||||||
public static final byte[] RESPONSE_FINISH_SUCCESS = new byte[] {RESPONSE, 2, SUCCESS };
|
|
||||||
public static final byte[] RESPONSE_ACK_SUCCESS = new byte[] {RESPONSE, 3, SUCCESS };
|
|
||||||
public static final byte[] RESPONSE_FIRMWARE_DATA_SUCCESS = new byte[] {RESPONSE, COMMAND_FIRMWARE_START_DATA, SUCCESS };
|
|
||||||
/**
|
|
||||||
* Received in response to any dateformat configuration request (byte 0 in the byte[] value.
|
|
||||||
*/
|
|
||||||
public static final byte[] RESPONSE_DATEFORMAT_SUCCESS = new byte[] { RESPONSE, ENDPOINT_DISPLAY, 0x0a, 0x0, 0x01 };
|
|
||||||
public static final byte[] RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS = new byte[] { RESPONSE, COMMAND_ACTIVITY_DATA_START_DATE, SUCCESS};
|
public static final byte[] RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS = new byte[] { RESPONSE, COMMAND_ACTIVITY_DATA_START_DATE, SUCCESS};
|
||||||
|
|
||||||
public static final byte[] WEAR_LOCATION_LEFT_WRIST = new byte[] { 0x20, 0x00, 0x00, 0x02 };
|
public static final byte[] WEAR_LOCATION_LEFT_WRIST = new byte[] { 0x20, 0x00, 0x00, 0x02 };
|
||||||
|
@ -33,7 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSuppo
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.SMAQ2OSSProtos;
|
import nodomain.freeyourgadget.gadgetbridge.proto.SMAQ2OSSProtos;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||||
|
|
||||||
|
@ -16,14 +16,9 @@
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Summarized information about a temporal activity.
|
* Summarized information about a temporal activity.
|
||||||
*
|
*
|
||||||
|
@ -16,9 +16,11 @@ public class ActivitySummaryJsonSummary {
|
|||||||
private JSONObject groupData;
|
private JSONObject groupData;
|
||||||
private JSONObject summaryData;
|
private JSONObject summaryData;
|
||||||
private JSONObject summaryGroupedList;
|
private JSONObject summaryGroupedList;
|
||||||
|
private ActivitySummaryParser summaryParser;
|
||||||
private BaseActivitySummary baseActivitySummary;
|
private BaseActivitySummary baseActivitySummary;
|
||||||
|
|
||||||
public ActivitySummaryJsonSummary(BaseActivitySummary baseActivitySummary){
|
public ActivitySummaryJsonSummary(final ActivitySummaryParser summaryParser, BaseActivitySummary baseActivitySummary){
|
||||||
|
this.summaryParser=summaryParser;
|
||||||
this.baseActivitySummary=baseActivitySummary;
|
this.baseActivitySummary=baseActivitySummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,8 +69,7 @@ public class ActivitySummaryJsonSummary {
|
|||||||
|
|
||||||
private String getCorrectSummary(BaseActivitySummary item){
|
private String getCorrectSummary(BaseActivitySummary item){
|
||||||
if (item.getRawSummaryData() != null) {
|
if (item.getRawSummaryData() != null) {
|
||||||
ActivitySummaryParser parser = new HuamiActivitySummaryParser(); // FIXME: if something else than huami supports that make sure to have the right parser
|
item = summaryParser.parseBinaryData(item);
|
||||||
item = parser.parseBinaryData(item);
|
|
||||||
}
|
}
|
||||||
return item.getSummaryData();
|
return item.getSummaryData();
|
||||||
}
|
}
|
||||||
|
2
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/proto/.gitignore
vendored
Normal file
2
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/proto/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# This folder will contain auto-generated protobuf classes
|
||||||
|
*.java
|
@ -149,6 +149,10 @@ public class BLETypeConversions {
|
|||||||
return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8);
|
return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int toUint16(byte[] bytes, int offset) {
|
||||||
|
return (bytes[offset + 0] & 0xff) | ((bytes[offset + 1] & 0xff) << 8);
|
||||||
|
}
|
||||||
|
|
||||||
public static int toInt16(byte... bytes) {
|
public static int toInt16(byte... bytes) {
|
||||||
return (short) (bytes[0] & 0xff | ((bytes[1] & 0xff) << 8));
|
return (short) (bytes[0] & 0xff | ((bytes[1] & 0xff) << 8));
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
/* Copyright (C) 2017-2021 Andreas Shimokawa, AndrewH, Carsten Pfeiffer,
|
||||||
|
szilardx, José Rebelo
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||||
|
|
||||||
|
public abstract class AbstractHuamiActivityDetailsParser {
|
||||||
|
private static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal("3000000.0");
|
||||||
|
|
||||||
|
public abstract ActivityTrack parse(final byte[] bytes) throws GBException;
|
||||||
|
|
||||||
|
public static double convertHuamiValueToDecimalDegrees(final long huamiValue) {
|
||||||
|
BigDecimal result = new BigDecimal(huamiValue)
|
||||||
|
.divide(HUAMI_TO_DECIMAL_DEGREES_DIVISOR, GPSCoordinate.GPS_DECIMAL_DEGREES_SCALE, RoundingMode.HALF_UP);
|
||||||
|
return result.doubleValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String createActivityName(final BaseActivitySummary summary) {
|
||||||
|
String name = summary.getName();
|
||||||
|
String nameText = "";
|
||||||
|
Long id = summary.getId();
|
||||||
|
if (name != null) {
|
||||||
|
nameText = name + " - ";
|
||||||
|
}
|
||||||
|
return nameText + id;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,351 @@
|
|||||||
|
/* Copyright (C) 2022 José Rebelo
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||||
|
|
||||||
|
public class Huami2021ActivityDetailsParser extends AbstractHuamiActivityDetailsParser {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Huami2021ActivityDetailsParser.class);
|
||||||
|
|
||||||
|
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
|
||||||
|
|
||||||
|
static {
|
||||||
|
SDF.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date timestamp;
|
||||||
|
private long offset = 0;
|
||||||
|
|
||||||
|
private long longitude;
|
||||||
|
private long latitude;
|
||||||
|
private double altitude;
|
||||||
|
|
||||||
|
private final ActivityTrack activityTrack;
|
||||||
|
private ActivityPoint lastActivityPoint;
|
||||||
|
|
||||||
|
public Huami2021ActivityDetailsParser(final BaseActivitySummary summary) {
|
||||||
|
this.timestamp = summary.getStartTime();
|
||||||
|
|
||||||
|
this.longitude = summary.getBaseLongitude();
|
||||||
|
this.latitude = summary.getBaseLatitude();
|
||||||
|
this.altitude = summary.getBaseAltitude();
|
||||||
|
|
||||||
|
this.activityTrack = new ActivityTrack();
|
||||||
|
this.activityTrack.setUser(summary.getUser());
|
||||||
|
this.activityTrack.setDevice(summary.getDevice());
|
||||||
|
this.activityTrack.setName(createActivityName(summary));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ActivityTrack parse(final byte[] bytes) throws GBException {
|
||||||
|
final ByteBuffer buf = ByteBuffer.wrap(bytes)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
// Keep track of unknown type codes so we can print them without spamming the logs
|
||||||
|
final Map<Byte, Integer> unknownTypeCodes = new HashMap<>();
|
||||||
|
|
||||||
|
while (buf.position() < buf.limit()) {
|
||||||
|
final byte typeCode = buf.get();
|
||||||
|
final byte length = buf.get();
|
||||||
|
final int initialPosition = buf.position();
|
||||||
|
|
||||||
|
final Type type = Type.fromCode(typeCode);
|
||||||
|
if (type == null) {
|
||||||
|
if (!unknownTypeCodes.containsKey(typeCode)) {
|
||||||
|
unknownTypeCodes.put(typeCode, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
unknownTypeCodes.put(typeCode, unknownTypeCodes.get(typeCode) + 1);
|
||||||
|
//LOG.warn("Unknown type code {} of length {}", String.format("0x%X", typeCode), length);
|
||||||
|
// Consume the reported length
|
||||||
|
buf.get(new byte[length]);
|
||||||
|
continue;
|
||||||
|
} else if (length != type.getExpectedLength()) {
|
||||||
|
LOG.warn("Unexpected length {} for type {}", length, type);
|
||||||
|
// Consume the reported length
|
||||||
|
buf.get(new byte[length]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume
|
||||||
|
switch (type) {
|
||||||
|
case TIMESTAMP:
|
||||||
|
consumeTimestamp(buf);
|
||||||
|
break;
|
||||||
|
case GPS_COORDS:
|
||||||
|
consumeGpsCoords(buf);
|
||||||
|
break;
|
||||||
|
case GPS_DELTA:
|
||||||
|
consumeGpsDelta(buf);
|
||||||
|
break;
|
||||||
|
case STATUS:
|
||||||
|
consumeStatus(buf);
|
||||||
|
break;
|
||||||
|
case SPEED:
|
||||||
|
consumeSpeed(buf);
|
||||||
|
break;
|
||||||
|
case ALTITUDE:
|
||||||
|
consumeAltitude(buf);
|
||||||
|
break;
|
||||||
|
case HEARTRATE:
|
||||||
|
consumeHeartRate(buf);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG.warn("No consumer for for type {}", type);
|
||||||
|
// Consume the reported length
|
||||||
|
buf.get(new byte[length]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int expectedPosition = initialPosition + length;
|
||||||
|
if (buf.position() != expectedPosition) {
|
||||||
|
// Should never happen unless there's a bug in one of the consumers
|
||||||
|
throw new IllegalStateException("Unexpected position " + buf.position() + ", expected " + expectedPosition + ", after consuming " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unknownTypeCodes.isEmpty()) {
|
||||||
|
for (final Map.Entry<Byte, Integer> e : unknownTypeCodes.entrySet()) {
|
||||||
|
LOG.warn("Unknown type code {} seen {} times", String.format("0x%X", e.getKey()), e.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.activityTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeTimestamp(final ByteBuffer buf) {
|
||||||
|
buf.getInt(); // ?
|
||||||
|
this.timestamp = new Date(buf.getLong());
|
||||||
|
this.offset = 0;
|
||||||
|
|
||||||
|
//trace("Consumed timestamp");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeTimestampOffset(final ByteBuffer buf) {
|
||||||
|
this.offset = buf.getShort();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeGpsCoords(final ByteBuffer buf) {
|
||||||
|
buf.get(new byte[6]); // ?
|
||||||
|
this.longitude = buf.getInt();
|
||||||
|
this.latitude = buf.getInt();
|
||||||
|
buf.get(new byte[6]); // ?
|
||||||
|
|
||||||
|
// TODO which one is the time offset? Not sure it is the first
|
||||||
|
|
||||||
|
addNewGpsCoordinates();
|
||||||
|
|
||||||
|
final double longitudeDeg = convertHuamiValueToDecimalDegrees(longitude);
|
||||||
|
final double latitudeDeg = convertHuamiValueToDecimalDegrees(latitude);
|
||||||
|
|
||||||
|
//trace("Consumed GPS coords: {} {}", longitudeDeg, latitudeDeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeGpsDelta(final ByteBuffer buf) {
|
||||||
|
consumeTimestampOffset(buf);
|
||||||
|
final short longitudeDelta = buf.getShort();
|
||||||
|
final short latitudeDelta = buf.getShort();
|
||||||
|
buf.getShort(); // ? seems to always be 2
|
||||||
|
|
||||||
|
this.longitude += longitudeDelta;
|
||||||
|
this.latitude += latitudeDelta;
|
||||||
|
|
||||||
|
if (lastActivityPoint == null) {
|
||||||
|
final String timestampStr = SDF.format(new Date(timestamp.getTime() + offset));
|
||||||
|
LOG.warn("{}: Got GPS delta before GPS coords, ignoring", timestampStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNewGpsCoordinates();
|
||||||
|
|
||||||
|
//trace("Consumed GPS delta: {} {}", longitudeDelta, latitudeDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeStatus(final ByteBuffer buf) {
|
||||||
|
consumeTimestampOffset(buf);
|
||||||
|
|
||||||
|
final int statusCode = buf.getShort();
|
||||||
|
final String status;
|
||||||
|
switch (statusCode) {
|
||||||
|
case 1:
|
||||||
|
status = "start";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
status = "pause";
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
status = "resume";
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
status = "stop";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
status = String.format("unknown (0x%X)", statusCode);
|
||||||
|
LOG.warn("Unknown status code {}", String.format("0x%X", statusCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO split track into multiple segments?
|
||||||
|
|
||||||
|
//trace("Consumed Status: {}", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeSpeed(final ByteBuffer buf) {
|
||||||
|
consumeTimestampOffset(buf);
|
||||||
|
|
||||||
|
final short cadence = buf.getShort(); // spm
|
||||||
|
final short stride = buf.getShort(); // cm
|
||||||
|
final short pace = buf.getShort(); // sec/km
|
||||||
|
|
||||||
|
// TODO integrate into gpx
|
||||||
|
|
||||||
|
//trace("Consumed speed: cadence={}, stride={}, ?={}", cadence, stride, );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeAltitude(final ByteBuffer buf) {
|
||||||
|
consumeTimestampOffset(buf);
|
||||||
|
altitude = (int) (buf.getInt() / 100.0f);
|
||||||
|
|
||||||
|
final ActivityPoint ap = getCurrentActivityPoint();
|
||||||
|
if (ap != null) {
|
||||||
|
final GPSCoordinate newCoordinate = new GPSCoordinate(
|
||||||
|
ap.getLocation().getLongitude(),
|
||||||
|
ap.getLocation().getLatitude(),
|
||||||
|
altitude
|
||||||
|
);
|
||||||
|
|
||||||
|
ap.setLocation(newCoordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//trace("Consumed altitude: {}", altitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void consumeHeartRate(final ByteBuffer buf) {
|
||||||
|
consumeTimestampOffset(buf);
|
||||||
|
final int heartRate = buf.get() & 0xff;
|
||||||
|
|
||||||
|
final ActivityPoint ap = getCurrentActivityPoint();
|
||||||
|
if (ap != null) {
|
||||||
|
ap.setHeartRate(heartRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//trace("Consumed HeartRate: {}", heartRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private ActivityPoint getCurrentActivityPoint() {
|
||||||
|
if (lastActivityPoint == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round to the nearest second
|
||||||
|
final long currentTime = timestamp.getTime() + offset;
|
||||||
|
if (currentTime - lastActivityPoint.getTime().getTime() > 500) {
|
||||||
|
addNewGpsCoordinates();
|
||||||
|
return lastActivityPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastActivityPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNewGpsCoordinates() {
|
||||||
|
final GPSCoordinate coordinate = new GPSCoordinate(
|
||||||
|
convertHuamiValueToDecimalDegrees(longitude),
|
||||||
|
convertHuamiValueToDecimalDegrees(latitude),
|
||||||
|
altitude
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastActivityPoint != null && lastActivityPoint.getLocation() != null && lastActivityPoint.getLocation().equals(coordinate)) {
|
||||||
|
// Ignore repeated location
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ActivityPoint ap = new ActivityPoint(new Date(timestamp.getTime() + offset));
|
||||||
|
ap.setLocation(coordinate);
|
||||||
|
add(ap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(final ActivityPoint ap) {
|
||||||
|
if (ap == lastActivityPoint) {
|
||||||
|
LOG.debug("skipping point!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastActivityPoint = ap;
|
||||||
|
activityTrack.addTrackPoint(ap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void trace(final String format, final Object... args) {
|
||||||
|
final Object[] argsWithDate = ArrayUtils.insert(0, args, SDF.format(new Date(timestamp.getTime() + offset)));
|
||||||
|
LOG.debug("{}: " + format, argsWithDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Type {
|
||||||
|
TIMESTAMP(1, 12),
|
||||||
|
GPS_COORDS(2, 20),
|
||||||
|
GPS_DELTA(3, 8),
|
||||||
|
STATUS(4, 4),
|
||||||
|
SPEED(5, 8),
|
||||||
|
ALTITUDE(7, 6),
|
||||||
|
HEARTRATE(8, 3),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final byte code;
|
||||||
|
private final int expectedLength;
|
||||||
|
|
||||||
|
Type(final int code, final int expectedLength) {
|
||||||
|
this.code = (byte) code;
|
||||||
|
this.expectedLength = expectedLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getCode() {
|
||||||
|
return this.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getExpectedLength() {
|
||||||
|
return this.expectedLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Type fromCode(final byte code) {
|
||||||
|
for (final Type type : values()) {
|
||||||
|
if (type.getCode() == code) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,22 +25,123 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
|||||||
* The workout types, used to start / when workout tracking starts on the band.
|
* The workout types, used to start / when workout tracking starts on the band.
|
||||||
*/
|
*/
|
||||||
public enum Huami2021WorkoutTrackActivityType {
|
public enum Huami2021WorkoutTrackActivityType {
|
||||||
// TODO 150 workouts :/
|
AerobicCombo(0x33),
|
||||||
|
Aerobics(0x6d),
|
||||||
|
AirWalker(0x90),
|
||||||
|
Archery(0x5d),
|
||||||
|
ArtisticSwimming(0x9c),
|
||||||
Badminton(0x5c),
|
Badminton(0x5c),
|
||||||
|
Ballet(0x47),
|
||||||
|
BallroomDance(0x4b),
|
||||||
|
Baseball(0x4f),
|
||||||
|
Basketball(0x55),
|
||||||
|
BattleRope(0xa7),
|
||||||
|
BeachVolleyball(0x7a),
|
||||||
|
BellyDance(0x48),
|
||||||
|
Billiards(0x97),
|
||||||
|
bmx(0x30),
|
||||||
|
BoardGame(0xb1),
|
||||||
|
Bocce(0xaa),
|
||||||
|
Bowling(0x50),
|
||||||
|
Boxing(0x61),
|
||||||
|
Breaking(0xa8),
|
||||||
|
Bridge(0xb0),
|
||||||
|
CardioCombat(0x72),
|
||||||
|
Checkers(0xae),
|
||||||
|
Chess(0xad),
|
||||||
|
CoreTraining(0x32),
|
||||||
|
Cricket(0x4e),
|
||||||
|
CrossTraining(0x82),
|
||||||
|
Curling(0x29),
|
||||||
Dance(0x4c),
|
Dance(0x4c),
|
||||||
|
Darts(0x75),
|
||||||
|
Dodgeball(0x99),
|
||||||
|
DragonBoat(0x8a),
|
||||||
Elliptical(0x09),
|
Elliptical(0x09),
|
||||||
|
Esports(0xbd),
|
||||||
|
Esquestrian(0x5e),
|
||||||
|
Fencing(0x94),
|
||||||
|
Finswimming(0x9b),
|
||||||
|
Fishing(0x40),
|
||||||
|
Flexibility(0x37),
|
||||||
|
Flowriding(0xac),
|
||||||
|
FolkDance(0x92),
|
||||||
Freestyle(0x05),
|
Freestyle(0x05),
|
||||||
|
Frisbee(0x74),
|
||||||
|
Futsal(0xa4),
|
||||||
|
Gateball(0x57),
|
||||||
|
Gymnastics(0x3b),
|
||||||
|
HackySack(0xa9),
|
||||||
|
Handball(0x5b),
|
||||||
|
HIIT(0x31),
|
||||||
|
HipHop(0xa5),
|
||||||
|
HorizontalBar(0x95),
|
||||||
|
HulaHoop(0x73),
|
||||||
|
IceHockey(0x9e),
|
||||||
|
IceSkating(0x2c),
|
||||||
IndoorCycling(0x08),
|
IndoorCycling(0x08),
|
||||||
IndoorFitness(0x18),
|
IndoorFitness(0x18),
|
||||||
|
IndoorIceSkating(0x2d),
|
||||||
|
JaiAlai(0xab),
|
||||||
|
JazzDance(0x71),
|
||||||
|
Judo(0x62),
|
||||||
|
Jujitsu(0x93),
|
||||||
JumpRope(0x15),
|
JumpRope(0x15),
|
||||||
|
Karate(0x60),
|
||||||
|
Kayaking(0x8c),
|
||||||
|
Kendo(0x5f),
|
||||||
|
Kickboxing(0x68),
|
||||||
|
KiteFlying(0x76),
|
||||||
|
LatinDance(0x70),
|
||||||
|
MartialArts(0x67),
|
||||||
|
MassGymnastics(0x6f),
|
||||||
|
ModernDance(0xb9),
|
||||||
|
MuayThai(0x65),
|
||||||
OutdoorCycling(0x04),
|
OutdoorCycling(0x04),
|
||||||
OutdoorRunning(0x01),
|
OutdoorRunning(0x01),
|
||||||
|
ParallelBars(0x96),
|
||||||
|
Parkour(0x81),
|
||||||
|
Pilates(0x3d),
|
||||||
|
PoleDance(0xa6),
|
||||||
PoolSwimming(0x06),
|
PoolSwimming(0x06),
|
||||||
|
RaceWalking(0x83),
|
||||||
|
RockClimbing(0x46),
|
||||||
|
RollerSkating(0x45),
|
||||||
Rowing(0x17),
|
Rowing(0x17),
|
||||||
|
Sailing(0x41),
|
||||||
|
SepakTakraw(0x98),
|
||||||
|
Shuffleboard(0xa0),
|
||||||
|
Shuttlecock(0xa2),
|
||||||
|
Skateboarding(0x43),
|
||||||
|
Snorkeling(0x9d),
|
||||||
Soccer(0xbf),
|
Soccer(0xbf),
|
||||||
|
Softball(0x56),
|
||||||
|
SomatosensoryGame(0xa3),
|
||||||
|
Spinning(0x8f),
|
||||||
|
SquareDance(0x49),
|
||||||
|
Squash(0x51),
|
||||||
|
StairClimber(0x36),
|
||||||
|
Stepper(0x39),
|
||||||
|
StreetDance(0x4a),
|
||||||
|
Strength(0x34),
|
||||||
|
Stretching(0x35),
|
||||||
|
Swinging(0x9f),
|
||||||
|
TableFootball(0xa1),
|
||||||
|
TableTennis(0x59),
|
||||||
|
TaiChi(0x64),
|
||||||
|
Taekwondo(0x66),
|
||||||
|
Tennis(0x11),
|
||||||
Treadmill(0x02),
|
Treadmill(0x02),
|
||||||
|
TugOfWar(0x77),
|
||||||
|
Volleyball(0x58),
|
||||||
Walking(0x03),
|
Walking(0x03),
|
||||||
|
WallBall(0x91),
|
||||||
|
WaterPolo(0x9a),
|
||||||
|
WaterRowing(0x42),
|
||||||
|
Weiqi(0xaf),
|
||||||
|
Wrestling(0x63),
|
||||||
Yoga(0x3c),
|
Yoga(0x3c),
|
||||||
|
Zumba(0x4d),
|
||||||
;
|
;
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(Huami2021WorkoutTrackActivityType.class);
|
private static final Logger LOG = LoggerFactory.getLogger(Huami2021WorkoutTrackActivityType.class);
|
||||||
@ -61,6 +162,9 @@ public enum Huami2021WorkoutTrackActivityType {
|
|||||||
return ActivityKind.TYPE_BADMINTON;
|
return ActivityKind.TYPE_BADMINTON;
|
||||||
case Elliptical:
|
case Elliptical:
|
||||||
return ActivityKind.TYPE_ELLIPTICAL_TRAINER;
|
return ActivityKind.TYPE_ELLIPTICAL_TRAINER;
|
||||||
|
case Freestyle:
|
||||||
|
case IndoorFitness:
|
||||||
|
return ActivityKind.TYPE_EXERCISE;
|
||||||
case IndoorCycling:
|
case IndoorCycling:
|
||||||
return ActivityKind.TYPE_INDOOR_CYCLING;
|
return ActivityKind.TYPE_INDOOR_CYCLING;
|
||||||
case JumpRope:
|
case JumpRope:
|
||||||
@ -78,6 +182,7 @@ public enum Huami2021WorkoutTrackActivityType {
|
|||||||
case Treadmill:
|
case Treadmill:
|
||||||
return ActivityKind.TYPE_TREADMILL;
|
return ActivityKind.TYPE_TREADMILL;
|
||||||
case Walking:
|
case Walking:
|
||||||
|
case RaceWalking:
|
||||||
return ActivityKind.TYPE_WALKING;
|
return ActivityKind.TYPE_WALKING;
|
||||||
case Yoga:
|
case Yoga:
|
||||||
return ActivityKind.TYPE_YOGA;
|
return ActivityKind.TYPE_YOGA;
|
||||||
|
@ -35,7 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
public class HuamiActivityDetailsParser {
|
public class HuamiActivityDetailsParser extends AbstractHuamiActivityDetailsParser {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(HuamiActivityDetailsParser.class);
|
private static final Logger LOG = LoggerFactory.getLogger(HuamiActivityDetailsParser.class);
|
||||||
|
|
||||||
private static final byte TYPE_GPS = 0;
|
private static final byte TYPE_GPS = 0;
|
||||||
@ -47,7 +47,6 @@ public class HuamiActivityDetailsParser {
|
|||||||
private static final byte TYPE_SPEED6 = 6;
|
private static final byte TYPE_SPEED6 = 6;
|
||||||
private static final byte TYPE_SWIMMING = 8;
|
private static final byte TYPE_SWIMMING = 8;
|
||||||
|
|
||||||
private static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal(3000000.0);
|
|
||||||
private final ActivityTrack activityTrack;
|
private final ActivityTrack activityTrack;
|
||||||
private final Date baseDate;
|
private final Date baseDate;
|
||||||
private long baseLongitude;
|
private long baseLongitude;
|
||||||
@ -195,11 +194,6 @@ public class HuamiActivityDetailsParser {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double convertHuamiValueToDecimalDegrees(long huamiValue) {
|
|
||||||
BigDecimal result = new BigDecimal(huamiValue).divide(HUAMI_TO_DECIMAL_DEGREES_DIVISOR, GPSCoordinate.GPS_DECIMAL_DEGREES_SCALE, RoundingMode.HALF_UP);
|
|
||||||
return result.doubleValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int consumeHeartRate(byte[] bytes, int offset, long timeOffsetSeconds) {
|
private int consumeHeartRate(byte[] bytes, int offset, long timeOffsetSeconds) {
|
||||||
int v1 = BLETypeConversions.toUint16(bytes[offset]);
|
int v1 = BLETypeConversions.toUint16(bytes[offset]);
|
||||||
int v2 = BLETypeConversions.toUint16(bytes[offset + 1]);
|
int v2 = BLETypeConversions.toUint16(bytes[offset + 1]);
|
||||||
@ -295,14 +289,4 @@ public class HuamiActivityDetailsParser {
|
|||||||
LOG.debug("got packet type 8 (swimming?): " + GB.hexdump(bytes, offset, 6));
|
LOG.debug("got packet type 8 (swimming?): " + GB.hexdump(bytes, offset, 6));
|
||||||
return 6;
|
return 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createActivityName(BaseActivitySummary summary) {
|
|
||||||
String name = summary.getName();
|
|
||||||
String nameText = "";
|
|
||||||
Long id = summary.getId();
|
|
||||||
if (name != null) {
|
|
||||||
nameText = name + " - ";
|
|
||||||
}
|
|
||||||
return nameText + id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
@ -84,6 +85,7 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void startFetching() throws IOException {
|
protected void startFetching() throws IOException {
|
||||||
|
expectedDataLength = 0;
|
||||||
lastPacketCounter = -1;
|
lastPacketCounter = -1;
|
||||||
|
|
||||||
TransactionBuilder builder = performInitialized(getName());
|
TransactionBuilder builder = performInitialized(getName());
|
||||||
@ -122,13 +124,28 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the finishing of fetching the activity.
|
||||||
|
* @param success whether fetching was successful
|
||||||
|
* @return whether handling the activity fetch finish was successful
|
||||||
|
*/
|
||||||
@CallSuper
|
@CallSuper
|
||||||
protected void handleActivityFetchFinish(boolean success) {
|
protected boolean handleActivityFetchFinish(boolean success) {
|
||||||
GB.updateTransferNotification(null, "", false, 100, getContext());
|
GB.updateTransferNotification(null, "", false, 100, getContext());
|
||||||
operationFinished();
|
operationFinished();
|
||||||
unsetBusy();
|
unsetBusy();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the received data has the expected checksum. Only
|
||||||
|
* relevant for Huami2021Support devices.
|
||||||
|
*
|
||||||
|
* @param crc32 the expected checksum
|
||||||
|
* @return whether the checksum was valid
|
||||||
|
*/
|
||||||
|
protected abstract boolean validChecksum(int crc32);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to handle the incoming activity data.
|
* Method to handle the incoming activity data.
|
||||||
* There are two kind of messages we currently know:
|
* There are two kind of messages we currently know:
|
||||||
@ -158,13 +175,18 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
|
|||||||
|
|
||||||
if (ArrayUtils.equals(value, HuamiService.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) {
|
if (ArrayUtils.equals(value, HuamiService.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) {
|
||||||
handleActivityMetadata(value);
|
handleActivityMetadata(value);
|
||||||
TransactionBuilder newBuilder = createTransactionBuilder(taskName + " Step 2");
|
if (expectedDataLength == 0 && getSupport() instanceof Huami2021Support) {
|
||||||
newBuilder.notify(characteristicActivityData, true);
|
// Nothing to receive, if we try to fetch data it will fail
|
||||||
newBuilder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_FETCH_DATA});
|
sendAck2021(true);
|
||||||
try {
|
} else {
|
||||||
performImmediately(newBuilder);
|
TransactionBuilder newBuilder = createTransactionBuilder(taskName + " Step 2");
|
||||||
} catch (IOException ex) {
|
newBuilder.notify(characteristicActivityData, true);
|
||||||
GB.toast(getContext(), "Error fetching debug logs: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
newBuilder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_FETCH_DATA});
|
||||||
|
try {
|
||||||
|
performImmediately(newBuilder);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
GB.toast(getContext(), "Error fetching debug logs: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@ -177,54 +199,119 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleActivityMetadata(byte[] value) {
|
private void handleActivityMetadata(byte[] value) {
|
||||||
// it's 16 on the MB7, with a 0 at the end
|
if (value.length < 3) {
|
||||||
if (value.length == 15 || (value.length == 16 && value[15] == 0x00)) {
|
LOG.warn("Activity metadata too short: {}", Logging.formatBytes(value));
|
||||||
// first two bytes are whether our request was accepted
|
handleActivityFetchFinish(false);
|
||||||
if (ArrayUtils.equals(value, HuamiService.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) {
|
return;
|
||||||
// the third byte (0x01 on success) = ?
|
}
|
||||||
// the 4th - 7th bytes represent the number of bytes/packets to expect, excluding the counter bytes
|
|
||||||
expectedDataLength = BLETypeConversions.toUint32(Arrays.copyOfRange(value, 3, 7));
|
|
||||||
|
|
||||||
// last 8 bytes are the start date
|
if (value[0] != HuamiService.RESPONSE) {
|
||||||
Calendar startTimestamp = getSupport().fromTimeBytes(Arrays.copyOfRange(value, 7, value.length));
|
LOG.warn("Activity metadata not a response: {}", Logging.formatBytes(value));
|
||||||
setStartTimestamp(startTimestamp);
|
handleActivityFetchFinish(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LOG.info("Will transfer {} packets since {}", expectedDataLength, startTimestamp.getTime());
|
switch (value[1]) {
|
||||||
|
case HuamiService.COMMAND_ACTIVITY_DATA_START_DATE:
|
||||||
GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data),
|
handleStartDateResponse(value);
|
||||||
getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since,
|
return;
|
||||||
DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), true, 0, getContext());
|
case HuamiService.COMMAND_FETCH_DATA:
|
||||||
} else {
|
handleFetchDataResponse(value);
|
||||||
|
return;
|
||||||
|
case HuamiService.COMMAND_ACK_ACTIVITY_DATA:
|
||||||
|
// ignore, this is just the reply to the COMMAND_ACK_ACTIVITY_DATA
|
||||||
|
LOG.info("Got reply to COMMAND_ACK_ACTIVITY_DATA");
|
||||||
|
return;
|
||||||
|
default:
|
||||||
LOG.warn("Unexpected activity metadata: {}", Logging.formatBytes(value));
|
LOG.warn("Unexpected activity metadata: {}", Logging.formatBytes(value));
|
||||||
handleActivityFetchFinish(false);
|
handleActivityFetchFinish(false);
|
||||||
}
|
}
|
||||||
} else if (ArrayUtils.startsWith(value, HuamiService.RESPONSE_FINISH_SUCCESS)) {
|
}
|
||||||
if (value.length == 3) {
|
|
||||||
// older Huami devices, just finish
|
private void handleStartDateResponse(final byte[] value) {
|
||||||
handleActivityFetchFinish(true);
|
if (value[2] != HuamiService.SUCCESS) {
|
||||||
} else if (value.length == 7 && getSupport() instanceof Huami2021Support) {
|
LOG.warn("Start date unsuccessful response: {}", Logging.formatBytes(value));
|
||||||
// TODO: What do the extra 4 bytes mean?
|
|
||||||
try {
|
|
||||||
// not sure why we need to send this (it's acknowledging the data?) but it will get stuck otherwise
|
|
||||||
final TransactionBuilder builder = performInitialized(getName() + " end");
|
|
||||||
builder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_ACK_ACTIVITY_DATA, 0x09});
|
|
||||||
builder.queue(getQueue());
|
|
||||||
} catch (final IOException e) {
|
|
||||||
LOG.error("Ending failed", e);
|
|
||||||
handleActivityFetchFinish(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleActivityFetchFinish(true);
|
|
||||||
} else {
|
|
||||||
LOG.warn("Unexpected activity metadata finish success: {}", Logging.formatBytes(value));
|
|
||||||
handleActivityFetchFinish(false);
|
|
||||||
}
|
|
||||||
} else if (Arrays.equals(HuamiService.RESPONSE_ACK_SUCCESS, value) && getSupport() instanceof Huami2021Support) {
|
|
||||||
// ignore, this is just the reply to the COMMAND_ACK_ACTIVITY_DATA
|
|
||||||
LOG.info("Got reply to COMMAND_ACK_ACTIVITY_DATA");
|
|
||||||
} else {
|
|
||||||
LOG.warn("Unexpected activity metadata: {}", Logging.formatBytes(value));
|
|
||||||
handleActivityFetchFinish(false);
|
handleActivityFetchFinish(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's 16 on the MB7, with a 0 at the end
|
||||||
|
if (value.length != 15 && (value.length != 16 && value[15] != 0x00)) {
|
||||||
|
LOG.warn("Start date response length: {}", Logging.formatBytes(value));
|
||||||
|
handleActivityFetchFinish(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the third byte (0x01 on success) = ?
|
||||||
|
// the 4th - 7th bytes represent the number of bytes/packets to expect, excluding the counter bytes
|
||||||
|
expectedDataLength = BLETypeConversions.toUint32(Arrays.copyOfRange(value, 3, 7));
|
||||||
|
|
||||||
|
// last 8 bytes are the start date
|
||||||
|
Calendar startTimestamp = getSupport().fromTimeBytes(Arrays.copyOfRange(value, 7, value.length));
|
||||||
|
|
||||||
|
if (expectedDataLength == 0) {
|
||||||
|
LOG.info("No data to fetch since {}", startTimestamp.getTime());
|
||||||
|
handleActivityFetchFinish(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStartTimestamp(startTimestamp);
|
||||||
|
LOG.info("Will transfer {} packets since {}", expectedDataLength, startTimestamp.getTime());
|
||||||
|
|
||||||
|
GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data),
|
||||||
|
getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since,
|
||||||
|
DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), true, 0, getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFetchDataResponse(final byte[] value) {
|
||||||
|
if (value[2] != HuamiService.SUCCESS) {
|
||||||
|
LOG.warn("Fetch data unsuccessful response: {}", Logging.formatBytes(value));
|
||||||
|
handleActivityFetchFinish(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length != 3 && value.length != 7) {
|
||||||
|
LOG.warn("Fetch data unexpected metadata length: {}", Logging.formatBytes(value));
|
||||||
|
handleActivityFetchFinish(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length == 7 && !validChecksum(BLETypeConversions.toUint32(value, 3))) {
|
||||||
|
LOG.warn("Data checksum invalid");
|
||||||
|
handleActivityFetchFinish(false);
|
||||||
|
sendAck2021(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean handleFinishSuccess;
|
||||||
|
try {
|
||||||
|
handleFinishSuccess = handleActivityFetchFinish(true);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.warn("Failed to handle activity fetch finish", e);
|
||||||
|
handleFinishSuccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean keepActivityDataOnDevice = HuamiCoordinator.getKeepActivityDataOnDevice(getDevice().getAddress());
|
||||||
|
|
||||||
|
sendAck2021(keepActivityDataOnDevice || !handleFinishSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendAck2021(final boolean keepDataOnDevice) {
|
||||||
|
if (!(getSupport() instanceof Huami2021Support)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0x01 to ACK, mark as saved on phone (drop from band)
|
||||||
|
// 0x09 to ACK, but keep it marked as not saved
|
||||||
|
// If 0x01 is sent, detailed information seems to be discarded, and is not sent again anymore
|
||||||
|
final byte ackByte = (byte) (keepDataOnDevice ? 0x09 : 0x01);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final TransactionBuilder builder = performInitialized(getName() + " end");
|
||||||
|
builder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_ACK_ACTIVITY_DATA, ackByte});
|
||||||
|
performImmediately(builder);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
LOG.error("Ending failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,7 +329,6 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
|
|||||||
editor.apply();
|
editor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected GregorianCalendar getLastSuccessfulSyncTime() {
|
protected GregorianCalendar getLastSuccessfulSyncTime() {
|
||||||
long timeStampMillis = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getLong(getLastSyncTimeKey(), 0);
|
long timeStampMillis = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getLong(getLastSyncTimeKey(), 0);
|
||||||
if (timeStampMillis != 0) {
|
if (timeStampMillis != 0) {
|
||||||
|
@ -45,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
@ -78,21 +79,31 @@ public class FetchActivityOperation extends AbstractFetchOperation {
|
|||||||
startFetching(builder, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY, sinceWhen);
|
startFetching(builder, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY, sinceWhen);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void handleActivityFetchFinish(boolean success) {
|
@Override
|
||||||
|
protected boolean handleActivityFetchFinish(boolean success) {
|
||||||
LOG.info("{} has finished round {}", getName(), fetchCount);
|
LOG.info("{} has finished round {}", getName(), fetchCount);
|
||||||
GregorianCalendar lastSyncTimestamp = saveSamples();
|
GregorianCalendar lastSyncTimestamp = saveSamples();
|
||||||
|
|
||||||
if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) {
|
if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) {
|
||||||
try {
|
try {
|
||||||
startFetching();
|
startFetching();
|
||||||
return;
|
return true;
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
LOG.error("Error starting another round of {}", getName(), ex);
|
LOG.error("Error starting another round of {}", getName(), ex);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.handleActivityFetchFinish(success);
|
final boolean superSuccess = super.handleActivityFetchFinish(success);
|
||||||
GB.signalActivityDataFinish();
|
GB.signalActivityDataFinish();
|
||||||
|
return superSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean validChecksum(int crc32) {
|
||||||
|
// TODO actually check it
|
||||||
|
LOG.warn("Checksum not implemented for activity data, assuming it's valid");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) {
|
private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) {
|
||||||
|
@ -24,6 +24,8 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@ -39,8 +41,10 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
@ -51,15 +55,20 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|||||||
*/
|
*/
|
||||||
public class FetchSportsDetailsOperation extends AbstractFetchOperation {
|
public class FetchSportsDetailsOperation extends AbstractFetchOperation {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(FetchSportsDetailsOperation.class);
|
private static final Logger LOG = LoggerFactory.getLogger(FetchSportsDetailsOperation.class);
|
||||||
|
private final AbstractHuamiActivityDetailsParser detailsParser;
|
||||||
private final BaseActivitySummary summary;
|
private final BaseActivitySummary summary;
|
||||||
private final String lastSyncTimeKey;
|
private final String lastSyncTimeKey;
|
||||||
|
|
||||||
private ByteArrayOutputStream buffer;
|
private ByteArrayOutputStream buffer;
|
||||||
|
|
||||||
FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary, @NonNull HuamiSupport support, @NonNull String lastSyncTimeKey) {
|
FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary,
|
||||||
|
@NonNull AbstractHuamiActivityDetailsParser detailsParser,
|
||||||
|
@NonNull HuamiSupport support,
|
||||||
|
@NonNull String lastSyncTimeKey) {
|
||||||
super(support);
|
super(support);
|
||||||
setName("fetching sport details");
|
setName("fetching sport details");
|
||||||
this.summary = summary;
|
this.summary = summary;
|
||||||
|
this.detailsParser = detailsParser;
|
||||||
this.lastSyncTimeKey = lastSyncTimeKey;
|
this.lastSyncTimeKey = lastSyncTimeKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +81,7 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleActivityFetchFinish(boolean success) {
|
protected boolean handleActivityFetchFinish(boolean success) {
|
||||||
LOG.info(getName() + " has finished round " + fetchCount);
|
LOG.info(getName() + " has finished round " + fetchCount);
|
||||||
// GregorianCalendar lastSyncTimestamp = saveSamples();
|
// GregorianCalendar lastSyncTimestamp = saveSamples();
|
||||||
// if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) {
|
// if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) {
|
||||||
@ -84,12 +93,14 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
boolean parseSuccess = true;
|
||||||
|
|
||||||
if (success) {
|
if (success && buffer.size() > 0) {
|
||||||
HuamiActivityDetailsParser parser = new HuamiActivityDetailsParser(summary);
|
if (detailsParser instanceof HuamiActivityDetailsParser) {
|
||||||
parser.setSkipCounterByte(false); // is already stripped
|
((HuamiActivityDetailsParser) detailsParser).setSkipCounterByte(false); // is already stripped
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
ActivityTrack track = parser.parse(buffer.toByteArray());
|
ActivityTrack track = detailsParser.parse(buffer.toByteArray());
|
||||||
ActivityTrackExporter exporter = createExporter();
|
ActivityTrackExporter exporter = createExporter();
|
||||||
String trackType = "track";
|
String trackType = "track";
|
||||||
switch (summary.getActivityKind()) {
|
switch (summary.getActivityKind()) {
|
||||||
@ -112,6 +123,8 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
|
|||||||
trackType = getContext().getString(R.string.activity_type_swimming);
|
trackType = getContext().getString(R.string.activity_type_swimming);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
final String rawBytesPath = saveRawBytes();
|
||||||
|
|
||||||
String fileName = FileUtils.makeValidFileName("gadgetbridge-"+trackType.toLowerCase()+"-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx");
|
String fileName = FileUtils.makeValidFileName("gadgetbridge-"+trackType.toLowerCase()+"-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx");
|
||||||
File targetFile = new File(FileUtils.getExternalFilesDir(), fileName);
|
File targetFile = new File(FileUtils.getExternalFilesDir(), fileName);
|
||||||
|
|
||||||
@ -120,21 +133,35 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
|
|||||||
|
|
||||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||||
summary.setGpxTrack(targetFile.getAbsolutePath());
|
summary.setGpxTrack(targetFile.getAbsolutePath());
|
||||||
|
if (rawBytesPath != null) {
|
||||||
|
summary.setRawDetailsPath(rawBytesPath);
|
||||||
|
}
|
||||||
dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary);
|
dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary);
|
||||||
}
|
}
|
||||||
} catch (ActivityTrackExporter.GPXTrackEmptyException ex) {
|
} catch (ActivityTrackExporter.GPXTrackEmptyException ex) {
|
||||||
GB.toast(getContext(), "This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.ERROR, ex);
|
GB.toast(getContext(), "This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
GregorianCalendar endTime = BLETypeConversions.createCalendar();
|
|
||||||
endTime.setTime(summary.getEndTime());
|
|
||||||
saveLastSyncTimestamp(endTime);
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
GB.toast(getContext(), "Error getting activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
GB.toast(getContext(), "Error getting activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||||
|
parseSuccess = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.handleActivityFetchFinish(success);
|
if (success && parseSuccess) {
|
||||||
|
// Always increment the sync timestamp on success, even if we did not get data
|
||||||
|
GregorianCalendar endTime = BLETypeConversions.createCalendar();
|
||||||
|
endTime.setTime(summary.getEndTime());
|
||||||
|
saveLastSyncTimestamp(endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean superSuccess = super.handleActivityFetchFinish(success);
|
||||||
|
|
||||||
|
return superSuccess && parseSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean validChecksum(int crc32) {
|
||||||
|
return crc32 == CheckSums.getCRC32(buffer.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActivityTrackExporter createExporter() {
|
private ActivityTrackExporter createExporter() {
|
||||||
@ -198,4 +225,23 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
|
|||||||
calendar.setTime(summary.getStartTime());
|
calendar.setTime(summary.getStartTime());
|
||||||
return calendar;
|
return calendar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String saveRawBytes() {
|
||||||
|
final String fileName = FileUtils.makeValidFileName(String.format("%s.bin", DateTimeUtils.formatIso8601(summary.getStartTime())));
|
||||||
|
FileOutputStream outputStream = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final File targetFolder = new File(FileUtils.getExternalFilesDir(), "rawDetails");
|
||||||
|
targetFolder.mkdirs();
|
||||||
|
final File targetFile = new File(targetFolder, fileName);
|
||||||
|
outputStream = new FileOutputStream(targetFile);
|
||||||
|
outputStream.write(buffer.toByteArray());
|
||||||
|
outputStream.close();
|
||||||
|
return targetFile.getAbsolutePath();
|
||||||
|
} catch (final IOException e) {
|
||||||
|
LOG.error("Failed to save raw bytes", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,14 +32,21 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021ActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService;
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021ActivityDetailsParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,7 +70,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleActivityFetchFinish(boolean success) {
|
protected boolean handleActivityFetchFinish(boolean success) {
|
||||||
LOG.info(getName() + " has finished round " + fetchCount);
|
LOG.info(getName() + " has finished round " + fetchCount);
|
||||||
|
|
||||||
// GregorianCalendar lastSyncTimestamp = saveSamples();
|
// GregorianCalendar lastSyncTimestamp = saveSamples();
|
||||||
@ -77,12 +84,16 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
BaseActivitySummary summary = null;
|
BaseActivitySummary summary = null;
|
||||||
if (success) {
|
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(getDevice());
|
||||||
|
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(getDevice());
|
||||||
|
|
||||||
|
boolean parseSummarySuccess = true;
|
||||||
|
|
||||||
|
if (success && buffer.size() > 0) {
|
||||||
summary = new BaseActivitySummary();
|
summary = new BaseActivitySummary();
|
||||||
summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set
|
summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set
|
||||||
summary.setRawSummaryData(buffer.toByteArray());
|
summary.setRawSummaryData(buffer.toByteArray());
|
||||||
HuamiActivitySummaryParser parser = new HuamiActivitySummaryParser();
|
summary = summaryParser.parseBinaryData(summary);
|
||||||
summary = parser.parseBinaryData(summary);
|
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
summary.setSummaryData(null); // remove json before saving to database,
|
summary.setSummaryData(null); // remove json before saving to database,
|
||||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||||
@ -95,21 +106,32 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
|
|||||||
session.getBaseActivitySummaryDao().insertOrReplace(summary);
|
session.getBaseActivitySummaryDao().insertOrReplace(summary);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
GB.toast(getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex);
|
GB.toast(getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||||
|
parseSummarySuccess = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.handleActivityFetchFinish(success);
|
final boolean superSuccess = super.handleActivityFetchFinish(success);
|
||||||
|
boolean getDetailsSuccess = true;
|
||||||
|
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, getSupport(), getLastSyncTimeKey());
|
final AbstractHuamiActivityDetailsParser detailsParser = ((HuamiActivitySummaryParser) summaryParser).getDetailsParser(summary);
|
||||||
|
|
||||||
|
FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, detailsParser, getSupport(), getLastSyncTimeKey());
|
||||||
try {
|
try {
|
||||||
nextOperation.perform();
|
nextOperation.perform();
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
GB.toast(getContext(), "Unable to fetch activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
GB.toast(getContext(), "Unable to fetch activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||||
|
getDetailsSuccess = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return parseSummarySuccess && superSuccess && getDetailsSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean validChecksum(int crc32) {
|
||||||
|
return crc32 == CheckSums.getCRC32(buffer.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -36,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipS
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
@ -80,16 +81,24 @@ public class HuamiFetchDebugLogsOperation extends AbstractFetchOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleActivityFetchFinish(boolean success) {
|
protected boolean handleActivityFetchFinish(boolean success) {
|
||||||
LOG.info(getName() +" data has finished");
|
LOG.info("{} data has finished", getName());
|
||||||
try {
|
try {
|
||||||
logOutputStream.close();
|
logOutputStream.close();
|
||||||
logOutputStream = null;
|
logOutputStream = null;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warn("could not close output stream", e);
|
LOG.warn("could not close output stream", e);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
super.handleActivityFetchFinish(success);
|
|
||||||
|
return super.handleActivityFetchFinish(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean validChecksum(int crc32) {
|
||||||
|
// TODO actually check it?
|
||||||
|
LOG.warn("Checksum not implemented for debug logs, assuming it's valid");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
80
app/src/main/proto/huami.proto
Normal file
80
app/src/main/proto/huami.proto
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto";
|
||||||
|
option java_outer_classname = "HuamiProtos";
|
||||||
|
|
||||||
|
message WorkoutSummary {
|
||||||
|
string version = 1;
|
||||||
|
Location location = 2;
|
||||||
|
Type type = 3;
|
||||||
|
Distance distance = 4;
|
||||||
|
Steps steps = 11;
|
||||||
|
Time time = 7;
|
||||||
|
Pace pace = 10;
|
||||||
|
HeartRate heartRate = 19;
|
||||||
|
Calories calories = 16;
|
||||||
|
TrainingEffect trainingEffect = 21;
|
||||||
|
HeartRateZones heartRateZones = 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Location {
|
||||||
|
// TODO 1, 2, 3
|
||||||
|
int32 baseLatitude = 5; // /6000000 -> coords
|
||||||
|
int32 baseLongitude = 6; // /-6000000 -> coords
|
||||||
|
int32 baseAltitude = 7; // /2 -> meters
|
||||||
|
int32 maxLatitude = 8; // /3000000 -> coords
|
||||||
|
int32 minLatitude = 9; // /3000000 -> coords
|
||||||
|
int32 maxLongitude = 10; // /3000000 -> coords
|
||||||
|
int32 minLongitude = 11; // /3000000 -> coords
|
||||||
|
}
|
||||||
|
|
||||||
|
message HeartRate {
|
||||||
|
int32 avg = 1; // bpm
|
||||||
|
int32 max = 2; // bpm
|
||||||
|
int32 min = 3; // bpm
|
||||||
|
}
|
||||||
|
|
||||||
|
message Steps {
|
||||||
|
float avgCadence = 1; // steps/sec
|
||||||
|
float maxCadence = 2; // steps/sec
|
||||||
|
int32 avgStride = 3; // cm
|
||||||
|
int32 steps = 4; // count
|
||||||
|
}
|
||||||
|
|
||||||
|
message Type {
|
||||||
|
int32 type = 1; // 1 = running, 4 = bike, 3 = walk
|
||||||
|
// TODO 2, always 0?
|
||||||
|
}
|
||||||
|
|
||||||
|
message Distance {
|
||||||
|
float distance = 1; // meters
|
||||||
|
}
|
||||||
|
|
||||||
|
message Time {
|
||||||
|
int32 totalDuration = 1; // seconds
|
||||||
|
int32 workoutDuration = 2; // seconds
|
||||||
|
int32 pauseDuration = 3; // seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
message Pace {
|
||||||
|
float avg = 1; // val * 1000 / 60 -> min/km
|
||||||
|
float best = 2; // val * 1000 / 60 -> min/km
|
||||||
|
}
|
||||||
|
|
||||||
|
message Calories {
|
||||||
|
int32 calories = 1; // kcal
|
||||||
|
}
|
||||||
|
|
||||||
|
message HeartRateZones {
|
||||||
|
// TODO 1, is always = 1?
|
||||||
|
// Zones: N/A, Warm-up, Fat-burn time, Aerobic, Anaerobic, Extreme
|
||||||
|
repeated int32 zoneMax = 2; // bpm
|
||||||
|
repeated int32 zoneTime = 3; // seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
message TrainingEffect {
|
||||||
|
float aerobicTrainingEffect = 4;
|
||||||
|
float anaerobicTrainingEffect = 5;
|
||||||
|
int32 currentWorkoutLoad = 6;
|
||||||
|
int32 maximumOxygenUptake = 7; // ml/kg/min
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
option java_package = "nodomain.freeyourgadget.gadgetbridge";
|
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto";
|
||||||
option java_outer_classname = "SMAQ2OSSProtos";
|
option java_outer_classname = "SMAQ2OSSProtos";
|
||||||
|
|
||||||
message SetTime
|
message SetTime
|
||||||
|
@ -246,6 +246,7 @@
|
|||||||
<string name="pref_title_canned_messages_dismisscall">Call Dismissal</string>
|
<string name="pref_title_canned_messages_dismisscall">Call Dismissal</string>
|
||||||
<string name="pref_title_canned_messages_set">Update on device</string>
|
<string name="pref_title_canned_messages_set">Update on device</string>
|
||||||
<string name="pref_header_development">Developer options</string>
|
<string name="pref_header_development">Developer options</string>
|
||||||
|
<string name="pref_header_authentication">Authentication</string>
|
||||||
<string name="pref_title_development_miaddr">Mi Band address</string>
|
<string name="pref_title_development_miaddr">Mi Band address</string>
|
||||||
<string name="pref_title_pebble_settings">Pebble settings</string>
|
<string name="pref_title_pebble_settings">Pebble settings</string>
|
||||||
<string name="pref_header_activitytrackers">Activity trackers</string>
|
<string name="pref_header_activitytrackers">Activity trackers</string>
|
||||||
@ -723,7 +724,7 @@
|
|||||||
<string name="average">Average: %1$s</string>
|
<string name="average">Average: %1$s</string>
|
||||||
<string name="pref_title_dont_ack_transfer">Do not ACK activity data transfer</string>
|
<string name="pref_title_dont_ack_transfer">Do not ACK activity data transfer</string>
|
||||||
<string name="pref_summary_dont_ack_transfers">If not ACKed to the band, activity data is not cleared. Useful if GB is used together with other apps.</string>
|
<string name="pref_summary_dont_ack_transfers">If not ACKed to the band, activity data is not cleared. Useful if GB is used together with other apps.</string>
|
||||||
<string name="pref_summary_keep_data_on_device">Will keep activity data on the Mi Band even after synchronization. Useful if GB is used together with other apps.</string>
|
<string name="pref_summary_keep_data_on_device">Will keep activity data on the device even after synchronization. Useful if GB is used together with other apps.</string>
|
||||||
<string name="pref_title_low_latency_fw_update">Use low-latency mode for firmware flashing</string>
|
<string name="pref_title_low_latency_fw_update">Use low-latency mode for firmware flashing</string>
|
||||||
<string name="pref_summary_low_latency_fw_update">This might help on devices where firmware flashing fails.</string>
|
<string name="pref_summary_low_latency_fw_update">This might help on devices where firmware flashing fails.</string>
|
||||||
<string name="pref_title_third_party_app_device_settings">Allow 3rd party apps to change settings</string>
|
<string name="pref_title_third_party_app_device_settings">Allow 3rd party apps to change settings</string>
|
||||||
@ -1459,6 +1460,8 @@
|
|||||||
<string name="sports_activity_quick_filter_7days">7 days</string>
|
<string name="sports_activity_quick_filter_7days">7 days</string>
|
||||||
<string name="sports_activity_quick_filter_30days">30 days</string>
|
<string name="sports_activity_quick_filter_30days">30 days</string>
|
||||||
<string name="sports_activity_quick_filter_select">Time period</string>
|
<string name="sports_activity_quick_filter_select">Time period</string>
|
||||||
|
<string name="sports_activity_confirm_delete_title">Delete %d activities</string>
|
||||||
|
<string name="sports_activity_confirm_delete_description">Are you sure you want to delete %d activities?</string>
|
||||||
<string name="activity_summaries_all_devices">All devices</string>
|
<string name="activity_summaries_all_devices">All devices</string>
|
||||||
<string name="activity_filter_from_placeholder">distant past</string>
|
<string name="activity_filter_from_placeholder">distant past</string>
|
||||||
<string name="activity_filter_to_placeholder">today</string>
|
<string name="activity_filter_to_placeholder">today</string>
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="pref_header_authentication"
|
||||||
|
android:title="@string/pref_header_authentication" />
|
||||||
|
</androidx.preference.PreferenceScreen>
|
6
app/src/main/res/xml/devicesettings_header_developer.xml
Normal file
6
app/src/main/res/xml/devicesettings_header_developer.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="pref_header_development"
|
||||||
|
android:title="@string/pref_header_development" />
|
||||||
|
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<SwitchPreference
|
||||||
|
android:icon="@drawable/ic_activity_unknown_small"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="keep_activity_data_on_device"
|
||||||
|
android:summary="@string/pref_summary_keep_data_on_device"
|
||||||
|
android:title="@string/pref_title_keep_data_on_device" />
|
||||||
|
</androidx.preference.PreferenceScreen>
|
Loading…
x
Reference in New Issue
Block a user