forked from upstream-mirrors/OpenTracks
HealthConnect: Queue exports in the background
This commit is contained in:
@@ -93,6 +93,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
|
|||||||
viewBinding.finishButton.setOnClickListener(v -> {
|
viewBinding.finishButton.setOnClickListener(v -> {
|
||||||
storeTrackMetaData(contentProviderUtils, track);
|
storeTrackMetaData(contentProviderUtils, track);
|
||||||
ExportUtils.postWorkoutExport(this, trackId);
|
ExportUtils.postWorkoutExport(this, trackId);
|
||||||
|
ExportUtils.postWorkoutHealthConnectExport(this, trackId);
|
||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,29 @@ public class ContentProviderUtils {
|
|||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Track> getTracksOrderedByStartTime(Instant startTimeInclusive) {
|
||||||
|
ArrayList<Track> tracks = new ArrayList<>();
|
||||||
|
String selection = null;
|
||||||
|
String[] selectionArgs = null;
|
||||||
|
|
||||||
|
if (startTimeInclusive != null) {
|
||||||
|
selection = TracksColumns.STARTTIME + ">=?";
|
||||||
|
selectionArgs = new String[]{Long.toString(startTimeInclusive.toEpochMilli())};
|
||||||
|
}
|
||||||
|
|
||||||
|
String sortOrder = TracksColumns.STARTTIME + " ASC, " + TracksColumns._ID + " ASC";
|
||||||
|
try (Cursor cursor = getTrackCursor(selection, selectionArgs, sortOrder)) {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
tracks.ensureCapacity(cursor.getCount());
|
||||||
|
do {
|
||||||
|
tracks.add(createTrack(cursor));
|
||||||
|
} while (cursor.moveToNext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
public TrackListIterator searchTracks(String searchQuery) {
|
public TrackListIterator searchTracks(String searchQuery) {
|
||||||
// Needed, because MARKER_COUNT is a virtual column and has to be explicitly requested.
|
// Needed, because MARKER_COUNT is a virtual column and has to be explicitly requested.
|
||||||
final String[] PROJECTION = new String[]{
|
final String[] PROJECTION = new String[]{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import androidx.work.Data;
|
import androidx.work.Data;
|
||||||
|
import androidx.work.ExistingWorkPolicy;
|
||||||
import androidx.work.OneTimeWorkRequest;
|
import androidx.work.OneTimeWorkRequest;
|
||||||
import androidx.work.WorkInfo;
|
import androidx.work.WorkInfo;
|
||||||
import androidx.work.WorkManager;
|
import androidx.work.WorkManager;
|
||||||
@@ -27,6 +28,7 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
|
|||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
|
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
|
||||||
|
import de.dennisguse.opentracks.io.healthconnect.HealthConnectWorker;
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
@@ -67,6 +69,48 @@ public class ExportUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void postWorkoutHealthConnectExport(Context context, Track.Id trackId) {
|
||||||
|
if (!PreferencesUtils.shouldExportToHealthConnect()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PreferencesUtils.shouldRunHealthConnectInitialSync()) {
|
||||||
|
startHealthConnectInitialSync(context, PreferencesUtils.getHealthConnectInitialSyncStartTimeMillis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkRequest request = new OneTimeWorkRequest.Builder(HealthConnectWorker.class)
|
||||||
|
.addTag(HealthConnectWorker.WORK_TAG)
|
||||||
|
.setInputData(new Data.Builder()
|
||||||
|
.putString(HealthConnectWorker.MODE_KEY, HealthConnectWorker.MODE_SINGLE_TRACK)
|
||||||
|
.putLong(HealthConnectWorker.TRACK_ID_KEY, trackId.id())
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
WorkManager.getInstance(context).enqueue(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startHealthConnectInitialSync(Context context, long startTimeMillis) {
|
||||||
|
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(HealthConnectWorker.class)
|
||||||
|
.addTag(HealthConnectWorker.WORK_TAG)
|
||||||
|
.setInputData(new Data.Builder()
|
||||||
|
.putString(HealthConnectWorker.MODE_KEY, HealthConnectWorker.MODE_INITIAL_SYNC)
|
||||||
|
.putLong(HealthConnectWorker.INITIAL_SYNC_START_TIME_MILLIS_KEY, startTimeMillis)
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||||
|
HealthConnectWorker.INITIAL_SYNC_WORK_NAME,
|
||||||
|
ExistingWorkPolicy.KEEP,
|
||||||
|
request
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void cancelHealthConnectExport(Context context) {
|
||||||
|
WorkManager workManager = WorkManager.getInstance(context);
|
||||||
|
workManager.cancelUniqueWork(HealthConnectWorker.INITIAL_SYNC_WORK_NAME);
|
||||||
|
workManager.cancelAllWorkByTag(HealthConnectWorker.WORK_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
public static void exportTrack(Context context, DocumentFile directory, @Nullable String filenameForMultiple, TrackFileFormat trackFileFormat, List<Track.Id> trackIds) {
|
public static void exportTrack(Context context, DocumentFile directory, @Nullable String filenameForMultiple, TrackFileFormat trackFileFormat, List<Track.Id> trackIds) {
|
||||||
|
|
||||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
package de.dennisguse.opentracks.io.healthconnect;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.work.Data;
|
||||||
|
import androidx.work.Worker;
|
||||||
|
import androidx.work.WorkerParameters;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
|
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||||
|
|
||||||
|
public class HealthConnectWorker extends Worker {
|
||||||
|
|
||||||
|
private static final String TAG = HealthConnectWorker.class.getSimpleName();
|
||||||
|
public static final String WORK_TAG = "HealthConnectSync";
|
||||||
|
public static final String INITIAL_SYNC_WORK_NAME = "HealthConnectInitialSync";
|
||||||
|
public static final String MODE_KEY = "mode";
|
||||||
|
public static final String MODE_SINGLE_TRACK = "single_track";
|
||||||
|
public static final String MODE_INITIAL_SYNC = "initial_sync";
|
||||||
|
public static final String TRACK_ID_KEY = "trackId";
|
||||||
|
public static final String INITIAL_SYNC_START_TIME_MILLIS_KEY = "initialSyncStartTimeMillis";
|
||||||
|
private static final long UNSET_TIME_MILLIS = Long.MIN_VALUE;
|
||||||
|
private static final long SYNC_ALL_TIME_MILLIS = -1L;
|
||||||
|
|
||||||
|
public HealthConnectWorker(Context context, WorkerParameters params) {
|
||||||
|
super(context, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result doWork() {
|
||||||
|
boolean includeDetailedRoute = PreferencesUtils.shouldExportDetailedHealthConnectWorkout();
|
||||||
|
|
||||||
|
if (!PreferencesUtils.shouldExportToHealthConnect()) {
|
||||||
|
Log.i(TAG, "Skipping Health Connect export because export is disabled");
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HealthConnectExporter.isAvailable(getApplicationContext())) {
|
||||||
|
Log.w(TAG, "Health Connect SDK not available");
|
||||||
|
updateStatus(R.string.health_connect_sync_status_failure, "Health Connect not available");
|
||||||
|
return Result.failure(new Data.Builder().putString("error", "Health Connect not available").build());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HealthConnectExporter.hasRequiredPermissions(getApplicationContext(), includeDetailedRoute)) {
|
||||||
|
Log.w(TAG, "Health Connect required permissions not granted");
|
||||||
|
updateStatus(R.string.health_connect_sync_status_failure, "Health Connect permission not granted");
|
||||||
|
return Result.failure(new Data.Builder().putString("error", "Health Connect permission not granted").build());
|
||||||
|
}
|
||||||
|
|
||||||
|
String mode = getInputData().getString(MODE_KEY);
|
||||||
|
if (MODE_INITIAL_SYNC.equals(mode)) {
|
||||||
|
return doInitialSync(includeDetailedRoute);
|
||||||
|
}
|
||||||
|
return doSingleTrackExport(includeDetailedRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result doSingleTrackExport(boolean includeDetailedRoute) {
|
||||||
|
long trackId = getInputData().getLong(TRACK_ID_KEY, -1L);
|
||||||
|
if (trackId == -1L) {
|
||||||
|
Log.e(TAG, "No track ID provided");
|
||||||
|
updateStatus(R.string.health_connect_sync_status_failure, "No track ID provided");
|
||||||
|
return Result.failure();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
HealthConnectExporter.ExportResult result =
|
||||||
|
HealthConnectExporter.export(getApplicationContext(), new Track.Id(trackId), includeDetailedRoute);
|
||||||
|
String routeSuffix = result.includedRoute
|
||||||
|
? getApplicationContext().getString(R.string.health_connect_sync_status_route_suffix)
|
||||||
|
: "";
|
||||||
|
PreferencesUtils.setHealthConnectLastSyncStatus(
|
||||||
|
prefixedStatus(getApplicationContext().getString(
|
||||||
|
R.string.health_connect_sync_status_success, result.trackName, routeSuffix)));
|
||||||
|
return Result.success();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return Result.retry();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Health Connect export failed for track " + trackId, e);
|
||||||
|
updateStatus(R.string.health_connect_sync_status_failure,
|
||||||
|
e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||||
|
return Result.failure(new Data.Builder().putString("error", e.getMessage()).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result doInitialSync(boolean includeDetailedRoute) {
|
||||||
|
long configuredStartTimeMillis = getInputData().getLong(
|
||||||
|
INITIAL_SYNC_START_TIME_MILLIS_KEY,
|
||||||
|
PreferencesUtils.getHealthConnectInitialSyncStartTimeMillis());
|
||||||
|
|
||||||
|
if (!PreferencesUtils.hasHealthConnectInitialSyncConfigured()) {
|
||||||
|
Log.i(TAG, "Skipping initial sync because no start time was configured");
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant startInstant = configuredStartTimeMillis == SYNC_ALL_TIME_MILLIS
|
||||||
|
? null
|
||||||
|
: Instant.ofEpochMilli(configuredStartTimeMillis);
|
||||||
|
|
||||||
|
List<Track> rawTracks = new ContentProviderUtils(getApplicationContext())
|
||||||
|
.getTracksOrderedByStartTime(startInstant);
|
||||||
|
List<Track> allTracks = new ArrayList<>();
|
||||||
|
for (Track track : rawTracks) {
|
||||||
|
if (track.stopTime().isAfter(track.startTime())) {
|
||||||
|
allTracks.add(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long resumeStartTimeMillis = PreferencesUtils.getHealthConnectInitialSyncCursorStartTimeMillis();
|
||||||
|
long resumeTrackId = PreferencesUtils.getHealthConnectInitialSyncCursorTrackId();
|
||||||
|
List<Track> remainingTracks = new ArrayList<>();
|
||||||
|
for (Track track : allTracks) {
|
||||||
|
long trackStartMillis = track.startTime().toInstant().toEpochMilli();
|
||||||
|
if (resumeStartTimeMillis == UNSET_TIME_MILLIS
|
||||||
|
|| trackStartMillis > resumeStartTimeMillis
|
||||||
|
|| (trackStartMillis == resumeStartTimeMillis && track.id().id() > resumeTrackId)) {
|
||||||
|
remainingTracks.add(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTracks.isEmpty()) {
|
||||||
|
PreferencesUtils.clearHealthConnectInitialSyncCursor();
|
||||||
|
PreferencesUtils.setHealthConnectInitialSyncCompleted(true);
|
||||||
|
setStatus(getApplicationContext().getString(R.string.health_connect_sync_status_initial_no_tracks));
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTracks.isEmpty()) {
|
||||||
|
PreferencesUtils.clearHealthConnectInitialSyncCursor();
|
||||||
|
PreferencesUtils.setHealthConnectInitialSyncCompleted(true);
|
||||||
|
setStatus(getApplicationContext().getString(
|
||||||
|
R.string.health_connect_sync_status_initial_success, allTracks.size()));
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int index = 0; index < remainingTracks.size(); index++) {
|
||||||
|
if (isStopped()) {
|
||||||
|
Log.i(TAG, "Initial Health Connect sync stopped by WorkManager");
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PreferencesUtils.shouldExportToHealthConnect()) {
|
||||||
|
Log.i(TAG, "Stopping initial Health Connect sync because export was disabled");
|
||||||
|
setStatus(getApplicationContext().getString(R.string.health_connect_sync_status_initial_stopped));
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
Track track = remainingTracks.get(index);
|
||||||
|
String trackName = track.name().isBlank() ? "Workout " + track.id().id() : track.name();
|
||||||
|
setStatus(getApplicationContext().getString(
|
||||||
|
R.string.health_connect_sync_status_initial_progress,
|
||||||
|
index + 1, remainingTracks.size(), trackName));
|
||||||
|
|
||||||
|
try {
|
||||||
|
HealthConnectExporter.export(getApplicationContext(), track.id(), includeDetailedRoute);
|
||||||
|
PreferencesUtils.setHealthConnectInitialSyncCursorStartTimeMillis(
|
||||||
|
track.startTime().toInstant().toEpochMilli());
|
||||||
|
PreferencesUtils.setHealthConnectInitialSyncCursorTrackId(track.id().id());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return Result.retry();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Initial Health Connect sync failed for track " + track.id().id(), e);
|
||||||
|
PreferencesUtils.setHealthConnectInitialSyncCompleted(false);
|
||||||
|
setStatus(getApplicationContext().getString(
|
||||||
|
R.string.health_connect_sync_status_initial_failure,
|
||||||
|
trackName, e.getMessage() != null ? e.getMessage() : "Unknown error"));
|
||||||
|
return Result.failure(new Data.Builder().putString("error", e.getMessage()).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferencesUtils.clearHealthConnectInitialSyncCursor();
|
||||||
|
PreferencesUtils.setHealthConnectInitialSyncCompleted(true);
|
||||||
|
setStatus(getApplicationContext().getString(
|
||||||
|
R.string.health_connect_sync_status_initial_success, allTracks.size()));
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatus(@StringRes int messageResId, String detail) {
|
||||||
|
setStatus(getApplicationContext().getString(messageResId, detail));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setStatus(String message) {
|
||||||
|
PreferencesUtils.setHealthConnectLastSyncStatus(prefixedStatus(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String prefixedStatus(String message) {
|
||||||
|
String timestamp = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(new Date());
|
||||||
|
return timestamp + ": " + message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,6 +71,7 @@ public class PreferencesUtils {
|
|||||||
private static SharedPreferences sharedPreferences;
|
private static SharedPreferences sharedPreferences;
|
||||||
|
|
||||||
private static Resources resources;
|
private static Resources resources;
|
||||||
|
private static final long LONG_UNSET = Long.MIN_VALUE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must be called during application startup.
|
* Must be called during application startup.
|
||||||
@@ -168,24 +169,36 @@ public class PreferencesUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long getLong(int keyId, long defaultValue) {
|
||||||
|
try {
|
||||||
|
return sharedPreferences.getLong(getKey(keyId), defaultValue);
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String stringValue = sharedPreferences.getString(getKey(keyId), null);
|
||||||
|
return Long.parseLong(stringValue);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String getString(int keyId, String defaultValue) {
|
public static String getString(int keyId, String defaultValue) {
|
||||||
return sharedPreferences.getString(getKey(keyId), defaultValue);
|
return sharedPreferences.getString(getKey(keyId), defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
static void setString(int keyId, String value) {
|
||||||
public static void setString(int keyId, String value) {
|
|
||||||
Editor editor = sharedPreferences.edit();
|
Editor editor = sharedPreferences.edit();
|
||||||
editor.putString(getKey(keyId), value);
|
editor.putString(getKey(keyId), value);
|
||||||
editor.apply();
|
editor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
static void setString(int keyId, int valueId) {
|
||||||
public static void setString(int keyId, int valueId) {
|
|
||||||
setString(keyId, resources.getString(valueId));
|
setString(keyId, resources.getString(valueId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
static void setBoolean(int keyId, Boolean value) {
|
||||||
public static void setBoolean(int keyId, Boolean value) {
|
|
||||||
Editor editor = sharedPreferences.edit();
|
Editor editor = sharedPreferences.edit();
|
||||||
editor.putBoolean(getKey(keyId), value);
|
editor.putBoolean(getKey(keyId), value);
|
||||||
editor.apply();
|
editor.apply();
|
||||||
@@ -197,6 +210,12 @@ public class PreferencesUtils {
|
|||||||
editor.apply();
|
editor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void setLong(int keyId, long value) {
|
||||||
|
Editor editor = sharedPreferences.edit();
|
||||||
|
editor.putLong(getKey(keyId), value);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isPublicAPIenabled() {
|
public static boolean isPublicAPIenabled() {
|
||||||
return getBoolean(R.string.publicapi_enabled_key, resources.getBoolean(R.bool.publicapi_enabled_default));
|
return getBoolean(R.string.publicapi_enabled_key, resources.getBoolean(R.bool.publicapi_enabled_default));
|
||||||
}
|
}
|
||||||
@@ -666,6 +685,75 @@ public class PreferencesUtils {
|
|||||||
return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri();
|
return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean shouldExportToHealthConnect() {
|
||||||
|
final boolean DEFAULT = resources.getBoolean(R.bool.health_connect_export_enabled_default);
|
||||||
|
return getBoolean(R.string.health_connect_export_enabled_key, DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean shouldExportDetailedHealthConnectWorkout() {
|
||||||
|
final boolean DEFAULT = resources.getBoolean(R.bool.health_connect_detailed_sync_default);
|
||||||
|
return getBoolean(R.string.health_connect_detailed_sync_key, DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getHealthConnectLastSyncStatus() {
|
||||||
|
return getString(R.string.health_connect_last_sync_status_key, resources.getString(R.string.health_connect_sync_status_never));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setHealthConnectLastSyncStatus(String value) {
|
||||||
|
setString(R.string.health_connect_last_sync_status_key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasHealthConnectInitialSyncConfigured() {
|
||||||
|
return getLong(R.string.health_connect_initial_sync_start_time_key, LONG_UNSET) != LONG_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getHealthConnectInitialSyncStartTimeMillis() {
|
||||||
|
return getLong(R.string.health_connect_initial_sync_start_time_key, LONG_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setHealthConnectInitialSyncStartTimeMillis(long value) {
|
||||||
|
setLong(R.string.health_connect_initial_sync_start_time_key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getHealthConnectInitialSyncCursorStartTimeMillis() {
|
||||||
|
return getLong(R.string.health_connect_initial_sync_cursor_start_time_key, LONG_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setHealthConnectInitialSyncCursorStartTimeMillis(long value) {
|
||||||
|
setLong(R.string.health_connect_initial_sync_cursor_start_time_key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getHealthConnectInitialSyncCursorTrackId() {
|
||||||
|
return getLong(R.string.health_connect_initial_sync_cursor_track_id_key, LONG_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setHealthConnectInitialSyncCursorTrackId(long value) {
|
||||||
|
setLong(R.string.health_connect_initial_sync_cursor_track_id_key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isHealthConnectInitialSyncCompleted() {
|
||||||
|
return getBoolean(R.string.health_connect_initial_sync_completed_key, resources.getBoolean(R.bool.health_connect_initial_sync_completed_default));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setHealthConnectInitialSyncCompleted(boolean value) {
|
||||||
|
setBoolean(R.string.health_connect_initial_sync_completed_key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void resetHealthConnectInitialSyncState(long startTimeMillis) {
|
||||||
|
setHealthConnectInitialSyncStartTimeMillis(startTimeMillis);
|
||||||
|
clearHealthConnectInitialSyncCursor();
|
||||||
|
setHealthConnectInitialSyncCompleted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearHealthConnectInitialSyncCursor() {
|
||||||
|
setHealthConnectInitialSyncCursorStartTimeMillis(LONG_UNSET);
|
||||||
|
setHealthConnectInitialSyncCursorTrackId(LONG_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean shouldRunHealthConnectInitialSync() {
|
||||||
|
return shouldExportToHealthConnect() && hasHealthConnectInitialSyncConfigured() && !isHealthConnectInitialSyncCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
public static TrackFilenameGenerator getTrackFileformatGenerator() {
|
public static TrackFilenameGenerator getTrackFileformatGenerator() {
|
||||||
String DEFAULT = resources.getString(R.string.export_filename_format_default);
|
String DEFAULT = resources.getString(R.string.export_filename_format_default);
|
||||||
TrackFilenameGenerator generator = new TrackFilenameGenerator(getString(R.string.export_filename_format_key, DEFAULT));
|
TrackFilenameGenerator generator = new TrackFilenameGenerator(getString(R.string.export_filename_format_key, DEFAULT));
|
||||||
|
|||||||
@@ -271,6 +271,18 @@
|
|||||||
<string name="import_prevent_reimport_key" translatable="false">preventReimportTrackKey</string>
|
<string name="import_prevent_reimport_key" translatable="false">preventReimportTrackKey</string>
|
||||||
<bool name="import_prevent_reimport_default">true</bool>
|
<bool name="import_prevent_reimport_default">true</bool>
|
||||||
|
|
||||||
|
<string name="health_connect_export_enabled_key" translatable="false">healthConnectExportEnabled</string>
|
||||||
|
<bool name="health_connect_export_enabled_default" translatable="false">false</bool>
|
||||||
|
<string name="health_connect_detailed_sync_key" translatable="false">healthConnectDetailedSync</string>
|
||||||
|
<bool name="health_connect_detailed_sync_default" translatable="false">false</bool>
|
||||||
|
<string name="health_connect_last_sync_status_key" translatable="false">healthConnectLastSyncStatus</string>
|
||||||
|
<string name="health_connect_settings_key" translatable="false">healthConnectSettings</string>
|
||||||
|
<string name="health_connect_initial_sync_start_time_key" translatable="false">healthConnectInitialSyncStartTime</string>
|
||||||
|
<string name="health_connect_initial_sync_cursor_start_time_key" translatable="false">healthConnectInitialSyncCursorStartTime</string>
|
||||||
|
<string name="health_connect_initial_sync_cursor_track_id_key" translatable="false">healthConnectInitialSyncCursorTrackId</string>
|
||||||
|
<string name="health_connect_initial_sync_completed_key" translatable="false">healthConnectInitialSyncCompleted</string>
|
||||||
|
<bool name="health_connect_initial_sync_completed_default" translatable="false">false</bool>
|
||||||
|
|
||||||
<string name="settings_api_key" translatable="false">settingsAPI</string>
|
<string name="settings_api_key" translatable="false">settingsAPI</string>
|
||||||
<string name="publicapi_package_key" translatable="false">apiPackageKey</string>
|
<string name="publicapi_package_key" translatable="false">apiPackageKey</string>
|
||||||
<string name="publicapi_enabled_key" translatable="false">apiEnabledKey</string>
|
<string name="publicapi_enabled_key" translatable="false">apiEnabledKey</string>
|
||||||
|
|||||||
@@ -679,6 +679,53 @@ limitations under the License.
|
|||||||
<string name="hold_to_stop">Hold to stop</string>
|
<string name="hold_to_stop">Hold to stop</string>
|
||||||
<string name="instant_export_enabled_summary">Export track to storage after recording is finished</string>
|
<string name="instant_export_enabled_summary">Export track to storage after recording is finished</string>
|
||||||
<string name="instant_export_enabled_title">Instant post-workout export</string>
|
<string name="instant_export_enabled_title">Instant post-workout export</string>
|
||||||
|
<string name="health_connect_export_enabled_title">Export to Health Connect</string>
|
||||||
|
<string name="health_connect_export_enabled_summary">Export activity to Health Connect after recording is finished</string>
|
||||||
|
<string name="health_connect_not_available">Health Connect is not available on this device</string>
|
||||||
|
<string name="health_connect_screen_title">Health Connect</string>
|
||||||
|
<string name="health_connect_settings_summary">Configure export and synchronization with Health Connect</string>
|
||||||
|
<string name="health_connect_settings_summary_enabled">Health Connect export is enabled</string>
|
||||||
|
<string name="health_connect_settings_summary_enabled_detailed">Health Connect export and detailed GPS route sync are enabled</string>
|
||||||
|
<string name="health_connect_section_general">General settings</string>
|
||||||
|
<string name="health_connect_manage_data_title">Manage Health Connect data</string>
|
||||||
|
<string name="health_connect_open_settings_title">Health Connect settings</string>
|
||||||
|
<string name="health_connect_open_settings_summary">Open Android Health Connect settings</string>
|
||||||
|
<string name="health_connect_disable_note">To fully disconnect Health Connect, remove OpenTracks permissions in the Health Connect app.</string>
|
||||||
|
<string name="health_connect_section_sync">Synchronization</string>
|
||||||
|
<string name="health_connect_sync_status_title">Sync status</string>
|
||||||
|
<string name="health_connect_sync_status_never">No Health Connect export yet.</string>
|
||||||
|
<string name="health_connect_sync_already_running">A Health Connect sync is already running.</string>
|
||||||
|
<string name="health_connect_manual_sync_title">Manual sync</string>
|
||||||
|
<string name="health_connect_manual_sync_summary">Choose a time range and sync existing workouts to Health Connect.</string>
|
||||||
|
<string name="health_connect_manual_sync_requires_export">Enable Health Connect export to manually sync existing workouts.</string>
|
||||||
|
<string name="health_connect_manual_sync_button">Sync now</string>
|
||||||
|
<string name="health_connect_detailed_sync_title">Detailed workout sync</string>
|
||||||
|
<string name="health_connect_detailed_sync_summary">Sync the recorded GPS route with workouts. This provides richer workout data but may take longer.</string>
|
||||||
|
<string name="health_connect_detailed_sync_requires_export">Enable Health Connect export to include detailed route data.</string>
|
||||||
|
<string name="health_connect_sync_note">Depending on the amount of data, a Health Connect export might take a while.</string>
|
||||||
|
<string name="health_connect_sync_status_success">Finished Health Connect export for %1$s%2$s</string>
|
||||||
|
<string name="health_connect_sync_status_failure">Health Connect export failed: %1$s</string>
|
||||||
|
<string name="health_connect_sync_status_route_suffix"> with GPS route</string>
|
||||||
|
<string name="health_connect_initial_sync_dialog_title">Initial sync</string>
|
||||||
|
<string name="health_connect_initial_sync_dialog_message">Choose how much existing workout history OpenTracks should sync to Health Connect.</string>
|
||||||
|
<string name="health_connect_initial_sync_preset">Use a preset time range</string>
|
||||||
|
<string name="health_connect_initial_sync_last_3_days">Last 3 days</string>
|
||||||
|
<string name="health_connect_initial_sync_last_7_days">Last 7 days</string>
|
||||||
|
<string name="health_connect_initial_sync_last_30_days">Last 30 days</string>
|
||||||
|
<string name="health_connect_initial_sync_all">All recorded workouts</string>
|
||||||
|
<string name="health_connect_initial_sync_custom_date">Choose date…</string>
|
||||||
|
<string name="health_connect_initial_sync_select_date">Choose start date</string>
|
||||||
|
<string name="health_connect_sync_now_title">Sync now</string>
|
||||||
|
<string name="health_connect_sync_now_message">Sync existing workouts from %1$s to Health Connect now?</string>
|
||||||
|
<string name="health_connect_sync_now">Sync now</string>
|
||||||
|
<string name="health_connect_sync_later">Sync later</string>
|
||||||
|
<string name="health_connect_sync_status_initial_pending">Initial Health Connect sync is ready for %1$s.</string>
|
||||||
|
<string name="health_connect_sync_status_initial_starting">Starting initial Health Connect sync for %1$s.</string>
|
||||||
|
<string name="health_connect_sync_status_initial_progress">Initial Health Connect sync %1$d of %2$d: %3$s</string>
|
||||||
|
<string name="health_connect_sync_status_initial_success">Finished initial Health Connect sync for %1$d workout(s)</string>
|
||||||
|
<string name="health_connect_sync_status_initial_no_tracks">No recorded workouts found for initial Health Connect sync.</string>
|
||||||
|
<string name="health_connect_sync_status_initial_stopped">Initial Health Connect sync stopped because export was disabled.</string>
|
||||||
|
<string name="health_connect_sync_status_initial_failure">Initial Health Connect sync failed for %1$s: %2$s</string>
|
||||||
<string name="health_connect_permissions_rationale_title">Health Connect access</string>
|
<string name="health_connect_permissions_rationale_title">Health Connect access</string>
|
||||||
<string name="health_connect_permissions_rationale_heading">How OpenTracks uses Health Connect</string>
|
<string name="health_connect_permissions_rationale_heading">How OpenTracks uses Health Connect</string>
|
||||||
<string name="health_connect_permissions_rationale_summary">OpenTracks only writes finished activities to Health Connect when you explicitly enable Health Connect export in settings.</string>
|
<string name="health_connect_permissions_rationale_summary">OpenTracks only writes finished activities to Health Connect when you explicitly enable Health Connect export in settings.</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user