1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-24 01:25:50 +01:00

Bangle.js: Fetch activity data

This commit is contained in:
José Rebelo 2023-08-08 22:11:14 +01:00
parent f97250d46b
commit a95820d09e
10 changed files with 370 additions and 44 deletions

View File

@ -43,7 +43,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception {
final Schema schema = new Schema(50, MAIN_PACKAGE + ".entities");
final Schema schema = new Schema(51, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@ -613,6 +613,7 @@ public class GBDaoGenerator {
Entity activitySample = addEntity(schema, "BangleJSActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
addHeartRateProperties(activitySample);

View File

@ -0,0 +1,38 @@
/* Copyright (C) 2023 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.BangleJSActivitySampleDao;
public class GadgetbridgeUpdate_51 implements DBUpdateScript {
@Override
public void upgradeSchema(final SQLiteDatabase db) {
if (!DBHelper.existsColumn(BangleJSActivitySampleDao.TABLENAME, BangleJSActivitySampleDao.Properties.RawIntensity.columnName, db)) {
final String sqlAddColumnRawIntensity = "ALTER TABLE " + BangleJSActivitySampleDao.TABLENAME + " ADD COLUMN "
+ BangleJSActivitySampleDao.Properties.RawIntensity.columnName + " INTEGER DEFAULT -1 NOT NULL";
db.execSQL(sqlAddColumnRawIntensity);
}
}
@Override
public void downgradeSchema(final SQLiteDatabase db) {
}
}

View File

@ -25,4 +25,8 @@ public final class BangleJSConstants {
public static final UUID UUID_CHARACTERISTIC_NORDIC_UART_TX = UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
public static final UUID UUID_CHARACTERISTIC_NORDIC_UART_RX = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
public static final String PREF_BANGLEJS_ACTIVITY_FULL_SYNC_TRIGGER = "pref_banglejs_activity_full_sync_trigger";
public static final String PREF_BANGLEJS_ACTIVITY_FULL_SYNC_STATUS = "pref_banglejs_activity_full_sync_status";
public static final String PREF_BANGLEJS_ACTIVITY_FULL_SYNC_START = "pref_banglejs_activity_full_sync_start";
public static final String PREF_BANGLEJS_ACTIVITY_FULL_SYNC_STOP = "pref_banglejs_activity_full_sync_stop";
}

View File

@ -34,6 +34,7 @@ import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
@ -111,7 +112,7 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public boolean supportsActivityDataFetching() {
return false;
return true;
}
@Override
@ -188,6 +189,7 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
return true;
}
@Override
public int[] getSupportedDeviceSpecificSettings(final GBDevice device) {
final List<Integer> settings = new ArrayList<>();
@ -205,6 +207,8 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
if (BuildConfig.INTERNET_ACCESS)
settings.add(R.xml.devicesettings_device_internet_access);
settings.add(R.xml.devicesettings_banglejs_activity);
settings.add(R.xml.devicesettings_header_developer);
settings.add(R.xml.devicesettings_banglejs_apploader);
settings.add(R.xml.devicesettings_device_intents);
@ -212,6 +216,11 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
return ArrayUtils.toPrimitive(settings.toArray(new Integer[0]));
}
@Override
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
return new BangleJSSettingsCustomizer(device);
}
@Override
public boolean supportsNavigation() {
return true;

View File

@ -19,18 +19,23 @@ package nodomain.freeyourgadget.gadgetbridge.devices.banglejs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.BangleJSActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.BangleJSActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.ID115ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.ID115ActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class BangleJSSampleProvider extends AbstractSampleProvider<BangleJSActivitySample> {
private static final Logger LOG = LoggerFactory.getLogger(BangleJSSampleProvider.class);
public BangleJSSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@ -77,11 +82,54 @@ public class BangleJSSampleProvider extends AbstractSampleProvider<BangleJSActiv
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity;
return rawIntensity / 256.0f;
}
@Override
public BangleJSActivitySample createActivitySample() {
return new BangleJSActivitySample();
}
/**
* Upserts a sample in the database, avoiding duplicated samples if a sample already exists in a
* close timestamp (within 2 minutes);
*/
public void upsertSample(final BangleJSActivitySample sample) {
final List<BangleJSActivitySample> nearSamples = getGBActivitySamples(
sample.getTimestamp() - 60 * 2,
sample.getTimestamp() + 60 * 2,
normalizeType(sample.getRawKind())
);
if (nearSamples.isEmpty()) {
// No nearest sample, just insert
LOG.debug("No duplicate found at {}, inserting", sample.getTimestamp());
addGBActivitySample(sample);
return;
}
BangleJSActivitySample nearestSample = nearSamples.get(0);
for (final BangleJSActivitySample s : nearSamples) {
final int curDist = Math.abs(nearestSample.getTimestamp() - s.getTimestamp());
final int newDist = Math.abs(sample.getTimestamp() - s.getTimestamp());
if (newDist < curDist) {
nearestSample = s;
}
}
LOG.debug("Found {} duplicates for {}, updating nearest sample at {}", nearSamples.size(), sample.getTimestamp(), nearestSample.getTimestamp());
if (sample.getHeartRate() != 0) {
nearestSample.setHeartRate(sample.getHeartRate());
}
if (sample.getSteps() != 0) {
nearestSample.setSteps(sample.getSteps());
}
if (sample.getRawIntensity() != 0) {
nearestSample.setRawIntensity(sample.getRawIntensity());
}
addGBActivitySample(nearestSample);
}
}

View File

@ -0,0 +1,141 @@
/* Copyright (C) 2023 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.banglejs;
import static nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSConstants.PREF_BANGLEJS_ACTIVITY_FULL_SYNC_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSConstants.PREF_BANGLEJS_ACTIVITY_FULL_SYNC_STATUS;
import static nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSConstants.PREF_BANGLEJS_ACTIVITY_FULL_SYNC_STOP;
import static nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSConstants.PREF_BANGLEJS_ACTIVITY_FULL_SYNC_TRIGGER;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Parcel;
import androidx.preference.EditTextPreference;
import androidx.preference.Preference;
import java.util.Collections;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class BangleJSSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
private ProgressDialog activityFullSyncDialog;
final GBDevice device;
public BangleJSSettingsCustomizer(final GBDevice device) {
this.device = device;
}
@Override
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
// Handle full sync status
if (preference.getKey().equals(PREF_BANGLEJS_ACTIVITY_FULL_SYNC_STATUS)) {
final EditTextPreference fullSyncStatusPreference = (EditTextPreference) preference;
final String statusValue = fullSyncStatusPreference.getText();
if (activityFullSyncDialog != null) {
switch (statusValue) {
case "start":
activityFullSyncDialog.setMessage(handler.getContext().getString(R.string.busy_task_fetch_activity_data));
break;
case "end":
activityFullSyncDialog.dismiss();
activityFullSyncDialog = null;
break;
default:
activityFullSyncDialog.setMessage(statusValue);
}
}
}
}
@Override
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs) {
final Preference fullSyncPref = handler.findPreference(PREF_BANGLEJS_ACTIVITY_FULL_SYNC_TRIGGER);
if (fullSyncPref != null) {
fullSyncPref.setOnPreferenceClickListener(preference -> {
if (activityFullSyncDialog != null) {
// Already syncing
return true;
}
final Context context = preference.getContext();
new AlertDialog.Builder(context)
.setTitle(R.string.pref_activity_full_sync_trigger_title)
.setMessage(R.string.pref_activity_full_sync_trigger_warning)
.setIcon(R.drawable.ic_refresh)
.setPositiveButton(R.string.start, (dialog, whichButton) -> {
handler.notifyPreferenceChanged(PREF_BANGLEJS_ACTIVITY_FULL_SYNC_START);
activityFullSyncDialog = new ProgressDialog(context);
activityFullSyncDialog.setCancelable(false);
activityFullSyncDialog.setMessage(context.getString(R.string.sony_anc_optimizer_status_starting));
activityFullSyncDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
activityFullSyncDialog.setProgress(0);
activityFullSyncDialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.Cancel), (dialog1, which) -> {
dialog1.dismiss();
activityFullSyncDialog = null;
handler.notifyPreferenceChanged(PREF_BANGLEJS_ACTIVITY_FULL_SYNC_STOP);
});
activityFullSyncDialog.show();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
});
}
}
@Override
public Set<String> getPreferenceKeysWithSummary() {
return Collections.emptySet();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeParcelable(device, 0);
}
public static final Creator<BangleJSSettingsCustomizer> CREATOR = new Creator<BangleJSSettingsCustomizer>() {
@Override
public BangleJSSettingsCustomizer createFromParcel(final Parcel in) {
final GBDevice device = in.readParcelable(BangleJSSettingsCustomizer.class.getClassLoader());
return new BangleJSSettingsCustomizer(device);
}
@Override
public BangleJSSettingsCustomizer[] newArray(final int size) {
return new BangleJSSettingsCustomizer[size];
}
};
}

View File

@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;

View File

@ -25,6 +25,8 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_INTENTS;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_INTERNET_ACCESS;
import static nodomain.freeyourgadget.gadgetbridge.database.DBHelper.getUser;
import static nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSConstants.PREF_BANGLEJS_ACTIVITY_FULL_SYNC_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSConstants.PREF_BANGLEJS_ACTIVITY_FULL_SYNC_STATUS;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
@ -32,6 +34,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
@ -72,6 +75,7 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@ -89,6 +93,7 @@ import io.wax911.emojify.EmojiManager;
import io.wax911.emojify.EmojiUtils;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
@ -96,6 +101,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallContro
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.BangleJSActivitySample;
@ -105,6 +111,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.LocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
@ -117,6 +124,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.util.EmojiConverter;
@ -328,27 +336,6 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
getDevice().setFirmwareVersion2("N/A");
lastBatteryPercent = -1;
/* Here we get the last Activity info saved from Bangle.js, and then send
its timestamp. Bangle.js can then look back at its history and can try and
send any missing data. */
try (DBHandler dbHandler = GBApplication.acquireDB()) {
BangleJSSampleProvider provider = new BangleJSSampleProvider(getDevice(), dbHandler.getDaoSession());
BangleJSActivitySample sample = provider.getLatestActivitySample();
if (sample!=null) {
LOG.info("Send 'actlast' with last activity's timestamp: "+sample.getTimestamp());
try {
JSONObject o = new JSONObject();
o.put("t", "actlast");
o.put("time", sample.getTimestamp());
uartTxJSON("actlast", o);
} catch (JSONException e) {
LOG.info("JSONException: " + e.getLocalizedMessage());
}
}
} catch (Exception ex) {
LOG.warn("Error getting last activity: " + ex.getLocalizedMessage());
}
LOG.info("Initialization Done");
requestBangleGPSPowerStatus();
@ -547,6 +534,9 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
case "notify" :
handleNotificationControl(json);
break;
case "actfetch":
handleActivityFetch(json);
break;
case "act":
handleActivity(json);
break;
@ -626,17 +616,36 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
evaluateGBDeviceEvent(deviceEvtNotificationControl);
}
private void handleActivityFetch(final JSONObject json) throws JSONException {
final String state = json.getString("state");
if ("start".equals(state)) {
GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data),"", true, 0, getContext());
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data));
} else if ("end".equals(state)) {
saveLastSyncTimestamp(System.currentTimeMillis() - 1000L * 60);
getDevice().unsetBusyTask();
GB.updateTransferNotification(null, "", false, 100, getContext());
} else {
LOG.warn("Unknown actfetch state {}", state);
}
final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences()
.withPreference(PREF_BANGLEJS_ACTIVITY_FULL_SYNC_STATUS, state);
evaluateGBDeviceEvent(event);
getDevice().sendDeviceUpdateIntent(getContext());
}
/**
* Handle "act" packet, used to send activity reports
*/
private void handleActivity(JSONObject json) throws JSONException {
BangleJSActivitySample sample = new BangleJSActivitySample();
sample.setTimestamp((int) (System.currentTimeMillis() / 1000L));
int hrm = 0;
int steps = 0;
if (json.has("time")) sample.setTimestamp(json.getInt("time"));
if (json.has("hrm")) hrm = json.getInt("hrm");
if (json.has("stp")) steps = json.getInt("stp");
int timestamp = (int) (json.optLong("ts", System.currentTimeMillis()) / 1000);
int hrm = json.optInt("hrm", 0);
int steps = json.optInt("stp", 0);
int intensity = json.optInt("mov", ActivitySample.NOT_MEASURED);
boolean realtime = json.optInt("rt", 0) == 1;
int activity = BangleJSSampleProvider.TYPE_ACTIVITY;
/*if (json.has("act")) {
String actName = "TYPE_" + json.getString("act").toUpperCase();
@ -651,21 +660,26 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
LOG.info("JSON activity '"+actName+"' not found");
}
}*/
sample.setTimestamp(timestamp);
sample.setRawKind(activity);
sample.setHeartRate(hrm);
sample.setSteps(steps);
try (DBHandler dbHandler = GBApplication.acquireDB()) {
Long userId = getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
BangleJSSampleProvider provider = new BangleJSSampleProvider(getDevice(), dbHandler.getDaoSession());
sample.setDeviceId(deviceId);
sample.setUserId(userId);
provider.addGBActivitySample(sample);
} catch (Exception ex) {
LOG.warn("Error saving activity: " + ex.getLocalizedMessage());
sample.setRawIntensity(intensity);
if (!realtime) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final Long userId = getUser(dbHandler.getDaoSession()).getId();
final Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
BangleJSSampleProvider provider = new BangleJSSampleProvider(getDevice(), dbHandler.getDaoSession());
sample.setDeviceId(deviceId);
sample.setUserId(userId);
provider.upsertSample(sample);
} catch (final Exception ex) {
LOG.warn("Error saving activity: " + ex.getLocalizedMessage());
}
}
// push realtime data
if (realtimeHRM || realtimeStep) {
if (realtime && (realtimeHRM || realtimeStep)) {
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
@ -942,6 +956,17 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
return intent;
}
@Override
public void onSendConfiguration(final String config) {
switch (config) {
case PREF_BANGLEJS_ACTIVITY_FULL_SYNC_START:
fetchActivityData(0);
return;
}
LOG.warn("Unknown config changed: {}", config);
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
@ -1306,7 +1331,11 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
@Override
public void onFetchRecordedData(int dataTypes) {
if (dataTypes == RecordedDataTypes.TYPE_DEBUGLOGS) {
if ((dataTypes & RecordedDataTypes.TYPE_ACTIVITY) != 0) {
fetchActivityData(getLastSuccessfulSyncTime());
}
if ((dataTypes & RecordedDataTypes.TYPE_DEBUGLOGS) != 0) {
File dir;
try {
dir = FileUtils.getExternalFilesDir();
@ -1329,6 +1358,37 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
}
}
protected void fetchActivityData(final long timestampMillis) {
try {
JSONObject o = new JSONObject();
o.put("t", "actfetch");
o.put("ts", timestampMillis);
uartTxJSON("fetch activity data", o);
} catch (final JSONException e) {
LOG.warn("Failed to fetch activity data", e);
}
}
protected String getLastSyncTimeKey() {
return "lastSyncTimeMillis";
}
protected void saveLastSyncTimestamp(final long timestamp) {
final SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).edit();
editor.putLong(getLastSyncTimeKey(), timestamp);
editor.apply();
}
protected long getLastSuccessfulSyncTime() {
long timeStampMillis = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getLong(getLastSyncTimeKey(), 0);
if (timeStampMillis != 0) {
return timeStampMillis;
}
final GregorianCalendar calendar = BLETypeConversions.createCalendar();
calendar.add(Calendar.DAY_OF_MONTH, -1);
return calendar.getTimeInMillis();
}
@Override
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
if (enable == realtimeHRM) return;

View File

@ -2222,4 +2222,7 @@
<string name="withings_bt_calibration_next">Next</string>
<string name="drag_handle">drag handle</string>
<string name="find_my_phone_found_it">FOUND IT</string>
<string name="pref_activity_full_sync_trigger_summary">Trigger a full sync of all activity data</string>
<string name="pref_activity_full_sync_trigger_title">Full sync</string>
<string name="pref_activity_full_sync_trigger_warning">This will trigger a full sync of all activity data from the device. It may take a few minutes to complete.</string>
</resources>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:key="pref_key_header_banglejs_activity"
android:title="@string/Activity">
<Preference
android:icon="@drawable/ic_refresh"
android:key="pref_banglejs_activity_full_sync_trigger"
android:summary="@string/pref_activity_full_sync_trigger_summary"
android:title="@string/pref_activity_full_sync_trigger_title" />
<!-- dummy preference, just to trigger customizer notifications -->
<EditTextPreference
android:defaultValue="NOT_RUNNING"
android:enabled="false"
android:key="pref_banglejs_activity_full_sync_status"
android:shouldDisableView="false"
android:title="pref_banglejs_activity_full_sync_status"
app:isPreferenceVisible="false" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>