From 3f0c7e74d3dece29d542fd20494a210d9ce43af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?= <593829+jamescr@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:32:58 -0600 Subject: [PATCH 01/14] Create FUNDING.yml --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..5be040e0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms +# github: [labexp] +liberapay: OSMTracker From 7aa42fe2b3ba04b015acab90d5f64abd2f1ec7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?= Date: Sun, 25 Jan 2026 09:17:25 -0600 Subject: [PATCH 02/14] Feature(CI): Add Signing to nightly APK --- .github/workflows/nightly.yml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index c139e76e..d1505ca0 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -51,12 +51,34 @@ jobs: distribution: 'temurin' java-version: '17' + - name: Setup Signing Keystore + run: | + # 1. Decode nightly keystore secret + echo "${{ secrets.NIGHTLY_KEYSTORE }}" | base64 -d > /tmp/nightly-keystore.jks + echo "Keystore created in /tmp/nightly-keystore.jks" + + # Verify + echo "✅ Keystore created in: /tmp/nightly-keystore.jks" + ls -la /tmp/nightly-keystore.jks + echo "ks_path=/tmp/nightly-keystore.jks" >> $GITHUB_OUTPUT + - name: Setup Android SDK uses: android-actions/setup-android@v2 - name: Build with Gradle - run: ./gradlew assembleDebug --stacktrace + run: | + ./gradlew assembleDebug --stacktrace \ + -Pandroid.injected.signing.store.file="/tmp/nightly-keystore.jks" \ + -Pandroid.injected.signing.store.password="${{ secrets.KEYSTORE_PASSWORD }}" \ + -Pandroid.injected.signing.key.alias="${{ secrets.KEY_ALIAS }}" \ + -Pandroid.injected.signing.key.password="${{ secrets.KEY_PASSWORD }}" + + - name: Cleanup Keystore + run: | + echo "Cleaning keystore..." + rm -f app/nightly-keystore.jks + echo "✅ Keystore removed" - name: Rename output APK run: | From d0c6d90d3fa3dece211d28a31434a51408cc0841 Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Sat, 16 May 2026 16:12:30 +0200 Subject: [PATCH 03/14] Start / Stop button on map screen --- .../osmtracker/activity/DisplayTrackMap.java | 101 +++++++++++++++++- .../net/osmtracker/activity/TrackLogger.java | 98 ++++++++++------- .../net/osmtracker/activity/TrackManager.java | 10 +- .../gps/GPSLoggerConnectionListener.java | 9 ++ .../gps/GPSLoggerServiceConnection.java | 24 ++--- app/src/main/res/drawable-mdpi/started.png | Bin 0 -> 3212 bytes app/src/main/res/drawable-mdpi/stopped.png | Bin 0 -> 3228 bytes app/src/main/res/layout/displaytrackmap.xml | 23 +++- app/src/main/res/values/accessibility.xml | 1 + 9 files changed, 200 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java create mode 100644 app/src/main/res/drawable-mdpi/started.png create mode 100644 app/src/main/res/drawable-mdpi/stopped.png diff --git a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java index 2d178e41..5ba57600 100644 --- a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java +++ b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java @@ -1,6 +1,5 @@ package net.osmtracker.activity; -import android.app.Activity; import android.content.ContentUris; import android.content.Intent; import android.content.SharedPreferences; @@ -12,18 +11,24 @@ import android.os.Handler; import android.util.DisplayMetrics; import android.util.Log; +import android.view.View; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; +import android.widget.ImageView; import androidx.preference.PreferenceManager; import net.osmtracker.OSMTracker; import net.osmtracker.R; +import net.osmtracker.db.DataHelper; import net.osmtracker.db.TrackContentProvider; import net.osmtracker.overlay.WayPointsOverlay; import net.osmtracker.overlay.Polylines; +import net.osmtracker.service.gps.GPSLoggerConnectionListener; +import net.osmtracker.service.gps.GPSLoggerServiceConnection; +import net.osmtracker.service.gps.GPSLogger; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; @@ -50,7 +55,7 @@ * @author Viesturs Zarins * */ -public class DisplayTrackMap extends Activity { +public class DisplayTrackMap extends GPSLoggerConnectionListener { private static final String TAG = DisplayTrackMap.class.getSimpleName(); @@ -146,6 +151,11 @@ public class DisplayTrackMap extends Activity { */ private boolean zoomedToTrackAlready = false; + /** + * Are we currently tracking? + */ + private boolean isTracking; + /** * the last position we know */ @@ -173,6 +183,75 @@ public class DisplayTrackMap extends Activity { */ private SharedPreferences prefs = null; + private ImageView startStopButton = null; + + private GPSLogger gpsLogger; + + private GPSLoggerServiceConnection gpsLoggerConnection; + + private synchronized GPSLoggerServiceConnection getGpsLoggerConnection() { + if(gpsLoggerConnection == null) + gpsLoggerConnection = new GPSLoggerServiceConnection(this); + return gpsLoggerConnection; + } + + private synchronized void unbindGpsLoggerConnection() { + if(gpsLoggerConnection != null) { + unbindService(gpsLoggerConnection); + gpsLoggerConnection=null; + } + } + + @Override + public long getCurrentTrackId() { + return currentTrackId; + } + + @Override + public void setGpsLogger(GPSLogger l) { + gpsLogger = l; + if(l == null) + isTracking = false; + updateTrackingIcon(); + } + + private boolean isTracking() { + if(gpsLogger != null && + gpsLogger.isTracking()) + isTracking = true; + return isTracking; + } + + private void updateTrackingIcon() { + if(startStopButton != null) + startStopButton.setImageResource(isTracking() ? + R.drawable.started : + R.drawable.stopped); + } + + private void toggleStartStop() { + DataHelper dataHelper = new DataHelper(this); + boolean forceTracking = false; + if(isTracking()) { + // currently tracking => stop it + Intent intent = new Intent(OSMTracker.INTENT_STOP_TRACKING); + intent.setPackage(getPackageName()); + sendBroadcast(intent); + + isTracking = false; + dataHelper.stopTracking(currentTrackId); + } else { + // currently not tracking => start it + Intent intent = new Intent(this, GPSLogger.class); + intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, currentTrackId); + startService(intent); + bindService(intent, getGpsLoggerConnection(), 0); + isTracking = true; + TrackManager.setActiveTrack(this, currentTrackId); + } + updateTrackingIcon(); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -183,6 +262,9 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.displaytrackmap); currentTrackId = getIntent().getExtras().getLong(TrackContentProvider.Schema.COL_TRACK_ID); + long activeTrackId = + DataHelper.getActiveTrackId(getContentResolver()); + isTracking = currentTrackId == activeTrackId; setTitle(getTitle() + ": #" + currentTrackId); // Initialize OSM view @@ -232,8 +314,23 @@ public void onChange(boolean selfChange) { osmViewController.animateTo(currentPosition,CENTER_DEFAULT_ZOOM_LEVEL, ANIMATION_DURATION_MS); } }); + startStopButton = + (ImageView)findViewById(R.id.displaytrackmap_imgStartStop); + if(activeTrackId == currentTrackId || + activeTrackId == TrackManager.TRACK_ID_NO_TRACK) { + updateTrackingIcon(); + startStopButton.setOnClickListener(view -> toggleStartStop()); + } else { + startStopButton.setVisibility(View.INVISIBLE); + } } + @Override + public void onDestroy() { + super.onDestroy(); + unbindGpsLoggerConnection(); + } + /** * Sets the map tile provider according to the user's demands in the settings. */ diff --git a/app/src/main/java/net/osmtracker/activity/TrackLogger.java b/app/src/main/java/net/osmtracker/activity/TrackLogger.java index 45c4a1f3..c7be3024 100644 --- a/app/src/main/java/net/osmtracker/activity/TrackLogger.java +++ b/app/src/main/java/net/osmtracker/activity/TrackLogger.java @@ -46,6 +46,7 @@ import net.osmtracker.listener.PressureListener; import net.osmtracker.listener.SensorListener; import net.osmtracker.receiver.MediaButtonReceiver; +import net.osmtracker.service.gps.GPSLoggerConnectionListener; import net.osmtracker.service.gps.GPSLogger; import net.osmtracker.service.gps.GPSLoggerServiceConnection; import net.osmtracker.util.CustomLayoutsUtils; @@ -67,7 +68,7 @@ * @author Nicolas Guillaumin * */ -public class TrackLogger extends Activity { +public class TrackLogger extends GPSLoggerConnectionListener { private static final String TAG = TrackLogger.class.getSimpleName(); @@ -332,30 +333,33 @@ protected void onResume() { OSMTracker.Preferences.VAL_GPS_CHECKSTARTUP)) { checkGPSProvider(); } - - // Register GPS status update for upper controls - ((GpsStatusRecord) findViewById(R.id.gpsStatus)).requestLocationUpdates(true); - - // Start GPS Logger service - startService(gpsLoggerServiceIntent); - - // Bind to GPS service. - // We can't use BIND_AUTO_CREATE here, because when we'll ubound - // later, we want to keep the service alive in background - bindService(gpsLoggerServiceIntent, gpsLoggerConnection, 0); + if(currentTrackId == + DataHelper.getActiveTrackId(getContentResolver())) { + // Register GPS status update for upper controls + ((GpsStatusRecord) findViewById(R.id.gpsStatus)).requestLocationUpdates(true); + + // Start GPS Logger service + startService(gpsLoggerServiceIntent); + + // Bind to GPS service. + // We can't use BIND_AUTO_CREATE here, because when we'll ubound + // later, we want to keep the service alive in background + bindService(gpsLoggerServiceIntent, gpsLoggerConnection, 0); - // connect the sensor listener - sensorListener.register(this); + // connect the sensor listener + sensorListener.register(this); - // connect the pressure listener - pressureListener.register(this, prefs.getBoolean(OSMTracker.Preferences.KEY_USE_BAROMETER,OSMTracker.Preferences.VAL_USE_BAROMETER)); + // connect the pressure listener + pressureListener.register(this, prefs.getBoolean(OSMTracker.Preferences.KEY_USE_BAROMETER,OSMTracker.Preferences.VAL_USE_BAROMETER)); - setEnabledActionButtons(buttonsEnabled); - if(!buttonsEnabled){ - Toast.makeText(this, R.string.tracklogger_waiting_gps, Toast.LENGTH_LONG).show(); + setEnabledActionButtons(buttonsEnabled); + if(!buttonsEnabled){ + Toast.makeText(this, R.string.tracklogger_waiting_gps, Toast.LENGTH_LONG).show(); + } + mAudioManager.registerMediaButtonEventReceiver(mediaButtonReceiver); + gpsStarted = true; } - mAudioManager.registerMediaButtonEventReceiver(mediaButtonReceiver); //save the layout file name if it change, in tags array String layoutName = CustomLayoutsUtils.getCurrentLayoutName(getApplicationContext()); @@ -387,32 +391,35 @@ public void onClick(DialogInterface dialog, int which) { } } + boolean gpsStarted = false; + @Override protected void onPause() { - - // Un-register GPS status update for upper controls - ((GpsStatusRecord) findViewById(R.id.gpsStatus)).requestLocationUpdates(false); - - if (gpsLogger != null) { - if (!gpsLogger.isTracking()) { - Log.v(TAG, "Service is not tracking, trying to stopService()"); - unbindService(gpsLoggerConnection); - stopService(gpsLoggerServiceIntent); - } else { - unbindService(gpsLoggerConnection); + if(gpsStarted) { + // Un-register GPS status update for upper controls + ((GpsStatusRecord) findViewById(R.id.gpsStatus)).requestLocationUpdates(false); + + if (gpsLogger != null) { + if (!gpsLogger.isTracking()) { + Log.v(TAG, "Service is not tracking, trying to stopService()"); + unbindService(gpsLoggerConnection); + stopService(gpsLoggerServiceIntent); + } else { + unbindService(gpsLoggerConnection); + } } - } - if (sensorListener!=null) { - sensorListener.unregister(); - } - - if (pressureListener != null) { - pressureListener.unregister(); - } + if (sensorListener!=null) { + sensorListener.unregister(); + } - mAudioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver); + if (pressureListener != null) { + pressureListener.unregister(); + } + mAudioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver); + gpsStarted = false; + } super.onPause(); } @@ -666,6 +673,17 @@ public GPSLogger getGpsLogger() { */ public void setGpsLogger(GPSLogger l) { this.gpsLogger = l; + if(l == null) { + setEnabledActionButtons(false); + return; + } + + // Update record status regarding of current tracking state + GpsStatusRecord gpsStatusRecord = (GpsStatusRecord) findViewById(R.id.gpsStatus); + if (gpsStatusRecord != null) + gpsStatusRecord.manageRecordingIndicator(getGpsLogger().isTracking()); + if (!gpsLogger.isTracking()) + setEnabledActionButtons(false); } /** diff --git a/app/src/main/java/net/osmtracker/activity/TrackManager.java b/app/src/main/java/net/osmtracker/activity/TrackManager.java index 4f9f2318..d0fc9471 100644 --- a/app/src/main/java/net/osmtracker/activity/TrackManager.java +++ b/app/src/main/java/net/osmtracker/activity/TrackManager.java @@ -1,5 +1,6 @@ package net.osmtracker.activity; +import android.app.Activity; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; @@ -76,7 +77,7 @@ public class TrackManager extends AppCompatActivity private static final String PREV_VISIBLE = "prev_visible"; /** Constant used if no track is active (-1)*/ - private static final long TRACK_ID_NO_TRACK = -1; + public static final long TRACK_ID_NO_TRACK = -1; // The active track being recorded, if any, or {TRACK_ID_NO_TRACK}; // value is updated in {@link #onResume()} @@ -774,12 +775,17 @@ private void setActiveTrack(long trackId){ // to be sure that no tracking will be in progress when we set a new track stopActiveTrack(); + setActiveTrack(this, trackId); + } + public static void setActiveTrack(Activity activity, long trackId){ // set the track active ContentValues values = new ContentValues(); values.put(TrackContentProvider.Schema.COL_ACTIVE, TrackContentProvider.Schema.VAL_TRACK_ACTIVE); - getContentResolver().update(TrackContentProvider.CONTENT_URI_TRACK, values, + activity + .getContentResolver() + .update(TrackContentProvider.CONTENT_URI_TRACK, values, TrackContentProvider.Schema.COL_ID + " = ?", new String[] {Long.toString(trackId)}); } diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java new file mode 100644 index 00000000..f01a97e5 --- /dev/null +++ b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java @@ -0,0 +1,9 @@ +package net.osmtracker.service.gps; + +import android.app.Activity; + +public abstract class GPSLoggerConnectionListener extends Activity { + public abstract void setGpsLogger(GPSLogger l); + + public abstract long getCurrentTrackId(); +} diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java index e7d22f67..338100ca 100644 --- a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java +++ b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java @@ -1,9 +1,7 @@ package net.osmtracker.service.gps; import net.osmtracker.OSMTracker; -import net.osmtracker.R; -import net.osmtracker.activity.TrackLogger; -import net.osmtracker.layout.GpsStatusRecord; +import net.osmtracker.service.gps.GPSLoggerConnectionListener; import net.osmtracker.db.TrackContentProvider; import android.content.ComponentName; @@ -20,34 +18,26 @@ public class GPSLoggerServiceConnection implements ServiceConnection { /** - * Reference to TrackLogger activity + * Reference to client activity */ - private TrackLogger activity; + private GPSLoggerConnectionListener activity; - public GPSLoggerServiceConnection(TrackLogger tl) { + public GPSLoggerServiceConnection(GPSLoggerConnectionListener tl) { activity = tl; } @Override public void onServiceDisconnected(ComponentName name) { - activity.setEnabledActionButtons(false); activity.setGpsLogger(null); } @Override public void onServiceConnected(ComponentName name, IBinder service) { - - activity.setGpsLogger( ((GPSLogger.GPSLoggerBinder) service).getService()); + GPSLogger gpsLogger = ((GPSLogger.GPSLoggerBinder) service).getService(); + activity.setGpsLogger(gpsLogger); - // Update record status regarding of current tracking state - GpsStatusRecord gpsStatusRecord = (GpsStatusRecord) activity.findViewById(R.id.gpsStatus); - if (gpsStatusRecord != null) { - gpsStatusRecord.manageRecordingIndicator(activity.getGpsLogger().isTracking()); - } - // If not already tracking, start tracking - if (!activity.getGpsLogger().isTracking()) { - activity.setEnabledActionButtons(false); + if (!gpsLogger.isTracking()) { Intent intent = new Intent(OSMTracker.INTENT_START_TRACKING); intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, activity.getCurrentTrackId()); intent.setPackage(activity.getPackageName()); diff --git a/app/src/main/res/drawable-mdpi/started.png b/app/src/main/res/drawable-mdpi/started.png new file mode 100644 index 0000000000000000000000000000000000000000..6181d034d326cb7208557d155823c658e257c1ea GIT binary patch literal 3212 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 O0000 + + - \ No newline at end of file + diff --git a/app/src/main/res/values/accessibility.xml b/app/src/main/res/values/accessibility.xml index a5f171e2..ffffc7d3 100644 --- a/app/src/main/res/values/accessibility.xml +++ b/app/src/main/res/values/accessibility.xml @@ -6,6 +6,7 @@ Zoom in Zoom out Zoom center + Start / Stop Satellite signal strength indicator Record in progress indicator From 56ce36ebd1ef3687cc56d99749c1569adec397f3 Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Sun, 9 Aug 2026 22:53:06 +0200 Subject: [PATCH 04/14] "Dummy" change to trigger rebuild --- .../java/net/osmtracker/activity/PreferencesTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java b/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java index bef2a78b..7e0babe8 100644 --- a/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java +++ b/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java @@ -62,6 +62,7 @@ public void tearDown() { */ @Test public void testStorageDirectoryValidatesNonEmpty() { + if(true)return; String keyTitle = context.getString(R.string.prefs_storage_dir); String defaultValue = OSMTracker.Preferences.VAL_STORAGE_DIR; From 0c8af6b3d58bf14a77a18bdca193f9ffb0b94fa6 Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Sun, 9 Aug 2026 23:44:03 +0200 Subject: [PATCH 05/14] reverted test --- .../java/net/osmtracker/activity/PreferencesTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java b/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java index 7e0babe8..bef2a78b 100644 --- a/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java +++ b/app/src/androidTest/java/net/osmtracker/activity/PreferencesTest.java @@ -62,7 +62,6 @@ public void tearDown() { */ @Test public void testStorageDirectoryValidatesNonEmpty() { - if(true)return; String keyTitle = context.getString(R.string.prefs_storage_dir); String defaultValue = OSMTracker.Preferences.VAL_STORAGE_DIR; From 1c129f2370be71b312692fea8b411ed421b49c16 Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Sat, 24 Oct 2026 12:19:14 +0200 Subject: [PATCH 06/14] Make GPSLoggerConnectionListener an interface --- .../net/osmtracker/activity/DisplayTrackMap.java | 4 +++- .../java/net/osmtracker/activity/TrackLogger.java | 3 ++- .../service/gps/GPSLoggerConnectionListener.java | 12 +++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java index 5ba57600..5ab07599 100644 --- a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java +++ b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java @@ -1,5 +1,6 @@ package net.osmtracker.activity; +import android.app.Activity; import android.content.ContentUris; import android.content.Intent; import android.content.SharedPreferences; @@ -55,7 +56,8 @@ * @author Viesturs Zarins * */ -public class DisplayTrackMap extends GPSLoggerConnectionListener { +public class DisplayTrackMap extends Activity + implements GPSLoggerConnectionListener { private static final String TAG = DisplayTrackMap.class.getSimpleName(); diff --git a/app/src/main/java/net/osmtracker/activity/TrackLogger.java b/app/src/main/java/net/osmtracker/activity/TrackLogger.java index 43a49566..6288cf29 100644 --- a/app/src/main/java/net/osmtracker/activity/TrackLogger.java +++ b/app/src/main/java/net/osmtracker/activity/TrackLogger.java @@ -68,7 +68,8 @@ * @author Nicolas Guillaumin * */ -public class TrackLogger extends GPSLoggerConnectionListener { +public class TrackLogger extends Activity + implements GPSLoggerConnectionListener { private static final String TAG = TrackLogger.class.getSimpleName(); diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java index f01a97e5..d564ff12 100644 --- a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java +++ b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java @@ -1,9 +1,15 @@ package net.osmtracker.service.gps; +import android.content.Intent; import android.app.Activity; -public abstract class GPSLoggerConnectionListener extends Activity { - public abstract void setGpsLogger(GPSLogger l); +public interface GPSLoggerConnectionListener { + void setGpsLogger(GPSLogger l); - public abstract long getCurrentTrackId(); + long getCurrentTrackId(); + + /* Stuff supplied by Activity / AppCompatActivity */ + String getPackageName(); + + void sendBroadcast(Intent intent); } From b78418939cb8d321057aa77fce3ac019bf1d2a3e Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Sun, 25 Oct 2026 00:13:12 +0200 Subject: [PATCH 07/14] Remove direct link between DisplayTrackMap and TrackManager, but rather copy DB updating code --- .../java/net/osmtracker/activity/DisplayTrackMap.java | 11 ++++++++++- .../java/net/osmtracker/activity/TrackManager.java | 8 +------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java index 5ab07599..71444f2b 100644 --- a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java +++ b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java @@ -1,6 +1,7 @@ package net.osmtracker.activity; import android.app.Activity; +import android.content.ContentValues; import android.content.ContentUris; import android.content.Intent; import android.content.SharedPreferences; @@ -248,8 +249,16 @@ private void toggleStartStop() { intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, currentTrackId); startService(intent); bindService(intent, getGpsLoggerConnection(), 0); + + // update DB, copied from TrackManager + ContentValues values = new ContentValues(); + values.put(TrackContentProvider.Schema.COL_ACTIVE, + TrackContentProvider.Schema.VAL_TRACK_ACTIVE); + getContentResolver().update(TrackContentProvider.CONTENT_URI_TRACK, values, + TrackContentProvider.Schema.COL_ID + " = ?", + new String[] {Long.toString(currentTrackId)}); + isTracking = true; - TrackManager.setActiveTrack(this, currentTrackId); } updateTrackingIcon(); } diff --git a/app/src/main/java/net/osmtracker/activity/TrackManager.java b/app/src/main/java/net/osmtracker/activity/TrackManager.java index d0fc9471..7ecb67de 100644 --- a/app/src/main/java/net/osmtracker/activity/TrackManager.java +++ b/app/src/main/java/net/osmtracker/activity/TrackManager.java @@ -1,6 +1,5 @@ package net.osmtracker.activity; -import android.app.Activity; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; @@ -775,17 +774,12 @@ private void setActiveTrack(long trackId){ // to be sure that no tracking will be in progress when we set a new track stopActiveTrack(); - setActiveTrack(this, trackId); - } - public static void setActiveTrack(Activity activity, long trackId){ // set the track active ContentValues values = new ContentValues(); values.put(TrackContentProvider.Schema.COL_ACTIVE, TrackContentProvider.Schema.VAL_TRACK_ACTIVE); - activity - .getContentResolver() - .update(TrackContentProvider.CONTENT_URI_TRACK, values, + getContentResolver().update(TrackContentProvider.CONTENT_URI_TRACK, values, TrackContentProvider.Schema.COL_ID + " = ?", new String[] {Long.toString(trackId)}); } From 2e4a2de5bcbe3c29d78d9c68bc2e712d10b6125f Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Sun, 25 Oct 2026 01:31:39 +0200 Subject: [PATCH 08/14] Tracklogger simplification --- .../osmtracker/activity/DisplayTrackMap.java | 8 ++- .../net/osmtracker/activity/TrackLogger.java | 72 +++++++++---------- .../osmtracker/layout/GpsStatusRecord.java | 5 ++ .../gps/GPSLoggerConnectionListener.java | 7 ++ .../gps/GPSLoggerServiceConnection.java | 19 +++-- app/src/main/res/values/strings.xml | 1 + 6 files changed, 67 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java index 71444f2b..b86bb06c 100644 --- a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java +++ b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java @@ -58,7 +58,8 @@ * */ public class DisplayTrackMap extends Activity - implements GPSLoggerConnectionListener { + implements GPSLoggerConnectionListener +{ private static final String TAG = DisplayTrackMap.class.getSimpleName(); @@ -218,6 +219,11 @@ public void setGpsLogger(GPSLogger l) { updateTrackingIcon(); } + @Override + public GPSLogger getGpsLogger() { + return gpsLogger; + } + private boolean isTracking() { if(gpsLogger != null && gpsLogger.isTracking()) diff --git a/app/src/main/java/net/osmtracker/activity/TrackLogger.java b/app/src/main/java/net/osmtracker/activity/TrackLogger.java index 6288cf29..97ada85d 100644 --- a/app/src/main/java/net/osmtracker/activity/TrackLogger.java +++ b/app/src/main/java/net/osmtracker/activity/TrackLogger.java @@ -357,15 +357,15 @@ protected void onResume() { if(!buttonsEnabled){ Toast.makeText(this, R.string.tracklogger_waiting_gps, Toast.LENGTH_LONG).show(); } - mAudioManager.registerMediaButtonEventReceiver(mediaButtonReceiver); - gpsStarted = true; - } - - //save the layout file name if it change, in tags array - String layoutName = CustomLayoutsUtils.getCurrentLayoutName(getApplicationContext()); - layoutNameTags.add(layoutName); + mAudioManager.registerMediaButtonEventReceiver(mediaButtonReceiver); + //save the layout file name if it change, in tags array + String layoutName = CustomLayoutsUtils.getCurrentLayoutName(getApplicationContext()); + layoutNameTags.add(layoutName); + } else { + ((GpsStatusRecord) findViewById(R.id.gpsStatus)).showStopped(); + } super.onResume(); } @@ -392,35 +392,38 @@ public void onClick(DialogInterface dialog, int which) { } } - boolean gpsStarted = false; - @Override protected void onPause() { - if(gpsStarted) { - // Un-register GPS status update for upper controls - ((GpsStatusRecord) findViewById(R.id.gpsStatus)).requestLocationUpdates(false); - - if (gpsLogger != null) { - if (!gpsLogger.isTracking()) { - Log.v(TAG, "Service is not tracking, trying to stopService()"); - unbindService(gpsLoggerConnection); - stopService(gpsLoggerServiceIntent); - } else { - unbindService(gpsLoggerConnection); + + // Un-register GPS status update for upper controls + ((GpsStatusRecord) findViewById(R.id.gpsStatus)).requestLocationUpdates(false); + + if (gpsLogger != null) { + if (!gpsLogger.isTracking()) { + Log.v(TAG, "Service is not tracking, trying to stopService()"); + try { + unbindService(gpsLoggerConnection); + } catch(Exception e) { + } + try { + stopService(gpsLoggerServiceIntent); + } catch(Exception e) { } + } else { + unbindService(gpsLoggerConnection); } + } - if (sensorListener!=null) { - sensorListener.unregister(); - } - - if (pressureListener != null) { - pressureListener.unregister(); - } + if (sensorListener!=null) { + sensorListener.unregister(); + } - mAudioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver); - gpsStarted = false; + if (pressureListener != null) { + pressureListener.unregister(); } + + mAudioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver); + super.onPause(); } @@ -676,17 +679,6 @@ public GPSLogger getGpsLogger() { */ public void setGpsLogger(GPSLogger l) { this.gpsLogger = l; - if(l == null) { - setEnabledActionButtons(false); - return; - } - - // Update record status regarding of current tracking state - GpsStatusRecord gpsStatusRecord = (GpsStatusRecord) findViewById(R.id.gpsStatus); - if (gpsStatusRecord != null) - gpsStatusRecord.manageRecordingIndicator(getGpsLogger().isTracking()); - if (!gpsLogger.isTracking()) - setEnabledActionButtons(false); } /** diff --git a/app/src/main/java/net/osmtracker/layout/GpsStatusRecord.java b/app/src/main/java/net/osmtracker/layout/GpsStatusRecord.java index 3afd60bc..ba854b14 100644 --- a/app/src/main/java/net/osmtracker/layout/GpsStatusRecord.java +++ b/app/src/main/java/net/osmtracker/layout/GpsStatusRecord.java @@ -128,6 +128,11 @@ public void requestLocationUpdates(boolean request) { } } + public void showStopped() { + TextView tvAccuracy = findViewById(R.id.gpsstatus_record_tvAccuracy); + tvAccuracy.setText(R.string.various_stopped); + } + private GnssStatus.Callback mStatusCallback = new GnssStatus.Callback() { @Override public void onSatelliteStatusChanged(GnssStatus status) { diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java index d564ff12..1ced67e1 100644 --- a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java +++ b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerConnectionListener.java @@ -1,5 +1,6 @@ package net.osmtracker.service.gps; +import android.view.View; import android.content.Intent; import android.app.Activity; @@ -8,8 +9,14 @@ public interface GPSLoggerConnectionListener { long getCurrentTrackId(); + default void setEnabledActionButtons(boolean enabled) { } + + GPSLogger getGpsLogger(); + /* Stuff supplied by Activity / AppCompatActivity */ String getPackageName(); void sendBroadcast(Intent intent); + + T findViewById (int id); } diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java index 338100ca..d59755df 100644 --- a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java +++ b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java @@ -1,7 +1,10 @@ package net.osmtracker.service.gps; import net.osmtracker.OSMTracker; -import net.osmtracker.service.gps.GPSLoggerConnectionListener; +import net.osmtracker.R; +import net.osmtracker.activity.TrackLogger; +import net.osmtracker.layout.GpsStatusRecord; +//import net.osmtracker.service.gps.GPSLoggerConnectionListener; import net.osmtracker.db.TrackContentProvider; import android.content.ComponentName; @@ -28,16 +31,24 @@ public GPSLoggerServiceConnection(GPSLoggerConnectionListener tl) { @Override public void onServiceDisconnected(ComponentName name) { + activity.setEnabledActionButtons(false); activity.setGpsLogger(null); } @Override public void onServiceConnected(ComponentName name, IBinder service) { - GPSLogger gpsLogger = ((GPSLogger.GPSLoggerBinder) service).getService(); - activity.setGpsLogger(gpsLogger); + + activity.setGpsLogger( ((GPSLogger.GPSLoggerBinder) service).getService()); + // Update record status regarding of current tracking state + GpsStatusRecord gpsStatusRecord = (GpsStatusRecord) activity.findViewById(R.id.gpsStatus); + if (gpsStatusRecord != null) { + gpsStatusRecord.manageRecordingIndicator(activity.getGpsLogger().isTracking()); + } + // If not already tracking, start tracking - if (!gpsLogger.isTracking()) { + if (!activity.getGpsLogger().isTracking()) { + activity.setEnabledActionButtons(false); Intent intent = new Intent(OSMTracker.INTENT_START_TRACKING); intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, activity.getCurrentTrackId()); intent.setPackage(activity.getPackageName()); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9f18028c..881c1461 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -183,6 +183,7 @@ Export process finished successfully Position not available {0} {1} / {2} {3} + Stopped OpenStreetMap track display From 89149aad8b399f6a595dfa930f7f4e71ced2cca9 Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Sun, 25 Oct 2026 10:25:44 +0100 Subject: [PATCH 09/14] test case --- .../activity/StartStopOnMapTest.java | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java diff --git a/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java b/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java new file mode 100644 index 00000000..173ee38f --- /dev/null +++ b/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java @@ -0,0 +1,230 @@ +package net.osmtracker.activity; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; +import android.content.ContentValues; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.widget.ImageView; +import android.app.Application; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowLog; +import org.robolectric.shadows.ShadowApplication; + +import androidx.preference.PreferenceManager; + +import net.osmtracker.OSMTracker; +import net.osmtracker.R; +import net.osmtracker.db.TrackContentProvider; +import net.osmtracker.service.gps.GPSLogger; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.inOrder; +import org.mockito.InOrder; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 25) +public class StartStopOnMapTest { + + private DisplayTrackMap displayTrackMap; + private Context context; + private ShadowApplication shadowApplication; + private ContentResolver resolver; + + private static final long TEST_TRACK_ID = 42L; + + private static final String TAG = "StartStopOnMapTest"; + + private ImageView mockStartStopButton; + private InOrder startStopButtonInOrder; + + @Before + public void setUp() throws NoSuchFieldException, IllegalAccessException { + // Comment out to redirect log output to stdout: + // ShadowLog.stream = System.out; + + // application for creating activities, etc. + Application app = RuntimeEnvironment.getApplication(); + context = app; + + // shadow application, with robolectric-specific + // methods for finding out whether a service has been + // started + shadowApplication = Shadow.extract(app); + + resolver = context.getContentResolver(); + + // Set up a default track for this test + ContentValues values = new ContentValues(); + values.put(TrackContentProvider.Schema.COL_START_DATE, + System.currentTimeMillis()); + values.put(TrackContentProvider.Schema.COL_ID, + TEST_TRACK_ID); + var uri = resolver.insert(TrackContentProvider.CONTENT_URI_TRACK, values); + assertNotNull("Track should have been created", uri); + + // Set up default preferences + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putString(OSMTracker.Preferences.KEY_GPS_LOGGING_INTERVAL, "0") + .putString(OSMTracker.Preferences.KEY_GPS_LOGGING_MIN_DISTANCE, "0") + .commit(); + + // Create activity with test track ID + Intent intent = new Intent(); + intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, + TEST_TRACK_ID); + displayTrackMap = Robolectric + .buildActivity(DisplayTrackMap.class, intent) + .create().get(); + + // Create a mock start/stop button + mockStartStopButton = mock(ImageView.class); + setInternalState(displayTrackMap, "startStopButton", mockStartStopButton); + startStopButtonInOrder = inOrder(mockStartStopButton); + } + + @Test + public void testToggleStartStop_WhenNotTracking_ShouldStartTracking() + throws NoSuchFieldException, NoSuchMethodException, + InvocationTargetException, IllegalAccessException { + // Given: currently not tracking + assertFalse("Initial state should be 'not tracking'", + isTracking(displayTrackMap)); + + // When: toggle is called + toggleStartStop(displayTrackMap); + + // Then: should have attempted to start the service + Intent expectedIntent = shadowApplication.getNextStartedService(); + assertTrue("Service should have been started", + expectedIntent != null); + assertTrue("Started service should be GPSLogger", + expectedIntent.getComponent().getClassName() + .contains("GPSLogger")); + assertTrue("Intent should contain track ID", + expectedIntent + .getLongExtra(TrackContentProvider.Schema.COL_TRACK_ID, -1L) == + TEST_TRACK_ID); + assertTrue("isTracking flag should be true after starting", + isTracking(displayTrackMap)); + + // button should show "started" color: + startStopButtonInOrder + .verify(mockStartStopButton) + .setImageResource(R.drawable.started); + + // Now try stop tracking + toggleStartStop(displayTrackMap); + assertFalse("isTracking flag should be false after stopping", + isTracking(displayTrackMap)); + // button should show "stopped" color: + startStopButtonInOrder + .verify(mockStartStopButton) + .setImageResource(R.drawable.stopped); + } + + @Test + public void testIsTracking_WhenGpsLoggerIsNull_ShouldReturnFalse() + throws NoSuchFieldException, NoSuchMethodException, + InvocationTargetException, IllegalAccessException { + // Given: gpsLogger is null + setInternalState(displayTrackMap, "gpsLogger", null); + + // When: isTracking() is called + boolean result = isTracking(displayTrackMap); + + // Then: should return false + assertFalse("isTracking() should return false when gpsLogger is null", result); + } + + @Test + public void testSetGpsLogger_WhenNull_ShouldSetIsTrackingFalse() + throws NoSuchFieldException, NoSuchMethodException, + InvocationTargetException, IllegalAccessException { + // Given: isTracking is true + setInternalState(displayTrackMap, "isTracking", true); + + // When: setGpsLogger(null) is called + displayTrackMap.setGpsLogger(null); + + // Then: isTracking should be set to false + assertFalse("isTracking should be false when gpsLogger is set to null", + isTracking(displayTrackMap)); + } + + @Test + public void testUpdateTrackingIcon_WhenButtonNull_ShouldNotCrash() + throws NoSuchFieldException, NoSuchMethodException, + InvocationTargetException, IllegalAccessException + { + // Given: button is null + setInternalState(displayTrackMap, "startStopButton", null); + setInternalState(displayTrackMap, "isTracking", true); + + // When: updateTrackingIcon() is called + // Then: should not crash + updateTrackingIcon(displayTrackMap); + } + + // Call private methods + private void toggleStartStop(DisplayTrackMap displayTrackMap) + throws NoSuchMethodException, InvocationTargetException, + IllegalAccessException + { + invokeInternalMethod(displayTrackMap, "toggleStartStop"); + } + + private boolean isTracking(DisplayTrackMap displayTrackMap) + throws NoSuchMethodException, InvocationTargetException, + IllegalAccessException + { + return (Boolean) invokeInternalMethod(displayTrackMap, + "isTracking"); + } + + private void updateTrackingIcon(DisplayTrackMap displayTrackMap) + throws NoSuchMethodException, InvocationTargetException, + IllegalAccessException + { + invokeInternalMethod(displayTrackMap, "updateTrackingIcon"); + } + + // Helper methods + private void setInternalState(Object target, String fieldName, + Object value) + throws NoSuchFieldException, IllegalAccessException + { + Field field; + if (target instanceof Class) { + field = ((Class) target).getDeclaredField(fieldName); + } else { + field = target.getClass().getDeclaredField(fieldName); + } + field.setAccessible(true); + field.set(target instanceof Class ? null : target, value); + } + + private Object invokeInternalMethod(Object target, String getterName) + throws NoSuchMethodException, InvocationTargetException, + IllegalAccessException { + Method getter = target.getClass() + .getDeclaredMethod(getterName); + getter.setAccessible(true); + return getter.invoke(target); + } +} From 6388bacdbf78fff8ddad7675e1fe2ca6a17cf7c9 Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Sun, 25 Oct 2026 19:39:15 +0100 Subject: [PATCH 10/14] Not directly related to start/stop button, but this capitalization error made automated test fail in strange ways. --- app/src/main/java/net/osmtracker/OSMTracker.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/osmtracker/OSMTracker.java b/app/src/main/java/net/osmtracker/OSMTracker.java index 3accb77c..650bb0b6 100644 --- a/app/src/main/java/net/osmtracker/OSMTracker.java +++ b/app/src/main/java/net/osmtracker/OSMTracker.java @@ -100,7 +100,7 @@ public static final class Preferences { public final static String VAL_UI_ORIENTATION_LANDSCAPE = "landscape"; public final static String VAL_UI_ORIENTATION = VAL_UI_ORIENTATION_NONE; - public final static String VAL_UI_MAP_TILE_MAPNIK = "MAPNIK"; + public final static String VAL_UI_MAP_TILE_MAPNIK = "Mapnik"; //default values for repository settings public final static String VAL_GITHUB_USERNAME = "labexp"; @@ -187,4 +187,4 @@ public static final class Preferences { public static final class Devices { public static final String NEXUS_S = "Nexus S"; } -} \ No newline at end of file +} From 2e99f33f3af28466ba7ac0581b4b9303858e989a Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Mon, 26 Oct 2026 22:22:42 +0100 Subject: [PATCH 11/14] Cleanup in import statements --- .../net/osmtracker/service/gps/GPSLoggerServiceConnection.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java index d59755df..0fb275f0 100644 --- a/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java +++ b/app/src/main/java/net/osmtracker/service/gps/GPSLoggerServiceConnection.java @@ -2,9 +2,7 @@ import net.osmtracker.OSMTracker; import net.osmtracker.R; -import net.osmtracker.activity.TrackLogger; import net.osmtracker.layout.GpsStatusRecord; -//import net.osmtracker.service.gps.GPSLoggerConnectionListener; import net.osmtracker.db.TrackContentProvider; import android.content.ComponentName; From 8287d063f3064744264ffc789b7f64da38787ce1 Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Mon, 26 Oct 2026 22:38:42 +0100 Subject: [PATCH 12/14] Test that the GPS Logger service can be stopped. --- .../osmtracker/activity/DisplayTrackMap.java | 35 +++++--- .../activity/StartStopOnMapTest.java | 88 ++++++++++++++++++- 2 files changed, 108 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java index b86bb06c..b0528e36 100644 --- a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java +++ b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java @@ -11,6 +11,7 @@ import android.graphics.Paint; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.util.DisplayMetrics; import android.util.Log; import android.view.View; @@ -249,23 +250,35 @@ private void toggleStartStop() { isTracking = false; dataHelper.stopTracking(currentTrackId); + updateTrackingIcon(); } else { // currently not tracking => start it Intent intent = new Intent(this, GPSLogger.class); intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, currentTrackId); startService(intent); - bindService(intent, getGpsLoggerConnection(), 0); - - // update DB, copied from TrackManager - ContentValues values = new ContentValues(); - values.put(TrackContentProvider.Schema.COL_ACTIVE, - TrackContentProvider.Schema.VAL_TRACK_ACTIVE); - getContentResolver().update(TrackContentProvider.CONTENT_URI_TRACK, values, - TrackContentProvider.Schema.COL_ID + " = ?", - new String[] {Long.toString(currentTrackId)}); - - isTracking = true; + new Handler(Looper.getMainLooper()) + .post(() -> finishStartTracking(intent)); } + } + + /** + * Second part of StartTracking. Bind has to be done at a + * later mainlooper invocation than startService or the + * service might not yet been ready, especially in test + * scenarios + */ + private void finishStartTracking(Intent intent) { + bindService(intent, getGpsLoggerConnection(), 0); + + // update DB, copied from TrackManager + ContentValues values = new ContentValues(); + values.put(TrackContentProvider.Schema.COL_ACTIVE, + TrackContentProvider.Schema.VAL_TRACK_ACTIVE); + getContentResolver().update(TrackContentProvider.CONTENT_URI_TRACK, values, + TrackContentProvider.Schema.COL_ID + " = ?", + new String[] {Long.toString(currentTrackId)}); + + isTracking = true; updateTrackingIcon(); } diff --git a/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java b/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java index 173ee38f..27eb0e96 100644 --- a/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java +++ b/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java @@ -9,8 +9,10 @@ import android.content.Intent; import android.widget.ImageView; import android.app.Application; +import android.content.ComponentName; import org.junit.Before; +import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; @@ -20,6 +22,8 @@ import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowLog; import org.robolectric.shadows.ShadowApplication; +import org.robolectric.shadows.ShadowService; +import org.robolectric.shadows.ShadowLooper; import androidx.preference.PreferenceManager; @@ -27,15 +31,19 @@ import net.osmtracker.R; import net.osmtracker.db.TrackContentProvider; import net.osmtracker.service.gps.GPSLogger; +import net.osmtracker.service.gps.GPSLoggerServiceConnection; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.inOrder; import org.mockito.InOrder; +import android.content.ContentProviderClient; + @RunWith(RobolectricTestRunner.class) @Config(sdk = 25) public class StartStopOnMapTest { @@ -52,10 +60,15 @@ public class StartStopOnMapTest { private ImageView mockStartStopButton; private InOrder startStopButtonInOrder; + private ShadowService shadowService; + @Before - public void setUp() throws NoSuchFieldException, IllegalAccessException { + public void setUp() + throws NoSuchFieldException, IllegalAccessException, + NoSuchMethodException, InvocationTargetException + { // Comment out to redirect log output to stdout: - // ShadowLog.stream = System.out; + ShadowLog.stream = System.out; // application for creating activities, etc. Application app = RuntimeEnvironment.getApplication(); @@ -98,6 +111,22 @@ public void setUp() throws NoSuchFieldException, IllegalAccessException { startStopButtonInOrder = inOrder(mockStartStopButton); } + @After + public void tearDown() { + try { + ContentProviderClient client = + RuntimeEnvironment.getApplication() + .getContentResolver() + .acquireContentProviderClient(TrackContentProvider.AUTHORITY); + + if (client != null) { + client.release(); // Release the client + } + } catch (Exception e) { + // Ignore + } + } + @Test public void testToggleStartStop_WhenNotTracking_ShouldStartTracking() throws NoSuchFieldException, NoSuchMethodException, @@ -120,21 +149,60 @@ public void testToggleStartStop_WhenNotTracking_ShouldStartTracking() expectedIntent .getLongExtra(TrackContentProvider.Schema.COL_TRACK_ID, -1L) == TEST_TRACK_ID); + + // Now that we have established that the application + // asked to start the service, actually do so + + // Indeed, with Robolectric, the shadowApplication + // merely registers the *request* to start a service, + // but does not actually start it. This needs to be + // done "manually" here. + var serviceController = Robolectric + .buildService(GPSLogger.class, expectedIntent); + GPSLogger service = serviceController.get(); + + // Create the real binder from the service + ComponentName componentName = new ComponentName(RuntimeEnvironment.getApplication(), GPSLogger.class); + GPSLogger.GPSLoggerBinder binder = service.new GPSLoggerBinder(); + shadowApplication + .setComponentNameAndServiceForBindService(componentName, + binder); + serviceController.create(); + + shadowService = Shadow.extract(service); + + // allow Android framework to process + ShadowLooper.idleMainLooper(); + assertTrue("isTracking flag should be true after starting", isTracking(displayTrackMap)); + assertFalse("Service should not yet have been stopped", + shadowService.isStoppedBySelf()); // button should show "started" color: startStopButtonInOrder - .verify(mockStartStopButton) + .verify(mockStartStopButton, atLeastOnce()) .setImageResource(R.drawable.started); // Now try stop tracking toggleStartStop(displayTrackMap); + ShadowLooper.idleMainLooper(100); + assertTrue("Service should have been stopped", + shadowService.isStoppedBySelf()); + + // now that we've established that the service + // did initiated a stopSelf, help robolectric along, and + // actually perform what this entails + GPSLoggerServiceConnection cnx = (GPSLoggerServiceConnection) + getInternalState(displayTrackMap, + "gpsLoggerConnection"); + cnx.onServiceDisconnected(componentName); + assertFalse("isTracking flag should be false after stopping", isTracking(displayTrackMap)); // button should show "stopped" color: startStopButtonInOrder - .verify(mockStartStopButton) + .verify(mockStartStopButton, atLeastOnce()) .setImageResource(R.drawable.stopped); } @@ -219,6 +287,18 @@ private void setInternalState(Object target, String fieldName, field.set(target instanceof Class ? null : target, value); } + private Object getInternalState(Object target, String fieldName) + throws NoSuchFieldException, IllegalAccessException { + Field field; + if (target instanceof Class) { + field = ((Class) target).getDeclaredField(fieldName); + } else { + field = target.getClass().getDeclaredField(fieldName); + } + field.setAccessible(true); + return field.get(target instanceof Class ? null : target); + } + private Object invokeInternalMethod(Object target, String getterName) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { From 0c55e03a434cc5fdfe0f46d404427682bc2ca079 Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Mon, 26 Oct 2026 23:10:46 +0100 Subject: [PATCH 13/14] tearDown was trying to solve a problem not actually due to this test. Remove again --- .../activity/StartStopOnMapTest.java | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java b/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java index 27eb0e96..2163611e 100644 --- a/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java +++ b/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java @@ -5,6 +5,7 @@ import java.lang.reflect.InvocationTargetException; import android.content.ContentValues; import android.content.ContentResolver; +import android.content.ContentProvider; import android.content.Context; import android.content.Intent; import android.widget.ImageView; @@ -29,6 +30,7 @@ import net.osmtracker.OSMTracker; import net.osmtracker.R; +import net.osmtracker.db.DatabaseHelper; import net.osmtracker.db.TrackContentProvider; import net.osmtracker.service.gps.GPSLogger; import net.osmtracker.service.gps.GPSLoggerServiceConnection; @@ -68,7 +70,7 @@ public void setUp() NoSuchMethodException, InvocationTargetException { // Comment out to redirect log output to stdout: - ShadowLog.stream = System.out; + // ShadowLog.stream = System.out; // application for creating activities, etc. Application app = RuntimeEnvironment.getApplication(); @@ -111,22 +113,6 @@ public void setUp() startStopButtonInOrder = inOrder(mockStartStopButton); } - @After - public void tearDown() { - try { - ContentProviderClient client = - RuntimeEnvironment.getApplication() - .getContentResolver() - .acquireContentProviderClient(TrackContentProvider.AUTHORITY); - - if (client != null) { - client.release(); // Release the client - } - } catch (Exception e) { - // Ignore - } - } - @Test public void testToggleStartStop_WhenNotTracking_ShouldStartTracking() throws NoSuchFieldException, NoSuchMethodException, From 25e4c8b8ba5ab3822065cd1beb40a5c5ec010cb8 Mon Sep 17 00:00:00 2001 From: Alain Knaff Date: Tue, 27 Oct 2026 23:44:20 +0100 Subject: [PATCH 14/14] Cleaner way to set up service starting and binding that does not actually require to modify application code --- .../osmtracker/activity/DisplayTrackMap.java | 35 +++------- .../activity/StartStopOnMapTest.java | 70 ++++++++++++------- 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java index b0528e36..b86bb06c 100644 --- a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java +++ b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java @@ -11,7 +11,6 @@ import android.graphics.Paint; import android.os.Bundle; import android.os.Handler; -import android.os.Looper; import android.util.DisplayMetrics; import android.util.Log; import android.view.View; @@ -250,35 +249,23 @@ private void toggleStartStop() { isTracking = false; dataHelper.stopTracking(currentTrackId); - updateTrackingIcon(); } else { // currently not tracking => start it Intent intent = new Intent(this, GPSLogger.class); intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, currentTrackId); startService(intent); - new Handler(Looper.getMainLooper()) - .post(() -> finishStartTracking(intent)); - } - } + bindService(intent, getGpsLoggerConnection(), 0); - /** - * Second part of StartTracking. Bind has to be done at a - * later mainlooper invocation than startService or the - * service might not yet been ready, especially in test - * scenarios - */ - private void finishStartTracking(Intent intent) { - bindService(intent, getGpsLoggerConnection(), 0); - - // update DB, copied from TrackManager - ContentValues values = new ContentValues(); - values.put(TrackContentProvider.Schema.COL_ACTIVE, - TrackContentProvider.Schema.VAL_TRACK_ACTIVE); - getContentResolver().update(TrackContentProvider.CONTENT_URI_TRACK, values, - TrackContentProvider.Schema.COL_ID + " = ?", - new String[] {Long.toString(currentTrackId)}); - - isTracking = true; + // update DB, copied from TrackManager + ContentValues values = new ContentValues(); + values.put(TrackContentProvider.Schema.COL_ACTIVE, + TrackContentProvider.Schema.VAL_TRACK_ACTIVE); + getContentResolver().update(TrackContentProvider.CONTENT_URI_TRACK, values, + TrackContentProvider.Schema.COL_ID + " = ?", + new String[] {Long.toString(currentTrackId)}); + + isTracking = true; + } updateTrackingIcon(); } diff --git a/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java b/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java index 2163611e..7eceb74b 100644 --- a/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java +++ b/app/src/test/java/net/osmtracker/activity/StartStopOnMapTest.java @@ -3,9 +3,9 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; +import android.database.sqlite.SQLiteDatabase; import android.content.ContentValues; import android.content.ContentResolver; -import android.content.ContentProvider; import android.content.Context; import android.content.Intent; import android.widget.ImageView; @@ -30,7 +30,6 @@ import net.osmtracker.OSMTracker; import net.osmtracker.R; -import net.osmtracker.db.DatabaseHelper; import net.osmtracker.db.TrackContentProvider; import net.osmtracker.service.gps.GPSLogger; import net.osmtracker.service.gps.GPSLoggerServiceConnection; @@ -42,10 +41,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.any; import org.mockito.InOrder; -import android.content.ContentProviderClient; - @RunWith(RobolectricTestRunner.class) @Config(sdk = 25) public class StartStopOnMapTest { @@ -63,6 +63,7 @@ public class StartStopOnMapTest { private InOrder startStopButtonInOrder; private ShadowService shadowService; + private ComponentName componentName; @Before public void setUp() @@ -107,12 +108,47 @@ public void setUp() .buildActivity(DisplayTrackMap.class, intent) .create().get(); + componentName = new ComponentName(context, GPSLogger.class); + + // spy the startService call + displayTrackMap = spy(displayTrackMap); + doAnswer(invocation -> { + Intent expectedIntent = invocation.getArgument(0); + GPSLogger service = Robolectric + .buildService(GPSLogger.class, expectedIntent) + .create() + .get(); + + // Create the real binder from the service + GPSLogger.GPSLoggerBinder binder = service.new GPSLoggerBinder(); + shadowApplication + .setComponentNameAndServiceForBindService(componentName, + binder); + invocation.callRealMethod(); + return null; + }).when(displayTrackMap).startService(any(Intent.class)); + // Create a mock start/stop button mockStartStopButton = mock(ImageView.class); setInternalState(displayTrackMap, "startStopButton", mockStartStopButton); startStopButtonInOrder = inOrder(mockStartStopButton); } + @After + public void tearDown() { + // Close all databases opened by SQLiteOpenHelper + resolver = null; + displayTrackMap = null; + context = null; + System.gc(); + try { + // allow finalizers to run + Thread.sleep(10); + } catch(InterruptedException e) { + } + SQLiteDatabase.releaseMemory(); + } + @Test public void testToggleStartStop_WhenNotTracking_ShouldStartTracking() throws NoSuchFieldException, NoSuchMethodException, @@ -136,30 +172,14 @@ public void testToggleStartStop_WhenNotTracking_ShouldStartTracking() .getLongExtra(TrackContentProvider.Schema.COL_TRACK_ID, -1L) == TEST_TRACK_ID); - // Now that we have established that the application - // asked to start the service, actually do so - - // Indeed, with Robolectric, the shadowApplication - // merely registers the *request* to start a service, - // but does not actually start it. This needs to be - // done "manually" here. - var serviceController = Robolectric - .buildService(GPSLogger.class, expectedIntent); - GPSLogger service = serviceController.get(); - - // Create the real binder from the service - ComponentName componentName = new ComponentName(RuntimeEnvironment.getApplication(), GPSLogger.class); - GPSLogger.GPSLoggerBinder binder = service.new GPSLoggerBinder(); - shadowApplication - .setComponentNameAndServiceForBindService(componentName, - binder); - serviceController.create(); - - shadowService = Shadow.extract(service); - // allow Android framework to process ShadowLooper.idleMainLooper(); + GPSLogger service = + (GPSLogger) getInternalState(displayTrackMap, "gpsLogger"); + assertNotNull("GPS Logger should not be null", service); + shadowService = Shadow.extract(service); + assertTrue("isTracking flag should be true after starting", isTracking(displayTrackMap)); assertFalse("Service should not yet have been stopped",