Skip to content

Commit 0c32781

Browse files
committed
Merge branch 'develop' into 2026.02
2 parents 7aa42fe + d315510 commit 0c32781

File tree

108 files changed

+4266
-2348
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

108 files changed

+4266
-2348
lines changed

app/build.gradle

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ android {
1515
multiDexEnabled true
1616

1717
// Version code should be increased after each release
18-
versionCode 70
18+
versionCode 73
1919
versionName new Date().format('yyyy.MM.dd')
2020

2121
testApplicationId "net.osmtracker.test"
@@ -61,6 +61,8 @@ android {
6161
}
6262
testOptions {
6363
unitTests.returnDefaultValues = true
64+
// This flag is required for Robolectric to find XML resources
65+
unitTests.includeAndroidResources = true
6466
unitTests.all {
6567
it.jvmArgs = [
6668
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
@@ -85,6 +87,12 @@ dependencies {
8587
exclude group: 'net.sf.kxml', module: 'kxml2'
8688
exclude group: 'xmlpull', module: 'xmlpull'
8789
}
90+
// For upload notes to osm server
91+
implementation ('de.westnordost:osmapi-notes:3.1'){
92+
// Already included in Android
93+
exclude group: 'net.sf.kxml', module: 'kxml2'
94+
exclude group: 'xmlpull', module: 'xmlpull'
95+
}
8896
// App intro
8997
implementation 'com.github.AppIntro:AppIntro:6.3.1'
9098

@@ -95,22 +103,24 @@ dependencies {
95103
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
96104
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
97105
implementation 'androidx.appcompat:appcompat:1.7.0'
106+
implementation 'androidx.preference:preference:1.2.0'
107+
implementation 'androidx.preference:preference-ktx:1.2.1'
98108

99109
// Required -- JUnit 4 framework
100110
testImplementation 'junit:junit:4.13.2'
101-
// Robolectric environment
111+
// Robolectric
112+
testImplementation 'org.robolectric:robolectric:4.11.1'
113+
// AndroidX Test core for Robolectric
102114
testImplementation "androidx.test:core:1.6.1"
103115
// Mockito framework
104116
testImplementation "org.mockito:mockito-core:3.12.4"
105117

106-
testImplementation 'org.powermock:powermock-core:2.0.9'
107-
testImplementation 'org.powermock:powermock-api-mockito2:2.0.9'
108-
testImplementation 'org.powermock:powermock-module-junit4:2.0.9'
109118
// Required for local unit tests. Prevent null in JSONObject, JSONArray, etc.
110119
testImplementation 'org.json:json:20240303'
111120

112121
// Required for instrumented tests
113122
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
123+
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.7.0'
114124
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
115125
androidTestImplementation 'androidx.test:rules:1.6.1'
116126
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package net.osmtracker.activity;
2+
3+
import static androidx.test.espresso.Espresso.onView;
4+
import static androidx.test.espresso.action.ViewActions.clearText;
5+
import static androidx.test.espresso.action.ViewActions.click;
6+
import static androidx.test.espresso.action.ViewActions.typeText;
7+
import static androidx.test.espresso.assertion.ViewAssertions.matches;
8+
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
9+
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
10+
import static androidx.test.espresso.matcher.ViewMatchers.withId;
11+
import static androidx.test.espresso.matcher.ViewMatchers.withText;
12+
import static org.hamcrest.Matchers.stringContainsInOrder;
13+
14+
import android.content.Context;
15+
import android.content.SharedPreferences;
16+
17+
import androidx.preference.PreferenceManager;
18+
import androidx.recyclerview.widget.RecyclerView;
19+
import androidx.test.core.app.ActivityScenario;
20+
import androidx.test.espresso.contrib.RecyclerViewActions;
21+
import androidx.test.espresso.matcher.ViewMatchers;
22+
import androidx.test.ext.junit.runners.AndroidJUnit4;
23+
import androidx.test.platform.app.InstrumentationRegistry;
24+
25+
import net.osmtracker.OSMTracker;
26+
import net.osmtracker.R;
27+
28+
import org.junit.After;
29+
import org.junit.Before;
30+
import org.junit.Test;
31+
import org.junit.runner.RunWith;
32+
33+
import java.io.File;
34+
import java.util.Arrays;
35+
36+
37+
@RunWith(AndroidJUnit4.class)
38+
public class PreferencesTest {
39+
40+
private Context context;
41+
private ActivityScenario<Preferences> activity;
42+
43+
@Before
44+
public void setup() {
45+
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
46+
47+
// Reset preferences to default before each test to ensure a clean state
48+
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
49+
prefs.edit().clear().commit();
50+
51+
// Launch the activity
52+
activity = ActivityScenario.launch(Preferences.class);
53+
}
54+
55+
@After
56+
public void tearDown() {
57+
activity.close();
58+
}
59+
60+
/**
61+
* Test that the Storage Directory preference logic works to rejects empty input.
62+
*/
63+
@Test
64+
public void testStorageDirectoryValidatesNonEmpty() {
65+
String keyTitle = context.getString(R.string.prefs_storage_dir);
66+
String defaultValue = OSMTracker.Preferences.VAL_STORAGE_DIR;
67+
68+
// Looks for storage directory preference
69+
scrollToAndClick(keyTitle);
70+
71+
// Try to save an empty value
72+
onView(withId(android.R.id.edit)).perform(clearText());
73+
onView(withText(android.R.string.ok)).perform(click());
74+
75+
// Open the preference to verify the value in the list remains the default (unchanged)
76+
onView(ViewMatchers.isAssignableFrom(RecyclerView.class))
77+
.check(matches(hasDescendant(withText(defaultValue))));
78+
}
79+
80+
/**
81+
* Test that the Storage Directory preference logic works to automatically append a leading
82+
* slash separator if missing.
83+
*/
84+
@Test
85+
public void testStorageDirectoryValidatesAppendLeadingSlash() {
86+
String keyTitle = context.getString(R.string.prefs_storage_dir);
87+
String expected = File.separator + "my_folder";
88+
89+
90+
// Looks for storage directory preference
91+
scrollToAndClick(keyTitle);
92+
93+
// Try to type a value without a slash
94+
onView(withId(android.R.id.edit)).perform(clearText());
95+
onView(withId(android.R.id.edit))
96+
.perform(typeText("my_folder"));
97+
onView(withText(android.R.string.ok)).perform(click());
98+
99+
// Open the preference to verify the value in the list is the expected
100+
onView(ViewMatchers.isAssignableFrom(RecyclerView.class))
101+
.check(matches(hasDescendant(withText(expected))));
102+
}
103+
104+
/**
105+
* Test Numeric Input logic (GPS Logging Interval): update summary with suffix.
106+
*/
107+
@Test
108+
public void testNumericInputLogic() {
109+
String title = context.getString(R.string.prefs_gps_logging_interval);
110+
String suffix = context.getString(R.string.prefs_gps_logging_interval_seconds);
111+
112+
scrollToAndClick(title);
113+
114+
// Enter a valid number
115+
onView(withId(android.R.id.edit))
116+
.perform(clearText(), typeText("30"));
117+
onView(withText(android.R.string.ok)).perform(click());
118+
119+
// Verify summary format: "30 seconds. <Static Summary>"
120+
onView(ViewMatchers.isAssignableFrom(RecyclerView.class))
121+
.check(matches(hasDescendant(withText(stringContainsInOrder(Arrays.asList("30",
122+
suffix))))));
123+
}
124+
125+
/**
126+
* Test that the Reset button in numeric preferences restores the default value.
127+
*/
128+
@Test
129+
public void testResetButtonResetsValue() {
130+
String title = context.getString(R.string.prefs_gps_logging_interval);
131+
String suffix = context.getString(R.string.prefs_gps_logging_interval_seconds);
132+
String defaultValue = OSMTracker.Preferences.VAL_GPS_LOGGING_INTERVAL;
133+
134+
scrollToAndClick(title);
135+
136+
// Set a custom value "50"
137+
onView(withId(android.R.id.edit)).perform(clearText(), typeText("50"));
138+
onView(withText(android.R.string.ok)).perform(click());
139+
140+
// Verify custom value is set
141+
onView(ViewMatchers.isAssignableFrom(RecyclerView.class))
142+
.check(matches(hasDescendant(withText(stringContainsInOrder(Arrays.asList("50",
143+
suffix))))));
144+
145+
// Reopen dialog
146+
scrollToAndClick(title);
147+
148+
// Click the Reset button (Neutral button)
149+
onView(withText(R.string.prefs_reset_default_value)).perform(click());
150+
151+
// Verify value is back to default "0"
152+
onView(ViewMatchers.isAssignableFrom(RecyclerView.class))
153+
.check(matches(hasDescendant(withText(stringContainsInOrder(Arrays.asList(
154+
defaultValue,
155+
suffix))))));
156+
}
157+
158+
/**
159+
* Test ListPreference custom summary logic (Screen Orientation)
160+
* Should show "Selected Value. \n ..." (don't check for the 2nd line of the summary)
161+
*/
162+
@Test
163+
public void testListPreferenceCustomSummary() {
164+
String title = context.getString(R.string.prefs_ui_orientation);
165+
166+
scrollToAndClick(title);
167+
168+
// Select 1st option from array resource entries
169+
String[] entries = context.getResources()
170+
.getStringArray(R.array.prefs_ui_orientation_options_keys);
171+
onView(withText(entries[0])).perform(click());
172+
173+
// Verify the two-line summary exists
174+
onView(ViewMatchers.isAssignableFrom(RecyclerView.class)).check(matches(hasDescendant(
175+
withText(stringContainsInOrder(Arrays.asList(entries[0], ".\n"))))));
176+
}
177+
178+
/**
179+
* Test Clear OAuth Data logic.
180+
*/
181+
@Test
182+
public void testClearOAuthData() {
183+
String title = context.getString(R.string.prefs_osm_clear_oauth_data);
184+
185+
// Inject a fake token to enable the button
186+
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context)
187+
.edit();
188+
editor.putString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN, "fake_token");
189+
editor.commit();
190+
191+
// Relaunch to refresh UI state
192+
ActivityScenario.launch(Preferences.class);
193+
194+
195+
scrollToAndClick(title);
196+
197+
// Click OK on Confirmation Dialog
198+
onView(withText(R.string.prefs_osm_clear_oauth_data_dialog)).check(matches(isDisplayed()));
199+
onView(withText(android.R.string.ok)).perform(click());
200+
201+
// Verify token is gone in prefs
202+
assert(!PreferenceManager.getDefaultSharedPreferences(context)
203+
.contains(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN));
204+
}
205+
206+
// --- Helper Methods ---
207+
208+
/**
209+
* Helper to scroll to a preference in the RecyclerView and click it.
210+
*/
211+
private void scrollToAndClick(String text) {
212+
onView(ViewMatchers.isAssignableFrom(RecyclerView.class))
213+
.perform(RecyclerViewActions.actionOnItem(
214+
hasDescendant(withText(text)),
215+
click()));
216+
}
217+
218+
}

0 commit comments

Comments
 (0)