mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-12 22:03:07 +02:00
Compare commits
26 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 |
@@ -112,3 +112,15 @@ Here is a list of the smartphones tested:
|
|||||||
| Google | Nexus 4 | LineageOS 18.1 | Ok |
|
| Google | Nexus 4 | LineageOS 18.1 | Ok |
|
||||||
| Samsung | S4 | Android 11 | Ok |
|
| Samsung | S4 | Android 11 | Ok |
|
||||||
| Sony | Xperia XZ1 Compact | LineageOS 16.1 | Failure |
|
| 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
|
||||||
+15
-10
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:9.1.0'
|
classpath 'com.android.tools.build:gradle:9.2.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,11 +18,11 @@ allprojects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def getVersionName = { ->
|
def getVersionName = { ->
|
||||||
return 'git describe --tags'.execute().text.trim()
|
return 'git describe --tags'.execute(null, project.rootDir).text.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
def getVersionCodeNightly = { ->
|
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 {
|
android {
|
||||||
@@ -47,8 +47,8 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'de.dennisguse.opentracks'
|
applicationId 'de.dennisguse.opentracks'
|
||||||
versionCode 6627
|
versionCode 6653
|
||||||
versionName "v4.26.2"
|
versionName "v4.27.0"
|
||||||
|
|
||||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||||
|
|
||||||
@@ -107,7 +107,6 @@ android {
|
|||||||
nightly {
|
nightly {
|
||||||
dimension 'version'
|
dimension 'version'
|
||||||
applicationId 'de.dennisguse.opentracks.nightly'
|
applicationId 'de.dennisguse.opentracks.nightly'
|
||||||
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.nightly'
|
|
||||||
|
|
||||||
versionCode getVersionCodeNightly()
|
versionCode getVersionCodeNightly()
|
||||||
versionName getVersionName()
|
versionName getVersionName()
|
||||||
@@ -119,7 +118,6 @@ android {
|
|||||||
// Non-reproducible: https://f-droid.org/de/packages/de.dennisguse.opentracks/
|
// Non-reproducible: https://f-droid.org/de/packages/de.dennisguse.opentracks/
|
||||||
dimension 'version'
|
dimension 'version'
|
||||||
applicationId 'de.dennisguse.opentracks'
|
applicationId 'de.dennisguse.opentracks'
|
||||||
resValue 'string', 'applicationId', 'de.dennisguse.opentracks'
|
|
||||||
versionNameSuffix 'irreproducible'
|
versionNameSuffix 'irreproducible'
|
||||||
}
|
}
|
||||||
reproducible {
|
reproducible {
|
||||||
@@ -128,7 +126,6 @@ android {
|
|||||||
// PlayStore: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
|
// PlayStore: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
|
||||||
dimension 'version'
|
dimension 'version'
|
||||||
applicationId 'de.dennisguse.opentracks.playstore'
|
applicationId 'de.dennisguse.opentracks.playstore'
|
||||||
resValue 'string', 'applicationId', 'de.dennisguse.opentracks.playstore'
|
|
||||||
|
|
||||||
base.archivesName = "de.dennisguse.opentracks.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
|
base.archivesName = "de.dennisguse.opentracks.playstore_${getVersionName()}_${android.defaultConfig.versionCode}"
|
||||||
|
|
||||||
@@ -158,14 +155,22 @@ dependencies {
|
|||||||
implementation 'androidx.core:core-splashscreen:1.2.0'
|
implementation 'androidx.core:core-splashscreen:1.2.0'
|
||||||
implementation 'androidx.mediarouter:mediarouter:1.8.1'
|
implementation 'androidx.mediarouter:mediarouter:1.8.1'
|
||||||
implementation 'androidx.core:core-location-altitude:1.0.0-rc01'
|
implementation 'androidx.core:core-location-altitude:1.0.0-rc01'
|
||||||
implementation 'androidx.work:work-runtime:2.11.1'
|
implementation 'androidx.work:work-runtime:2.11.2'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:core:1.7.0'
|
androidTestImplementation 'androidx.test:core:1.7.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
||||||
androidTestImplementation 'androidx.test:rules:1.7.0'
|
androidTestImplementation 'androidx.test:rules:1.7.0'
|
||||||
androidTestImplementation 'androidx.test:runner:1.7.0'
|
androidTestImplementation 'androidx.test:runner:1.7.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
||||||
androidTestImplementation 'org.mockito:mockito-android:5.23.0'
|
|
||||||
|
|
||||||
androidTestUtil 'androidx.test:orchestrator:1.6.1'
|
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,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)
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -2,76 +2,117 @@ package de.dennisguse.opentracks;
|
|||||||
|
|
||||||
|
|
||||||
import static androidx.test.espresso.Espresso.onView;
|
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.action.ViewActions.longClick;
|
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
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.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 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.rules.ActivityScenarioRule;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.filters.LargeTest;
|
import androidx.test.filters.LargeTest;
|
||||||
import androidx.test.rule.GrantPermissionRule;
|
import androidx.test.rule.GrantPermissionRule;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class EspressoDeleteTrackTest {
|
public class EspressoDeleteTrackTest {
|
||||||
|
|
||||||
|
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||||
|
|
||||||
@Test
|
@Before
|
||||||
public void espressoDeleteTrackTest() {
|
public void setUp() {
|
||||||
// TrackListActivity: start recording
|
resetTrackRecordingServiceAndDeleteTracks();
|
||||||
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()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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,34 +2,43 @@ package de.dennisguse.opentracks;
|
|||||||
|
|
||||||
|
|
||||||
import static androidx.test.espresso.Espresso.onView;
|
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.closeSoftKeyboard;
|
||||||
import static androidx.test.espresso.action.ViewActions.longClick;
|
|
||||||
import static androidx.test.espresso.action.ViewActions.replaceText;
|
import static androidx.test.espresso.action.ViewActions.replaceText;
|
||||||
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
import static androidx.test.espresso.action.ViewActions.scrollTo;
|
||||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
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.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.withId;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
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 org.hamcrest.Matchers.allOf;
|
||||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.filters.LargeTest;
|
import androidx.test.filters.LargeTest;
|
||||||
import androidx.test.rule.GrantPermissionRule;
|
import androidx.test.rule.GrantPermissionRule;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class EspressoEditTrackRecordingTest {
|
public class EspressoEditTrackRecordingTest {
|
||||||
|
|
||||||
|
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
|
public final LocaleRule mLocaleRule = new LocaleRule(Locale.ENGLISH);
|
||||||
|
|
||||||
@@ -39,27 +48,27 @@ public class EspressoEditTrackRecordingTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
resetTrackRecordingServiceAndDeleteTracks();
|
||||||
|
}
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@Test
|
@Test
|
||||||
public void espressoEditTrackRecordingTest() {
|
public void espressoEditTrackRecordingTest() {
|
||||||
{
|
startRecordingFromTrackList(UI_TIMEOUT);
|
||||||
// TrackListActivity: start recording
|
|
||||||
onView(withId(R.id.track_list_fab_action))
|
|
||||||
.perform(click());
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
// TrackRecordingActivity
|
// TrackRecordingActivity
|
||||||
|
|
||||||
// wait; stay recording
|
|
||||||
onView(withId(R.id.track_recording_fab_action))
|
|
||||||
.perform(waitFor(4000));
|
|
||||||
|
|
||||||
// open menu
|
// open menu
|
||||||
openContextualActionModeOverflowMenu();
|
onView(allOf(
|
||||||
|
withContentDescription(androidx.appcompat.R.string.abc_action_menu_overflow_description),
|
||||||
// Click the item.
|
isDescendantOfA(withId(R.id.bottom_app_bar))))
|
||||||
|
.perform(performClick());
|
||||||
onView(withText(R.string.menu_edit))
|
onView(withText(R.string.menu_edit))
|
||||||
.perform(click());
|
.inRoot(isPlatformPopup())
|
||||||
|
.perform(performClickOnAncestor());
|
||||||
|
waitUntilDisplayed(withId(R.id.track_edit_name), UI_TIMEOUT);
|
||||||
|
|
||||||
// change name for "New Name"
|
// change name for "New Name"
|
||||||
onView(withId(R.id.track_edit_name))
|
onView(withId(R.id.track_edit_name))
|
||||||
@@ -70,11 +79,11 @@ public class EspressoEditTrackRecordingTest {
|
|||||||
|
|
||||||
// save edition
|
// save edition
|
||||||
onView(withId(R.id.track_edit_save))
|
onView(withId(R.id.track_edit_save))
|
||||||
.perform(click());
|
.perform(performClick());
|
||||||
|
waitUntilDisplayed(withId(R.id.track_recording_fab_action), UI_TIMEOUT);
|
||||||
|
|
||||||
// stop;
|
// stop;
|
||||||
onView(withId(R.id.track_recording_fab_action))
|
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button, R.id.finish_button);
|
||||||
.perform(longClick());
|
|
||||||
|
|
||||||
// it's on track stopped activity and there are two buttons
|
// it's on track stopped activity and there are two buttons
|
||||||
onView(withId(R.id.resume_button))
|
onView(withId(R.id.resume_button))
|
||||||
|
|||||||
@@ -1,100 +1,142 @@
|
|||||||
package de.dennisguse.opentracks;
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
import static androidx.test.espresso.Espresso.onView;
|
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.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.isDisplayed;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
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 androidx.test.espresso.matcher.ViewMatchers.withParentIndex;
|
import static de.dennisguse.opentracks.util.EspressoUtils.performLongClickOnAncestor;
|
||||||
import static org.hamcrest.Matchers.allOf;
|
|
||||||
import static de.dennisguse.opentracks.util.EspressoUtils.selectTabAtIndex;
|
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.espresso.ViewInteraction;
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.filters.LargeTest;
|
import androidx.test.filters.LargeTest;
|
||||||
import androidx.test.rule.GrantPermissionRule;
|
import androidx.test.rule.GrantPermissionRule;
|
||||||
|
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.RuleChain;
|
||||||
import org.junit.runner.RunWith;
|
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
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class EspressoUITest {
|
public class EspressoUITest {
|
||||||
|
|
||||||
@Rule
|
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
|
||||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
|
||||||
|
private final ActivityScenarioRule<TrackListActivity> activityRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||||
|
private final GrantPermissionRule grantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
public RuleChain rules = RuleChain.outerRule(grantPermissionRule)
|
||||||
|
.around(activityRule);
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
resetTrackRecordingServiceAndDeleteTracks();
|
||||||
|
}
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@Test
|
@Test
|
||||||
public void record_stop_resume_stop_finish() {
|
public void record_stop_resume_stop_finish() {
|
||||||
|
startRecordingFromTrackList(UI_TIMEOUT);
|
||||||
{
|
{
|
||||||
// TrackListActivity: start recording
|
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button);
|
||||||
onView(withId(R.id.track_list_fab_action))
|
resumeStoppedTrack(UI_TIMEOUT);
|
||||||
.perform(click());
|
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.finish_button);
|
||||||
}
|
finishStoppedTrack(UI_TIMEOUT);
|
||||||
{
|
|
||||||
// TrackRecordingActivity: wait to record some time and then stop
|
|
||||||
onView(withId(R.id.track_recording_fab_action))
|
|
||||||
.perform(waitFor(5000))
|
|
||||||
.perform(longClick());
|
|
||||||
|
|
||||||
// TrackStoppedActivity: resume
|
onView(withId(R.id.track_list))
|
||||||
onView(allOf(withId(R.id.resume_button), isClickable()))
|
.check(matches(isDisplayed()));
|
||||||
.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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@Test
|
@Test
|
||||||
public void record_move_through_tabs() {
|
public void record_move_through_tabs() {
|
||||||
{
|
startRecordingFromTrackList(UI_TIMEOUT);
|
||||||
// TrackListActivity: start recording
|
waitUntilDisplayed(withId(R.id.track_detail_activity_tablayout), UI_TIMEOUT);
|
||||||
onView(withId(R.id.track_list_fab_action))
|
waitUntilDisplayed(withId(R.id.track_detail_activity_view_pager), UI_TIMEOUT);
|
||||||
.perform(click());
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
// TrackRecordingActivity
|
// TrackRecordingActivity
|
||||||
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
|
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
|
||||||
|
|
||||||
tabLayout.perform(selectTabAtIndex(1));
|
tabLayout.perform(selectTabAtIndex(1));
|
||||||
tabLayout.perform(waitFor(1000));
|
waitUntilTabSelectionSettlesAt(1);
|
||||||
|
|
||||||
tabLayout.perform(selectTabAtIndex(2));
|
tabLayout.perform(selectTabAtIndex(2));
|
||||||
tabLayout.perform(waitFor(1000));
|
waitUntilTabSelectionSettlesAt(2);
|
||||||
|
|
||||||
tabLayout.perform(selectTabAtIndex(3));
|
tabLayout.perform(selectTabAtIndex(3));
|
||||||
tabLayout.perform(waitFor(1000));
|
waitUntilTabSelectionSettlesAt(3);
|
||||||
|
|
||||||
tabLayout.perform(selectTabAtIndex(0));
|
tabLayout.perform(selectTabAtIndex(0));
|
||||||
tabLayout.perform(waitFor(1000));
|
waitUntilTabSelectionSettlesAt(0);
|
||||||
|
|
||||||
// stop
|
// stop
|
||||||
onView(withId(R.id.track_recording_fab_action)).
|
stopRecordingToTrackStopped(UI_TIMEOUT, R.id.resume_button, R.id.finish_button);
|
||||||
perform(longClick());
|
|
||||||
|
onView(withId(R.id.resume_button))
|
||||||
|
.check(matches(isDisplayed()));
|
||||||
|
onView(withId(R.id.finish_button))
|
||||||
|
.check(matches(isDisplayed()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@Test
|
@Test
|
||||||
public void selectAndDeleteTrack() {
|
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(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.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.database.MatrixCursor;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
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.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -78,7 +75,6 @@ import de.dennisguse.opentracks.util.FileUtils;
|
|||||||
* @author Bartlomiej Niechwiej
|
* @author Bartlomiej Niechwiej
|
||||||
* @author Youtao Liu
|
* @author Youtao Liu
|
||||||
*/
|
*/
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
|
||||||
public class CustomContentProviderUtilsTest {
|
public class CustomContentProviderUtilsTest {
|
||||||
|
|
||||||
private static final String NAME_PREFIX = "test name";
|
private static final String NAME_PREFIX = "test name";
|
||||||
@@ -90,12 +86,6 @@ public class CustomContentProviderUtilsTest {
|
|||||||
private final Context context = ApplicationProvider.getApplicationContext();
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
private ContentProviderUtils contentProviderUtils;
|
private ContentProviderUtils contentProviderUtils;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private Cursor cursorMock;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ContentResolver contentResolverMock;
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
contentProviderUtils = new ContentProviderUtils(context);
|
contentProviderUtils = new ContentProviderUtils(context);
|
||||||
@@ -104,21 +94,21 @@ public class CustomContentProviderUtilsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLocationIterator_noPoints() {
|
public void testLocationIterator_noPoints() {
|
||||||
testIterator(new Track.Id(1), 0);
|
assertEquals(0, testIterator(new Track.Id(1), 0).size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLocationIterator_noAscending() {
|
public void testLocationIterator_noAscending() {
|
||||||
testIterator(new Track.Id(1), 50);
|
assertEquals(50, testIterator(new Track.Id(1), 50).size());
|
||||||
testIterator(new Track.Id(2), 50);
|
assertEquals(50, testIterator(new Track.Id(2), 50).size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLocationIterator_largeTrack() {
|
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);
|
TrackPoint.Id lastPointId = initializeTrack(trackId, numPoints);
|
||||||
List<TrackPoint> locations = new ArrayList<>(numPoints);
|
List<TrackPoint> locations = new ArrayList<>(numPoints);
|
||||||
try (TrackPointIterator it = contentProviderUtils.getTrackPointIterator(trackId, null)) {
|
try (TrackPointIterator it = contentProviderUtils.getTrackPointIterator(trackId, null)) {
|
||||||
@@ -131,6 +121,7 @@ public class CustomContentProviderUtilsTest {
|
|||||||
}
|
}
|
||||||
assertEquals(numPoints, locations.size());
|
assertEquals(numPoints, locations.size());
|
||||||
}
|
}
|
||||||
|
return locations;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TrackPoint.Id initializeTrack(Track.Id id, int numPoints) {
|
private TrackPoint.Id initializeTrack(Track.Id id, int numPoints) {
|
||||||
@@ -187,27 +178,49 @@ public class CustomContentProviderUtilsTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testCreateTrack() {
|
public void testCreateTrack() {
|
||||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
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();
|
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(trackId, track.id());
|
||||||
assertEquals(name, track.name());
|
assertEquals(name, track.name());
|
||||||
}
|
}
|
||||||
@@ -449,8 +462,6 @@ public class CustomContentProviderUtilsTest {
|
|||||||
marker.setDescription(TEST_DESC);
|
marker.setDescription(TEST_DESC);
|
||||||
contentProviderUtils.insertMarker(marker.getMarker());
|
contentProviderUtils.insertMarker(marker.getMarker());
|
||||||
|
|
||||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
|
|
||||||
|
|
||||||
Marker.Id markerId = new Marker.Id(System.currentTimeMillis());
|
Marker.Id markerId = new Marker.Id(System.currentTimeMillis());
|
||||||
marker.setId(markerId);
|
marker.setId(markerId);
|
||||||
ContentValues contentValues = contentProviderUtils.createContentValues(marker.getMarker(), trackId);
|
ContentValues contentValues = contentProviderUtils.createContentValues(marker.getMarker(), trackId);
|
||||||
@@ -1184,7 +1195,7 @@ public class CustomContentProviderUtilsTest {
|
|||||||
assertEquals(sensorStatistics.avgPower().getW(), stats.avgPower, 0f);
|
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
|
// given
|
||||||
Instant start = Instant.now();
|
Instant start = Instant.now();
|
||||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||||
@@ -1207,22 +1218,27 @@ public class CustomContentProviderUtilsTest {
|
|||||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||||
|
|
||||||
// then
|
return Pair.create(sensorStatistics, stats);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetSensorStats_veryLongActivity12h() {
|
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
|
@Test
|
||||||
public void testGetSensorStats_withSeveralRandomStartSegments() {
|
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) {
|
private TrackPoint getLastValidTrackPoint(Track.Id trackId) {
|
||||||
|
|||||||
+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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -1,6 +1,7 @@
|
|||||||
package de.dennisguse.opentracks.sensors.driver;
|
package de.dennisguse.opentracks.sensors.driver;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
import android.bluetooth.BluetoothGattCharacteristic;
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
|
||||||
@@ -35,4 +36,27 @@ public class CyclingPowerBluetoothTest {
|
|||||||
assertEquals(17125, powerCadence.crank().crankRevolutionsTime());
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+51
-34
@@ -1,24 +1,23 @@
|
|||||||
package de.dennisguse.opentracks.services;
|
package de.dennisguse.opentracks.services;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
|
||||||
|
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||||
import de.dennisguse.opentracks.data.models.Altitude;
|
import de.dennisguse.opentracks.data.models.Altitude;
|
||||||
import de.dennisguse.opentracks.data.models.Distance;
|
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.data.models.TrackPoint;
|
||||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||||
|
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
|
||||||
public class TrackRecordingServiceNotificationManagerTest {
|
public class TrackRecordingServiceNotificationManagerTest {
|
||||||
|
|
||||||
|
private static final String CHANNEL_ID = "TrackRecordingServiceNotificationManagerTest";
|
||||||
|
|
||||||
private final Context context = ApplicationProvider.getApplicationContext();
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
private final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||||
|
|
||||||
@Mock
|
@After
|
||||||
private NotificationCompat.Builder notificationCompatBuilder;
|
public void tearDown() {
|
||||||
|
notificationManager.cancelAll();
|
||||||
@Mock
|
}
|
||||||
private NotificationManager notificationManager;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
|
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
|
||||||
TrackPoint trackPointMock = new TrackPoint(TrackPoint.Type.TRACKPOINT,
|
notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_DEFAULT));
|
||||||
new Position(
|
|
||||||
null,
|
|
||||||
null, null, Distance.of(999f),
|
|
||||||
Altitude.WGS84.of(10), null,
|
|
||||||
null,
|
|
||||||
Speed.ZERO)
|
|
||||||
);
|
|
||||||
|
|
||||||
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean()))
|
TrackingNotificationBuilder notificationBuilder = new TrackingNotificationBuilder(context, CHANNEL_ID);
|
||||||
.thenReturn(notificationCompatBuilder);
|
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationBuilder);
|
||||||
|
|
||||||
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
|
|
||||||
subject.setUnitSystem(UnitSystem.METRIC);
|
subject.setUnitSystem(UnitSystem.METRIC);
|
||||||
|
|
||||||
Track track = new Track(
|
Track track = new Track(
|
||||||
@@ -68,16 +59,42 @@ public class TrackRecordingServiceNotificationManagerTest {
|
|||||||
Statistics.DEFAULT
|
Statistics.DEFAULT
|
||||||
);
|
);
|
||||||
|
|
||||||
RecordingData data = new RecordingData(track, trackPointMock, null, null);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// when
|
private RecordingData createRecordingData(Track track, Distance horizontalAccuracy) {
|
||||||
subject.updateTrackPoint(context, data, Distance.of(100));
|
TrackPoint trackPoint = new TrackPoint(
|
||||||
subject.updateTrackPoint(context, data, Distance.of(100));
|
TrackPoint.Type.TRACKPOINT,
|
||||||
subject.updateTrackPoint(context, data, Distance.of(1000));
|
new Position(
|
||||||
subject.updateTrackPoint(context, data, Distance.of(100));
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
horizontalAccuracy,
|
||||||
|
Altitude.WGS84.of(10),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Speed.ZERO
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return new RecordingData(track, trackPoint, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
// then
|
private static final class TrackingNotificationBuilder extends NotificationCompat.Builder {
|
||||||
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
|
private final List<Boolean> onlyAlertOnceValues = new ArrayList<>();
|
||||||
verify(notificationCompatBuilder, times(2)).setOnlyAlertOnce(false);
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+38
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package de.dennisguse.opentracks.services;
|
package de.dennisguse.opentracks.services;
|
||||||
|
|
||||||
|
import static de.dennisguse.opentracks.util.LiveDataTestUtils.waitForValue;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNotEquals;
|
||||||
@@ -41,6 +42,7 @@ import org.junit.Rule;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
@@ -56,6 +58,8 @@ import de.dennisguse.opentracks.sensors.GpsStatusValue;
|
|||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class TrackRecordingServiceStateMachineTest {
|
public class TrackRecordingServiceStateMachineTest {
|
||||||
|
|
||||||
|
private static final Duration WAIT_TIMEOUT = Duration.ofSeconds(2);
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
||||||
|
|
||||||
@@ -123,6 +127,7 @@ public class TrackRecordingServiceStateMachineTest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
service.tryStartSensors();
|
service.tryStartSensors();
|
||||||
|
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
||||||
@@ -131,6 +136,7 @@ public class TrackRecordingServiceStateMachineTest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
service.stopSensors();
|
service.stopSensors();
|
||||||
|
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
assertEquals(TrackRecordingService.STATUS_DEFAULT, service.getRecordingStatusObservable().getValue());
|
||||||
@@ -146,6 +152,9 @@ public class TrackRecordingServiceStateMachineTest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
Track.Id trackId = service.startNewTrack();
|
Track.Id trackId = service.startNewTrack();
|
||||||
|
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
|
||||||
|
waitUntilRecordingDataIsNotDefault();
|
||||||
|
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertTrue(service.isRecording());
|
assertTrue(service.isRecording());
|
||||||
@@ -154,9 +163,13 @@ public class TrackRecordingServiceStateMachineTest {
|
|||||||
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
|
assertEquals(GpsStatusValue.GPS_ENABLED, service.getGpsStatusObservable().getValue());
|
||||||
|
|
||||||
service.endCurrentTrack();
|
service.endCurrentTrack();
|
||||||
|
waitUntilRecordingStatusIs(TrackRecordingService.STATUS_DEFAULT);
|
||||||
|
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
service.resumeTrack(trackId);
|
service.resumeTrack(trackId);
|
||||||
|
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
|
||||||
|
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertTrue(service.isRecording());
|
assertTrue(service.isRecording());
|
||||||
@@ -167,6 +180,8 @@ public class TrackRecordingServiceStateMachineTest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
service.endCurrentTrack();
|
service.endCurrentTrack();
|
||||||
|
waitUntilRecordingStatusIs(TrackRecordingService.STATUS_DEFAULT);
|
||||||
|
waitUntilGpsStatusIs(GpsStatusValue.GPS_NONE);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertFalse(service.isRecording());
|
assertFalse(service.isRecording());
|
||||||
@@ -177,6 +192,8 @@ public class TrackRecordingServiceStateMachineTest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
service.resumeTrack(trackId);
|
service.resumeTrack(trackId);
|
||||||
|
waitUntilRecordingStatusIs(new RecordingStatus(trackId));
|
||||||
|
waitUntilGpsStatusIs(GpsStatusValue.GPS_ENABLED);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertTrue(service.isRecording());
|
assertTrue(service.isRecording());
|
||||||
@@ -254,4 +271,25 @@ public class TrackRecordingServiceStateMachineTest {
|
|||||||
assertNotNull(trackId);
|
assertNotNull(trackId);
|
||||||
assertNull(newTrackId);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-20
@@ -2,12 +2,12 @@ package de.dennisguse.opentracks.services;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static de.dennisguse.opentracks.services.TrackRecordingService.RECORDING_DATA_UPDATE_INTERVAL;
|
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.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
import androidx.core.location.LocationManagerCompat;
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.rule.GrantPermissionRule;
|
import androidx.test.rule.GrantPermissionRule;
|
||||||
@@ -30,6 +30,7 @@ import de.dennisguse.opentracks.data.models.Speed;
|
|||||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||||
import de.dennisguse.opentracks.io.file.importer.ExportImportTest;
|
import de.dennisguse.opentracks.io.file.importer.ExportImportTest;
|
||||||
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
|
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.sensors.driver.GpsInternal;
|
||||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ public class TrackRecordingServiceUITest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getDataForUI_gps() throws TimeoutException, InterruptedException {
|
public void getDataForUI_gps() throws TimeoutException {
|
||||||
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
|
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
|
||||||
.getService();
|
.getService();
|
||||||
RecordingData recordingData;
|
RecordingData recordingData;
|
||||||
@@ -63,34 +64,23 @@ public class TrackRecordingServiceUITest {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
service.startNewTrack();
|
service.startNewTrack();
|
||||||
//We do not want the real GPS to interfere.
|
disconnectRealGps(trackPointCreator.getSensorManager().getGpsHandler());
|
||||||
GpsInternal gpsInternal = (GpsInternal)trackPointCreator.getSensorManager().getGpsHandler().driver;
|
trackPointCreator.getSensorManager().reset();
|
||||||
LocationManagerCompat.removeUpdates(gpsInternal.locationManager, gpsInternal.locationListenerCompat);
|
RecordingData initialRecordingData = waitUntilLatestTrackPointMatches(service, Instant.parse("2020-02-02T02:02:02Z"), false);
|
||||||
Thread.sleep(RECORDING_DATA_UPDATE_INTERVAL.toMillis());
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(new TrackPoint(
|
TrackPoint initialTrackPoint = initialRecordingData.latestTrackPoint();
|
||||||
null,
|
assertEquals(TrackPoint.Type.TRACKPOINT, initialTrackPoint.type());
|
||||||
TrackPoint.Type.TRACKPOINT,
|
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), initialTrackPoint.getTime());
|
||||||
Position.of( Instant.parse("2020-02-02T02:02:02Z")),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
),
|
|
||||||
service.getRecordingDataObservable().getValue().latestTrackPoint());
|
|
||||||
|
|
||||||
|
|
||||||
// when
|
// when
|
||||||
// In this step, the data is saved (and Aggregator.resetAggregated called), but data should still be shown in the UI.
|
// 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");
|
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);
|
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
|
// then
|
||||||
Thread.sleep(RECORDING_DATA_UPDATE_INTERVAL.toMillis());
|
|
||||||
recordingData = service.getRecordingDataObservable().getValue();
|
|
||||||
new TrackPointAssert().assertEquals(new TrackPoint(
|
new TrackPointAssert().assertEquals(new TrackPoint(
|
||||||
null,
|
null,
|
||||||
TrackPoint.Type.TRACKPOINT,
|
TrackPoint.Type.TRACKPOINT,
|
||||||
@@ -109,4 +99,23 @@ public class TrackRecordingServiceUITest {
|
|||||||
),
|
),
|
||||||
recordingData.latestTrackPoint());
|
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);
|
PreferencesUtils.setVoiceAnnounceLapHeartRate(true);
|
||||||
|
|
||||||
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
|
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);
|
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();
|
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
|
||||||
|
|
||||||
// then
|
// 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
|
@Test
|
||||||
@@ -589,6 +590,7 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
PreferencesUtils.setVoiceAnnounceLapSpeedPace(false);
|
PreferencesUtils.setVoiceAnnounceLapSpeedPace(false);
|
||||||
|
|
||||||
Pair<Track, Statistics> data = buildTrackWithTrackPoints();
|
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);
|
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();
|
String announcement = VoiceAnnouncementUtils.createStatistics(context, data.first, dataSet, UnitSystem.METRIC, true, data.second, sensorStatistics).toString();
|
||||||
|
|
||||||
// then
|
// 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
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
-22
@@ -1,36 +1,41 @@
|
|||||||
package de.dennisguse.opentracks.ui.aggregatedStatistics;
|
package de.dennisguse.opentracks.ui.aggregatedStatistics;
|
||||||
|
|
||||||
import static androidx.test.espresso.Espresso.onView;
|
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.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.hasDescendant;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
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 org.hamcrest.Matchers.allOf;
|
||||||
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
|
|
||||||
|
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ActivityScenario;
|
||||||
import androidx.test.espresso.matcher.ViewMatchers;
|
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.filters.LargeTest;
|
import androidx.test.filters.LargeTest;
|
||||||
import androidx.test.rule.GrantPermissionRule;
|
import androidx.test.rule.GrantPermissionRule;
|
||||||
|
|
||||||
import org.hamcrest.core.IsInstanceOf;
|
import org.hamcrest.core.IsInstanceOf;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.TestUtil;
|
import de.dennisguse.opentracks.TestUtil;
|
||||||
import de.dennisguse.opentracks.TrackListActivity;
|
|
||||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
@@ -41,53 +46,69 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
|
|||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class EspressoAggregatedFilterTest {
|
public class EspressoAggregatedFilterTest {
|
||||||
|
|
||||||
private final String ACTIVITY_TYPE_LOCALIZED = "activityTypeLocalized";
|
private static final Duration UI_TIMEOUT = Duration.ofSeconds(10);
|
||||||
|
|
||||||
@Rule
|
private final String ACTIVITY_TYPE_LOCALIZED = "activityTypeLocalized";
|
||||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
public GrantPermissionRule mGrantPermissionRule = TestUtil.createGrantPermissionRule();
|
||||||
|
|
||||||
|
private ActivityScenario<AggregatedStatisticsActivity> scenario;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
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);
|
Pair<Track, List<TrackPoint>> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 20);
|
||||||
TrackBuilder trackBuilder = new TrackBuilder(pair.first);
|
TrackBuilder trackBuilder = new TrackBuilder(pair.first);
|
||||||
trackBuilder.setActivityTypeLocalized(ACTIVITY_TYPE_LOCALIZED);
|
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
|
@Test
|
||||||
public void espressoAggregatedFilterTest() {
|
public void espressoAggregatedFilterTest() {
|
||||||
// open AggregatedStatisticsActivity through toolbar's menu item
|
waitUntilDisplayed(withId(R.id.bottom_app_bar), UI_TIMEOUT);
|
||||||
onView(ViewMatchers.withId(R.id.bottom_app_bar))
|
waitUntilDisplayed(withId(R.id.aggregated_statistics_filter), UI_TIMEOUT);
|
||||||
.check(matches(isDisplayed()));
|
waitUntilRecyclerViewItemCount(withId(R.id.aggregated_stats_list), 1, UI_TIMEOUT);
|
||||||
onView(withId(R.id.bottom_app_bar))
|
waitUntilDisplayed(allOf(withId(R.id.aggregated_stats_type_label), withText(ACTIVITY_TYPE_LOCALIZED)), UI_TIMEOUT);
|
||||||
.check(matches(hasDescendant(withId(R.id.aggregated_stats_button))));
|
|
||||||
onView(withId(R.id.aggregated_stats_button))
|
|
||||||
.perform(click());
|
|
||||||
|
|
||||||
// open FilterDialogFragment through toolbar's menu item
|
// open FilterDialogFragment through the current bottom app bar action item
|
||||||
onView(withId(R.id.bottom_app_bar))
|
onView(withId(R.id.bottom_app_bar))
|
||||||
.check(matches(isDisplayed()));
|
.check(matches(isDisplayed()));
|
||||||
onView(withId(R.id.bottom_app_bar))
|
onView(withId(R.id.bottom_app_bar))
|
||||||
.check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
|
.check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
|
||||||
onView(withId(R.id.aggregated_statistics_filter))
|
onView(withId(R.id.aggregated_statistics_filter))
|
||||||
.perform(click());
|
.perform(performClick());
|
||||||
|
|
||||||
// check there's a checkbox with ACTIVITY_TYPE_LOCALIZED text
|
// check there's a checkbox with ACTIVITY_TYPE_LOCALIZED text
|
||||||
onView(allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
|
Matcher<View> activityTypeFilter = allOf(withId(R.id.filter_dialog_check_button), withText(ACTIVITY_TYPE_LOCALIZED),
|
||||||
withParent(allOf(withId(R.id.filter_items),
|
withParent(allOf(withId(R.id.filter_items),
|
||||||
withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))),
|
withParent(IsInstanceOf.instanceOf(android.view.ViewGroup.class)))),
|
||||||
isDisplayed()))
|
isDisplayed());
|
||||||
.perform(waitFor(2000));
|
onView(activityTypeFilter)
|
||||||
|
.inRoot(isDialog())
|
||||||
|
.check(matches(isDisplayed()));
|
||||||
|
|
||||||
// check there's an edit text for "from date"
|
// check there's an edit text for "from date"
|
||||||
onView(withId(R.id.filter_date_edit_text_from))
|
onView(withId(R.id.filter_date_edit_text_from))
|
||||||
|
.inRoot(isDialog())
|
||||||
.check(matches(isDisplayed()));
|
.check(matches(isDisplayed()));
|
||||||
|
|
||||||
// check there's an edit text for "to date"
|
// check there's an edit text for "to date"
|
||||||
onView(withId(R.id.filter_date_edit_text_to))
|
onView(withId(R.id.filter_date_edit_text_to))
|
||||||
|
.inRoot(isDialog())
|
||||||
.check(matches(isDisplayed()));
|
.check(matches(isDisplayed()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-21
@@ -35,7 +35,10 @@ public class IntervalStatisticsUpdaterTest {
|
|||||||
float distanceInterval = 1000f;
|
float distanceInterval = 1000f;
|
||||||
|
|
||||||
// when and then
|
// 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;
|
float distanceInterval = 1000f;
|
||||||
|
|
||||||
// when and then
|
// 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;
|
float distanceInterval = 3000f;
|
||||||
|
|
||||||
// when and then
|
// 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;
|
float distanceInterval = 3000f;
|
||||||
|
|
||||||
// when and then
|
// 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;
|
float distanceInterval = 1000f;
|
||||||
|
|
||||||
// when and then
|
// 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
|
@Test
|
||||||
@@ -122,15 +137,18 @@ public class IntervalStatisticsUpdaterTest {
|
|||||||
Pair<Track, List<TrackPoint>> trackWithStats = new Pair<>(dummyTrack, trackPoints);
|
Pair<Track, List<TrackPoint>> trackWithStats = new Pair<>(dummyTrack, trackPoints);
|
||||||
|
|
||||||
// when and then
|
// 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);
|
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));
|
IntervalStatisticsUpdater intervalStatistics = new IntervalStatisticsUpdater(Distance.of(distanceInterval));
|
||||||
|
|
||||||
intervalStatistics.addTrackPoints(trackPoints.iterator());
|
intervalStatistics.addTrackPoints(trackPoints.iterator());
|
||||||
@@ -157,23 +175,27 @@ public class IntervalStatisticsUpdaterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// then
|
return new IntervalStatisticsComputation(track, intervalList, totalDistance, totalTime, totalGain, totalLoss);
|
||||||
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));
|
private void assertIntervalStatisticsComputation(IntervalStatisticsComputation computation, int numberOfPoints, float distanceInterval) {
|
||||||
if (totalGain != null && totalLoss != null) {
|
assertEquals(computation.track.statistics().totalDuration().toSeconds(), computation.totalTime, 0.01);
|
||||||
assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1);
|
assertEquals(computation.track.statistics().totalDistance().toM(), computation.totalDistance.toM(), 0.01);
|
||||||
assertEquals(totalLoss, numberOfPoints * TestDataUtil.ALTITUDE_LOSS, 0.1);
|
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 {
|
} 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++) {
|
Distance remainingDistance = computation.totalDistance;
|
||||||
assertEquals(intervalList.get(i).totalDistance().toM(), distanceInterval, 0.001);
|
for (int i = 0; i < computation.intervalList.size() - 1; i++) {
|
||||||
totalDistance = totalDistance.minus(intervalList.get(i).totalDistance());
|
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) {
|
private static Pair<Track, List<TrackPoint>> buildTrackWithTrackPoints(int numberOfPoints) {
|
||||||
@@ -195,4 +217,27 @@ public class IntervalStatisticsUpdaterTest {
|
|||||||
|
|
||||||
return new Pair<>(track, trackPoints);
|
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;
|
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.isDisplayed;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.ViewParent;
|
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.UiController;
|
||||||
import androidx.test.espresso.ViewAction;
|
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;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
|
|
||||||
@@ -16,61 +27,114 @@ import org.hamcrest.Description;
|
|||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
import org.hamcrest.TypeSafeMatcher;
|
import org.hamcrest.TypeSafeMatcher;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
public class EspressoUtils {
|
public class EspressoUtils {
|
||||||
|
|
||||||
public static Matcher<View> childAtPosition(
|
private static final long VIEW_POLLING_INTERVAL_MS = 50;
|
||||||
final Matcher<View> parentMatcher, final int position) {
|
|
||||||
|
|
||||||
|
public static Matcher<View> withRecyclerViewItemCount(final int count) {
|
||||||
return new TypeSafeMatcher<>() {
|
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
|
@Override
|
||||||
public void describeTo(Description description) {
|
public void describeTo(Description description) {
|
||||||
description.appendText("Child at position " + position + " in parent ");
|
description.appendText("RecyclerView should have " + count + " items");
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Matcher<View> withListSize (final int size) {
|
public static Matcher<View> withSelectedTab(final int index) {
|
||||||
return new TypeSafeMatcher<>() {
|
return new TypeSafeMatcher<>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean matchesSafely(final View view) {
|
protected boolean matchesSafely(View view) {
|
||||||
return ((ListView) view).getCount() == size;
|
return view instanceof TabLayout tabLayout
|
||||||
|
&& tabLayout.getSelectedTabPosition() == index;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void describeTo(final Description description) {
|
public void describeTo(Description description) {
|
||||||
description.appendText("ListView should have " + size + " items");
|
description.appendText("TabLayout should have selected tab " + index);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ViewAction waitFor(final long duration_ms) {
|
public static Matcher<View> withViewPagerCurrentItem(final int index) {
|
||||||
return new ViewAction() {
|
return new TypeSafeMatcher<>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDescription() {
|
protected boolean matchesSafely(View view) {
|
||||||
return "Wait for milliseconds.";
|
return view instanceof ViewPager2 viewPager2
|
||||||
|
&& viewPager2.getCurrentItem() == index
|
||||||
|
&& viewPager2.getScrollState() == ViewPager2.SCROLL_STATE_IDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Matcher<View> getConstraints() {
|
public void describeTo(Description description) {
|
||||||
return isDisplayed();
|
description.appendText("ViewPager2 should be idle on item " + index);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void perform(UiController uiController, final View view) {
|
|
||||||
uiController.loopMainThreadForAtLeast(duration_ms);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
public static ViewAction selectTabAtIndex(final int index) {
|
||||||
return new ViewAction() {
|
return new ViewAction() {
|
||||||
@Override
|
@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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">OpenTracks (Debug)</string>
|
<string name="launcher_name" translatable="false">OpenTracks (Debug)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ limitations under the License.
|
|||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:hasFragileUserData="false"
|
android:hasFragileUserData="false"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/launcher_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/DayNightDynamicTheme"> <!-- Default for AppCompatActivities -->
|
android:theme="@style/DayNightDynamicTheme"> <!-- Default for AppCompatActivities -->
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
getContext().getContentResolver().notifyChange(url, null, false);
|
notifyChanges(url);
|
||||||
|
|
||||||
int totalChanges = getTotalChanges() - totalChangesBefore;
|
int totalChanges = getTotalChanges() - totalChangesBefore;
|
||||||
Log.i(TAG, "Deleted " + totalChanges + " total rows from database");
|
Log.i(TAG, "Deleted " + totalChanges + " total rows from database");
|
||||||
@@ -212,7 +212,8 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
getContext().getContentResolver().notifyChange(url, null, false);
|
notifyChanges(url);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +236,8 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
getContext().getContentResolver().notifyChange(url, null, false);
|
notifyChanges(url);
|
||||||
|
|
||||||
return numInserted;
|
return numInserted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,10 +350,36 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getContext().getContentResolver().notifyChange(url, null, false);
|
notifyChanges(url);
|
||||||
|
|
||||||
return count;
|
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
|
@NonNull
|
||||||
private UrlType getUrlType(Uri url) {
|
private UrlType getUrlType(Uri url) {
|
||||||
UrlType[] urlTypes = UrlType.values();
|
UrlType[] urlTypes = UrlType.values();
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
|||||||
cyclingDistanceSpeedHandler.connect();
|
cyclingDistanceSpeedHandler.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PreferencesUtils.isKey(cyclingCadenceHandler.getSensorPreferenceKey(), key)) {
|
if (PreferencesUtils.isKey(cyclingPowerHandler.getSensorPreferenceKey(), key)) {
|
||||||
cyclingPowerHandler.connect();
|
cyclingPowerHandler.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import android.util.Log;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||||
@@ -17,12 +19,14 @@ public class BarometerInternal implements Driver {
|
|||||||
|
|
||||||
private static final String TAG = BarometerInternal.class.getSimpleName();
|
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 DriverObserver<AtmosphericPressure> observer;
|
private final DriverObserver<AtmosphericPressure> observer;
|
||||||
|
|
||||||
private android.hardware.SensorManager sensorService;
|
private android.hardware.SensorManager sensorService;
|
||||||
|
|
||||||
|
private Instant lastEventTime = Instant.EPOCH;
|
||||||
|
|
||||||
private final SensorEventListener sensorEventListener = new SensorEventListener() {
|
private final SensorEventListener sensorEventListener = new SensorEventListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onSensorChanged(SensorEvent event) {
|
public void onSensorChanged(SensorEvent event) {
|
||||||
@@ -31,6 +35,13 @@ public class BarometerInternal implements Driver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Instant currentTime = Instant.now();
|
||||||
|
|
||||||
|
if (lastEventTime.plus(SAMPLING_PERIOD).isAfter(currentTime)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEventTime = currentTime;
|
||||||
observer.onDataReceived(AtmosphericPressure.ofHPA(event.values[0]));
|
observer.onDataReceived(AtmosphericPressure.ofHPA(event.values[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +59,9 @@ public class BarometerInternal implements Driver {
|
|||||||
public void connect(Context context, Handler handler, String addressIgnored) {
|
public void connect(Context context, Handler handler, String addressIgnored) {
|
||||||
sensorService = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
sensorService = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||||
Sensor pressureSensor = sensorService.getDefaultSensor(Sensor.TYPE_PRESSURE);
|
Sensor pressureSensor = sensorService.getDefaultSensor(Sensor.TYPE_PRESSURE);
|
||||||
|
int samplingPeriodUs = (int) TimeUnit.MICROSECONDS.convert(SAMPLING_PERIOD);
|
||||||
|
|
||||||
if (pressureSensor != null && sensorService.registerListener(sensorEventListener, pressureSensor, SAMPLING_PERIOD, handler)) {
|
if (pressureSensor != null && sensorService.registerListener(sensorEventListener, pressureSensor, samplingPeriodUs, handler)) {
|
||||||
observer.onConnected(null, null);
|
observer.onConnected(null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,14 +18,28 @@ public class CyclingPowerBluetooth implements BluetoothDriver.BluetoothParser<Cy
|
|||||||
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
|
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static final List<ServiceMeasurementUUID> SUPPORTED_SERVICES = List.of(
|
||||||
|
CYCLING_POWER,
|
||||||
|
BoschEbikeParser.BOSCH_EBIKE
|
||||||
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ServiceMeasurementUUID> getServices() {
|
public List<ServiceMeasurementUUID> getServices() {
|
||||||
return List.of(CYCLING_POWER);
|
return SUPPORTED_SERVICES;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CyclingPowerBluetooth.Data parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
public CyclingPowerBluetooth.Data parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
||||||
return parseCyclingPower(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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -4,9 +4,6 @@ import android.content.Context;
|
|||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
|
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
|
||||||
|
|
||||||
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
|
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
|
||||||
@@ -29,6 +26,8 @@ public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreferen
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PreferenceDialogFragmentCompat createInstance() {
|
public PreferenceDialogFragmentCompat createInstance() {
|
||||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(CyclingPowerBluetooth.CYCLING_POWER));
|
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(
|
||||||
|
getKey(),
|
||||||
|
CyclingPowerBluetooth.SUPPORTED_SERVICES);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+30
-21
@@ -191,26 +191,6 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
|||||||
List<ParcelUuid> serviceUUIDs = getArguments().getParcelableArrayList(ARG_BLE_SERVICE_UUIDS);
|
List<ParcelUuid> serviceUUIDs = getArguments().getParcelableArrayList(ARG_BLE_SERVICE_UUIDS);
|
||||||
boolean includeInternalSensor = getArguments().getBoolean(ARG_INCLUDE_INTERNAL);
|
boolean includeInternalSensor = getArguments().getBoolean(ARG_INCLUDE_INTERNAL);
|
||||||
|
|
||||||
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(getContext());
|
|
||||||
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
|
|
||||||
Log.w(TAG, "Bluetooth adapter is present or not enabled.");
|
|
||||||
Toast.makeText(getContext(), R.string.bluetooth_disabled, Toast.LENGTH_SHORT).show();
|
|
||||||
dismiss();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bluetoothAdapter.isDiscovering()) {
|
|
||||||
Log.i(TAG, "Cancelling ongoing Bluetooth discovery.");
|
|
||||||
bluetoothAdapter.cancelDiscovery();
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner = bluetoothAdapter.getBluetoothLeScanner();
|
|
||||||
if (scanner == null) {
|
|
||||||
Log.e(TAG, "BluetoothLeScanner is null.");
|
|
||||||
dismiss();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
listAdapter.add(SensorType.NONE.getPreferenceValue(), getContext().getString(DEVICE_NONE_RESOURCEID));
|
listAdapter.add(SensorType.NONE.getPreferenceValue(), getContext().getString(DEVICE_NONE_RESOURCEID));
|
||||||
selectedEntryIndex = 0;
|
selectedEntryIndex = 0;
|
||||||
|
|
||||||
@@ -228,8 +208,37 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
|||||||
selectedEntryIndex = !includeInternalSensor ? 1 : 2;
|
selectedEntryIndex = !includeInternalSensor ? 1 : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(getContext());
|
||||||
|
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
|
||||||
|
Log.w(TAG, "Bluetooth adapter is present or not enabled. Skipping BLE scan.");
|
||||||
|
Toast.makeText(getContext(), R.string.bluetooth_disabled, Toast.LENGTH_SHORT).show();
|
||||||
|
if (!includeInternalSensor) {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bluetoothAdapter.isDiscovering()) {
|
||||||
|
Log.i(TAG, "Cancelling ongoing Bluetooth discovery.");
|
||||||
|
bluetoothAdapter.cancelDiscovery();
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner = bluetoothAdapter.getBluetoothLeScanner();
|
||||||
|
if (scanner == null) {
|
||||||
|
Log.e(TAG, "BluetoothLeScanner is null.");
|
||||||
|
dismiss();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-populate with already-bonded BLE devices so that devices that are paired but not actively advertising (e.g. Bosch eBike) are still shown. In case we dont have any cached UUIDs for a device, we just add it to the list and let the user decide
|
||||||
|
boolean filterEnabled = PreferencesUtils.getBluetoothFilterEnabled();
|
||||||
|
bluetoothAdapter.getBondedDevices().stream()
|
||||||
|
.filter(bonded -> bonded.getType() == BluetoothDevice.DEVICE_TYPE_LE || bonded.getType() == BluetoothDevice.DEVICE_TYPE_DUAL)
|
||||||
|
.filter(bonded -> !filterEnabled || bonded.getUuids() == null || java.util.Arrays.stream(bonded.getUuids()).anyMatch(serviceUUIDs::contains))
|
||||||
|
.forEach(bonded -> listAdapter.add(bonded.getAddress(), bonded.getName()));
|
||||||
|
|
||||||
List<ScanFilter> scanFilter = null;
|
List<ScanFilter> scanFilter = null;
|
||||||
if (PreferencesUtils.getBluetoothFilterEnabled()) {
|
if (filterEnabled) {
|
||||||
scanFilter = serviceUUIDs.stream()
|
scanFilter = serviceUUIDs.stream()
|
||||||
.map(it -> new ScanFilter.Builder().setServiceUuid(it).build())
|
.map(it -> new ScanFilter.Builder().setServiceUuid(it).build())
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
@@ -654,4 +654,9 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
|
|||||||
<string name="settings_night_mode_option_day_dynamic">Tag (DynamicColor)</string>
|
<string name="settings_night_mode_option_day_dynamic">Tag (DynamicColor)</string>
|
||||||
<string name="settings_night_mode_option_night_dynamic">Nacht (DynamicColor)</string>
|
<string name="settings_night_mode_option_night_dynamic">Nacht (DynamicColor)</string>
|
||||||
<string name="settings_night_mode_option_night_oled_dynamic">OLED-freundlich (DynamicColor)</string>
|
<string name="settings_night_mode_option_night_oled_dynamic">OLED-freundlich (DynamicColor)</string>
|
||||||
|
<string name="stats_sensors_temperature">Temperatur</string>
|
||||||
|
<string name="sensor_state_temperature">Thermometer</string>
|
||||||
|
<string name="settings_announcements_segment_average_speed_pace">Durchschnittsgeschwindigkeit/pace (Segment)</string>
|
||||||
|
<string name="stats_average_segment_pace">Durchschnittliche Segment Geschwindigkeit</string>
|
||||||
|
<string name="stats_elapsed_segment_time">Verstrichene Segment Zeit</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_web_url" translatable="false">https://codeberg.org/OpenTracksApp/OpenTracks</string>
|
<string name="app_web_url" translatable="false">https://codeberg.org/OpenTracksApp/OpenTracks</string>
|
||||||
<string name="app_name" translatable="false">OpenTracks</string>
|
<string name="app_name" translatable="false">OpenTracks</string>
|
||||||
|
<string name="launcher_name" translatable="false">OpenTracks</string>
|
||||||
|
|
||||||
<string name="egm2008" translatable="false">EGM2008</string>
|
<string name="egm2008" translatable="false">EGM2008</string>
|
||||||
<string name="wgs84" translatable="false">WGS84</string>
|
<string name="wgs84" translatable="false">WGS84</string>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">OpenTracks (Nightly)</string>
|
<string name="launcher_name" translatable="false">OpenTracks (Nightly)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user