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 -> {
|
||||
storeTrackMetaData(contentProviderUtils, track);
|
||||
ExportUtils.postWorkoutExport(this, trackId);
|
||||
ExportUtils.postWorkoutHealthConnectExport(this, trackId);
|
||||
finish();
|
||||
});
|
||||
|
||||
|
||||
@@ -168,6 +168,29 @@ public class ContentProviderUtils {
|
||||
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) {
|
||||
// Needed, because MARKER_COUNT is a virtual column and has to be explicitly requested.
|
||||
final String[] PROJECTION = new String[]{
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ExistingWorkPolicy;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkInfo;
|
||||
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.io.file.TrackFileFormat;
|
||||
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.SettingsActivity;
|
||||
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) {
|
||||
|
||||
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 Resources resources;
|
||||
private static final long LONG_UNSET = Long.MIN_VALUE;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return sharedPreferences.getString(getKey(keyId), defaultValue);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setString(int keyId, String value) {
|
||||
static void setString(int keyId, String value) {
|
||||
Editor editor = sharedPreferences.edit();
|
||||
editor.putString(getKey(keyId), value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setString(int keyId, int valueId) {
|
||||
static void setString(int keyId, int valueId) {
|
||||
setString(keyId, resources.getString(valueId));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setBoolean(int keyId, Boolean value) {
|
||||
static void setBoolean(int keyId, Boolean value) {
|
||||
Editor editor = sharedPreferences.edit();
|
||||
editor.putBoolean(getKey(keyId), value);
|
||||
editor.apply();
|
||||
@@ -197,6 +210,12 @@ public class PreferencesUtils {
|
||||
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() {
|
||||
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();
|
||||
}
|
||||
|
||||
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() {
|
||||
String DEFAULT = resources.getString(R.string.export_filename_format_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>
|
||||
<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="publicapi_package_key" translatable="false">apiPackageKey</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="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="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_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>
|
||||
|
||||
Reference in New Issue
Block a user