diff --git a/build.gradle b/build.gradle index b6ae340a1..68aad6a69 100644 --- a/build.gradle +++ b/build.gradle @@ -156,6 +156,7 @@ dependencies { implementation 'androidx.mediarouter:mediarouter:1.8.1' implementation 'androidx.core:core-location-altitude:1.0.0' implementation 'androidx.work:work-runtime:2.11.2' + implementation 'androidx.health.connect:connect-client:1.1.0' androidTestImplementation 'androidx.test:core:1.7.0' androidTestImplementation 'androidx.test.ext:junit:1.3.0' diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index c231beac1..3b1339e6b 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -69,6 +69,9 @@ limitations under the License. + + + + + + android:name=".HealthConnectPermissionsRationaleActivity" + android:exported="true"> + + + + + + + + + + + + + + + + + @@ -410,5 +436,6 @@ limitations under the License. + diff --git a/src/main/java/de/dennisguse/opentracks/HealthConnectPermissionsRationaleActivity.java b/src/main/java/de/dennisguse/opentracks/HealthConnectPermissionsRationaleActivity.java new file mode 100644 index 000000000..8d054b30c --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/HealthConnectPermissionsRationaleActivity.java @@ -0,0 +1,28 @@ +package de.dennisguse.opentracks; + +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import de.dennisguse.opentracks.databinding.ActivityHealthConnectPermissionsRationaleBinding; + +public class HealthConnectPermissionsRationaleActivity extends AppCompatActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ActivityHealthConnectPermissionsRationaleBinding viewBinding = ActivityHealthConnectPermissionsRationaleBinding.inflate(getLayoutInflater()); + setSupportActionBar(viewBinding.healthConnectActivityToolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + setContentView(viewBinding.getRoot()); + } + + @Override + public boolean onSupportNavigateUp() { + getOnBackPressedDispatcher().onBackPressed(); + return true; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/TrackStoppedActivity.java b/src/main/java/de/dennisguse/opentracks/TrackStoppedActivity.java index 27f869d0f..4f2ce5a78 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackStoppedActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackStoppedActivity.java @@ -18,6 +18,7 @@ import de.dennisguse.opentracks.data.models.TrackBuilder; import de.dennisguse.opentracks.databinding.TrackStoppedBinding; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.io.file.exporter.ExportUtils; +import de.dennisguse.opentracks.io.healthconnect.exporter.HealthConnectUtils; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment; @@ -93,6 +94,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements viewBinding.finishButton.setOnClickListener(v -> { storeTrackMetaData(contentProviderUtils, track); ExportUtils.postWorkoutExport(this, trackId); + HealthConnectUtils.postExportTrack(this, trackId); finish(); }); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportUtils.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportUtils.java index 10e34bd50..29d0e3a63 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportUtils.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportUtils.java @@ -36,7 +36,7 @@ public class ExportUtils { private static final String TAG = ExportUtils.class.getSimpleName(); public static void postWorkoutExport(AppCompatActivity context, Track.Id trackId) { - if (PreferencesUtils.shouldInstantExportAfterWorkout()) { + if (PreferencesUtils.shouldInstantExportToFile()) { TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(); DocumentFile directory = IntentUtils.toDocumentFile(context, PreferencesUtils.getDefaultExportDirectoryUri()); diff --git a/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectUtils.java b/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectUtils.java new file mode 100644 index 000000000..591a0070c --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectUtils.java @@ -0,0 +1,93 @@ +package de.dennisguse.opentracks.io.healthconnect.exporter; + +import android.content.Context; + +import androidx.health.connect.client.HealthConnectClient; +import androidx.health.connect.client.PermissionController; +import androidx.health.connect.client.permission.HealthPermission; +import androidx.health.connect.client.records.ExerciseSessionRecord; +import androidx.work.Data; +import androidx.work.ExistingWorkPolicy; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; + +import java.util.Set; + +import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.settings.PreferencesUtils; +import kotlin.coroutines.Continuation; +import kotlin.jvm.JvmClassMappingKt; +import kotlin.jvm.functions.Function2; +import kotlinx.coroutines.BuildersKt; + +/** + * Exports a single track as an ExerciseSession to Health Connect. + * Uses the Track.uuid as clientRecordId to prevent duplicates on re-export. + */ +public class HealthConnectUtils { + + private static final String TAG = HealthConnectUtils.class.getSimpleName(); + + static final String WRITE_EXERCISE_PERMISSION = HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ExerciseSessionRecord.class)); + static final String WRITE_EXERCISE_ROUTE_PERMISSION = HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE; + + //TODO Should be package private + public static final Set PERMISSIONS = Set.of(WRITE_EXERCISE_PERMISSION, WRITE_EXERCISE_ROUTE_PERMISSION); + + private HealthConnectUtils() { + } + + public static boolean isAvailable(Context context) { + return HealthConnectClient.getSdkStatus(context) == HealthConnectClient.SDK_AVAILABLE; + } + + public static Set getGrantedPermissions(Context context) { + try { + HealthConnectClient client = HealthConnectClient.getOrCreate(context); + PermissionController controller = client.getPermissionController(); + + return (Set) BuildersKt.runBlocking( + kotlin.coroutines.EmptyCoroutineContext.INSTANCE, + (Function2) (scope, cont) -> controller.getGrantedPermissions((Continuation) cont)); + } catch (InterruptedException e) { + return Set.of(); + } + } + + static boolean hasRequiredPermissions(Context context) { + return getGrantedPermissions(context) + .containsAll(PERMISSIONS); + } + + public static void postExportTrack(Context context, Track.Id trackId) { + if (!PreferencesUtils.shouldInstantExportToHealthConnect()) { + return; + } + + WorkRequest request = new OneTimeWorkRequest.Builder(HealthConnectWorker.class) + .addTag(HealthConnectWorker.WORK_TAG) + .setInputData(new Data.Builder() + .putLong(HealthConnectWorker.TRACK_ID_KEY, trackId.id()) + .build()) + .build(); + WorkManager.getInstance(context).enqueue(request); + } + + public static void postExportAll(Context context) { + OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(HealthConnectWorker.class) + .addTag(HealthConnectWorker.WORK_TAG) + .build(); + + WorkManager.getInstance(context).enqueueUniqueWork( + HealthConnectWorker.WORK_TAG, + ExistingWorkPolicy.REPLACE, + request + ); + } + + public static void cancelHealthConnectExport(Context context) { + WorkManager workManager = WorkManager.getInstance(context); + workManager.cancelAllWorkByTag(HealthConnectWorker.WORK_TAG); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectWorker.java b/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectWorker.java new file mode 100644 index 000000000..65832d4f1 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectWorker.java @@ -0,0 +1,185 @@ +package de.dennisguse.opentracks.io.healthconnect.exporter; + +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.health.connect.client.HealthConnectClient; +import androidx.health.connect.client.records.ExerciseRoute; +import androidx.health.connect.client.records.ExerciseSessionRecord; +import androidx.health.connect.client.records.metadata.Device; +import androidx.health.connect.client.records.metadata.Metadata; +import androidx.health.connect.client.units.Length; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import de.dennisguse.opentracks.data.ContentProviderUtils; +import de.dennisguse.opentracks.data.TrackPointIterator; +import de.dennisguse.opentracks.data.models.ActivityType; +import de.dennisguse.opentracks.data.models.Position; +import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.data.models.TrackPoint; +import kotlin.coroutines.Continuation; +import kotlin.jvm.functions.Function2; +import kotlinx.coroutines.BuildersKt; + +/** + * Exports a single track as an ExerciseSession to Health Connect. + * Uses the Track.uuid as clientRecordId to prevent duplicates on re-export. + */ +//TODO add reasons for failures (if shown in the UI: must be localized) +//TODO Export sensor data (permissions needed) +//TODO Show progress: like ExportActivity? Or use a notification (aka handle it as a background process) +public class HealthConnectWorker extends Worker { + + private static final String TAG = HealthConnectWorker.class.getSimpleName(); + static final String WORK_TAG = "HealthConnectSync"; + + static final String TRACK_ID_KEY = "trackId"; + + public HealthConnectWorker(Context context, WorkerParameters params) { + super(context, params); + } + + @NonNull + @Override + public Result doWork() { + if (!HealthConnectUtils.isAvailable(getApplicationContext())) { + Log.w(TAG, "Health Connect SDK not available"); + return Result.failure(); + } + + //TODO If not all permissions are granted, export the data that was granted. + if (!HealthConnectUtils.hasRequiredPermissions(getApplicationContext())) { + Log.w(TAG, "Health Connect required permissions not granted"); + return Result.failure(); + } + + Track.Id trackId = new Track.Id(getInputData().getLong(TRACK_ID_KEY, -1L)); + if (trackId.id() != -1L) { + return exportTrack(trackId); + } + return exportAll(); + } + + private Result exportTrack(Track.Id trackId) { + try { + export(getApplicationContext(), trackId); + 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); + return Result.failure(); + } + } + + private Result exportAll() { + List allTracks = new ContentProviderUtils(getApplicationContext()) + .getTracks(); + + for (Track track : allTracks) { + if (isStopped()) { + Log.i(TAG, "Initial Health Connect sync stopped by WorkManager"); + return Result.success(); + } + + try { + export(getApplicationContext(), track.id()); + } catch (InterruptedException e) { + return Result.retry(); + } catch (Exception e) { + Log.e(TAG, "Initial Health Connect sync failed for track " + track.id().id(), e); + return Result.failure(); + } + } + + return Result.success(); + } + + + private static ExportResult export(Context context, Track.Id trackId) throws InterruptedException { + ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); + Track track = contentProviderUtils.getTrack(trackId); + if (track == null) { + throw new RuntimeException("Track not found: " + trackId); + } + + ExerciseRoute exerciseRoute = createExerciseRoute(contentProviderUtils, trackId); + + HealthConnectClient client = HealthConnectClient.getOrCreate(context); + + ExerciseSessionRecord record = new ExerciseSessionRecord( + track.startTime().toInstant(), + track.startTime().getOffset(), + track.stopTime().toInstant(), + track.stopTime().getOffset(), + Metadata.activelyRecorded( + new Device(Device.TYPE_PHONE, Build.MANUFACTURER, Build.MODEL), + track.uuid().toString() + ), + toExerciseType(track.activityType()), + track.name(), + track.description(), + Collections.emptyList(), + Collections.emptyList(), + exerciseRoute); + + List records = Collections.singletonList(record); + Object ignored = BuildersKt.runBlocking( + kotlin.coroutines.EmptyCoroutineContext.INSTANCE, + (Function2) (scope, cont) -> client.insertRecords(records, (Continuation) cont)); + + Log.i(TAG, "Exported track to Health Connect: " + track.name()); + return new ExportResult(track.name()); + } + + private static ExerciseRoute createExerciseRoute(ContentProviderUtils contentProviderUtils, Track.Id trackId) { + List route = new ArrayList<>(); + + try (TrackPointIterator iterator = contentProviderUtils.getTrackPointIterator(trackId, null)) { + while (iterator.hasNext()) { + TrackPoint trackPoint = iterator.next(); + if (trackPoint.type() != TrackPoint.Type.TRACKPOINT) continue; + + Position position = trackPoint.position(); + + route.add(new ExerciseRoute.Location( + trackPoint.getTime(), + position.latitude(), + position.longitude(), + position.horizontalAccuracy() != null ? Length.meters(position.horizontalAccuracy().toM()) : null, + position.verticalAccuracy() != null ? Length.meters(position.verticalAccuracy().toM()) : null, + position.altitude() != null ? Length.meters(position.altitude().toM()) : null) + ); + } + } + + return new ExerciseRoute(route); + } + + private static int toExerciseType(ActivityType activityType) { + return switch (activityType) { + case RUNNING, STREET_RUNNING, TRAIL_RUNNING, TRACK_RUNNING -> ExerciseSessionRecord.EXERCISE_TYPE_RUNNING; + case BIKING, CYCLING, ROAD_BIKING, MOUNTAIN_BIKING, TRACK_CYCLING -> ExerciseSessionRecord.EXERCISE_TYPE_BIKING; + case WALKING, SPEED_WALKING -> ExerciseSessionRecord.EXERCISE_TYPE_WALKING; + case HIKING, OFF_TRAIL_HIKING, TRAIL_HIKING -> ExerciseSessionRecord.EXERCISE_TYPE_HIKING; + case CROSS_COUNTRY_SKIING, SKIING -> ExerciseSessionRecord.EXERCISE_TYPE_SKIING; + case SNOW_BOARDING -> ExerciseSessionRecord.EXERCISE_TYPE_SNOWBOARDING; + case KAYAKING -> ExerciseSessionRecord.EXERCISE_TYPE_PADDLING; + case ROWING -> ExerciseSessionRecord.EXERCISE_TYPE_ROWING_MACHINE; + case SWIMMING -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL; + case SWIMMING_OPEN -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_OPEN_WATER; + default -> ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT; + }; + } + + public record ExportResult(String trackName) { + } +} diff --git a/src/main/java/de/dennisguse/opentracks/settings/HealthConnect.java b/src/main/java/de/dennisguse/opentracks/settings/HealthConnect.java new file mode 100644 index 000000000..23b063c56 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/settings/HealthConnect.java @@ -0,0 +1,54 @@ +package de.dennisguse.opentracks.settings; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.health.connect.client.HealthConnectClient; +import androidx.health.connect.client.PermissionController; + +import de.dennisguse.opentracks.io.healthconnect.exporter.HealthConnectUtils; + +public class HealthConnect { + + public static class Settings extends AppCompatActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + startActivity(new Intent(HealthConnectClient.getHealthConnectSettingsAction())); + + finish(); + } + } + + public static class ConfigurePermissions extends AppCompatActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (!HealthConnectUtils.isAvailable(this)) { + finish(); + } + + registerForActivityResult( + PermissionController.createRequestPermissionResultContract(), + grantedPermissions -> { + // TODO User feedback via Toast.makeText()? + finish(); + } + ).launch(HealthConnectUtils.PERMISSIONS); + } + } + + public static class ExportAll extends AppCompatActivity { + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + HealthConnectUtils.postExportAll(this); + + finish(); + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/settings/ImportExportSettingsFragment.java b/src/main/java/de/dennisguse/opentracks/settings/ImportExportSettingsFragment.java index d67b428f0..9b97b0286 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/ImportExportSettingsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/settings/ImportExportSettingsFragment.java @@ -14,11 +14,11 @@ import java.util.Map; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFilenameGenerator; +import de.dennisguse.opentracks.io.healthconnect.exporter.HealthConnectUtils; import de.dennisguse.opentracks.util.IntentUtils; -public class ImportExportSettingsFragment extends PreferenceFragmentCompat { - private static final String TAG = ImportExportSettingsFragment.class.getSimpleName(); +public class ImportExportSettingsFragment extends PreferenceFragmentCompat { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -27,6 +27,11 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat { setExportTrackFileFormatOptions(); setExportDirectorySummary(); setFilenameTemplate(); + + findPreference("healthConnectPermissions").setSummaryProvider(preference -> { + //TODO This should be localized and prefixes trimmed. + return String.join(",", HealthConnectUtils.getGrantedPermissions(getActivity())); + }); } @Override @@ -40,9 +45,21 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat { super.onResume(); setExportDirectorySummary(); + findPreference(getString(R.string.post_workout_export_enabled_key)).setEnabled(PreferencesUtils.isDefaultExportDirectoryUri()); - Preference instantExportEnabledPreference = findPreference(getString(R.string.post_workout_export_enabled_key)); - instantExportEnabledPreference.setEnabled(PreferencesUtils.isDefaultExportDirectoryUri()); + setHealthConnectPreference(); + } + + private void setHealthConnectPreference() { + boolean healthConnectAvailable = HealthConnectUtils.isAvailable(requireContext()); + findPreference("healthConnectCategory") + .setEnabled(healthConnectAvailable); + + if (!healthConnectAvailable) { + findPreference(getString(R.string.post_workout_export_health_connect_enabled_key)) + .setSummary(R.string.health_connect_not_available); + return; + } } private void setExportTrackFileFormatOptions() { diff --git a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java index b47056472..327e0ff26 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java @@ -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)); } @@ -660,12 +679,16 @@ public class PreferencesUtils { return entries; } - - public static boolean shouldInstantExportAfterWorkout() { + public static boolean shouldInstantExportToFile() { final boolean INSTANT_POST_WORKOUT_EXPORT_DEFAULT = resources.getBoolean(R.bool.post_workout_export_enabled_default); return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri(); } + public static boolean shouldInstantExportToHealthConnect() { + final boolean DEFAULT = resources.getBoolean(R.bool.post_workout_export_health_connect_enabled_default); + return getBoolean(R.string.post_workout_export_health_connect_enabled_key, DEFAULT); + } + 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)); diff --git a/src/main/res/layout/activity_health_connect_permissions_rationale.xml b/src/main/res/layout/activity_health_connect_permissions_rationale.xml new file mode 100644 index 000000000..101d913e3 --- /dev/null +++ b/src/main/res/layout/activity_health_connect_permissions_rationale.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index e5a3599f8..f16842570 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -456,7 +456,7 @@ limitations under the License. التسجيل واجهة برمجة التطبيقات العامة واجهة برمجة التطبيقات العامة لـ المسارات المفتوحة معطلة: يمكن تفعيلها في الإعدادات. - التصدير + التصدير تصدير المسار إلى التخزين بعد انتهاء التسجيل غير قادر على إنشاء علامة. حاول مرة أخرى. متوسط معدل ضربات القلب diff --git a/src/main/res/values-b+es+419/strings.xml b/src/main/res/values-b+es+419/strings.xml index b77f43620..fe440e34b 100644 --- a/src/main/res/values-b+es+419/strings.xml +++ b/src/main/res/values-b+es+419/strings.xml @@ -430,7 +430,7 @@ Intervalo de tiempo, Intervalo de distancia, Distancia máxima, Precisión Sensores Sensores de ciclismo, Circunferencia de la rueda, Sensores de carrera - Exportar + Exportar Natación Natación en aguas abiertas Entrenamiento diff --git a/src/main/res/values-co/strings.xml b/src/main/res/values-co/strings.xml index 81c235b0c..bef5919a9 100644 --- a/src/main/res/values-co/strings.xml +++ b/src/main/res/values-co/strings.xml @@ -456,7 +456,7 @@ Imperiale (mi, m) Metrica (km, m) Impurtà - Espurtà + Espurtà Nome di u pacchettu per l’API publica Solu OpenTracks pò lancià è piantà l’arregistramenti. D’altre appiecazioni installate ponu lancià o piantà l’arregistramenti. diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 066465f7f..43af0ba2d 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -444,7 +444,7 @@ limitations under the License. Cyklistika Běh Importovat - Exportovat + Exportovat Napájení V nastavení systému je třeba ručně povolit aplikaci OpenTracks přístup k blízkým zařízením. %1$.1f mi/h diff --git a/src/main/res/values-da/strings.xml b/src/main/res/values-da/strings.xml index 4c596b843..4f940acf5 100644 --- a/src/main/res/values-da/strings.xml +++ b/src/main/res/values-da/strings.xml @@ -471,7 +471,7 @@ limitations under the License. Vælg layout Kører Importere - Eksport + Eksport Ur %1$.1f mi/h (anbefalet) %1$.1f mi/h diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index fdd4b14d6..209671998 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -511,7 +511,7 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka Zeige nur Bluetooth-Geräte, die benötigte Dienste bekanntmachen Andere installierte Anwendungen können Aufzeichnungen starten oder stoppen. OpenTracks öffentliche API ist deaktiviert: es kann in den Einstellungen aktiviert werden. - Export + Export Nur OpenTracks kann Aufzeichnungen starten und stoppen. Import Öffentliche API diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 2583012cc..e841f634b 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -503,7 +503,7 @@ limitations under the License. Es necesario conceder manualmente a OpenTracks el acceso a los Dispositivos Cercanos en la configuración del sistema. Correr Importar - Exportar + Exportar Hoy Ayer Nuevo diseño diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml index b1738ca00..f19756daa 100644 --- a/src/main/res/values-et/strings.xml +++ b/src/main/res/values-et/strings.xml @@ -442,7 +442,7 @@ limitations under the License. Lähtesta seadistused vaikeväärtusteks Kohanda oma salvestuse paigutust Andmeväljad rea kohta - Eksport + Eksport Ajavahemik Lähtesta kohandatud paigutus Kas lähtestad oma paigutused? diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index 51ac47b5e..e48484bf1 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -458,7 +458,7 @@ GPS gailuak datu okerrak salatzen baditu (adibidez, kokapena, abiadura, kota), O Diseinuaren ezarpen guztiak balio lehenetsietara itzuli dira Berrezarri Inportatu - Esportatu + Esportatu Latitudea /Longitudea Erlojua %1$.1f mi/h diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 1c34e3582..53dcff658 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -490,7 +490,7 @@ limitations under the License. Teho Yhteensopiva tiedostojen hallinta puuttuu Tiedostonimen muoto - Vienti + Vienti Muokkaa tallennuksen asettelua Näytä vain Bluetooth-laitteet, jotka ilmoittavat tarvittavat palvelut Tallennettuja tietoja ei jaeta automaattisesti muiden sovellusten kanssa. diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index a95234885..dbaeed102 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -505,7 +505,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio Cyclisme Course à pied Importer - Exporter + Exporter Réinitialiser les paramètres aux valeurs par défaut Valeurs par défaut de l\'activité Capteurs de cyclisme, Circonférence de roue, Capteurs de course à pied diff --git a/src/main/res/values-ga/strings.xml b/src/main/res/values-ga/strings.xml index 83517bc18..0008be978 100644 --- a/src/main/res/values-ga/strings.xml +++ b/src/main/res/values-ga/strings.xml @@ -257,7 +257,7 @@ Ní féidir ach le OpenTracks taifeadtaí a thosú agus a stopadh. Ní dhéanfar sonraí taifeadta a roinnt go huathoibríoch le feidhmchláir eile. Iompórtáil - Easpórtáil + Easpórtáil Sílim go mb\'fhéidir go mbeadh suim agat sa rian seo. \n \n%1$s diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index ae8d951fe..ab5e7e2a3 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -443,7 +443,7 @@ limitations under the License. Ciclismo Correr Importar - Exportar + Exportar Lonxitude da volta OpenTracks require permiso para usar o Bluetooth. Tes que permitirlle a OpenTracks nos axustes do sistema que acceda a Dispositivos Próximos. diff --git a/src/main/res/values-hr/strings.xml b/src/main/res/values-hr/strings.xml index 467c26d74..f30c20149 100644 --- a/src/main/res/values-hr/strings.xml +++ b/src/main/res/values-hr/strings.xml @@ -305,7 +305,7 @@ limitations under the License. fontana točka na ruti Uvoz - Izvoz + Izvoz Spriječi ponovni uvoz ruta Izvezi sve (jedna datoteka za sve rute) Odaberi raspored diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index b097ab5d3..ba157c5c5 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -467,7 +467,7 @@ limitations under the License. Átlagos pulzusszám Csak azokat a Bluetooth-eszközöket jelenítse meg, amelyek szükséges szolgáltatásokat kérnek Importálás - Exportálás + Exportálás Az elrendezések visszaállítása? Az összes elrendezési beállítás visszaállt az alapértelmezett értékekre Az elrendezést eltávolították diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index fb03bae6d..bf87abb25 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -448,7 +448,7 @@ Infatti, un\'applicazione deve supportare ACTION_VIEW con MIME applica Ciclismo Corsa Importa - Esporta + Esporta Latitudine/Longitudine Rimuovi Primario diff --git a/src/main/res/values-kn/strings.xml b/src/main/res/values-kn/strings.xml index 5354abb0f..39d6553f2 100644 --- a/src/main/res/values-kn/strings.xml +++ b/src/main/res/values-kn/strings.xml @@ -333,7 +333,7 @@ ರೆಕಾರ್ಡಿಂಗ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿದ ಅಪ್ಲಿಕೇಶನ್ ರೆಕಾರ್ಡ್ ಮಾಡಿದ ಡೇಟಾವನ್ನು ಸಹ ಪ್ರವೇಶಿಸಬಹುದು. ರೆಕಾರ್ಡ್ ಮಾಡಲಾದ ಡೇಟಾವನ್ನು ಇತರ ಅಪ್ಲಿಕೇಶನ್‌ಗಳೊಂದಿಗೆ ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಹಂಚಿಕೊಳ್ಳಲಾಗುವುದಿಲ್ಲ. ಆಮದು - ರಫ್ತು + ರಫ್ತು ಟ್ರ್ಯಾಕ್‌ಗಳ ಮರು-ಆಮದು ತಡೆಯಿರಿ ನೀವು ಈ ಟ್ರ್ಯಾಕ್‌ನಲ್ಲಿ ಆಸಕ್ತಿ ಹೊಂದಿರಬಹುದು ಎಂದು ನಾನು ಭಾವಿಸುತ್ತೇನೆ. \n diff --git a/src/main/res/values-nb/strings.xml b/src/main/res/values-nb/strings.xml index f00d477bc..42d8c3512 100644 --- a/src/main/res/values-nb/strings.xml +++ b/src/main/res/values-nb/strings.xml @@ -437,7 +437,7 @@ limitations under the License. Sykling Løping Import - Eksport + Eksport %1$.1f km/t %1$.1f km/t (anbefales) %1$.1f mi/h diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 1fc9fe4d4..da6a11bf0 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -465,7 +465,7 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid Fietsen Hardlopen Invoer - Uitvoer + Uitvoer Generiek " (geen schrijfrecht!)" %1$.1f km/u (aanbevolen) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 5e4eb4c49..f0465d28c 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -450,7 +450,7 @@ limitations under the License. Kolarstwo Bieganie Import - Eksport + Eksport Domyślny GPS OpenTracks wymaga uprawnień do urzycia Bluetooth. diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 5498a7e9d..aeb27c819 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -410,7 +410,7 @@ Ciclismo Correndo Importar - Exportar + Exportar %1$.1f km/h %1$.1f km/h (Recomendado) Relógio diff --git a/src/main/res/values-pt-rPT/strings.xml b/src/main/res/values-pt-rPT/strings.xml index b2f2249e6..cf20937e1 100644 --- a/src/main/res/values-pt-rPT/strings.xml +++ b/src/main/res/values-pt-rPT/strings.xml @@ -451,7 +451,7 @@ Uma aplicação que iniciou uma gravação também pode acessar os dados gravados. Os dados gravados não serão compartilhados automaticamente com outras aplicações. Importar - Exportar + Exportar Impedir a reimportação de trilhas Perda Latitude/Longitude diff --git a/src/main/res/values-pt/strings.xml b/src/main/res/values-pt/strings.xml index 8173251f4..58f3f4860 100644 --- a/src/main/res/values-pt/strings.xml +++ b/src/main/res/values-pt/strings.xml @@ -466,7 +466,7 @@ limitations under the License. Apenas o OpenTracks pode iniciar e parar gravações. Os dados gravados não serão compartilhados automaticamente com outras aplicações. Importar - Exportar + Exportar Impedir a reimportação de trilhas Perda Latitude/Longitude diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 4e880a866..d94017a84 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -498,7 +498,7 @@ limitations under the License. Текущий пульс Фильтр с - Экспорт + Экспорт Импорт Публичный API Публичный API OpenTracks отключен: он может быть включен в настройках. diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml index c352df742..28e8a0670 100644 --- a/src/main/res/values-sk/strings.xml +++ b/src/main/res/values-sk/strings.xml @@ -381,7 +381,7 @@ limitations under the License. Spúšťanie Cyklistika Rozloženie bolo odstránené - Exportujte + Exportujte Importovať Presnosť polohy: %1$s Hodiny diff --git a/src/main/res/values-sl/strings.xml b/src/main/res/values-sl/strings.xml index 18645527a..97266aaca 100644 --- a/src/main/res/values-sl/strings.xml +++ b/src/main/res/values-sl/strings.xml @@ -352,7 +352,7 @@ limitations under the License. Napaka analizatorja: %1$s Kadenca Izberite postavitev - Izvoz + Izvoz Kadenca Srčni utrip Uporabniški vmesnik diff --git a/src/main/res/values-ta/strings.xml b/src/main/res/values-ta/strings.xml index 958e68299..b1e006e1b 100644 --- a/src/main/res/values-ta/strings.xml +++ b/src/main/res/values-ta/strings.xml @@ -338,7 +338,7 @@ பதிவைத் தொடங்கிய பயன்பாடு பதிவு செய்யப்பட்ட தரவையும் அணுகலாம். பதிவுசெய்யப்பட்ட தரவு மற்ற பயன்பாடுகளுடன் தானாக பகிரப்படாது. இறக்குமதி - ஏற்றுமதி + ஏற்றுமதி இந்த பாதையில் நீங்கள் ஆர்வமாக இருக்கலாம் என்று நினைக்கிறேன்.\n\n %1$s தடங்களை மீண்டும் இறக்குமதி செய்வதைத் தடுக்கவும் நான் உங்களுடன் ஒரு பாதையைப் பகிர்ந்து கொள்ள விரும்புகிறேன் diff --git a/src/main/res/values-tr/strings.xml b/src/main/res/values-tr/strings.xml index c38d5ce1b..f14973740 100644 --- a/src/main/res/values-tr/strings.xml +++ b/src/main/res/values-tr/strings.xml @@ -438,7 +438,7 @@ limitations under the License. Kaydedilen veriler diğer uygulamalarla otomatik olarak paylaşılmayacaktır. İçe Aktar - Dışa Aktar + Dışa Aktar Parkurların yeniden içe aktarılmasını engelle Bu parkurun ilginizi çekebileceğini düşündüm.\n\n%1$s diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index c948ac703..1ccf718c5 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -425,7 +425,7 @@ limitations under the License. Видалити Годинник Поля даних у рядку - Експорт + Експорт Вторинний Скидання Фільтр diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index f3f81718a..861fbc09a 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -455,7 +455,7 @@ limitations under the License. Đạp xe Chạy Nhập - Xuất + Xuất Chung chung %1$.1f mi/h %1$.1f mi/h (khuyến khích) diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 0dc56dfcd..a1cf63177 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -393,7 +393,7 @@ limitations under the License. 記錄的資料將不會自動與其他應用程式共用。 匯入 - 匯出 + 匯出 防止重新匯入蹤跡 我想您應該會對這個路線感興趣。 diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml index 0de8196df..ca59f48ec 100644 --- a/src/main/res/values-zh/strings.xml +++ b/src/main/res/values-zh/strings.xml @@ -539,7 +539,7 @@ limitations under the License. OpenTracks 公共 API 已禁用:可以在设置中启用。 自动数据传输 记录的数据不会自动与其他应用程序共享。 - 导出 + 导出 下降 纬度/经度 配速:%1$s diff --git a/src/main/res/values/settings.xml b/src/main/res/values/settings.xml index 900491b77..5d9c2038e 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -58,6 +58,9 @@ instantExportFilename {date}_{time}_{name} + instantExportHealthConnectEnabled + false + settingsLayoutReset diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 4cd054a72..e29c54efe 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -280,6 +280,7 @@ limitations under the License. Edit Export all (one file per track) Export all (one file for all tracks) + Export all Help Import all Insert marker @@ -448,7 +449,8 @@ limitations under the License. Recorded data will not be shared automatically with other applications. Import - Export + Export as file + Export to Health Connect Prevent re-import of tracks I think you might be interested in this track.\n\n%1$s @@ -679,6 +681,13 @@ limitations under the License. Hold to stop Export track to storage after recording is finished Instant post-workout export + Export activity to Health Connect after recording is finished + Health Connect is not available on this device + Health Connect settings + Open the Health Connect settings + Health Connect permissions + How OpenTracks uses Health Connect + If enabled in the settings, OpenTracks writes exercise to Health Connect. OpenTracks does not read data from Health Connect. Track export directory Primary diff --git a/src/main/res/xml/settings_import_export.xml b/src/main/res/xml/settings_import_export.xml index a9c5dc180..136b72684 100644 --- a/src/main/res/xml/settings_import_export.xml +++ b/src/main/res/xml/settings_import_export.xml @@ -17,7 +17,7 @@ android:title="@string/settings_prevent_reimport_tracks_title" /> - + - \ No newline at end of file + + + + + + + + + + + + + + + + +