forked from upstream-mirrors/OpenTracks
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f143aabfbf | |||
| d58342233c | |||
| 91c254ba7f | |||
| 19edb47554 | |||
| 1129c96562 | |||
| 7224efa8a9 | |||
| 149f8f809e | |||
| c2f59c11bf | |||
| 8b7d4fa813 | |||
| 81081b0ee5 | |||
| def4f3a1b9 | |||
| b679f00cc5 | |||
| 40494557d4 | |||
| a94f023a17 | |||
| ee105a1610 | |||
| 3bec73e074 | |||
| 1155a56e49 | |||
| 9b7e858d1d | |||
| d1490fc343 | |||
| 3f6a4975fa | |||
| b5408eabcd | |||
| e1e34452ae | |||
| df121b40d7 | |||
| 4e737616cb | |||
| fb963781af | |||
| 96339b6a36 | |||
| d043842901 | |||
| cefb6b3abf | |||
| 3ee6d43e3f | |||
| 2edd9a1f05 | |||
| f9a4fcfa0d | |||
| 6bed9c5ddf | |||
| 4e400c60f6 | |||
| ca6f973360 | |||
| edb0e7f7bb | |||
| 876b013c2e | |||
| 7fb27cbe95 | |||
| 6d519e2fc1 | |||
| c2f3b50767 | |||
| a95c6a1857 | |||
| f45dad167b | |||
| 88c75d31a8 | |||
| d49e7749f3 | |||
| 8d3e953220 | |||
| 6566904906 | |||
| 1d0e8f59b7 | |||
| be7987f88a | |||
| a92a1e7838 | |||
| 8cba53cfe4 | |||
| fcedb9c444 | |||
| 2ec82af8f1 | |||
| 447d1bb7a3 | |||
| 2f6765e45f | |||
| 90bf2e6ab0 | |||
| d12423522e | |||
| 24f052491b | |||
| 6b4e7d7d9b | |||
| 00fcbb070a | |||
| 708cbf0b2a | |||
| 60e2fe57f9 | |||
| 14cebf3fec | |||
| b651a8ca6a | |||
| d75472ef11 | |||
| 0b849c3d55 | |||
| 1c767c41a7 | |||
| dfa80d61d7 | |||
| fa558c9ab7 | |||
| a1a363698f | |||
| b79acfb942 | |||
| 24ea718ff2 | |||
| 6d187fa66d | |||
| ea27db13fa | |||
| 29d2e7c78c | |||
| 4b7badbecc | |||
| 0dcb8010e2 | |||
| 0a9d4b207c | |||
| c6cb388ce4 | |||
| e6e009ba14 | |||
| f8aa456549 | |||
| 167d90332e | |||
| 719a052f34 | |||
| 244ca26210 | |||
| 476f3a517b | |||
| 289e93eb5d | |||
| 18ff96733e | |||
| 0f2c5adf26 | |||
| 74954df600 | |||
| 1a14fed287 | |||
| 744853ee91 | |||
| 597e1b2c7f | |||
| 8156993921 | |||
| 9890dc28bd | |||
| b65d83010d | |||
| 1a5d06f9c6 | |||
| 723c21d7ec | |||
| 8fe0c3be7d | |||
| e64792bf3f | |||
| 21bafff761 | |||
| c33c2de51d |
@@ -112,3 +112,15 @@ Here is a list of the smartphones tested:
|
||||
| Google | Nexus 4 | LineageOS 18.1 | Ok |
|
||||
| Samsung | S4 | Android 11 | Ok |
|
||||
| Sony | Xperia XZ1 Compact | LineageOS 16.1 | Failure |
|
||||
|
||||
## Bosch Smart System eBike
|
||||
|
||||
OpenTracks also supports Bosch Smart System eBikes via Bosch's custom BLE service `00000010-eaa2-11e9-81b4-2a2ae2dbcce4`.
|
||||
|
||||
Tested with:
|
||||
|
||||
* Bosch Perfomance Line CX (SW-Version 16.9.0) / Kiox 300 (SW-Version 16.4.0)
|
||||
|
||||
Currently supported measurements:
|
||||
|
||||
* Human power
|
||||
+17
-12
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:9.0.0'
|
||||
classpath 'com.android.tools.build:gradle:9.2.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ allprojects {
|
||||
}
|
||||
|
||||
def getVersionName = { ->
|
||||
return 'git describe --tags'.execute().text.trim()
|
||||
return 'git describe --tags'.execute(null, project.rootDir).text.trim()
|
||||
}
|
||||
|
||||
def getVersionCodeNightly = { ->
|
||||
return Integer.valueOf('git rev-list HEAD --count'.execute().text.trim())
|
||||
return Integer.valueOf('git rev-list HEAD --count'.execute(null, project.rootDir).text.trim())
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -47,8 +47,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'de.dennisguse.opentracks'
|
||||
versionCode 6554
|
||||
versionName "v4.25.1"
|
||||
versionCode 6653
|
||||
versionName "v4.27.0"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -107,7 +107,6 @@ android {
|
||||
nightly {
|
||||
dimension 'version'
|
||||
applicationId 'de.dennisguse.opentracks.nightly'
|
||||
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.nightly'
|
||||
|
||||
versionCode getVersionCodeNightly()
|
||||
versionName getVersionName()
|
||||
@@ -119,7 +118,6 @@ android {
|
||||
// Non-reproducible: https://f-droid.org/de/packages/de.dennisguse.opentracks/
|
||||
dimension 'version'
|
||||
applicationId 'de.dennisguse.opentracks'
|
||||
resValue 'string', 'applicationId', 'de.dennisguse.opentracks'
|
||||
versionNameSuffix 'irreproducible'
|
||||
}
|
||||
reproducible {
|
||||
@@ -128,7 +126,6 @@ android {
|
||||
// PlayStore: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
|
||||
dimension 'version'
|
||||
applicationId 'de.dennisguse.opentracks.playstore'
|
||||
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.playstore'
|
||||
|
||||
base.archivesName = "de.dennisguse.opentracks.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
|
||||
|
||||
@@ -154,18 +151,26 @@ dependencies {
|
||||
implementation 'androidx.gridlayout:gridlayout:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.13.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'androidx.core:core:1.17.0'
|
||||
implementation 'androidx.core:core:1.18.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.2.0'
|
||||
implementation 'androidx.mediarouter:mediarouter:1.8.1'
|
||||
implementation 'androidx.core:core-location-altitude:1.0.0-beta01'
|
||||
implementation 'androidx.work:work-runtime:2.11.1'
|
||||
implementation 'androidx.core:core-location-altitude:1.0.0-rc01'
|
||||
implementation 'androidx.work:work-runtime:2.11.2'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.7.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.7.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.7.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:5.21.0'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.6.1'
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().all()) { variant ->
|
||||
variant.resValues.put(
|
||||
new com.android.build.api.variant.impl.ResValueKeyImpl("string", "applicationId"),
|
||||
new com.android.build.api.variant.ResValue(variant.applicationId.get(), null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
(versionName): OpenTracks
|
||||
|
||||
Ændringer:
|
||||
-
|
||||
|
||||
Fejlrettelser:
|
||||
-
|
||||
|
||||
Udvikler:
|
||||
-
|
||||
@@ -0,0 +1,8 @@
|
||||
v4.25.2: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Restored compatibility with OSMDashboard (Dashboard API protocol version 2)
|
||||
|
||||
Bugfix:
|
||||
- Update UI immediately after resuming a track
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
v4.26.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- GPS is optional: configurable in the settings
|
||||
- BLE sensor address/name is only shown after successful connection
|
||||
|
||||
Bugfix:
|
||||
- Altitude is now shown again in EGM2008 (introduced in v4.25.0)
|
||||
- Altitude and speed were shown as 0.0 briefly when data was stored
|
||||
|
||||
Developer:
|
||||
- Refactored sensor subsystem
|
||||
@@ -0,0 +1,6 @@
|
||||
v4.26.1: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Crash when deleting a UI layout
|
||||
- Crash when heartrate sensor data could not be decoded
|
||||
- Crash when exporting zero tracks
|
||||
@@ -0,0 +1,5 @@
|
||||
v4.26.2: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- GPS via Bluetooth is not (yet) supported
|
||||
- Altitude type (EGM2008) wasn't shown
|
||||
@@ -0,0 +1,14 @@
|
||||
v4.27.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Sensor: power from Bosch eBike
|
||||
- Sensors: include already bonded Bluetooth devices (like Bosch eBike)
|
||||
- Barometer: enforce sampling frequency
|
||||
|
||||
Bugfix:
|
||||
- Dashboard API: did not provide update
|
||||
- GPS could not be enabled with Bluetooth disabled
|
||||
|
||||
Developer:
|
||||
- Removed Mockito
|
||||
- UI tests: less flacky
|
||||
@@ -0,0 +1,5 @@
|
||||
v4.26.2: OpenTracks
|
||||
|
||||
Arranxos:
|
||||
- (aínda) non hai compatibilidade co GPS vía Bluetooth
|
||||
- non se mostraba a altitude tipo (EGM2008)
|
||||
@@ -0,0 +1,22 @@
|
||||
Un tracker sportivo che rispetta la tua privacy.
|
||||
|
||||
Sport e attività all'aria aperta portano a felicità, allenamento e fiducia
|
||||
|
||||
Valorizza la tua salute tenendo traccia dei tuoi progressi.
|
||||
Registra quando stai correndo o passeggiando e regalati un monitor più grande quando fai ciclismo.
|
||||
Segna con fotografie i posti interessanti incrociati durante il tragitto.
|
||||
Tieni traccia delle statistiche in dettaglio per analizzarle.
|
||||
Condividi solo i dati che vuoi.
|
||||
|
||||
* Annunci vocali.
|
||||
* Supporto a sensori Bluetooth LE: battito cardiaco, velocità e distanza (ciclismo), cadenza (ciclismo), rilevamento potenza (ciclismo).
|
||||
* Altitudine positiva o negativa tramite sensore barometrico.
|
||||
* Altitudine mostrata in EGM2008 (sopra il livello del mare) ed esportata come WGS84.
|
||||
* Esporta tracce come KMZ (con foto incluse), KML o GPX.
|
||||
* Non richiede accesso a internet o altri permessi.
|
||||
* Temi chiaro e scuro, o seguendo la impostazioni di sistema.
|
||||
* Nessuna pubblicità.
|
||||
|
||||
Libre software / Free Software / Open Source
|
||||
cioè puoi usare, studiare, cambiare e condividere il codice sorgente.
|
||||
Licenza: Apache 2.0
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -2,71 +2,117 @@ package de.dennisguse.opentracks;
|
||||
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParentIndex;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
|
||||
import static androidx.test.espresso.matcher.RootMatchers.isPlatformPopup;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.performClick;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.performClickOnAncestor;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.performLongClickOnAncestor;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilRecyclerViewItemCount;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.withRecyclerViewItemCount;
|
||||
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
|
||||
import static de.dennisguse.opentracks.util.TrackUiTestUtils.renameStoppedTrack;
|
||||
import static de.dennisguse.opentracks.util.TrackUiTestUtils.startRecordingFromTrackList;
|
||||
import static de.dennisguse.opentracks.util.TrackUiTestUtils.stopRecordingToTrackStopped;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.childAtPosition;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
||||
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class EspressoDeleteTrackTest {
|
||||
|
||||
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||
|
||||
@Test
|
||||
public void espressoDeleteTrackTest() {
|
||||
// TrackListActivity: start recording
|
||||
onView(withId(R.id.track_list_fab_action)).perform(click());
|
||||
|
||||
// TrackRecordingActivity
|
||||
onView(withId(R.id.track_recording_fab_action))
|
||||
// wait; stay recording
|
||||
.perform(waitFor(5000))
|
||||
// stop;
|
||||
.perform(longClick());
|
||||
|
||||
// TrackStoppedActivity
|
||||
onView(withId(R.id.finish_button)).perform(click());
|
||||
|
||||
// select track
|
||||
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0))).perform(longClick());
|
||||
|
||||
// open menu and delete selected track
|
||||
onView(
|
||||
allOf(withContentDescription("More options"),
|
||||
childAtPosition(
|
||||
childAtPosition(
|
||||
withId(androidx.appcompat.R.id.action_mode_bar),
|
||||
1),
|
||||
2),
|
||||
isDisplayed()))
|
||||
.perform(click());
|
||||
|
||||
onView(withText("Delete")).perform(click());
|
||||
|
||||
onView(withText("OK")).perform(click());
|
||||
|
||||
// tracklist is empty now
|
||||
onView(allOf(withText("Start recording your next adventure here"), isDisplayed()));
|
||||
@Before
|
||||
public void setUp() {
|
||||
resetTrackRecordingServiceAndDeleteTracks();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteTrackFromTrackStoppedActivity() {
|
||||
recordTrackAndOpenTrackStoppedActivity();
|
||||
|
||||
// TrackStoppedActivity: discard the just-recorded track directly
|
||||
onView(withId(R.id.discard_button))
|
||||
.perform(performClick());
|
||||
onView(withText(android.R.string.ok))
|
||||
.inRoot(isDialog())
|
||||
.perform(performClick());
|
||||
waitUntilDisplayed(withId(R.id.track_list), UI_TIMEOUT);
|
||||
waitUntilRecyclerViewItemCount(withId(R.id.track_list), 0, UI_TIMEOUT);
|
||||
|
||||
// the recycler is empty again after discarding the only recorded track
|
||||
onView(withId(R.id.track_list))
|
||||
.check(matches(withRecyclerViewItemCount(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteTrackFromSelectedTrackInTrackList() {
|
||||
String uniqueTrackName = "DeleteFromTrackList-" + System.currentTimeMillis();
|
||||
|
||||
recordTrackAndOpenTrackStoppedActivity();
|
||||
renameStoppedTrack(uniqueTrackName, UI_TIMEOUT);
|
||||
|
||||
onView(withId(R.id.finish_button))
|
||||
.perform(performClick());
|
||||
waitUntilDisplayed(withId(R.id.track_list), UI_TIMEOUT);
|
||||
waitUntilDisplayed(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)), UI_TIMEOUT);
|
||||
waitUntilRecyclerViewItemCount(withId(R.id.track_list), 1, UI_TIMEOUT);
|
||||
|
||||
onView(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)))
|
||||
.perform(performLongClickOnAncestor());
|
||||
|
||||
clickDeleteSelectedTrackAction();
|
||||
onView(withText(android.R.string.ok))
|
||||
.inRoot(isDialog())
|
||||
.perform(performClick());
|
||||
waitUntilRecyclerViewItemCount(withId(R.id.track_list), 0, UI_TIMEOUT);
|
||||
|
||||
onView(withId(R.id.track_list))
|
||||
.check(matches(withRecyclerViewItemCount(0)));
|
||||
}
|
||||
|
||||
private void recordTrackAndOpenTrackStoppedActivity() {
|
||||
startRecordingFromTrackList(UI_TIMEOUT);
|
||||
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.discard_button, R.id.finish_button);
|
||||
}
|
||||
|
||||
private void clickDeleteSelectedTrackAction() {
|
||||
try {
|
||||
waitUntilDisplayed(withId(R.id.list_context_menu_delete), UI_TIMEOUT);
|
||||
onView(withId(R.id.list_context_menu_delete))
|
||||
.perform(performClick());
|
||||
} catch (AssertionError ignored) {
|
||||
onView(allOf(
|
||||
withContentDescription(androidx.appcompat.R.string.abc_action_menu_overflow_description),
|
||||
isDescendantOfA(withId(androidx.appcompat.R.id.action_mode_bar))))
|
||||
.perform(performClick());
|
||||
onView(withText(R.string.menu_delete))
|
||||
.inRoot(isPlatformPopup())
|
||||
.perform(performClickOnAncestor());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,35 +2,43 @@ package de.dennisguse.opentracks;
|
||||
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.RootMatchers.isPlatformPopup;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.performClick;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.performClickOnAncestor;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
|
||||
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
|
||||
import static de.dennisguse.opentracks.util.TrackUiTestUtils.startRecordingFromTrackList;
|
||||
import static de.dennisguse.opentracks.util.TrackUiTestUtils.stopRecordingToTrackStopped;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
||||
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class EspressoEditTrackRecordingTest {
|
||||
|
||||
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
@Rule
|
||||
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
|
||||
|
||||
@@ -40,46 +48,48 @@ public class EspressoEditTrackRecordingTest {
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
resetTrackRecordingServiceAndDeleteTracks();
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void espressoEditTrackRecordingTest() {
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction fabRecordButton = onView(withId(R.id.track_list_fab_action));
|
||||
fabRecordButton.perform(click());
|
||||
}
|
||||
startRecordingFromTrackList(UI_TIMEOUT);
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
|
||||
|
||||
// wait; stay recording
|
||||
trackControllerStopButton.perform(waitFor(4000));
|
||||
|
||||
// open menu
|
||||
openContextualActionModeOverflowMenu();
|
||||
|
||||
// Click the item.
|
||||
onView(withText(R.string.menu_edit)).perform(click());
|
||||
onView(allOf(
|
||||
withContentDescription(androidx.appcompat.R.string.abc_action_menu_overflow_description),
|
||||
isDescendantOfA(withId(R.id.bottom_app_bar))))
|
||||
.perform(performClick());
|
||||
onView(withText(R.string.menu_edit))
|
||||
.inRoot(isPlatformPopup())
|
||||
.perform(performClickOnAncestor());
|
||||
waitUntilDisplayed(withId(R.id.track_edit_name), UI_TIMEOUT);
|
||||
|
||||
// change name for "New Name"
|
||||
ViewInteraction textInputEditText = onView(withId(R.id.track_edit_name));
|
||||
textInputEditText.perform(scrollTo(), replaceText("New Name"));
|
||||
onView(withId(R.id.track_edit_name))
|
||||
.perform(scrollTo(), replaceText("New Name"));
|
||||
|
||||
ViewInteraction textInputEditText2 = onView(allOf(withId(R.id.track_edit_name), withText("New Name"), isDisplayed()));
|
||||
textInputEditText2.perform(closeSoftKeyboard());
|
||||
onView(allOf(withId(R.id.track_edit_name), withText("New Name"), isDisplayed()))
|
||||
.perform(closeSoftKeyboard());
|
||||
|
||||
// save edition
|
||||
ViewInteraction appCompatButton = onView(withId(R.id.track_edit_save));
|
||||
appCompatButton.perform(click());
|
||||
onView(withId(R.id.track_edit_save))
|
||||
.perform(performClick());
|
||||
waitUntilDisplayed(withId(R.id.track_recording_fab_action), UI_TIMEOUT);
|
||||
|
||||
// stop;
|
||||
trackControllerStopButton.perform(longClick());
|
||||
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button, R.id.finish_button);
|
||||
|
||||
// it's on track stopped activity and there are two buttons
|
||||
ViewInteraction resumeButton = onView(withId(R.id.resume_button));
|
||||
resumeButton.check(matches(isDisplayed()));
|
||||
ViewInteraction finishButton = onView(withId(R.id.finish_button));
|
||||
finishButton.check(matches(isDisplayed()));
|
||||
onView(withId(R.id.resume_button))
|
||||
.check(matches(isDisplayed()));
|
||||
onView(withId(R.id.finish_button))
|
||||
.check(matches(isDisplayed()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +1,142 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParentIndex;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.performLongClickOnAncestor;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.selectTabAtIndex;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitForResumedViewCondition;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.withSelectedTab;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.withViewPagerCurrentItem;
|
||||
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
|
||||
import static de.dennisguse.opentracks.util.TrackUiTestUtils.finishStoppedTrack;
|
||||
import static de.dennisguse.opentracks.util.TrackUiTestUtils.resumeStoppedTrack;
|
||||
import static de.dennisguse.opentracks.util.TrackUiTestUtils.startRecordingFromTrackList;
|
||||
import static de.dennisguse.opentracks.util.TrackUiTestUtils.stopRecordingToTrackStopped;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.RuleChain;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackBuilder;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class EspressoUITest {
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
private final ActivityScenarioRule<TrackListActivity> activityRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||
private final GrantPermissionRule grantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||
public RuleChain rules = RuleChain.outerRule(grantPermissionRule)
|
||||
.around(activityRule);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
resetTrackRecordingServiceAndDeleteTracks();
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void record_stop_resume_stop_finish() {
|
||||
startRecordingFromTrackList(UI_TIMEOUT);
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
{
|
||||
// TrackRecordingActivity: wait to record some time and then stop
|
||||
onView(withId(R.id.track_recording_fab_action))
|
||||
.perform(waitFor(5000))
|
||||
.perform(longClick());
|
||||
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button);
|
||||
resumeStoppedTrack(UI_TIMEOUT);
|
||||
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.finish_button);
|
||||
finishStoppedTrack(UI_TIMEOUT);
|
||||
|
||||
// TrackStoppedActivity: resume
|
||||
onView(allOf(withId(R.id.resume_button), isClickable()))
|
||||
.perform(click());
|
||||
|
||||
// TrackRecordingActivity: wait and then stop
|
||||
onView(withId(R.id.track_recording_fab_action))
|
||||
.perform(waitFor(5000))
|
||||
.perform(longClick());
|
||||
|
||||
// TrackStoppedActivity
|
||||
onView(withId(R.id.finish_button))
|
||||
.perform(click());
|
||||
onView(withId(R.id.track_list))
|
||||
.check(matches(isDisplayed()));
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void record_move_through_tabs() {
|
||||
{
|
||||
// TrackListActivity: start recording
|
||||
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
|
||||
trackControllerRecordButton.perform(click());
|
||||
}
|
||||
startRecordingFromTrackList(UI_TIMEOUT);
|
||||
waitUntilDisplayed(withId(R.id.track_detail_activity_tablayout), UI_TIMEOUT);
|
||||
waitUntilDisplayed(withId(R.id.track_detail_activity_view_pager), UI_TIMEOUT);
|
||||
{
|
||||
// TrackRecordingActivity
|
||||
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
|
||||
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
|
||||
|
||||
tabLayout.perform(selectTabAtIndex(1));
|
||||
tabLayout.perform(waitFor(1000));
|
||||
waitUntilTabSelectionSettlesAt(1);
|
||||
|
||||
tabLayout.perform(selectTabAtIndex(2));
|
||||
tabLayout.perform(waitFor(1000));
|
||||
waitUntilTabSelectionSettlesAt(2);
|
||||
|
||||
tabLayout.perform(selectTabAtIndex(3));
|
||||
tabLayout.perform(waitFor(1000));
|
||||
waitUntilTabSelectionSettlesAt(3);
|
||||
|
||||
tabLayout.perform(selectTabAtIndex(0));
|
||||
tabLayout.perform(waitFor(1000));
|
||||
waitUntilTabSelectionSettlesAt(0);
|
||||
|
||||
// stop
|
||||
trackControllerStopButton.perform(longClick());
|
||||
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button, R.id.finish_button);
|
||||
|
||||
onView(withId(R.id.resume_button))
|
||||
.check(matches(isDisplayed()));
|
||||
onView(withId(R.id.finish_button))
|
||||
.check(matches(isDisplayed()));
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void selectAndDeleteTrack() {
|
||||
Pair<Track, List<TrackPoint>> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 5);
|
||||
String uniqueTrackName = "SelectAndDeleteTrack-" + System.currentTimeMillis();
|
||||
TrackBuilder trackBuilder = new TrackBuilder(pair.first);
|
||||
trackBuilder.setName(uniqueTrackName);
|
||||
TestDataUtil.insertTrackWithLocations(new ContentProviderUtils(ApplicationProvider.getApplicationContext()), trackBuilder.getTrack(), pair.second);
|
||||
activityRule.getScenario().recreate();
|
||||
waitUntilDisplayed(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)), UI_TIMEOUT);
|
||||
|
||||
onView(withId(R.id.track_list)).check(matches(isDisplayed()));
|
||||
onView(allOf(withParent(withId(R.id.track_list)), withParentIndex(0))).perform(longClick());
|
||||
onView(allOf(withId(R.id.track_list_item_name), withText(uniqueTrackName)))
|
||||
.perform(performLongClickOnAncestor());
|
||||
|
||||
onView(withId(androidx.appcompat.R.id.action_mode_bar))
|
||||
.check(matches(isDisplayed()));
|
||||
}
|
||||
|
||||
private void waitUntilTabSelectionSettlesAt(int index) {
|
||||
waitForResumedViewCondition(
|
||||
withId(R.id.track_detail_activity_tablayout),
|
||||
view -> withSelectedTab(index).matches(view),
|
||||
"selected tab to be " + index,
|
||||
UI_TIMEOUT);
|
||||
waitForResumedViewCondition(
|
||||
withId(R.id.track_detail_activity_view_pager),
|
||||
view -> withViewPagerCurrentItem(index).matches(view),
|
||||
"ViewPager current item to be " + index,
|
||||
UI_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
+62
-46
@@ -20,22 +20,19 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -78,7 +75,6 @@ import de.dennisguse.opentracks.util.FileUtils;
|
||||
* @author Bartlomiej Niechwiej
|
||||
* @author Youtao Liu
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class CustomContentProviderUtilsTest {
|
||||
|
||||
private static final String NAME_PREFIX = "test name";
|
||||
@@ -90,12 +86,6 @@ public class CustomContentProviderUtilsTest {
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
@Mock
|
||||
private Cursor cursorMock;
|
||||
|
||||
@Mock
|
||||
private ContentResolver contentResolverMock;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
@@ -104,21 +94,21 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_noPoints() {
|
||||
testIterator(new Track.Id(1), 0);
|
||||
assertEquals(0, testIterator(new Track.Id(1), 0).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_noAscending() {
|
||||
testIterator(new Track.Id(1), 50);
|
||||
testIterator(new Track.Id(2), 50);
|
||||
assertEquals(50, testIterator(new Track.Id(1), 50).size());
|
||||
assertEquals(50, testIterator(new Track.Id(2), 50).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_largeTrack() {
|
||||
testIterator(new Track.Id(1), 20000 / 2);
|
||||
assertEquals(20000 / 2, testIterator(new Track.Id(1), 20000 / 2).size());
|
||||
}
|
||||
|
||||
private void testIterator(Track.Id trackId, int numPoints) {
|
||||
private List<TrackPoint> testIterator(Track.Id trackId, int numPoints) {
|
||||
TrackPoint.Id lastPointId = initializeTrack(trackId, numPoints);
|
||||
List<TrackPoint> locations = new ArrayList<>(numPoints);
|
||||
try (TrackPointIterator it = contentProviderUtils.getTrackPointIterator(trackId, null)) {
|
||||
@@ -131,6 +121,7 @@ public class CustomContentProviderUtilsTest {
|
||||
}
|
||||
assertEquals(numPoints, locations.size());
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
|
||||
private TrackPoint.Id initializeTrack(Track.Id id, int numPoints) {
|
||||
@@ -187,27 +178,49 @@ public class CustomContentProviderUtilsTest {
|
||||
@Test
|
||||
public void testCreateTrack() {
|
||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||
|
||||
int columnIndex = 1;
|
||||
// Id
|
||||
when(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)).thenReturn(columnIndex);
|
||||
when(cursorMock.isNull(columnIndex)).thenReturn(false);
|
||||
when(cursorMock.getLong(columnIndex)).thenReturn(trackId.id());
|
||||
|
||||
//Uuid
|
||||
columnIndex++;
|
||||
when(cursorMock.getColumnIndexOrThrow(TracksColumns.UUID)).thenReturn(columnIndex);
|
||||
when(cursorMock.isNull(columnIndex)).thenReturn(false);
|
||||
when(cursorMock.getBlob(columnIndex)).thenReturn(UUIDUtils.toBytes(UUID.randomUUID()));
|
||||
|
||||
// Name
|
||||
columnIndex++;
|
||||
String name = NAME_PREFIX + trackId.id();
|
||||
when(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).thenReturn(columnIndex);
|
||||
when(cursorMock.isNull(columnIndex)).thenReturn(false);
|
||||
when(cursorMock.getString(columnIndex)).thenReturn(name);
|
||||
|
||||
Track track = ContentProviderUtils.createTrack(cursorMock);
|
||||
MatrixCursor cursor = new MatrixCursor(new String[]{
|
||||
TracksColumns._ID,
|
||||
TracksColumns.UUID,
|
||||
TracksColumns.NAME,
|
||||
TracksColumns.DESCRIPTION,
|
||||
TracksColumns.ACTIVITY_TYPE,
|
||||
TracksColumns.ACTIVITY_TYPE_LOCALIZED,
|
||||
TracksColumns.STARTTIME,
|
||||
TracksColumns.STARTTIME_OFFSET,
|
||||
TracksColumns.STOPTIME,
|
||||
TracksColumns.TOTALDISTANCE,
|
||||
TracksColumns.TOTALTIME,
|
||||
TracksColumns.MOVINGTIME,
|
||||
TracksColumns.MAXSPEED,
|
||||
TracksColumns.MIN_ALTITUDE,
|
||||
TracksColumns.MAX_ALTITUDE,
|
||||
TracksColumns.ALTITUDE_GAIN,
|
||||
TracksColumns.ALTITUDE_LOSS,
|
||||
});
|
||||
cursor.addRow(new Object[]{
|
||||
trackId.id(),
|
||||
UUIDUtils.toBytes(UUID.randomUUID()),
|
||||
name,
|
||||
"",
|
||||
ActivityType.UNKNOWN.getId(),
|
||||
"",
|
||||
1_000L,
|
||||
ZoneOffset.UTC.getTotalSeconds(),
|
||||
2_000L,
|
||||
123.45f,
|
||||
1_000L,
|
||||
500L,
|
||||
12.34f,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
});
|
||||
assertTrue(cursor.moveToFirst());
|
||||
|
||||
Track track = ContentProviderUtils.createTrack(cursor);
|
||||
assertEquals(trackId, track.id());
|
||||
assertEquals(name, track.name());
|
||||
}
|
||||
@@ -449,8 +462,6 @@ public class CustomContentProviderUtilsTest {
|
||||
marker.setDescription(TEST_DESC);
|
||||
contentProviderUtils.insertMarker(marker.getMarker());
|
||||
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
|
||||
|
||||
Marker.Id markerId = new Marker.Id(System.currentTimeMillis());
|
||||
marker.setId(markerId);
|
||||
ContentValues contentValues = contentProviderUtils.createContentValues(marker.getMarker(), trackId);
|
||||
@@ -1184,7 +1195,7 @@ public class CustomContentProviderUtilsTest {
|
||||
assertEquals(sensorStatistics.avgPower().getW(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
private void testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
|
||||
private Pair<SensorStatistics, TestSensorDataUtil.SensorDataStats> testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
|
||||
// given
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
@@ -1207,22 +1218,27 @@ public class CustomContentProviderUtilsTest {
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertEquals(sensorStatistics.avgHeartRate().getBPM(), stats.avgHr, 0.01f);
|
||||
assertEquals(sensorStatistics.maxHeartRate().getBPM(), stats.maxHr, 0.01f);
|
||||
assertEquals(sensorStatistics.avgCadence().getRPM(), stats.avgCadence, 0.01f);
|
||||
assertEquals(sensorStatistics.maxCadence().getRPM(), stats.maxCadence, 0.01f);
|
||||
assertEquals(sensorStatistics.avgPower().getW(), stats.avgPower, 0.01f);
|
||||
return Pair.create(sensorStatistics, stats);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_veryLongActivity12h() {
|
||||
testGetSensorStats_randomData(43200 / 6, false);
|
||||
Pair<SensorStatistics, TestSensorDataUtil.SensorDataStats> result = testGetSensorStats_randomData(43200 / 6, false);
|
||||
assertEquals(result.first.avgHeartRate().getBPM(), result.second.avgHr, 0.01f);
|
||||
assertEquals(result.first.maxHeartRate().getBPM(), result.second.maxHr, 0.01f);
|
||||
assertEquals(result.first.avgCadence().getRPM(), result.second.avgCadence, 0.01f);
|
||||
assertEquals(result.first.maxCadence().getRPM(), result.second.maxCadence, 0.01f);
|
||||
assertEquals(result.first.avgPower().getW(), result.second.avgPower, 0.01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_withSeveralRandomStartSegments() {
|
||||
testGetSensorStats_randomData(5000, true);
|
||||
Pair<SensorStatistics, TestSensorDataUtil.SensorDataStats> result = testGetSensorStats_randomData(5000, true);
|
||||
assertEquals(result.first.avgHeartRate().getBPM(), result.second.avgHr, 0.01f);
|
||||
assertEquals(result.first.maxHeartRate().getBPM(), result.second.maxHr, 0.01f);
|
||||
assertEquals(result.first.avgCadence().getRPM(), result.second.avgCadence, 0.01f);
|
||||
assertEquals(result.first.maxCadence().getRPM(), result.second.maxCadence, 0.01f);
|
||||
assertEquals(result.first.avgPower().getW(), result.second.avgPower, 0.01f);
|
||||
}
|
||||
|
||||
private TrackPoint getLastValidTrackPoint(Track.Id trackId) {
|
||||
|
||||
+51
-27
@@ -66,7 +66,8 @@ import de.dennisguse.opentracks.data.models.TrackBuilder;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.SensorManager;
|
||||
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
|
||||
@@ -74,10 +75,9 @@ import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpee
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataAggregator;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
|
||||
@@ -94,17 +94,19 @@ public class ExportImportTest {
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||
|
||||
//For csv_export_only() as we the timezone is hardcoded in the expectation.
|
||||
//For csv_export_only() as the timezone is hardcoded in the expectation.
|
||||
@Rule
|
||||
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
|
||||
|
||||
private static final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
PreferencesUtils.resetPreferences(context, true);
|
||||
}
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
@@ -162,7 +164,7 @@ public class ExportImportTest {
|
||||
"Marker 1 desc",
|
||||
"Marker 1 typeLocalized",
|
||||
new Position(
|
||||
Instant.parse("2020-02-02T02:02:03Z"),
|
||||
Instant.parse("2020-02-02T02:02:03Z"),
|
||||
3.1234567, 14.0014567,
|
||||
Distance.of(10),
|
||||
Altitude.WGS84.of(1020.25),
|
||||
@@ -197,7 +199,6 @@ public class ExportImportTest {
|
||||
));
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:18Z");
|
||||
trackPointCreator.getSensorManager().sensorDataAggregator = new SensorDataAggregator();
|
||||
service.endCurrentTrack();
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:03:20Z");
|
||||
@@ -212,7 +213,6 @@ public class ExportImportTest {
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3.1234567, 16.001, 10, 27, 15, 999.123, 0f);
|
||||
|
||||
trackPointCreator.getSensorManager().sensorDataAggregator = new SensorDataAggregator();
|
||||
trackPointCreator.setClock("2020-02-02T02:04:00Z");
|
||||
service.endCurrentTrack();
|
||||
|
||||
@@ -296,7 +296,22 @@ public class ExportImportTest {
|
||||
Power.of(50),
|
||||
new AltitudeGainLoss(0, 0)
|
||||
),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:02:18Z")),
|
||||
new TrackPoint(
|
||||
null,
|
||||
TrackPoint.Type.SEGMENT_END_MANUAL,
|
||||
new Position(
|
||||
Instant.parse("2020-02-02T02:02:18Z"),
|
||||
3.123456, 14.001456, Distance.of(10),
|
||||
Altitude.WGS84.of(1020.25), null,
|
||||
null,
|
||||
Speed.of(5)),
|
||||
Distance.of(0),
|
||||
HeartRate.of(69),
|
||||
null,
|
||||
Cadence.of(3),
|
||||
Power.of(50),
|
||||
new AltitudeGainLoss(0, 0)
|
||||
),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse("2020-02-02T02:03:20Z")),
|
||||
new TrackPoint(
|
||||
null,
|
||||
@@ -354,7 +369,16 @@ public class ExportImportTest {
|
||||
null,
|
||||
null,
|
||||
new AltitudeGainLoss(0, 0)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z"))
|
||||
new TrackPoint(
|
||||
null,
|
||||
TrackPoint.Type.SEGMENT_END_MANUAL,
|
||||
Position.of(Instant.parse("2020-02-02T02:04:00Z")),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new AltitudeGainLoss(0, 0))
|
||||
), actual);
|
||||
|
||||
assertEquals(new Statistics(
|
||||
@@ -660,17 +684,17 @@ public class ExportImportTest {
|
||||
}
|
||||
}
|
||||
|
||||
private void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain, float temperature) {
|
||||
SensorDataAggregator sensorDataAggregator = trackPointCreator.getSensorManager().sensorDataAggregator;
|
||||
private static void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain, float temperature) {
|
||||
SensorManager sensorManager = trackPointCreator.getSensorManager();
|
||||
|
||||
AggregatorCyclingPower cyclingPower = new AggregatorCyclingPower("", "");
|
||||
cyclingPower.add(new Raw<>(trackPointCreator.createNow(), new BluetoothHandlerCyclingPower.Data(Power.of(power), null)));
|
||||
sensorDataAggregator.add(cyclingPower);
|
||||
cyclingPower.add(trackPointCreator.getNow(), new CyclingPowerBluetooth.Data(Power.of(power), null));
|
||||
sensorManager.setAggregator(cyclingPower);
|
||||
|
||||
|
||||
AggregatorHeartRate avgHeartRate = new AggregatorHeartRate("", "");
|
||||
avgHeartRate.add(new Raw<>(trackPointCreator.createNow(), HeartRate.of(heartRate)));
|
||||
sensorDataAggregator.add(avgHeartRate);
|
||||
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(heartRate));
|
||||
sensorManager.setAggregator(avgHeartRate);
|
||||
|
||||
AggregatorCyclingCadence cyclingCadence = new AggregatorCyclingCadence("", "") {
|
||||
@NonNull
|
||||
@@ -684,7 +708,7 @@ public class ExportImportTest {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
sensorDataAggregator.add(cyclingCadence);
|
||||
sensorManager.setAggregator(cyclingCadence);
|
||||
|
||||
if (distance != null && speed != null) {
|
||||
AggregatorCyclingDistanceSpeed aggregatorCyclingDistanceSpeed = new AggregatorCyclingDistanceSpeed("", "") {
|
||||
@@ -699,14 +723,14 @@ public class ExportImportTest {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
sensorDataAggregator.add(aggregatorCyclingDistanceSpeed);
|
||||
sensorManager.setAggregator(aggregatorCyclingDistanceSpeed);
|
||||
} else {
|
||||
sensorDataAggregator.add(new AggregatorCyclingDistanceSpeed("", ""));
|
||||
sensorManager.setAggregator(new AggregatorCyclingDistanceSpeed("", ""));
|
||||
}
|
||||
|
||||
mockAltitudeChange(trackPointCreator, altitudeGain);
|
||||
|
||||
sensorDataAggregator.add(new AggregatorTemperature("", "'") {
|
||||
sensorManager.setAggregator(new AggregatorTemperature("", "'") {
|
||||
@NonNull
|
||||
@Override
|
||||
public Temperature getAggregatedValue(Instant now) {
|
||||
@@ -722,11 +746,11 @@ public class ExportImportTest {
|
||||
trackPointCreator.onChange();
|
||||
}
|
||||
|
||||
private void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
|
||||
SensorDataAggregator sensorDataAggregator = trackPointCreator.getSensorManager().sensorDataAggregator;
|
||||
private static void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) {
|
||||
SensorManager sensorManager = trackPointCreator.getSensorManager();
|
||||
|
||||
if (altitudeGain == null) {
|
||||
sensorDataAggregator.add(new AggregatorBarometer("test", null));
|
||||
sensorManager.getAltitudeChangeHandler().setAggregator(new AggregatorBarometer("test", null));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -742,10 +766,10 @@ public class ExportImportTest {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
sensorDataAggregator.add(aggregatorBarometer);
|
||||
sensorManager.getAltitudeChangeHandler().setAggregator(aggregatorBarometer);
|
||||
}
|
||||
|
||||
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
|
||||
public static void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, double altitude, Float altitudeGain) {
|
||||
Location location = new Location("mock");
|
||||
location.setLatitude(latitude);
|
||||
location.setLongitude(longitude);
|
||||
@@ -757,6 +781,6 @@ public class ExportImportTest {
|
||||
mockAltitudeChange(trackPointCreator, altitudeGain);
|
||||
|
||||
trackPointCreator.setClock(time);
|
||||
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
|
||||
trackPointCreator.getSensorManager().getGpsHandler().onDataReceived(location);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public class TrackPointAssert {
|
||||
|
||||
Assert.assertEquals("has altitude", expected.position().hasAltitude(), actual.position().hasAltitude());
|
||||
if (expected.position().hasAltitude()) {
|
||||
Assert.assertEquals("altitude", expected.position().altitude().getClass(), actual.position().altitude().getClass());
|
||||
Assert.assertEquals("altitude", expected.position().altitude().toM(), actual.position().altitude().toM(), delta);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
package de.dennisguse.opentracks.publicapi;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.data.CustomContentProvider;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DataProviderTest {
|
||||
|
||||
private final ContentResolver resolver = ApplicationProvider.getApplicationContext().getContentResolver();
|
||||
|
||||
@Test
|
||||
public void supportsVersion2_tracks() {
|
||||
// given
|
||||
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/tracks/1");
|
||||
String[] expectedTracksColumns = new String[]{
|
||||
"_id",
|
||||
"name",
|
||||
"description",
|
||||
"category",
|
||||
"activity_type_localized",
|
||||
"starttime",
|
||||
"stoptime",
|
||||
"totaltime",
|
||||
"movingtime",
|
||||
"totaldistance",
|
||||
"maxspeed",
|
||||
"avgspeed",
|
||||
"avgmovingspeed",
|
||||
"minelevation",
|
||||
"maxelevation",
|
||||
"elevationgain",
|
||||
"elevationloss"
|
||||
};
|
||||
|
||||
// when
|
||||
try (Cursor cursor = resolver.query(
|
||||
uri,
|
||||
expectedTracksColumns,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)) {
|
||||
// then
|
||||
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.track", resolver.getType(uri));
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertArrayEquals(expectedTracksColumns, cursor.getColumnNames());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsVersion2_trackpoints() {
|
||||
// given
|
||||
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
|
||||
String[] expectedTrackpointsColumns = {
|
||||
"_id",
|
||||
"trackid",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"time",
|
||||
"type",
|
||||
"speed"
|
||||
};
|
||||
|
||||
// when
|
||||
try (Cursor cursor = resolver.query(
|
||||
uri,
|
||||
expectedTrackpointsColumns,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)) {
|
||||
// then
|
||||
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint", resolver.getType(uri));
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertArrayEquals(expectedTrackpointsColumns, cursor.getColumnNames());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsVersion2_markers() {
|
||||
// given
|
||||
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/markers/1");
|
||||
String[] expectedMarkersColumns = {
|
||||
"_id",
|
||||
"name",
|
||||
"description",
|
||||
"category",
|
||||
"trackid",
|
||||
"longitude",
|
||||
"latitude",
|
||||
"photoUrl",
|
||||
"icon"
|
||||
};
|
||||
|
||||
// when
|
||||
try (Cursor cursor = resolver.query(
|
||||
uri,
|
||||
expectedMarkersColumns,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)) {
|
||||
// then
|
||||
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.waypoint", resolver.getType(uri));
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertArrayEquals(expectedMarkersColumns, cursor.getColumnNames());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsVersion3_tracks() {
|
||||
// given
|
||||
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/tracks/1");
|
||||
String[] expectedTracksColumns = {
|
||||
"_id",
|
||||
"name",
|
||||
"description",
|
||||
"activity_type",
|
||||
"activity_type_localized",
|
||||
"time_start",
|
||||
"time_stop",
|
||||
"duration_total",
|
||||
"duration_moving",
|
||||
"distance",
|
||||
"speed_max",
|
||||
"altitude_min",
|
||||
"altitude_max",
|
||||
"altitude_gain",
|
||||
"altitude_loss"
|
||||
};
|
||||
|
||||
// when
|
||||
try (Cursor cursor = resolver.query(
|
||||
uri,
|
||||
expectedTracksColumns,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)) {
|
||||
// then
|
||||
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.track", resolver.getType(uri));
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertArrayEquals(expectedTracksColumns, cursor.getColumnNames());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsVersion3_trackpoints() {
|
||||
//given
|
||||
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/trackpoints/1");
|
||||
String[] expectedTrackpointsColumns = {
|
||||
"_id",
|
||||
"trackid",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"time",
|
||||
"type",
|
||||
"speed"
|
||||
};
|
||||
|
||||
// when
|
||||
try (Cursor cursor = resolver.query(
|
||||
uri,
|
||||
expectedTrackpointsColumns,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)) {
|
||||
//then
|
||||
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint", resolver.getType(uri));
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertArrayEquals(expectedTrackpointsColumns, cursor.getColumnNames());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsVersion3_markers() {
|
||||
// given
|
||||
Uri uri = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/dashboard/markers/1");
|
||||
String[] expectedMarkersColumns = {
|
||||
"_id",
|
||||
"name",
|
||||
"description",
|
||||
"marker_type_localized",
|
||||
"trackid",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"photoUrl"
|
||||
};
|
||||
|
||||
// when
|
||||
try (Cursor cursor = resolver.query(
|
||||
uri,
|
||||
expectedMarkersColumns,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)) {
|
||||
// then
|
||||
assertEquals("vnd.android.cursor.dir/vnd.de.dennisguse.waypoint", resolver.getType(uri));
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertArrayEquals(expectedMarkersColumns, cursor.getColumnNames());
|
||||
}
|
||||
}
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class BluetoothHandlerCyclingPowerTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0, 0, 40, 0});
|
||||
|
||||
// when
|
||||
BluetoothHandlerCyclingPower.Data powerCadence = BluetoothHandlerCyclingPower.parseCyclingPower(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(40, powerCadence.power().getW(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power_with_cadence() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
|
||||
|
||||
// when
|
||||
BluetoothHandlerCyclingPower.Data powerCadence = BluetoothHandlerCyclingPower.parseCyclingPower(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(0, powerCadence.power().getW(), 0.01);
|
||||
|
||||
assertEquals(12, powerCadence.crank().crankRevolutionsCount());
|
||||
assertEquals(17125, powerCadence.crank().crankRevolutionsTime());
|
||||
}
|
||||
|
||||
}
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@@ -13,11 +13,11 @@ public class BluetoothHandlerBarometricPressureTest {
|
||||
@Test
|
||||
public void parseEnvironmentalSensing_Pa() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BarometerBluetooth.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00});
|
||||
|
||||
// when
|
||||
AtmosphericPressure pressure = BluetoothHandlerBarometricPressure.parseEnvironmentalSensing(characteristic);
|
||||
AtmosphericPressure pressure = BarometerBluetooth.parseEnvironmentalSensing(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(AtmosphericPressure.ofHPA(1001.65f), pressure);
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@@ -13,11 +13,11 @@ public class BluetoothHandlerHeartRateTest {
|
||||
@Test
|
||||
public void parseHeartRate_uint8() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(HeartRateBluetooth.HEARTRATE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, 0x3C});
|
||||
|
||||
// when
|
||||
HeartRate heartRate = BluetoothHandlerHeartRate.parseHeartRate(characteristic);
|
||||
HeartRate heartRate = HeartRateBluetooth.parseHeartRate(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(HeartRate.of(60), heartRate);
|
||||
@@ -26,11 +26,11 @@ public class BluetoothHandlerHeartRateTest {
|
||||
@Test
|
||||
public void parseHeartRate_uint16() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerHeartRate.HEARTRATE.serviceUUID(), 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(HeartRateBluetooth.HEARTRATE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
|
||||
|
||||
// when
|
||||
HeartRate heartRate = BluetoothHandlerHeartRate.parseHeartRate(characteristic);
|
||||
HeartRate heartRate = HeartRateBluetooth.parseHeartRate(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(HeartRate.of(257), heartRate);
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class BoschEbikeParserTest {
|
||||
|
||||
@Test
|
||||
public void parse_multipleFrames_returnsCombinedMeasurements() {
|
||||
byte[] raw = new byte[] {
|
||||
// Bosch cadence frame
|
||||
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76,
|
||||
// Bosch human power frame
|
||||
0x30, 0x05, (byte) 0x98, 0x5B, 0x08, (byte) 0x83, 0x01,
|
||||
// Bosch speed frame
|
||||
0x30, 0x07, (byte) 0x98, 0x2D, 0x08, (byte) 0x94, 0x0B, 0x10, 0x01
|
||||
};
|
||||
|
||||
BoschEbikeParser.Data data = BoschEbikeParser.parse(raw);
|
||||
|
||||
assertNotNull(data);
|
||||
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
|
||||
assertEquals(131.0f, data.humanPower().getW(), 0.01f);
|
||||
assertEquals(14.28 / 3.6, data.speed().toMPS(), 0.0001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_multiByteVarint_returnsScaledSpeed() {
|
||||
// Bosch speed frame using a multi-byte varint
|
||||
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||
0x30, 0x07, (byte) 0x98, 0x2D, 0x08, (byte) 0xE1, 0x0E, 0x10, 0x01
|
||||
});
|
||||
|
||||
assertNotNull(data);
|
||||
assertEquals(18.89 / 3.6, data.speed().toMPS(), 0.0001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_zeroValueFrame_returnsZeroCadence() {
|
||||
// Bosch zero-value cadence frame
|
||||
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||
0x30, 0x02, (byte) 0x98, 0x5A
|
||||
});
|
||||
|
||||
assertNotNull(data);
|
||||
assertEquals(0.0f, data.cadence().getRPM(), 0.01f);
|
||||
assertNull(data.speed());
|
||||
assertNull(data.humanPower());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_unsupportedMeasurementsOnly_returnsNull() {
|
||||
// Bosch frame for message 0x9865, which the parser does not map to a measurement.
|
||||
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||
0x30, 0x02, (byte) 0x98, 0x65
|
||||
});
|
||||
|
||||
assertNull(data);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_unimplementedMeasurementsOnly_returnsNull() {
|
||||
// Bosch motor power frame: message 0x985D with value 0x42, currently ignored.
|
||||
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||
0x30, 0x04, (byte) 0x98, 0x5D, 0x08, 0x42
|
||||
});
|
||||
|
||||
assertNull(data);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_malformedVarint_returnsNull() {
|
||||
// Truncated Bosch speed frame: starts like a real 0x982D frame but the varint is incomplete.
|
||||
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94
|
||||
});
|
||||
|
||||
assertNull(data);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_varintLargerThan32Bit_returnsNull() {
|
||||
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||
0x30, 0x08, (byte) 0x98, 0x2D, 0x08,
|
||||
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01
|
||||
});
|
||||
|
||||
assertNull(data);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_validMeasurementBeforeMalformedFrame_returnsValidMeasurement() {
|
||||
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||
// Bosch cadence frame: message 0x985A with value 0x76
|
||||
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76,
|
||||
// Truncated Bosch speed frame: malformed trailing frame should not discard the valid cadence above.
|
||||
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94
|
||||
});
|
||||
|
||||
assertNotNull(data);
|
||||
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
|
||||
assertNull(data.speed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_malformedVarintDoesNotConsumeNextFrameStartByte() {
|
||||
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||
// Truncated Bosch speed frame: the varint continuation bit is set, but the frame ends
|
||||
// before the next varint byte. The following frame start byte must not be consumed.
|
||||
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94,
|
||||
// Bosch cadence frame: message 0x985A with value 0x76
|
||||
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76
|
||||
});
|
||||
|
||||
assertNotNull(data);
|
||||
assertNull(data.speed());
|
||||
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_leadingNoise_ignored() {
|
||||
byte[] raw = new byte[] {
|
||||
// Noise bytes to verify the parser resynchronizes on the Bosch frame start byte 0x30.
|
||||
0x01, 0x02, 0x03,
|
||||
// Bosch cadence frame: message 0x985A with value 0x76
|
||||
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76
|
||||
};
|
||||
|
||||
BoschEbikeParser.Data data = BoschEbikeParser.parse(raw);
|
||||
|
||||
assertNotNull(data);
|
||||
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
@@ -8,14 +8,14 @@ import android.util.Pair;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class BluetoothHandlerCyclingDistanceSpeedTest {
|
||||
public class CyclingDistanceSpeedBluetoothTest {
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
|
||||
|
||||
// when
|
||||
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
|
||||
|
||||
// then
|
||||
assertNull(sensor.first);
|
||||
@@ -24,11 +24,11 @@ public class BluetoothHandlerCyclingDistanceSpeedTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_wheelOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(65535 + 16777216, sensor.first.wheelRevolutionsCount());
|
||||
@@ -37,11 +37,11 @@ public class BluetoothHandlerCyclingDistanceSpeedTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankWheel() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, BluetoothHandlerCyclingCadence.CrankData> sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
Pair<CyclingDistanceSpeedBluetooth.WheelData, CyclingCadenceBluetooth.CrankData> sensor = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(200 + 16777216, sensor.first.wheelRevolutionsCount());
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class CyclingPowerBluetoothTest {
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingPowerBluetooth.CYCLING_POWER.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0, 0, 40, 0});
|
||||
|
||||
// when
|
||||
CyclingPowerBluetooth.Data powerCadence = CyclingPowerBluetooth.parseCyclingPower(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(40, powerCadence.power().getW(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power_with_cadence() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(CyclingPowerBluetooth.CYCLING_POWER.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42});
|
||||
|
||||
// when
|
||||
CyclingPowerBluetooth.Data powerCadence = CyclingPowerBluetooth.parseCyclingPower(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(0, powerCadence.power().getW(), 0.01);
|
||||
|
||||
assertEquals(12, powerCadence.crank().crankRevolutionsCount());
|
||||
assertEquals(17125, powerCadence.crank().crankRevolutionsTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parsePayload_boschPower() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BoschEbikeParser.BOSCH_EBIKE.measurementUUID(), 0, 0);
|
||||
// Bosch human power frame: message 0x985B with value 0x7B => 123 W.
|
||||
characteristic.setValue(new byte[] { 0x30, 0x04, (byte) 0x98, 0x5B, 0x08, 0x7B });
|
||||
|
||||
CyclingPowerBluetooth.Data power = new CyclingPowerBluetooth().parsePayload(BoschEbikeParser.BOSCH_EBIKE, null, characteristic);
|
||||
|
||||
assertEquals(123, power.power().getW(), 0.01);
|
||||
assertNull(power.crank());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parsePayload_boschCadenceOnly_returnsNull() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BoschEbikeParser.BOSCH_EBIKE.measurementUUID(), 0, 0);
|
||||
// Bosch cadence-only frame: message 0x985A with value 0x01B8 => 220 / 2 = 110 rpm, so power parsing should ignore it.
|
||||
characteristic.setValue(new byte[] { 0x30, 0x05, (byte) 0x98, 0x5A, 0x08, (byte) 0xB8, 0x01 });
|
||||
|
||||
CyclingPowerBluetooth.Data power = new CyclingPowerBluetooth().parsePayload(BoschEbikeParser.BOSCH_EBIKE, null, characteristic);
|
||||
|
||||
assertNull(power);
|
||||
}
|
||||
|
||||
}
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@@ -10,15 +10,15 @@ import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
|
||||
public class BluetoothHandlerRunningSpeedAndCadenceTest {
|
||||
public class RunningSpeedAndCadenceBluetoothTest {
|
||||
|
||||
@Test
|
||||
public void parseRunningSpeedAndCadence_with_distance() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(RunningSpeedAndCadenceBluetooth.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0);
|
||||
characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1});
|
||||
|
||||
// when
|
||||
BluetoothHandlerRunningSpeedAndCadence.Data sensor = BluetoothHandlerRunningSpeedAndCadence.parseRunningSpeedAndCadence("sensorName", characteristic);
|
||||
RunningSpeedAndCadenceBluetooth.Data sensor = RunningSpeedAndCadenceBluetooth.parseRunningSpeedAndCadence("sensorName", characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(Speed.of(5), sensor.speed());
|
||||
+3
-3
@@ -11,7 +11,7 @@ public class AggregatorBarometerTest {
|
||||
|
||||
private static void addSensorValue(AggregatorBarometer aggregatorBarometer, float[] values) {
|
||||
for (float f : values) {
|
||||
aggregatorBarometer.add(new Raw<>(Instant.MIN, AtmosphericPressure.ofHPA(f)));
|
||||
aggregatorBarometer.add(Instant.MIN, AtmosphericPressure.ofHPA(f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class AggregatorBarometerTest {
|
||||
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});
|
||||
|
||||
// then
|
||||
Assert.assertEquals(0f, subject.aggregatedValue.gain_m(), 0.01);
|
||||
Assert.assertEquals(15f, subject.aggregatedValue.loss_m(), 0.01);
|
||||
Assert.assertEquals(0f, subject.output.gain_m(), 0.01);
|
||||
Assert.assertEquals(15f, subject.output.loss_m(), 0.01);
|
||||
}
|
||||
}
|
||||
+21
-22
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
@@ -12,10 +13,9 @@ import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
|
||||
import de.dennisguse.opentracks.sensors.driver.CyclingDistanceSpeedBluetooth;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SensorDataCyclingTest {
|
||||
@@ -25,8 +25,8 @@ public class SensorDataCyclingTest {
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 2048)));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 2048));
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
|
||||
@@ -37,8 +37,8 @@ public class SensorDataCyclingTest {
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 6184)));
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 8016)));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 6184));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 8016));
|
||||
|
||||
// then
|
||||
assertEquals(33.53, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
|
||||
@@ -49,8 +49,8 @@ public class SensorDataCyclingTest {
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 2048)));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 2048));
|
||||
|
||||
// then
|
||||
assertEquals(Cadence.of(0), current.getAggregatedValue(Instant.MIN));
|
||||
@@ -62,8 +62,8 @@ public class SensorDataCyclingTest {
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, 1024)));
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 1024)));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, 1024));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 1024));
|
||||
|
||||
// then
|
||||
assertFalse(current.hasReceivedData()); //TODO Cadence should be 0?
|
||||
@@ -74,8 +74,8 @@ public class SensorDataCyclingTest {
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(1, UintUtils.UINT16_MAX - 1024)));
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(2, 0)));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1, UintUtils.UINT16_MAX - 1024));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(2, 0));
|
||||
|
||||
// then
|
||||
assertEquals(60, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
|
||||
@@ -87,13 +87,13 @@ public class SensorDataCyclingTest {
|
||||
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
|
||||
|
||||
// when
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(UintUtils.UINT32_MAX - 1, 1024)));
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingCadence.CrankData(0, 2048)));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(UintUtils.UINT32_MAX - 1, 1024));
|
||||
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(0, 2048));
|
||||
|
||||
// then
|
||||
// TODO See #953
|
||||
// assertEquals(60, current.getValue().getRPM(), 0.01);
|
||||
assertEquals(Cadence.of(0), current.getAggregatedValue(Instant.MIN));
|
||||
assertNull(current.getAggregatedValue(Instant.MIN));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -102,8 +102,8 @@ public class SensorDataCyclingTest {
|
||||
current.setWheelCircumference(Distance.ofMM(2150));
|
||||
|
||||
// when
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(1, 6184)));
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(2, 8016)));
|
||||
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(1, 6184));
|
||||
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(2, 8016));
|
||||
|
||||
// then
|
||||
assertEquals(2.15, current.getAggregatedValue(Instant.MIN).distance().toM(), 0.01);
|
||||
@@ -117,14 +117,13 @@ public class SensorDataCyclingTest {
|
||||
current.setWheelCircumference(Distance.ofMM(2000));
|
||||
|
||||
// when
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(UintUtils.UINT32_MAX - 1, 1024)));
|
||||
current.add(new Raw<>(Instant.MIN, new BluetoothHandlerCyclingDistanceSpeed.WheelData(0, 2048)));
|
||||
|
||||
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(UintUtils.UINT32_MAX - 1, 1024));
|
||||
current.add(Instant.MIN, new CyclingDistanceSpeedBluetooth.WheelData(0, 2048));
|
||||
|
||||
// then
|
||||
// TODO See #953
|
||||
// assertEquals(2, current.getValue().getDistance().toM(), 0.01);
|
||||
// assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01);
|
||||
assertEquals(new AggregatorCyclingDistanceSpeed.Data(Distance.ZERO, Distance.ZERO, Speed.ZERO), current.getAggregatedValue(Instant.MIN));
|
||||
assertNull(current.getAggregatedValue(Instant.MIN));
|
||||
}
|
||||
}
|
||||
+54
-37
@@ -1,24 +1,23 @@
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
@@ -29,32 +28,24 @@ import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class TrackRecordingServiceNotificationManagerTest {
|
||||
|
||||
private static final String CHANNEL_ID = "TrackRecordingServiceNotificationManagerTest";
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
|
||||
@Mock
|
||||
private NotificationCompat.Builder notificationCompatBuilder;
|
||||
|
||||
@Mock
|
||||
private NotificationManager notificationManager;
|
||||
@After
|
||||
public void tearDown() {
|
||||
notificationManager.cancelAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
|
||||
TrackPoint trackPointMock = new TrackPoint(TrackPoint.Type.TRACKPOINT,
|
||||
new Position(
|
||||
null,
|
||||
null, null, Distance.of(999f),
|
||||
Altitude.WGS84.of(10), null,
|
||||
null,
|
||||
Speed.ZERO)
|
||||
);
|
||||
notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT));
|
||||
|
||||
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean()))
|
||||
.thenReturn(notificationCompatBuilder);
|
||||
|
||||
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
|
||||
TrackingNotificationBuilder notificationBuilder = new TrackingNotificationBuilder(context, CHANNEL_ID);
|
||||
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationBuilder);
|
||||
subject.setUnitSystem(UnitSystem.METRIC);
|
||||
|
||||
Track track = new Track(
|
||||
@@ -68,16 +59,42 @@ public class TrackRecordingServiceNotificationManagerTest {
|
||||
Statistics.DEFAULT
|
||||
);
|
||||
|
||||
RecordingData data = new RecordingData(track, trackPointMock, null, null);
|
||||
|
||||
// when
|
||||
subject.updateTrackPoint(context, data, Distance.of(100));
|
||||
subject.updateTrackPoint(context, data, Distance.of(100));
|
||||
subject.updateTrackPoint(context, data, Distance.of(1000));
|
||||
subject.updateTrackPoint(context, data, Distance.of(100));
|
||||
|
||||
// then
|
||||
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
|
||||
verify(notificationCompatBuilder, times(2)).setOnlyAlertOnce(false);
|
||||
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(100));
|
||||
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(100));
|
||||
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(1000));
|
||||
subject.updateTrackPoint(context, createRecordingData(track, Distance.of(999f)), Distance.of(100));
|
||||
assertEquals(java.util.Arrays.asList(false, true, true, true, true, true, false, true), notificationBuilder.onlyAlertOnceValues);
|
||||
}
|
||||
}
|
||||
|
||||
private RecordingData createRecordingData(Track track, Distance horizontalAccuracy) {
|
||||
TrackPoint trackPoint = new TrackPoint(
|
||||
TrackPoint.Type.TRACKPOINT,
|
||||
new Position(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
horizontalAccuracy,
|
||||
Altitude.WGS84.of(10),
|
||||
null,
|
||||
null,
|
||||
Speed.ZERO
|
||||
)
|
||||
);
|
||||
return new RecordingData(track, trackPoint, null, null);
|
||||
}
|
||||
|
||||
private static final class TrackingNotificationBuilder extends NotificationCompat.Builder {
|
||||
private final List<Boolean> onlyAlertOnceValues = new ArrayList<>();
|
||||
|
||||
private TrackingNotificationBuilder(Context context, String channelId) {
|
||||
super(context, channelId);
|
||||
setSmallIcon(R.drawable.ic_logo_color_24dp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotificationCompat.Builder setOnlyAlertOnce(boolean onlyAlertOnce) {
|
||||
onlyAlertOnceValues.add(onlyAlertOnce);
|
||||
return super.setOnlyAlertOnce(onlyAlertOnce);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-17
@@ -6,7 +6,6 @@ import static org.junit.Assert.assertFalse;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -44,12 +43,12 @@ import de.dennisguse.opentracks.data.models.Statistics;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
|
||||
import de.dennisguse.opentracks.sensors.SensorManager;
|
||||
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
|
||||
import de.dennisguse.opentracks.sensors.driver.RunningSpeedAndCadenceBluetooth;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
@@ -154,7 +153,8 @@ public class TrackRecordingServiceRecordingTest {
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
|
||||
//We do not want the real GPS to interfere.
|
||||
LocationManagerCompat.removeUpdates((LocationManager) context.getSystemService(Context.LOCATION_SERVICE), trackPointCreator.getSensorManager().getGpsManager());
|
||||
GpsInternal gpsInternal = (GpsInternal)trackPointCreator.getSensorManager().getGpsHandler().driver;
|
||||
LocationManagerCompat.removeUpdates(gpsInternal.locationManager, gpsInternal.locationListenerCompat);
|
||||
|
||||
String gps1 = "2020-02-02T02:02:03Z";
|
||||
sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
|
||||
@@ -339,16 +339,21 @@ public class TrackRecordingServiceRecordingTest {
|
||||
mockAltitudeChange(trackPointCreator, 0);
|
||||
|
||||
SensorManager sensorManager = trackPointCreator.getSensorManager();
|
||||
sensorManager.sensorDataAggregator.add(new AggregatorHeartRate("", ""));
|
||||
// when
|
||||
String sensor1 = "2020-02-02T02:02:03Z";
|
||||
trackPointCreator.setClock(sensor1);
|
||||
|
||||
sensorManager.onChanged(new Raw<>(sensor1, HeartRate.of(5))); //Should be ignored
|
||||
AggregatorHeartRate avgHeartRate = new AggregatorHeartRate("", "");
|
||||
sensorManager.setAggregator(avgHeartRate);
|
||||
|
||||
//Should be ignored
|
||||
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(5));
|
||||
sensorManager.onChange();
|
||||
|
||||
String sensor3 = "2020-02-02T02:02:13Z";
|
||||
trackPointCreator.setClock(sensor3);
|
||||
sensorManager.onChanged(new Raw<>(sensor3, HeartRate.of(7)));
|
||||
avgHeartRate.add(trackPointCreator.getNow(), HeartRate.of(7));
|
||||
sensorManager.onChange();
|
||||
|
||||
String stopTime = "2020-02-02T02:02:15Z";
|
||||
trackPointCreator.setClock(stopTime);
|
||||
@@ -808,19 +813,22 @@ public class TrackRecordingServiceRecordingTest {
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
|
||||
AggregatorRunning aggregatorRunning = new AggregatorRunning("", "");
|
||||
SensorManager sensorManager = trackPointCreator.getSensorManager();
|
||||
sensorManager.sensorDataAggregator.add(new AggregatorRunning("", ""));
|
||||
sensorManager.sensorDataAggregator.barometer = null;
|
||||
sensorManager.setAggregator(aggregatorRunning);
|
||||
|
||||
// when
|
||||
String sensor1 = "2020-02-02T02:02:03Z";
|
||||
trackPointCreator.setClock(sensor1);
|
||||
sensorManager.onChanged(new Raw<>(sensor1, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(1), Distance.ZERO))); //Should be ignored
|
||||
//Should be ignored
|
||||
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(1), Distance.ZERO));
|
||||
sensorManager.onChange();
|
||||
|
||||
// when
|
||||
String sensor2 = "2020-02-02T02:02:04Z";
|
||||
trackPointCreator.setClock(sensor2);
|
||||
sensorManager.onChanged(new Raw<>(sensor2, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(2), Distance.of(2))));
|
||||
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(2), Distance.of(2)));
|
||||
sensorManager.onChange();
|
||||
|
||||
// when
|
||||
String gps1 = "2020-02-02T02:02:05Z";
|
||||
@@ -829,12 +837,15 @@ public class TrackRecordingServiceRecordingTest {
|
||||
// when
|
||||
String sensor3 = "2020-02-02T02:02:06Z";
|
||||
trackPointCreator.setClock(sensor3);
|
||||
sensorManager.onChanged(new Raw<>(sensor3, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(3), Distance.of(12))));
|
||||
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(3), Distance.of(12)));
|
||||
sensorManager.onChange();
|
||||
|
||||
// when
|
||||
String sensor4 = "2020-02-02T02:02:07Z";
|
||||
trackPointCreator.setClock(sensor4);
|
||||
sensorManager.onChanged(new Raw<>(sensor4, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(4), Distance.of(14)))); //Should be ignored
|
||||
//Should be ignored
|
||||
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(4), Distance.of(14)));
|
||||
sensorManager.onChange();
|
||||
|
||||
// when
|
||||
String gps2 = "2020-02-02T02:02:08Z";
|
||||
@@ -843,7 +854,9 @@ public class TrackRecordingServiceRecordingTest {
|
||||
// when
|
||||
String sensor5 = "2020-02-02T02:02:10Z";
|
||||
trackPointCreator.setClock(sensor5);
|
||||
sensorManager.onChanged(new Raw<>(sensor5, new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), Cadence.of(5), Distance.of(16)))); //Should be ignored
|
||||
//Should be ignored
|
||||
aggregatorRunning.add(trackPointCreator.getNow(), new RunningSpeedAndCadenceBluetooth.Data(Speed.of(5), Cadence.of(5), Distance.of(16)));
|
||||
sensorManager.onChange();
|
||||
|
||||
// when
|
||||
String gps3 = "2020-02-02T02:02:12Z";
|
||||
@@ -962,7 +975,7 @@ public class TrackRecordingServiceRecordingTest {
|
||||
}
|
||||
|
||||
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
|
||||
trackPointCreator.getSensorManager().sensorDataAggregator.barometer = new AggregatorBarometer("", "") {
|
||||
trackPointCreator.getSensorManager().getAltitudeChangeHandler().setAggregator(new AggregatorBarometer("", "") {
|
||||
@Override
|
||||
public boolean hasReceivedData() {
|
||||
return true;
|
||||
@@ -973,7 +986,7 @@ public class TrackRecordingServiceRecordingTest {
|
||||
public AltitudeGainLoss getAggregatedValue(Instant now) {
|
||||
return new AltitudeGainLoss(altitudeGain, altitudeGain);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static void sendGPSLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, long speed) {
|
||||
@@ -985,6 +998,6 @@ public class TrackRecordingServiceRecordingTest {
|
||||
location.setSpeed(speed);
|
||||
|
||||
trackPointCreator.setClock(time);
|
||||
trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
|
||||
trackPointCreator.getSensorManager().getGpsHandler().onDataReceived(location);
|
||||
}
|
||||
}
|
||||
|
||||
+44
-18
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import static de.dennisguse.opentracks.util.LiveDataTestUtils.waitForValue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
@@ -41,6 +42,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -52,11 +54,12 @@ import de.dennisguse.opentracks.sensors.GpsStatusValue;
|
||||
|
||||
/**
|
||||
* Testing the states of TrackRecordingService.
|
||||
* As states are checked against MutableLiveData that are updated via `postValue()` (is asynchronous to be inform other Threads), Thread.sleep() is required.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TrackRecordingServiceStateMachineTest {
|
||||
|
||||
private static final Duration WAIT_TIMEOUT = Duration.ofSeconds(2);
|
||||
|
||||
@Rule
|
||||
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
||||
|
||||
@@ -118,13 +121,13 @@ public class TrackRecordingServiceStateMachineTest {
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void gps_startStop() throws InterruptedException {
|
||||
public void gps_startStop() {
|
||||
// given
|
||||
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
|
||||
|
||||
// when
|
||||
service.tryStartSensors();
|
||||
Thread.sleep(1000);
|
||||
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
|
||||
|
||||
// then
|
||||
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
||||
@@ -133,23 +136,25 @@ public class TrackRecordingServiceStateMachineTest {
|
||||
|
||||
// when
|
||||
service.stopSensors();
|
||||
Thread.sleep(1000);
|
||||
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
|
||||
|
||||
// then
|
||||
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
||||
assertEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
// assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue()); TODO BUG: GPS can only be stopped if a service is recording.
|
||||
assertEquals(GpsStatusValue.GPS_NONE, service.getGpsStatusObservable().getValue());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void recording_startStopResume_no_data() throws InterruptedException {
|
||||
public void recording_startStopResume_no_data() {
|
||||
// given
|
||||
assertFalse(service.isRecording());
|
||||
|
||||
// when
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
Thread.sleep(1000);
|
||||
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
|
||||
waitUntilRecordingDataIsNotDefault();
|
||||
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
@@ -158,10 +163,13 @@ public class TrackRecordingServiceStateMachineTest {
|
||||
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
|
||||
|
||||
service.endCurrentTrack();
|
||||
waitUntilRecordingStatusIs(TrackRecordingService.STATUS_DEFAULT);
|
||||
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
|
||||
|
||||
// when
|
||||
service.resumeTrack(trackId);
|
||||
Thread.sleep(1000);
|
||||
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
|
||||
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
@@ -172,7 +180,8 @@ public class TrackRecordingServiceStateMachineTest {
|
||||
|
||||
// when
|
||||
service.endCurrentTrack();
|
||||
Thread.sleep(1000);
|
||||
waitUntilRecordingStatusIs(TrackRecordingService.STATUS_DEFAULT);
|
||||
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
|
||||
|
||||
// then
|
||||
assertFalse(service.isRecording());
|
||||
@@ -183,12 +192,12 @@ public class TrackRecordingServiceStateMachineTest {
|
||||
|
||||
// when
|
||||
service.resumeTrack(trackId);
|
||||
Thread.sleep(1000);
|
||||
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
|
||||
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
|
||||
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
|
||||
assertNotEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
|
||||
}
|
||||
@@ -214,7 +223,7 @@ public class TrackRecordingServiceStateMachineTest {
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void cannotEnd_without_starting() throws InterruptedException {
|
||||
public void cannotEnd_without_starting() {
|
||||
// given
|
||||
assertFalse(service.isRecording());
|
||||
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
||||
@@ -223,7 +232,6 @@ public class TrackRecordingServiceStateMachineTest {
|
||||
|
||||
// when
|
||||
service.endCurrentTrack();
|
||||
Thread.sleep(1000);
|
||||
|
||||
// then
|
||||
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
||||
@@ -234,36 +242,54 @@ public class TrackRecordingServiceStateMachineTest {
|
||||
@Ignore("TODO Bug: GPS can be stopped although the current track is recording")
|
||||
@MediumTest
|
||||
@Test
|
||||
public void recording_stopGPS_noop() throws InterruptedException {
|
||||
public void recording_stopGPS_noop() {
|
||||
// given
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
|
||||
|
||||
// when
|
||||
service.stopSensors(); //TODO Should be ignored as service is recording
|
||||
Thread.sleep(1000);
|
||||
|
||||
// then
|
||||
assertTrue(service.isRecording());
|
||||
assertEquals(new RecordingStatus(trackId), service.getRecordingStatusObservable().getValue());
|
||||
Thread.sleep(1000); //TODO Figure out why we need to wait here until the update is happening
|
||||
assertNotEquals(RecordingData.NOT_RECORDING, service.getRecordingDataObservable().getValue());
|
||||
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void recording_startRecording_alreadyRecording() throws InterruptedException {
|
||||
public void recording_startRecording_alreadyRecording() {
|
||||
// given
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
assertTrue(service.isRecording());
|
||||
|
||||
// when
|
||||
Track.Id newTrackId = service.startNewTrack();
|
||||
Thread.sleep(1000);
|
||||
|
||||
// then
|
||||
assertNotNull(trackId);
|
||||
assertNull(newTrackId);
|
||||
}
|
||||
|
||||
private void waitUntilRecordingDataIsNotDefault() {
|
||||
waitForValue(service.getRecordingDataObservable(),
|
||||
recordingData -> !RecordingData.NOT_RECORDING.equals(recordingData),
|
||||
WAIT_TIMEOUT,
|
||||
"recording data to leave NOT_RECORDING");
|
||||
}
|
||||
|
||||
private void waitUntilGpsStatusIs(GpsStatusValue expected) {
|
||||
waitForValue(service.getGpsStatusObservable(),
|
||||
expected::equals,
|
||||
WAIT_TIMEOUT,
|
||||
"gps status to become " + expected);
|
||||
}
|
||||
|
||||
private void waitUntilRecordingStatusIs(RecordingStatus expected) {
|
||||
waitForValue(service.getRecordingStatusObservable(),
|
||||
expected::equals,
|
||||
WAIT_TIMEOUT,
|
||||
"recording status to become " + expected);
|
||||
}
|
||||
}
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static de.dennisguse.opentracks.services.TrackRecordingService.RECORDING_DATA_UPDATE_INTERVAL;
|
||||
import static de.dennisguse.opentracks.util.LiveDataTestUtils.waitForValue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import de.dennisguse.opentracks.TestUtil;
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.io.file.importer.ExportImportTest;
|
||||
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
|
||||
import de.dennisguse.opentracks.sensors.GpsHandler;
|
||||
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TrackRecordingServiceUITest {
|
||||
|
||||
@Rule
|
||||
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||
|
||||
private static final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
if (Looper.myLooper() == null) Looper.prepare();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDataForUI_gps() throws TimeoutException {
|
||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
|
||||
.getService();
|
||||
RecordingData recordingData;
|
||||
|
||||
// given
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:02Z");
|
||||
|
||||
// when
|
||||
service.startNewTrack();
|
||||
disconnectRealGps(trackPointCreator.getSensorManager().getGpsHandler());
|
||||
trackPointCreator.getSensorManager().reset();
|
||||
RecordingData initialRecordingData = waitUntilLatestTrackPointMatches(service, Instant.parse("2020-02-02T02:02:02Z"), false);
|
||||
|
||||
// then
|
||||
TrackPoint initialTrackPoint = initialRecordingData.latestTrackPoint();
|
||||
assertEquals(TrackPoint.Type.TRACKPOINT, initialTrackPoint.type());
|
||||
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), initialTrackPoint.getTime());
|
||||
|
||||
|
||||
// when
|
||||
// In this step, the data is saved (and Aggregator.resetAggregated called), but data should still be shown in the UI.
|
||||
trackPointCreator.setClock("2020-02-02T02:02:03Z");
|
||||
ExportImportTest.sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3.1234567, 14.0014567, 10, 13, 15, 1020.25, 1f);
|
||||
recordingData = waitUntilLatestTrackPointMatches(service, Instant.parse("2020-02-02T02:02:03Z"), true);
|
||||
|
||||
// then
|
||||
new TrackPointAssert().assertEquals(new TrackPoint(
|
||||
null,
|
||||
TrackPoint.Type.TRACKPOINT,
|
||||
new Position(
|
||||
Instant.parse("2020-02-02T02:02:03Z"),
|
||||
3.1234567, 14.0014567, null,
|
||||
Altitude.EGM2008.of(1013.05), null,
|
||||
null,
|
||||
Speed.of(15)),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new AltitudeGainLoss(1, 1)
|
||||
),
|
||||
recordingData.latestTrackPoint());
|
||||
}
|
||||
|
||||
private void disconnectRealGps(GpsHandler gpsHandler) {
|
||||
if (gpsHandler.driver != null) {
|
||||
gpsHandler.driver.disconnect();
|
||||
}
|
||||
gpsHandler.onConnected(null, GpsInternal.LOCATION_PROVIDER);
|
||||
}
|
||||
|
||||
private RecordingData waitUntilLatestTrackPointMatches(TrackRecordingService service, Instant expectedTime, boolean requiresLocation) {
|
||||
return waitForValue(service.getRecordingDataObservable(),
|
||||
recordingData -> {
|
||||
TrackPoint latestTrackPoint = recordingData != null ? recordingData.latestTrackPoint() : null;
|
||||
return latestTrackPoint != null
|
||||
&& expectedTime.equals(latestTrackPoint.getTime())
|
||||
&& (!requiresLocation || latestTrackPoint.position().hasLocation());
|
||||
},
|
||||
RECORDING_DATA_UPDATE_INTERVAL.multipliedBy(3),
|
||||
"latest track point to reach " + expectedTime + (requiresLocation ? " with location" : ""));
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -556,6 +556,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
|
||||
|
||||
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
|
||||
int lapHeartRate = Math.round(data.second.avgHeartRate().getBPM());
|
||||
|
||||
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
|
||||
|
||||
@@ -573,7 +574,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
|
||||
|
||||
// then
|
||||
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement);
|
||||
assertEquals("12:16 AM. Total distance 14.2 kilometers. 16 minutes 39 seconds. Average moving speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Lap heart rate " + lapHeartRate + " bpm.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -589,6 +590,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
PreferencesUtils.setVoiceAnnounceLapSpeedPace(false);
|
||||
|
||||
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
|
||||
int lapHeartRate = Math.round(data.second.avgHeartRate().getBPM());
|
||||
|
||||
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null);
|
||||
|
||||
@@ -606,7 +608,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
|
||||
|
||||
// then
|
||||
assertEquals("12:16 AM. Current heart rate 60 bpm. Average heart rate 180 bpm. Lap heart rate 133 bpm.", announcement);
|
||||
assertEquals("12:16 AM. Current heart rate 60 bpm. Average heart rate 180 bpm. Lap heart rate " + lapHeartRate + " bpm.", announcement);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package de.dennisguse.opentracks.settings;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.BuildConfig;
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ImportExportSettingsIntentTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Test
|
||||
public void applicationIdResource_matchesInstalledPackage() {
|
||||
assertEquals(BuildConfig.APPLICATION_ID, context.getString(R.string.applicationId));
|
||||
assertEquals(context.getPackageName(), context.getString(R.string.applicationId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void importExportPreferenceTargets_areResolvableInCurrentPackage() throws PackageManager.NameNotFoundException {
|
||||
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity");
|
||||
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity");
|
||||
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserOneFileActivity");
|
||||
assertResolvable("de.dennisguse.opentracks.io.file.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity");
|
||||
}
|
||||
|
||||
private void assertResolvable(String activityClassName) throws PackageManager.NameNotFoundException {
|
||||
String applicationId = context.getString(R.string.applicationId);
|
||||
ComponentName componentName = new ComponentName(applicationId, activityClassName);
|
||||
|
||||
ActivityInfo activityInfo = context.getPackageManager().getActivityInfo(componentName, 0);
|
||||
assertEquals(applicationId, activityInfo.packageName);
|
||||
|
||||
ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(new Intent().setComponent(componentName), 0);
|
||||
assertNotNull(resolveInfo);
|
||||
}
|
||||
}
|
||||
+53
-27
@@ -1,37 +1,41 @@
|
||||
package de.dennisguse.opentracks.ui.aggregatedStatistics;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.performClick;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilRecyclerViewItemCount;
|
||||
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.resetTrackRecordingServiceAndDeleteTracks;
|
||||
import static de.dennisguse.opentracks.util.TestEnvironmentUtils.stopTrackRecordingService;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
||||
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.espresso.matcher.ViewMatchers;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
|
||||
import org.hamcrest.core.IsInstanceOf;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TestUtil;
|
||||
import de.dennisguse.opentracks.TrackListActivity;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
@@ -42,47 +46,69 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class EspressoAggregatedFilterTest {
|
||||
|
||||
private final String ACTIVITY_TYPE_LOCALIZED = "activityTypeLocalized";
|
||||
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||
private final String ACTIVITY_TYPE_LOCALIZED = "activityTypeLocalized";
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||
|
||||
private ActivityScenario<AggregatedStatisticsActivity> scenario;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
resetTrackRecordingServiceAndDeleteTracks();
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(androidx.test.core.app.ApplicationProvider.getApplicationContext());
|
||||
|
||||
Pair<Track, List<TrackPoint>> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 20);
|
||||
TrackBuilder trackBuilder = new TrackBuilder(pair.first);
|
||||
trackBuilder.setActivityTypeLocalized(ACTIVITY_TYPE_LOCALIZED);
|
||||
TestDataUtil.insertTrackWithLocations(new ContentProviderUtils(ApplicationProvider.getApplicationContext()), trackBuilder.getTrack(), pair.second);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, trackBuilder.getTrack(), pair.second);
|
||||
|
||||
scenario = ActivityScenario.launch(AggregatedStatisticsActivity.class);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
stopTrackRecordingService();
|
||||
if (scenario != null) {
|
||||
scenario.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void espressoAggregatedFilterTest() {
|
||||
// open AggregatedStatisticsActivity through toolbar's menu item
|
||||
onView(ViewMatchers.withId(R.id.bottom_app_bar)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.aggregated_stats_button))));
|
||||
onView(withId(R.id.aggregated_stats_button)).perform(click());
|
||||
waitUntilDisplayed(withId(R.id.bottom_app_bar), UI_TIMEOUT);
|
||||
waitUntilDisplayed(withId(R.id.aggregated_statistics_filter), UI_TIMEOUT);
|
||||
waitUntilRecyclerViewItemCount(withId(R.id.aggregated_stats_list), 1, UI_TIMEOUT);
|
||||
waitUntilDisplayed(allOf(withId(R.id.aggregated_stats_type_label), withText(ACTIVITY_TYPE_LOCALIZED)), UI_TIMEOUT);
|
||||
|
||||
// open FilterDialogFragment through toolbar's menu item
|
||||
onView(withId(R.id.bottom_app_bar)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
|
||||
onView(withId(R.id.aggregated_statistics_filter)).perform(click());
|
||||
// open FilterDialogFragment through the current bottom app bar action item
|
||||
onView(withId(R.id.bottom_app_bar))
|
||||
.check(matches(isDisplayed()));
|
||||
onView(withId(R.id.bottom_app_bar))
|
||||
.check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
|
||||
onView(withId(R.id.aggregated_statistics_filter))
|
||||
.perform(performClick());
|
||||
|
||||
// check there's a checkbox with ACTIVITY_TYPE_LOCALIZED text
|
||||
ViewInteraction checkBox = onView(
|
||||
allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
|
||||
withParent(allOf(withId(R.id.filter_items),
|
||||
withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))),
|
||||
isDisplayed()));
|
||||
checkBox.perform(waitFor(2000));
|
||||
Matcher<View> activityTypeFilter = allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
|
||||
withParent(allOf(withId(R.id.filter_items),
|
||||
withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))),
|
||||
isDisplayed());
|
||||
onView(activityTypeFilter)
|
||||
.inRoot(isDialog())
|
||||
.check(matches(isDisplayed()));
|
||||
|
||||
// check there's an edit text for "from date"
|
||||
onView(withId(R.id.filter_date_edit_text_from)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.filter_date_edit_text_from))
|
||||
.inRoot(isDialog())
|
||||
.check(matches(isDisplayed()));
|
||||
|
||||
// check there's an edit text for "to date"
|
||||
onView(withId(R.id.filter_date_edit_text_to)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.filter_date_edit_text_to))
|
||||
.inRoot(isDialog())
|
||||
.check(matches(isDisplayed()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+67
-22
@@ -35,7 +35,10 @@ public class IntervalStatisticsUpdaterTest {
|
||||
float distanceInterval = 1000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(50, distanceInterval);
|
||||
IntervalStatisticsComputation computation = computeIntervalStatistics(50, distanceInterval);
|
||||
assertIntervalStatisticsComputation(computation, 50, distanceInterval);
|
||||
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
|
||||
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +52,10 @@ public class IntervalStatisticsUpdaterTest {
|
||||
float distanceInterval = 1000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(200, distanceInterval);
|
||||
IntervalStatisticsComputation computation = computeIntervalStatistics(200, distanceInterval);
|
||||
assertIntervalStatisticsComputation(computation, 200, distanceInterval);
|
||||
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
|
||||
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +69,10 @@ public class IntervalStatisticsUpdaterTest {
|
||||
float distanceInterval = 3000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(3000, distanceInterval);
|
||||
IntervalStatisticsComputation computation = computeIntervalStatistics(3000, distanceInterval);
|
||||
assertIntervalStatisticsComputation(computation, 3000, distanceInterval);
|
||||
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
|
||||
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +86,10 @@ public class IntervalStatisticsUpdaterTest {
|
||||
float distanceInterval = 3000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(1000, distanceInterval);
|
||||
IntervalStatisticsComputation computation = computeIntervalStatistics(1000, distanceInterval);
|
||||
assertIntervalStatisticsComputation(computation, 1000, distanceInterval);
|
||||
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
|
||||
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +103,10 @@ public class IntervalStatisticsUpdaterTest {
|
||||
float distanceInterval = 1000f;
|
||||
|
||||
// when and then
|
||||
whenAndThen(10000, distanceInterval);
|
||||
IntervalStatisticsComputation computation = computeIntervalStatistics(10000, distanceInterval);
|
||||
assertIntervalStatisticsComputation(computation, 10000, distanceInterval);
|
||||
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
|
||||
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -122,15 +137,18 @@ public class IntervalStatisticsUpdaterTest {
|
||||
Pair<Track, List<TrackPoint>> trackWithStats = new Pair<>(dummyTrack, trackPoints);
|
||||
|
||||
// when and then
|
||||
whenAndThen(trackWithStats.first, trackWithStats.second, numberOfPoints, distanceInterval);
|
||||
IntervalStatisticsComputation computation = computeIntervalStatistics(trackWithStats.first, trackWithStats.second, distanceInterval);
|
||||
assertIntervalStatisticsComputation(computation, numberOfPoints, distanceInterval);
|
||||
assertEquals((int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval), computation.intervalList.size());
|
||||
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
|
||||
}
|
||||
|
||||
private void whenAndThen(int numberOfPoints, float distanceInterval) {
|
||||
private IntervalStatisticsComputation computeIntervalStatistics(int numberOfPoints, float distanceInterval) {
|
||||
Pair<Track, List<TrackPoint>> trackWithStats = buildTrackWithTrackPoints(numberOfPoints);
|
||||
whenAndThen(trackWithStats.first, trackWithStats.second, numberOfPoints, distanceInterval);
|
||||
return computeIntervalStatistics(trackWithStats.first, trackWithStats.second, distanceInterval);
|
||||
}
|
||||
|
||||
private void whenAndThen(Track track, List<TrackPoint> trackPoints, int numberOfPoints, float distanceInterval) {
|
||||
private IntervalStatisticsComputation computeIntervalStatistics(Track track, List<TrackPoint> trackPoints, float distanceInterval) {
|
||||
IntervalStatisticsUpdater intervalStatistics = new IntervalStatisticsUpdater(Distance.of(distanceInterval));
|
||||
|
||||
intervalStatistics.addTrackPoints(trackPoints.iterator());
|
||||
@@ -157,23 +175,27 @@ public class IntervalStatisticsUpdaterTest {
|
||||
}
|
||||
}
|
||||
|
||||
// then
|
||||
assertEquals(track.statistics().totalDuration().toSeconds(), totalTime, 0.01);
|
||||
assertEquals(track.statistics().totalDistance().toM(), totalDistance.toM(), 0.01);
|
||||
assertEquals(intervalList.size(), (int) Math.ceil(track.statistics().totalDistance().toM() / distanceInterval));
|
||||
if (totalGain != null && totalLoss != null) {
|
||||
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
|
||||
assertEquals(totalLoss, numberOfPoints * TestDataUtil.ALTITUDE_LOSS, 0.1);
|
||||
return new IntervalStatisticsComputation(track, intervalList, totalDistance, totalTime, totalGain, totalLoss);
|
||||
}
|
||||
|
||||
private void assertIntervalStatisticsComputation(IntervalStatisticsComputation computation, int numberOfPoints, float distanceInterval) {
|
||||
assertEquals(computation.track.statistics().totalDuration().toSeconds(), computation.totalTime, 0.01);
|
||||
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
|
||||
assertEquals(computation.intervalList.size(), (int) Math.ceil(computation.track.statistics().totalDistance().toM() / distanceInterval));
|
||||
if (computation.totalGain != null && computation.totalLoss != null) {
|
||||
assertEquals(computation.totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
|
||||
assertEquals(computation.totalLoss, numberOfPoints * TestDataUtil.ALTITUDE_LOSS, 0.1);
|
||||
|
||||
} else {
|
||||
assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(i -> i.altitudeGainLoss() != null));
|
||||
assertTrue(computation.intervalList.stream().noneMatch(i -> i.altitudeGainLoss() != null));
|
||||
}
|
||||
|
||||
for (int i = 0; i < intervalList.size() - 1; i++) {
|
||||
assertEquals(intervalList.get(i).totalDistance().toM(), distanceInterval, 0.001);
|
||||
totalDistance = totalDistance.minus(intervalList.get(i).totalDistance());
|
||||
Distance remainingDistance = computation.totalDistance;
|
||||
for (int i = 0; i < computation.intervalList.size() - 1; i++) {
|
||||
assertEquals(computation.intervalList.get(i).totalDistance().toM(), distanceInterval, 0.001);
|
||||
remainingDistance = remainingDistance.minus(computation.intervalList.get(i).totalDistance());
|
||||
}
|
||||
assertEquals(intervalList.get(intervalList.size() - 1).totalDistance().toM(), totalDistance.toM(), 0.01);
|
||||
assertEquals(computation.intervalList.get(computation.intervalList.size() - 1).totalDistance().toM(), remainingDistance.toM(), 0.01);
|
||||
}
|
||||
|
||||
private static Pair<Track, List<TrackPoint>> buildTrackWithTrackPoints(int numberOfPoints) {
|
||||
@@ -195,4 +217,27 @@ public class IntervalStatisticsUpdaterTest {
|
||||
|
||||
return new Pair<>(track, trackPoints);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class IntervalStatisticsComputation {
|
||||
private final Track track;
|
||||
private final List<Statistics> intervalList;
|
||||
private final Distance totalDistance;
|
||||
private final float totalTime;
|
||||
private final Float totalGain;
|
||||
private final Float totalLoss;
|
||||
|
||||
private IntervalStatisticsComputation(Track track,
|
||||
List<Statistics> intervalList,
|
||||
Distance totalDistance,
|
||||
float totalTime,
|
||||
Float totalGain,
|
||||
Float totalLoss) {
|
||||
this.track = track;
|
||||
this.intervalList = intervalList;
|
||||
this.totalDistance = totalDistance;
|
||||
this.totalTime = totalTime;
|
||||
this.totalGain = totalGain;
|
||||
this.totalLoss = totalLoss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
|
||||
import androidx.test.runner.lifecycle.Stage;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class ActivityTestUtils {
|
||||
|
||||
private ActivityTestUtils() {
|
||||
}
|
||||
|
||||
public static <T extends Activity> T waitForResumedActivity(Class<T> activityClass, Duration timeout) {
|
||||
T resumedActivity = getResumedActivity(activityClass);
|
||||
if (resumedActivity != null) {
|
||||
return resumedActivity;
|
||||
}
|
||||
|
||||
Application application = ApplicationProvider.getApplicationContext();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<T> resumedActivityRef = new AtomicReference<>();
|
||||
|
||||
Application.ActivityLifecycleCallbacks callbacks = new Application.ActivityLifecycleCallbacks() {
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {
|
||||
if (activityClass.isInstance(activity)) {
|
||||
resumedActivityRef.set(activityClass.cast(activity));
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) {
|
||||
}
|
||||
};
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
|
||||
application.registerActivityLifecycleCallbacks(callbacks);
|
||||
|
||||
T currentResumedActivity = findResumedActivity(activityClass);
|
||||
if (currentResumedActivity != null) {
|
||||
resumedActivityRef.set(currentResumedActivity);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
if (!latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
|
||||
fail("Timed out waiting for resumed activity " + activityClass.getSimpleName());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
fail("Interrupted while waiting for resumed activity " + activityClass.getSimpleName());
|
||||
} finally {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> application.unregisterActivityLifecycleCallbacks(callbacks));
|
||||
}
|
||||
|
||||
return resumedActivityRef.get();
|
||||
}
|
||||
|
||||
private static <T extends Activity> T getResumedActivity(Class<T> activityClass) {
|
||||
AtomicReference<T> resumedActivity = new AtomicReference<>();
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> resumedActivity.set(findResumedActivity(activityClass)));
|
||||
return resumedActivity.get();
|
||||
}
|
||||
|
||||
private static <T extends Activity> T findResumedActivity(Class<T> activityClass) {
|
||||
for (Activity activity : ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)) {
|
||||
if (activityClass.isInstance(activity)) {
|
||||
return activityClass.cast(activity);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.SystemClock;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.ListView;
|
||||
import android.widget.AdapterView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.espresso.PerformException;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
|
||||
import androidx.test.runner.lifecycle.Stage;
|
||||
import androidx.test.espresso.util.HumanReadables;
|
||||
import androidx.test.espresso.util.TreeIterables;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
@@ -16,61 +27,114 @@ import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class EspressoUtils {
|
||||
|
||||
public static Matcher<View> childAtPosition(
|
||||
final Matcher<View> parentMatcher, final int position) {
|
||||
private static final long VIEW_POLLING_INTERVAL_MS = 50;
|
||||
|
||||
public static Matcher<View> withRecyclerViewItemCount(final int count) {
|
||||
return new TypeSafeMatcher<>() {
|
||||
@Override
|
||||
protected boolean matchesSafely(View view) {
|
||||
if (!(view instanceof RecyclerView recyclerView)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RecyclerView.Adapter<?> adapter = recyclerView.getAdapter();
|
||||
return adapter != null && adapter.getItemCount() == count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("Child at position " + position + " in parent ");
|
||||
parentMatcher.describeTo(description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matchesSafely(View view) {
|
||||
ViewParent parent = view.getParent();
|
||||
return parent instanceof ViewGroup && parentMatcher.matches(parent)
|
||||
&& view.equals(((ViewGroup) parent).getChildAt(position));
|
||||
description.appendText("RecyclerView should have " + count + " items");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Matcher<View> withListSize (final int size) {
|
||||
public static Matcher<View> withSelectedTab(final int index) {
|
||||
return new TypeSafeMatcher<>() {
|
||||
@Override
|
||||
public boolean matchesSafely(final View view) {
|
||||
return ((ListView) view).getCount() == size;
|
||||
protected boolean matchesSafely(View view) {
|
||||
return view instanceof TabLayout tabLayout
|
||||
&& tabLayout.getSelectedTabPosition() == index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(final Description description) {
|
||||
description.appendText("ListView should have " + size + " items");
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("TabLayout should have selected tab " + index);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ViewAction waitFor(final long duration_ms) {
|
||||
return new ViewAction() {
|
||||
|
||||
public static Matcher<View> withViewPagerCurrentItem(final int index) {
|
||||
return new TypeSafeMatcher<>() {
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Wait for milliseconds.";
|
||||
protected boolean matchesSafely(View view) {
|
||||
return view instanceof ViewPager2 viewPager2
|
||||
&& viewPager2.getCurrentItem() == index
|
||||
&& viewPager2.getScrollState() == ViewPager2.SCROLL_STATE_IDLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isDisplayed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, final View view) {
|
||||
uiController.loopMainThreadForAtLeast(duration_ms);
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("ViewPager2 should be idle on item " + index);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void waitForResumedView(final Matcher<View> viewMatcher, final Duration timeout) {
|
||||
waitForResumedViewCondition(viewMatcher, ignored -> true, "view to match " + viewMatcher, timeout);
|
||||
}
|
||||
|
||||
public static void waitUntilDisplayed(final Matcher<View> viewMatcher, final Duration timeout) {
|
||||
waitForResumedView(allOf(viewMatcher, isDisplayed()), timeout);
|
||||
}
|
||||
|
||||
public static void waitUntilRecyclerViewItemCount(final Matcher<View> viewMatcher, final int expectedCount, final Duration timeout) {
|
||||
waitForResumedViewCondition(
|
||||
allOf(viewMatcher, isDisplayed()),
|
||||
view -> withRecyclerViewItemCount(expectedCount).matches(view),
|
||||
"RecyclerView item count to be " + expectedCount,
|
||||
timeout);
|
||||
}
|
||||
|
||||
public static void waitForResumedViewCondition(final Matcher<View> viewMatcher,
|
||||
final Predicate<View> condition,
|
||||
final String conditionDescription,
|
||||
final Duration timeout) {
|
||||
long deadline = SystemClock.uptimeMillis() + timeout.toMillis();
|
||||
|
||||
do {
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
|
||||
AtomicBoolean matched = new AtomicBoolean(false);
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
|
||||
for (Activity activity : ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)) {
|
||||
View decorView = activity.getWindow() != null ? activity.getWindow().getDecorView() : null;
|
||||
if (decorView == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (View child : TreeIterables.breadthFirstViewTraversal(decorView)) {
|
||||
if (viewMatcher.matches(child) && condition.test(child)) {
|
||||
matched.set(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (matched.get()) {
|
||||
return;
|
||||
}
|
||||
} while (SystemClock.uptimeMillis() < deadline);
|
||||
|
||||
throw new AssertionError("Timed out waiting for " + conditionDescription);
|
||||
}
|
||||
|
||||
public static ViewAction selectTabAtIndex(final int index) {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
@@ -91,4 +155,135 @@ public class EspressoUtils {
|
||||
};
|
||||
}
|
||||
|
||||
public static ViewAction performClick() {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "invoke View.performClick()";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isDisplayed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
if (!view.performClick()) {
|
||||
throw new PerformException.Builder()
|
||||
.withActionDescription(getDescription())
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.build();
|
||||
}
|
||||
uiController.loopMainThreadUntilIdle();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ViewAction performClickOnAncestor() {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "invoke performClick() on the matched view or its ancestor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isAssignableFrom(View.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
View current = view;
|
||||
while (current != null) {
|
||||
if (current.performClick()) {
|
||||
uiController.loopMainThreadUntilIdle();
|
||||
return;
|
||||
}
|
||||
|
||||
ViewParent parent = current.getParent();
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
|
||||
current = view;
|
||||
while (current != null) {
|
||||
ViewParent parent = current.getParent();
|
||||
if (parent instanceof AdapterView<?> adapterView) {
|
||||
int position = adapterView.getPositionForView(current);
|
||||
if (position != AdapterView.INVALID_POSITION
|
||||
&& adapterView.performItemClick(current, position, adapterView.getAdapter().getItemId(position))) {
|
||||
uiController.loopMainThreadUntilIdle();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
|
||||
throw new PerformException.Builder()
|
||||
.withActionDescription(getDescription())
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.build();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ViewAction performLongClick() {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "invoke View.performLongClick()";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isDisplayed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
if (!view.performLongClick()) {
|
||||
throw new PerformException.Builder()
|
||||
.withActionDescription(getDescription())
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.build();
|
||||
}
|
||||
uiController.loopMainThreadUntilIdle();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ViewAction performLongClickOnAncestor() {
|
||||
return new ViewAction() {
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "invoke performLongClick() on the matched view or its ancestor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matcher<View> getConstraints() {
|
||||
return isDisplayed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
View current = view;
|
||||
while (current != null) {
|
||||
if (current.performLongClick()) {
|
||||
uiController.loopMainThreadUntilIdle();
|
||||
return;
|
||||
}
|
||||
|
||||
ViewParent parent = current.getParent();
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
|
||||
throw new PerformException.Builder()
|
||||
.withActionDescription(getDescription())
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.build();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class LiveDataTestUtils {
|
||||
|
||||
private LiveDataTestUtils() {
|
||||
}
|
||||
|
||||
public static <T> T waitForValue(LiveData<T> liveData,
|
||||
Predicate<T> predicate,
|
||||
Duration timeout,
|
||||
String description) {
|
||||
AtomicReference<T> lastValue = new AtomicReference<>(liveData.getValue());
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
Observer<T> observer = value -> {
|
||||
lastValue.set(value);
|
||||
if (predicate.test(value)) {
|
||||
latch.countDown();
|
||||
}
|
||||
};
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
|
||||
liveData.observeForever(observer);
|
||||
|
||||
T currentValue = liveData.getValue();
|
||||
lastValue.set(currentValue);
|
||||
if (predicate.test(currentValue)) {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
if (!latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
|
||||
fail("Timed out waiting for " + description + ". Last value: " + lastValue.get());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
fail("Interrupted while waiting for " + description);
|
||||
} finally {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> liveData.removeObserver(observer));
|
||||
}
|
||||
|
||||
return lastValue.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
|
||||
public final class TestEnvironmentUtils {
|
||||
|
||||
private TestEnvironmentUtils() {
|
||||
}
|
||||
|
||||
public static void stopTrackRecordingService() {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
context.stopService(new Intent(context, TrackRecordingService.class));
|
||||
}
|
||||
|
||||
public static void deleteAllTracks() {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
new ContentProviderUtils(context).deleteAllTracks(context);
|
||||
}
|
||||
|
||||
public static void resetTrackRecordingServiceAndDeleteTracks() {
|
||||
stopTrackRecordingService();
|
||||
deleteAllTracks();
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
|
||||
public final class TrackRecordingServiceTestUtils {
|
||||
|
||||
private TrackRecordingServiceTestUtils() {
|
||||
}
|
||||
|
||||
public static void waitForRecordingState(boolean isRecording, Duration timeout) {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
CountDownLatch connectedLatch = new CountDownLatch(1);
|
||||
AtomicReference<TrackRecordingService> serviceRef = new AtomicReference<>();
|
||||
TrackRecordingServiceConnection connection = new TrackRecordingServiceConnection((service, self) -> {
|
||||
serviceRef.set(service);
|
||||
connectedLatch.countDown();
|
||||
});
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> connection.bind(context));
|
||||
|
||||
try {
|
||||
if (!connectedLatch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
|
||||
fail("Timed out waiting to bind TrackRecordingService");
|
||||
}
|
||||
|
||||
LiveDataTestUtils.waitForValue(serviceRef.get().getRecordingStatusObservable(),
|
||||
status -> status != null && status.isRecording() == isRecording,
|
||||
timeout,
|
||||
"recording state to become " + isRecording);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
fail("Interrupted while waiting for recording state " + isRecording);
|
||||
} finally {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> connection.unbind(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
|
||||
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static de.dennisguse.opentracks.util.ActivityTestUtils.waitForResumedActivity;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.performClick;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.performLongClick;
|
||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitUntilDisplayed;
|
||||
import static de.dennisguse.opentracks.util.TrackRecordingServiceTestUtils.waitForRecordingState;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackListActivity;
|
||||
import de.dennisguse.opentracks.TrackRecordingActivity;
|
||||
import de.dennisguse.opentracks.TrackStoppedActivity;
|
||||
|
||||
public final class TrackUiTestUtils {
|
||||
|
||||
private TrackUiTestUtils() {
|
||||
}
|
||||
|
||||
public static void startRecordingFromTrackList(Duration timeout) {
|
||||
onView(withId(R.id.track_list_fab_action))
|
||||
.perform(performClick());
|
||||
waitForResumedActivity(TrackRecordingActivity.class, timeout);
|
||||
waitForRecordingState(true, timeout);
|
||||
waitUntilDisplayed(withId(R.id.track_recording_fab_action), timeout);
|
||||
}
|
||||
|
||||
public static void stopRecordingToTrackStopped(Duration timeout, @IdRes int... expectedVisibleViewIds) {
|
||||
onView(withId(R.id.track_recording_fab_action))
|
||||
.perform(performLongClick());
|
||||
waitForResumedActivity(TrackStoppedActivity.class, timeout);
|
||||
waitUntilDisplayed(withId(R.id.track_edit_name), timeout);
|
||||
for (int viewId : expectedVisibleViewIds) {
|
||||
waitUntilDisplayed(withId(viewId), timeout);
|
||||
}
|
||||
onView(withId(R.id.track_edit_name))
|
||||
.perform(closeSoftKeyboard());
|
||||
}
|
||||
|
||||
public static void resumeStoppedTrack(Duration timeout) {
|
||||
onView(allOf(withId(R.id.resume_button), isClickable()))
|
||||
.perform(performClick());
|
||||
waitForResumedActivity(TrackRecordingActivity.class, timeout);
|
||||
waitForRecordingState(true, timeout);
|
||||
waitUntilDisplayed(withId(R.id.track_recording_fab_action), timeout);
|
||||
}
|
||||
|
||||
public static void finishStoppedTrack(Duration timeout) {
|
||||
onView(withId(R.id.finish_button))
|
||||
.perform(performClick());
|
||||
waitForResumedActivity(TrackListActivity.class, timeout);
|
||||
waitUntilDisplayed(withId(R.id.track_list), timeout);
|
||||
}
|
||||
|
||||
public static void renameStoppedTrack(String name, Duration timeout) {
|
||||
waitUntilDisplayed(withId(R.id.track_edit_name), timeout);
|
||||
onView(withId(R.id.track_edit_name))
|
||||
.perform(scrollTo(), replaceText(name), closeSoftKeyboard());
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@
|
||||
"2020-02-02T03:02:04+01:00","TRACKPOINT",,,,,,54,1,1,10,66,3,50
|
||||
"2020-02-02T03:02:15+01:00","TRACKPOINT",,,,,,,,,,68,3,50
|
||||
"2020-02-02T03:02:17+01:00","TRACKPOINT",3.123456,14.001456,1020.2,10,,18,0,0,2,69,3,50
|
||||
"2020-02-02T03:02:18+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
|
||||
"2020-02-02T03:02:18+01:00","SEGMENT_END_MANUAL",3.123456,14.001456,1020.2,10,,18,0,0,0,69,3,50
|
||||
"2020-02-02T03:03:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
|
||||
"2020-02-02T03:03:21+01:00","TRACKPOINT",3.123456,14.002456,999.1,10,,54,0,0,,,,
|
||||
"2020-02-02T03:03:22+01:00","SEGMENT_START_AUTOMATIC",3.123456,16,999.1,10,,54,0,0,,,,
|
||||
"2020-02-02T03:03:30+01:00","IDLE",,,,,,,0,0,,,,
|
||||
"2020-02-02T03:03:50+01:00","TRACKPOINT",3.123456,16.001,999.1,10,,54,0,0,,,,
|
||||
"2020-02-02T03:04:00+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
|
||||
"2020-02-02T03:04:00+01:00","SEGMENT_END_MANUAL",,,,,,,0,0,,,,
|
||||
|
@@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">OpenTracks (Debug)</string>
|
||||
<string name="launcher_name" translatable="false">OpenTracks (Debug)</string>
|
||||
</resources>
|
||||
|
||||
@@ -78,7 +78,7 @@ limitations under the License.
|
||||
android:hardwareAccelerated="true"
|
||||
android:hasFragileUserData="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:label="@string/launcher_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/DayNightDynamicTheme"> <!-- Default for AppCompatActivities -->
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import java.util.Objects;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.databinding.TrackListBinding;
|
||||
import de.dennisguse.opentracks.publicapi.DataProvider;
|
||||
import de.dennisguse.opentracks.sensors.GpsStatusValue;
|
||||
import de.dennisguse.opentracks.services.MissingPermissionException;
|
||||
import de.dennisguse.opentracks.services.RecordingStatus;
|
||||
@@ -62,7 +63,6 @@ import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragm
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
|
||||
import de.dennisguse.opentracks.ui.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.ui.util.RecyclerViewSwipeDeleteCallback;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||
|
||||
@@ -367,7 +367,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
}
|
||||
|
||||
if (itemId == R.id.list_context_menu_show_on_map) {
|
||||
IntentDashboardUtils.showTrackOnMap(this, false, trackIds);
|
||||
DataProvider.showTrackOnMap(this, false, trackIds);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import de.dennisguse.opentracks.data.TrackDataHub;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
|
||||
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
|
||||
import de.dennisguse.opentracks.publicapi.DataProvider;
|
||||
import de.dennisguse.opentracks.services.RecordingStatus;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
@@ -45,7 +46,6 @@ import de.dennisguse.opentracks.share.ShareUtils;
|
||||
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.ui.intervals.IntervalsFragment;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
/**
|
||||
@@ -167,7 +167,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
|
||||
IntentDashboardUtils.showTrackOnMap(this, false, trackId);
|
||||
DataProvider.showTrackOnMap(this, false, trackId);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.databinding.TrackRecordingBinding;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
|
||||
import de.dennisguse.opentracks.publicapi.DataProvider;
|
||||
import de.dennisguse.opentracks.sensors.GpsStatusValue;
|
||||
import de.dennisguse.opentracks.services.RecordingStatus;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
@@ -49,7 +50,6 @@ import de.dennisguse.opentracks.ui.intervals.IntervalsFragment;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
|
||||
import de.dennisguse.opentracks.ui.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
/**
|
||||
@@ -267,7 +267,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
|
||||
IntentDashboardUtils.showTrackOnMap(this, true, trackId);
|
||||
DataProvider.showTrackOnMap(this, true, trackId);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.BuildConfig;
|
||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
import de.dennisguse.opentracks.data.models.AltitudeExtremities;
|
||||
@@ -69,21 +68,10 @@ public class ContentProviderUtils {
|
||||
|
||||
private static final String TAG = ContentProviderUtils.class.getSimpleName();
|
||||
|
||||
// The authority (the first part of the URI) for the app's content provider.
|
||||
static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
|
||||
|
||||
// The base URI for the app's content provider.
|
||||
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
|
||||
|
||||
private static final String ID_SEPARATOR = ",";
|
||||
|
||||
private final ContentResolver contentResolver;
|
||||
|
||||
//TODO Check if this can be used in more than one place. If not remove.
|
||||
public interface ContentProviderSelectionInterface {
|
||||
SelectionData buildSelection();
|
||||
}
|
||||
|
||||
public ContentProviderUtils(Context context) {
|
||||
contentResolver = context.getContentResolver();
|
||||
}
|
||||
@@ -753,7 +741,7 @@ public class ContentProviderUtils {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<AggregatedStatistic> getAggregatedStatisticsForTracks(ContentProviderSelectionInterface selection) {
|
||||
public List<AggregatedStatistic> getAggregatedStatisticsForTracks(TrackSelection selection) {
|
||||
SelectionData data = selection.buildSelection();
|
||||
try (Cursor cursor = contentResolver.query(TracksColumns.CONTENT_URI_AGGREGATED_STATISTICS, null, data.selection(), data.getSelectionArgs(), null)) {
|
||||
ArrayList<AggregatedStatistic> aggregatedStatistics = new ArrayList<>(cursor.getCount());
|
||||
|
||||
@@ -34,10 +34,12 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.dennisguse.opentracks.BuildConfig;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
import de.dennisguse.opentracks.publicapi.DataProvider;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
@@ -49,6 +51,11 @@ import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
*/
|
||||
public class CustomContentProvider extends ContentProvider {
|
||||
|
||||
// The authority (the first part of the URI) for the app's content provider.
|
||||
static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
|
||||
|
||||
// The base URI for the app's content provider.
|
||||
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
|
||||
private static final String TAG = CustomContentProvider.class.getSimpleName();
|
||||
|
||||
private static final String SQL_LIST_DELIMITER = ",";
|
||||
@@ -94,20 +101,23 @@ public class CustomContentProvider extends ContentProvider {
|
||||
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
|
||||
|
||||
public CustomContentProvider() {
|
||||
//TODO Add tests for used URIs (incl. Dashboard API)
|
||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath() + "/#", UrlType.TRACKPOINTS_BY_ID.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath() + "/#", UrlType.TRACKPOINTS_BY_ID.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_SENSOR_STATS.getPath() + "/#", UrlType.TRACKS_SENSOR_STATS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_AGGREGATED_STATISTICS.getPath(), UrlType.TRACKS_AGGREGATED_STATISTICS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_SENSOR_STATS.getPath() + "/#", UrlType.TRACKS_SENSOR_STATS.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_AGGREGATED_STATISTICS.getPath(), UrlType.TRACKS_AGGREGATED_STATISTICS.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.MARKERS_BY_ID.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.MARKERS_BY_TRACKID.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.MARKERS_BY_ID.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.MARKERS_BY_TRACKID.ordinal());
|
||||
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, UrlType.DASHBOARD_TRACKS_BY_IDS.getPath() + "*", UrlType.DASHBOARD_TRACKS_BY_IDS.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.getPath() + "*", UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.ordinal());
|
||||
uriMatcher.addURI(AUTHORITY_PACKAGE, UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.getPath() + "*", UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.ordinal());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -143,7 +153,7 @@ public class CustomContentProvider extends ContentProvider {
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(url, null, false);
|
||||
notifyChanges(url);
|
||||
|
||||
int totalChanges = getTotalChanges() - totalChangesBefore;
|
||||
Log.i(TAG, "Deleted " + totalChanges + " total rows from database");
|
||||
@@ -170,12 +180,21 @@ public class CustomContentProvider extends ContentProvider {
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri url) {
|
||||
String contentType = getUrlType(url).contentType;
|
||||
if (contentType != null) {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
//TODO replace with getUrlType(url).getContentType()
|
||||
return switch (getUrlType(url)) {
|
||||
case TRACKPOINTS -> TrackPointsColumns.CONTENT_TYPE;
|
||||
case TRACKPOINTS_BY_TRACKID -> TrackPointsColumns.CONTENT_ITEMTYPE;
|
||||
case TRACKS -> TracksColumns.CONTENT_TYPE;
|
||||
case MARKERS -> MarkerColumns.CONTENT_TYPE;
|
||||
case MARKERS_BY_TRACKID -> MarkerColumns.CONTENT_ITEMTYPE;
|
||||
case DASHBOARD_TRACKS_BY_IDS -> UrlType.DASHBOARD_TRACKS_BY_IDS.contentType;
|
||||
case DASHBOARD_TRACKPOINTS_BY_TRACKIDS -> UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.contentType;
|
||||
case DASHBOARD_MARKERS_BY_TRACKIDS -> UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.contentType;
|
||||
default -> throw new IllegalArgumentException("Unknown URL " + url);
|
||||
};
|
||||
}
|
||||
@@ -193,7 +212,8 @@ public class CustomContentProvider extends ContentProvider {
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(url, null, false);
|
||||
notifyChanges(url);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -216,7 +236,8 @@ public class CustomContentProvider extends ContentProvider {
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(url, null, false);
|
||||
notifyChanges(url);
|
||||
|
||||
return numInserted;
|
||||
}
|
||||
|
||||
@@ -225,6 +246,7 @@ public class CustomContentProvider extends ContentProvider {
|
||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
String sortOrder = null;
|
||||
String groupBy = null;
|
||||
//TODO queryBuilder.setStrict(true);
|
||||
//TODO queryBuilder.appendWhere() should use selectionArgs?
|
||||
switch (getUrlType(url)) {
|
||||
case TRACKPOINTS -> {
|
||||
@@ -239,6 +261,11 @@ public class CustomContentProvider extends ContentProvider {
|
||||
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
}
|
||||
case DASHBOARD_TRACKPOINTS_BY_TRACKIDS -> {
|
||||
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_TRACKPOINTS);
|
||||
}
|
||||
case TRACKS -> {
|
||||
if (projection != null && Arrays.asList(projection).contains(TracksColumns.MARKER_COUNT)) {
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME + " LEFT OUTER JOIN (SELECT " + MarkerColumns.TRACKID + " AS markerTrackId, COUNT(*) AS " + TracksColumns.MARKER_COUNT + " FROM " + MarkerColumns.TABLE_NAME + " GROUP BY " + MarkerColumns.TRACKID + ") ON (" + TracksColumns.TABLE_NAME + "." + TracksColumns._ID + "= markerTrackId)");
|
||||
@@ -251,6 +278,11 @@ public class CustomContentProvider extends ContentProvider {
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
}
|
||||
case DASHBOARD_TRACKS_BY_IDS -> {
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_TRACKS);
|
||||
}
|
||||
case TRACKS_SENSOR_STATS -> {
|
||||
//TODO Replace with SQLQueryBuilder?
|
||||
long trackId = ContentUris.parseId(url);
|
||||
@@ -281,6 +313,11 @@ public class CustomContentProvider extends ContentProvider {
|
||||
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
}
|
||||
case DASHBOARD_MARKERS_BY_TRACKIDS -> {
|
||||
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
queryBuilder.setProjectionMap(DataProvider.DATA_PROJECTIONMAP_MARKERS);
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unknown url " + url);
|
||||
}
|
||||
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder);
|
||||
@@ -313,10 +350,36 @@ public class CustomContentProvider extends ContentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
getContext().getContentResolver().notifyChange(url, null, false);
|
||||
notifyChanges(url);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private void notifyChanges(@NonNull Uri uri) {
|
||||
getContext().getContentResolver().notifyChange(uri, null, false);
|
||||
// Also notify dashboard URIs for compatibility
|
||||
Uri dashboardUri = getDashboardUri(uri);
|
||||
if (dashboardUri != null) {
|
||||
getContext().getContentResolver().notifyChange(dashboardUri, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri getDashboardUri(Uri uri) {
|
||||
String path = uri.getPath();
|
||||
if (path == null) return null;
|
||||
String dashboardPath;
|
||||
if (path.startsWith("/tracks")) {
|
||||
dashboardPath = path.replaceFirst("/tracks", "/dashboard/tracks");
|
||||
} else if (path.startsWith("/markers")) {
|
||||
dashboardPath = path.replaceFirst("/markers", "/dashboard/markers");
|
||||
} else if (path.startsWith("/trackpoints")) {
|
||||
dashboardPath = path.replaceFirst("/trackpoints", "/dashboard/trackpoints");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return Uri.parse(CONTENT_BASE_URI + dashboardPath);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private UrlType getUrlType(Uri url) {
|
||||
UrlType[] urlTypes = UrlType.values();
|
||||
@@ -372,9 +435,11 @@ public class CustomContentProvider extends ContentProvider {
|
||||
throw new SQLException("Failed to insert a marker " + url);
|
||||
}
|
||||
|
||||
private enum UrlType {
|
||||
//TODO Add ContentType as attribute
|
||||
public enum UrlType {
|
||||
TRACKPOINTS,
|
||||
TRACKPOINTS_BY_ID,
|
||||
//TODO Check if still used
|
||||
TRACKPOINTS_BY_TRACKID,
|
||||
TRACKS,
|
||||
TRACKS_BY_ID,
|
||||
@@ -382,6 +447,36 @@ public class CustomContentProvider extends ContentProvider {
|
||||
TRACKS_AGGREGATED_STATISTICS,
|
||||
MARKERS,
|
||||
MARKERS_BY_ID,
|
||||
MARKERS_BY_TRACKID
|
||||
@Deprecated //TODO Should not be used anymore
|
||||
MARKERS_BY_TRACKID,
|
||||
|
||||
DASHBOARD_TRACKS_BY_IDS(Uri.parse(CONTENT_BASE_URI + "/dashboard/tracks/"), TracksColumns.CONTENT_TYPE),
|
||||
DASHBOARD_TRACKPOINTS_BY_TRACKIDS(Uri.parse(CONTENT_BASE_URI + "/dashboard/trackpoints/"), TrackPointsColumns.CONTENT_TYPE),
|
||||
DASHBOARD_MARKERS_BY_TRACKIDS(Uri.parse(CONTENT_BASE_URI + "/dashboard/markers/"), MarkerColumns.CONTENT_TYPE);
|
||||
|
||||
private final Uri uri;
|
||||
|
||||
private final String contentType;
|
||||
|
||||
//TODO Add Uri as attribute:
|
||||
//TODO Add ContentType as attribute:
|
||||
@Deprecated()
|
||||
UrlType() {
|
||||
this.uri = null;
|
||||
this.contentType = null;
|
||||
}
|
||||
|
||||
UrlType(Uri uri, String contentType) {
|
||||
this.uri = uri;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
String getPath() {
|
||||
return uri.getPath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,12 +59,12 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
private static final String TRACKID_DELIMITER = "_";
|
||||
|
||||
static {
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getPreferenceId() + "/*/*", URI_GPX);
|
||||
uriMatcher.addURI(CustomContentProvider.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getPreferenceId() + "/*/*", URI_GPX);
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getPreferenceId() + "/*/*", URI_KML_WITH_TRACKDETAIL_SENSORDATA);
|
||||
uriMatcher.addURI(CustomContentProvider.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getPreferenceId() + "/*/*", URI_KML_WITH_TRACKDETAIL_SENSORDATA);
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getPreferenceId() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.CSV.getPreferenceId() + "/*/*", URI_CSV);
|
||||
uriMatcher.addURI(CustomContentProvider.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getPreferenceId() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
|
||||
uriMatcher.addURI(CustomContentProvider.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.CSV.getPreferenceId() + "/*/*", URI_CSV);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,11 +89,10 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
trackIdBuilder.deleteCharAt(trackIdBuilder.lastIndexOf(TRACKID_DELIMITER));
|
||||
|
||||
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getPreferenceId() + "/" + trackIdBuilder + "/" + Uri.encode(filename));
|
||||
String mime = getTypeMime(uri);
|
||||
|
||||
Log.d(TAG, "Created uri " + uri + " with MIME " + mime);
|
||||
Log.d(TAG, "Created uri " + uri + " with MIME " + trackFileFormat.getMimeType());
|
||||
|
||||
return new Pair<>(uri, mime);
|
||||
return new Pair<>(uri, trackFileFormat.getMimeType());
|
||||
}
|
||||
|
||||
static Set<Track.Id> parseURI(Uri uri) {
|
||||
@@ -126,6 +125,7 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static TrackFileFormat getTrackFileFormat(@NonNull Uri uri) {
|
||||
return switch (uriMatcher.match(uri)) {
|
||||
case URI_GPX -> TrackFileFormat.GPX;
|
||||
@@ -134,16 +134,10 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES ->
|
||||
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
|
||||
case URI_CSV -> TrackFileFormat.CSV;
|
||||
default ->
|
||||
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getTypeMime(@NonNull Uri uri) {
|
||||
return getTrackFileFormat(uri).getMimeType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
|
||||
if (uriMatcher.match(uri) == -1) {
|
||||
@@ -182,9 +176,9 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
String mime = getTypeMime(uri);
|
||||
if (mime != null) {
|
||||
return mime;
|
||||
TrackFileFormat trackFileFormat = getTrackFileFormat(uri);
|
||||
if (trackFileFormat != null) {
|
||||
return trackFileFormat.getMimeType();
|
||||
}
|
||||
|
||||
return super.getType(uri);
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.util.List;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
|
||||
public class TrackSelection implements ContentProviderUtils.ContentProviderSelectionInterface {
|
||||
public class TrackSelection {
|
||||
private final List<Track.Id> trackIds = new ArrayList<>();
|
||||
private final List<String> categories = new ArrayList<>();
|
||||
private Instant from;
|
||||
@@ -40,7 +40,6 @@ public class TrackSelection implements ContentProviderUtils.ContentProviderSelec
|
||||
return trackIds.isEmpty() && categories.isEmpty() && from == null && to == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentProviderUtils.SelectionData buildSelection() {
|
||||
String selection = "";
|
||||
List<String> selectionArgs = new ArrayList<>();
|
||||
|
||||
@@ -28,7 +28,6 @@ import androidx.annotation.VisibleForTesting;
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
//TODO All data should be final; no default values.
|
||||
public final class MarkerBuilder {
|
||||
|
||||
private Marker.Id id;
|
||||
@@ -57,14 +56,6 @@ public final class MarkerBuilder {
|
||||
this.position = trackPoint.position();
|
||||
}
|
||||
|
||||
public MarkerBuilder(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint, String name, String description, String typeLocalized, Uri photoUrl) {
|
||||
this(trackId, trackPoint);
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.typeLocalized = typeLocalized;
|
||||
this.photoUrl = photoUrl;
|
||||
}
|
||||
|
||||
public MarkerBuilder(Marker marker) {
|
||||
this.id = marker.id();
|
||||
this.trackId = marker.trackId();
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.annotation.Nullable;
|
||||
import java.time.Instant;
|
||||
|
||||
public record Position(
|
||||
@Nullable Instant time,
|
||||
@Nullable Instant time, //TODO It may be a good idea to move time out of Position
|
||||
@Nullable Double latitude,
|
||||
@Nullable Double longitude,
|
||||
@Nullable Distance horizontalAccuracy,
|
||||
@@ -17,10 +17,6 @@ public record Position(
|
||||
@Nullable Float bearing,
|
||||
@Nullable Speed speed
|
||||
) {
|
||||
@Deprecated
|
||||
public static Position empty() {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
public static Position of(@NonNull Instant time) {
|
||||
return new Position(
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@@ -63,7 +64,7 @@ public record TrackPoint(
|
||||
);
|
||||
}
|
||||
|
||||
@Deprecated //TOOD Private?
|
||||
@VisibleForTesting
|
||||
public TrackPoint(Type type, Instant now) {
|
||||
this(
|
||||
null,
|
||||
|
||||
@@ -22,7 +22,7 @@ import android.provider.BaseColumns;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.CustomContentProvider;
|
||||
|
||||
/**
|
||||
* Constants for markers table.
|
||||
@@ -32,8 +32,8 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
public interface MarkerColumns extends BaseColumns {
|
||||
|
||||
String TABLE_NAME = "markers";
|
||||
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_BY_TRACKID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
|
||||
Uri CONTENT_URI = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_BY_TRACKID = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
|
||||
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
|
||||
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.waypoint";
|
||||
String DEFAULT_SORT_ORDER = _ID;
|
||||
|
||||
@@ -19,7 +19,7 @@ package de.dennisguse.opentracks.data.tables;
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.CustomContentProvider;
|
||||
|
||||
/**
|
||||
* Constants for the track points table.
|
||||
@@ -29,8 +29,8 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
public interface TrackPointsColumns extends BaseColumns {
|
||||
|
||||
String TABLE_NAME = "trackpoints";
|
||||
Uri CONTENT_URI_BY_ID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_BY_TRACKID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
|
||||
Uri CONTENT_URI_BY_ID = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_BY_TRACKID = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
|
||||
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint";
|
||||
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.trackpoint";
|
||||
String DEFAULT_SORT_ORDER = _ID;
|
||||
|
||||
@@ -19,7 +19,7 @@ package de.dennisguse.opentracks.data.tables;
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.CustomContentProvider;
|
||||
|
||||
/**
|
||||
* Constants for the tracks table.
|
||||
@@ -29,9 +29,9 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
public interface TracksColumns extends BaseColumns {
|
||||
|
||||
String TABLE_NAME = "tracks";
|
||||
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_SENSOR_STATS = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/sensorstats");
|
||||
Uri CONTENT_URI_AGGREGATED_STATISTICS = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/aggregated/");
|
||||
Uri CONTENT_URI = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_SENSOR_STATS = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME + "/sensorstats");
|
||||
Uri CONTENT_URI_AGGREGATED_STATISTICS = Uri.parse(CustomContentProvider.CONTENT_BASE_URI + "/" + TABLE_NAME + "/aggregated/");
|
||||
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track";
|
||||
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
|
||||
String DEFAULT_SORT_ORDER = _ID;
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.res.Resources;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -31,11 +30,13 @@ public enum TrackFileFormat {
|
||||
return new KMLTrackExporter(context, contentProviderUtils, false);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return "application/vnd.google-earth.kml+xml";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getExtension() {
|
||||
return "kml";
|
||||
}
|
||||
@@ -50,11 +51,13 @@ public enum TrackFileFormat {
|
||||
return new KMZTrackExporter(context, contentProviderUtils, exportPhotos);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return "application/vnd.google-earth.kmz";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getExtension() {
|
||||
return "kmz";
|
||||
}
|
||||
@@ -72,11 +75,13 @@ public enum TrackFileFormat {
|
||||
return new GPXTrackExporter(contentProviderUtils, context.getString(R.string.app_name));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return "application/gpx+xml";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getExtension() {
|
||||
return "gpx";
|
||||
}
|
||||
@@ -88,11 +93,13 @@ public enum TrackFileFormat {
|
||||
return new CSVTrackExporter(contentProviderUtils);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return "text/csv";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return "csv";
|
||||
@@ -115,16 +122,10 @@ public enum TrackFileFormat {
|
||||
return preferenceIdLabelMap;
|
||||
}
|
||||
|
||||
public static TrackFileFormat valueOfPreferenceId(final String preferenceId) {
|
||||
return Arrays.stream(values())
|
||||
.filter(trackFileFormat -> trackFileFormat.getPreferenceId().equals(preferenceId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type for each format.
|
||||
*/
|
||||
@NonNull
|
||||
public abstract String getMimeType();
|
||||
|
||||
/**
|
||||
@@ -137,6 +138,7 @@ public enum TrackFileFormat {
|
||||
/**
|
||||
* Returns the file extension for each format.
|
||||
*/
|
||||
@NonNull
|
||||
public abstract String getExtension();
|
||||
|
||||
/**
|
||||
@@ -149,6 +151,7 @@ public enum TrackFileFormat {
|
||||
/**
|
||||
* The identifier to be stored in the preferences.
|
||||
*/
|
||||
@NonNull
|
||||
public String getPreferenceId() {
|
||||
return preferenceId;
|
||||
}
|
||||
|
||||
@@ -167,8 +167,11 @@ public class ExportActivity extends AbstractActivity {
|
||||
new Thread(() -> {
|
||||
directoryFiles = ExportUtils.getAllFiles(ExportActivity.this, documentFile.getUri());
|
||||
runOnUiThread(() -> {
|
||||
createExportTasks(allInOneFile);
|
||||
nextExport(null);
|
||||
if (createExportTasks(allInOneFile)) {
|
||||
nextExport(null);
|
||||
} else {
|
||||
onExportEnded();
|
||||
}
|
||||
});
|
||||
}).start();
|
||||
} else {
|
||||
@@ -217,8 +220,12 @@ public class ExportActivity extends AbstractActivity {
|
||||
exportTasks.clear();
|
||||
}
|
||||
|
||||
private void createExportTasks(boolean allInOneFile) {
|
||||
private boolean createExportTasks(boolean allInOneFile) {
|
||||
List<Track> tracks = contentProviderUtils.getTracks();
|
||||
if (tracks.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
exportTasks = new ArrayList<>();
|
||||
if (allInOneFile) {
|
||||
String filename = "OpenTracks-Backup";
|
||||
@@ -227,6 +234,7 @@ public class ExportActivity extends AbstractActivity {
|
||||
exportTasks.addAll(tracks.stream().map(it -> new ExportTask(null, trackFileFormat, List.of(it.id()))).toList());
|
||||
}
|
||||
trackExportTotalCount = exportTasks.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -391,7 +399,7 @@ public class ExportActivity extends AbstractActivity {
|
||||
|
||||
/**
|
||||
* Try to resolve the conflict if user gave the info needed.
|
||||
* Otherwise shows the buttons and views needed to gives the user the possibility of take a decision.
|
||||
* Otherwise, shows the buttons and views needed to gives the user the possibility of take a decision.
|
||||
*
|
||||
* @return true if it could resolve the conflict or false otherwise.
|
||||
*/
|
||||
|
||||
@@ -297,7 +297,17 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
}
|
||||
|
||||
if (latitude == null || longitude == null) {
|
||||
return new TrackPoint(TrackPoint.Type.TRACKPOINT, parsedTime.toInstant());
|
||||
return new TrackPoint(
|
||||
null,
|
||||
TrackPoint.Type.TRACKPOINT,
|
||||
Position.of(parsedTime.toInstant()),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
double latitudeParsed;
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package de.dennisguse.opentracks.publicapi;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.CustomContentProvider;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Create an {@link Intent} to request showing tracks on a Map or a Dashboard.
|
||||
* The receiving {@link android.app.Activity} gets temporary access to the {@link TracksColumns} and the {@link TrackPointsColumns} (incl. update).
|
||||
*/
|
||||
public class DataProvider {
|
||||
|
||||
private static final String TAG = DataProvider.class.getSimpleName();
|
||||
|
||||
private static final String ACTION_DASHBOARD = "Intent.OpenTracks-Dashboard";
|
||||
|
||||
private static final String ACTION_DASHBOARD_PAYLOAD = ACTION_DASHBOARD + ".Payload";
|
||||
|
||||
/**
|
||||
* Assume "1" if not present.
|
||||
*/
|
||||
private static final String EXTRAS_PROTOCOL_VERSION = "PROTOCOL_VERSION";
|
||||
|
||||
/**
|
||||
* version 1: the initial version.
|
||||
* version 2: replaced pause/resume trackpoints for track segmentation (lat=100 / lat=200) by TrackPoint.Type.
|
||||
* version 3: renamed track database columns.
|
||||
*/
|
||||
private static final int CURRENT_VERSION = 2;
|
||||
|
||||
private static final String EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK = "EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK";
|
||||
private static final String EXTRAS_SHOULD_KEEP_SCREEN_ON = "EXTRAS_SHOULD_KEEP_SCREEN_ON";
|
||||
private static final String EXTRAS_SHOW_WHEN_LOCKED = "EXTRAS_SHOULD_KEEP_SCREEN_ON";
|
||||
private static final String EXTRAS_SHOW_FULLSCREEN = "EXTRAS_SHOULD_FULLSCREEN";
|
||||
|
||||
private static final int TRACK_URI_INDEX = 0;
|
||||
private static final int TRACKPOINTS_URI_INDEX = 1;
|
||||
private static final int MARKERS_URI_INDEX = 2;
|
||||
|
||||
public static final Map<String, String> DATA_PROJECTIONMAP_TRACKS = Map.ofEntries(
|
||||
//V2
|
||||
Map.entry("_id", TracksColumns._ID),
|
||||
Map.entry("name", TracksColumns.NAME),
|
||||
Map.entry("description", TracksColumns.DESCRIPTION),
|
||||
Map.entry("category", TracksColumns.ACTIVITY_TYPE + " AS category"),
|
||||
Map.entry("activity_type_localized", TracksColumns.ACTIVITY_TYPE_LOCALIZED),
|
||||
Map.entry("starttime", TracksColumns.STARTTIME + " AS starttime"),
|
||||
Map.entry("stoptime", TracksColumns.STOPTIME + " AS stoptime"),
|
||||
Map.entry("totaltime", TracksColumns.TOTALTIME + " AS totaltime"),
|
||||
Map.entry("movingtime", TracksColumns.MOVINGTIME + " AS movingtime"),
|
||||
Map.entry("totaldistance", TracksColumns.TOTALDISTANCE + " AS totaldistance"),
|
||||
Map.entry("maxspeed", TracksColumns.MAXSPEED + " AS maxspeed"),
|
||||
Map.entry("avgspeed", "null AS avgspeed"),
|
||||
Map.entry("avgmovingspeed", "null AS avgmovingspeed"),
|
||||
Map.entry("minelevation", TracksColumns.MIN_ALTITUDE + " AS minelevation"),
|
||||
Map.entry("maxelevation", TracksColumns.MAX_ALTITUDE + " AS maxelevation"),
|
||||
Map.entry("elevationgain", TracksColumns.ALTITUDE_GAIN + " AS elevationgain"),
|
||||
Map.entry("elevationloss", TracksColumns.ALTITUDE_LOSS + " AS elevationloss"),
|
||||
|
||||
//V3
|
||||
// Map.entry("_id", TracksColumns._ID),
|
||||
// Map.entry("name",TracksColumns.NAME),
|
||||
// Map.entry("description",TracksColumns.DESCRIPTION),
|
||||
Map.entry("activity_type", TracksColumns.ACTIVITY_TYPE),
|
||||
// Map.entry("activity_type_localized",TracksColumns.ACTIVITY_TYPE_LOCALIZED),
|
||||
Map.entry("time_start", TracksColumns.STARTTIME),
|
||||
Map.entry("time_stop", TracksColumns.STOPTIME),
|
||||
Map.entry("duration_total", TracksColumns.TOTALTIME),
|
||||
Map.entry("duration_moving", TracksColumns.MOVINGTIME),
|
||||
Map.entry("distance", TracksColumns.TOTALDISTANCE),
|
||||
Map.entry("speed_max", TracksColumns.MAXSPEED),
|
||||
Map.entry("altitude_min", TracksColumns.MIN_ALTITUDE),
|
||||
Map.entry("altitude_max", TracksColumns.MAX_ALTITUDE),
|
||||
Map.entry("altitude_gain", TracksColumns.ALTITUDE_GAIN),
|
||||
Map.entry("altitude_loss", TracksColumns.ALTITUDE_LOSS)
|
||||
);
|
||||
|
||||
public static final Map<String, String> DATA_PROJECTIONMAP_TRACKPOINTS = Map.ofEntries(
|
||||
//V2 and V3
|
||||
Map.entry("_id", TrackPointsColumns._ID),
|
||||
Map.entry("trackid", TrackPointsColumns.TRACKID),
|
||||
Map.entry("latitude", TrackPointsColumns.LATITUDE),
|
||||
Map.entry("longitude", TrackPointsColumns.LONGITUDE),
|
||||
Map.entry("time", TrackPointsColumns.TIME),
|
||||
Map.entry("type", TrackPointsColumns.TYPE),
|
||||
Map.entry("speed", TrackPointsColumns.SPEED)
|
||||
);
|
||||
public static final Map<String, String> DATA_PROJECTIONMAP_MARKERS = Map.ofEntries(
|
||||
//V2
|
||||
Map.entry("_id", MarkerColumns._ID),
|
||||
Map.entry("trackid", MarkerColumns.TRACKID),
|
||||
Map.entry("name", MarkerColumns.NAME),
|
||||
Map.entry("description", MarkerColumns.DESCRIPTION),
|
||||
Map.entry("category", MarkerColumns.TYPE_LOCALIZED + " AS category"),
|
||||
Map.entry("latitude", MarkerColumns.LATITUDE),
|
||||
Map.entry("longitude", MarkerColumns.LONGITUDE),
|
||||
Map.entry("photoUrl", MarkerColumns.PHOTOURL),
|
||||
Map.entry("icon", "null AS icon"),
|
||||
|
||||
//V3
|
||||
// Map.entry("_id", MarkerColumns._ID),
|
||||
// Map.entry("trackid",MarkerColumns.TRACKID),
|
||||
// Map.entry("name",MarkerColumns.NAME),
|
||||
// Map.entry("description",MarkerColumns.DESCRIPTION),
|
||||
Map.entry("marker_type_localized", MarkerColumns.TYPE_LOCALIZED)
|
||||
// Map.entry("latitude",MarkerColumns.LATITUDE),
|
||||
// Map.entry("longitude",MarkerColumns.LONGITUDE),
|
||||
// Map.entry("photoUrl",MarkerColumns.PHOTOURL)
|
||||
);
|
||||
|
||||
|
||||
private DataProvider() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send intent to show tracks on a map (needs an another app) as resource URIs.
|
||||
*
|
||||
* @param context the context
|
||||
* @param isRecording are we currently recording?
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
public static void showTrackOnMap(Context context, boolean isRecording, Track.Id... trackIds) {
|
||||
startDashboard(context, isRecording, null, null, trackIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send intent to show tracks on a map (needs an another app) as resource URIs.
|
||||
* By providing a targetPackage and targetClass an explicit intent can be sent,
|
||||
* thus bypassing the need for the user to select an app.
|
||||
*
|
||||
* @param context the context
|
||||
* @param isRecording are we currently recording?
|
||||
* @param targetPackage the target package
|
||||
* @param targetClass the target class
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
public static void startDashboard(Context context, boolean isRecording, @Nullable String targetPackage, @Nullable String targetClass, Track.Id... trackIds) {
|
||||
if (trackIds.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
String trackIdList = ContentProviderUtils.formatIdListForUri(trackIds);
|
||||
|
||||
ArrayList<Uri> uris = new ArrayList<>();
|
||||
uris.add(TRACK_URI_INDEX, Uri.withAppendedPath(CustomContentProvider.UrlType.DASHBOARD_TRACKS_BY_IDS.getUri(), trackIdList));
|
||||
uris.add(TRACKPOINTS_URI_INDEX, Uri.withAppendedPath(CustomContentProvider.UrlType.DASHBOARD_TRACKPOINTS_BY_TRACKIDS.getUri(), trackIdList));
|
||||
uris.add(MARKERS_URI_INDEX, Uri.withAppendedPath(CustomContentProvider.UrlType.DASHBOARD_MARKERS_BY_TRACKIDS.getUri(), trackIdList));
|
||||
|
||||
Intent intent = new Intent(ACTION_DASHBOARD);
|
||||
intent.putExtra(EXTRAS_PROTOCOL_VERSION, CURRENT_VERSION);
|
||||
|
||||
intent.putParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD, uris);
|
||||
|
||||
intent.putExtra(EXTRAS_SHOULD_KEEP_SCREEN_ON, PreferencesUtils.shouldKeepScreenOn());
|
||||
intent.putExtra(EXTRAS_SHOW_WHEN_LOCKED, PreferencesUtils.shouldShowStatsOnLockscreen());
|
||||
intent.putExtra(EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK, isRecording);
|
||||
if (isRecording) {
|
||||
intent.putExtra(EXTRAS_SHOW_FULLSCREEN, PreferencesUtils.shouldUseFullscreen());
|
||||
}
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
ClipData clipData = ClipData.newRawUri(null, uris.get(TRACK_URI_INDEX));
|
||||
clipData.addItem(new ClipData.Item(uris.get(TRACKPOINTS_URI_INDEX)));
|
||||
clipData.addItem(new ClipData.Item(uris.get(MARKERS_URI_INDEX)));
|
||||
intent.setClipData(clipData);
|
||||
|
||||
if (targetPackage != null && targetClass != null) {
|
||||
Log.i(TAG, "Starting dashboard activity with explicit intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
||||
intent.setClassName(targetPackage, targetClass);
|
||||
} else {
|
||||
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.e(TAG, "Dashboard not installed; cannot start it.");
|
||||
Toast.makeText(context, R.string.show_on_dashboard_not_installed, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackBuilder;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||
|
||||
public class StartRecording extends AbstractAPIActivity {
|
||||
|
||||
@@ -61,7 +60,7 @@ public class StartRecording extends AbstractAPIActivity {
|
||||
String targetPackage = bundle.getString(EXTRA_STATS_TARGET_PACKAGE, null);
|
||||
String targetClass = bundle.getString(EXTRA_STATS_TARGET_CLASS, null);
|
||||
if (targetClass != null && targetPackage != null) {
|
||||
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId);
|
||||
DataProvider.startDashboard(this, true, targetPackage, targetClass, trackId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.sensors.driver.BarometerBluetooth;
|
||||
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
|
||||
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
|
||||
|
||||
/**
|
||||
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
|
||||
*/
|
||||
//TODO Rename class
|
||||
public class AltitudeChangeHandler extends SensorHandler<AtmosphericPressure, AltitudeGainLoss> {
|
||||
|
||||
public AltitudeChangeHandler(SensorManager sensorManager) {
|
||||
super(sensorManager);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Driver createDriverInternal() {
|
||||
return new BarometerInternal(this);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Driver createDriverBluetooth(String address) {
|
||||
return new BluetoothDriver<>(
|
||||
BluetoothUtils.getAdapter(getContext()),
|
||||
this,
|
||||
new BarometerBluetooth()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AggregatorBarometer createAggregator(String sensorAddress, String sensorName) {
|
||||
return new AggregatorBarometer(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSensorPreferenceKey() {
|
||||
return R.string.settings_sensor_bluetooth_pressure_key;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface {
|
||||
private static final String TAG = BluetoothHandlerCyclingCadence.class.getSimpleName();
|
||||
|
||||
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
|
||||
BluetoothHandlerCyclingPower.CYCLING_POWER,
|
||||
BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return CYCLING_CADENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatorCyclingCadence createEmptySensorData(String address, String name) {
|
||||
return new AggregatorCyclingCadence(address, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
if (serviceMeasurementUUID.equals(BluetoothHandlerCyclingPower.CYCLING_POWER)) {
|
||||
BluetoothHandlerCyclingPower.Data data = BluetoothHandlerCyclingPower.parseCyclingPower(characteristic);
|
||||
if (data != null && data.crank() != null) {
|
||||
observer.onChange(new Raw<>(observer.getNow(), data.crank()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (serviceMeasurementUUID.equals(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE)) {
|
||||
Pair<BluetoothHandlerCyclingDistanceSpeed.WheelData, CrankData> data = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
|
||||
if (data != null && data.second != null) {
|
||||
observer.onChange(new Raw<>(observer.getNow(), data.second));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Log.e(TAG, "Don't know how to decode this payload.");
|
||||
}
|
||||
|
||||
public record CrankData(
|
||||
long crankRevolutionsCount, // UINT32
|
||||
int crankRevolutionsTime // UINT16; 1/1024s
|
||||
) {}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import static de.dennisguse.opentracks.sensors.BluetoothHandlerBarometricPressure.ENVIRONMENTAL_SENSING_SERVICE;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Temperature;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
|
||||
public class BluetoothHandlerTemperature implements SensorHandlerInterface {
|
||||
public static final ServiceMeasurementUUID TEMPERATURE = new ServiceMeasurementUUID(
|
||||
ENVIRONMENTAL_SENSING_SERVICE,
|
||||
new UUID(0x2A6E00001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return List.of(TEMPERATURE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Aggregator<?, ?> createEmptySensorData(String address, String name) {
|
||||
return new AggregatorTemperature(address, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Temperature value = parseEnvironmentalSensing(characteristic);
|
||||
if (value == null) return;
|
||||
|
||||
observer.onChange(new Raw<>(observer.getNow(), value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoding: org.bluetooth.characteristic.temperature.xml
|
||||
*/
|
||||
public static Temperature parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] raw = characteristic.getValue();
|
||||
|
||||
if (raw.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 0);
|
||||
return Temperature.ofCentiCelsius(pressure);
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010 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.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
|
||||
* <p>
|
||||
* Note: should only be instantiated once.
|
||||
* TODO: listen for Bluetooth enabled/disabled events.
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class BluetoothRemoteSensorManager implements SensorConnector, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final Handler handler;
|
||||
private boolean started = false;
|
||||
|
||||
private final BluetoothConnectionManager heartRate;
|
||||
private final BluetoothConnectionManager temperature;
|
||||
private final BluetoothConnectionManager cyclingCadence;
|
||||
private final BluetoothConnectionManager cyclingSpeed;
|
||||
private final BluetoothConnectionManager cyclingPower;
|
||||
private final BluetoothConnectionManager runningSpeedAndCadence;
|
||||
|
||||
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @Nullable SensorManager.SensorDataChangedObserver observer) {
|
||||
this.context = context;
|
||||
this.handler = handler;
|
||||
|
||||
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(context);
|
||||
this.heartRate = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerHeartRate());
|
||||
this.temperature = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerTemperature());
|
||||
this.cyclingCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingCadence());
|
||||
this.cyclingSpeed = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingDistanceSpeed());
|
||||
this.cyclingPower = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingPower());
|
||||
this.runningSpeedAndCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerRunningSpeedAndCadence());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Context context, Handler handler) {
|
||||
started = true;
|
||||
|
||||
// Triggers connection startup
|
||||
onSharedPreferenceChanged(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop(Context context) {
|
||||
heartRate.disconnect();
|
||||
temperature.disconnect();
|
||||
cyclingCadence.disconnect();
|
||||
cyclingSpeed.disconnect();
|
||||
cyclingPower.disconnect();
|
||||
runningSpeedAndCadence.disconnect();
|
||||
|
||||
started = false;
|
||||
}
|
||||
|
||||
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
|
||||
connectionManager.connect(context, handler, address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences unused, @Nullable String key) {
|
||||
if (!started) return;
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
|
||||
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress();
|
||||
connect(heartRate, address);
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_temperature_key, key)) {
|
||||
String address = PreferencesUtils.getBluetoothTemperatureSensorAddress();
|
||||
connect(temperature, address);
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
|
||||
String address = PreferencesUtils.getBluetoothCyclingCadenceSensorAddress();
|
||||
connect(cyclingCadence, address);
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_speed_key, key)) {
|
||||
String address = PreferencesUtils.getBluetoothCyclingSpeedSensorAddress();
|
||||
|
||||
connect(cyclingSpeed, address);
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
|
||||
String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress();
|
||||
|
||||
connect(cyclingPower, address);
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_running_speed_and_cadence_key, key)) {
|
||||
String address = PreferencesUtils.getBluetoothRunningSpeedAndCadenceAddress();
|
||||
|
||||
connect(runningSpeedAndCadence, address);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
|
||||
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
|
||||
|
||||
public class CyclingCadenceHandler extends SensorHandler<CyclingCadenceBluetooth.CrankData, Cadence> {
|
||||
|
||||
protected CyclingCadenceHandler(SensorManager sensorManager) {
|
||||
super(sensorManager);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Driver createDriverBluetooth(String address) {
|
||||
return new BluetoothDriver<>(
|
||||
BluetoothUtils.getAdapter(getContext()),
|
||||
this,
|
||||
new CyclingCadenceBluetooth()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected AggregatorCyclingCadence createAggregator(String sensorAddress, String sensorName) {
|
||||
return new AggregatorCyclingCadence(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSensorPreferenceKey() {
|
||||
return R.string.settings_sensor_bluetooth_cycling_cadence_key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
|
||||
import de.dennisguse.opentracks.sensors.driver.CyclingDistanceSpeedBluetooth;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
public class CyclingDistanceSpeedHandler extends SensorHandler<CyclingDistanceSpeedBluetooth.WheelData, AggregatorCyclingDistanceSpeed.Data> {
|
||||
|
||||
protected CyclingDistanceSpeedHandler(SensorManager sensorManager) {
|
||||
super(sensorManager);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Driver createDriverBluetooth(String address) {
|
||||
return new BluetoothDriver<>(
|
||||
BluetoothUtils.getAdapter(getContext()),
|
||||
this,
|
||||
new CyclingDistanceSpeedBluetooth()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected AggregatorCyclingDistanceSpeed createAggregator(String sensorAddress, String sensorName) {
|
||||
return new AggregatorCyclingDistanceSpeed(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSensorPreferenceKey() {
|
||||
return R.string.settings_sensor_bluetooth_cycling_speed_key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataReceived(CyclingDistanceSpeedBluetooth.WheelData value) {
|
||||
//TODO Fetch once and then listen for changes.
|
||||
((AggregatorCyclingDistanceSpeed) aggregator).setWheelCircumference(PreferencesUtils.getWheelCircumference());
|
||||
|
||||
super.onDataReceived(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
|
||||
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
|
||||
|
||||
public class CyclingPowerHandler extends SensorHandler<CyclingPowerBluetooth.Data, Power> {
|
||||
|
||||
protected CyclingPowerHandler(SensorManager sensorManager) {
|
||||
super(sensorManager);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Driver createDriverBluetooth(String address) {
|
||||
return new BluetoothDriver<>(
|
||||
BluetoothUtils.getAdapter(getContext()),
|
||||
this,
|
||||
new CyclingPowerBluetooth()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected AggregatorCyclingPower createAggregator(String sensorAddress, String sensorName) {
|
||||
return new AggregatorCyclingPower(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSensorPreferenceKey() {
|
||||
return R.string.settings_sensor_bluetooth_cycling_power_key;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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.sensors.driver.BarometerInternal;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
|
||||
*/
|
||||
public class GainManager implements SensorConnector {
|
||||
|
||||
private static final String TAG = GainManager.class.getSimpleName();
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void start(Context context, Handler handler) {
|
||||
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);
|
||||
|
||||
onDisconnect();
|
||||
}
|
||||
|
||||
private void connect() {
|
||||
onDisconnect();
|
||||
|
||||
String address = PreferencesUtils.getBarometerSensorAddress();
|
||||
switch (PreferencesUtils.getSensorType(address)) {
|
||||
case NONE -> {
|
||||
driver = null;
|
||||
listener.onRemove(new AggregatorBarometer(null, null));
|
||||
return;
|
||||
}
|
||||
case INTERNAL -> driver = new BarometerInternal(listener);
|
||||
case REMOTE -> driver =
|
||||
new BluetoothConnectionManager(
|
||||
BluetoothUtils.getAdapter(context),
|
||||
listener,
|
||||
new BluetoothHandlerBarometricPressure()
|
||||
);
|
||||
default -> throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
driver.connect(context, handler, address);
|
||||
}
|
||||
|
||||
private void onDisconnect() {
|
||||
if (driver == null) return;
|
||||
|
||||
driver.disconnect();
|
||||
listener.onDisconnect(new AggregatorBarometer("GainManager", null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
|
||||
import de.dennisguse.opentracks.sensors.driver.SensorType;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public class GpsHandler extends SensorHandler<Location, Position> {
|
||||
|
||||
private final String TAG = GpsHandler.class.getSimpleName();
|
||||
|
||||
//TODO Refactor to just pass information via SensorManager.
|
||||
private TrackPointCreator trackPointCreator;
|
||||
|
||||
private GpsStatusManager gpsStatusManager;
|
||||
private Distance thresholdHorizontalAccuracy;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
|
||||
if (gpsStatusManager != null) {
|
||||
Distance gpsMinDistance = PreferencesUtils.getRecordingDistanceInterval();
|
||||
gpsStatusManager.onRecordingDistanceChanged(gpsMinDistance);
|
||||
}
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.recording_gps_accuracy_key, key)) {
|
||||
thresholdHorizontalAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.min_sampling_interval_key, key)) {
|
||||
if (gpsStatusManager != null) {
|
||||
gpsStatusManager.onMinSamplingIntervalChanged(PreferencesUtils.getMinSamplingInterval());
|
||||
}
|
||||
|
||||
//TODO
|
||||
connect();
|
||||
}
|
||||
};
|
||||
|
||||
public GpsHandler(SensorManager sensorManager, TrackPointCreator trackPointCreator) {
|
||||
super(sensorManager);
|
||||
this.trackPointCreator = trackPointCreator;
|
||||
gpsStatusManager = new GpsStatusManager(sensorManager.getContext(), trackPointCreator::sendGpsStatus, sensorManager.getHandler());
|
||||
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected(String sensorAddress, String sensorName) {
|
||||
super.onConnected(sensorAddress, sensorName);
|
||||
gpsStatusManager.onGpsEnabled();
|
||||
gpsStatusManager.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
super.onDisconnected();
|
||||
gpsStatusManager.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorDeactivated() {
|
||||
super.onSensorDeactivated();
|
||||
gpsStatusManager.onGpsDisabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataReceived(Location location) {
|
||||
// Send each update to the status; please note that this TrackPoint is not stored.
|
||||
Position position = Position.of(location, trackPointCreator.getNow());
|
||||
gpsStatusManager.onNewTrackPoint(position);
|
||||
|
||||
//TODO We could move the following check into the AggregatorGPS
|
||||
if (!position.hasValidLocation()) {
|
||||
Log.w(TAG, "Ignore newTrackPoint. Location is invalid.");
|
||||
return;
|
||||
}
|
||||
if (!position.fulfillsAccuracy(thresholdHorizontalAccuracy)) {
|
||||
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
|
||||
return;
|
||||
}
|
||||
|
||||
super.onDataReceived(location);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Driver createDriverInternal() {
|
||||
return new GpsInternal(this);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected AggregatorGPS createAggregator(String sensorAddress, String sensorName) {
|
||||
return new AggregatorGPS(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSensorPreferenceKey() {
|
||||
return R.string.settings_sensor_gps_key;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSensorPreferenceDefaultValue() {
|
||||
return SensorType.INTERNAL.getPreferenceValue();
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.location.LocationListenerCompat;
|
||||
import androidx.core.location.LocationManagerCompat;
|
||||
import androidx.core.location.LocationRequestCompat;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public class GpsManager implements SensorConnector, LocationListenerCompat, GpsStatusManager.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private final String TAG = GpsManager.class.getSimpleName();
|
||||
|
||||
private static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
|
||||
|
||||
private TrackPointCreator trackPointCreator;
|
||||
|
||||
private SensorManager.SensorDataChangedObserver observer;
|
||||
private Context context;
|
||||
private Handler handler;
|
||||
|
||||
private LocationManager locationManager;
|
||||
private GpsStatusManager gpsStatusManager;
|
||||
private Duration gpsInterval;
|
||||
private Distance thresholdHorizontalAccuracy;
|
||||
|
||||
public GpsManager(TrackPointCreator trackPointCreator, SensorManager.SensorDataChangedObserver observer) {
|
||||
this.trackPointCreator = trackPointCreator;
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
public void start(@NonNull Context context, @NonNull Handler handler) {
|
||||
this.context = context;
|
||||
this.handler = handler;
|
||||
|
||||
onSharedPreferenceChanged(null, null);
|
||||
|
||||
gpsStatusManager = new GpsStatusManager(context, this, handler);
|
||||
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
|
||||
observer.onConnect(new AggregatorGPS("internal"));
|
||||
|
||||
registerLocationListener();
|
||||
gpsStatusManager.start();
|
||||
}
|
||||
|
||||
private boolean isStarted() {
|
||||
return locationManager != null;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"MissingPermission"})
|
||||
public void stop(Context context) {
|
||||
if (isStarted()) {
|
||||
LocationManagerCompat.removeUpdates(locationManager, this);
|
||||
}
|
||||
locationManager = null;
|
||||
this.context = null;
|
||||
handler = null;
|
||||
|
||||
gpsStatusManager.stop();
|
||||
gpsStatusManager = null;
|
||||
|
||||
observer.onDisconnect(new AggregatorGPS("internal"));
|
||||
observer = null;
|
||||
|
||||
trackPointCreator = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
boolean registerListener = false;
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.min_sampling_interval_key, key)) {
|
||||
registerListener = true;
|
||||
|
||||
gpsInterval = PreferencesUtils.getMinSamplingInterval();
|
||||
|
||||
if (gpsStatusManager != null) {
|
||||
gpsStatusManager.onMinSamplingIntervalChanged(gpsInterval);
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(R.string.recording_gps_accuracy_key, key)) {
|
||||
thresholdHorizontalAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
|
||||
}
|
||||
if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
|
||||
registerListener = true;
|
||||
|
||||
if (gpsStatusManager != null) {
|
||||
Distance gpsMinDistance = PreferencesUtils.getRecordingDistanceInterval();
|
||||
gpsStatusManager.onRecordingDistanceChanged(gpsMinDistance);
|
||||
}
|
||||
}
|
||||
|
||||
if (registerListener && isStarted()) {
|
||||
registerLocationListener();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if location is valid and builds a track point that will be send through TrackPointCreator.
|
||||
*
|
||||
* @param location {@link Location} object.
|
||||
*/
|
||||
@Override
|
||||
public void onLocationChanged(@NonNull Location location) {
|
||||
// Send each update to the status; please note that this TrackPoint is not stored.
|
||||
Position position = Position.of(location, trackPointCreator.createNow());
|
||||
gpsStatusManager.onNewTrackPoint(position);
|
||||
|
||||
if (!position.hasValidLocation()) {
|
||||
Log.w(TAG, "Ignore newTrackPoint. Location is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!position.fulfillsAccuracy(thresholdHorizontalAccuracy)) {
|
||||
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
|
||||
return;
|
||||
}
|
||||
|
||||
observer.onChange(new Raw<>(observer.getNow(), position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(@NonNull String provider, int status, Bundle extras) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(@NonNull String provider) {
|
||||
gpsStatusManager.onGpsEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(@NonNull String provider) {
|
||||
gpsStatusManager.onGpsDisabled();
|
||||
}
|
||||
|
||||
private void registerLocationListener() {
|
||||
if (!LocationManagerCompat.hasProvider(locationManager, LOCATION_PROVIDER)) {
|
||||
Log.e(TAG, "Device doesn't have GPS.");
|
||||
return;
|
||||
}
|
||||
|
||||
LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(gpsInterval.toMillis())
|
||||
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
|
||||
.setMaxUpdateDelayMillis(0)
|
||||
.build();
|
||||
|
||||
if (PermissionRequester.GPS.hasPermission(context)) {
|
||||
try {
|
||||
Log.i(TAG, "Register for location updates " + context);
|
||||
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, handler::post, this);
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGpsStatusChanged(GpsStatusValue currentStatus) {
|
||||
trackPointCreator.sendGpsStatus(currentStatus);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class GpsStatusManager {
|
||||
private Duration signalLostThreshold;
|
||||
|
||||
private GpsStatusValue gpsStatus = GpsStatusValue.GPS_NONE;
|
||||
private GpsStatusListener client;
|
||||
private GpsStatusListener observer;
|
||||
private final Context context;
|
||||
|
||||
@Nullable
|
||||
@@ -44,8 +44,8 @@ class GpsStatusManager {
|
||||
};
|
||||
|
||||
|
||||
public GpsStatusManager(Context context, GpsStatusListener client, Handler handler) {
|
||||
this.client = client;
|
||||
public GpsStatusManager(Context context, GpsStatusListener observer, Handler handler) {
|
||||
this.observer = observer;
|
||||
this.context = context;
|
||||
this.handler = handler;
|
||||
|
||||
@@ -54,7 +54,7 @@ class GpsStatusManager {
|
||||
}
|
||||
|
||||
public void start() {
|
||||
client.onGpsStatusChanged(GpsStatusValue.GPS_ENABLED);
|
||||
observer.onGpsStatusChanged(GpsStatusValue.GPS_ENABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,9 +62,7 @@ class GpsStatusManager {
|
||||
*/
|
||||
public void stop() {
|
||||
stopTimer();
|
||||
client.onGpsStatusChanged(GpsStatusValue.GPS_NONE);
|
||||
client = null;
|
||||
handler = null;
|
||||
observer.onGpsStatusChanged(GpsStatusValue.GPS_NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,8 +157,8 @@ class GpsStatusManager {
|
||||
|
||||
private void setGpsStatus(GpsStatusValue current) {
|
||||
gpsStatus = current;
|
||||
if (client != null) {
|
||||
client.onGpsStatusChanged(current);
|
||||
if (observer != null) {
|
||||
observer.onGpsStatusChanged(current);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.driver.HeartRateBluetooth;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
|
||||
|
||||
public class HeartRateHandler extends SensorHandler<HeartRate, HeartRate> {
|
||||
|
||||
protected HeartRateHandler(SensorManager sensorManager) {
|
||||
super(sensorManager);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Driver createDriverBluetooth(String address) {
|
||||
return new BluetoothDriver<>(
|
||||
BluetoothUtils.getAdapter(getContext()),
|
||||
this,
|
||||
new HeartRateBluetooth()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected AggregatorHeartRate createAggregator(String sensorAddress, String sensorName) {
|
||||
return new AggregatorHeartRate(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSensorPreferenceKey() {
|
||||
return R.string.settings_sensor_bluetooth_heart_rate_key;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.driver.RunningSpeedAndCadenceBluetooth;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
|
||||
|
||||
public class RunningSpeedCadenceDistanceHandler extends SensorHandler<RunningSpeedAndCadenceBluetooth.Data, AggregatorRunning.Data> {
|
||||
|
||||
protected RunningSpeedCadenceDistanceHandler(SensorManager sensorManager) {
|
||||
super(sensorManager);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Driver createDriverBluetooth(String address) {
|
||||
return new BluetoothDriver<>(
|
||||
BluetoothUtils.getAdapter(getContext()),
|
||||
this,
|
||||
new RunningSpeedAndCadenceBluetooth()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected AggregatorRunning createAggregator(String sensorAddress, String sensorName) {
|
||||
return new AggregatorRunning("", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSensorPreferenceKey() {
|
||||
return R.string.settings_sensor_bluetooth_running_speed_and_cadence_key;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
|
||||
public interface SensorConnector {
|
||||
|
||||
void start(Context context, Handler handler);
|
||||
|
||||
void stop(Context context);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.driver.DriverObserver;
|
||||
import de.dennisguse.opentracks.sensors.driver.SensorType;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
|
||||
public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implements DriverObserver<AggregatorInput> {
|
||||
|
||||
private static final String TAG = SensorHandler.class.getSimpleName();
|
||||
|
||||
private final SensorManager sensorManager;
|
||||
|
||||
@VisibleForTesting
|
||||
public Driver driver;
|
||||
|
||||
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
|
||||
|
||||
protected SensorHandler(SensorManager sensorManager) {
|
||||
this.sensorManager = sensorManager;
|
||||
}
|
||||
|
||||
void connect() {
|
||||
disconnect();
|
||||
|
||||
String address = PreferencesUtils.getString(getSensorPreferenceKey(), getSensorPreferenceDefaultValue());
|
||||
|
||||
switch (PreferencesUtils.getSensorType(address)) {
|
||||
case NONE -> {
|
||||
onDisconnected();
|
||||
return;
|
||||
}
|
||||
case INTERNAL -> {
|
||||
driver = createDriverInternal();
|
||||
}
|
||||
case REMOTE -> {
|
||||
driver = createDriverBluetooth(address);
|
||||
}
|
||||
default -> throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
driver.connect(sensorManager.getContext(), sensorManager.getHandler(), address);
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
if (driver == null) return;
|
||||
|
||||
driver.disconnect();
|
||||
driver = null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected Driver createDriverInternal() {
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected Driver createDriverBluetooth(String address) {
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected abstract Aggregator<AggregatorInput, AggregatorOutput> createAggregator(String sensorAddress, String sensorName);
|
||||
|
||||
protected abstract int getSensorPreferenceKey();
|
||||
|
||||
protected String getSensorPreferenceDefaultValue() {
|
||||
return SensorType.NONE.getPreferenceValue();
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return sensorManager.getContext();
|
||||
}
|
||||
|
||||
void resetAggregated() {
|
||||
if (aggregator != null) {
|
||||
aggregator.resetAggregated();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
SensorData<AggregatorOutput> getSensorData(Instant now) {
|
||||
if (aggregator == null) return null;
|
||||
if (!aggregator.hasReceivedData()) return null;
|
||||
|
||||
return aggregator.getAggregatedValueWithSensorName(now);
|
||||
}
|
||||
|
||||
//TODO REMOVE
|
||||
@VisibleForTesting
|
||||
public void setAggregator(Aggregator<AggregatorInput, AggregatorOutput> aggregator) {
|
||||
this.aggregator = aggregator;
|
||||
}
|
||||
|
||||
public void onConnected(String sensorAddress, String sensorName) {
|
||||
SensorHandler.this.aggregator = createAggregator(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionLost() {
|
||||
if (SensorHandler.this.aggregator == null) {
|
||||
Log.d(TAG, "Connection lost, but where not connected.");
|
||||
return;
|
||||
}
|
||||
|
||||
SensorHandler.this.aggregator.resetAggregated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataReceived(AggregatorInput value) {
|
||||
if (aggregator == null) {
|
||||
throw new RuntimeException("Received data while being disconnected.");
|
||||
}
|
||||
SensorHandler.this.aggregator.add(sensorManager.getNow(), value);
|
||||
sensorManager.onChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
SensorHandler.this.aggregator = null;
|
||||
}
|
||||
}
|
||||
@@ -6,153 +6,290 @@ import android.os.Handler;
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataAggregator;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.SystemUtils;
|
||||
|
||||
public class SensorManager implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private static final String TAG = SensorManager.class.getSimpleName();
|
||||
|
||||
//TODO Should be final and not be visible for testing
|
||||
@VisibleForTesting
|
||||
public SensorDataAggregator sensorDataAggregator;
|
||||
private Context context;
|
||||
|
||||
private Handler handler;
|
||||
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
|
||||
private final TrackPointCreator observer;
|
||||
|
||||
private final SensorDataChangedObserver listener = new SensorDataChangedObserver() {
|
||||
private HeartRateHandler heartRateHandler;
|
||||
private TemperatureHandler temperatureHandler;
|
||||
private CyclingCadenceHandler cyclingCadenceHandler;
|
||||
private CyclingDistanceSpeedHandler cyclingDistanceSpeedHandler;
|
||||
private CyclingPowerHandler cyclingPowerHandler;
|
||||
private RunningSpeedCadenceDistanceHandler runningSpeedCadenceDistanceHandler;
|
||||
|
||||
@Override
|
||||
public void onConnect(Aggregator<?, ?> aggregator) {
|
||||
sensorDataAggregator.add(aggregator);
|
||||
}
|
||||
private AltitudeChangeHandler altitudeChangeHandler;
|
||||
|
||||
@Override
|
||||
public void onChange(Raw<?> data) {
|
||||
sensorDataAggregator.update(data);
|
||||
observer.onChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Aggregator<?, ?> aggregator) {
|
||||
sensorDataAggregator.add(aggregator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemove(Aggregator<?, ?> aggregator) {
|
||||
sensorDataAggregator.remove(aggregator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getNow() {
|
||||
return observer.createNow();
|
||||
}
|
||||
};
|
||||
|
||||
private BluetoothRemoteSensorManager bluetoothSensorManager;
|
||||
|
||||
private GainManager altitudeSumManager;
|
||||
|
||||
private GpsManager gpsManager;
|
||||
private GpsHandler gpsHandler;
|
||||
|
||||
public SensorManager(TrackPointCreator observer) {
|
||||
this.observer = observer;
|
||||
this.sensorDataAggregator = new SensorDataAggregator();
|
||||
}
|
||||
|
||||
public void start(Context context, Handler handler) {
|
||||
if (gpsManager != null) {
|
||||
if (isStarted()) {
|
||||
throw new RuntimeException("SensorManager cannot be started twice; stop first.");
|
||||
}
|
||||
|
||||
wakeLock = SystemUtils.acquireWakeLock(context, wakeLock);
|
||||
this.context = context;
|
||||
this.handler = handler;
|
||||
|
||||
gpsManager = new GpsManager(observer, listener);
|
||||
altitudeSumManager = new GainManager(listener);
|
||||
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, listener);
|
||||
gpsHandler = new GpsHandler(this, observer);
|
||||
altitudeChangeHandler = new AltitudeChangeHandler(this);
|
||||
heartRateHandler = new HeartRateHandler(this);
|
||||
temperatureHandler = new TemperatureHandler(this);
|
||||
cyclingCadenceHandler = new CyclingCadenceHandler(this);
|
||||
cyclingDistanceSpeedHandler = new CyclingDistanceSpeedHandler(this);
|
||||
cyclingPowerHandler = new CyclingPowerHandler(this);
|
||||
runningSpeedCadenceDistanceHandler = new RunningSpeedCadenceDistanceHandler(this);
|
||||
|
||||
onSharedPreferenceChanged(null, null);
|
||||
|
||||
gpsManager.start(context, handler);
|
||||
altitudeSumManager.start(context, handler);
|
||||
bluetoothSensorManager.start(context, handler);
|
||||
}
|
||||
|
||||
public void stop(Context context) {
|
||||
public void stop() {
|
||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
||||
|
||||
bluetoothSensorManager.stop(context);
|
||||
bluetoothSensorManager = null;
|
||||
heartRateHandler.disconnect();
|
||||
heartRateHandler = null;
|
||||
|
||||
altitudeSumManager.stop(context);
|
||||
altitudeSumManager = null;
|
||||
temperatureHandler.disconnect();
|
||||
temperatureHandler = null;
|
||||
|
||||
gpsManager.stop(context);
|
||||
gpsManager = null;
|
||||
cyclingCadenceHandler.disconnect();
|
||||
cyclingCadenceHandler = null;
|
||||
|
||||
sensorDataAggregator.clear();
|
||||
}
|
||||
cyclingDistanceSpeedHandler.disconnect();
|
||||
cyclingDistanceSpeedHandler = null;
|
||||
|
||||
public SensorDataSet getSensorDataSet(Instant now) {
|
||||
return sensorDataAggregator.getSensorData(now);
|
||||
cyclingPowerHandler.disconnect();
|
||||
cyclingPowerHandler = null;
|
||||
|
||||
runningSpeedCadenceDistanceHandler.disconnect();
|
||||
runningSpeedCadenceDistanceHandler = null;
|
||||
|
||||
altitudeChangeHandler.disconnect();
|
||||
altitudeChangeHandler = null;
|
||||
|
||||
gpsHandler.disconnect();
|
||||
gpsHandler = null;
|
||||
|
||||
context = null;
|
||||
handler = null;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
if (bluetoothSensorManager == null || altitudeSumManager == null) {
|
||||
if (gpsHandler == null || altitudeChangeHandler == null) {
|
||||
Log.d(TAG, "No recording running and no reset necessary.");
|
||||
return;
|
||||
}
|
||||
sensorDataAggregator.reset();
|
||||
|
||||
Log.i(TAG, "Resetting data");
|
||||
|
||||
heartRateHandler.resetAggregated();
|
||||
temperatureHandler.resetAggregated();
|
||||
cyclingCadenceHandler.resetAggregated();
|
||||
cyclingDistanceSpeedHandler.resetAggregated();
|
||||
cyclingPowerHandler.resetAggregated();
|
||||
runningSpeedCadenceDistanceHandler.resetAggregated();
|
||||
altitudeChangeHandler.resetAggregated();
|
||||
gpsHandler.resetAggregated();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void onChanged(Raw<?> data) {
|
||||
listener.onChange(data);
|
||||
}
|
||||
|
||||
public GpsManager getGpsManager() {
|
||||
return gpsManager;
|
||||
public GpsHandler getGpsHandler() {
|
||||
return gpsHandler;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@VisibleForTesting
|
||||
public GainManager getAltitudeSumManager() {
|
||||
return altitudeSumManager;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@VisibleForTesting
|
||||
public void setAltitudeSumManager(GainManager altitudeSumManager) {
|
||||
this.altitudeSumManager = altitudeSumManager;
|
||||
public AltitudeChangeHandler getAltitudeChangeHandler() {
|
||||
return altitudeChangeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) {
|
||||
if (gpsManager != null) {
|
||||
gpsManager.onSharedPreferenceChanged(sharedPreferences, key);
|
||||
bluetoothSensorManager.onSharedPreferenceChanged(sharedPreferences, key);
|
||||
if (!isStarted()) return;
|
||||
|
||||
if (PreferencesUtils.isKey(gpsHandler.getSensorPreferenceKey(), key)) {
|
||||
gpsHandler.connect();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(altitudeChangeHandler.getSensorPreferenceKey(), key)) {
|
||||
altitudeChangeHandler.connect();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(heartRateHandler.getSensorPreferenceKey(), key)) {
|
||||
heartRateHandler.connect();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(temperatureHandler.getSensorPreferenceKey(), key)) {
|
||||
temperatureHandler.connect();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(cyclingCadenceHandler.getSensorPreferenceKey(), key)) {
|
||||
cyclingCadenceHandler.connect();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(cyclingDistanceSpeedHandler.getSensorPreferenceKey(), key)) {
|
||||
cyclingDistanceSpeedHandler.connect();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(cyclingPowerHandler.getSensorPreferenceKey(), key)) {
|
||||
cyclingPowerHandler.connect();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(runningSpeedCadenceDistanceHandler.getSensorPreferenceKey(), key)) {
|
||||
runningSpeedCadenceDistanceHandler.connect();
|
||||
}
|
||||
}
|
||||
|
||||
public interface SensorDataChangedObserver {
|
||||
public Instant getNow() {
|
||||
return observer.getNow();
|
||||
}
|
||||
|
||||
void onConnect(Aggregator<?, ?> sensorData);
|
||||
void onChange(Raw<?> sensorData);
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
void onDisconnect(Aggregator<?, ?> sensorData);
|
||||
public Handler getHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
void onRemove(Aggregator<?, ?> sensorData);
|
||||
private SensorData<Cadence> getCadence(Instant now) {
|
||||
{
|
||||
SensorData<Cadence> value = cyclingCadenceHandler.getSensorData(now);
|
||||
if (value != null) {
|
||||
return cyclingCadenceHandler.getSensorData(now);
|
||||
}
|
||||
}
|
||||
|
||||
Instant getNow();
|
||||
SensorData<AggregatorRunning.Data> value = runningSpeedCadenceDistanceHandler.getSensorData(now);
|
||||
if (value != null && value.data().cadence() != null) {
|
||||
return new SensorData<>(value.data().cadence(), value.sensorNameOrAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private SensorData<Distance> getDistance(Instant now) {
|
||||
{
|
||||
SensorData<AggregatorCyclingDistanceSpeed.Data> value = cyclingDistanceSpeedHandler.getSensorData(now);
|
||||
if (value != null) {
|
||||
return new SensorData<>(value.data().distanceOverall(), value.sensorNameOrAddress());
|
||||
}
|
||||
}
|
||||
|
||||
SensorData<AggregatorRunning.Data> value = runningSpeedCadenceDistanceHandler.getSensorData(now);
|
||||
if (value != null) {
|
||||
return new SensorData<>(value.data().distance(), value.sensorNameOrAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
//TOOD simplify?
|
||||
private SensorData<Speed> getSpeed(Instant now) {
|
||||
{
|
||||
SensorData<AggregatorCyclingDistanceSpeed.Data> value = cyclingDistanceSpeedHandler.getSensorData(now);
|
||||
if (value != null && value.data() != null && value.data().speed() != null) {
|
||||
return new SensorData<>(value.data().speed(), value.sensorNameOrAddress());
|
||||
}
|
||||
}
|
||||
|
||||
SensorData<AggregatorRunning.Data> value = runningSpeedCadenceDistanceHandler.getSensorData(now);
|
||||
if (value != null && value.data() != null && value.data().speed() != null) {
|
||||
return new SensorData<>(value.data().speed(), value.sensorNameOrAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void onChange() {
|
||||
observer.onChange();
|
||||
}
|
||||
|
||||
public SensorDataSet getSensorDataSet(Instant now) {
|
||||
//We always need a Position with now
|
||||
SensorData<Position> gpsSensorData = gpsHandler.getSensorData(now);
|
||||
if (gpsSensorData != null && gpsSensorData.data() != null) {
|
||||
gpsSensorData = new SensorData<>(gpsSensorData.data().with(now), gpsSensorData.sensorNameOrAddress());
|
||||
} else {
|
||||
gpsSensorData = new SensorData<>(Position.of(now), "");
|
||||
}
|
||||
|
||||
return new SensorDataSet(
|
||||
gpsSensorData,
|
||||
getSpeed(now),
|
||||
getDistance(now),
|
||||
heartRateHandler.getSensorData(now),
|
||||
temperatureHandler.getSensorData(now),
|
||||
getCadence(now),
|
||||
cyclingPowerHandler.getSensorData(now),
|
||||
altitudeChangeHandler.getSensorData(now)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isStarted() {
|
||||
return wakeLock != null;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setAggregator(AggregatorHeartRate data) {
|
||||
heartRateHandler.setAggregator(data);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setAggregator(@NonNull AggregatorTemperature data) {
|
||||
temperatureHandler.setAggregator(data);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setAggregator(@NonNull AggregatorCyclingCadence data) {
|
||||
cyclingCadenceHandler.setAggregator(data);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setAggregator(@NonNull AggregatorCyclingDistanceSpeed data) {
|
||||
cyclingDistanceSpeedHandler.setAggregator(data);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setAggregator(@NonNull AggregatorCyclingPower data) {
|
||||
cyclingPowerHandler.setAggregator(data);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setAggregator(@NonNull AggregatorRunning data) {
|
||||
runningSpeedCadenceDistanceHandler.setAggregator(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Temperature;
|
||||
import de.dennisguse.opentracks.sensors.driver.BluetoothDriver;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.driver.TemperatureBluetooth;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
|
||||
|
||||
public class TemperatureHandler extends SensorHandler<Temperature, Temperature> {
|
||||
|
||||
protected TemperatureHandler(SensorManager sensorManager) {
|
||||
super(sensorManager);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Driver createDriverBluetooth(String address) {
|
||||
return new BluetoothDriver<>(
|
||||
BluetoothUtils.getAdapter(getContext()),
|
||||
this,
|
||||
new TemperatureBluetooth()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected AggregatorTemperature createAggregator(String sensorAddress, String sensorName) {
|
||||
return new AggregatorTemperature(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSensorPreferenceKey() {
|
||||
return R.string.settings_sensor_bluetooth_temperature_key;
|
||||
}
|
||||
}
|
||||
+5
-16
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
@@ -6,12 +6,9 @@ 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;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface {
|
||||
public class BarometerBluetooth implements BluetoothDriver.BluetoothParser<AtmosphericPressure> {
|
||||
static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
|
||||
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
|
||||
ENVIRONMENTAL_SENSING_SERVICE,
|
||||
@@ -24,16 +21,8 @@ public class BluetoothHandlerBarometricPressure implements SensorHandlerInterfac
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
AtmosphericPressure value = parseEnvironmentalSensing(characteristic);
|
||||
if (value == null) return;
|
||||
|
||||
observer.onChange(new Raw<>(observer.getNow(), value));
|
||||
public AtmosphericPressure parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
||||
return parseEnvironmentalSensing(characteristic);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9,22 +9,23 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.sensors.SensorManager;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
|
||||
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 static final Duration SAMPLING_PERIOD = Duration.ofSeconds(5);
|
||||
|
||||
private final SensorManager.SensorDataChangedObserver observer;
|
||||
private final DriverObserver<AtmosphericPressure> observer;
|
||||
|
||||
private Context context;
|
||||
private android.hardware.SensorManager sensorService;
|
||||
|
||||
private Instant lastEventTime = Instant.EPOCH;
|
||||
|
||||
private final SensorEventListener sensorEventListener = new SensorEventListener() {
|
||||
@Override
|
||||
@@ -34,7 +35,14 @@ public class BarometerInternal implements Driver {
|
||||
return;
|
||||
}
|
||||
|
||||
observer.onChange(new Raw<>(observer.getNow(), AtmosphericPressure.ofHPA(event.values[0])));
|
||||
Instant currentTime = Instant.now();
|
||||
|
||||
if (lastEventTime.plus(SAMPLING_PERIOD).isAfter(currentTime)) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastEventTime = currentTime;
|
||||
observer.onDataReceived(AtmosphericPressure.ofHPA(event.values[0]));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -43,40 +51,35 @@ public class BarometerInternal implements Driver {
|
||||
}
|
||||
};
|
||||
|
||||
public BarometerInternal(@NonNull SensorManager.SensorDataChangedObserver observer) {
|
||||
public BarometerInternal(@NonNull DriverObserver<AtmosphericPressure> observer) {
|
||||
this.observer = observer;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(Context context, Handler handler, String addressIgnored) {
|
||||
sensorService = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||
Sensor pressureSensor = sensorService.getDefaultSensor(Sensor.TYPE_PRESSURE);
|
||||
int samplingPeriodUs = (int) TimeUnit.MICROSECONDS.convert(SAMPLING_PERIOD);
|
||||
|
||||
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.");
|
||||
if (pressureSensor != null && sensorService.registerListener(sensorEventListener, pressureSensor, samplingPeriodUs, handler)) {
|
||||
observer.onConnected(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sensorManager.registerListener(sensorEventListener, pressureSensor, SAMPLING_PERIOD, handler)) {
|
||||
this.context = context;
|
||||
observer.onConnect(new AggregatorBarometer("internal", null));
|
||||
return;
|
||||
}
|
||||
|
||||
disconnect();
|
||||
Log.w(TAG, "No pressure sensor available.");
|
||||
observer.onDisconnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return context != null;
|
||||
return sensorService != 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;
|
||||
sensorService.unregisterListener(sensorEventListener);
|
||||
sensorService = null;
|
||||
}
|
||||
}
|
||||
|
||||
+37
-49
@@ -14,7 +14,7 @@
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
@@ -31,23 +31,25 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
/**
|
||||
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
|
||||
*/
|
||||
//TODO Re-act to Bluetooth disabled/enabled
|
||||
@SuppressLint("MissingPermission")
|
||||
public class BluetoothConnectionManager implements Driver {
|
||||
public class BluetoothDriver<T> implements Driver {
|
||||
|
||||
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
|
||||
private static final String TAG = BluetoothDriver.class.getSimpleName();
|
||||
|
||||
private final SensorManager.SensorDataChangedObserver observer;
|
||||
private final DriverObserver<T> observer;
|
||||
|
||||
private final SensorHandlerInterface sensorHandler;
|
||||
private final BluetoothParser<T> sensorHandler;
|
||||
|
||||
private final BluetoothAdapter bluetoothAdapter;
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
@@ -55,27 +57,29 @@ public class BluetoothConnectionManager implements Driver {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
switch (newState) {
|
||||
case BluetoothProfile.STATE_CONNECTING ->
|
||||
Log.i(TAG, gatt.getDevice() + ": connecting to sensor");
|
||||
case BluetoothProfile.STATE_CONNECTING -> {
|
||||
Log.i(TAG, gatt.getDevice() + ": connecting to sensor");
|
||||
}
|
||||
case BluetoothProfile.STATE_CONNECTED -> {
|
||||
Log.i(TAG, gatt.getDevice() + ": connected to sensor; discovering services");
|
||||
gatt.discoverServices();
|
||||
}
|
||||
case BluetoothProfile.STATE_DISCONNECTING ->
|
||||
Log.i(TAG, gatt.getDevice() + ": disconnecting from sensor: ");
|
||||
case BluetoothProfile.STATE_DISCONNECTING -> 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, gatt.getDevice() + ": disconnected from sensor: trying to reconnect");
|
||||
if (gatt.connect()) {
|
||||
Log.e(TAG, gatt.getDevice() + ": could not trigger reconnect for sensor");
|
||||
}
|
||||
clearData();
|
||||
observer.onConnectionLost();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
|
||||
BluetoothDevice device = gatt.getDevice();
|
||||
|
||||
BluetoothGattService gattService = null;
|
||||
ServiceMeasurementUUID serviceMeasurement = null;
|
||||
for (ServiceMeasurementUUID s : sensorHandler.getServices()) {
|
||||
@@ -87,13 +91,13 @@ public class BluetoothConnectionManager implements Driver {
|
||||
}
|
||||
|
||||
if (gattService == null) {
|
||||
Log.e(TAG, gatt.getDevice() + ": could not get gattService for serviceUUID=" + serviceMeasurement);
|
||||
Log.e(TAG, device + ": could not get gattService for serviceUUID=" + serviceMeasurement);
|
||||
return;
|
||||
}
|
||||
|
||||
BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(serviceMeasurement.measurementUUID());
|
||||
if (characteristic == null) {
|
||||
Log.e(TAG, gatt.getDevice() + ": could not get BluetoothCharacteristic for serviceUUID=" + serviceMeasurement.serviceUUID() + " characteristicUUID=" + serviceMeasurement.measurementUUID());
|
||||
Log.e(TAG, device + ": could not get BluetoothCharacteristic for serviceUUID=" + serviceMeasurement.serviceUUID() + " characteristicUUID=" + serviceMeasurement.measurementUUID());
|
||||
return;
|
||||
}
|
||||
gatt.setCharacteristicNotification(characteristic, true);
|
||||
@@ -111,6 +115,8 @@ public class BluetoothConnectionManager implements Driver {
|
||||
if (!gatt.writeDescriptor(descriptor)) {
|
||||
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID descriptor could not be written");
|
||||
}
|
||||
|
||||
observer.onConnected(device.getAddress(), device.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -127,11 +133,15 @@ public class BluetoothConnectionManager implements Driver {
|
||||
return;
|
||||
}
|
||||
|
||||
sensorHandler.handlePayload(observer, serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
|
||||
T value = sensorHandler.parsePayload(serviceMeasurementUUID.get(), gatt.getDevice().getName(), characteristic);
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
observer.onDataReceived(value);
|
||||
}
|
||||
};
|
||||
|
||||
BluetoothConnectionManager(BluetoothAdapter bluetoothAdapter, SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) {
|
||||
public BluetoothDriver(BluetoothAdapter bluetoothAdapter, DriverObserver<T> observer, BluetoothParser<T> sensorHandler) {
|
||||
this.bluetoothAdapter = bluetoothAdapter;
|
||||
this.observer = observer;
|
||||
this.sensorHandler = sensorHandler;
|
||||
@@ -144,26 +154,11 @@ public class BluetoothConnectionManager implements Driver {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SensorType.NONE.getPreferenceValue().equals(address)) {
|
||||
Log.w(TAG, "NONE: going to disconnect");
|
||||
if (isConnected()) {
|
||||
disconnect();
|
||||
observer.onRemove(sensorHandler.createEmptySensorData(null, null));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isConnected()) {
|
||||
Log.w(TAG, "Already connected; ignoring.");
|
||||
Log.e(TAG, "Already connected; ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSameBluetoothDevice(address)) {
|
||||
return;
|
||||
} else {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
BluetoothDevice device;
|
||||
try {
|
||||
device = bluetoothAdapter.getRemoteDevice(address);
|
||||
@@ -175,37 +170,23 @@ public class BluetoothConnectionManager implements Driver {
|
||||
Log.d(TAG, device + ": trying to connect");
|
||||
|
||||
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
|
||||
|
||||
observer.onConnect(sensorHandler.createEmptySensorData(device.getAddress(), device.getName()));
|
||||
}
|
||||
|
||||
private synchronized void clearData() {
|
||||
observer.onDisconnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress(), bluetoothGatt.getDevice().getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void disconnect() {
|
||||
if (!isConnected()) {
|
||||
Log.w(TAG, "Not connected; no need to re-connect.");
|
||||
Log.w(TAG, "Not connected; cannot disconnect.");
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, bluetoothGatt.getDevice() + ": start disconnect");
|
||||
bluetoothGatt.disconnect();
|
||||
bluetoothGatt.close();
|
||||
clearData();
|
||||
Log.i(TAG, bluetoothGatt.getDevice() + ": disconnect finished");
|
||||
bluetoothGatt = null;
|
||||
|
||||
observer.onDisconnected();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -214,4 +195,11 @@ public class BluetoothConnectionManager implements Driver {
|
||||
public boolean isConnected() {
|
||||
return bluetoothGatt != null;
|
||||
}
|
||||
|
||||
public interface BluetoothParser<T> {
|
||||
|
||||
List<ServiceMeasurementUUID> getServices();
|
||||
|
||||
T parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
// Implementation of metrics from a Bosch Smart System eBike via BLE.
|
||||
// as described and implemented here: https://github.com/RobbyPee/Bosch-Smart-System-Ebike-Garmin-Android
|
||||
//
|
||||
// The bike exposes a single custom BLE service with one notification characteristic onto
|
||||
// which all measurements are multiplexed as concatenated frames.
|
||||
//
|
||||
// ```
|
||||
// 30 <length> <id-hi> <id-lo> [08 <varint...>]
|
||||
// ```
|
||||
//
|
||||
// Frame format
|
||||
//
|
||||
// - `30` : start byte
|
||||
// - `<length>` - number of payload bytes following (min 2, which means value = 0)
|
||||
// - two-byte big-endian message ID
|
||||
// - `08` : varint type tag (absent when value is 0)
|
||||
// - varint : strip MSB from each byte, 7-bit groups, little-endian concatenation
|
||||
//
|
||||
// Multiple frames may be packed into a single characteristic notification.
|
||||
//
|
||||
|
||||
public final class BoschEbikeParser {
|
||||
|
||||
private static final String TAG = BoschEbikeParser.class.getSimpleName();
|
||||
|
||||
private static final int MESSAGE_ID_CADENCE = 0x985A;
|
||||
private static final int MESSAGE_ID_HUMAN_POWER = 0x985B;
|
||||
private static final int MESSAGE_ID_SPEED = 0x982D;
|
||||
private static final int MESSAGE_ID_MOTOR_POWER = 0x985D;
|
||||
private static final int MESSAGE_ID_BATTERY = 0x8088;
|
||||
private static final int MESSAGE_ID_ASSIST_MODE = 0x9809;
|
||||
|
||||
private static final int START_BYTE = 0x30;
|
||||
private static final int VARINT_TAG = 0x08;
|
||||
private static final int MAX_VARINT_32_BIT_BYTES = 5;
|
||||
|
||||
public static final ServiceMeasurementUUID BOSCH_EBIKE = new ServiceMeasurementUUID(
|
||||
UUID.fromString("00000010-eaa2-11e9-81b4-2a2ae2dbcce4"),
|
||||
UUID.fromString("00000011-eaa2-11e9-81b4-2a2ae2dbcce4"));
|
||||
|
||||
private BoschEbikeParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a single varint from {@code data}. Uses little-endian 7-bit groups; MSB=1 means another byte follows.
|
||||
*/
|
||||
private static Integer decodeVarint(byte[] data) {
|
||||
if (data == null || data.length == 0) {
|
||||
Log.d(TAG, "Malformed varint: empty payload");
|
||||
return null;
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
if (i >= MAX_VARINT_32_BIT_BYTES) {
|
||||
Log.d(TAG, "Malformed varint: exceeds 32-bit limit after " + i + " bytes");
|
||||
return null;
|
||||
}
|
||||
|
||||
int byteValue = data[i] & 0x7F;
|
||||
result |= byteValue << (7 * i);
|
||||
|
||||
if ((data[i] & 0x80) == 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Malformed varint: truncated after " + data.length + " bytes");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses one Bosch notification payload, which may contain multiple concatenated frames, and returns the measurements recognized in that payload.
|
||||
*/
|
||||
static Data parse(byte[] raw) {
|
||||
if (raw == null || raw.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Data data = splitFrames(raw).stream()
|
||||
.filter(BoschEbikeParser::isValidFrame)
|
||||
.filter(BoschEbikeParser::isSupportedFrame)
|
||||
.map(BoschEbikeParser::decodeFrame)
|
||||
.filter(decodedFrame -> decodedFrame != null)
|
||||
.reduce(null,
|
||||
BoschEbikeParser::processMessage,
|
||||
(left, right) -> right);
|
||||
Log.d(TAG, "Parsed Bosch data: " + data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static List<byte[]> splitFrames(byte[] raw) {
|
||||
List<byte[]> frames = new ArrayList<>();
|
||||
|
||||
int i = 0;
|
||||
while (i < raw.length) {
|
||||
if (raw[i] != START_BYTE) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i + 1 >= raw.length) {
|
||||
Log.d(TAG, "Malformed frame at offset " + i + ": missing length byte (raw length=" + raw.length + ")");
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int length = raw[i + 1] & 0xFF;
|
||||
int frameEnd = i + length + 2;
|
||||
frames.add(Arrays.copyOfRange(raw, i, Math.min(frameEnd, raw.length)));
|
||||
i = frameEnd;
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
private static boolean isValidFrame(byte[] frame) {
|
||||
int length = getFrameLength(frame);
|
||||
if (length < 2) {
|
||||
Log.d(TAG, "Malformed frame: payload length " + length + " is shorter than minimum 2 bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frame.length != length + 2) {
|
||||
Log.d(TAG, "Malformed frame: expected " + (length + 2) + " bytes, got " + frame.length);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isSupportedFrame(byte[] frame) {
|
||||
boolean supported = getFrameLength(frame) == 2 || (frame[4] & 0xFF) == VARINT_TAG;
|
||||
if (!supported) {
|
||||
Log.d(TAG, "Unsupported frame: tag 0x" + String.format("%02X", frame[4] & 0xFF));
|
||||
}
|
||||
return supported;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static DecodedFrame decodeFrame(byte[] frame) {
|
||||
if (getFrameLength(frame) == 2) {
|
||||
return new DecodedFrame(getMessageId(frame), 0);
|
||||
}
|
||||
|
||||
Integer varintResult = decodeVarint(Arrays.copyOfRange(frame, 5, frame.length));
|
||||
if (varintResult == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DecodedFrame(getMessageId(frame), varintResult);
|
||||
}
|
||||
|
||||
private static int getFrameLength(byte[] frame) {
|
||||
return frame[1] & 0xFF;
|
||||
}
|
||||
|
||||
private static int getMessageId(byte[] frame) {
|
||||
return (frame[2] & 0xFF) << 8 | (frame[3] & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a message ID and value, updating the data with any measurements.
|
||||
*/
|
||||
@Nullable
|
||||
private static Data processMessage(@Nullable Data data, DecodedFrame decodedFrame) {
|
||||
switch (decodedFrame.messageId()) {
|
||||
case MESSAGE_ID_CADENCE:
|
||||
return new Data(null, Cadence.of(decodedFrame.value() / 2.0f), null, data);
|
||||
case MESSAGE_ID_HUMAN_POWER:
|
||||
return new Data(null, null, Power.of((float) decodedFrame.value()), data);
|
||||
case MESSAGE_ID_SPEED:
|
||||
double speed_kmh = decodedFrame.value() / 100.0;
|
||||
return new Data(Speed.ofKMH(speed_kmh), null, null, data);
|
||||
case MESSAGE_ID_MOTOR_POWER:
|
||||
case MESSAGE_ID_BATTERY:
|
||||
case MESSAGE_ID_ASSIST_MODE:
|
||||
Log.d(TAG, "Ignoring unimplemented message: raw=" + decodedFrame.value());
|
||||
return data;
|
||||
}
|
||||
Log.d(TAG, "Ignoring unknown message: raw=" + decodedFrame.value());
|
||||
return data;
|
||||
}
|
||||
|
||||
public record Data(
|
||||
@Nullable Speed speed,
|
||||
@Nullable Cadence cadence,
|
||||
@Nullable Power humanPower) {
|
||||
|
||||
public Data(@Nullable Speed speed, @Nullable Cadence cadence, @Nullable Power humanPower, @Nullable Data oldData) {
|
||||
this(
|
||||
speed != null ? speed : oldData != null ? oldData.speed() : null,
|
||||
cadence != null ? cadence : oldData != null ? oldData.cadence() : null,
|
||||
humanPower != null ? humanPower : oldData != null ? oldData.humanPower() : null);
|
||||
}
|
||||
}
|
||||
|
||||
private record DecodedFrame(int messageId, int value) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
public class CyclingCadenceBluetooth implements BluetoothDriver.BluetoothParser<CyclingCadenceBluetooth.CrankData> {
|
||||
private static final String TAG = CyclingCadenceBluetooth.class.getSimpleName();
|
||||
|
||||
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of(
|
||||
CyclingPowerBluetooth.CYCLING_POWER,
|
||||
CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return CYCLING_CADENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CrankData parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
||||
if (serviceMeasurementUUID.equals(CyclingPowerBluetooth.CYCLING_POWER)) {
|
||||
CyclingPowerBluetooth.Data data = CyclingPowerBluetooth.parseCyclingPower(characteristic);
|
||||
if (data != null && data.crank() != null) {
|
||||
return data.crank();
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceMeasurementUUID.equals(CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE)) {
|
||||
Pair<CyclingDistanceSpeedBluetooth.WheelData, CrankData> data = CyclingDistanceSpeedBluetooth.parseCyclingCrankAndWheel(characteristic);
|
||||
|
||||
if (data != null && data.second != null) {
|
||||
return data.second;
|
||||
}
|
||||
}
|
||||
|
||||
Log.e(TAG, "Don't know how to decode this payload.");
|
||||
return null;
|
||||
}
|
||||
|
||||
public record CrankData(
|
||||
long crankRevolutionsCount, // UINT32
|
||||
int crankRevolutionsTime // UINT16; 1/1024s
|
||||
) {}
|
||||
}
|
||||
+14
-18
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.util.Pair;
|
||||
@@ -9,11 +9,9 @@ import androidx.annotation.VisibleForTesting;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterface {
|
||||
public class CyclingDistanceSpeedBluetooth implements BluetoothDriver.BluetoothParser<CyclingDistanceSpeedBluetooth.WheelData> {
|
||||
|
||||
public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID(
|
||||
new UUID(0x181600001000L, 0x800000805f9b34fbL),
|
||||
@@ -26,21 +24,18 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatorCyclingDistanceSpeed createEmptySensorData(String address, String name) {
|
||||
return new AggregatorCyclingDistanceSpeed(address, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> data = parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
if (data.first != null) {
|
||||
observer.onChange(new Raw<>(observer.getNow(), data.first));
|
||||
public WheelData parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
||||
Pair<WheelData, CyclingCadenceBluetooth.CrankData> data = parseCyclingCrankAndWheel(characteristic);
|
||||
if (data != null && data.first != null) {
|
||||
return data.first;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public static Pair<WheelData, BluetoothHandlerCyclingCadence.CrankData> parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
public static Pair<WheelData, CyclingCadenceBluetooth.CrankData> parseCyclingCrankAndWheel(@NonNull BluetoothGattCharacteristic characteristic) {
|
||||
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength == 0) {
|
||||
@@ -61,13 +56,13 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
|
||||
index += 2;
|
||||
}
|
||||
|
||||
BluetoothHandlerCyclingCadence.CrankData crankData = null;
|
||||
CyclingCadenceBluetooth.CrankData crankData = null;
|
||||
if (hasCrank && valueLength - index >= 4) {
|
||||
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
|
||||
index += 2;
|
||||
|
||||
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime);
|
||||
crankData = new CyclingCadenceBluetooth.CrankData(crankCount, crankTime);
|
||||
}
|
||||
|
||||
return new Pair<>(wheelData, crankData);
|
||||
@@ -78,5 +73,6 @@ public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterf
|
||||
long wheelRevolutionsCount, // UINT32
|
||||
|
||||
int wheelRevolutionsTime // UINT16; 1/1024s
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
+26
-20
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
@@ -9,34 +9,37 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
public class BluetoothHandlerCyclingPower implements SensorHandlerInterface {
|
||||
public class CyclingPowerBluetooth implements BluetoothDriver.BluetoothParser<CyclingPowerBluetooth.Data> {
|
||||
|
||||
public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID(
|
||||
new UUID(0x181800001000L, 0x800000805f9b34fbL),
|
||||
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
public static final List<ServiceMeasurementUUID> SUPPORTED_SERVICES = List.of(
|
||||
CYCLING_POWER,
|
||||
BoschEbikeParser.BOSCH_EBIKE
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return List.of(CYCLING_POWER);
|
||||
return SUPPORTED_SERVICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatorCyclingPower createEmptySensorData(String address, String name) {
|
||||
return new AggregatorCyclingPower(address, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Data cyclingPower = parseCyclingPower(characteristic);
|
||||
|
||||
if (cyclingPower != null) {
|
||||
observer.onChange(new Raw<>(observer.getNow(), cyclingPower));
|
||||
public CyclingPowerBluetooth.Data parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
||||
if (serviceMeasurementUUID.equals(CYCLING_POWER)) {
|
||||
return parseCyclingPower(characteristic);
|
||||
}
|
||||
if (serviceMeasurementUUID.equals(BoschEbikeParser.BOSCH_EBIKE)) {
|
||||
BoschEbikeParser.Data boschData = BoschEbikeParser.parse(characteristic.getValue());
|
||||
if (boschData != null && boschData.humanPower() != null) {
|
||||
return new Data(boschData.humanPower(), null);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,19 +72,22 @@ public class BluetoothHandlerCyclingPower implements SensorHandlerInterface {
|
||||
index += 2 + 2;
|
||||
}
|
||||
|
||||
BluetoothHandlerCyclingCadence.CrankData crankData = null;
|
||||
CyclingCadenceBluetooth.CrankData crankData = null;
|
||||
if (hasCrank && valueLength - index >= 4) {
|
||||
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
|
||||
index += 2;
|
||||
|
||||
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
|
||||
crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime);
|
||||
crankData = new CyclingCadenceBluetooth.CrankData(crankCount, crankTime);
|
||||
}
|
||||
|
||||
return new Data(Power.of(instantaneousPower), crankData);
|
||||
}
|
||||
|
||||
public record Data(Power power, BluetoothHandlerCyclingCadence.CrankData crank) {}
|
||||
|
||||
public record Data(
|
||||
Power power,
|
||||
CyclingCadenceBluetooth.CrankData crank
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,11 @@ import android.content.Context;
|
||||
import android.os.Handler;
|
||||
|
||||
public interface Driver {
|
||||
|
||||
//TODO Address is optional for internal drivers. Move address to driver instantiation.
|
||||
void connect(Context context, Handler handler, String address);
|
||||
|
||||
//TODO Remove from interface?
|
||||
boolean isConnected();
|
||||
|
||||
void disconnect();
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
|
||||
public interface DriverObserver<T> {
|
||||
/**
|
||||
* The Driver could connect to the sensor.
|
||||
*/
|
||||
void onConnected(String sensorAddress, String sensorName);
|
||||
|
||||
/**
|
||||
* Sensor got temporarily disconnected, Driver should try to re-connect.
|
||||
*/
|
||||
void onConnectionLost();
|
||||
|
||||
/**
|
||||
* The Driver provided data from the sensor.
|
||||
*/
|
||||
void onDataReceived(T value);
|
||||
|
||||
/**
|
||||
* The Driver was shutdown and no further data will be provided.
|
||||
*/
|
||||
//TODO check if really need this, because the driver was told to disconnect and then just signalizes that it finished this task.
|
||||
void onDisconnected();
|
||||
|
||||
/**
|
||||
* The Driver detected that the sensor got deactivated.
|
||||
* For example, the user deactivated the internal GPS.
|
||||
*/
|
||||
default void onSensorDeactivated() {}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.location.LocationListenerCompat;
|
||||
import androidx.core.location.LocationManagerCompat;
|
||||
import androidx.core.location.LocationRequestCompat;
|
||||
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.PermissionRequester;
|
||||
|
||||
public class GpsInternal implements Driver {
|
||||
|
||||
private static final String TAG = GpsInternal.class.getSimpleName();
|
||||
|
||||
public static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
|
||||
|
||||
@VisibleForTesting
|
||||
public final LocationListenerCompat locationListenerCompat = new LocationListenerCompat() {
|
||||
@Override
|
||||
public void onLocationChanged(@NonNull Location location) {
|
||||
driverObserver.onDataReceived(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(@NonNull String provider) {
|
||||
driverObserver.onConnected(null, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(@NonNull String provider) {
|
||||
driverObserver.onSensorDeactivated();
|
||||
}
|
||||
};
|
||||
|
||||
private final DriverObserver<Location> driverObserver;
|
||||
|
||||
@VisibleForTesting
|
||||
public LocationManager locationManager;
|
||||
|
||||
public GpsInternal(DriverObserver<Location> driverObserver) {
|
||||
this.driverObserver = driverObserver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(Context context, Handler handler, String address) {
|
||||
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
|
||||
if (!LocationManagerCompat.hasProvider(locationManager, LOCATION_PROVIDER)) {
|
||||
Log.e(TAG, "Device doesn't have GPS.");
|
||||
return;
|
||||
}
|
||||
|
||||
LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(PreferencesUtils.getMinSamplingInterval().toMillis())
|
||||
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
|
||||
.setMaxUpdateDelayMillis(0)
|
||||
.build();
|
||||
|
||||
if (PermissionRequester.GPS.hasPermission(context)) {
|
||||
try {
|
||||
Log.i(TAG, "Register for location updates " + context);
|
||||
driverObserver.onConnected(null, null);
|
||||
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, handler::post, locationListenerCompat);
|
||||
return;
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
|
||||
}
|
||||
}
|
||||
|
||||
driverObserver.onDisconnected();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"MissingPermission"})
|
||||
@Override
|
||||
public void disconnect() {
|
||||
if (!isConnected()) return;
|
||||
|
||||
LocationManagerCompat.removeUpdates(locationManager, locationListenerCompat);
|
||||
|
||||
locationManager = null;
|
||||
driverObserver.onDisconnected();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return locationManager != null;
|
||||
}
|
||||
}
|
||||
+5
-16
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
@@ -9,11 +9,9 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
public class BluetoothHandlerHeartRate implements SensorHandlerInterface {
|
||||
public class HeartRateBluetooth implements BluetoothDriver.BluetoothParser<HeartRate> {
|
||||
|
||||
public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID(
|
||||
new UUID(0x180D00001000L, 0x800000805f9b34fbL),
|
||||
@@ -36,17 +34,8 @@ public class BluetoothHandlerHeartRate implements SensorHandlerInterface {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatorHeartRate createEmptySensorData(String address, String name) {
|
||||
return new AggregatorHeartRate(address, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
HeartRate heartRate = parseHeartRate(characteristic);
|
||||
|
||||
if (heartRate != null) {
|
||||
observer.onChange(new Raw<>(observer.getNow(), heartRate));
|
||||
}
|
||||
public HeartRate parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
||||
return parseHeartRate(characteristic);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
+5
-13
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
@@ -11,11 +11,9 @@ import java.util.UUID;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
public class BluetoothHandlerRunningSpeedAndCadence implements SensorHandlerInterface {
|
||||
public class RunningSpeedAndCadenceBluetooth implements BluetoothDriver.BluetoothParser<RunningSpeedAndCadenceBluetooth.Data> {
|
||||
|
||||
|
||||
public static final ServiceMeasurementUUID RUNNING_SPEED_CADENCE = new ServiceMeasurementUUID(
|
||||
@@ -29,14 +27,8 @@ public class BluetoothHandlerRunningSpeedAndCadence implements SensorHandlerInte
|
||||
}
|
||||
|
||||
@Override
|
||||
public AggregatorRunning createEmptySensorData(String address, String name) {
|
||||
return new AggregatorRunning(address, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Data data = parseRunningSpeedAndCadence(sensorName, characteristic);
|
||||
observer.onChange(new Raw<>(observer.getNow(), data));
|
||||
public Data parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
||||
return parseRunningSpeedAndCadence(sensorName, characteristic);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
+5
-3
@@ -1,8 +1,10 @@
|
||||
package de.dennisguse.opentracks.sensors;
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
public enum SensorType {
|
||||
NONE("NONE"),
|
||||
INTERNAL("INTERNAL"),
|
||||
NONE("NONE"), //TODO Use R.string.sensor_type_value_none
|
||||
INTERNAL("INTERNAL"), //TODO Use R.string.sensor_type_value_none
|
||||
|
||||
//NOTE: preferenceValue of REMOTE should not be used anywhere.
|
||||
REMOTE("*");
|
||||
|
||||
private final String preferenceValue;
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.dennisguse.opentracks.sensors.driver;
|
||||
|
||||
import static de.dennisguse.opentracks.sensors.driver.BarometerBluetooth.ENVIRONMENTAL_SENSING_SERVICE;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Temperature;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
public class TemperatureBluetooth implements BluetoothDriver.BluetoothParser<Temperature> {
|
||||
public static final ServiceMeasurementUUID TEMPERATURE = new ServiceMeasurementUUID(
|
||||
ENVIRONMENTAL_SENSING_SERVICE,
|
||||
new UUID(0x2A6E00001000L, 0x800000805f9b34fbL)
|
||||
);
|
||||
|
||||
@Override
|
||||
public List<ServiceMeasurementUUID> getServices() {
|
||||
return List.of(TEMPERATURE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Temperature parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
||||
return parseEnvironmentalSensing(characteristic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoding: org.bluetooth.characteristic.temperature.xml
|
||||
*/
|
||||
public static Temperature parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] raw = characteristic.getValue();
|
||||
|
||||
if (raw.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 0);
|
||||
return Temperature.ofCentiCelsius(pressure);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,74 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
public abstract class Aggregator<Input, Output> {
|
||||
public abstract class Aggregator<InputType, OutputType> {
|
||||
|
||||
private static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
|
||||
|
||||
protected Raw<Input> previous;
|
||||
@Nullable
|
||||
protected Raw<InputType> previous;
|
||||
|
||||
protected Output aggregatedValue;
|
||||
//TODO Make private
|
||||
protected OutputType output;
|
||||
|
||||
private final String sensorAddress;
|
||||
private final String sensorName;
|
||||
|
||||
Aggregator(String sensorAddress) {
|
||||
this(sensorAddress, null);
|
||||
}
|
||||
|
||||
Aggregator(String sensorAddress, String sensorName) {
|
||||
this.sensorAddress = sensorAddress;
|
||||
this.sensorName = sensorName;
|
||||
}
|
||||
|
||||
public String getSensorNameOrAddress() {
|
||||
private String getSensorNameOrAddress() {
|
||||
return sensorName != null ? sensorName : sensorAddress;
|
||||
}
|
||||
|
||||
public final void add(Raw<Input> current) {
|
||||
computeValue(current);
|
||||
previous = current;
|
||||
public final void add(Instant now, InputType current) {
|
||||
Raw<InputType> next = new Raw<>(now, current);
|
||||
computeValue(next);
|
||||
previous = next;
|
||||
}
|
||||
|
||||
protected abstract void computeValue(Raw<Input> current);
|
||||
protected abstract void computeValue(@NonNull Raw<InputType> current);
|
||||
|
||||
/**
|
||||
* @return did we process data from a sensor.
|
||||
* NOTE: for some sensors this may require more than one measurement.
|
||||
*/
|
||||
public boolean hasReceivedData() {
|
||||
return aggregatedValue != null;
|
||||
return output != null;
|
||||
}
|
||||
|
||||
@Deprecated //TODO Remove: for sensor there is no default value. Make getAggregatedValue() @Nullable
|
||||
@NonNull
|
||||
protected abstract Output getNoneValue();
|
||||
|
||||
@NonNull
|
||||
public Output getAggregatedValue(Instant now) {
|
||||
if (!hasReceivedData()) {
|
||||
return getNoneValue();
|
||||
}
|
||||
@Nullable
|
||||
protected OutputType getAggregatedValue(Instant now) {
|
||||
if (isOutdated(now)) {
|
||||
resetImmediate();
|
||||
resetOutdated();
|
||||
}
|
||||
|
||||
return aggregatedValue;
|
||||
return output;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public SensorData<Output> getAggregatedValueWithSensorName(Instant now) {
|
||||
public SensorData<OutputType> getAggregatedValueWithSensorName(Instant now) {
|
||||
return new SensorData<>(getAggregatedValue(now), getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset short-term (i.e., non-aggregated) values that were directly derived from sensor data.
|
||||
*/
|
||||
protected abstract void resetImmediate();
|
||||
protected void resetOutdated() {
|
||||
output = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset long-term (i.e., aggregated) values (more than derived from previous SensorData) like overall distance.
|
||||
*/
|
||||
public abstract void resetAggregated();
|
||||
public void resetAggregated() {};
|
||||
|
||||
/**
|
||||
* Is the data recent considering the current time.
|
||||
@@ -97,6 +92,12 @@ public abstract class Aggregator<Input, Output> {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "sensorAddress=" + sensorAddress + " data=" + aggregatedValue;
|
||||
return "sensorAddress=" + sensorAddress + " data=" + output;
|
||||
}
|
||||
|
||||
protected record Raw<T>(
|
||||
@NonNull Instant time,
|
||||
@NonNull T value
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.sensors.PressureSensorUtils;
|
||||
@@ -16,35 +15,30 @@ public class AggregatorBarometer extends Aggregator<AtmosphericPressure, Altitud
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeValue(Raw<AtmosphericPressure> current) {
|
||||
protected void computeValue(@NonNull Raw<AtmosphericPressure> current) {
|
||||
if (previous == null) {
|
||||
lastAcceptedSensorValue = current.value();
|
||||
aggregatedValue = getNoneValue();
|
||||
output = new AltitudeGainLoss(0f, 0f);
|
||||
return;
|
||||
}
|
||||
|
||||
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedSensorValue, previous.value(), current.value());
|
||||
if (altitudeChange != null) {
|
||||
aggregatedValue = new AltitudeGainLoss(aggregatedValue.gain_m() + altitudeChange.getAltitudeGain_m(), aggregatedValue.loss_m() + altitudeChange.getAltitudeLoss_m());
|
||||
if (output == null) {
|
||||
output = new AltitudeGainLoss(0f, 0f);
|
||||
}
|
||||
output = new AltitudeGainLoss(output.gain_m() + altitudeChange.getAltitudeGain_m(), output.loss_m() + altitudeChange.getAltitudeLoss_m());
|
||||
|
||||
lastAcceptedSensorValue = altitudeChange.currentSensorValue();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetImmediate() {
|
||||
protected void resetOutdated() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetAggregated() {
|
||||
aggregatedValue = getNoneValue();
|
||||
output = null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected AltitudeGainLoss getNoneValue() {
|
||||
return new AltitudeGainLoss(0f, 0f);
|
||||
}
|
||||
|
||||
public record Data(Altitude gain, Altitude loss) {}
|
||||
}
|
||||
|
||||
+6
-20
@@ -7,10 +7,10 @@ import androidx.annotation.NonNull;
|
||||
import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
|
||||
|
||||
public class AggregatorCyclingCadence extends Aggregator<BluetoothHandlerCyclingCadence.CrankData, Cadence> {
|
||||
public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth.CrankData, Cadence> {
|
||||
|
||||
private final String TAG = AggregatorCyclingCadence.class.getSimpleName();
|
||||
|
||||
@@ -19,7 +19,7 @@ public class AggregatorCyclingCadence extends Aggregator<BluetoothHandlerCycling
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeValue(Raw<BluetoothHandlerCyclingCadence.CrankData> current) {
|
||||
protected void computeValue(@NonNull Raw<CyclingCadenceBluetooth.CrankData> current) {
|
||||
if (previous == null) {
|
||||
return;
|
||||
}
|
||||
@@ -32,32 +32,18 @@ public class AggregatorCyclingCadence extends Aggregator<BluetoothHandlerCycling
|
||||
}
|
||||
if (timeDiff.isNegative()) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
aggregatedValue = null;
|
||||
output = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO We have to treat with overflow according to the documentation: read https://codeberg.org/OpenTracksApp/OpenTracks/pulls/953#issuecomment-6466930
|
||||
if (current.value().crankRevolutionsCount() < previous.value().crankRevolutionsCount()) {
|
||||
Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence.");
|
||||
output = null;
|
||||
return;
|
||||
}
|
||||
|
||||
long crankDiff = UintUtils.diff(current.value().crankRevolutionsCount(), previous.value().crankRevolutionsCount(), UintUtils.UINT32_MAX);
|
||||
aggregatedValue = Cadence.of(crankDiff, timeDiff);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetImmediate() {
|
||||
aggregatedValue = getNoneValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetAggregated() {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Cadence getNoneValue() {
|
||||
return Cadence.of(0);
|
||||
output = Cadence.of(crankDiff, timeDiff);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-17
@@ -8,10 +8,10 @@ import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
import de.dennisguse.opentracks.sensors.driver.CyclingDistanceSpeedBluetooth;
|
||||
|
||||
public class AggregatorCyclingDistanceSpeed extends Aggregator<BluetoothHandlerCyclingDistanceSpeed.WheelData, AggregatorCyclingDistanceSpeed.Data> {
|
||||
public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSpeedBluetooth.WheelData, AggregatorCyclingDistanceSpeed.Data> {
|
||||
|
||||
private final String TAG = AggregatorCyclingDistanceSpeed.class.getSimpleName();
|
||||
|
||||
@@ -22,7 +22,7 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<BluetoothHandlerC
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeValue(Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData> current) {
|
||||
protected void computeValue(@NonNull Raw<CyclingDistanceSpeedBluetooth.WheelData> current) {
|
||||
if (previous == null) {
|
||||
return;
|
||||
}
|
||||
@@ -35,43 +35,39 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<BluetoothHandlerC
|
||||
}
|
||||
if (timeDiff.isNegative()) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
aggregatedValue = null;
|
||||
output = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.value().wheelRevolutionsCount() < previous.value().wheelRevolutionsCount()) {
|
||||
Log.e(TAG, "Wheel revolutions count difference is invalid: cannot compute speed.");
|
||||
output = null;
|
||||
return;
|
||||
}
|
||||
long wheelDiff = UintUtils.diff(current.value().wheelRevolutionsCount(), previous.value().wheelRevolutionsCount(), UintUtils.UINT32_MAX);
|
||||
|
||||
Distance distance = wheelCircumference.multipliedBy(wheelDiff);
|
||||
Distance distanceOverall = distance;
|
||||
if (aggregatedValue != null) {
|
||||
distanceOverall = distance.plus(aggregatedValue.distanceOverall);
|
||||
if (output != null) {
|
||||
distanceOverall = distance.plus(output.distanceOverall);
|
||||
}
|
||||
Speed speed_mps = Speed.of(distance, timeDiff);
|
||||
aggregatedValue = new Data(distance, distanceOverall, speed_mps);
|
||||
output = new Data(distance, distanceOverall, speed_mps);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetImmediate() {
|
||||
aggregatedValue = new Data(Distance.ZERO, aggregatedValue.distanceOverall, Speed.ZERO);
|
||||
protected void resetOutdated() {
|
||||
Distance overallDistance = output != null ? output.distanceOverall : Distance.ZERO;
|
||||
output = new Data(Distance.ZERO, overallDistance, Speed.ZERO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetAggregated() {
|
||||
if (aggregatedValue != null) {
|
||||
aggregatedValue = new Data(aggregatedValue.distance, Distance.ZERO, aggregatedValue.speed);
|
||||
if (output != null) {
|
||||
output = new Data(output.distance, Distance.ZERO, output.speed);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Data getNoneValue() {
|
||||
return new Data(Distance.ZERO, Distance.ZERO, Speed.ZERO);
|
||||
}
|
||||
|
||||
public void setWheelCircumference(Distance wheelCircumference) {
|
||||
this.wheelCircumference = wheelCircumference;
|
||||
}
|
||||
|
||||
+4
-19
@@ -3,31 +3,16 @@ package de.dennisguse.opentracks.sensors.sensorData;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
|
||||
|
||||
public class AggregatorCyclingPower extends Aggregator<BluetoothHandlerCyclingPower.Data, Power> {
|
||||
public class AggregatorCyclingPower extends Aggregator<CyclingPowerBluetooth.Data, Power> {
|
||||
|
||||
public AggregatorCyclingPower(String name, String address) {
|
||||
super(name, address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void computeValue(Raw<BluetoothHandlerCyclingPower.Data> current) {
|
||||
this.aggregatedValue = current.value().power();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetImmediate() {
|
||||
aggregatedValue = getNoneValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetAggregated() {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Power getNoneValue() {
|
||||
return Power.of(0f);
|
||||
public void computeValue(@NonNull Raw<CyclingPowerBluetooth.Data> current) {
|
||||
this.output = current.value().power();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
|
||||
public class AggregatorGPS extends Aggregator<Position, Position> {
|
||||
public class AggregatorGPS extends Aggregator<Location, Position> {
|
||||
|
||||
|
||||
public AggregatorGPS(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
public AggregatorGPS(String sensorAddress, String sensorName) {
|
||||
super(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeValue(Raw<Position> current) {
|
||||
aggregatedValue = current.value();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetImmediate() {
|
||||
aggregatedValue = Position.empty();
|
||||
protected void computeValue(@NonNull Raw<Location> current) {
|
||||
output = Position.of(current.value(), current.time());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -28,21 +22,7 @@ public class AggregatorGPS extends Aggregator<Position, Position> {
|
||||
/*
|
||||
* GPS data is not an aggregated value, but for now we want to ensure to only save the data once.
|
||||
* The data is too large to save it more often than needed (i.e., duplicated values).
|
||||
* TODO: this behavior can be changed if TrackRecordingManager.insertTrackPoint() would strip GPS data if it was already saved. This would simplify TrackPointCreator.createCurrentTrackPoint()
|
||||
*/
|
||||
aggregatedValue = Position.empty();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Position getNoneValue() {
|
||||
return Position.empty();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Position getAggregatedValue(Instant now) {
|
||||
return super.getAggregatedValue(now)
|
||||
.with(now);
|
||||
resetOutdated();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,24 +11,9 @@ public class AggregatorHeartRate extends Aggregator<HeartRate, HeartRate> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void computeValue(Raw<HeartRate> current) {
|
||||
protected void computeValue(@NonNull Raw<HeartRate> current) {
|
||||
if (current.value().isValid()) {
|
||||
this.aggregatedValue = current.value();
|
||||
this.output = current.value();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetImmediate() {
|
||||
aggregatedValue = getNoneValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetAggregated() {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected HeartRate getNoneValue() {
|
||||
return HeartRate.of(0);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user