Compare commits

...

48 Commits

Author SHA1 Message Date
Dennis Guse 9b2126550e Release: v4.10.0 2023-12-30 12:59:40 +01:00
Stephan Paternotte 0e9f9b4db7 Translated using Weblate (Dutch)
Currently translated at 100.0% (517 of 517 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/nl/
2023-12-30 11:24:24 +01:00
josé m 4dc524bf4f Translated using Weblate (Galician)
Currently translated at 100.0% (517 of 517 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/gl/
2023-12-30 11:24:24 +01:00
Fjuro 7455e591e1 Translated using Weblate (Czech)
Currently translated at 100.0% (517 of 517 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/cs/
2023-12-30 11:24:24 +01:00
Dennis Guse 9e4b1e0706 Merge pull request #1767 from OpenTracksApp/barometer2#1424
Support for BLE Environmental Sensing (Pressure/Barometer).
2023-12-30 11:24:20 +01:00
Dennis Guse 03dd0c2308 Implement gain/loss via Bluetooth LE Environmental Sensing Sensor.
Fixes #1424.
2023-12-30 11:23:28 +01:00
Dennis Guse 8eed8e9d8d Bugfix: Bluetooth sensor data TLS is 5s instead of 50s.
Introduced in 58d79380cf
2023-12-29 13:17:06 +01:00
Dennis Guse 94540606ef Barometer: select INTERNAL or NONE (also while recording).
Part of #1424.
2023-12-29 13:17:06 +01:00
Dennis Guse 490141db2a Barometer: added settings for sensor selection.
Part of #1424.
2023-12-29 13:17:06 +01:00
Dennis Guse 892c56389b SplashScreen supports day and night mode. 2023-12-29 13:09:52 +01:00
Dennis Guse a141bd77ad Bugfix: intervals row needs to use complete row.
Introduced in be0a7e2fac
2023-12-29 13:09:52 +01:00
Dennis Guse e48b6054df Cleanup: Theme is called OpenTracksTheme. 2023-12-29 13:09:52 +01:00
Dennis Guse bb23bac7ec Use Material3 switch for settings.
Part of #1586.
2023-12-28 20:24:32 +01:00
Stephan Paternotte fa362de67a Translated using Weblate (Dutch)
Currently translated at 100.0% (129 of 129 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-12-27 11:33:20 +01:00
Fjuro 76cba3f611 Translated using Weblate (Czech)
Currently translated at 100.0% (129 of 129 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2023-12-27 11:33:20 +01:00
Stephan Paternotte 7ccb59beae Translated using Weblate (Dutch)
Currently translated at 100.0% (128 of 128 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-12-27 11:33:20 +01:00
Hikaru a70118b2dc Translated using Weblate (Japanese)
Currently translated at 50.0% (258 of 516 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://hosted.weblate.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/ja/
2023-12-27 11:33:20 +01:00
Software In Interlingua 738cbe150f Added translation using Weblate (Interlingua) 2023-12-27 11:33:20 +01:00
Fjuro 7f40f67dc4 Translated using Weblate (Czech)
Currently translated at 100.0% (128 of 128 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2023-12-27 11:33:20 +01:00
Fjuro f1b7adb6a0 Translated using Weblate (Czech)
Currently translated at 100.0% (127 of 127 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2023-12-27 11:33:20 +01:00
Dennis Guse 1bd75754e4 Bluetooth sensor settings: show sensor name in addition (if cached via Android OS).
Fixes #508.
2023-12-27 11:32:56 +01:00
Dennis Guse eaad33c23b Merge pull request #1797 from OpenTracksApp/refactoring#1708
Refactoring TrackRecordingServiceConnection
2023-12-27 11:06:35 +01:00
Dennis Guse 9a0f927182 Cleanup: TrackRecordingServiceConnection has now an execute method (avoid instantiation). 2023-12-27 11:05:56 +01:00
Dennis Guse 59aeec4fbc Cleanup: TrackRecordingServiceConnection. 2023-12-27 11:05:56 +01:00
Dennis Guse df8306c2f8 Cleanup: TrackEditActivity doesn't need a TrackRecordingServiceConnection. 2023-12-27 11:05:56 +01:00
Dennis Guse 5c6a4a71a4 TrackRecordingService: always starts as foreground service. 2023-12-27 11:05:56 +01:00
Dennis Guse abbc61d419 TrackRecordingService: start via ServiceCompat. 2023-12-27 11:05:56 +01:00
Dennis Guse e2f598b282 Cleanup: simplify TrackServiceConnection. 2023-12-27 11:05:56 +01:00
Dennis Guse 41da2f5b53 Cleanup: simplify TrackServiceConnection. 2023-12-27 11:05:56 +01:00
Dennis Guse 8d7a8719bc Release: v4.9.9 2023-12-27 10:32:39 +01:00
Dennis Guse 8f5388edce While recording: show BLE sensor name instead of address.
Fixes #1796.
2023-12-26 11:22:05 +01:00
Dennis Guse 81a78820cc Merge pull request #1793 from OpenTracksApp/crash#1708
Bugfix: sensors were started twice.
2023-12-26 09:04:29 +01:00
Dennis Guse 2fe452c77c Revert "fix NPE at null barometer after unregister listener"
This reverts commit 5dbd17f3bb.
2023-12-26 08:42:38 +01:00
Dennis Guse 826cf0d468 Bugfix: TrackListActivity couldn't stop and restart sensors.
Part of #1780.
2023-12-26 08:42:38 +01:00
Dennis Guse 6cd627e832 Bugfix: TrackRecordingService can only start sensors once.
Fixes #1780.
2023-12-26 08:42:37 +01:00
Dennis Guse 0ee4a4e72f Bugfix: TrackListActivity triggered TrackRecordingService twice (startSensors and startRecording).
Part of #1780.
2023-12-25 20:49:49 +01:00
Dennis Guse 150cbee95b BarometerInternal: if there is no internal barometer - don't register a listener. 2023-12-25 18:48:14 +01:00
Dennis Guse 47ceb297c9 SensorManager: crash if started twice.
Part of #1780.
2023-12-24 22:10:58 +01:00
Dennis Guse aed4af5736 Bluetooth logs: make them slightly more readable. 2023-12-24 21:49:48 +01:00
Dennis Guse 333f7956b5 Release: v4.9.8 2023-12-17 22:38:37 +01:00
Dennis Guse d08552a07b Bugfix: cycling cadence data should be handled by BluetoothHandlerCyclingCadence.
Fixes #1787.

Introduced in 15f3ccbcbd
2023-12-17 22:37:32 +01:00
dependabot[bot] 330ca4a720 Bump actions/upload-artifact from 3 to 4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-15 07:19:37 +01:00
dependabot[bot] 85171cdc46 Bump com.google.android.material:material from 1.10.0 to 1.11.0
Bumps [com.google.android.material:material](https://github.com/material-components/material-components-android) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/material-components/material-components-android/releases)
- [Commits](https://github.com/material-components/material-components-android/compare/1.10.0...1.11.0)

---
updated-dependencies:
- dependency-name: com.google.android.material:material
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-14 23:11:24 +01:00
Dennis Guse 5c1cb97c02 Release: v4.9.7 2023-12-12 18:46:17 +01:00
Stephan Paternotte 98a009e7c7 Translated using Weblate (Dutch)
Currently translated at 100.0% (126 of 126 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/nl/
2023-12-09 10:04:59 +01:00
Fjuro 8f15382409 Translated using Weblate (Czech)
Currently translated at 100.0% (126 of 126 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://hosted.weblate.org/projects/opentracks/app-store-metadata-files/cs/
2023-12-09 10:04:59 +01:00
Dennis Guse 24212a226e Bugfix: prevent crash when ending a recording (barometer present).
Fixes #1766.
2023-12-09 10:04:50 +01:00
dependabot[bot] f80aaf00ec Bump reactivecircus/android-emulator-runner from 2.28.0 to 2.29.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.28.0 to 2.29.0.
- [Release notes](https://github.com/reactivecircus/android-emulator-runner/releases)
- [Changelog](https://github.com/ReactiveCircus/android-emulator-runner/blob/main/CHANGELOG.md)
- [Commits](https://github.com/reactivecircus/android-emulator-runner/compare/v2.28.0...v2.29.0)

---
updated-dependencies:
- dependency-name: reactivecircus/android-emulator-runner
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-07 20:36:04 +01:00
71 changed files with 656 additions and 456 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- name: Archive
if: always()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: OpenTracks-APK.apk
path: ./build/outputs/apk/nightly/*.apk
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
java-version: '17'
- name: run tests
uses: reactivecircus/android-emulator-runner@v2.28.0
uses: reactivecircus/android-emulator-runner@v2.29.0
with:
arch: x86_64
api-level: 34
@@ -26,7 +26,7 @@ jobs:
- name: Archive
if: always()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: code-coverage-report
path: build/reports/androidTests/
+4 -3
View File
@@ -64,8 +64,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 5554
versionName "v4.9.6"
versionCode 5602
versionName "v4.10.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -131,8 +131,9 @@ dependencies {
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'com.google.android.material:material:1.10.0'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core:1.12.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'androidx.mediarouter:mediarouter:1.6.0'
@@ -0,0 +1,7 @@
v4.9.6: OpenTracks
Oprava chyby:
- Export: existující soubory jsou nyní kompletně přepisovány
Vývojář:
- Předěláno zpracování barometrických dat
@@ -0,0 +1,4 @@
v4.9.7: OpenTracks
Oprava chyby:
- zabránění pádu při ukončení nahrávání (vzniknuvší ve verzi v4.9.6)
@@ -0,0 +1,4 @@
v4.9.8: OpenTracks
Oprava chyby:
- Senzor kadence znovu funguje (chyba vznikla ve verzi v4.9.6)
@@ -0,0 +1,6 @@
v4.9.9: OpenTracks
Oprava chyby:
- Pád při používání senzoru BLE na konci záznamu (závod)
Důvod: subsystém senzorů byl spuštěn dvakrát a pouze druhý z nich byl vypnut.
- Při nahrávání jsou znovu zobrazeny názvy senzorů BLE
@@ -0,0 +1,5 @@
v4.9.7: OpenTracks
Bugfix:
- prevent crash when stopping a recording(introduced in v4.9.6)
@@ -0,0 +1,4 @@
v4.9.8: OpenTracks
Bugfix:
- Cycling cadence sensor works again (introduced in v4.9.6)
@@ -0,0 +1,6 @@
v4.9.9: OpenTracks
Bugfix:
- Crash when using a BLE sensor on recording end (race condition)
Reason: sensor subsystem was started twice and only the 2nd one was shutdown.
- While recording, BLE sensor names are shown again
@@ -0,0 +1,15 @@
v4.10.0: OpenTracks
Changes:
- Support gain/loss via BLE Environmental Sensing (barometer)
- Internal barometer can be turned off (e.g., if broken)
- Material3 for preference switches
- SplashScreen supports day/night mode
- Bluetooth sensor settings: show selected sensor name (if cached by Android)
Bugfix:
- Interval UI was misaligned
Developer:
- Cleanup TrackRecordingServiceConnection
- TrackRecordingService always starts as ForegroundService
@@ -0,0 +1,7 @@
v4.9.6: OpenTracks
Bugfix :
- Exporteren: bestaande bestanden worden nu volledig overschreven
Ontwikkelaar:
- Barometrische gegevensverwerking aangepast
@@ -0,0 +1,4 @@
v4.9.7: OpenTracks
Bugfix :
- voorkoming van crash bij het stoppen van een opname (geïntroduceerd in v4.9.6)
@@ -0,0 +1,4 @@
v4.9.8: OpenTracks
Bugfix :
- Fietscadanssensor werkt weer (geïntroduceerd in v4.9.6)
@@ -0,0 +1,6 @@
v4.9.9: OpenTracks
Bugfix :
- Crash bij gebruik van een BLE-sensor aan het einde van opname(raceconditie)
Reden: het subsysteem sensor is tweemaal gestart en alleen het tweede is uitgeschakeld.
- Tijdens het opnemen worden namen van BLE-sensoren opnieuw weergegeven
@@ -586,7 +586,7 @@ public class ExportImportTest {
Mockito.when(barometer.getValue()).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain));
sensorDataSet.add(barometer);
} else {
sensorDataSet.add(new AggregatorBarometer(""));
sensorDataSet.add(new AggregatorBarometer("test", null));
}
}
@@ -16,7 +16,7 @@ public class AggregatorBarometerTest {
@Test
public void getAltitudeGainLoss_downhill() {
// given
AggregatorBarometer subject = new AggregatorBarometer("");
AggregatorBarometer subject = new AggregatorBarometer("test", null);
// then
addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f});
+1 -1
View File
@@ -75,7 +75,7 @@ limitations under the License.
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/ThemeCustom">
android:theme="@style/OpenTracksTheme">
<activity
android:name=".publicapi.StartRecording"
@@ -28,7 +28,6 @@ import de.dennisguse.opentracks.data.models.ActivityType;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackEditBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackUtils;
/**
@@ -44,7 +43,6 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
private static final String ICON_VALUE_KEY = "icon_value_key";
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private ContentProviderUtils contentProviderUtils;
private Track track;
private ActivityType activityType;
@@ -55,7 +53,6 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
trackRecordingServiceConnection = new TrackRecordingServiceConnection();
Track.Id trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
if (trackId == null) {
Log.e(TAG, "invalid trackId");
@@ -114,18 +111,6 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
}
@Override
protected void onStart() {
super.onStart();
trackRecordingServiceConnection.startConnection(this);
}
@Override
protected void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind(this);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
@@ -25,6 +25,7 @@ import android.graphics.drawable.AnimatedVectorDrawable;
import android.location.LocationManager;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
@@ -73,7 +74,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private static final String TAG = TrackListActivity.class.getSimpleName();
// The following are set in onCreate
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackRecordingServiceConnection recordingStatusConnection;
private TrackListAdapter adapter;
private TrackListBinding viewBinding;
@@ -138,13 +139,6 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
.observe(TrackListActivity.this, this::onGpsStatusChanged);
updateGpsMenuItem(true, recordingStatus.isRecording());
if (service.getGpsStatusObservable().getValue().isGpsStarted()) {
return;
}
//TODO Not cool to do this in a callback that might be called more than once!
service.tryStartSensors();
};
@Override
@@ -154,7 +148,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
requestRequiredPermissions();
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
recordingStatusConnection = new TrackRecordingServiceConnection(bindChangedCallback);
viewBinding.aggregatedStatsButton.setOnClickListener((view) -> startActivity(IntentUtils.newIntent(this, AggregatedStatisticsActivity.class)));
viewBinding.sensorStartButton.setOnClickListener((view) -> {
@@ -163,9 +157,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else {
if (gpsStatusValue.isGpsStarted()) {
trackRecordingServiceConnection.unbindAndStop(this);
recordingStatusConnection.stopService(this);
} else {
trackRecordingServiceConnection.startAndBindWithCallback(this);
TrackRecordingServiceConnection.execute(this, (service, connection) -> service.tryStartSensors());
}
}
});
@@ -182,16 +176,15 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
// Not Recording -> Recording
Log.i(TAG, "Starting recording");
updateGpsMenuItem(false, true);
new TrackRecordingServiceConnection((service, connection) -> {
TrackRecordingServiceConnection.execute(this, (service, connection) -> {
Track.Id trackId = service.startNewTrack();
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
newIntent.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
connection.unbind(this);
}).startAndBind(this, true);
});
});
viewBinding.trackListFabAction.setOnLongClickListener((view) -> {
if (!recordingStatus.isRecording()) {
@@ -201,7 +194,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
// Recording -> Stop
ActivityUtils.vibrate(this, 1000);
updateGpsMenuItem(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
recordingStatusConnection.stopRecording(TrackListActivity.this);
viewBinding.trackListFabAction.setImageResource(R.drawable.ic_baseline_record_24);
viewBinding.trackListFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.red_dark));
return true;
@@ -220,7 +213,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
super.onStart();
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.startConnection(this);
recordingStatusConnection.bind(this);
}
@Override
@@ -240,14 +233,14 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
super.onStop();
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind(this);
recordingStatusConnection.unbind(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
trackRecordingServiceConnection = null;
recordingStatusConnection = null;
adapter = null;
}
@@ -201,7 +201,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
}
if (item.getItemId() == R.id.track_detail_resume_track) {
new TrackRecordingServiceConnection((service, connection) -> {
TrackRecordingServiceConnection.execute(this, (service, connection) -> {
service.resumeTrack(trackId);
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
@@ -209,9 +209,8 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
connection.unbind(this);
finish();
}).startAndBind(this, true);
});
return true;
}
@@ -207,7 +207,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.startConnection(this);
trackRecordingServiceConnection.bind(this);
trackDataHub.start();
}
@@ -224,7 +224,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
trackDataHub.setRecordingStatus(recordingStatus);
}
trackRecordingServiceConnection.startAndBindWithCallback(this);
trackRecordingServiceConnection.bind(this);
}
@Override
@@ -136,7 +136,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
}
private void resumeTrackAndFinish() {
new TrackRecordingServiceConnection((service, connection) -> {
TrackRecordingServiceConnection.execute(this, (service, connection) -> {
service.resumeTrack(trackId);
Intent newIntent = IntentUtils.newIntent(TrackStoppedActivity.this, TrackRecordingActivity.class)
@@ -144,9 +144,8 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
connection.unbind(this);
finish();
}).startAndBind(this, true);
});
}
@Override
@@ -88,7 +88,7 @@ public class StatisticsRecordingFragment extends Fragment {
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.startConnection(getContext());
trackRecordingServiceConnection.bind(getContext());
}
@Override
@@ -37,8 +37,7 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
if (PreferencesUtils.isPublicAPIenabled()) {
Log.i(TAG, "Received and trying to execute requested action.");
new TrackRecordingServiceConnection(serviceConnectedCallback)
.startAndBind(this, isStartServiceForeground());
TrackRecordingServiceConnection.execute(this, serviceConnectedCallback);
} else {
Toast.makeText(this, getString(R.string.settings_public_api_disabled_toast), Toast.LENGTH_LONG).show();
Log.w(TAG, "Public API is disabled; ignoring request.");
@@ -46,10 +45,6 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
}
}
protected boolean isStartServiceForeground() {
return false;
}
protected abstract void execute(TrackRecordingService service);
protected abstract boolean isPostExecuteStopService();
@@ -62,9 +62,4 @@ public class StartRecording extends AbstractAPIActivity {
protected boolean isPostExecuteStopService() {
return false;
}
@Override
protected boolean isStartServiceForeground() {
return true;
}
}
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.sensors;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
@@ -33,38 +34,40 @@ import androidx.annotation.NonNull;
import java.util.Optional;
import java.util.UUID;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
/**
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
*/
@SuppressLint("MissingPermission")
public class BluetoothConnectionManager {
public class BluetoothConnectionManager implements Driver {
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private final SensorManager.SensorDataChangedObserver observer;
private final SensorHandlerInterface sensorHandler;
private BluetoothGatt bluetoothGatt;
private final BluetoothAdapter bluetoothAdapter;
private BluetoothGatt bluetoothGatt;
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
switch (newState) {
case BluetoothProfile.STATE_CONNECTING ->
Log.i(TAG, "Connecting to sensor: " + gatt.getDevice());
Log.i(TAG, gatt.getDevice() + ": connecting to sensor");
case BluetoothProfile.STATE_CONNECTED -> {
Log.i(TAG, "Connected to sensor: " + gatt.getDevice() + "; discovering services.");
Log.i(TAG, gatt.getDevice() + ": connected to sensor; discovering services");
gatt.discoverServices();
}
case BluetoothProfile.STATE_DISCONNECTING ->
Log.i(TAG, "Disconnecting from sensor: " + gatt.getDevice());
Log.i(TAG, gatt.getDevice() + ": disconnecting from sensor: ");
case BluetoothProfile.STATE_DISCONNECTED -> {
//This is also triggered, if no connection was established (ca. 30s)
Log.i(TAG, "Disconnected from sensor: " + gatt.getDevice() + "; trying to reconnect");
Log.i(TAG, gatt.getDevice() + ": disconnected from sensor: trying to reconnect");
if (gatt.connect()) {
Log.e(TAG, "Could not trigger reconnect for sensor: " + gatt.getDevice());
Log.e(TAG, gatt.getDevice() + ": could not trigger reconnect for sensor");
}
clearData();
}
@@ -84,13 +87,13 @@ public class BluetoothConnectionManager {
}
if (gattService == null) {
Log.e(TAG, "Could not get gattService for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceMeasurement);
Log.e(TAG, gatt.getDevice() + ": could not get gattService for serviceUUID=" + serviceMeasurement);
return;
}
BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(serviceMeasurement.measurementUUID());
if (characteristic == null) {
Log.e(TAG, "Could not get BluetoothCharacteristic for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceMeasurement.serviceUUID() + " characteristicUUID=" + serviceMeasurement.measurementUUID());
Log.e(TAG, gatt.getDevice() + ": could not get BluetoothCharacteristic for serviceUUID=" + serviceMeasurement.serviceUUID() + " characteristicUUID=" + serviceMeasurement.measurementUUID());
return;
}
gatt.setCharacteristicNotification(characteristic, true);
@@ -102,21 +105,25 @@ public class BluetoothConnectionManager {
return;
}
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
if (!descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) {
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID could not be set to ENABLE_NOTIFICATION_VALUE");
}
if (!gatt.writeDescriptor(descriptor)) {
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID descriptor could not be written");
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
UUID serviceUUID = characteristic.getService().getUuid();
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress() + " with service " + serviceUUID + " and characteristics " + characteristic.getUuid());
BluetoothDevice device = gatt.getDevice();
Log.d(TAG, device + ": Received data with service " + serviceUUID + " and characteristics " + characteristic.getUuid());
Optional<ServiceMeasurementUUID> serviceMeasurementUUID = sensorHandler.getServices()
.stream()
.filter(s -> s.serviceUUID().equals(characteristic.getService().getUuid()))
.findFirst();
if (serviceMeasurementUUID.isEmpty()) {
Log.e(TAG, "Unknown service UUID; not supported?");
Log.e(TAG, device + ": Unknown service UUID; not supported?");
return;
}
@@ -124,43 +131,86 @@ public class BluetoothConnectionManager {
}
};
BluetoothConnectionManager(SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) {
BluetoothConnectionManager(BluetoothAdapter bluetoothAdapter, SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) {
this.bluetoothAdapter = bluetoothAdapter;
this.observer = observer;
this.sensorHandler = sensorHandler;
}
synchronized void connect(Context context, Handler handler, @NonNull BluetoothDevice device) {
if (bluetoothGatt != null) {
@Override
public synchronized void connect(Context context, Handler handler, @NonNull String address) {
if (!isBluetoothEnabled()) {
Log.w(TAG, "Bluetooth not enabled.");
return;
}
if (SensorType.NONE.getPreferenceValue().equals(address)) {
Log.w(TAG, "NONE: going to disconnect");
if (isConnected()) {
disconnect();
}
return;
}
if (isConnected()) {
Log.w(TAG, "Already connected; ignoring.");
return;
}
Log.d(TAG, "Connecting to: " + device);
if (isSameBluetoothDevice(address)) {
return;
} else {
disconnect();
}
BluetoothDevice device;
try {
device = bluetoothAdapter.getRemoteDevice(address);
} catch (IllegalArgumentException e) {
Log.e(TAG, address + ": Unable to get remote device for", e);
return;
}
Log.d(TAG, device + ": trying to connect");
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
observer.onConnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
observer.onConnect(sensorHandler.createEmptySensorData(device.getAddress(), device.getName()));
}
private synchronized void clearData() {
observer.onDisconnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
observer.onDisconnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress(), bluetoothGatt.getDevice().getName()));
}
synchronized void disconnect() {
if (bluetoothGatt == null) {
Log.w(TAG, "Cannot disconnect if not connected.");
@Override
public synchronized void disconnect() {
if (!isConnected()) {
Log.w(TAG, "Not connected; no need to re-connect.");
return;
}
Log.i(TAG, bluetoothGatt.getDevice() + ": start disconnect");
bluetoothGatt.disconnect();
bluetoothGatt.close();
clearData();
Log.i(TAG, bluetoothGatt.getDevice() + ": disconnect finished");
bluetoothGatt = null;
}
synchronized boolean isSameBluetoothDevice(String address) {
private synchronized boolean isSameBluetoothDevice(String address) {
if (bluetoothGatt == null) {
return false;
}
return address.equals(bluetoothGatt.getDevice().getAddress());
}
private boolean isBluetoothEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
@Override
public boolean isConnected() {
return bluetoothGatt != null;
}
}
@@ -1,12 +1,15 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface {
@@ -22,15 +25,23 @@ public class BluetoothHandlerBarometricPressure implements SensorHandlerInterfac
}
@Override
public Aggregator<?, ?> createEmptySensorData(String address) {
return null; //TODO
public Aggregator<?, ?> createEmptySensorData(String address, String name) {
return new AggregatorBarometer(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
//TODO
AtmosphericPressure value = parseEnvironmentalSensing(characteristic);
if (value == null) return;
Log.i("ZXCV", "VALUE: " + value);
observer.onChange(new Raw<>(value));
}
/**
* Decoding:
* org.bluetooth.service.environmental_sensing.xml
* org.bluetooth.characteristic.pressure.xml
*/
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
@@ -24,8 +24,8 @@ public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface {
}
@Override
public AggregatorCyclingCadence createEmptySensorData(String address) {
return new AggregatorCyclingCadence(address);
public AggregatorCyclingCadence createEmptySensorData(String address, String name) {
return new AggregatorCyclingCadence(address, name);
}
@Override
@@ -26,8 +26,8 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
}
@Override
public AggregatorCyclingDistanceSpeed createEmptySensorData(String address) {
return new AggregatorCyclingDistanceSpeed(address);
public AggregatorCyclingDistanceSpeed createEmptySensorData(String address, String name) {
return new AggregatorCyclingDistanceSpeed(address, name);
}
@Override
@@ -26,8 +26,8 @@ public class BluetoothHandlerManagerCyclingPower implements SensorHandlerInterfa
}
@Override
public AggregatorCyclingPower createEmptySensorData(String address) {
return new AggregatorCyclingPower(address);
public AggregatorCyclingPower createEmptySensorData(String address, String name) {
return new AggregatorCyclingPower(address, name);
}
@Override
@@ -36,8 +36,8 @@ public class BluetoothHandlerManagerHeartRate implements SensorHandlerInterface
}
@Override
public AggregatorHeartRate createEmptySensorData(String address) {
return new AggregatorHeartRate(address);
public AggregatorHeartRate createEmptySensorData(String address, String name) {
return new AggregatorHeartRate(address, name);
}
@Override
@@ -29,8 +29,8 @@ public class BluetoothHandlerRunningSpeedAndCadence implements SensorHandlerInte
}
@Override
public AggregatorRunning createEmptySensorData(String address) {
return new AggregatorRunning(address);
public AggregatorRunning createEmptySensorData(String address, String name) {
return new AggregatorRunning(address, name);
}
@Override
@@ -17,11 +17,9 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -30,29 +28,20 @@ import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
/**
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
* <p>
* Note: should only be instantiated once.
* <p>
* TODO: listen for Bluetooth enabled/disabled events.
* <p>
* TODO: In case, a cycling (Cadence and Speed) sensor reports both values, testing is required.
* We establish two GATT separate GATT connections (as if two different sensors were used).
* However, it is not clear if this is allowed.
* Even if this works, it is not clear what happens if a user (while recording) changes one of the sensors in the settings as this will trigger a disconnect of one GATT.
*
* @author Sandor Dornbush
*/
public class BluetoothRemoteSensorManager implements SensorConnector, SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(50); //TODO
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
private final BluetoothAdapter bluetoothAdapter;
private final Context context;
private final Handler handler;
private boolean started = false;
@@ -66,14 +55,13 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @Nullable SensorManager.SensorDataChangedObserver observer) {
this.context = context;
this.handler = handler;
bluetoothAdapter = BluetoothUtils.getAdapter(context);
this.heartRate = new BluetoothConnectionManager(observer, new BluetoothHandlerManagerHeartRate());
this.cyclingCadence = new BluetoothConnectionManager(observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingSpeed = new BluetoothConnectionManager(observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingPower = new BluetoothConnectionManager(observer, new BluetoothHandlerManagerCyclingPower());
this.runningSpeedAndCadence = new BluetoothConnectionManager(observer, new BluetoothHandlerRunningSpeedAndCadence());
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(context);
this.heartRate = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerManagerHeartRate());
this.cyclingCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingCadence());
this.cyclingSpeed = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingPower = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerManagerCyclingPower());
this.runningSpeedAndCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerRunningSpeedAndCadence());
}
@Override
@@ -95,39 +83,8 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
started = false;
}
public boolean isEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
if (!isEnabled()) {
Log.w(TAG, "Bluetooth not enabled.");
return;
}
if (PreferencesUtils.isBluetoothSensorAddressNone(address)) {
Log.w(TAG, "No Bluetooth address.");
connectionManager.disconnect();
return;
}
// Check if there is an ongoing connection; if yes, check if the address changed.
if (connectionManager.isSameBluetoothDevice(address)) {
return;
} else {
connectionManager.disconnect();
}
if (!PermissionRequester.BLUETOOTH.hasPermission(context)) {
Log.w(TAG, "BLUETOOTH_SCAN and/or BLUETOOTH_CONNECT not granted; not connecting.");
}
Log.i(TAG, "Connecting to bluetooth address: " + address);
try {
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
connectionManager.connect(context, handler, device);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unable to get remote device for: " + address, e);
}
connectionManager.connect(context, handler, address);
}
@Override
@@ -1,13 +1,14 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Log;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
@@ -16,31 +17,61 @@ public class GainManager implements SensorConnector {
private static final String TAG = GainManager.class.getSimpleName();
private final BarometerInternal driver;
private final SensorManager.SensorDataChangedObserver listener;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
connect();
};
private Context context;
private Handler handler;
private Driver driver;
public GainManager(SensorManager.SensorDataChangedObserver listener) {
this.listener = listener;
driver = new BarometerInternal();
}
public void start(Context context, Handler handler) {
driver.connect(context, handler, this);
if (driver.isConnected()) {
listener.onConnect(new AggregatorBarometer("internal"));
}
this.context = context;
this.handler = handler;
PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
public void stop(Context context) {
Log.d(TAG, "Stop");
this.context = null;
this.handler = null;
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
driver.disconnect(context);
listener.onDisconnect(new AggregatorBarometer("internal"));
onDisconnect();
}
public void onSensorValueChanged(AtmosphericPressure currentSensorValue) {
listener.onChange(new Raw<>(currentSensorValue));
private void connect() {
onDisconnect();
String address = PreferencesUtils.getBarometerSensorAddress();
switch (PreferencesUtils.getSensorType(address)) {
case NONE -> driver = null;
case INTERNAL -> driver = new BarometerInternal(listener);
case REMOTE -> driver =
new BluetoothConnectionManager(
BluetoothUtils.getAdapter(context),
listener,
new BluetoothHandlerBarometricPressure()
);
default -> throw new RuntimeException("Not implemented");
}
if (driver != null) {
driver.connect(context, handler, address);
}
}
private void onDisconnect() {
if (driver == null) return;
driver.disconnect();
listener.onDisconnect(new AggregatorBarometer("GainManager", null));
}
}
@@ -57,6 +57,10 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
}
public void start(Context context, Handler handler) {
if (gpsManager != null) {
throw new RuntimeException("SensorManager cannot be started twice; stop first.");
}
gpsManager = new GPSManager(observer); //TODO Pass listener
altitudeSumManager = new GainManager(listener);
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, listener);
@@ -0,0 +1,17 @@
package de.dennisguse.opentracks.sensors;
public enum SensorType {
NONE("NONE"),
INTERNAL("INTERNAL"),
REMOTE("*");
private final String preferenceValue;
SensorType(String preferenceValue) {
this.preferenceValue = preferenceValue;
}
public String getPreferenceValue() {
return preferenceValue;
}
}
@@ -4,61 +4,79 @@ import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.concurrent.TimeUnit;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.GainManager;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
public class BarometerInternal implements SensorEventListener {
public class BarometerInternal implements Driver {
private static final String TAG = BarometerInternal.class.getSimpleName();
private static final int SAMPLING_PERIOD = (int) TimeUnit.SECONDS.toMicros(5);
private GainManager observer;
private final SensorManager.SensorDataChangedObserver listener;
@Override
public void onSensorChanged(SensorEvent event) {
if (!isConnected()) {
Log.w(TAG, "Not connected to sensor, cannot process data.");
return;
private Context context;
private final SensorEventListener sensorEventListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
if (!isConnected()) {
Log.w(TAG, "Not connected to sensor, cannot process data.");
return;
}
listener.onChange(new Raw<>(AtmosphericPressure.ofHPA(event.values[0])));
}
observer.onSensorValueChanged(AtmosphericPressure.ofHPA(event.values[0]));
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
Log.w(TAG, "Sensor accuracy changes are (currently) ignored.");
}
};
public BarometerInternal(@NonNull SensorManager.SensorDataChangedObserver listener) {
this.listener = listener;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
Log.w(TAG, "Sensor accuracy changes are (currently) ignored.");
}
public void connect(Context context, Handler handler, String addressIgnored) {
public void connect(Context context, Handler handler, GainManager observer) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
android.hardware.SensorManager sensorManager = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
if (pressureSensor == null) {
Log.w(TAG, "No pressure sensor available.");
this.observer = null;
}
if (sensorManager.registerListener(this, pressureSensor, SAMPLING_PERIOD, handler)) {
this.observer = observer;
return;
}
disconnect(context);
}
if (sensorManager.registerListener(sensorEventListener, pressureSensor, SAMPLING_PERIOD, handler)) {
this.context = context;
listener.onConnect(new AggregatorBarometer("internal", null));
return;
}
public void disconnect(Context context) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(this);
observer = null;
disconnect();
}
public boolean isConnected() {
return observer != null;
return context != null;
}
@Override
public void disconnect() {
if (!isConnected()) return;
android.hardware.SensorManager sensorManager = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(sensorEventListener);
this.context = null;
}
}
@@ -0,0 +1,12 @@
package de.dennisguse.opentracks.sensors.driver;
import android.content.Context;
import android.os.Handler;
public interface Driver {
void connect(Context context, Handler handler, String address);
boolean isConnected();
void disconnect();
}
@@ -11,8 +11,8 @@ public class AggregatorBarometer extends Aggregator<AtmosphericPressure, Altitud
private AtmosphericPressure lastAcceptedSensorValue;
public AggregatorBarometer(String sensorAddress) {
super(sensorAddress);
public AggregatorBarometer(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
@Override
@@ -14,10 +14,6 @@ public class AggregatorCyclingCadence extends Aggregator<BluetoothHandlerCycling
private final String TAG = AggregatorCyclingCadence.class.getSimpleName();
public AggregatorCyclingCadence(String sensorAddress) {
super(sensorAddress);
}
public AggregatorCyclingCadence(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
@@ -17,10 +17,6 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<BluetoothHandlerC
private Distance wheelCircumference;
public AggregatorCyclingDistanceSpeed(String sensorAddress) {
super(sensorAddress);
}
public AggregatorCyclingDistanceSpeed(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
@@ -6,10 +6,6 @@ import de.dennisguse.opentracks.data.models.Power;
public class AggregatorCyclingPower extends Aggregator<Power, Power> {
public AggregatorCyclingPower(String address) {
super(address);
}
public AggregatorCyclingPower(String name, String address) {
super(name, address);
}
@@ -6,10 +6,6 @@ import de.dennisguse.opentracks.data.models.HeartRate;
public class AggregatorHeartRate extends Aggregator<HeartRate, HeartRate> {
public AggregatorHeartRate(String address) {
super(address);
}
public AggregatorHeartRate(String name, String address) {
super(name, address);
}
@@ -14,10 +14,6 @@ public final class AggregatorRunning extends Aggregator<BluetoothHandlerRunningS
private static final String TAG = AggregatorRunning.class.getSimpleName();
public AggregatorRunning(String sensorAddress) {
super(sensorAddress);
}
public AggregatorRunning(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
@@ -184,7 +184,7 @@ public final class SensorDataSet {
}
private void set(@NonNull Aggregator<?, ?> type, @Nullable Aggregator<?, ?> sensorData) {
Log.i(TAG, "Setting aggregator " + type.getClass().getCanonicalName());
Log.i(TAG, "Setting aggregator " + type.getClass().getCanonicalName() + " to " + sensorData);
if (type instanceof AggregatorHeartRate) {
heartRate = (AggregatorHeartRate) sensorData;
@@ -11,7 +11,7 @@ public interface SensorHandlerInterface {
List<ServiceMeasurementUUID> getServices();
Aggregator<?, ?> createEmptySensorData(String address);
Aggregator<?, ?> createEmptySensorData(String address, String name);
void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic);
}
@@ -30,6 +30,7 @@ import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.ServiceCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -111,7 +112,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
recordingDataObservable = new MutableLiveData<>(NOT_RECORDING);
trackPointCreator = new TrackPointCreator(this);
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this , handler);
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, handler);
voiceAnnouncementManager = new VoiceAnnouncementManager(this);
notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -125,6 +126,9 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
if (isRecording()) {
endCurrentTrack();
}
if (isSensorStarted()) {
stopSensors();
}
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(this);
@@ -163,6 +167,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
Log.w(TAG, "Ignore startNewTrack. Already recording.");
return null;
}
Log.i(TAG, "startNewTrack");
// Set recording status
Track.Id trackId = trackRecordingManager.startNewTrack();
@@ -177,6 +182,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
Log.w(TAG, "Cannot resume a non-existing track.");
return;
}
Log.i(TAG, "resumeTrack");
updateRecordingStatus(RecordingStatus.record(trackId));
@@ -193,27 +199,30 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
public void tryStartSensors() {
if (isRecording()) return;
if (isSensorStarted()) return;
Log.i(TAG, "tryStartSensors");
startSensors();
}
private void startSensors() {
private synchronized void startSensors() {
if (isSensorStarted()) {
Log.i(TAG, "sensors already started; skipping");
return;
}
Log.i(TAG, "startSensors");
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
trackPointCreator.start(this, handler);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (!PermissionRequester.RECORDING.hasPermission(this)) {
Toast.makeText(this, R.string.permission_recording_failed, Toast.LENGTH_LONG).show();
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (!PermissionRequester.RECORDING.hasPermission(this)) {
Toast.makeText(this, R.string.permission_recording_failed, Toast.LENGTH_LONG).show();
return;
}
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
} else {
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
}
ServiceCompat.startForeground(this, TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE);
}
public void endCurrentTrack() {
@@ -239,6 +248,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
stopForeground(true);
notificationManager.cancelNotification();
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
gpsStatusObservable.postValue(STATUS_GPS_DEFAULT);
}
@Override
@@ -255,7 +265,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
@Override
public void newGpsStatus(GpsStatusValue gpsStatusValue) {
Log.e(TAG, "newGpsStatus: " + gpsStatusValue.message);
Log.i(TAG, "newGpsStatus: " + gpsStatusValue.message);
if (notificationManager == null) {
@@ -336,6 +346,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
return recordingStatus.isRecording();
}
private boolean isSensorStarted() {
return wakeLock != null;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) {
voiceAnnouncementManager.onSharedPreferenceChanged(sharedPreferences, key);
@@ -42,7 +42,7 @@ import de.dennisguse.opentracks.data.models.Marker;
*
* @author Rodrigo Damazio
*/
public class TrackRecordingServiceConnection implements ServiceConnection, DeathRecipient {
public class TrackRecordingServiceConnection {
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName();
@@ -50,9 +50,29 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
private TrackRecordingService trackRecordingService;
public TrackRecordingServiceConnection() {
callback = null;
}
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.i(TAG, "Connected to the service: " + service);
try {
service.linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
Log.e(TAG, "Failed to bind a death recipient.", e);
}
setTrackRecordingService(((TrackRecordingService.Binder) service).getService());
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.i(TAG, "Disconnected from the service.");
setTrackRecordingService(null);
}
};
private final DeathRecipient deathRecipient = () -> {
Log.d(TAG, "Service died.");
setTrackRecordingService(null);
};
public TrackRecordingServiceConnection(@NonNull Callback callback) {
this.callback = callback;
@@ -60,58 +80,13 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
public void bind(@NonNull Context context) {
if (trackRecordingService != null) {
return;
}
context.bindService(new Intent(context, TrackRecordingService.class), this, 0);
}
/**
* Starts and binds the service.
*
* @param foreground is the service expected to call `startForeground()`?
*/
public void startAndBind(Context context, boolean foreground) {
if (trackRecordingService != null) {
// Service is already started and bound.
return;
}
Log.i(TAG, "Starting the service.");
if (foreground) {
ContextCompat.startForegroundService(context, new Intent(context, TrackRecordingService.class));
} else {
context.startService(new Intent(context, TrackRecordingService.class));
}
startConnection(context);
}
//TODO There should be a better way to implement this.
/**
* Triggers the onConnected() callback even if already connected.
*/
//TODO Check if this is actually needed as it is used to re-connect from Activities in onResume by using a LiveData; might be obsolete. If not, there should be a better way to implement this.
@Deprecated
public void startAndBindWithCallback(Context context) {
if (trackRecordingService == null) {
startAndBind(context, false);
return;
}
if (callback != null) {
callback.onConnected(trackRecordingService, this);
}
}
public void startConnection(@NonNull Context context) {
if (trackRecordingService != null) {
// Service is already started and bound.
return;
}
Log.i(TAG, "Binding the service.");
int flags = BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0;
context.bindService(new Intent(context, TrackRecordingService.class), this, flags);
context.bindService(new Intent(context, TrackRecordingService.class), serviceConnection, flags);
}
/**
@@ -120,75 +95,45 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
//TODO This is often called for one-shot operations and should be refactored as unbinding is required.
public void unbind(Context context) {
try {
context.unbindService(this);
context.unbindService(serviceConnection);
} catch (IllegalArgumentException e) {
// Means not bound to the service. OK to ignore.
}
setTrackRecordingService(null);
}
/**
* Unbinds and stops the service.
*/
public void unbindAndStop(Context context) {
unbind(context);
public void stopService(Context context) {
context.stopService(new Intent(context, TrackRecordingService.class));
}
@Nullable
public TrackRecordingService getServiceIfBound() {
return trackRecordingService;
public void unbindAndStop(Context context) {
unbind(context);
stopService(context);
}
private void setTrackRecordingService(TrackRecordingService value) {
trackRecordingService = value;
if (callback != null) {
if (value != null) {
callback.onConnected(value, this);
} else {
callback.onDisconnected();
}
if (value != null) {
callback.onConnected(value, this);
}
}
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.i(TAG, "Connected to the service.");
try {
service.linkToDeath(this, 0);
} catch (RemoteException e) {
Log.e(TAG, "Failed to bind a death recipient.", e);
}
setTrackRecordingService(((TrackRecordingService.Binder) service).getService());
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.i(TAG, "Disconnected from the service.");
setTrackRecordingService(null);
}
@Override
public void binderDied() {
Log.d(TAG, "Service died.");
setTrackRecordingService(null);
}
//TODO Move to some other place; not needed here.
@Nullable
public Marker.Id addMarker(Context context, String name, String category, String description, String photoUrl) {
TrackRecordingService trackRecordingService = getServiceIfBound();
if (trackRecordingService == null) {
Log.d(TAG, "Unable to add marker, no track recording service");
} else {
try {
Marker.Id marker = trackRecordingService.insertMarker(name, category, description, photoUrl);
if (marker != null) {
Toast.makeText(context, R.string.marker_add_success, Toast.LENGTH_SHORT).show();
return marker;
}
} catch (IllegalStateException e) {
Log.e(TAG, "Unable to add marker.", e);
return null;
}
try {
Marker.Id marker = trackRecordingService.insertMarker(name, category, description, photoUrl);
if (marker != null) {
Toast.makeText(context, R.string.marker_add_success, Toast.LENGTH_SHORT).show();
return marker;
}
} catch (IllegalStateException e) {
Log.e(TAG, "Unable to add marker.", e);
}
Toast.makeText(context, R.string.marker_add_error, Toast.LENGTH_LONG).show();
@@ -196,7 +141,6 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
}
public void stopRecording(@NonNull Context context) {
TrackRecordingService trackRecordingService = getServiceIfBound();
if (trackRecordingService == null) {
Log.e(TAG, "TrackRecordingService not connected.");
} else {
@@ -206,9 +150,17 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
}
public interface Callback {
void onConnected(TrackRecordingService service, TrackRecordingServiceConnection connection);
void onConnected(TrackRecordingService service, TrackRecordingServiceConnection self);
}
default void onDisconnected() {
}
public static void execute(Context context, Callback callback) {
Callback withUnbind = (service, connection) -> {
callback.onConnected(service, connection);
connection.unbind(context);
};
new TrackRecordingServiceConnection(withUnbind)
.bind(context);
ContextCompat.startForegroundService(context, new Intent(context, TrackRecordingService.class));
}
}
@@ -48,6 +48,7 @@ import de.dennisguse.opentracks.data.models.HeartRateZones;
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.sensors.SensorType;
import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout;
import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayoutIO;
@@ -249,11 +250,18 @@ public class PreferencesUtils {
}
private static String getBluetoothSensorAddressNone() {
return resources.getString(R.string.sensor_type_value_none);
return SensorType.NONE.getPreferenceValue();
}
public static boolean isBluetoothSensorAddressNone(String currentValue) {
return getBluetoothSensorAddressNone().equals(currentValue);
public static SensorType getSensorType(String address) {
return Arrays.stream(SensorType.values())
.filter(it -> it.getPreferenceValue().equals(address))
.findFirst()
.orElse(SensorType.REMOTE);
}
public static String getBarometerSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_pressure_key, getBluetoothSensorAddressNone());
}
public static String getBluetoothHeartRateSensorAddress() {
@@ -1,5 +1,6 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
import android.view.LayoutInflater;
import android.view.View;
@@ -7,10 +8,13 @@ import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@SuppressLint("MissingPermission")
public class BluetoothLeAdapter extends BaseAdapter {
/**
@@ -50,22 +54,30 @@ public class BluetoothLeAdapter extends BaseAdapter {
return currentView;
}
public void add(String name, String address) {
Device device = new Device(name, address);
/**
* @return Data changed?
*/
public boolean add(String address, String name) {
Device device = new Device(address, name);
if (!devices.contains(device)) {
devices.add(new Device(name, address));
devices.add(device);
return true;
} else {
for (Device currentDevice : devices) {
if (currentDevice.getAddress().equals(address)) {
currentDevice.setName(name);
if (currentDevice.address.equals(address)) {
currentDevice.name = name;
return true;
}
}
}
notifyDataSetChanged();
return false;
}
public void add(BluetoothDevice bluetoothDevice) {
add(bluetoothDevice.getName(), bluetoothDevice.getAddress());
public void addAll(List<BluetoothDevice> bluetoothDevices) {
boolean dataSetChanged = bluetoothDevices.stream()
.anyMatch(bluetoothDevice -> add(bluetoothDevice.getAddress(), bluetoothDevice.getName()));
if (dataSetChanged) notifyDataSetChanged();
}
public Device get(int index) {
@@ -73,19 +85,14 @@ public class BluetoothLeAdapter extends BaseAdapter {
}
public static class Device {
private String name;
@NonNull
private final String address;
private String name;
public Device(String name, String address) {
this.name = name;
Device(@NonNull String address, String name) {
Objects.requireNonNull(address);
this.address = address;
}
public String getNameOrAddress() {
return name != null ? name : getAddress();
}
public void setName(String name) {
this.name = name;
}
@@ -93,16 +100,21 @@ public class BluetoothLeAdapter extends BaseAdapter {
return address;
}
public String getNameOrAddress() {
return name != null ? name : address;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Device device)) return false;
return address.equals(device.address);
}
@Override
public int hashCode() {
return Objects.hash(name, address);
return Objects.hash(address);
}
}
}
@@ -5,6 +5,8 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
@@ -27,6 +29,6 @@ public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreferen
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerManagerCyclingPower.CYCLING_POWER);
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerManagerCyclingPower.CYCLING_POWER));
}
}
@@ -5,6 +5,8 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreference {
@@ -27,6 +29,6 @@ public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreferen
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE);
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE));
}
}
@@ -0,0 +1,41 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerBarometricPressure;
public class BluetoothLePressurePreference extends BluetoothLeSensorPreference {
public BluetoothLePressurePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public BluetoothLePressurePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BluetoothLePressurePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BluetoothLePressurePreference(Context context) {
super(context);
}
@Override
public PreferenceDialogFragmentCompat createInstance() {
SensorManager sensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
boolean hasInternalSensor = pressureSensor != null;
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog
.newInstance(getKey(), List.of(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE), hasInternalSensor);
}
}
@@ -5,6 +5,8 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSensorPreference {
@@ -27,6 +29,6 @@ public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSens
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE);
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE));
}
}
@@ -1,6 +1,8 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
@@ -21,12 +23,12 @@ import androidx.preference.PreferenceDialogFragmentCompat;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.sensors.SensorType;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@@ -35,14 +37,18 @@ import de.dennisguse.opentracks.util.PermissionRequester;
* Preference to select a discoverable Bluetooth LE device.
* Based upon ListPreference.
*/
@SuppressLint("MissingPermission")
public abstract class BluetoothLeSensorPreference extends DialogPreference {
private static final String TAG = BluetoothLeSensorPreference.class.getSimpleName();
private static final String ARG_BLE_SERVICE_UUIDS = "bluetoothUUID";
private static final String ARG_INCLUDE_INTERNAL = "supportsInternal";
private static final int DEVICE_NONE_RESOURCEID = R.string.value_none;
private static final int SENSOR_INTERNAL_RESOURCEID = R.string.value_internal_sensor;
public BluetoothLeSensorPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@@ -62,11 +68,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
private String value;
private boolean valueSet = false;
public String getValue() {
return value;
}
public void setValue(String value) {
private void setValue(String value) {
final boolean changed = !TextUtils.equals(this.value, value);
if (changed || !valueSet) {
this.value = value;
@@ -85,11 +87,24 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
@Override
public CharSequence getSummary() {
if (getValue() == null || PreferencesUtils.isBluetoothSensorAddressNone(getValue())) {
if (value == null || SensorType.NONE.getPreferenceValue().equals(value)) {
return getContext().getString(DEVICE_NONE_RESOURCEID);
}
if (SensorType.INTERNAL.getPreferenceValue().equals(value)) {
return getContext().getString(SENSOR_INTERNAL_RESOURCEID);
}
return getValue();
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(getContext());
if (bluetoothAdapter == null) {
Log.w(TAG, "No Bluetooth adapter present");
return value;
}
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(value);
if (device != null && device.getName() != null) {
return getContext().getString(R.string.bluetooth_sensor_summary, device.getAddress(), device.getName());
}
return value;
}
public abstract PreferenceDialogFragmentCompat createInstance();
@@ -106,14 +121,12 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
@Override
public void onScanResult(int callbackType, ScanResult result) {
Log.d(TAG, "Found device " + result.getDevice().getName() + " " + result);
listAdapter.add(result.getDevice());
onBatchScanResults(List.of(result));
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
for (ScanResult result : results) {
onScanResult(-1, result);
}
listAdapter.addAll(results.stream().map(ScanResult::getDevice).collect(Collectors.toList()));
}
@Override
@@ -125,18 +138,20 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
}
};
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, ServiceMeasurementUUID sensorUUID) {
return newInstance(preferenceKey, Collections.singletonList(sensorUUID));
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, List<ServiceMeasurementUUID> sensorUUIDs) {
return newInstance(preferenceKey, sensorUUIDs, false);
}
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, List<ServiceMeasurementUUID> sensorUUIDs) {
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, List<ServiceMeasurementUUID> sensorUUIDs, boolean includeInternalSensor) {
final BluetoothLeSensorPreferenceDialog fragment = new BluetoothLeSensorPreferenceDialog();
final Bundle b = new Bundle(1);
final Bundle b = new Bundle(3);
b.putString(ARG_KEY, preferenceKey);
b.putParcelableArrayList(ARG_BLE_SERVICE_UUIDS, new ArrayList<>(sensorUUIDs.stream()
.map(ServiceMeasurementUUID::serviceUUID)
.map(ParcelUuid::new)
.collect(Collectors.toList())));
b.putBoolean(ARG_INCLUDE_INTERNAL, includeInternalSensor);
fragment.setArguments(b);
return fragment;
}
@@ -166,6 +181,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
private void startBluetoothScan() {
List<ParcelUuid> serviceUUIDs = getArguments().getParcelableArrayList(ARG_BLE_SERVICE_UUIDS);
boolean includeInternalSensor = getArguments().getBoolean(ARG_INCLUDE_INTERNAL);
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(getContext());
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
@@ -187,15 +203,21 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
return;
}
String deviceNone = getContext().getString(R.string.sensor_type_value_none);
listAdapter.add(getContext().getString(DEVICE_NONE_RESOURCEID), deviceNone);
listAdapter.add(SensorType.NONE.getPreferenceValue(), getContext().getString(DEVICE_NONE_RESOURCEID));
selectedEntryIndex = 0;
BluetoothLeSensorPreference preference = (BluetoothLeSensorPreference) getPreference();
String deviceSelected = preference.getValue();
if (deviceSelected != null && !deviceNone.equals(deviceSelected)) {
listAdapter.add(preference.getValue(), preference.getValue());
selectedEntryIndex = 1;
String deviceSelected = preference.value;
if (includeInternalSensor) {
listAdapter.add(SensorType.INTERNAL.getPreferenceValue(), getString(SENSOR_INTERNAL_RESOURCEID));
if (SensorType.INTERNAL.getPreferenceValue().equals(deviceSelected)) {
selectedEntryIndex = 1;
}
}
if (deviceSelected != null && SensorType.REMOTE.equals(PreferencesUtils.getSensorType(deviceSelected))) {
listAdapter.add(preference.value, preference.value);
selectedEntryIndex = !includeInternalSensor ? 1 : 2;
}
List<ScanFilter> scanFilter = null;
@@ -34,7 +34,7 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new IntervalStatisticsAdapter.ViewHolder(IntervalStatsListItemBinding.inflate(LayoutInflater.from(parent.getContext())));
return new IntervalStatisticsAdapter.ViewHolder(IntervalStatsListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
@@ -32,7 +32,7 @@ public class MarkerEditViewModel extends AndroidViewModel {
private MutableLiveData<Marker> markerData;
private boolean isNewMarker;
private Uri photoOriginalUri;
private final TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private final TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection((service, connection) -> {});
public MarkerEditViewModel(@NonNull Application application) {
super(application);
@@ -41,7 +41,7 @@ public class MarkerEditViewModel extends AndroidViewModel {
public LiveData<Marker> getMarkerData(@NonNull Track.Id trackId, @Nullable Marker.Id markerId) {
if (markerData == null) {
markerData = new MutableLiveData<>();
trackRecordingServiceConnection.startConnection(getApplication());
trackRecordingServiceConnection.bind(getApplication());
loadData(trackId, markerId);
}
return markerData;
@@ -119,7 +119,7 @@ public class MarkerListActivity extends AbstractActivity {
@Override
protected void onStart() {
super.onStart();
trackRecordingServiceConnection.startConnection(this);
trackRecordingServiceConnection.bind(this);
}
@Override
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.materialswitch.MaterialSwitch xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
+22 -21
View File
@@ -30,36 +30,36 @@ limitations under the License.
\n* funguje zcela offline
\n* zobrazit trasy na mapě (viz níže)</string>
<string name="help_track_map_title">Jak ukázat trasu na mapě\?</string>
<string name="help_track_map_content"> Použijte malou ikonu mapy.
\n a) (živé aktualizace) Nainstalujte <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Poskytuje online nebo offline mapy prostřednictvím <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
\n Ve skutečnosti je OSMDashboard samostatná aplikace, která má přístup (dočasně) pouze k datům stop, které se budou zobrazovat.
\n b) Prostřednictvím „normální“ mapové aplikace (např. <a href="http://osmand.net/">OsmAnd</a> a <a href="https://maps.me/">MAPS.ME</a>). Tyto aplikace musí podporovat formát souboru KMZ (pouze umístění a časová razítka). Ve skutečnosti musí aplikace podporovat <i>ACTION_VIEW</i> pomocí MIME <i>application/vnd.google-earth.kmz</i>. </string>
<string name="help_track_map_content"> \u0020Použijte malou ikonu mapy.
\n \u0020a) \u0020(živé aktualizace) Nainstalujte <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. \u0020Poskytuje online nebo offline mapy prostřednictvím <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
\n \u0020Ve skutečnosti je OSMDashboard samostatná aplikace, která má přístup (dočasně) pouze k datům stop, které se budou zobrazovat. \u0020
\n \u0020b) \u0020Prostřednictvím normální mapové aplikace (např. <a href="http://osmand.net/">OsmAnd</a> a <a href="https://maps.me/">MAPS.ME</a>). \u0020Tyto aplikace musí podporovat formát souboru KMZ (pouze umístění a časová razítka). \u0020Ve skutečnosti musí aplikace podporovat <i>ACTION_VIEW</i> pomocí MIME <i>application/vnd.google-earth.kmz</i>. \u0020</string>
<string name="help_storage_pictures_title">Jak ukládá OpenTracks obrázky pořízené jako značky na cestě\?</string>
<string name="help_storage_pictures_content">Jsou uloženy v interním úložišti aplikace (tj. přístupné pouze v rámci OpenTracks).</string>
<string name="help_track_export_title">Jak mohu exportovat/sdílet zaznamenané stopy\?</string>
<string name="help_track_export_content"> Trasy lze exportovat z Nastavení jako KMZ, KML nebo GPX. KMZ a KML obsahuje všechna data, GPX pouze polohu. Trasy sdílené pomocí <i>ikony sdílení</i> jsou sdíleny jako KMZ. </string>
<string name="help_track_export_content"> \u0020Trasy lze exportovat z Nastavení jako KMZ, KML nebo GPX. \u0020KMZ a KML obsahuje všechna data, GPX pouze polohu. \u0020Trasy sdílené pomocí <i>ikony sdílení</i> jsou sdíleny jako KMZ. \u0020</string>
<string name="help_data_google_play_services_title">Vyžaduje/používá OpenTracks službu Google Play\?</string>
<string name="help_recording_title">Jak OpenTracks zaznamenává data?</string>
<string name="help_recording_content">OpenTracks používá GPS senzor zařízení k nepřetržitému zaznamenávání polohy. Pokud zařízení GPS hlásí nepřesná data (např. polohu, rychlost, nadmořskou výšku), OpenTracks s tím nemůže nic udělat.</string>
<string name="help_data_storage_title">Jak OpenTracks ukládá data ze senzorů\?</string>
<string name="help_data_storage_content"> Data senzoru se ukládají na zaznamenané místo. Pokud jde o snímač srdeční frekvence, znamená to, že na každé místo je uloženo pouze <i> jedno </i> měření srdeční frekvence. Pokud stojíte v klidu nebo jste uvnitř, nebudou zaznamenána žádná data ze senzorů (ale budou zobrazena). </string>
<string name="help_data_storage_content"> \u0020Data senzoru se ukládají na zaznamenané místo. \u0020Pokud jde o snímač srdeční frekvence, znamená to, že na každé místo je uloženo pouze <i> jedno </i> měření srdeční frekvence. \u0020Pokud stojíte v klidu nebo jste uvnitř, nebudou zaznamenána žádná data ze senzorů (ale budou zobrazena). \u0020</string>
<string name="help_gps_troubleshooting_title">OpenTracks nezaznamenává místa?</string>
<string name="help_gps_troubleshooting_content"> Co můžete udělat:
\n a) zkontrolujte zda je zapnuta GPS,
\n b) zkuste zlepšit příjem GPS (např. vyjít ven),
\n c) zkontrolujte, zda používáte aplikaci <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest </a> a
\n d) deaktivujte úsporu energie.
\n
\n Pokud nic nepomůže, nahlaste to jako chybu. </string>
<string name="help_gps_troubleshooting_content"> \u0020Co můžete udělat:
\n \u0020a) zkontrolujte zda je zapnuta GPS,
\n \u0020b) zkuste zlepšit příjem GPS (např. vyjít ven),
\n \u0020c) zkontrolujte, zda používáte aplikaci <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest </a> a
\n \u0020d) deaktivujte úsporu energie.
\n \u0020
\n \u0020Pokud nic nepomůže, nahlaste to jako chybu. \u0020</string>
<string name="help_support_title">Jak mohu podpořit OpenTracks\?</string>
<string name="help_support_content"> Můžete:
\n a) dát vědět ostatním o OpenTracks
\n b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">navrhnout nové funkce</a>,
\n c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">nahlásit chybu</a>,
\n d) pomoci při vývoji aplikace,
\n e) zakoupit v <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Obchodě Google Play</a>,
\n f) zaslat dar či
\n g) udělat něco jiného, co si myslíte že pomůže OpenTracks. </string>
<string name="help_support_content"> \u0020Můžete:
\n \u0020a) dát vědět ostatním o OpenTracks
\n \u0020b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">navrhnout nové funkce</a>,
\n \u0020c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">nahlásit chybu</a>,
\n \u0020d) pomoci při vývoji aplikace,
\n \u0020e) zakoupit v <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Obchodě Google Play</a>,
\n \u0020f) zaslat dar či
\n \u0020g) udělat něco jiného, co si myslíte že pomůže OpenTracks. \u0020</string>
<!-- Activity Type -->
<string name="activity_type_airplane">letadlo</string>
<string name="activity_type_atv">terénní auta</string>
@@ -594,4 +594,5 @@ limitations under the License.
<string name="settings_recording_title">Záznam</string>
<string name="settings_ui_dynamic_colors_summary">Používat dynamické barvy definované uživatelem (pouze Android 12+; vyžaduje ruční restart)</string>
<string name="permission_recording_failed">Aplikace OpenTrack vyžaduje oprávnění k přístupu k vaší poloze a zařízením poblíž.</string>
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
</resources>
+3 -2
View File
@@ -37,14 +37,14 @@ limitations under the License.
<string name="help_storage_pictures_title">De que xeito garda OpenTracks as fotos tomadas como marcadores\?</string>
<string name="help_storage_pictures_content">Gárdanse na almacenaxe interna da app (só accesible desde OpenTracks).</string>
<string name="help_track_export_title">De que xeito exporto/importo as rutas gravadas\?</string>
<string name="help_track_export_content"> As rutas pódense exportar desde os Axustes como KMZ, KML ou GPX. KMZ/KML inclúe todos os datos; GPX só localización. As rutas compartidas a través da <i>icona compartir</i> compártense como KMZ. </string>
<string name="help_track_export_content"> \u0020As rutas pódense exportar desde os Axustes como KMZ, KML ou GPX. \u0020KMZ/KML inclúe todos os datos; GPX só localización. \u0020As rutas compartidas a través da <i>icona compartir</i> compártense como KMZ. \u0020</string>
<string name="help_data_google_play_services_title">OpenTracks precisa/utiliza Google Play Service\?</string>
<string name="help_recording_title">Como grava OpenTracks os datos\?</string>
<string name="help_recording_content">
OpenTracks usa o sensor GPS do dispositivo para gravar a localización de xeito contínuo.
Se o GPS informa sobre datos pouco precisos (ex.: localización, velocidade, elevación), OpenTracks non pode facer nada ao respecto.</string>
<string name="help_data_storage_title">De que xeito garda OpenTracks os datos dos sensores\?</string>
<string name="help_data_storage_content"> Os datos dos sensores gárdanse asociados á localización. Respecto do ritmo cardíaco, significa que só se garda <i>unha medida</i> asociada a unha localización. Se te detés ou estás en interiores, non se gardarán datos do sensor (mais serán mostrados). </string>
<string name="help_data_storage_content"> \u0020Os datos dos sensores gárdanse asociados á localización. \u0020 Respecto do ritmo cardíaco, significa que só se garda <i>unha medida</i> asociada a unha localización. \u0020 Se te detés ou estás en interiores, non se gardarán datos do sensor (mais serán mostrados). \u0020</string>
<string name="help_gps_troubleshooting_title">OpenTracks non grava as localizacións\?</string>
<string name="help_gps_troubleshooting_content"> Podes intentar:
\n a) mira que o GPS esté activado,
@@ -596,4 +596,5 @@ Se o GPS informa sobre datos pouco precisos (ex.: localización, velocidade, ele
<string name="settings_recording_title">Ao gravar</string>
<string name="settings_ui_dynamic_colors_summary">Usar Cores Dinámicas definidas pola usuaria (Só Android 12+; require reiniciar)</string>
<string name="permission_recording_failed">OpenTracks precisa permiso para acceder á túa localización así como a dispositivos próximos.</string>
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
</resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+10 -5
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,8 +13,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
--><resources>
<!-- Activity Type -->
<string name="activity_type_airplane">飛行機</string>
<string name="activity_type_atv">全地形対応車(ATV)</string>
@@ -195,7 +195,6 @@ limitations under the License.
<string name="settings_stats_units_imperial">ヤード・ポンド法(マイル、フィート)</string>
<string name="settings_stats_units_metric">メートル法(km、m)</string>
<string name="settings_stats_units_title">計測単位</string>
<!-- Share Track -->
<string name="share_track_share_file_body">次のトラックを見てみてください。\n\nトラックを表示するには、Google Earthか他のマッピングソフトウェアを使用してください。\n\n%1$s</string>
<string name="share_track_subject">トラック共有のお知らせ</string>
@@ -306,4 +305,10 @@ limitations under the License.
<string name="waypoint_type_waterfall">滝</string>
<string name="waypoint_type_water_fountain">噴水</string>
<string name="waypoint_type_waypoint">地点</string>
</resources>
<string name="help_feature_content">"* GPS トラックを作成します
\n * 心拍センサーを使用します (Bluetooth LE のみ)
\n * 音声アナウンス(時間または距離)
\n * マーカーの作成 (写真を含む)
\n * 完全にオフラインで動作します
\n * 地図上にトラックを表示します (下記を参照)"</string>
</resources>
@@ -1,19 +1,32 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="ThemeCustom" parent="Theme.Material3.DayNight.NoActionBar">
<style name="OpenTracksTheme" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Material3 tech documentation; https://github.com/material-components/material-components-android/blob/master/docs/components/ -->
<item name="colorPrimary">@color/opentracks</item>
<item name="colorSecondary">@color/opentracks</item>
<!-- Material3 https://github.com/material-components/material-components-android/issues/2828#issuecomment-1187563903 -->
<item name="switchPreferenceCompatStyle">@style/MaterialSwitchPreferenceCompat</item>
<item name="android:statusBarColor">@color/oled_friendly</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:navigationBarColor">@color/oled_friendly</item>
<item name="bottomAppBarStyle">@style/Widget.OpenTracks.BottomAppBar</item>
<item name="floatingActionButtonStyle">@style/Widget.OpenTracks.FloatingActionButton</item>
<item name="windowActionModeOverlay">true</item>
</style>
<style name="SplashTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_logo_color_splashscreen</item>
<item name="postSplashScreenTheme">@style/OpenTracksTheme</item>
</style>
<style name="Widget.OpenTracks.BottomAppBar" parent="Widget.Material3.BottomAppBar">
<item name="materialThemeOverlay">@style/ThemeOverlay.OpenTracks.BottomAppBar</item>
</style>
+22 -21
View File
@@ -30,38 +30,38 @@ limitations under the License.
\n * werkt volledig offline
\n * toon tracks op een kaart (zie hieronder)</string>
<string name="help_track_map_title">Hoe wordt een spoor op een kaart weergegeven\?</string>
<string name="help_track_map_content"> Gebruik het kleine kaartpictogram.
\n a) (live updates) Installeer <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Het biedt online of offline kaarten via <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
\n Eigenlijk is OSMDashboard een aparte app die alleen (tijdelijk) toegang heeft tot de gegevens van de track(s) die getoond gaan worden.
\n b) Via een normale kaartapplicatie (bijv. <a href="http://osmand.net/">OsmAnd</a> en <a href="https://maps.me/">MAPS.ME</a>). Deze apps moeten het KMZ-bestandsformaat ondersteunen (alleen locaties en tijdstempels). In feite moet een app <i>ACTION_VIEW</i> ondersteunen met MIME <i>application/vnd.google-earth.kmz</i>. </string>
<string name="help_track_map_content"> \u0020Gebruik het kleine kaartpictogram.
\n \u0020a) \u0020(live updates) Installeer <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Het biedt online of offline kaarten via <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
\n \u0020Eigenlijk is OSMDashboard een aparte app die alleen (tijdelijk) toegang heeft tot de gegevens van de track(s) die getoond gaan worden.
\n \u0020b) \u0020Via een normale kaartapplicatie (bijv. <a href="http://osmand.net/">OsmAnd</a> en <a href="https://maps.me/">MAPS.ME</a>). Deze apps moeten het KMZ-bestandsformaat ondersteunen (alleen locaties en tijdstempels). In feite moet een app <i>ACTION_VIEW</i> ondersteunen met MIME <i>application/vnd.google-earth.kmz</i>. \u0020</string>
<string name="help_storage_pictures_title">Hoe slaat OpenTracks foto\'s op die als markeringen zijn genomen\?</string>
<string name="help_storage_pictures_content">Deze worden opgeslagen in de interne opslag van de app (d.w.z. alleen toegankelijk binnen OpenTracks).</string>
<string name="help_track_export_title">Hoe kan ik opgenomen tracks exporteren/delen?</string>
<string name="help_track_export_content"> Tracks kunnen worden geëxporteerd vanuit de instellingen als KMZ, KML of GPX. KMZ/KML alle gegevens; GPX alleen locaties. Tracks die worden gedeeld via het <i>share-icoontje</i> worden gedeeld als KMZ </string>
<string name="help_track_export_content"> \u0020Tracks kunnen worden geëxporteerd vanuit de instellingen als KMZ, KML of GPX. KMZ/KML alle gegevens; GPX alleen locaties. \u0020Tracks die worden gedeeld via het <i>share-icoontje</i> worden gedeeld als KMZ \u0020</string>
<string name="help_data_google_play_services_title">Vereist/gebruikt OpenTracks Google Play Service\?</string>
<string name="help_recording_title">Hoe legt OpenTracks gegevens vast?</string>
<string name="help_recording_content">
OpenTracks gebruikt de GPS-sensor van het apparaat om de locatie continu te registreren.
Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid, hoogte), kan OpenTracks daar niets aan doen.</string>
<string name="help_data_storage_title">Hoe slaat OpenTracks sensorgegevens op?</string>
<string name="help_data_storage_content"> De sensorgegevens worden per opgenomen locatie opgeslagen. In termen van een hartslagsensor betekent dit dat slechts <i>een</i> hartslagmeting per locatie wordt opgeslagen. Als u stilstaat of binnenshuis bent, worden er geen sensorgegevens geregistreerd (maar wel getoond) </string>
<string name="help_data_storage_content"> \u0020De sensorgegevens worden per opgenomen locatie opgeslagen. In termen van een hartslagsensor betekent dit dat slechts <i>een</i> hartslagmeting per locatie wordt opgeslagen. Als u stilstaat of binnenshuis bent, worden er geen sensorgegevens geregistreerd (maar wel getoond) \u0020</string>
<string name="help_gps_troubleshooting_title">OpenTracks neemt geen locaties op?</string>
<string name="help_gps_troubleshooting_content"> Wat u kunt doen:
\n a) Controleer of GPS is ingeschakeld,
\n b) verbeter de GPS-ontvangst (ga bijvoorbeeld naar buiten),
\n c) controleren met <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> app, en
\n d) schakel energiebesparing uit.
\n
\n Als alles niet helpt, rapporteer dan een mogelijke fout. </string>
<string name="help_gps_troubleshooting_content"> \u0020Wat u kunt doen:
\n \u0020a) Controleer of GPS is ingeschakeld,
\n \u0020b) verbeter de GPS-ontvangst (ga bijvoorbeeld naar buiten),
\n \u0020c) controleren met <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> app, en
\n \u0020d) schakel energiebesparing uit.
\n
\n \u0020Als alles niet helpt, rapporteer dan een mogelijke fout. \u0020</string>
<string name="help_support_title">Hoe kan ik OpenTracks ondersteunen?</string>
<string name="help_support_content"> U kunt:
\n a) anderen vertellen over OpenTracks,
\n b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">functies voorstellen</a>,
\n c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">bugs melden</a>,
\n d) helpen bij de ontwikkeling van software,
\n e) aanschaffen in <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>,
\n f) doneren, en
\n g) wat u ook denkt dat OpenTracks helpt. </string>
<string name="help_support_content"> \u0020U kunt:
\n \u0020a) anderen vertellen over OpenTracks,
\n \u0020b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">functies voorstellen</a>,
\n \u0020c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">bugs melden</a>,
\n \u0020d) helpen bij de ontwikkeling van software,
\n \u0020e) aanschaffen in <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>,
\n \u0020f) doneren, en
\n \u0020g) wat u ook denkt dat OpenTracks helpt. \u0020</string>
<!-- Activity Type -->
<string name="activity_type_airplane">vliegtuig</string>
<string name="activity_type_atv">quad</string>
@@ -595,4 +595,5 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
<string name="sensor_state_heart_rate_unit">/min</string>
<string name="sensor_state_cadence_unit">/min</string>
<string name="voice_per_kilometer">/km</string>
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
</resources>
+2
View File
@@ -32,7 +32,9 @@
<string name="settings_sensor_bluetooth_cycling_speed_key" translatable="false">bluetoothCyclingSpeedSensor</string>
<string name="settings_sensor_bluetooth_cycling_power_key" translatable="false">bluetoothCyclingPowerSensor</string>
<string name="settings_sensor_bluetooth_running_speed_and_cadence_key" translatable="false">bluetoothRunningSpeedAndCadenceSensor</string>
<string name="settings_sensor_bluetooth_pressure_key" translatable="false">bluetoothPressureSensor</string>
<string name="sensor_type_value_none" translatable="false">NONE</string>
<string name="sensor_type_value_internal" translatable="false">INTERNAL</string>
<string name="settings_sensor_bluetooth_cycling_speed_wheel_circumference_key" translatable="false">bluetoothCyclingSpeedWheelCircumference</string>
<string name="settings_sensor_bluetooth_cycling_speed_wheel_circumference_default" translatable="false">2135</string>
+3
View File
@@ -331,11 +331,13 @@ limitations under the License.
<string name="sensor_state_heart_rate_unit">bpm</string>
<string name="sensor_state_heart_rate_value">%1$d bpm</string>
<string name="sensor_state_power">Power</string>
<string name="sensor_state_pressure">Barometer</string>
<string name="sensor_state_power_avg">Avg Power</string>
<string name="sensor_state_power_max">Max Power</string>
<string name="sensor_state_power_unit">W</string>
<string name="sensor_could_not_scan">Could not scan for Bluetooth devices (error %1$i).</string>
<string name="bluetooth_disabled">Please enable Bluetooth.</string>
<string name="bluetooth_sensor_summary">%1$s (%2$s)</string>
<string name="settings_open_tracks_summary">All About OpenTracks Project</string>
@@ -558,6 +560,7 @@ limitations under the License.
<string name="value_integer_second">%1$d s</string>
<string name="value_none">None</string>
<string name="value_off">Off</string>
<string name="value_internal_sensor">Internal sensor</string>
<string name="value_smallest_recommended">Smallest (recommended)</string>
<string name="value_int_seconds">%1$d s (recommended)</string>
<string name="voiceIdle">Becoming idle.</string>
+7
View File
@@ -93,4 +93,11 @@ limitations under the License.
<item name="colorOnSurface">@android:color/holo_red_dark</item>
</style>
<style name="Widget.OpenTracks.FloatingActionButton" parent="Widget.MaterialComponents.FloatingActionButton">
<item name="fabCustomSize">64dp</item>
</style>
<style name="MaterialSwitchPreferenceCompat" parent="@style/Preference.SwitchPreferenceCompat.Material">
<item name="widgetLayout">@layout/view_preference_switch</item>
</style>
</resources>
@@ -1,24 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="ThemeCustom" parent="Theme.Material3.DayNight.NoActionBar">
<style name="OpenTracksTheme" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/opentracks</item>
<item name="colorSecondary">@color/opentracks</item>
<!-- Material3 https://github.com/material-components/material-components-android/issues/2828#issuecomment-1187563903 -->
<item name="switchPreferenceCompatStyle">@style/MaterialSwitchPreferenceCompat</item>
<item name="android:statusBarColor">?attr/colorSurfaceBright</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:navigationBarColor">?attr/colorSurfaceContainer</item>
@@ -35,18 +24,11 @@ limitations under the License.
</style>
<style name="SplashTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_logo_color_splashscreen</item>
<item name="postSplashScreenTheme">@style/ThemeCustom</item>
<item name="postSplashScreenTheme">@style/OpenTracksTheme</item>
</style>
<style name="Widget.OpenTracks.BottomAppBar" parent="Widget.Material3.BottomAppBar">
<item name="backgroundTint">?attr/colorSurfaceContainer</item>
</style>
<style name="Widget.OpenTracks.FloatingActionButton" parent="Widget.MaterialComponents.FloatingActionButton">
<item name="fabCustomSize">64dp</item>
</style>
</resources>
+4
View File
@@ -15,6 +15,10 @@
android:key="@string/settings_sensor_heart_rate_max_key"
android:title="@string/settings_sensor_heart_rate_max"
app:useSimpleSummaryProvider="true" />
<de.dennisguse.opentracks.settings.bluetooth.BluetoothLePressurePreference
android:defaultValue="@string/sensor_type_value_none"
android:key="@string/settings_sensor_bluetooth_pressure_key"
android:title="@string/sensor_state_pressure" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_cycling_sensor">