This activity may be called twice during a single
+ * upload cycle: First to start the upload, then a second
+ * time when the user has authenticated using the browser.
+ *
+ * @author Most of the code was made by Nicolas Guillaumin, adapted by Jose Andrés Vargas Serrano
+ */
+public class OpenStreetMapNotesUpload extends Activity {
+
+ private static final String TAG = OpenStreetMapNotesUpload.class.getSimpleName();
+
+ private double latitude;
+ private double longitude;
+
+ private TextView noteContentView;
+ private TextView noteFooterView;
+
+ /** URL that the browser will call once the user is authenticated */
+ public final static String OAUTH2_CALLBACK_URL = "osmtracker://osm-upload/oath2-completed/";
+ public final static int RC_AUTH = 7;
+
+ private AuthorizationService authService;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ View uploadNoteView = getLayoutInflater().inflate(R.layout.osm_note_upload, null);
+ setContentView(uploadNoteView);
+ setTitle(R.string.osm_note_upload);
+
+ noteContentView = uploadNoteView.findViewById(R.id.wplist_item_name);
+ noteFooterView = uploadNoteView.findViewById(R.id.osm_note_footer);
+
+ // Read and cache extras
+ Bundle extras = getIntent().getExtras();
+ if (extras == null) {
+ Log.e(TAG, "Missing extras for note upload.");
+ finish();
+ return;
+ }
+
+ String initialNoteText = extras.getString("noteContent", "");
+ String appName = extras.getString("appName", getString(R.string.app_name));
+ String version = extras.getString("version", "");
+
+ if (extras.containsKey("latitude")) latitude = extras.getDouble("latitude");
+ if (extras.containsKey("longitude")) longitude = extras.getDouble("longitude");
+
+ // fill UI with note content and note footer
+ noteContentView.setText(initialNoteText);
+ noteFooterView.setText(getString(R.string.osm_note_footer, appName, version));
+
+ final Button btnOk = (Button) findViewById(R.id.osm_note_upload_button_ok);
+ btnOk.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startUpload();
+ }
+ });
+ final Button btnCancel = (Button) findViewById(R.id.osm_note_upload_button_cancel);
+ btnCancel.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ }
+
+
+ /**
+ * Either starts uploading directly if we are authenticated against OpenStreetMap,
+ * or ask the user to authenticate via the browser.
+ */
+ private void startUpload() {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ if ( prefs.contains(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN) ) {
+ // Re-use saved token
+ uploadToOsm(prefs.getString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN, ""));
+ } else {
+ // Open browser and request token
+ requestOsmAuth();
+ }
+ }
+ /*
+ * Init Authorization request workflow.
+ */
+ public void requestOsmAuth() {
+ // Authorization service configuration
+ AuthorizationServiceConfiguration serviceConfig =
+ new AuthorizationServiceConfiguration(
+ Uri.parse(OpenStreetMapConstants.OAuth2.Urls.AUTHORIZATION_ENDPOINT),
+ Uri.parse(OpenStreetMapConstants.OAuth2.Urls.TOKEN_ENDPOINT));
+
+ // Obtaining an authorization code
+ Uri redirectURI = Uri.parse(OAUTH2_CALLBACK_URL);
+ AuthorizationRequest.Builder authRequestBuilder =
+ new AuthorizationRequest.Builder(
+ serviceConfig, OpenStreetMapConstants.OAuth2.CLIENT_ID,
+ ResponseTypeValues.CODE, redirectURI);
+ AuthorizationRequest authRequest = authRequestBuilder
+ .setScope(OpenStreetMapConstants.OAuth2.SCOPE)
+ .build();
+
+ // Start activity.
+ authService = new AuthorizationService(this);
+ Intent authIntent = authService.getAuthorizationRequestIntent(authRequest);
+ startActivityForResult(authIntent, RC_AUTH); //when done onActivityResult will be called.
+ }
+
+
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ // User is returning from authentication
+ if (requestCode == RC_AUTH) {
+ // Handling the authorization response
+ AuthorizationResponse resp = AuthorizationResponse.fromIntent(data);
+ AuthorizationException ex = AuthorizationException.fromIntent(data);
+ // ... process the response or exception ...
+ if (ex != null) {
+ Log.e(TAG, "Authorization Error. Exception received from server.");
+ Log.e(TAG, ex.getMessage());
+ } else if (resp == null) {
+ Log.e(TAG, "Authorization Error. Null response from server.");
+ } else {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ //Exchanging the authorization code
+ authService.performTokenRequest(
+ resp.createTokenExchangeRequest(),
+ new AuthorizationService.TokenResponseCallback() {
+ @Override public void onTokenRequestCompleted(
+ TokenResponse resp, AuthorizationException ex) {
+ if (resp != null) {
+ // exchange succeeded
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN, resp.accessToken);
+ editor.apply();
+ //continue with the note Upload.
+ uploadToOsm(resp.accessToken);
+ } else {
+ // authorization failed, check ex for more details
+ Log.e(TAG, "OAuth failed.");
+ }
+ }
+ });
+ }
+ } else {
+ Log.e(TAG, "Unexpected requestCode:" + requestCode + ".");
+ }
+ }
+
+ /**
+ * Uploads notes to OSM.
+ */
+ public void uploadToOsm(String accessToken) {
+ String noteText = noteContentView.getText().toString();
+ String footer = noteFooterView.getText().toString();
+ if (!footer.isEmpty()) {
+ noteText = noteText + "\n\n" + footer;
+ }
+ new UploadToOpenStreetMapNotesTask(
+ OpenStreetMapNotesUpload.this,
+ accessToken,
+ noteText,
+ latitude,
+ longitude
+ ).execute();
+ }
+
+
+}
diff --git a/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java b/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java
new file mode 100644
index 000000000..21e676312
--- /dev/null
+++ b/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java
@@ -0,0 +1,178 @@
+package net.osmtracker.osm;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.SharedPreferences.Editor;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import net.osmtracker.OSMTracker;
+import net.osmtracker.R;
+import net.osmtracker.util.DialogUtils;
+
+import de.westnordost.osmapi.OsmConnection;
+import de.westnordost.osmapi.common.errors.OsmAuthorizationException;
+import de.westnordost.osmapi.common.errors.OsmBadUserInputException;
+import de.westnordost.osmapi.map.data.OsmLatLon;
+import de.westnordost.osmapi.notes.Note; // Note object
+import de.westnordost.osmapi.notes.NotesApi; // Api for uploading notes to OSM
+import de.westnordost.osmapi.map.data.LatLon; // Data type for location points, maybe I'll put it in the dialog file
+
+/**
+ * Uploads a note to OpenStreetMap
+ *
+ * @author Most of the code was made by Nicolas Guillaumin, adapted by Jose Andrés Vargas Serrano
+ */
+public class UploadToOpenStreetMapNotesTask extends AsyncTask {
+
+ private static final String TAG = UploadToOpenStreetMapNotesTask.class.getSimpleName();
+
+ /** Upload progress dialog */
+ private ProgressDialog dialog;
+
+ private final Activity activity;
+ private final String accessToken;
+
+ /** Note text */
+ private final String noteText;
+
+ /** Note longitude */
+ private final double longitude;
+
+ /** Note latitude */
+ private final double latitude;
+
+ /**
+ * Error message, or text of the response returned by OSM
+ * if the request completed
+ */
+ private String errorMsg;
+
+ /**
+ * Either the HTTP result code, or -1 for an internal error
+ */
+ private int resultCode = -1;
+ private final int authorizationErrorResultCode = -2;
+ private final int anotherErrorResultCode = -3;
+ private final int okResultCode = 1;
+
+ // Not using an activity yet
+ public UploadToOpenStreetMapNotesTask(Activity activity, String accessToken, String noteText,
+ double latitude, double longitude) {
+ this.activity = activity;
+ this.accessToken = accessToken;
+ this.noteText = noteText;
+ this.longitude = longitude;
+ this.latitude = latitude;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ try {
+ // Display progress dialog
+ dialog = new ProgressDialog(activity);
+ dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ dialog.setIndeterminate(true);
+ dialog.setTitle(R.string.osm_note_upload);
+
+ dialog.setCancelable(false);
+ dialog.show();
+
+ } catch (Exception e) {
+ Log.e(TAG, "onPreExecute() failed", e);
+ errorMsg = e.getLocalizedMessage();
+ cancel(true);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ switch (resultCode) {
+ case -1:
+ dialog.dismiss();
+ // Internal error, the request didn't start at all
+ DialogUtils.showErrorDialog(activity,
+ activity.getResources().getString(R.string.osm_note_upload_error)
+ + ": " + errorMsg);
+ break;
+ case okResultCode:
+ dialog.dismiss();
+
+ new AlertDialog.Builder(activity)
+ .setTitle(android.R.string.dialog_alert_title)
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .setMessage(R.string.osm_upload_sucess)
+ .setCancelable(true)
+ .setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ activity.finish();
+ }
+ }).create().show();
+
+ break;
+ case authorizationErrorResultCode:
+ dialog.dismiss();
+ Log.e(TAG, "onPostExecute() authorization failed: " + errorMsg + " (" + resultCode + ")");
+ // Authorization issue. Provide a way to clear credentials
+ new AlertDialog.Builder(activity)
+ .setTitle(android.R.string.dialog_alert_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.osm_note_upload_unauthorized)
+ .setCancelable(true)
+ .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Editor editor = PreferenceManager.getDefaultSharedPreferences(activity).edit();
+ editor.remove(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN);
+ editor.commit();
+
+ dialog.dismiss();
+ }
+ }).create().show();
+ break;
+
+ default:
+ // Another error. Display OSM response
+ dialog.dismiss();
+ // Internal error, the request didn't start at all
+ Log.e(TAG, "onPostExecute() default failed: " + errorMsg + " (" + resultCode + ")");
+ DialogUtils.showErrorDialog(activity,
+ activity.getResources().getString(R.string.osm_note_upload_error)
+ + ": " + errorMsg);
+ }
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ OsmConnection osm = new OsmConnection(OpenStreetMapConstants.Api.OSM_API_URL_PATH,
+ OpenStreetMapConstants.OAuth2.USER_AGENT, accessToken);
+
+ try {
+ LatLon point = new OsmLatLon(latitude, longitude);
+ Note note = new NotesApi(osm).create(point, noteText);
+ resultCode = okResultCode;
+ } catch (/*IOException |*/ IllegalArgumentException | OsmBadUserInputException e) {
+ Log.d(TAG, e.getMessage());
+ resultCode = -1; //internal error.
+ } catch (OsmAuthorizationException oae) {
+ Log.d(TAG, "OsmAuthorizationException");
+ resultCode = authorizationErrorResultCode;
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ resultCode = anotherErrorResultCode;
+ }
+ return null;
+ }
+
+}
From bea2013b221e0a4881fbe491a19bcbec474c41ff Mon Sep 17 00:00:00 2001
From: JoseAndresVargas
Date: Fri, 21 Nov 2025 15:12:10 -0600
Subject: [PATCH 38/60] Notes upload UI text added
---
app/src/main/res/layout/osm_note_upload.xml | 65 +++++++++++++++++++++
app/src/main/res/values/strings.xml | 9 +++
2 files changed, 74 insertions(+)
create mode 100644 app/src/main/res/layout/osm_note_upload.xml
diff --git a/app/src/main/res/layout/osm_note_upload.xml b/app/src/main/res/layout/osm_note_upload.xml
new file mode 100644
index 000000000..268319633
--- /dev/null
+++ b/app/src/main/res/layout/osm_note_upload.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 91dbf116b..e2f260507 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -25,6 +25,7 @@
Accur: Comp. head.:Comp. accur.:
+ Upload as OSM noteTrack managerTrack list:
@@ -83,6 +84,14 @@
The OSM server returned an error: ({0}) message {1}Authorization error. Would you like to clear the saved OpenStreetMap credentials?OpenStreetMap upload succeeded
+
+ OpenStreetMap notes upload
+ Error while uploading note
+ Note text
+ Upload
+ Cancel
+ via %1$s %2$s
+ Authorization error. If you previously granted the app permission to upload traces, you must clear your saved credentials in order to authorize the app to upload traces and notes. Would you like to clear your saved OpenStreetMap credentials?Voice recordTake photo
From d37b5bffdaaa57b2fb41f2a1525aaf520de7315c Mon Sep 17 00:00:00 2001
From: JoseAndresVargas
Date: Fri, 21 Nov 2025 15:13:01 -0600
Subject: [PATCH 39/60] In waypoint list, implemented context menu to upload
notes
---
.../net/osmtracker/activity/WaypointList.java | 53 +++++++++++++++++--
.../main/res/menu/waypoint_contextmenu.xml | 6 +++
2 files changed, 54 insertions(+), 5 deletions(-)
create mode 100644 app/src/main/res/menu/waypoint_contextmenu.xml
diff --git a/app/src/main/java/net/osmtracker/activity/WaypointList.java b/app/src/main/java/net/osmtracker/activity/WaypointList.java
index 73dce3f77..42764e99f 100644
--- a/app/src/main/java/net/osmtracker/activity/WaypointList.java
+++ b/app/src/main/java/net/osmtracker/activity/WaypointList.java
@@ -5,18 +5,19 @@
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
+import android.view.ContextMenu;
import android.view.LayoutInflater;
+import android.view.MenuItem;
import android.view.View;
-import android.widget.Button;
-import android.widget.CursorAdapter;
-import android.widget.EditText;
-import android.widget.ListView;
-import android.widget.Toast;
+import android.widget.*;
+import android.widget.AdapterView.AdapterContextMenuInfo;
import androidx.core.content.FileProvider;
import net.osmtracker.R;
import net.osmtracker.db.DataHelper;
@@ -43,6 +44,8 @@ protected void onCreate(Bundle savedInstanceState) {
listView.setFitsSystemWindows(true);
listView.setClipToPadding(false);
listView.setPadding(0, 48, 0, 0);
+
+ registerForContextMenu(listView);
}
@Override
@@ -224,4 +227,44 @@ private boolean isAudioFile(String path) {
return path.endsWith(DataHelper.EXTENSION_3GPP);
}
+ // Where the menu items get defined
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ getMenuInflater().inflate(R.menu.waypoint_contextmenu, menu);
+ }
+
+ // What happens when a menu item is selected
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
+ final Cursor cursor = ((CursorAdapter) getListAdapter()).getCursor();
+ if (!cursor.moveToPosition(info.position)) return super.onContextItemSelected(item);
+
+ // Menu options when you long press on a waypoint
+ switch (item.getItemId()) {
+ case R.id.wplist_contextmenu_osm_note_upload:
+ String noteText = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME));
+ String appName = getString(R.string.app_name);
+ double lat = cursor.getDouble(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE));
+ double lon = cursor.getDouble(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE));
+
+ Intent intent = new Intent(this, OpenStreetMapNotesUpload.class);
+ intent.putExtra("noteContent", noteText);
+ intent.putExtra("appName", appName);
+ // Retrieve app. version number
+ try {
+ PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
+ String version = pi.versionName;
+ intent.putExtra("version", version);
+ } catch (PackageManager.NameNotFoundException nnfe) {
+ // Should not occur
+ }
+ intent.putExtra("latitude", lat);
+ intent.putExtra("longitude", lon);
+ startActivity(intent);
+ return true;
+ }
+ return super.onContextItemSelected(item);
+ }
}
diff --git a/app/src/main/res/menu/waypoint_contextmenu.xml b/app/src/main/res/menu/waypoint_contextmenu.xml
new file mode 100644
index 000000000..0e21fabae
--- /dev/null
+++ b/app/src/main/res/menu/waypoint_contextmenu.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
From cfb3d3c8fc97cc41d463620378b8007f35bf1892 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Sun, 18 Jan 2026 08:19:49 -0600
Subject: [PATCH 40/60] Feature UI: add Text Note to preferences
---
app/src/main/java/net/osmtracker/OSMTracker.java | 2 ++
app/src/main/java/net/osmtracker/activity/Preferences.java | 5 +++++
app/src/main/res/values/strings-preferences.xml | 7 +++++++
app/src/main/res/values/values-preferences.xml | 6 ++++++
app/src/main/res/xml/preferences.xml | 7 +++++++
5 files changed, 27 insertions(+)
diff --git a/app/src/main/java/net/osmtracker/OSMTracker.java b/app/src/main/java/net/osmtracker/OSMTracker.java
index 1d5da5e78..6099efc32 100644
--- a/app/src/main/java/net/osmtracker/OSMTracker.java
+++ b/app/src/main/java/net/osmtracker/OSMTracker.java
@@ -24,6 +24,7 @@ public static final class Preferences {
public final static String KEY_GPS_LOGGING_INTERVAL = "gps.logging.interval";
public final static String KEY_GPS_LOGGING_MIN_DISTANCE = "gps.logging.min_distance";
public final static String KEY_USE_BAROMETER = "gpx.use_barometer";
+ public final static String KEY_USE_NOTES = "gpx.notes";
public final static String KEY_OUTPUT_FILENAME = "gpx.filename";
public final static String KEY_OUTPUT_FILENAME_LABEL = "gpx.filename.label";
public final static String KEY_OUTPUT_ACCURACY = "gpx.accuracy";
@@ -61,6 +62,7 @@ public static final class Preferences {
public final static String VAL_GPS_LOGGING_INTERVAL = "0";
public final static String VAL_GPS_LOGGING_MIN_DISTANCE = "0";
public final static boolean VAL_USE_BAROMETER = false;
+ public final static String VAL_USE_NOTES = "both";
public final static String VAL_OUTPUT_FILENAME_NAME = "name";
public final static String VAL_OUTPUT_FILENAME_NAME_DATE = "name_date";
diff --git a/app/src/main/java/net/osmtracker/activity/Preferences.java b/app/src/main/java/net/osmtracker/activity/Preferences.java
index 4ea78206f..9e637d2ab 100644
--- a/app/src/main/java/net/osmtracker/activity/Preferences.java
+++ b/app/src/main/java/net/osmtracker/activity/Preferences.java
@@ -57,6 +57,11 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
// General settings
setupVoiceRecDuration();
+ // Notes
+ setupListPreference(
+ OSMTracker.Preferences.KEY_USE_NOTES,
+ getString(R.string.prefs_notes_summary)
+ );
// OSM track visibility
setupListPreference(
OSMTracker.Preferences.KEY_OSM_TRACK_VISIBILITY,
diff --git a/app/src/main/res/values/strings-preferences.xml b/app/src/main/res/values/strings-preferences.xml
index 2d73889ac..41414b313 100644
--- a/app/src/main/res/values/strings-preferences.xml
+++ b/app/src/main/res/values/strings-preferences.xml
@@ -12,6 +12,13 @@
Ignore GPS clock and use Android clock for timestampsLog barometric pressure [hPa]Toggling requires track restart
+ Text notes
+ Choose how the values of the Note Text button in layouts will be saved as
+
+ Waypoint
+ OSM Note
+ Both
+ GPS logging intervalUse 0 for the shortest possible (affects battery life)seconds
diff --git a/app/src/main/res/values/values-preferences.xml b/app/src/main/res/values/values-preferences.xml
index eb85f7f2e..af4443a11 100644
--- a/app/src/main/res/values/values-preferences.xml
+++ b/app/src/main/res/values/values-preferences.xml
@@ -62,4 +62,10 @@
Identifiable
+
+ waypoint
+ osm_note
+ both
+
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index a73c72a16..fb05bec8a 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -20,6 +20,13 @@
app:key="gpx.use_barometer"
app:summary="@string/prefs_use_barometer_summary"
app:title="@string/prefs_use_barometer" />
+
Date: Sun, 18 Jan 2026 10:25:31 -0600
Subject: [PATCH 41/60] (WIP) Feature: DB Add Note table, UI trackdetails
include notes, UI notelist actions
---
app/src/main/AndroidManifest.xml | 3 +
.../main/java/net/osmtracker/OSMTracker.java | 11 +-
.../net/osmtracker/activity/NoteList.java | 218 ++++++++++++++++++
.../net/osmtracker/activity/TrackDetail.java | 37 ++-
.../net/osmtracker/activity/WaypointList.java | 53 +----
.../java/net/osmtracker/db/DataHelper.java | 71 +++++-
.../net/osmtracker/db/DatabaseHelper.java | 23 +-
.../net/osmtracker/db/NoteListAdapter.java | 89 +++++++
.../osmtracker/db/TrackContentProvider.java | 69 +++++-
.../java/net/osmtracker/db/model/Track.java | 14 +-
.../EditNoteDialogOnClickListener.java | 24 ++
.../net/osmtracker/service/gps/GPSLogger.java | 31 ++-
.../net/osmtracker/view/TextNoteDialog.java | 78 +++++--
app/src/main/res/layout/edit_note_dialog.xml | 77 +++++++
app/src/main/res/layout/notelist_item.xml | 46 ++++
app/src/main/res/menu/note_contextmenu.xml | 6 +
.../main/res/menu/waypoint_contextmenu.xml | 6 -
app/src/main/res/values/strings.xml | 13 +-
18 files changed, 773 insertions(+), 96 deletions(-)
create mode 100644 app/src/main/java/net/osmtracker/activity/NoteList.java
create mode 100644 app/src/main/java/net/osmtracker/db/NoteListAdapter.java
create mode 100644 app/src/main/java/net/osmtracker/listener/EditNoteDialogOnClickListener.java
create mode 100644 app/src/main/res/layout/edit_note_dialog.xml
create mode 100644 app/src/main/res/layout/notelist_item.xml
create mode 100644 app/src/main/res/menu/note_contextmenu.xml
delete mode 100644 app/src/main/res/menu/waypoint_contextmenu.xml
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f1307ea79..97f6435b6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -58,6 +58,9 @@
+
diff --git a/app/src/main/java/net/osmtracker/OSMTracker.java b/app/src/main/java/net/osmtracker/OSMTracker.java
index 6099efc32..3accb77c9 100644
--- a/app/src/main/java/net/osmtracker/OSMTracker.java
+++ b/app/src/main/java/net/osmtracker/OSMTracker.java
@@ -133,7 +133,16 @@ public static final class Preferences {
* Intent for deleting a previously tracked waypoint
*/
public final static String INTENT_DELETE_WP = OSMTracker.PACKAGE_NAME + ".intent.DELETE_WP";
-
+
+ /**
+ * Intent for tracking a note
+ */
+ public final static String INTENT_TRACK_NOTE = OSMTracker.PACKAGE_NAME + ".intent.TRACK_NOTE";
+ /**
+ * Intent for updating a previously tracked waypoint
+ */
+ public final static String INTENT_UPDATE_NOTE = OSMTracker.PACKAGE_NAME + ".intent.UPDATE_NOTE";
+
/**
* Intent to start tracking
*/
diff --git a/app/src/main/java/net/osmtracker/activity/NoteList.java b/app/src/main/java/net/osmtracker/activity/NoteList.java
new file mode 100644
index 000000000..280d1a695
--- /dev/null
+++ b/app/src/main/java/net/osmtracker/activity/NoteList.java
@@ -0,0 +1,218 @@
+package net.osmtracker.activity;
+
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.Button;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+
+import net.osmtracker.R;
+import net.osmtracker.db.DataHelper;
+import net.osmtracker.db.NoteListAdapter;
+import net.osmtracker.db.TrackContentProvider;
+import net.osmtracker.listener.EditNoteDialogOnClickListener;
+import net.osmtracker.listener.EditWaypointDialogOnClickListener;
+
+/**
+ * Activity that lists the previous notes tracked by the user.
+ *
+ */
+public class NoteList extends ListActivity {
+
+ private static final String TAG = NoteList.class.getSimpleName();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ListView listView = getListView();
+ listView.setFitsSystemWindows(true);
+ listView.setClipToPadding(false);
+ listView.setPadding(0, 48, 0, 0);
+
+ registerForContextMenu(listView);
+ }
+
+ @Override
+ protected void onResume() {
+ long trackId = getIntent().getExtras().getLong(TrackContentProvider.Schema.COL_TRACK_ID);
+
+ Cursor cursor = getContentResolver().query(TrackContentProvider.notesUri(trackId),
+ null, null, null, TrackContentProvider.Schema.COL_TIMESTAMP + " desc");
+ startManagingCursor(cursor);
+ setListAdapter(new NoteListAdapter(NoteList.this, cursor));
+
+ super.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ CursorAdapter adapter = (CursorAdapter) getListAdapter();
+ if (adapter != null) {
+ // Properly close the adapter cursor
+ Cursor cursor = adapter.getCursor();
+ stopManagingCursor(cursor);
+ cursor.close();
+ setListAdapter(null);
+ }
+
+ super.onPause();
+ }
+
+ /**
+ * Handles the selection of a note from the list and opens an edit dialog.
+ * This dialog allows the user to update the note name (text)
+ *
+ * @param l The ListView where the item was clicked.
+ * @param v The view that was clicked.
+ * @param position The position of the clicked item.
+ * @param id The ID of the clicked note.
+ */
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ final Cursor cursor = ((CursorAdapter) getListAdapter()).getCursor();
+ final DataHelper dataHelper = new DataHelper(l.getContext());
+ LayoutInflater inflater = this.getLayoutInflater();
+
+ // Inflate the note edit dialog layout
+ final View editNoteDialog = inflater.inflate(R.layout.edit_note_dialog, null);
+ final EditText editNoteName = editNoteDialog.findViewById(R.id.edit_note_et_name);
+
+ Button buttonUpdate = editNoteDialog.findViewById(R.id.edit_note_button_update);
+ Button buttonDelete = editNoteDialog.findViewById(R.id.edit_note_button_delete);
+ Button buttonOSMUpload = editNoteDialog.findViewById(R.id.edit_note_button_osm_upload);
+ Button buttonCancel = editNoteDialog.findViewById(R.id.edit_note_button_cancel);
+
+ // Retrieve existing note name
+ String oldName = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME));
+ editNoteName.setText(oldName);
+ editNoteName.setSelection(oldName.length());
+
+ // Retrieve waypoint details
+ final long trackId = cursor.getLong(cursor.getColumnIndex(TrackContentProvider.Schema.COL_TRACK_ID));
+ final String uuid = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_UUID));
+
+ //TODO Add visual element to represent if the note was uploaded to OSM
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setCancelable(true);
+ AlertDialog alert = builder.create();
+
+ // Update note text
+ buttonUpdate.setOnClickListener(new EditNoteDialogOnClickListener(alert, null) {
+ @Override
+ public void onClick(View view) {
+ String newName = editNoteName.getText().toString();
+ dataHelper.updateNote(trackId, uuid, newName);
+ alert.dismiss();
+ }
+ });
+
+ // Delete waypoint
+ buttonDelete.setOnClickListener(new EditNoteDialogOnClickListener(alert, cursor) {
+ @Override
+ public void onClick(View view) {
+ new AlertDialog.Builder(NoteList.this)
+ .setTitle(getString(R.string.delete_note_confirm_dialog_title))
+ .setMessage(getString(R.string.delete_note_confirm_dialog_msg))
+ .setPositiveButton(getString(R.string.delete_note_confirm_bt_ok), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dataHelper.deleteNote(uuid);
+ cursor.requery();
+ alert.dismiss();
+ dialog.dismiss();
+ }
+ })
+ .setNegativeButton(getString(R.string.delete_note_confirm_bt_cancel), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .show();
+ }
+ });
+
+ // Upload note text to OpenStreetMap
+ buttonOSMUpload.setOnClickListener(new EditNoteDialogOnClickListener(alert, null) {
+ @Override
+ public void onClick(View view) {
+ uploadNoteToOSM(cursor);
+ }
+ });
+
+ // Cancel button
+ buttonCancel.setOnClickListener(new EditWaypointDialogOnClickListener(alert, null) {
+ @Override
+ public void onClick(View view) {
+ alert.dismiss();
+ }
+ });
+
+ alert.setView(editNoteDialog);
+ alert.show();
+
+ super.onListItemClick(l, v, position, id);
+ }
+
+ // Where the menu items get defined
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ getMenuInflater().inflate(R.menu.note_contextmenu, menu);
+ }
+
+ // What happens when a menu item is selected
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
+ final Cursor cursor = ((CursorAdapter) getListAdapter()).getCursor();
+ if (!cursor.moveToPosition(info.position)) return super.onContextItemSelected(item);
+
+ // Menu options when you long press on a note
+ switch (item.getItemId()) {
+ case R.id.notelist_contextmenu_osm_note_upload:
+ uploadNoteToOSM(cursor);
+ return true;
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ /**
+ * Extracts note data from the cursor and launches the OSM Note Upload activity.
+ * @param cursor The cursor positioned at the selected note.
+ */
+ private void uploadNoteToOSM(Cursor cursor) {
+ String noteText = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME));
+ double lat = cursor.getDouble(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE));
+ double lon = cursor.getDouble(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE));
+
+ Intent intent = new Intent(this, OpenStreetMapNotesUpload.class);
+ intent.putExtra("noteContent", noteText);
+ intent.putExtra("appName", getString(R.string.app_name));
+ intent.putExtra("latitude", lat);
+ intent.putExtra("longitude", lon);
+
+ // Retrieve app version number
+ try {
+ PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
+ intent.putExtra("version", pi.versionName);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Log error or ignore
+ }
+
+ startActivity(intent);
+ }
+}
diff --git a/app/src/main/java/net/osmtracker/activity/TrackDetail.java b/app/src/main/java/net/osmtracker/activity/TrackDetail.java
index d73a40949..4e0d131c2 100644
--- a/app/src/main/java/net/osmtracker/activity/TrackDetail.java
+++ b/app/src/main/java/net/osmtracker/activity/TrackDetail.java
@@ -28,7 +28,6 @@
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
-import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -130,7 +129,7 @@ protected void onResume() {
return; // <--- Early return ---
}
- // Bind WP count, TP count, start date, etc.
+ // Bind WP count, TP count, Note count, start date, etc.
// Fill name-field only if empty (in case changed by user/restored by onRestoreInstanceState)
Track t = Track.build(trackId, cursor, cr, true);
@@ -154,6 +153,12 @@ protected void onResume() {
map.put(ITEM_VALUE, Integer.toString(t.getTpCount()));
data.add(map);
+ // Notes count
+ map = new HashMap();
+ map.put(ITEM_KEY, getResources().getString(R.string.trackmgr_notes_count));
+ map.put(ITEM_VALUE, Integer.toString(t.getNoteCount()));
+ data.add(map);
+
// Start date
map = new HashMap();
map.put(ITEM_KEY, getResources().getString(R.string.trackdetail_startdate));
@@ -336,22 +341,34 @@ public void onRequestPermissionsResult(int requestCode,
}
/**
- * Handle clicks on list items; for Waypoint count, show this track's list of waypoints ({@link WaypointList}).
+ * Handle clicks on list items; for Waypoint count and note count, show this track's list of
+ * waypoints ({@link WaypointList}) or notes ({@link NoteList}).
* Ignore all other clicks.
* @param position Item number in the list; this method assumes Waypoint count is position 0 (first item).
*/
public void onItemClick(AdapterView> parent, View view, final int position, final long rowid) {
- if (position != WP_COUNT_INDEX) {
- return;
+ // Get the Map associated with the clicked row
+ Map clickedItem;
+ clickedItem = (Map) parent.getItemAtPosition(position);
+
+ if (clickedItem != null) {
+ String key = clickedItem.get(ITEM_KEY);
+
+ // You can now logic based on the text if positions are dynamic
+ if (getString(R.string.trackmgr_waypoints_count).equals(key)) {
+ Intent i = new Intent(this, WaypointList.class);
+ i.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, trackId);
+ startActivity(i);
+ } else if (getString(R.string.trackmgr_notes_count).equals(key)) {
+ Intent i = new Intent(this, NoteList.class);
+ i.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, trackId);
+ startActivity(i);
+ }
}
-
- Intent i = new Intent(this, WaypointList.class);
- i.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, trackId);
- startActivity(i);
}
/**
- * Extend SimpleAdapter so we can underline the clickable Waypoint count.
+ * Extend SimpleAdapter so we can underline the clickable Waypoint and Note count.
* Always uses R.layout.trackdetail_item as its list item resource.
*/
private class TrackDetailSimpleAdapter extends SimpleAdapter
diff --git a/app/src/main/java/net/osmtracker/activity/WaypointList.java b/app/src/main/java/net/osmtracker/activity/WaypointList.java
index 42764e99f..73dce3f77 100644
--- a/app/src/main/java/net/osmtracker/activity/WaypointList.java
+++ b/app/src/main/java/net/osmtracker/activity/WaypointList.java
@@ -5,19 +5,18 @@
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
-import android.view.ContextMenu;
import android.view.LayoutInflater;
-import android.view.MenuItem;
import android.view.View;
-import android.widget.*;
-import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.Button;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.Toast;
import androidx.core.content.FileProvider;
import net.osmtracker.R;
import net.osmtracker.db.DataHelper;
@@ -44,8 +43,6 @@ protected void onCreate(Bundle savedInstanceState) {
listView.setFitsSystemWindows(true);
listView.setClipToPadding(false);
listView.setPadding(0, 48, 0, 0);
-
- registerForContextMenu(listView);
}
@Override
@@ -227,44 +224,4 @@ private boolean isAudioFile(String path) {
return path.endsWith(DataHelper.EXTENSION_3GPP);
}
- // Where the menu items get defined
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- super.onCreateContextMenu(menu, v, menuInfo);
- getMenuInflater().inflate(R.menu.waypoint_contextmenu, menu);
- }
-
- // What happens when a menu item is selected
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
- final Cursor cursor = ((CursorAdapter) getListAdapter()).getCursor();
- if (!cursor.moveToPosition(info.position)) return super.onContextItemSelected(item);
-
- // Menu options when you long press on a waypoint
- switch (item.getItemId()) {
- case R.id.wplist_contextmenu_osm_note_upload:
- String noteText = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME));
- String appName = getString(R.string.app_name);
- double lat = cursor.getDouble(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE));
- double lon = cursor.getDouble(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE));
-
- Intent intent = new Intent(this, OpenStreetMapNotesUpload.class);
- intent.putExtra("noteContent", noteText);
- intent.putExtra("appName", appName);
- // Retrieve app. version number
- try {
- PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
- String version = pi.versionName;
- intent.putExtra("version", version);
- } catch (PackageManager.NameNotFoundException nnfe) {
- // Should not occur
- }
- intent.putExtra("latitude", lat);
- intent.putExtra("longitude", lon);
- startActivity(intent);
- return true;
- }
- return super.onContextItemSelected(item);
- }
}
diff --git a/app/src/main/java/net/osmtracker/db/DataHelper.java b/app/src/main/java/net/osmtracker/db/DataHelper.java
index fa50d8a56..e86991245 100644
--- a/app/src/main/java/net/osmtracker/db/DataHelper.java
+++ b/app/src/main/java/net/osmtracker/db/DataHelper.java
@@ -307,7 +307,76 @@ public void deleteWayPoint(String uuid, String filepath) {
Log.v(TAG, "File deleted: " + filepath);
}
}
-
+
+ /**
+ * Tracks a note point with link
+ *
+ * @param trackId Id of the track
+ * @param location Location of note
+ * @param name text of the note
+ * @param uuid Unique id of the note
+ */
+ public void trackNote(long trackId, Location location, String name, String uuid) {
+ Log.d(TAG, "Tracking note '" + name + "', track=" + trackId + ", uuid=" + uuid
+ + ", nbSatellites=" + location.getExtras().getInt("satellites")
+ + ", location=" + location);
+
+ ContentValues values = new ContentValues();
+ values.put(TrackContentProvider.Schema.COL_TRACK_ID, trackId);
+ values.put(TrackContentProvider.Schema.COL_LATITUDE, location.getLatitude());
+ values.put(TrackContentProvider.Schema.COL_LONGITUDE, location.getLongitude());
+ values.put(TrackContentProvider.Schema.COL_NAME, name);
+
+ if (uuid != null) {
+ values.put(TrackContentProvider.Schema.COL_UUID, uuid);
+ }
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ if (prefs.getBoolean(OSMTracker.Preferences.KEY_GPS_IGNORE_CLOCK, OSMTracker.Preferences.VAL_GPS_IGNORE_CLOCK)) {
+ // Use OS clock
+ values.put(TrackContentProvider.Schema.COL_TIMESTAMP, System.currentTimeMillis());
+ } else {
+ // Use GPS clock
+ values.put(TrackContentProvider.Schema.COL_TIMESTAMP, location.getTime());
+ }
+
+ Uri trackUri = ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, trackId);
+ contentResolver.insert(Uri.withAppendedPath(trackUri,
+ TrackContentProvider.Schema.TBL_NOTE + "s"), values);
+ }
+
+ /**
+ * Updates a note
+ *
+ * @param trackId Id of the track
+ * @param uuid Unique ID of the target waypoint
+ * @param name New text value for the note
+ */
+ public void updateNote(long trackId, String uuid, String name) {
+ Log.v(TAG, "Updating note with uuid '" + uuid + "'. New values: name='" + name);
+ if (uuid != null) {
+ ContentValues values = new ContentValues();
+ if (name != null) {
+ values.put(TrackContentProvider.Schema.COL_NAME, name);
+ }
+
+ Uri trackUri = ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, trackId);
+ contentResolver.update(Uri.withAppendedPath(trackUri, TrackContentProvider.Schema.TBL_NOTE + "s"), values,
+ "uuid = ?", new String[] { uuid });
+ }
+ }
+
+ /**
+ * Deletes a note
+ *
+ * @param uuid Unique ID of the target waypoint
+ */
+ public void deleteNote(String uuid) {
+ Log.v(TAG, "Deleting note with uuid '" + uuid);
+ if (uuid != null) {
+ contentResolver.delete(Uri.withAppendedPath(TrackContentProvider.CONTENT_URI_WAYPOINT_UUID, uuid), null, null);
+ }
+ }
/**
* Stop tracking by making the track inactive
diff --git a/app/src/main/java/net/osmtracker/db/DatabaseHelper.java b/app/src/main/java/net/osmtracker/db/DatabaseHelper.java
index 45775608c..fda2032c5 100644
--- a/app/src/main/java/net/osmtracker/db/DatabaseHelper.java
+++ b/app/src/main/java/net/osmtracker/db/DatabaseHelper.java
@@ -98,6 +98,22 @@ public class DatabaseHelper extends SQLiteOpenHelper {
+ TrackContentProvider.Schema.COL_OSM_UPLOAD_DATE + " long" // null indicates not yet uploaded
+ ")";
+ /**
+ * SQL for creating table NOTE
+ * @since 18 (version 2026.02)
+ */
+ private static final String SQL_CREATE_TABLE_NOTE = ""
+ + "create table " + TrackContentProvider.Schema.TBL_NOTE + " ("
+ + TrackContentProvider.Schema.COL_ID + " integer primary key autoincrement,"
+ + TrackContentProvider.Schema.COL_TRACK_ID + " integer not null,"
+ + TrackContentProvider.Schema.COL_UUID + " text,"
+ + TrackContentProvider.Schema.COL_LATITUDE + " double not null,"
+ + TrackContentProvider.Schema.COL_LONGITUDE + " double not null,"
+ + TrackContentProvider.Schema.COL_TIMESTAMP + " long not null,"
+ + TrackContentProvider.Schema.COL_NAME + " text,"
+ + TrackContentProvider.Schema.COL_OSM_UPLOAD_DATE + " long" // null indicates not yet uploaded
+ + ")";
+
/**
* Database name.
*/
@@ -123,9 +139,10 @@ public class DatabaseHelper extends SQLiteOpenHelper {
* v16: add TBL_TRACKPOINT.COL_COMPASS, TBL_TRACKPOINT.COL_COMPASS_ACCURACY,
* TBL_WAYPOINT.COL_COMPASS and TBL_WAYPOINT.COL_COMPASS_ACCURACY
* v17: add TBL_TRACKPOINT.COL_ATMOSPHERIC_PRESSURE and TBL_WAYPOINT.COL_ATMOSPHERIC_PRESSURE
+ * v18: add TBL_NOTE
*
*/
- private static final int DB_VERSION = 17;
+ private static final int DB_VERSION = 18;
private Context context;
@@ -144,6 +161,8 @@ public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_IDX_WAYPOINT_TRACK);
db.execSQL("drop table if exists " + TrackContentProvider.Schema.TBL_TRACK);
db.execSQL(SQL_CREATE_TABLE_TRACK);
+ db.execSQL("drop table if exists " + TrackContentProvider.Schema.TBL_NOTE);
+ db.execSQL(SQL_CREATE_TABLE_NOTE);
}
@Override
@@ -181,6 +200,8 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
case 16:
db.execSQL("alter table " + TrackContentProvider.Schema.TBL_TRACKPOINT + " add column " + TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE + " double null");
db.execSQL("alter table " + TrackContentProvider.Schema.TBL_WAYPOINT + " add column " + TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE + " double null");
+ case 17:
+ db.execSQL(SQL_CREATE_TABLE_NOTE);
}
}
diff --git a/app/src/main/java/net/osmtracker/db/NoteListAdapter.java b/app/src/main/java/net/osmtracker/db/NoteListAdapter.java
new file mode 100644
index 000000000..88e71300e
--- /dev/null
+++ b/app/src/main/java/net/osmtracker/db/NoteListAdapter.java
@@ -0,0 +1,89 @@
+package net.osmtracker.db;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.TableLayout;
+import android.widget.TextView;
+
+import net.osmtracker.R;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * Adapter for the note list. Gets notes from database.
+ *
+ */
+public class NoteListAdapter extends CursorAdapter {
+
+ /**
+ * Date formatter
+ */
+ public static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("HH:mm:ss 'UTC'");
+ static {
+ DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param context
+ * Application context
+ * @param c
+ * {@link Cursor} to data
+ */
+ public NoteListAdapter(Context context, Cursor c) {
+ super(context, c);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ TableLayout tl = (TableLayout) view;
+ bind(cursor, tl, context);
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup vg) {
+ TableLayout tl = (TableLayout) LayoutInflater.from(vg.getContext()).inflate(R.layout.notelist_item,
+ vg, false);
+ return bind(cursor, tl, context);
+ }
+
+ /**
+ * Do the binding between data and item view.
+ *
+ * @param cursor Cursor to pull data
+ * @param tl RelativeView representing one item
+ * @param context Context, to get resources
+ * @return The relative view with data bound.
+ */
+ private View bind(Cursor cursor, TableLayout tl, Context context) {
+ TextView vName = tl.findViewById(R.id.notelist_item_name);
+ TextView vLocation = tl.findViewById(R.id.notelist_item_location);
+ TextView vTimestamp = tl.findViewById(R.id.notelist_item_timestamp);
+
+ // Bind name
+ String name = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME));
+ vName.setText(name);
+
+ // Bind location
+ StringBuffer locationAsString = new StringBuffer();
+ locationAsString.append(context.getResources().getString(R.string.wplist_latitude)
+ + cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE)));
+ locationAsString.append(", " + context.getResources().getString(R.string.wplist_longitude)
+ + cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE)));
+
+ vLocation.setText(locationAsString.toString());
+
+ // Bind timestamp
+ Date ts = new Date(cursor.getLong(cursor.getColumnIndex(TrackContentProvider.Schema.COL_TIMESTAMP)));
+ vTimestamp.setText(DATE_FORMATTER.format(ts));
+ return tl;
+ }
+
+}
diff --git a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java
index 38f344221..9c0b05982 100644
--- a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java
+++ b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java
@@ -51,6 +51,11 @@ public class TrackContentProvider extends ContentProvider {
*/
public static final Uri CONTENT_URI_WAYPOINT_UUID = Uri.parse("content://" + AUTHORITY + "/" + Schema.TBL_WAYPOINT + "/uuid");
+ /**
+ * Uri for a specific note by uuid
+ */
+ public static final Uri CONTENT_URI_NOTE_UUID = Uri.parse("content://" + AUTHORITY + "/" + Schema.TBL_NOTE + "/uuid");
+
/**
* Uri for a specific trackpoint
*/
@@ -76,7 +81,16 @@ public class TrackContentProvider extends ContentProvider {
Schema.COL_OSM_VISIBILITY,
Schema.COL_START_DATE,
"count(" + Schema.TBL_TRACKPOINT + "." + Schema.COL_ID + ") as " + Schema.COL_TRACKPOINT_COUNT,
- "(SELECT count("+Schema.TBL_WAYPOINT+"."+Schema.COL_TRACK_ID+") FROM "+Schema.TBL_WAYPOINT+" WHERE "+Schema.TBL_WAYPOINT+"."+Schema.COL_TRACK_ID+" = " + Schema.TBL_TRACK + "." + Schema.COL_ID + ") as " + Schema.COL_WAYPOINT_COUNT
+ "(SELECT count(" + Schema.TBL_WAYPOINT + "." + Schema.COL_TRACK_ID +") " +
+ "FROM " + Schema.TBL_WAYPOINT + " " +
+ "WHERE " + Schema.TBL_WAYPOINT + "." + Schema.COL_TRACK_ID +" " +
+ "= " + Schema.TBL_TRACK + "." + Schema.COL_ID +") " +
+ "as " + Schema.COL_WAYPOINT_COUNT,
+ "(SELECT count(" + Schema.TBL_NOTE + "." + Schema.COL_TRACK_ID +") " +
+ "FROM " + Schema.TBL_NOTE + " " +
+ "WHERE " + Schema.TBL_NOTE + "." + Schema.COL_TRACK_ID +" " +
+ "= " + Schema.TBL_TRACK + "." + Schema.COL_ID +") " +
+ "as " + Schema.COL_NOTE_COUNT,
};
/**
@@ -97,6 +111,7 @@ public class TrackContentProvider extends ContentProvider {
uriMatcher.addURI(AUTHORITY, Schema.TBL_TRACK + "/#/start", Schema.URI_CODE_TRACK_START);
uriMatcher.addURI(AUTHORITY, Schema.TBL_TRACK + "/#/end", Schema.URI_CODE_TRACK_END);
uriMatcher.addURI(AUTHORITY, Schema.TBL_TRACK + "/#/" + Schema.TBL_WAYPOINT + "s", Schema.URI_CODE_TRACK_WAYPOINTS);
+ uriMatcher.addURI(AUTHORITY, Schema.TBL_TRACK + "/#/" + Schema.TBL_NOTE + "s", Schema.URI_CODE_TRACK_NOTES);
uriMatcher.addURI(AUTHORITY, Schema.TBL_TRACK + "/#/" + Schema.TBL_TRACKPOINT + "s", Schema.URI_CODE_TRACK_TRACKPOINTS);
uriMatcher.addURI(AUTHORITY, Schema.TBL_WAYPOINT + "/#", Schema.URI_CODE_WAYPOINT_ID);
uriMatcher.addURI(AUTHORITY, Schema.TBL_WAYPOINT + "/uuid/*", Schema.URI_CODE_WAYPOINT_UUID);
@@ -121,7 +136,17 @@ public static final Uri waypointsUri(long trackId) {
public static final Uri waypointUri(long waypointId) {
return ContentUris.withAppendedId(CONTENT_URI_WAYPOINT, waypointId);
}
-
+
+ /**
+ * @param trackId target track id
+ * @return Uri for the notes of the track
+ */
+ public static final Uri notesUri(long trackId) {
+ return Uri.withAppendedPath(
+ ContentUris.withAppendedId(CONTENT_URI_TRACK, trackId),
+ Schema.TBL_NOTE + "s" );
+ }
+
/**
* @param trackId target track id
* @return Uri for the trackpoints of the track
@@ -207,7 +232,7 @@ public int delete(Uri uri, String selection, String[] selectionArgs) {
/**
* Match and get the URI type, if recognized:
* Matches {@link Schema#URI_CODE_TRACK_TRACKPOINTS}, {@link Schema#URI_CODE_TRACK_WAYPOINTS},
- * or {@link Schema#URI_CODE_TRACK}.
+ * {link Schema#URI_CODE_TRACK_NOTES} or {@link Schema#URI_CODE_TRACK}.
* @throws IllegalArgumentException if not matched
*/
@Override
@@ -222,6 +247,9 @@ public String getType(Uri uri) throws IllegalArgumentException {
case Schema.URI_CODE_TRACK_WAYPOINTS:
return ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd." + OSMTracker.class.getPackage() + "."
+ Schema.TBL_WAYPOINT;
+ case Schema.URI_CODE_TRACK_NOTES:
+ return ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd." + OSMTracker.class.getPackage() + "."
+ + Schema.TBL_NOTE;
case Schema.URI_CODE_TRACK:
return ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd." + OSMTracker.class.getPackage() + "."
+ Schema.TBL_TRACK;
@@ -268,6 +296,19 @@ public Uri insert(Uri uri, ContentValues values) {
+ Schema.COL_LATITUDE + ", " + Schema.COL_TIMESTAMP);
}
break;
+ case Schema.URI_CODE_TRACK_NOTES:
+ // Check that mandatory columns are present.
+ if (values.containsKey(Schema.COL_TRACK_ID) && values.containsKey(Schema.COL_LONGITUDE)
+ && values.containsKey(Schema.COL_LATITUDE) && values.containsKey(Schema.COL_TIMESTAMP) ) {
+
+ long rowId = dbHelper.getWritableDatabase().insert(Schema.TBL_NOTE, null, values);
+ if (rowId > 0) {
+ Uri noteUri = ContentUris.withAppendedId(uri, rowId);
+ getContext().getContentResolver().notifyChange(noteUri, null);
+ return noteUri;
+ }
+ }
+ break;
case Schema.URI_CODE_TRACK:
if (values.containsKey(Schema.COL_START_DATE)) {
long rowId = dbHelper.getWritableDatabase().insert(Schema.TBL_TRACK, null, values);
@@ -333,6 +374,16 @@ public Cursor query(Uri uri, String[] projection, String selectionIn, String[] s
selection = Schema.COL_TRACK_ID + " = ?";
selectionArgs = new String[] {trackId};
break;
+ case Schema.URI_CODE_TRACK_NOTES:
+ if (selectionIn != null || selectionArgsIn != null) {
+ // Any selection/selectionArgs will be ignored
+ throw new UnsupportedOperationException();
+ }
+ trackId = uri.getPathSegments().get(1);
+ qb.setTables(Schema.TBL_NOTE);
+ selection = Schema.COL_TRACK_ID + " = ?";
+ selectionArgs = new String[] {trackId};
+ break;
case Schema.URI_CODE_TRACK_START:
if (selectionIn != null || selectionArgsIn != null) {
// Any selection/selectionArgs will be ignored
@@ -430,6 +481,13 @@ public int update(Uri uri, ContentValues values, String selectionIn, String[] se
}
table = Schema.TBL_WAYPOINT;
break;
+ case Schema.URI_CODE_TRACK_NOTES:
+ if (selectionIn == null || selectionArgsIn == null) {
+ // Caller must narrow to a specific waypoint
+ throw new IllegalArgumentException();
+ }
+ table = Schema.TBL_NOTE;
+ break;
case Schema.URI_CODE_TRACK_ID:
if (selectionIn != null || selectionArgsIn != null) {
// Any selection/selectionArgs will be ignored
@@ -470,6 +528,7 @@ public int update(Uri uri, ContentValues values, String selectionIn, String[] se
public static final class Schema {
public static final String TBL_TRACKPOINT = "trackpoint";
public static final String TBL_WAYPOINT = "waypoint";
+ public static final String TBL_NOTE = "note";
public static final String TBL_TRACK = "track";
public static final String COL_ID = "_id";
public static final String COL_TRACK_ID = "track_id";
@@ -499,7 +558,8 @@ public static final class Schema {
// virtual colums that are used in some sqls but dont exist in database
public static final String COL_TRACKPOINT_COUNT = "tp_count";
public static final String COL_WAYPOINT_COUNT = "wp_count";
-
+ public static final String COL_NOTE_COUNT = "note_count";
+
// Codes for UriMatcher
public static final int URI_CODE_TRACK = 3;
public static final int URI_CODE_TRACK_ID = 4;
@@ -511,6 +571,7 @@ public static final class Schema {
public static final int URI_CODE_TRACK_END = 10;
public static final int URI_CODE_WAYPOINT_ID = 11;
public static final int URI_CODE_TRACKPOINT_ID = 12;
+ public static final int URI_CODE_TRACK_NOTES = 13;
public static final int VAL_TRACK_ACTIVE = 1;
diff --git a/app/src/main/java/net/osmtracker/db/model/Track.java b/app/src/main/java/net/osmtracker/db/model/Track.java
index b74fcfe3d..03261b482 100644
--- a/app/src/main/java/net/osmtracker/db/model/Track.java
+++ b/app/src/main/java/net/osmtracker/db/model/Track.java
@@ -51,7 +51,7 @@ public static OSMVisibility fromPosition(int position) {
private String description;
private OSMVisibility visibility;
private List tags = new ArrayList();
- private int tpCount, wpCount;
+ private int tpCount, wpCount, noteCount;
private long trackDate;
private long trackId;
@@ -92,7 +92,9 @@ public static Track build(final long trackId, Cursor tc, ContentResolver cr, boo
out.tpCount = tc.getInt(tc.getColumnIndex(TrackContentProvider.Schema.COL_TRACKPOINT_COUNT));
out.wpCount = tc.getInt(tc.getColumnIndex(TrackContentProvider.Schema.COL_WAYPOINT_COUNT));
-
+
+ out.noteCount = tc.getInt(tc.getColumnIndex(TrackContentProvider.Schema.COL_NOTE_COUNT));
+
if(withExtraInformation){
out.readExtraInformation();
}
@@ -145,6 +147,10 @@ public void setWpCount(int wpCount) {
this.wpCount = wpCount;
}
+ public void setNoteCount(int noteCount) {
+ this.noteCount = noteCount;
+ }
+
public void setTracktDate(long tracktDate) {
this.trackDate = tracktDate;
}
@@ -191,6 +197,10 @@ public Integer getTpCount() {
return tpCount;
}
+ public Integer getNoteCount() {
+ return noteCount;
+ }
+
// @deprecated
public String getDisplayName() {
if (name != null && name.length() > 0) {
diff --git a/app/src/main/java/net/osmtracker/listener/EditNoteDialogOnClickListener.java b/app/src/main/java/net/osmtracker/listener/EditNoteDialogOnClickListener.java
new file mode 100644
index 000000000..3152cc9a7
--- /dev/null
+++ b/app/src/main/java/net/osmtracker/listener/EditNoteDialogOnClickListener.java
@@ -0,0 +1,24 @@
+package net.osmtracker.listener;
+
+import android.app.AlertDialog;
+import android.database.Cursor;
+import android.view.View;
+
+/**
+ * Class that implements an OnClickListener to display an edit note dialog.
+ */
+public class EditNoteDialogOnClickListener implements View.OnClickListener {
+
+ private Cursor cursor;
+
+ protected AlertDialog alert;
+
+ protected EditNoteDialogOnClickListener(AlertDialog alert, Cursor cu) {
+ this.cursor = cu; // Assigns the received cursor to the class attribute
+ this.alert = alert; // Assigns the received alert to the class attribute
+ }
+
+ @Override
+ public void onClick(View view) {
+ }
+}
diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java b/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java
index a6a735fb2..7f180541a 100644
--- a/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java
+++ b/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java
@@ -105,7 +105,7 @@ public class GPSLogger extends Service implements LocationListener {
private PressureListener pressureListener = new PressureListener();
/**
- * Receives Intent for way point tracking, and stop/start logging.
+ * Receives Intent for way point and notes tracking, and stop/start logging.
*/
private BroadcastReceiver receiver = new BroadcastReceiver() {
@@ -158,6 +158,33 @@ public void onReceive(Context context, Intent intent) {
catch(NullPointerException ne){}
dataHelper.deleteWayPoint(uuid, filePath);
}
+ } else if (OSMTracker.INTENT_TRACK_NOTE.equals(intent.getAction())) {
+ // Track a note
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ // because of the gps logging interval our last fix could be very old
+ // so we'll request the last known location from the gps provider
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
+ lastLocation = lmgr.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+ if (lastLocation != null) {
+ //TODO: CHECK THIS
+ long trackId = extras.getLong(TrackContentProvider.Schema.COL_TRACK_ID);
+ String uuid = extras.getString(OSMTracker.INTENT_KEY_UUID);
+ String name = extras.getString(OSMTracker.INTENT_KEY_NAME);
+
+ dataHelper.trackNote(trackId, lastLocation, name, uuid);
+ }
+ }
+ }
+ } else if (OSMTracker.INTENT_UPDATE_NOTE.equals(intent.getAction())) {
+ // Update an existing note
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ long trackId = extras.getLong(TrackContentProvider.Schema.COL_TRACK_ID);
+ String uuid = extras.getString(OSMTracker.INTENT_KEY_UUID);
+ String name = extras.getString(OSMTracker.INTENT_KEY_NAME);
+ dataHelper.updateNote(trackId, uuid, name);
+ }
} else if (OSMTracker.INTENT_START_TRACKING.equals(intent.getAction())) {
Bundle extras = intent.getExtras();
if (extras != null) {
@@ -227,6 +254,8 @@ public void onCreate() {
IntentFilter filter = new IntentFilter();
filter.addAction(OSMTracker.INTENT_TRACK_WP);
filter.addAction(OSMTracker.INTENT_UPDATE_WP);
+ filter.addAction(OSMTracker.INTENT_TRACK_NOTE);
+ filter.addAction(OSMTracker.INTENT_UPDATE_NOTE);
filter.addAction(OSMTracker.INTENT_DELETE_WP);
filter.addAction(OSMTracker.INTENT_START_TRACKING);
filter.addAction(OSMTracker.INTENT_STOP_TRACKING);
diff --git a/app/src/main/java/net/osmtracker/view/TextNoteDialog.java b/app/src/main/java/net/osmtracker/view/TextNoteDialog.java
index 6064dea12..78195c4bc 100644
--- a/app/src/main/java/net/osmtracker/view/TextNoteDialog.java
+++ b/app/src/main/java/net/osmtracker/view/TextNoteDialog.java
@@ -40,12 +40,20 @@ public class TextNoteDialog extends AlertDialog {
* Unique identifier of the waypoint this dialog working on
*/
private String wayPointUuid = null;
-
+
+ // Unique identifier of the note this dialog working on
+ private String noteUuid = null;
+
/**
* Id of the track the dialog will add this waypoint to
*/
private long wayPointTrackId;
-
+
+ /**
+ * Id of the track the dialog will add this OSM Text note to
+ */
+ private long noteTrackId;
+
private Context context;
public TextNoteDialog(Context context, long trackId) {
@@ -53,7 +61,8 @@ public TextNoteDialog(Context context, long trackId) {
this.context = context;
this.wayPointTrackId = trackId;
-
+ this.noteTrackId = trackId;
+
// Text edit control for user input
input = new EditText(context);
@@ -62,26 +71,38 @@ public TextNoteDialog(Context context, long trackId) {
this.setCancelable(true);
this.setView(input);
- this.setButton(context.getResources().getString(android.R.string.ok), new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- // Track waypoint with user input text
- Intent intent = new Intent(OSMTracker.INTENT_UPDATE_WP);
- intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, TextNoteDialog.this.wayPointTrackId);
- intent.putExtra(OSMTracker.INTENT_KEY_NAME, input.getText().toString());
- intent.putExtra(OSMTracker.INTENT_KEY_UUID, TextNoteDialog.this.wayPointUuid);
- intent.setPackage(getContext().getPackageName());
- context.sendBroadcast(intent);
- }
+ this.setButton(DialogInterface.BUTTON_POSITIVE,
+ context.getString(android.R.string.ok),
+ (dialog, which) -> {
+
+
+ // CHECK Prefs to select if note is added as trackpoint, note or both
+ String noteText = input.getText().toString();
+
+ // Track waypoint with user input text
+ Intent intent = new Intent(OSMTracker.INTENT_UPDATE_WP);
+ intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, wayPointTrackId);
+ intent.putExtra(OSMTracker.INTENT_KEY_NAME, input.getText().toString());
+ intent.putExtra(OSMTracker.INTENT_KEY_UUID, TextNoteDialog.this.wayPointUuid);
+ intent.setPackage(getContext().getPackageName());
+ context.sendBroadcast(intent);
+
+ // For testing, this will be both (tp and note) case pref.
+ Intent noteIntent = new Intent(OSMTracker.INTENT_UPDATE_NOTE);
+ noteIntent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, noteTrackId);
+ noteIntent.putExtra(OSMTracker.INTENT_KEY_NAME, noteText);
+ noteIntent.putExtra(OSMTracker.INTENT_KEY_UUID, wayPointUuid);
+ noteIntent.setPackage(getContext().getPackageName());
+ context.sendBroadcast(noteIntent);
+
});
- this.setButton2(context.getResources().getString(android.R.string.cancel), new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- // cancel the dialog
- dialog.cancel();
- }
- });
+ this.setButton(DialogInterface.BUTTON_NEGATIVE,
+ context.getResources().getString(android.R.string.cancel),
+ (dialog, which) -> {
+ // cancel the dialog
+ dialog.cancel();
+ });
this.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
@@ -113,6 +134,21 @@ protected void onStart() {
context.sendBroadcast(intent);
}
+ if (noteUuid == null) {
+
+ // there is no UUID set for the note we're working on
+ // so we need to generate a UUID and track this note
+ noteUuid = UUID.randomUUID().toString();
+ Intent noteIntent = new Intent(OSMTracker.INTENT_TRACK_NOTE);
+ noteIntent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, noteTrackId);
+ noteIntent.putExtra(OSMTracker.INTENT_KEY_UUID, noteUuid);
+ noteIntent.putExtra(OSMTracker.INTENT_KEY_NAME, context.getResources().getString(R.string.gpsstatus_record_textnote));
+ noteIntent.setPackage(getContext().getPackageName());
+ context.sendBroadcast(noteIntent);
+
+
+ }
+
getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
super.onStart();
diff --git a/app/src/main/res/layout/edit_note_dialog.xml b/app/src/main/res/layout/edit_note_dialog.xml
new file mode 100644
index 000000000..5ecdfa4f1
--- /dev/null
+++ b/app/src/main/res/layout/edit_note_dialog.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/notelist_item.xml b/app/src/main/res/layout/notelist_item.xml
new file mode 100644
index 000000000..94fd72466
--- /dev/null
+++ b/app/src/main/res/layout/notelist_item.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/note_contextmenu.xml b/app/src/main/res/menu/note_contextmenu.xml
new file mode 100644
index 000000000..b02c11f7b
--- /dev/null
+++ b/app/src/main/res/menu/note_contextmenu.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/waypoint_contextmenu.xml b/app/src/main/res/menu/waypoint_contextmenu.xml
deleted file mode 100644
index 0e21fabae..000000000
--- a/app/src/main/res/menu/waypoint_contextmenu.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e2f260507..381506590 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -25,12 +25,12 @@
Accur: Comp. head.:Comp. accur.:
- Upload as OSM noteTrack managerTrack list:Waypoints: Trackpoints:
+ Notes: You don\'t have any tracks.Press to record a new track.Unable to create a new track: {0}
@@ -107,6 +107,17 @@
Delete this waypoint?DeleteCancel
+
+ Note Name/text
+ Save
+ Delete
+ Cancel
+ Delete note
+ Delete this note?
+ Delete
+ Cancel
+ Note list
+ Upload as OSM noteSettingsWaypoints
From 3f86c52a267de07f4b027e82d0a65d74605767cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Sun, 18 Jan 2026 11:17:56 -0600
Subject: [PATCH 42/60] Feature: Use Text Note preferences to save the note
---
.../net/osmtracker/view/TextNoteDialog.java | 115 ++++++++++++------
1 file changed, 75 insertions(+), 40 deletions(-)
diff --git a/app/src/main/java/net/osmtracker/view/TextNoteDialog.java b/app/src/main/java/net/osmtracker/view/TextNoteDialog.java
index 78195c4bc..c4cdf1835 100644
--- a/app/src/main/java/net/osmtracker/view/TextNoteDialog.java
+++ b/app/src/main/java/net/osmtracker/view/TextNoteDialog.java
@@ -4,10 +4,13 @@
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.WindowManager.LayoutParams;
import android.widget.EditText;
+import androidx.preference.PreferenceManager;
+
import net.osmtracker.OSMTracker;
import net.osmtracker.R;
import net.osmtracker.db.TrackContentProvider;
@@ -25,6 +28,7 @@ public class TextNoteDialog extends AlertDialog {
* bundle key for waypoint uuid
*/
private static final String KEY_WAYPOINT_UUID = "WAYPOINT_UUID";
+ private static final String KEY_NOTE_UUID = "NOTE_UUID";
/**
* bundle key for waypoints track id
@@ -54,6 +58,8 @@ public class TextNoteDialog extends AlertDialog {
*/
private long noteTrackId;
+ boolean saveAsWayPoint, saveAsNote = false;
+
private Context context;
public TextNoteDialog(Context context, long trackId) {
@@ -76,25 +82,26 @@ public TextNoteDialog(Context context, long trackId) {
(dialog, which) -> {
- // CHECK Prefs to select if note is added as trackpoint, note or both
String noteText = input.getText().toString();
- // Track waypoint with user input text
- Intent intent = new Intent(OSMTracker.INTENT_UPDATE_WP);
- intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, wayPointTrackId);
- intent.putExtra(OSMTracker.INTENT_KEY_NAME, input.getText().toString());
- intent.putExtra(OSMTracker.INTENT_KEY_UUID, TextNoteDialog.this.wayPointUuid);
- intent.setPackage(getContext().getPackageName());
- context.sendBroadcast(intent);
-
- // For testing, this will be both (tp and note) case pref.
- Intent noteIntent = new Intent(OSMTracker.INTENT_UPDATE_NOTE);
- noteIntent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, noteTrackId);
- noteIntent.putExtra(OSMTracker.INTENT_KEY_NAME, noteText);
- noteIntent.putExtra(OSMTracker.INTENT_KEY_UUID, wayPointUuid);
- noteIntent.setPackage(getContext().getPackageName());
- context.sendBroadcast(noteIntent);
+ if (saveAsWayPoint) {
+ // Track waypoint with user input text
+ Intent intent = new Intent(OSMTracker.INTENT_UPDATE_WP);
+ intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, wayPointTrackId);
+ intent.putExtra(OSMTracker.INTENT_KEY_NAME, noteText);
+ intent.putExtra(OSMTracker.INTENT_KEY_UUID, TextNoteDialog.this.wayPointUuid);
+ intent.setPackage(getContext().getPackageName());
+ context.sendBroadcast(intent);
+ }
+ if (saveAsNote) {
+ Intent noteIntent = new Intent(OSMTracker.INTENT_UPDATE_NOTE);
+ noteIntent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, noteTrackId);
+ noteIntent.putExtra(OSMTracker.INTENT_KEY_NAME, noteText);
+ noteIntent.putExtra(OSMTracker.INTENT_KEY_UUID, noteUuid);
+ noteIntent.setPackage(getContext().getPackageName());
+ context.sendBroadcast(noteIntent);
+ }
});
this.setButton(DialogInterface.BUTTON_NEGATIVE,
@@ -122,31 +129,52 @@ public void onCancel(DialogInterface dialog) {
*/
@Override
protected void onStart() {
- if (wayPointUuid == null) {
- // there is no UUID set for the waypoint we're working on
- // so we need to generate a UUID and track this point
- wayPointUuid = UUID.randomUUID().toString();
- Intent intent = new Intent(OSMTracker.INTENT_TRACK_WP);
- intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, wayPointTrackId);
- intent.putExtra(OSMTracker.INTENT_KEY_UUID, wayPointUuid);
- intent.putExtra(OSMTracker.INTENT_KEY_NAME, context.getResources().getString(R.string.gpsstatus_record_textnote));
- intent.setPackage(getContext().getPackageName());
- context.sendBroadcast(intent);
- }
-
- if (noteUuid == null) {
- // there is no UUID set for the note we're working on
- // so we need to generate a UUID and track this note
- noteUuid = UUID.randomUUID().toString();
- Intent noteIntent = new Intent(OSMTracker.INTENT_TRACK_NOTE);
- noteIntent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, noteTrackId);
- noteIntent.putExtra(OSMTracker.INTENT_KEY_UUID, noteUuid);
- noteIntent.putExtra(OSMTracker.INTENT_KEY_NAME, context.getResources().getString(R.string.gpsstatus_record_textnote));
- noteIntent.setPackage(getContext().getPackageName());
- context.sendBroadcast(noteIntent);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String prefSaveAs = prefs.getString(
+ OSMTracker.Preferences.KEY_USE_NOTES,
+ OSMTracker.Preferences.VAL_USE_NOTES);
+ switch (prefSaveAs) {
+ case "waypoint":
+ saveAsWayPoint = true;
+ saveAsNote = false;
+ break;
+ case "osm_note":
+ saveAsWayPoint = false;
+ saveAsNote = true;
+ break;
+ default: // Assuming "both" is the default
+ saveAsWayPoint = true;
+ saveAsNote = true;
+ break;
+ }
+ if (saveAsWayPoint) {
+ if (wayPointUuid == null) {
+ // there is no UUID set for the waypoint we're working on
+ // so we need to generate a UUID and track this point
+ wayPointUuid = UUID.randomUUID().toString();
+ Intent intent = new Intent(OSMTracker.INTENT_TRACK_WP);
+ intent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, wayPointTrackId);
+ intent.putExtra(OSMTracker.INTENT_KEY_UUID, wayPointUuid);
+ intent.putExtra(OSMTracker.INTENT_KEY_NAME, context.getResources().getString(R.string.gpsstatus_record_textnote));
+ intent.setPackage(getContext().getPackageName());
+ context.sendBroadcast(intent);
+ }
+ }
+ if (saveAsNote) {
+ if (noteUuid == null) {
+ // there is no UUID set for the note we're working on
+ // so we need to generate a UUID and track this note
+ noteUuid = UUID.randomUUID().toString();
+ Intent noteIntent = new Intent(OSMTracker.INTENT_TRACK_NOTE);
+ noteIntent.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, noteTrackId);
+ noteIntent.putExtra(OSMTracker.INTENT_KEY_UUID, noteUuid);
+ noteIntent.putExtra(OSMTracker.INTENT_KEY_NAME, context.getResources().getString(R.string.gpsstatus_record_textnote));
+ noteIntent.setPackage(getContext().getPackageName());
+ context.sendBroadcast(noteIntent);
+ }
}
getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
@@ -160,6 +188,7 @@ protected void onStart() {
*/
public void resetValues() {
wayPointUuid = null;
+ noteUuid = null;
input.setText("");
}
@@ -174,6 +203,7 @@ public void onRestoreInstanceState(Bundle savedInstanceState) {
}
wayPointUuid = savedInstanceState.getString(KEY_WAYPOINT_UUID);
wayPointTrackId = savedInstanceState.getLong(KEY_WAYPOINT_TRACKID);
+ noteUuid = savedInstanceState.getString(KEY_NOTE_UUID);
super.onRestoreInstanceState(savedInstanceState);
}
@@ -184,8 +214,13 @@ public void onRestoreInstanceState(Bundle savedInstanceState) {
public Bundle onSaveInstanceState() {
Bundle extras = super.onSaveInstanceState();
extras.putString(KEY_INPUT_TEXT, input.getText().toString());
- extras.putLong(KEY_WAYPOINT_TRACKID, wayPointTrackId);
- extras.putString(KEY_WAYPOINT_UUID, wayPointUuid);
+ if (saveAsWayPoint) {
+ extras.putLong(KEY_WAYPOINT_TRACKID, wayPointTrackId);
+ extras.putString(KEY_WAYPOINT_UUID, wayPointUuid);
+ }
+ if (saveAsNote) {
+ extras.putString(KEY_NOTE_UUID, noteUuid);
+ }
return extras;
}
}
From 4c2acc06c1c908a2430850134ed18c5198fa0c7c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Sun, 18 Jan 2026 16:27:21 -0600
Subject: [PATCH 43/60] Feature UI: Add icon to show when note is uploaded to
OSM
---
.../net/osmtracker/activity/NoteList.java | 2 ++
.../activity/OpenStreetMapNotesUpload.java | 14 ++++++----
.../java/net/osmtracker/db/DataHelper.java | 6 +++++
.../net/osmtracker/db/NoteListAdapter.java | 11 ++++++++
.../osmtracker/db/TrackContentProvider.java | 15 ++++++++++-
.../osm/UploadToOpenStreetMapNotesTask.java | 10 +++++--
app/src/main/res/layout/notelist_item.xml | 26 ++++++++++++++-----
7 files changed, 69 insertions(+), 15 deletions(-)
diff --git a/app/src/main/java/net/osmtracker/activity/NoteList.java b/app/src/main/java/net/osmtracker/activity/NoteList.java
index 280d1a695..464ed5ef1 100644
--- a/app/src/main/java/net/osmtracker/activity/NoteList.java
+++ b/app/src/main/java/net/osmtracker/activity/NoteList.java
@@ -195,11 +195,13 @@ public boolean onContextItemSelected(MenuItem item) {
* @param cursor The cursor positioned at the selected note.
*/
private void uploadNoteToOSM(Cursor cursor) {
+ long noteId = cursor.getLong(cursor.getColumnIndex(TrackContentProvider.Schema.COL_ID));
String noteText = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME));
double lat = cursor.getDouble(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE));
double lon = cursor.getDouble(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE));
Intent intent = new Intent(this, OpenStreetMapNotesUpload.class);
+ intent.putExtra("noteId", noteId);
intent.putExtra("noteContent", noteText);
intent.putExtra("appName", getString(R.string.app_name));
intent.putExtra("latitude", lat);
diff --git a/app/src/main/java/net/osmtracker/activity/OpenStreetMapNotesUpload.java b/app/src/main/java/net/osmtracker/activity/OpenStreetMapNotesUpload.java
index debf231dd..98142d9bd 100644
--- a/app/src/main/java/net/osmtracker/activity/OpenStreetMapNotesUpload.java
+++ b/app/src/main/java/net/osmtracker/activity/OpenStreetMapNotesUpload.java
@@ -41,6 +41,8 @@ public class OpenStreetMapNotesUpload extends Activity {
private static final String TAG = OpenStreetMapNotesUpload.class.getSimpleName();
+ private long noteId;
+
private double latitude;
private double longitude;
@@ -75,6 +77,7 @@ protected void onCreate(Bundle savedInstanceState) {
String appName = extras.getString("appName", getString(R.string.app_name));
String version = extras.getString("version", "");
+ if (extras.containsKey("noteId")) noteId = extras.getLong("noteId");
if (extras.containsKey("latitude")) latitude = extras.getDouble("latitude");
if (extras.containsKey("longitude")) longitude = extras.getDouble("longitude");
@@ -86,7 +89,7 @@ protected void onCreate(Bundle savedInstanceState) {
btnOk.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- startUpload();
+ startUpload(noteId);
}
});
final Button btnCancel = (Button) findViewById(R.id.osm_note_upload_button_cancel);
@@ -104,11 +107,11 @@ public void onClick(View v) {
* Either starts uploading directly if we are authenticated against OpenStreetMap,
* or ask the user to authenticate via the browser.
*/
- private void startUpload() {
+ private void startUpload(long noteId) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if ( prefs.contains(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN) ) {
// Re-use saved token
- uploadToOsm(prefs.getString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN, ""));
+ uploadToOsm(prefs.getString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN, ""), noteId);
} else {
// Open browser and request token
requestOsmAuth();
@@ -169,7 +172,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
editor.putString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN, resp.accessToken);
editor.apply();
//continue with the note Upload.
- uploadToOsm(resp.accessToken);
+ uploadToOsm(resp.accessToken, noteId);
} else {
// authorization failed, check ex for more details
Log.e(TAG, "OAuth failed.");
@@ -185,7 +188,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
/**
* Uploads notes to OSM.
*/
- public void uploadToOsm(String accessToken) {
+ public void uploadToOsm(String accessToken, long noteId) {
String noteText = noteContentView.getText().toString();
String footer = noteFooterView.getText().toString();
if (!footer.isEmpty()) {
@@ -194,6 +197,7 @@ public void uploadToOsm(String accessToken) {
new UploadToOpenStreetMapNotesTask(
OpenStreetMapNotesUpload.this,
accessToken,
+ noteId,
noteText,
latitude,
longitude
diff --git a/app/src/main/java/net/osmtracker/db/DataHelper.java b/app/src/main/java/net/osmtracker/db/DataHelper.java
index e86991245..68f8f85fc 100644
--- a/app/src/main/java/net/osmtracker/db/DataHelper.java
+++ b/app/src/main/java/net/osmtracker/db/DataHelper.java
@@ -437,6 +437,12 @@ public static void setTrackUploadDate(long trackId, long uploadTime, ContentReso
cr.update(trackUri, values, null, null);
}
+ public static void setNoteUploadDate(long noteId, long uploadTime, ContentResolver cr) {
+ Uri noteUri = ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_NOTE, noteId);
+ ContentValues values = new ContentValues();
+ values.put(TrackContentProvider.Schema.COL_OSM_UPLOAD_DATE, uploadTime);
+ cr.update(noteUri, values, null, null);
+ }
/**
* Renames a file inside track directory, keeping the extension
*
diff --git a/app/src/main/java/net/osmtracker/db/NoteListAdapter.java b/app/src/main/java/net/osmtracker/db/NoteListAdapter.java
index 88e71300e..17e83713a 100644
--- a/app/src/main/java/net/osmtracker/db/NoteListAdapter.java
+++ b/app/src/main/java/net/osmtracker/db/NoteListAdapter.java
@@ -6,6 +6,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
+import android.widget.ImageView;
import android.widget.TableLayout;
import android.widget.TextView;
@@ -64,6 +65,7 @@ public View newView(Context context, Cursor cursor, ViewGroup vg) {
*/
private View bind(Cursor cursor, TableLayout tl, Context context) {
TextView vName = tl.findViewById(R.id.notelist_item_name);
+ ImageView vUploadStatus = tl.findViewById(R.id.notelist_item_upload_status_icon);
TextView vLocation = tl.findViewById(R.id.notelist_item_location);
TextView vTimestamp = tl.findViewById(R.id.notelist_item_timestamp);
@@ -71,6 +73,15 @@ private View bind(Cursor cursor, TableLayout tl, Context context) {
String name = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME));
vName.setText(name);
+ // Upload status
+ if (cursor.isNull(cursor.getColumnIndex(TrackContentProvider.Schema.COL_OSM_UPLOAD_DATE))) {
+ vUploadStatus.setVisibility(View.GONE);
+ }
+ else{
+ vUploadStatus.setImageResource(android.R.drawable.stat_sys_upload_done);
+ vUploadStatus.setVisibility(View.VISIBLE);
+ }
+
// Bind location
StringBuffer locationAsString = new StringBuffer();
locationAsString.append(context.getResources().getString(R.string.wplist_latitude)
diff --git a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java
index 9c0b05982..8b9467370 100644
--- a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java
+++ b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java
@@ -36,6 +36,8 @@ public class TrackContentProvider extends ContentProvider {
*/
public static final Uri CONTENT_URI_TRACK = Uri.parse("content://" + AUTHORITY + "/" + Schema.TBL_TRACK);
+ public static final Uri CONTENT_URI_NOTE = Uri.parse("content://" + AUTHORITY + "/" + Schema.TBL_NOTE);
+
/**
* Uri for the active track
*/
@@ -116,7 +118,7 @@ public class TrackContentProvider extends ContentProvider {
uriMatcher.addURI(AUTHORITY, Schema.TBL_WAYPOINT + "/#", Schema.URI_CODE_WAYPOINT_ID);
uriMatcher.addURI(AUTHORITY, Schema.TBL_WAYPOINT + "/uuid/*", Schema.URI_CODE_WAYPOINT_UUID);
uriMatcher.addURI(AUTHORITY, Schema.TBL_TRACKPOINT + "/#", Schema.URI_CODE_TRACKPOINT_ID);
-
+ uriMatcher.addURI(AUTHORITY, Schema.TBL_NOTE + "/#", Schema.URI_CODE_NOTE_ID);
}
/**
@@ -488,6 +490,16 @@ public int update(Uri uri, ContentValues values, String selectionIn, String[] se
}
table = Schema.TBL_NOTE;
break;
+ case Schema.URI_CODE_NOTE_ID:
+ if (selectionIn != null || selectionArgsIn != null) {
+ // Any selection/selectionArgs will be ignored
+ throw new UnsupportedOperationException();
+ }
+ table = Schema.TBL_NOTE;
+ String noteId = uri.getLastPathSegment();
+ selection = Schema.COL_ID + " = ?";
+ selectionArgs = new String[] {noteId};
+ break;
case Schema.URI_CODE_TRACK_ID:
if (selectionIn != null || selectionArgsIn != null) {
// Any selection/selectionArgs will be ignored
@@ -572,6 +584,7 @@ public static final class Schema {
public static final int URI_CODE_WAYPOINT_ID = 11;
public static final int URI_CODE_TRACKPOINT_ID = 12;
public static final int URI_CODE_TRACK_NOTES = 13;
+ public static final int URI_CODE_NOTE_ID = 14;
public static final int VAL_TRACK_ACTIVE = 1;
diff --git a/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java b/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java
index 21e676312..55d75c749 100644
--- a/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java
+++ b/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java
@@ -11,6 +11,7 @@
import net.osmtracker.OSMTracker;
import net.osmtracker.R;
+import net.osmtracker.db.DataHelper;
import net.osmtracker.util.DialogUtils;
import de.westnordost.osmapi.OsmConnection;
@@ -36,6 +37,8 @@ public class UploadToOpenStreetMapNotesTask extends AsyncTask
private final Activity activity;
private final String accessToken;
+ private final long noteId;
+
/** Note text */
private final String noteText;
@@ -60,10 +63,11 @@ public class UploadToOpenStreetMapNotesTask extends AsyncTask
private final int okResultCode = 1;
// Not using an activity yet
- public UploadToOpenStreetMapNotesTask(Activity activity, String accessToken, String noteText,
- double latitude, double longitude) {
+ public UploadToOpenStreetMapNotesTask(Activity activity, String accessToken, long noteId,
+ String noteText, double latitude, double longitude) {
this.activity = activity;
this.accessToken = accessToken;
+ this.noteId = noteId;
this.noteText = noteText;
this.longitude = longitude;
this.latitude = latitude;
@@ -100,6 +104,8 @@ protected void onPostExecute(Void result) {
break;
case okResultCode:
dialog.dismiss();
+ // Success ! Update database and close activity
+ DataHelper.setNoteUploadDate(noteId, System.currentTimeMillis(), activity.getContentResolver());
new AlertDialog.Builder(activity)
.setTitle(android.R.string.dialog_alert_title)
diff --git a/app/src/main/res/layout/notelist_item.xml b/app/src/main/res/layout/notelist_item.xml
index 94fd72466..de51a0e5d 100644
--- a/app/src/main/res/layout/notelist_item.xml
+++ b/app/src/main/res/layout/notelist_item.xml
@@ -19,13 +19,17 @@
android:textColor="@android:color/holo_green_light"
android:textAppearance="?android:attr/textAppearanceLarge" />
-
+
+
+
\ No newline at end of file
From 514bf0390127b9f1f915108dcfe4342a0cfa7dfe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Sun, 18 Jan 2026 16:48:53 -0600
Subject: [PATCH 44/60] Feature UI: Add underline to note count in track detail
---
.../net/osmtracker/activity/TrackDetail.java | 31 ++++++++++++++-----
1 file changed, 23 insertions(+), 8 deletions(-)
diff --git a/app/src/main/java/net/osmtracker/activity/TrackDetail.java b/app/src/main/java/net/osmtracker/activity/TrackDetail.java
index 4e0d131c2..ef195fd64 100644
--- a/app/src/main/java/net/osmtracker/activity/TrackDetail.java
+++ b/app/src/main/java/net/osmtracker/activity/TrackDetail.java
@@ -74,6 +74,8 @@ public class TrackDetail extends TrackDetailEditor implements AdapterView.OnItem
/** Does this track have any waypoints? If true, underline Waypoint count in the list. */
private boolean trackHasWaypoints = false;
+ // Does this track have any notes? If true, underline Notes count in the list.
+ private boolean trackHasNotes = false;
/**
* List with track info
@@ -154,6 +156,8 @@ protected void onResume() {
data.add(map);
// Notes count
+ final int notesCount = t.getNoteCount();
+ trackHasNotes = (notesCount > 0);
map = new HashMap();
map.put(ITEM_KEY, getResources().getString(R.string.trackmgr_notes_count));
map.put(ITEM_VALUE, Integer.toString(t.getNoteCount()));
@@ -383,21 +387,32 @@ private class TrackDetailSimpleAdapter extends SimpleAdapter
* Get the layout for this list item. (trackdetail_item.xml)
* @param position Item number in the list
*/
- public View getView(final int position, View convertView, ViewGroup parent)
- {
+ public View getView(final int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
if (! (v instanceof ViewGroup))
return v; // should not happen; v is trackdetail_item, a LinearLayout
- final boolean wantsUnderline = ((position == WP_COUNT_INDEX) && trackHasWaypoints);
+ // Get the data for the current row
+ Map item = (Map) getItem(position);
+ String key = item.get(ITEM_KEY);
+ boolean wantsUnderline = false;
+
+ // Check the key to decide if we should underline
+ if (getString(R.string.trackmgr_waypoints_count).equals(key) && trackHasWaypoints) {
+ wantsUnderline = trackHasWaypoints;
+ } else if (getString(R.string.trackmgr_notes_count).equals(key) && trackHasNotes) {
+ wantsUnderline = trackHasNotes;
+ }
+
+
View vi = ((ViewGroup) v).findViewById(R.id.trackdetail_item_key);
- if ((vi != null) && (vi instanceof TextView))
- {
- final int flags = ((TextView) vi).getPaintFlags();
+ if ((vi != null) && (vi instanceof TextView)) {
+ TextView tv = (TextView) vi;
+ final int flags = tv.getPaintFlags();
if (wantsUnderline)
- ((TextView) vi).setPaintFlags(flags | Paint.UNDERLINE_TEXT_FLAG);
+ tv.setPaintFlags(flags | Paint.UNDERLINE_TEXT_FLAG);
else
- ((TextView) vi).setPaintFlags(flags & ~Paint.UNDERLINE_TEXT_FLAG);
+ tv.setPaintFlags(flags & ~Paint.UNDERLINE_TEXT_FLAG);
}
return v;
}
From 325eaf8d4b02d07162b7c9a92b52671ae5c2b86d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Sun, 18 Jan 2026 18:21:04 -0600
Subject: [PATCH 45/60] OAuth2 Client ID restore (changed for upload notes
development)
---
.../main/java/net/osmtracker/osm/OpenStreetMapConstants.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/app/src/main/java/net/osmtracker/osm/OpenStreetMapConstants.java b/app/src/main/java/net/osmtracker/osm/OpenStreetMapConstants.java
index 2db037506..a7610be46 100644
--- a/app/src/main/java/net/osmtracker/osm/OpenStreetMapConstants.java
+++ b/app/src/main/java/net/osmtracker/osm/OpenStreetMapConstants.java
@@ -14,8 +14,7 @@ public static class Api {
}
public static class OAuth2 {
- // Client ID prod was changed to test notes uploading with new scope, must be changed back
- public static final String CLIENT_ID_PROD = "2gBvqUryethglDBRXIZvXA-ijLMp--r6NUHV19NyRz4";
+ public static final String CLIENT_ID_PROD = "6s8TuIQoPeq89ZWUFOXU7EZ-ZaCUVtUoNZFIKCMdU-E";
public static final String CLIENT_ID_DEV = "94Ht-oVBJ2spydzfk18s1RV2z7NS98SBwMfzSCqLQLE"; // DEV
public static final String CLIENT_ID = (DEV_MODE) ? CLIENT_ID_DEV : CLIENT_ID_PROD;
From 3759f1f6efa27d1b45ca45a94c8b75ff0b23a100 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Mon, 19 Jan 2026 19:40:08 -0600
Subject: [PATCH 46/60] Refactor Note List, add unit test
---
app/src/main/AndroidManifest.xml | 1 +
.../net/osmtracker/activity/NoteList.java | 245 +++++++-----------
.../net/osmtracker/adapter/NoteAdapter.java | 122 +++++++++
.../java/net/osmtracker/db/DataHelper.java | 4 +-
.../net/osmtracker/db/NoteListAdapter.java | 100 -------
.../osmtracker/db/TrackContentProvider.java | 28 ++
app/src/main/res/layout/notelist.xml | 8 +
app/src/main/res/layout/notelist_item.xml | 104 ++++----
.../net/osmtracker/db/DataHelperNoteTest.java | 68 +++++
.../db/model/OSMVisibilityTest.java | 2 +-
.../{test => }/db/model/TrackTest.java | 2 +-
11 files changed, 374 insertions(+), 310 deletions(-)
create mode 100644 app/src/main/java/net/osmtracker/adapter/NoteAdapter.java
delete mode 100644 app/src/main/java/net/osmtracker/db/NoteListAdapter.java
create mode 100644 app/src/main/res/layout/notelist.xml
create mode 100644 app/src/test/java/net/osmtracker/db/DataHelperNoteTest.java
rename app/src/test/java/net/osmtracker/{test => }/db/model/OSMVisibilityTest.java (98%)
rename app/src/test/java/net/osmtracker/{test => }/db/model/TrackTest.java (98%)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 97f6435b6..397e7e289 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -60,6 +60,7 @@
android:label="@string/wplist" />
{
+ String newName = editNoteName.getText().toString();
+ dataHelper.updateNote(trackId, uuid, newName);
+ refreshData();
+ alert.dismiss();
});
- // Delete waypoint
- buttonDelete.setOnClickListener(new EditNoteDialogOnClickListener(alert, cursor) {
- @Override
- public void onClick(View view) {
- new AlertDialog.Builder(NoteList.this)
- .setTitle(getString(R.string.delete_note_confirm_dialog_title))
- .setMessage(getString(R.string.delete_note_confirm_dialog_msg))
- .setPositiveButton(getString(R.string.delete_note_confirm_bt_ok), new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dataHelper.deleteNote(uuid);
- cursor.requery();
- alert.dismiss();
- dialog.dismiss();
- }
- })
- .setNegativeButton(getString(R.string.delete_note_confirm_bt_cancel), new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- }
- })
- .show();
- }
- });
+ // Delete note
+ buttonDelete.setOnClickListener(v -> new AlertDialog.Builder(this)
+ .setTitle(R.string.delete_note_confirm_dialog_title)
+ .setMessage(R.string.delete_note_confirm_dialog_msg)
+ .setPositiveButton(R.string.delete_note_confirm_bt_ok, (dialog, which) -> {
+ dataHelper.deleteNote(uuid);
+ refreshData();
+ alert.dismiss();
+ })
+ .setNegativeButton(R.string.delete_note_confirm_bt_cancel,
+ (dialog, which) -> dialog.dismiss())
+ .show());
// Upload note text to OpenStreetMap
- buttonOSMUpload.setOnClickListener(new EditNoteDialogOnClickListener(alert, null) {
- @Override
- public void onClick(View view) {
- uploadNoteToOSM(cursor);
- }
+ buttonOSMUpload.setOnClickListener(v -> {
+ uploadNoteToOSM(noteId);
+ alert.dismiss();
});
// Cancel button
- buttonCancel.setOnClickListener(new EditWaypointDialogOnClickListener(alert, null) {
- @Override
- public void onClick(View view) {
- alert.dismiss();
- }
- });
+ buttonCancel.setOnClickListener(v -> alert.dismiss());
alert.setView(editNoteDialog);
alert.show();
-
- super.onListItemClick(l, v, position, id);
}
- // Where the menu items get defined
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- super.onCreateContextMenu(menu, v, menuInfo);
- getMenuInflater().inflate(R.menu.note_contextmenu, menu);
- }
-
- // What happens when a menu item is selected
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
- final Cursor cursor = ((CursorAdapter) getListAdapter()).getCursor();
- if (!cursor.moveToPosition(info.position)) return super.onContextItemSelected(item);
-
- // Menu options when you long press on a note
- switch (item.getItemId()) {
- case R.id.notelist_contextmenu_osm_note_upload:
- uploadNoteToOSM(cursor);
- return true;
- }
- return super.onContextItemSelected(item);
- }
-
/**
- * Extracts note data from the cursor and launches the OSM Note Upload activity.
- * @param cursor The cursor positioned at the selected note.
+ * Extracts note data from DB and launches the OSM Note Upload activity.
*/
- private void uploadNoteToOSM(Cursor cursor) {
- long noteId = cursor.getLong(cursor.getColumnIndex(TrackContentProvider.Schema.COL_ID));
- String noteText = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME));
- double lat = cursor.getDouble(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE));
- double lon = cursor.getDouble(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE));
-
- Intent intent = new Intent(this, OpenStreetMapNotesUpload.class);
- intent.putExtra("noteId", noteId);
- intent.putExtra("noteContent", noteText);
- intent.putExtra("appName", getString(R.string.app_name));
- intent.putExtra("latitude", lat);
- intent.putExtra("longitude", lon);
-
- // Retrieve app version number
- try {
- PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
- intent.putExtra("version", pi.versionName);
- } catch (PackageManager.NameNotFoundException e) {
- // Log error or ignore
- }
+ private void uploadNoteToOSM(long noteId) {
+ // Query the specific note to get latest Lat/Lon
+ Cursor cursor = getContentResolver().query(
+ TrackContentProvider.noteUri(noteId),
+ null,null,null,null);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ String noteText = cursor.getString(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_NAME));
+ double lat = cursor.getDouble(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_LATITUDE));
+ double lon = cursor.getDouble(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_LONGITUDE));
+
+ Intent intent = new Intent(this, OpenStreetMapNotesUpload.class);
+ intent.putExtra("noteId", noteId);
+ intent.putExtra("noteContent", noteText);
+ intent.putExtra("appName", getString(R.string.app_name));
+ intent.putExtra("latitude", lat);
+ intent.putExtra("longitude", lon);
+
+ // Retrieve app version number
+ try {
+ PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
+ intent.putExtra("version", pi.versionName);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Ignore
+ Log.d(TAG, "Package name not found", e);
+ }
- startActivity(intent);
+ cursor.close();
+ startActivity(intent);
+ }
}
}
diff --git a/app/src/main/java/net/osmtracker/adapter/NoteAdapter.java b/app/src/main/java/net/osmtracker/adapter/NoteAdapter.java
new file mode 100644
index 000000000..896c8dadc
--- /dev/null
+++ b/app/src/main/java/net/osmtracker/adapter/NoteAdapter.java
@@ -0,0 +1,122 @@
+package net.osmtracker.adapter;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.osmtracker.R;
+import net.osmtracker.db.TrackContentProvider;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class NoteAdapter extends RecyclerView.Adapter {
+
+ public static final SimpleDateFormat DATE_FORMATTER =
+ new SimpleDateFormat("HH:mm:ss 'UTC'", Locale.ROOT);
+ static {
+ DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ private Cursor cursor;
+ private final OnNoteClickListener listener;
+
+ public interface OnNoteClickListener {
+ void onNoteClick(long id, long noteId, String uuid, String name);
+ }
+
+ public NoteAdapter(OnNoteClickListener listener) {
+ this.listener = listener;
+ }
+
+ public void swapCursor(Cursor newCursor) {
+ if (cursor == newCursor) return;
+ if (cursor != null) cursor.close();
+ cursor = newCursor;
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public NoteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(
+ parent.getContext()).inflate(R.layout.notelist_item, parent, false);
+ return new NoteViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull NoteViewHolder holder, int position) {
+ if (cursor.moveToPosition(position)) {
+ Context context = holder.itemView.getContext();
+
+ // Bind name
+ String name = cursor.getString(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_NAME));
+ holder.tvName.setText(name);
+
+ // Upload status
+ if (cursor.isNull(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_OSM_UPLOAD_DATE))) {
+ holder.ivUploadStatus.setVisibility(View.GONE);
+ } else {
+ holder.ivUploadStatus.setImageResource(android.R.drawable.stat_sys_upload_done);
+ holder.ivUploadStatus.setVisibility(View.VISIBLE);
+ }
+
+ //Bind Location (Latitude/Longitude)
+ String lat = cursor.getString(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_LATITUDE));
+ String lon = cursor.getString(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_LONGITUDE));
+
+ String locationStr = context.getString(R.string.wplist_latitude) + lat + ", " +
+ context.getString(R.string.wplist_longitude) + lon;
+ holder.tvLocation.setText(locationStr);
+
+ // Bind Timestamp
+ Date ts = new Date(cursor.getLong(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_TIMESTAMP)));
+ holder.tvTimestamp.setText(DATE_FORMATTER.format(ts));
+
+ // Setup Click Listener
+ String uuid = cursor.getString(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_UUID));
+ long trackId = cursor.getLong(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_TRACK_ID));
+ long noteId = cursor.getLong(
+ cursor.getColumnIndexOrThrow(TrackContentProvider.Schema.COL_ID));
+ holder.itemView.setOnClickListener(
+ v -> listener.onNoteClick(trackId, noteId, uuid, name));
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return (cursor == null) ? 0 : cursor.getCount();
+ }
+
+ public static class NoteViewHolder extends RecyclerView.ViewHolder {
+ TextView tvName;
+ ImageView ivUploadStatus;
+ TextView tvLocation;
+ TextView tvTimestamp;
+
+
+ public NoteViewHolder(View v) {
+ super(v);
+ tvName = v.findViewById(R.id.notelist_item_name);
+ ivUploadStatus = v.findViewById(R.id.notelist_item_upload_status_icon);
+ tvLocation = v.findViewById(R.id.notelist_item_location);
+ tvTimestamp = v.findViewById(R.id.notelist_item_timestamp);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/osmtracker/db/DataHelper.java b/app/src/main/java/net/osmtracker/db/DataHelper.java
index 68f8f85fc..37935bec1 100644
--- a/app/src/main/java/net/osmtracker/db/DataHelper.java
+++ b/app/src/main/java/net/osmtracker/db/DataHelper.java
@@ -349,7 +349,7 @@ public void trackNote(long trackId, Location location, String name, String uuid)
* Updates a note
*
* @param trackId Id of the track
- * @param uuid Unique ID of the target waypoint
+ * @param uuid Unique ID of the target note
* @param name New text value for the note
*/
public void updateNote(long trackId, String uuid, String name) {
@@ -374,7 +374,7 @@ public void updateNote(long trackId, String uuid, String name) {
public void deleteNote(String uuid) {
Log.v(TAG, "Deleting note with uuid '" + uuid);
if (uuid != null) {
- contentResolver.delete(Uri.withAppendedPath(TrackContentProvider.CONTENT_URI_WAYPOINT_UUID, uuid), null, null);
+ contentResolver.delete(Uri.withAppendedPath(TrackContentProvider.CONTENT_URI_NOTE_UUID, uuid), null, null);
}
}
diff --git a/app/src/main/java/net/osmtracker/db/NoteListAdapter.java b/app/src/main/java/net/osmtracker/db/NoteListAdapter.java
deleted file mode 100644
index 17e83713a..000000000
--- a/app/src/main/java/net/osmtracker/db/NoteListAdapter.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package net.osmtracker.db;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CursorAdapter;
-import android.widget.ImageView;
-import android.widget.TableLayout;
-import android.widget.TextView;
-
-import net.osmtracker.R;
-
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.TimeZone;
-
-/**
- * Adapter for the note list. Gets notes from database.
- *
- */
-public class NoteListAdapter extends CursorAdapter {
-
- /**
- * Date formatter
- */
- public static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("HH:mm:ss 'UTC'");
- static {
- DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
- }
-
- /**
- * Constructor.
- *
- * @param context
- * Application context
- * @param c
- * {@link Cursor} to data
- */
- public NoteListAdapter(Context context, Cursor c) {
- super(context, c);
- }
-
- @Override
- public void bindView(View view, Context context, Cursor cursor) {
- TableLayout tl = (TableLayout) view;
- bind(cursor, tl, context);
- }
-
- @Override
- public View newView(Context context, Cursor cursor, ViewGroup vg) {
- TableLayout tl = (TableLayout) LayoutInflater.from(vg.getContext()).inflate(R.layout.notelist_item,
- vg, false);
- return bind(cursor, tl, context);
- }
-
- /**
- * Do the binding between data and item view.
- *
- * @param cursor Cursor to pull data
- * @param tl RelativeView representing one item
- * @param context Context, to get resources
- * @return The relative view with data bound.
- */
- private View bind(Cursor cursor, TableLayout tl, Context context) {
- TextView vName = tl.findViewById(R.id.notelist_item_name);
- ImageView vUploadStatus = tl.findViewById(R.id.notelist_item_upload_status_icon);
- TextView vLocation = tl.findViewById(R.id.notelist_item_location);
- TextView vTimestamp = tl.findViewById(R.id.notelist_item_timestamp);
-
- // Bind name
- String name = cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_NAME));
- vName.setText(name);
-
- // Upload status
- if (cursor.isNull(cursor.getColumnIndex(TrackContentProvider.Schema.COL_OSM_UPLOAD_DATE))) {
- vUploadStatus.setVisibility(View.GONE);
- }
- else{
- vUploadStatus.setImageResource(android.R.drawable.stat_sys_upload_done);
- vUploadStatus.setVisibility(View.VISIBLE);
- }
-
- // Bind location
- StringBuffer locationAsString = new StringBuffer();
- locationAsString.append(context.getResources().getString(R.string.wplist_latitude)
- + cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE)));
- locationAsString.append(", " + context.getResources().getString(R.string.wplist_longitude)
- + cursor.getString(cursor.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE)));
-
- vLocation.setText(locationAsString.toString());
-
- // Bind timestamp
- Date ts = new Date(cursor.getLong(cursor.getColumnIndex(TrackContentProvider.Schema.COL_TIMESTAMP)));
- vTimestamp.setText(DATE_FORMATTER.format(ts));
- return tl;
- }
-
-}
diff --git a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java
index 8b9467370..a5129c8a4 100644
--- a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java
+++ b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java
@@ -119,6 +119,7 @@ public class TrackContentProvider extends ContentProvider {
uriMatcher.addURI(AUTHORITY, Schema.TBL_WAYPOINT + "/uuid/*", Schema.URI_CODE_WAYPOINT_UUID);
uriMatcher.addURI(AUTHORITY, Schema.TBL_TRACKPOINT + "/#", Schema.URI_CODE_TRACKPOINT_ID);
uriMatcher.addURI(AUTHORITY, Schema.TBL_NOTE + "/#", Schema.URI_CODE_NOTE_ID);
+ uriMatcher.addURI(AUTHORITY, Schema.TBL_NOTE + "/uuid/*", Schema.URI_CODE_NOTE_UUID);
}
/**
@@ -139,6 +140,14 @@ public static final Uri waypointUri(long waypointId) {
return ContentUris.withAppendedId(CONTENT_URI_WAYPOINT, waypointId);
}
+ /**
+ * @param noteId target note id
+ * @return Uri for the note
+ */
+ public static final Uri noteUri(long noteId) {
+ return ContentUris.withAppendedId(CONTENT_URI_NOTE, noteId);
+ }
+
/**
* @param trackId target track id
* @return Uri for the notes of the track
@@ -223,6 +232,14 @@ public int delete(Uri uri, String selection, String[] selectionArgs) {
count = 0;
}
break;
+ case Schema.URI_CODE_NOTE_UUID:
+ String noteUUID = uri.getLastPathSegment();
+ if(noteUUID != null){
+ count = dbHelper.getWritableDatabase().delete(Schema.TBL_NOTE, Schema.COL_UUID + " = ?", new String[]{noteUUID});
+ }else{
+ count = 0;
+ }
+ break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
@@ -386,6 +403,16 @@ public Cursor query(Uri uri, String[] projection, String selectionIn, String[] s
selection = Schema.COL_TRACK_ID + " = ?";
selectionArgs = new String[] {trackId};
break;
+ case Schema.URI_CODE_NOTE_ID:
+ if (selectionIn != null || selectionArgsIn != null) {
+ // Any selection/selectionArgs will be ignored
+ throw new UnsupportedOperationException();
+ }
+ String noteId = uri.getPathSegments().get(1);
+ qb.setTables(Schema.TBL_NOTE);
+ selection = Schema.COL_ID + " = ?";
+ selectionArgs = new String[] {noteId};
+ break;
case Schema.URI_CODE_TRACK_START:
if (selectionIn != null || selectionArgsIn != null) {
// Any selection/selectionArgs will be ignored
@@ -585,6 +612,7 @@ public static final class Schema {
public static final int URI_CODE_TRACKPOINT_ID = 12;
public static final int URI_CODE_TRACK_NOTES = 13;
public static final int URI_CODE_NOTE_ID = 14;
+ public static final int URI_CODE_NOTE_UUID = 15;
public static final int VAL_TRACK_ACTIVE = 1;
diff --git a/app/src/main/res/layout/notelist.xml b/app/src/main/res/layout/notelist.xml
new file mode 100644
index 000000000..15b60f4ce
--- /dev/null
+++ b/app/src/main/res/layout/notelist.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/notelist_item.xml b/app/src/main/res/layout/notelist_item.xml
index de51a0e5d..1fef1e588 100644
--- a/app/src/main/res/layout/notelist_item.xml
+++ b/app/src/main/res/layout/notelist_item.xml
@@ -1,58 +1,58 @@
-
+
-
+
-
+
-
-
+
-
+
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/test/java/net/osmtracker/db/DataHelperNoteTest.java b/app/src/test/java/net/osmtracker/db/DataHelperNoteTest.java
new file mode 100644
index 000000000..b243fc9f4
--- /dev/null
+++ b/app/src/test/java/net/osmtracker/db/DataHelperNoteTest.java
@@ -0,0 +1,68 @@
+package net.osmtracker.db;
+
+import android.database.Cursor;
+import android.location.Location;
+import android.os.Bundle;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.UUID;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 25)
+public class DataHelperNoteTest {
+
+ private DataHelper dataHelper;
+
+ @Before
+ public void setup() {
+ // Initialize DataHelper with the Robolectric application context
+ dataHelper = new DataHelper(ApplicationProvider.getApplicationContext());
+ }
+
+ @Test
+ public void testDeleteNote_RemovesFromDatabase() {
+ String noteUUID = UUID.randomUUID().toString();
+ long trackId = 1L;
+
+ // 1. Insert a note
+ Location loc = new Location("gps");
+ loc.setLatitude(1.23);
+ loc.setLongitude(4.56);
+ loc.setTime(System.currentTimeMillis());
+
+ // Initialize extras to avoid NullPointerException if logic accesses location extras
+ loc.setExtras(new Bundle());
+
+ dataHelper.trackNote(trackId, loc, "Note to delete", noteUUID);
+
+ // 2. Verify it exists before deletion
+ Assert.assertTrue("Note should exist after insertion", noteExists(trackId));
+
+ // 3. Delete note
+ dataHelper.deleteNote(noteUUID);
+
+ // 4. Verify it is gone
+ Assert.assertFalse("Note should have been deleted from DB", noteExists(trackId));
+ }
+
+ private boolean noteExists(long TrackId) {
+ // Query using the content resolver provided by the Robolectric environment
+ Cursor c = ApplicationProvider.getApplicationContext().getContentResolver().query(
+ TrackContentProvider.notesUri(TrackId),
+ null,null,null,null);
+
+ boolean exists = (c != null && c.getCount() > 0);
+ if (c != null) {
+ c.close();
+ }
+ return exists;
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/net/osmtracker/test/db/model/OSMVisibilityTest.java b/app/src/test/java/net/osmtracker/db/model/OSMVisibilityTest.java
similarity index 98%
rename from app/src/test/java/net/osmtracker/test/db/model/OSMVisibilityTest.java
rename to app/src/test/java/net/osmtracker/db/model/OSMVisibilityTest.java
index 36e2fdde4..39ea4af0e 100644
--- a/app/src/test/java/net/osmtracker/test/db/model/OSMVisibilityTest.java
+++ b/app/src/test/java/net/osmtracker/db/model/OSMVisibilityTest.java
@@ -1,4 +1,4 @@
-package net.osmtracker.test.db.model;
+package net.osmtracker.db.model;
import android.content.Context;
diff --git a/app/src/test/java/net/osmtracker/test/db/model/TrackTest.java b/app/src/test/java/net/osmtracker/db/model/TrackTest.java
similarity index 98%
rename from app/src/test/java/net/osmtracker/test/db/model/TrackTest.java
rename to app/src/test/java/net/osmtracker/db/model/TrackTest.java
index 04c524fbc..fa02164cb 100644
--- a/app/src/test/java/net/osmtracker/test/db/model/TrackTest.java
+++ b/app/src/test/java/net/osmtracker/db/model/TrackTest.java
@@ -1,4 +1,4 @@
-package net.osmtracker.test.db.model;
+package net.osmtracker.db.model;
import android.content.ContentResolver;
import android.database.Cursor;
From b12a1f709058bd4b4436b9e33a01f9346e6f1bb6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Tue, 20 Jan 2026 18:41:07 -0600
Subject: [PATCH 47/60] Open Street Map Notes Upload clases and add unit test
---
app/src/main/AndroidManifest.xml | 3 +-
.../activity/OpenStreetMapNotesUpload.java | 227 +++++++-------
.../osm/UploadToOpenStreetMapNotesTask.java | 283 +++++++++---------
app/src/main/res/layout/osm_note_upload.xml | 36 ++-
.../OpenStreetMapNotesUploadTest.java | 107 +++++++
5 files changed, 382 insertions(+), 274 deletions(-)
create mode 100644 app/src/test/java/net/osmtracker/activity/OpenStreetMapNotesUploadTest.java
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 397e7e289..071c23b80 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -70,6 +70,8 @@
android:label="@string/osm_upload"
android:exported="true">
+
@@ -114,7 +116,6 @@
- Uploads a note on OSM using the API and
- * OAuth authentication.
+ *
Uploads a note on OSM using the API and OAuth authentication.
*
*
This activity may be called twice during a single
* upload cycle: First to start the upload, then a second
@@ -37,7 +38,7 @@
*
* @author Most of the code was made by Nicolas Guillaumin, adapted by Jose Andrés Vargas Serrano
*/
-public class OpenStreetMapNotesUpload extends Activity {
+public class OpenStreetMapNotesUpload extends AppCompatActivity {
private static final String TAG = OpenStreetMapNotesUpload.class.getSimpleName();
@@ -51,19 +52,39 @@ public class OpenStreetMapNotesUpload extends Activity {
/** URL that the browser will call once the user is authenticated */
public final static String OAUTH2_CALLBACK_URL = "osmtracker://osm-upload/oath2-completed/";
- public final static int RC_AUTH = 7;
-
private AuthorizationService authService;
+ private ActivityResultLauncher authLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- View uploadNoteView = getLayoutInflater().inflate(R.layout.osm_note_upload, null);
- setContentView(uploadNoteView);
- setTitle(R.string.osm_note_upload);
-
- noteContentView = uploadNoteView.findViewById(R.id.wplist_item_name);
- noteFooterView = uploadNoteView.findViewById(R.id.osm_note_footer);
+ super.onCreate(savedInstanceState);
+
+ // Register the launcher
+ authLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ // This replaces the logic previously in onActivityResult
+ Intent data = result.getData();
+ // RC_AUTH logic
+ if (data != null) {
+ AuthorizationResponse resp = AuthorizationResponse.fromIntent(data);
+ AuthorizationException ex = AuthorizationException.fromIntent(data);
+
+ if (resp != null) {
+ exchangeAuthorizationCode(resp);
+ } else {
+ Log.e(TAG, "Authorization failed: " + (ex != null ? ex.getMessage() : "Unknown error"));
+ Toast.makeText(this, R.string.osm_upload_oauth_failed, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ );
+
+
+ setContentView(R.layout.osm_note_upload);
+ setTitle(R.string.osm_note_upload);
+ noteContentView = findViewById(R.id.wplist_item_name);
+ noteFooterView = findViewById(R.id.osm_note_footer);
// Read and cache extras
Bundle extras = getIntent().getExtras();
@@ -85,20 +106,10 @@ protected void onCreate(Bundle savedInstanceState) {
noteContentView.setText(initialNoteText);
noteFooterView.setText(getString(R.string.osm_note_footer, appName, version));
- final Button btnOk = (Button) findViewById(R.id.osm_note_upload_button_ok);
- btnOk.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- startUpload(noteId);
- }
- });
- final Button btnCancel = (Button) findViewById(R.id.osm_note_upload_button_cancel);
- btnCancel.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- finish();
- }
- });
+ final Button btnOk = findViewById(R.id.osm_note_upload_button_ok);
+ btnOk.setOnClickListener(v -> startUpload(noteId));
+ final Button btnCancel = findViewById(R.id.osm_note_upload_button_cancel);
+ btnCancel.setOnClickListener(v -> finish());
}
@@ -109,100 +120,96 @@ public void onClick(View v) {
*/
private void startUpload(long noteId) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- if ( prefs.contains(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN) ) {
- // Re-use saved token
- uploadToOsm(prefs.getString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN, ""), noteId);
- } else {
- // Open browser and request token
+ String accessToken = prefs.getString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN, null);
+
+ if (accessToken != null && !accessToken.isEmpty()) {
+ // STATE: AUTHORIZED. Re-use saved token
+ Log.d(TAG, "Token found, proceeding to upload note to OSM.");
+ uploadToOsm(accessToken, noteId);
+ } else {
+ // STATE: UNAUTHORIZED. Open browser and request token
+ Log.d(TAG, "No token found, requesting authorization.");
requestOsmAuth();
}
}
/*
- * Init Authorization request workflow.
+ * Init Authorization request workflow. Launches browser to request authorization.
*/
public void requestOsmAuth() {
// Authorization service configuration
- AuthorizationServiceConfiguration serviceConfig =
- new AuthorizationServiceConfiguration(
+ AuthorizationServiceConfiguration serviceConfig = new AuthorizationServiceConfiguration(
Uri.parse(OpenStreetMapConstants.OAuth2.Urls.AUTHORIZATION_ENDPOINT),
Uri.parse(OpenStreetMapConstants.OAuth2.Urls.TOKEN_ENDPOINT));
- // Obtaining an authorization code
- Uri redirectURI = Uri.parse(OAUTH2_CALLBACK_URL);
- AuthorizationRequest.Builder authRequestBuilder =
- new AuthorizationRequest.Builder(
- serviceConfig, OpenStreetMapConstants.OAuth2.CLIENT_ID,
- ResponseTypeValues.CODE, redirectURI);
- AuthorizationRequest authRequest = authRequestBuilder
- .setScope(OpenStreetMapConstants.OAuth2.SCOPE)
- .build();
-
- // Start activity.
+ // Obtaining an authorization code
+ AuthorizationRequest authRequest = new AuthorizationRequest.Builder(
+ serviceConfig,
+ OpenStreetMapConstants.OAuth2.CLIENT_ID,
+ ResponseTypeValues.CODE,
+ Uri.parse(OAUTH2_CALLBACK_URL))
+ .setScope(OpenStreetMapConstants.OAuth2.SCOPE)
+ .build();
+
+ // Start activity.
authService = new AuthorizationService(this);
Intent authIntent = authService.getAuthorizationRequestIntent(authRequest);
- startActivityForResult(authIntent, RC_AUTH); //when done onActivityResult will be called.
+ //when done onActivityResult will be called.
+ // Use the launcher instead of startActivityForResult
+ authLauncher.launch(authIntent);
}
-
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- // User is returning from authentication
- if (requestCode == RC_AUTH) {
- // Handling the authorization response
- AuthorizationResponse resp = AuthorizationResponse.fromIntent(data);
- AuthorizationException ex = AuthorizationException.fromIntent(data);
- // ... process the response or exception ...
- if (ex != null) {
- Log.e(TAG, "Authorization Error. Exception received from server.");
- Log.e(TAG, ex.getMessage());
- } else if (resp == null) {
- Log.e(TAG, "Authorization Error. Null response from server.");
- } else {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
-
- //Exchanging the authorization code
- authService.performTokenRequest(
- resp.createTokenExchangeRequest(),
- new AuthorizationService.TokenResponseCallback() {
- @Override public void onTokenRequestCompleted(
- TokenResponse resp, AuthorizationException ex) {
- if (resp != null) {
- // exchange succeeded
- SharedPreferences.Editor editor = prefs.edit();
- editor.putString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN, resp.accessToken);
- editor.apply();
- //continue with the note Upload.
- uploadToOsm(resp.accessToken, noteId);
- } else {
- // authorization failed, check ex for more details
- Log.e(TAG, "OAuth failed.");
- }
- }
- });
- }
- } else {
- Log.e(TAG, "Unexpected requestCode:" + requestCode + ".");
- }
- }
+ private void exchangeAuthorizationCode(AuthorizationResponse resp) {
+ authService.performTokenRequest(resp.createTokenExchangeRequest(), (tokenResp, tokenEx) -> {
+ if (tokenResp != null && tokenResp.accessToken != null) {
+ // STATE: TRANSITION TO AUTHORIZED
+ persistToken(tokenResp.accessToken);
+ uploadToOsm(tokenResp.accessToken, noteId);
+ } else {
+ Log.e(TAG, "Token exchange failed");
+ }
+ });
+ }
+
+ private void persistToken(String token) {
+ PreferenceManager.getDefaultSharedPreferences(this).edit()
+ .putString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN, token)
+ .apply();
+ }
/**
* Uploads notes to OSM.
*/
public void uploadToOsm(String accessToken, long noteId) {
- String noteText = noteContentView.getText().toString();
- String footer = noteFooterView.getText().toString();
- if (!footer.isEmpty()) {
- noteText = noteText + "\n\n" + footer;
- }
- new UploadToOpenStreetMapNotesTask(
- OpenStreetMapNotesUpload.this,
- accessToken,
- noteId,
- noteText,
- latitude,
- longitude
- ).execute();
- }
-
+ String noteText = noteContentView.getText().toString();
+ String footer = noteFooterView.getText().toString();
+ if (!footer.isEmpty()) {
+ noteText = noteText + "\n\n" + footer;
+ }
+
+ // Final variables for the background thread
+ final String finalNoteText = noteText;
+
+ // This replaces the deprecated AsyncTask.execute()
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ executor.execute(() -> {
+ try {
+ new UploadToOpenStreetMapNotesTask(
+ OpenStreetMapNotesUpload.this,
+ accessToken,
+ noteId,
+ finalNoteText,
+ latitude,
+ longitude
+ ).run();
+ } catch (Exception e) {
+ Log.e(TAG, "Error during OSM Note upload", e);
+ runOnUiThread(() ->
+ Toast.makeText(this, R.string.osm_upload_error, Toast.LENGTH_SHORT).show()
+ );
+ } finally {
+ executor.shutdown();
+ }
+ });
+ }
}
diff --git a/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java b/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java
index 55d75c749..28c6c560b 100644
--- a/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java
+++ b/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java
@@ -1,184 +1,173 @@
package net.osmtracker.osm;
import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.ProgressDialog;
-import android.content.DialogInterface;
-import android.content.SharedPreferences.Editor;
-import android.os.AsyncTask;
-import android.preference.PreferenceManager;
import android.util.Log;
+import android.view.Gravity;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.preference.PreferenceManager;
import net.osmtracker.OSMTracker;
import net.osmtracker.R;
import net.osmtracker.db.DataHelper;
import net.osmtracker.util.DialogUtils;
+import java.lang.ref.WeakReference;
+
import de.westnordost.osmapi.OsmConnection;
import de.westnordost.osmapi.common.errors.OsmAuthorizationException;
import de.westnordost.osmapi.common.errors.OsmBadUserInputException;
import de.westnordost.osmapi.map.data.OsmLatLon;
-import de.westnordost.osmapi.notes.Note; // Note object
-import de.westnordost.osmapi.notes.NotesApi; // Api for uploading notes to OSM
-import de.westnordost.osmapi.map.data.LatLon; // Data type for location points, maybe I'll put it in the dialog file
+import de.westnordost.osmapi.notes.NotesApi;
/**
* Uploads a note to OpenStreetMap
*
* @author Most of the code was made by Nicolas Guillaumin, adapted by Jose Andrés Vargas Serrano
*/
-public class UploadToOpenStreetMapNotesTask extends AsyncTask {
+public class UploadToOpenStreetMapNotesTask {
private static final String TAG = UploadToOpenStreetMapNotesTask.class.getSimpleName();
- /** Upload progress dialog */
- private ProgressDialog dialog;
+ // Result constants
+ private static final int RESULT_OK = 1;
+ private static final int RESULT_ERROR_INTERNAL = -1;
+ private static final int RESULT_ERROR_AUTH = -2;
+ private static final int RESULT_ERROR_OSM_USER = -3;
- private final Activity activity;
+ private final WeakReference activityRef;
private final String accessToken;
+ //OSM Note data
private final long noteId;
-
- /** Note text */
private final String noteText;
-
- /** Note longitude */
private final double longitude;
-
- /** Note latitude */
private final double latitude;
- /**
- * Error message, or text of the response returned by OSM
- * if the request completed
- */
+ private AlertDialog progressDialog;
+ // Error message returned by OSM if the request completed
private String errorMsg;
+ private int resultCode = RESULT_ERROR_INTERNAL;
- /**
- * Either the HTTP result code, or -1 for an internal error
- */
- private int resultCode = -1;
- private final int authorizationErrorResultCode = -2;
- private final int anotherErrorResultCode = -3;
- private final int okResultCode = 1;
-
- // Not using an activity yet
- public UploadToOpenStreetMapNotesTask(Activity activity, String accessToken, long noteId,
+ public UploadToOpenStreetMapNotesTask(Activity activity, String accessToken, long noteId,
String noteText, double latitude, double longitude) {
- this.activity = activity;
- this.accessToken = accessToken;
+ this.activityRef = new WeakReference<>(activity);
+ this.accessToken = accessToken;
this.noteId = noteId;
- this.noteText = noteText;
- this.longitude = longitude;
- this.latitude = latitude;
- }
-
- @Override
- protected void onPreExecute() {
- try {
- // Display progress dialog
- dialog = new ProgressDialog(activity);
- dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
- dialog.setIndeterminate(true);
- dialog.setTitle(R.string.osm_note_upload);
-
- dialog.setCancelable(false);
- dialog.show();
-
- } catch (Exception e) {
- Log.e(TAG, "onPreExecute() failed", e);
- errorMsg = e.getLocalizedMessage();
- cancel(true);
- }
- }
-
- @Override
- protected void onPostExecute(Void result) {
- switch (resultCode) {
- case -1:
- dialog.dismiss();
- // Internal error, the request didn't start at all
- DialogUtils.showErrorDialog(activity,
- activity.getResources().getString(R.string.osm_note_upload_error)
- + ": " + errorMsg);
- break;
- case okResultCode:
- dialog.dismiss();
- // Success ! Update database and close activity
- DataHelper.setNoteUploadDate(noteId, System.currentTimeMillis(), activity.getContentResolver());
-
- new AlertDialog.Builder(activity)
- .setTitle(android.R.string.dialog_alert_title)
- .setIcon(android.R.drawable.ic_dialog_info)
- .setMessage(R.string.osm_upload_sucess)
- .setCancelable(true)
- .setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- activity.finish();
- }
- }).create().show();
-
- break;
- case authorizationErrorResultCode:
- dialog.dismiss();
- Log.e(TAG, "onPostExecute() authorization failed: " + errorMsg + " (" + resultCode + ")");
- // Authorization issue. Provide a way to clear credentials
- new AlertDialog.Builder(activity)
- .setTitle(android.R.string.dialog_alert_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.osm_note_upload_unauthorized)
- .setCancelable(true)
- .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- }
- })
- .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Editor editor = PreferenceManager.getDefaultSharedPreferences(activity).edit();
- editor.remove(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN);
- editor.commit();
-
- dialog.dismiss();
- }
- }).create().show();
- break;
-
- default:
- // Another error. Display OSM response
- dialog.dismiss();
- // Internal error, the request didn't start at all
- Log.e(TAG, "onPostExecute() default failed: " + errorMsg + " (" + resultCode + ")");
- DialogUtils.showErrorDialog(activity,
- activity.getResources().getString(R.string.osm_note_upload_error)
- + ": " + errorMsg);
- }
- }
-
- @Override
- protected Void doInBackground(Void... params) {
- OsmConnection osm = new OsmConnection(OpenStreetMapConstants.Api.OSM_API_URL_PATH,
- OpenStreetMapConstants.OAuth2.USER_AGENT, accessToken);
-
- try {
- LatLon point = new OsmLatLon(latitude, longitude);
- Note note = new NotesApi(osm).create(point, noteText);
- resultCode = okResultCode;
- } catch (/*IOException |*/ IllegalArgumentException | OsmBadUserInputException e) {
- Log.d(TAG, e.getMessage());
- resultCode = -1; //internal error.
- } catch (OsmAuthorizationException oae) {
- Log.d(TAG, "OsmAuthorizationException");
- resultCode = authorizationErrorResultCode;
- } catch (Exception e) {
- Log.e(TAG, e.getMessage());
- resultCode = anotherErrorResultCode;
- }
- return null;
- }
+ this.noteText = noteText;
+ this.longitude = longitude;
+ this.latitude = latitude;
+ }
+
+ /**
+ * Synchronous execution for use with ExecutorService.
+ * Replaces the lifecycle of AsyncTask. */
+ public void run() {
+ final Activity activity = activityRef.get();
+ if (activity == null || activity.isFinishing()) return;
+
+ // 1. Prepare UI (Equivalent to onPreExecute)
+ activity.runOnUiThread(() -> progressDialog = createProgressDialog(activity));
+
+ // 2. Execute Network Logic (Equivalent to doInBackground)
+ OsmConnection osm = new OsmConnection(
+ OpenStreetMapConstants.Api.OSM_API_URL_PATH,
+ OpenStreetMapConstants.OAuth2.USER_AGENT,
+ accessToken);
+
+ try {
+ new NotesApi(osm).create(new OsmLatLon(latitude, longitude), noteText);
+ resultCode = RESULT_OK;
+ } catch (OsmBadUserInputException e) {
+ Log.e(TAG, "Bad OSM user input or illegal argument", e);
+ errorMsg = e.getLocalizedMessage();
+ resultCode = RESULT_ERROR_OSM_USER;
+ } catch (OsmAuthorizationException oae) {
+ Log.e(TAG, "OSM Authorization error", oae);
+ errorMsg = oae.getLocalizedMessage();
+ resultCode = RESULT_ERROR_AUTH;
+ } catch (Exception e) {
+ Log.e(TAG, "Upload error", e);
+ errorMsg = e.getLocalizedMessage();
+ resultCode = RESULT_ERROR_INTERNAL;
+ }
+
+ // 3. Handle Results (Equivalent to onPostExecute)
+ activity.runOnUiThread(() -> {
+ Activity act = activityRef.get();
+ if (act == null || act.isFinishing()) return;
+
+ if (progressDialog != null && progressDialog.isShowing()) {
+ progressDialog.dismiss();
+ }
+
+ handleResult(act);
+ });
+ }
+
+ private void handleResult(Activity activity) {
+ switch (resultCode) {
+ case RESULT_OK:
+ DataHelper.setNoteUploadDate(noteId,
+ System.currentTimeMillis(), activity.getContentResolver());
+ new AlertDialog.Builder(activity)
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .setMessage(R.string.osm_upload_sucess)
+ .setPositiveButton(android.R.string.ok,
+ (d, w) -> activity.finish())
+ .show();
+ break;
+
+ case RESULT_ERROR_AUTH:
+ showAuthErrorDialog(activity);
+ break;
+
+ default:
+ DialogUtils.showErrorDialog(activity,
+ activity.getString(R.string.osm_note_upload_error) + ": " + errorMsg);
+ break;
+ }
+ }
+
+ private AlertDialog createProgressDialog(Activity activity) {
+ LinearLayout layout = new LinearLayout(activity);
+ layout.setOrientation(LinearLayout.HORIZONTAL);
+ layout.setPadding(50, 50, 50, 50);
+ layout.setGravity(Gravity.CENTER_VERTICAL);
+
+ ProgressBar pb = new ProgressBar(activity);
+ pb.setIndeterminate(true);
+ layout.addView(pb);
+
+ TextView tv = new TextView(activity);
+ tv.setText(R.string.osm_note_upload);
+ tv.setPadding(40, 0, 0, 0);
+ layout.addView(tv);
+
+ AlertDialog progressDialog = new AlertDialog.Builder(activity)
+ .setView(layout).setCancelable(false).create();
+ progressDialog.show();
+ return progressDialog;
+ }
+
+ /**
+ * Helper to show the specific authorization error dialog
+ */
+ private void showAuthErrorDialog(Activity activity) {
+ new AlertDialog.Builder(activity)
+ .setTitle(android.R.string.dialog_alert_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.osm_note_upload_unauthorized)
+ .setCancelable(true)
+ .setNegativeButton(android.R.string.no, (d, w) -> d.dismiss())
+ .setPositiveButton(android.R.string.yes, (d, w) -> {
+ PreferenceManager.getDefaultSharedPreferences(activity).edit()
+ .remove(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN).apply();
+ d.dismiss();
+ }).show();
+ }
}
diff --git a/app/src/main/res/layout/osm_note_upload.xml b/app/src/main/res/layout/osm_note_upload.xml
index 268319633..17de9d4e7 100644
--- a/app/src/main/res/layout/osm_note_upload.xml
+++ b/app/src/main/res/layout/osm_note_upload.xml
@@ -5,45 +5,50 @@
android:padding="16dp">
+ android:gravity="center"
+ android:orientation="vertical">
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textStyle="bold" />
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="@android:color/holo_green_light" />
+ android:id="@+id/osm_note_footer"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@android:color/holo_green_light" />
+ android:orientation="horizontal">
+
-
-
-
+
diff --git a/app/src/test/java/net/osmtracker/activity/OpenStreetMapNotesUploadTest.java b/app/src/test/java/net/osmtracker/activity/OpenStreetMapNotesUploadTest.java
new file mode 100644
index 000000000..9c2bb99cf
--- /dev/null
+++ b/app/src/test/java/net/osmtracker/activity/OpenStreetMapNotesUploadTest.java
@@ -0,0 +1,107 @@
+package net.osmtracker.activity;
+
+import static org.junit.Assert.assertEquals;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.widget.TextView;
+
+import androidx.preference.PreferenceManager;
+import androidx.test.core.app.ApplicationProvider;
+
+import net.osmtracker.OSMTracker;
+import net.osmtracker.R;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowActivity;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 25)
+public class OpenStreetMapNotesUploadTest {
+
+ private Intent intent;
+
+ @Before
+ public void setUp() {
+ // Prepare a valid intent with extras
+ intent = new Intent(ApplicationProvider.getApplicationContext(),
+ OpenStreetMapNotesUpload.class);
+ intent.putExtra("noteId", 123L);
+ intent.putExtra("noteContent", "Test Note Content");
+ intent.putExtra("latitude", 45.0);
+ intent.putExtra("longitude", 9.0);
+ intent.putExtra("version", "3.0.1");
+ intent.putExtra("appName", "OSMTrackerTest");
+
+ // Reset preferences
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
+ ApplicationProvider.getApplicationContext());
+ prefs.edit().clear().apply();
+ }
+
+
+ /**
+ * Verification of UI Binding and Intent Data Extraction
+ */
+ @Test
+ public void onCreate_populatesViewsCorrectly() {
+ // Launch Activity
+ OpenStreetMapNotesUpload activity = Robolectric.buildActivity(
+ OpenStreetMapNotesUpload.class, intent)
+ .create()
+ .start()
+ .resume()
+ .get();
+
+ TextView noteContentView = activity.findViewById(R.id.wplist_item_name);
+ TextView noteFooterView = activity.findViewById(R.id.osm_note_footer);
+
+ // Verify content extracted from intent
+ assertEquals("Test Note Content", noteContentView.getText().toString());
+
+ //check footer is correctly constructed according to strings.xml
+ // Verify footer constructed with intent extras
+ String expectedFooter = activity.getString(
+ R.string.osm_note_footer,"OSMTrackerTest", "3.0.1");
+ assertEquals(expectedFooter, noteFooterView.getText().toString());
+ }
+
+
+ /**
+ * Flow Control - Existing token should skip Auth and trigger task directly
+ */
+ @Test
+ public void startUpload_withExistingToken_skipsAuthFlow() {
+ // Inject a fake token into SharedPreferences
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
+ ApplicationProvider.getApplicationContext());
+ prefs.edit().putString(OSMTracker.Preferences.KEY_OSM_OAUTH2_ACCESSTOKEN,
+ "fake_token").commit();
+
+ OpenStreetMapNotesUpload activity = Robolectric.buildActivity(
+ OpenStreetMapNotesUpload.class, intent)
+ .create()
+ .start()
+ .resume()
+ .get();
+
+ // Trigger OK button
+ activity.findViewById(R.id.osm_note_upload_button_ok).performClick();
+
+ // Verify that NO new activity was started
+ // because it bypassed auth and went to background task
+ ShadowActivity shadowActivity = shadowOf(activity);
+ Intent startedIntent = shadowActivity.getNextStartedActivity();
+
+ Assert.assertNull("Should not start Auth browser if token is already present",
+ startedIntent);
+ }
+
+}
From 7da026bcd41d2f2ae54a07dd8bbb5ee1d175a063 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Wed, 21 Jan 2026 10:33:21 -0600
Subject: [PATCH 48/60] Refactor: Delete EditNoteDialogOnClickListener Java
class
---
.../EditNoteDialogOnClickListener.java | 24 -------------------
1 file changed, 24 deletions(-)
delete mode 100644 app/src/main/java/net/osmtracker/listener/EditNoteDialogOnClickListener.java
diff --git a/app/src/main/java/net/osmtracker/listener/EditNoteDialogOnClickListener.java b/app/src/main/java/net/osmtracker/listener/EditNoteDialogOnClickListener.java
deleted file mode 100644
index 3152cc9a7..000000000
--- a/app/src/main/java/net/osmtracker/listener/EditNoteDialogOnClickListener.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package net.osmtracker.listener;
-
-import android.app.AlertDialog;
-import android.database.Cursor;
-import android.view.View;
-
-/**
- * Class that implements an OnClickListener to display an edit note dialog.
- */
-public class EditNoteDialogOnClickListener implements View.OnClickListener {
-
- private Cursor cursor;
-
- protected AlertDialog alert;
-
- protected EditNoteDialogOnClickListener(AlertDialog alert, Cursor cu) {
- this.cursor = cu; // Assigns the received cursor to the class attribute
- this.alert = alert; // Assigns the received alert to the class attribute
- }
-
- @Override
- public void onClick(View view) {
- }
-}
From 4e2c374ce7012848184cae79bd83e835b51d6ee4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Thu, 22 Jan 2026 10:05:21 -0600
Subject: [PATCH 49/60] Refactor(test): Replace PowerMock with Mockito
---
app/build.gradle | 3 -
.../activity/ButtonsPresetsTest.java | 439 ++++++----------
.../net/osmtracker/data/WayPointMocks.java | 4 -
.../net/osmtracker/db/model/TrackTest.java | 19 +-
.../gpx/ExportToStorageTaskTest.java | 467 ++++++------------
.../layout/DownloadCustomLayoutTaskTest.java | 82 ++-
.../util/CustomLayoutsUtilsTest.java | 152 +++---
.../osmtracker/util/ThemeValidatorTest.java | 109 ++--
.../net/osmtracker/util/URLCreatorTest.java | 63 +--
.../net/osmtracker/util/UnitTestUtils.java | 48 --
10 files changed, 469 insertions(+), 917 deletions(-)
delete mode 100644 app/src/test/java/net/osmtracker/util/UnitTestUtils.java
diff --git a/app/build.gradle b/app/build.gradle
index 0d1a11d90..a7b6d9f61 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -114,9 +114,6 @@ dependencies {
// Mockito framework
testImplementation "org.mockito:mockito-core:3.12.4"
- testImplementation 'org.powermock:powermock-core:2.0.9'
- testImplementation 'org.powermock:powermock-api-mockito2:2.0.9'
- testImplementation 'org.powermock:powermock-module-junit4:2.0.9'
// Required for local unit tests. Prevent null in JSONObject, JSONArray, etc.
testImplementation 'org.json:json:20240303'
diff --git a/app/src/test/java/net/osmtracker/activity/ButtonsPresetsTest.java b/app/src/test/java/net/osmtracker/activity/ButtonsPresetsTest.java
index 0786ac9d5..e41c0e96e 100644
--- a/app/src/test/java/net/osmtracker/activity/ButtonsPresetsTest.java
+++ b/app/src/test/java/net/osmtracker/activity/ButtonsPresetsTest.java
@@ -1,313 +1,184 @@
package net.osmtracker.activity;
import android.content.SharedPreferences;
+import android.view.View;
import android.widget.CheckBox;
-import android.content.Context;
-import android.content.res.Resources;
import android.os.Environment;
-import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import net.osmtracker.OSMTracker;
import net.osmtracker.R;
+import org.junit.Assert;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
-import org.powermock.reflect.Whitebox;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowEnvironment;
import java.lang.reflect.Field;
import java.io.File;
import java.lang.reflect.Method;
import java.util.Hashtable;
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.verify;
-import static org.powermock.api.mockito.PowerMockito.mock;
-
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-import static org.powermock.api.mockito.PowerMockito.verifyPrivate;
-import static org.powermock.api.mockito.PowerMockito.when;
+import static org.junit.Assert.assertEquals;
import androidx.preference.PreferenceManager;
-@RunWith(PowerMockRunner.class)
-@PowerMockIgnore("jdk.internal.reflect.*")
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 25)
public class ButtonsPresetsTest {
-
-
- // Variables used in selectLayoutTest
ButtonsPresets activity;
- private CheckBox pressedCheckbox, priorSelectedCheckbox;
- private Field selectedField;
- private String label = "SOME LABEL";
- SharedPreferences.Editor mockEditor;
- SharedPreferences mockPrefs;
- Field layoutFileNamesField;
- Field prefsField;
- Hashtable mockHash;
-
- @Test
- public void getIsoTest(){
- ButtonsPresets activity = new ButtonsPresets();
-
- int VALUE = 0, EXPECTED = 1;
- String cases[][] = {
- {"test_es.xml", "es"},
- {"a_ge.xml", "ge"},
- {"en_fr.xml", "fr"},
- {"foo_en.xml", "en"},
- {"en.xml", "en"},
- }; // TODO add invalid cases when the real method is improved (now fails with outOfBounds)
-
- try{
- // Make the method callable
- Method getIso = activity.getClass().getDeclaredMethod("getIso", new Class[]{String.class});
- getIso.setAccessible(true);
- String result, expected;
- for(String[] option : cases){
- result = (String) getIso.invoke(activity, option[VALUE]);
- expected = option[EXPECTED];
- assertEquals(expected, result);
- }
-
-
- }catch(Exception e){
- e.printStackTrace();
- }
-
- }
-
- @Test
- public void selectLayoutTest() throws Exception {
- try {
- setupMocksForSelectLayoutTest();
- callSelectLayout();
- makeAssertionsForSelectLayout();
- }catch (Exception e){
- System.out.println("Error testing selectLayout method");
- e.printStackTrace();
- fail();
- }
-
-
- }
-
- @Test
- @PrepareForTest({ButtonsPresets.class, Environment.class})
- public void refreshActivityTest() {
- try {
- ButtonsPresets mockActivity = mock(ButtonsPresets.class);
-
- injectMockHashtable(mockActivity, 1); // to check it's reset actually
-
- LinearLayout downloadedLayouts = mock(LinearLayout.class);
- LinearLayout defaultSection = mock(LinearLayout.class);
-
-
- when(mockActivity,"findViewById", R.id.list_layouts).thenReturn(downloadedLayouts);
- when(mockActivity,"findViewById",R.id.buttons_presets).thenReturn(defaultSection);
-
-
- mockStatic(Environment.class);
- when(mockActivity.getExternalFilesDir(null)).thenReturn(new File(""));
-
- // Actual method call
- when(mockActivity,"refreshActivity").thenCallRealMethod();
- mockActivity.refreshActivity();
-
- Hashtable internalHash = Whitebox.getInternalState(mockActivity.getClass(), "layoutsFileNames");
- assertEquals(0, internalHash.size());
-
- // Check internal method calls happen
- verifyPrivate(mockActivity).invoke("listLayouts", downloadedLayouts);
- verifyPrivate(mockActivity).invoke("checkCurrentLayout", downloadedLayouts, defaultSection);
-
- }catch (Exception e){
- e.printStackTrace();
- System.out.println("Error testing refresh activity");
- fail();
- }
- }
-
- @Test
- @PrepareForTest(PreferenceManager.class)
- public void initializeAttributesTest() {
- ButtonsPresets mockActivity = mock(ButtonsPresets.class);
-
- // Setup the mock resources
- Resources mockResources = mock(Resources.class);
- when(mockResources.getString(R.string.prefs_ui_buttons_layout)).thenReturn("fooTitle");
- when(mockActivity.getResources()).thenReturn(mockResources);
-
- // Mock the shared preferences
- mockDefaultSharedPreferences(mockActivity);
-
- // Call actual method
- callInitializeAttributes(mockActivity);
-
- // Check internal methods calls
- verify(mockActivity).setTitle("fooTitle");
- verify(mockActivity).setContentView(R.layout.buttons_presets);
-
- // Check attributes are set
- checkAttributesAfterInitialization(mockActivity);
- }
-
- @Test
- @PrepareForTest({File.class, Preferences.class, Environment.class})
- public void listLayoutsTest(){
- for (int i = 0; i <= 1 ; i++) {
- try {
- doTestListLayouts(i);
- }
- catch (Exception e){
- e.printStackTrace();
- fail();
- }
- }
- }
-
-
-
-
-
- // TODO: make this test complete after refactoring the method,
- // now tests only a small part because couldn't be tested deeply
- // without refactoring or making it public
- private void doTestListLayouts(int numberOfDownloadedLayouts) throws Exception {
- ButtonsPresets mockActivity = mock(ButtonsPresets.class);
-
- mockStatic(Environment.class); // avoid getExternal call
-
- TextView mockEmpyText = mock(TextView.class);
- when(mockActivity.findViewById(R.id.btnpre_empty)).thenReturn(mockEmpyText);
-
- CheckBox mockDefaultcheckBox = mock(CheckBox.class);
- when(mockDefaultcheckBox.getText()).thenReturn("foo");
- when(mockActivity.findViewById(R.id.def_layout)).thenReturn(mockDefaultcheckBox);
-
- injectMockHashtable(mockActivity, numberOfDownloadedLayouts);
-
- callListLayouts(mockActivity, null);
-
- int expectedVisibility = (numberOfDownloadedLayouts > 1) ? View.INVISIBLE : View.VISIBLE;
- verify(mockEmpyText).setVisibility(expectedVisibility);
-
-
-
-
- }
-
- private void callListLayouts(ButtonsPresets mockActivity, LinearLayout mockRootLayout) {
- try {
- Method m = ButtonsPresets.class.getDeclaredMethod("listLayouts", LinearLayout.class);
- m.setAccessible(true);
- m.invoke(mockActivity, mockRootLayout);
- }catch (Exception e){
- e.printStackTrace();
- fail();
- }
- }
-
- private void mockDefaultSharedPreferences(Context context) {
- SharedPreferences mockPrefs = mock(SharedPreferences.class);
- mockStatic(PreferenceManager.class);
- when(PreferenceManager.getDefaultSharedPreferences(context)).thenReturn(mockPrefs);
- }
-
- private void checkAttributesAfterInitialization(ButtonsPresets mockActivity) {
- Hashtable hashtable = Whitebox.getInternalState(mockActivity.getClass(), "layoutsFileNames");
- Object listener = Whitebox.getInternalState(mockActivity, "listener");
- Object sharedPrefs = Whitebox.getInternalState(mockActivity, "prefs");
- String storageDir = Whitebox.getInternalState(mockActivity.getClass(),"storageDir");
-
- assertTrue(sharedPrefs instanceof SharedPreferences);
- assertEquals(0, hashtable.size());
- assertNotNull(listener);
- assertEquals(File.separator+OSMTracker.Preferences.VAL_STORAGE_DIR, storageDir);
- }
-
- private void callInitializeAttributes(ButtonsPresets mockActivity) {
- try {
- Method m = ButtonsPresets.class.getDeclaredMethod("initializeAttributes");
- m.setAccessible(true);
- m.invoke(mockActivity);
- }catch (Exception e){
- e.printStackTrace();
- fail();
- }
- }
-
- private void injectMockHashtable(ButtonsPresets mockActivity, int numberOfEntries) {
- Hashtable internalHash = new Hashtable();
- for (int i = 0; i < numberOfEntries; i++) {
- internalHash.put("foo", "bar");
- }
- Whitebox.setInternalState(mockActivity.getClass(), "layoutsFileNames", internalHash);
- }
-
- private void setupMocksForSelectLayoutTest() throws Exception{
-
- activity = new ButtonsPresets();
-
- // Mock a selected checkbox
- pressedCheckbox = mock(CheckBox.class);
- when(pressedCheckbox.getText()).thenReturn(label);
-
- // Mock and set a previously selected checkbox
- priorSelectedCheckbox = mock(CheckBox.class);
- selectedField = activity.getClass().getDeclaredField("selected");
- selectedField.setAccessible(true);
- selectedField.set(activity, priorSelectedCheckbox);
-
- // Mock and set the PreferencesEditor
- mockEditor = mock(SharedPreferences.Editor.class);
- when(mockEditor.commit()).thenReturn(true);
- when(mockEditor.putString("ui.buttons.layout", label)).thenReturn(mockEditor);
-
- // Mock prefs to use the mock editor
- mockPrefs = mock(SharedPreferences.class);
- when(mockPrefs.edit()).thenReturn(mockEditor);
-
- // Mock and set the preferences
- prefsField = activity.getClass().getDeclaredField("prefs");
- prefsField.setAccessible(true);
- prefsField.set(activity, mockPrefs);
-
- // Mock and set the layoutFilenames Hashtable
- mockHash = mock(Hashtable.class);
- when(mockHash.get(label)).thenReturn(label);
- layoutFileNamesField = activity.getClass().getDeclaredField("layoutsFileNames");
- layoutFileNamesField.setAccessible(true);
- layoutFileNamesField.set(activity, mockHash);
- }
-
- private void callSelectLayout() throws Exception{
- Method selectLayoutMethod = activity.getClass().getDeclaredMethod("selectLayout", CheckBox.class);
- selectLayoutMethod.setAccessible(true);
- selectLayoutMethod.invoke(activity, pressedCheckbox);
- }
-
- private void makeAssertionsForSelectLayout() throws Exception{
- // Make sure the previously selected is unchecked
- verify(priorSelectedCheckbox).setChecked(false);
-
- // Make sure the just selected is checked
- verify(pressedCheckbox).setChecked(true);
-
- // Make sure selected variable is updated to match the just selected
- assertEquals(selectedField.get(activity), pressedCheckbox);
-
- // Make sure the value in SharedPreferences is updated to match the just selected
- verify(mockEditor).putString(OSMTracker.Preferences.KEY_UI_BUTTONS_LAYOUT, label);
- verify(mockEditor).commit();
- }
+ @Before
+ public void setUp() {
+ // Build and start the activity lifecycle
+ activity = Robolectric.buildActivity(ButtonsPresets.class)
+ .create()
+ .start()
+ .resume()
+ .get();
+ }
+
+ @Test
+ public void getIsoTest() throws Exception {
+ int VALUE = 0, EXPECTED = 1;
+ String[][] cases = {
+ {"test_es.xml", "es"},
+ {"a_ge.xml", "ge"},
+ {"en_fr.xml", "fr"},
+ {"foo_en.xml", "en"},
+ {"en.xml", "en"},
+ };
+
+ Method getIso = ButtonsPresets.class.getDeclaredMethod("getIso", String.class);
+ getIso.setAccessible(true);
+
+ for (String[] option : cases) {
+ String result = (String) getIso.invoke(activity, option[VALUE]);
+ assertEquals(option[EXPECTED], result);
+ }
+ }
+
+ @Test
+ public void testSelectLayout_UpdatesUIAndPreferences() throws Exception {
+ // 1. Setup: Create two CheckBoxes to simulate "old" and "new" selection
+ CheckBox oldCheckBox = new CheckBox(activity);
+ oldCheckBox.setText("Default");
+ oldCheckBox.setChecked(true);
+
+ CheckBox newCheckBox = new CheckBox(activity);
+ newCheckBox.setText("Cycling");
+ newCheckBox.setChecked(false);
+
+ // 2. Setup: Populate the internal Hashtable and 'selected' field via reflection
+ Hashtable mockLayouts = new Hashtable<>();
+ mockLayouts.put("Default", "default.xml");
+ mockLayouts.put("Cycling", "cycling_en.xml");
+
+ setInternalState(ButtonsPresets.class, "layoutsFileNames", mockLayouts);
+ setInternalState(activity, "selected", oldCheckBox);
+
+ // 3. Execution: Invoke the private selectLayout method
+ Method selectLayoutMethod =
+ ButtonsPresets.class.getDeclaredMethod("selectLayout", CheckBox.class);
+ selectLayoutMethod.setAccessible(true);
+ selectLayoutMethod.invoke(activity, newCheckBox);
+
+ // 4. Assertions: Verify UI State
+ Assert.assertFalse("Previous checkbox should be unchecked", oldCheckBox.isChecked());
+ Assert.assertTrue("New checkbox should be checked", newCheckBox.isChecked());
+
+ // 5. Assertions: Verify Internal State
+ CheckBox currentSelected = (CheckBox) getInternalState(activity, "selected");
+ Assert.assertEquals(
+ "Internal 'selected' field should be updated",newCheckBox, currentSelected);
+
+ // 6. Assertions: Verify Persistence in SharedPreferences
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
+ String savedLayout = prefs.getString(OSMTracker.Preferences.KEY_UI_BUTTONS_LAYOUT, null);
+ Assert.assertEquals("SharedPreferences should store the filename from the map",
+ "cycling_en.xml", savedLayout);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked") // Suppress cast warning for the internal Hashtable
+ public void testRefreshActivity_PopulatesUIFromFilesystem() throws Exception {
+ // 1. Setup: Mock the SD Card being mounted
+ ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED);
+
+ // 2. Setup: Create dummy layout files in the expected directory
+ // The path logic in ButtonsPresets uses: getExternalFilesDir(null) + /osmtracker/layouts
+ File externalDir = activity.getExternalFilesDir(null);
+ File layoutsDir = new File(externalDir, "osmtracker" + File.separator + "layouts");
+ if (!layoutsDir.exists()) {
+ Assert.assertTrue(layoutsDir.mkdirs());
+ }
+
+ // Create two dummy layout files
+ File layout1 = new File(layoutsDir, "hiking_en.xml");
+ File layout2 = new File(layoutsDir, "cycling_es.xml");
+ Assert.assertTrue(layout1.createNewFile());
+ Assert.assertTrue(layout2.createNewFile());
+
+ // 3. Execution: Trigger the refresh
+ activity.refreshActivity();
+
+ // 4. Assertions: Check internal state (the Hashtable)
+ // We use reflection to get the private static field 'layoutsFileNames'
+ Hashtable layoutsMap = (Hashtable) getInternalState(
+ ButtonsPresets.class, "layoutsFileNames");
+
+ Assert.assertNotNull("Hashtable should be initialized", layoutsMap);
+ // Map should contain 'hiking', 'cycling', and 'Default' (from defaultCheckBox)
+ Assert.assertTrue("Should contain 'hiking' layout", layoutsMap.containsKey("hiking"));
+ Assert.assertTrue("Should contain 'cycling' layout", layoutsMap.containsKey("cycling"));
+
+ // 5. Assertions: Check UI state (the LinearLayout)
+ LinearLayout listLayouts = activity.findViewById(R.id.list_layouts);
+
+ // Count how many CheckBoxes were added.
+ // listLayouts should contain CheckBoxes for every file found.
+ int checkBoxCount = 0;
+ for (int i = 0; i < listLayouts.getChildCount(); i++) {
+ if (listLayouts.getChildAt(i) instanceof CheckBox) {
+ checkBoxCount++;
+ }
+ }
+
+ assertEquals("Two checkboxes should have been added to the UI", 2, checkBoxCount);
+
+ // 6. Verification: Check 'Empty Message' visibility
+ TextView emptyText = activity.findViewById(R.id.btnpre_empty);
+ Assert.assertEquals("Empty message should be INVISIBLE because layouts exist",
+ View.INVISIBLE, emptyText.getVisibility());
+ }
+
+ private void setInternalState(Object target, String fieldName, Object value) throws Exception {
+ 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 getInternalState(Object target, String fieldName) throws Exception {
+ 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);
+ }
}
diff --git a/app/src/test/java/net/osmtracker/data/WayPointMocks.java b/app/src/test/java/net/osmtracker/data/WayPointMocks.java
index adc23b6f6..9f82405ac 100644
--- a/app/src/test/java/net/osmtracker/data/WayPointMocks.java
+++ b/app/src/test/java/net/osmtracker/data/WayPointMocks.java
@@ -3,10 +3,6 @@
import net.osmtracker.db.model.WayPoint;
-import java.util.Date;
-
-import static net.osmtracker.util.UnitTestUtils.createDateFrom;
-
public class WayPointMocks {
// WayPoints of gpx-test.gpx
diff --git a/app/src/test/java/net/osmtracker/db/model/TrackTest.java b/app/src/test/java/net/osmtracker/db/model/TrackTest.java
index fa02164cb..d0ce59190 100644
--- a/app/src/test/java/net/osmtracker/db/model/TrackTest.java
+++ b/app/src/test/java/net/osmtracker/db/model/TrackTest.java
@@ -1,29 +1,32 @@
package net.osmtracker.db.model;
-import android.content.ContentResolver;
import android.database.Cursor;
import net.osmtracker.db.TrackContentProvider;
-import net.osmtracker.db.model.Track;
import static net.osmtracker.db.TrackContentProvider.Schema.*;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
-import static org.powermock.api.mockito.PowerMockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
-//@RunWith(PowerMockRunner.class)
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 25)
public class TrackTest {
final long START_DATE = 123;
final String NAME = "some name";
final String DESCRIPTION = "foo desc";
final String TAGS = "tag1,tag2,tag3";
- final List TAGS_LIST = Arrays.asList("tag1","tag2","tag3");
+ final List TAGS_LIST = Arrays.asList("tag1","tag2","tag3");
final String VISIBILITY = Track.OSMVisibility.Public.name();
final int TRACKPOINT_COUNT = 10;
final int WAYPOINT_COUNT = 20;
@@ -60,12 +63,10 @@ public Cursor initMockCursor(){
@Test
public void testBuild(){
- int trackId = 1;
- ContentResolver resolver = null; // Not used in the method
Cursor mockCursor = initMockCursor();
boolean withExtraInfo = false;
- Track t = Track.build(1, mockCursor, resolver, withExtraInfo);
+ Track t = Track.build(1, mockCursor, null, withExtraInfo);
try {
@@ -86,7 +87,7 @@ public void testBuild(){
}catch (Exception e){
- e.printStackTrace();
+ throw new RuntimeException("Reflection failed during Track fields verification", e);
}
}
}
diff --git a/app/src/test/java/net/osmtracker/gpx/ExportToStorageTaskTest.java b/app/src/test/java/net/osmtracker/gpx/ExportToStorageTaskTest.java
index 59b5b8013..51a024b24 100644
--- a/app/src/test/java/net/osmtracker/gpx/ExportToStorageTaskTest.java
+++ b/app/src/test/java/net/osmtracker/gpx/ExportToStorageTaskTest.java
@@ -1,355 +1,172 @@
package net.osmtracker.gpx;
import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertNull;
-import static net.osmtracker.OSMTracker.Preferences;
-import static net.osmtracker.OSMTracker.Preferences.KEY_OUTPUT_FILENAME;
-import static net.osmtracker.OSMTracker.Preferences.VAL_OUTPUT_FILENAME;
import static net.osmtracker.db.TrackContentProvider.Schema;
-import static net.osmtracker.util.UnitTestUtils.createDateFrom;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.powermock.api.mockito.PowerMockito.mock;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-import static org.powermock.api.mockito.PowerMockito.when;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.SharedPreferences;
-import android.content.res.Resources;
import android.database.Cursor;
import android.os.Environment;
import androidx.preference.PreferenceManager;
+import androidx.test.core.app.ApplicationProvider;
-import net.osmtracker.OSMTracker;
-import net.osmtracker.R;
+import net.osmtracker.OSMTracker.Preferences;
import net.osmtracker.db.DataHelper;
import net.osmtracker.db.model.Track;
import net.osmtracker.exception.ExportTrackException;
-import org.junit.After;
+import org.junit.Assert;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowEnvironment;
+
import java.io.File;
+import java.util.Calendar;
import java.util.Date;
+import java.util.TimeZone;
-@RunWith(PowerMockRunner.class)
-@PrepareForTest({Environment.class, PreferenceManager.class})
-@PowerMockIgnore("jdk.internal.reflect.*")
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 25)
public class ExportToStorageTaskTest {
- @Rule
- private final TemporaryFolder temporaryFolder = new TemporaryFolder();
- private final Context mockContext = mock(Context.class);
- private final DataHelper mockDataHelper = mock(DataHelper.class);
- private final SharedPreferences mockPrefs = mock(SharedPreferences.class);
- private final Resources mockResources = mock(Resources.class);
-
- private ExportToStorageTask task;
-
- private static final String ERROR_CREATE_TRACK_DIR = "Error creating track directory";
- private static final String UNABLE_TO_WRITE_STORAGE = "Unable to write to external storage";
-
- @Before
- public void setUp() {
- mockStatic(Environment.class);
- mockStatic(PreferenceManager.class);
-
- when(PreferenceManager.getDefaultSharedPreferences(mockContext)).thenReturn(mockPrefs);
- when(mockContext.getResources()).thenReturn(mockResources);
- when(mockResources.getString(R.string.error_create_track_dir)).thenReturn(ERROR_CREATE_TRACK_DIR);
- when(mockResources.getString(R.string.error_externalstorage_not_writable)).thenReturn(UNABLE_TO_WRITE_STORAGE);
-
- task = new ExportToStorageTask(mockContext, mockDataHelper, 1L);
- }
-
- @After
- public void tearDown() {
- temporaryFolder.delete();
- }
-
- @Test
- public void testBuildGPXFilenameUsingOnlyTrackName() {
- String trackNameInDatabase = "MyTrack";
- Date trackStartDate = createDateFrom(2000, 1, 2, 3, 4, 5);
- String preferenceSetting = Preferences.VAL_OUTPUT_FILENAME_NAME;
-
- String expectedFilename = "MyTrack";
- if(!(Preferences.VAL_OUTPUT_FILENAME_LABEL.equals("")))expectedFilename += "_";
- expectedFilename += Preferences.VAL_OUTPUT_FILENAME_LABEL+".gpx";
-
- doTestBuildGPXFilename(trackNameInDatabase, preferenceSetting, trackStartDate.getTime(), expectedFilename);
- }
-
- @Test
- public void testBuildGPXFilenameUsingTrackNameAndStartDate() {
- String trackNameInDatabase = "MyTrack";
- Date trackStartDate = createDateFrom(2000, 1, 2, 3, 4, 5);
- String preferenceSetting = Preferences.VAL_OUTPUT_FILENAME_NAME_DATE;
-
- String expectedFilename = "MyTrack_2000-01-02_03-04-05";
- if(!(Preferences.VAL_OUTPUT_FILENAME_LABEL.equals(""))) expectedFilename += "_";
- expectedFilename += Preferences.VAL_OUTPUT_FILENAME_LABEL+".gpx";
-
- doTestBuildGPXFilename(trackNameInDatabase, preferenceSetting, trackStartDate.getTime(), expectedFilename);
- }
-
- @Test
- public void testBuildGPXFilenameUsingStartDateAndTrackName() {
- String trackNameInDatabase = "MyTrack";
- Date trackStartDate = createDateFrom(2000, 1, 2, 3, 4, 5);
- String preferenceSetting = Preferences.VAL_OUTPUT_FILENAME_DATE_NAME;
-
- String expectedFilename = "2000-01-02_03-04-05_MyTrack";
- if(!(Preferences.VAL_OUTPUT_FILENAME_LABEL.equals(""))) expectedFilename += "_";
- expectedFilename += Preferences.VAL_OUTPUT_FILENAME_LABEL+".gpx";
-
- doTestBuildGPXFilename(trackNameInDatabase, preferenceSetting, trackStartDate.getTime(), expectedFilename);
- }
-
- @Test
- public void testBuildGPXFilenameUsingOnlyStartDate() {
- String trackNameInDatabase = "MyTrack";
- Date trackStartDate = createDateFrom(2000, 1, 2, 3, 4, 5);
- String preferenceSetting = Preferences.VAL_OUTPUT_FILENAME_DATE;
-
- String expectedFilename = "2000-01-02_03-04-05";
- if(!(Preferences.VAL_OUTPUT_FILENAME_LABEL.equals(""))) expectedFilename += "_";
- expectedFilename += Preferences.VAL_OUTPUT_FILENAME_LABEL+".gpx";
-
- doTestBuildGPXFilename(trackNameInDatabase, preferenceSetting, trackStartDate.getTime(), expectedFilename);
- }
-
- @Test
- public void testBuildGPXFilenameWhenSanitizesTrackName() {
- String trackNameInDatabase = ":M/y*T@r~a\\c?k:";
- Date trackStartDate = createDateFrom(2000, 1, 2, 3, 4, 5);
- String preferenceSetting = Preferences.VAL_OUTPUT_FILENAME_NAME;
-
- String expectedFilename = ";M_y_T_r_a_c_k;";
- if(!(Preferences.VAL_OUTPUT_FILENAME_LABEL.equals(""))) expectedFilename += "_";
- expectedFilename += Preferences.VAL_OUTPUT_FILENAME_LABEL+".gpx";
-
- doTestBuildGPXFilename(trackNameInDatabase, preferenceSetting, trackStartDate.getTime(), expectedFilename);
- }
-
- @Test
- public void testBuildGPXFilenameWhenUsesTrackNameButThereIsNoName() {
- String trackNameInDatabase = "";
- Date trackStartDate = createDateFrom(2000, 1, 2, 3, 4, 5);
- String preferenceSetting = Preferences.VAL_OUTPUT_FILENAME_NAME;
-
- String expectedFilename = "2000-01-02_03-04-05";
- if(!(Preferences.VAL_OUTPUT_FILENAME_LABEL.equals(""))) expectedFilename += "_";
- expectedFilename += Preferences.VAL_OUTPUT_FILENAME_LABEL+".gpx";
-
- doTestBuildGPXFilename(trackNameInDatabase, preferenceSetting, trackStartDate.getTime(), expectedFilename);
- }
-
- @Test
- public void testBuildGPXFilenameWhenUsesTrackNameAndStartDateButThereIsNoName() {
- String trackNameInDatabase = "";
- Date trackStartDate = createDateFrom(2000, 1, 2, 3, 4, 5);
- String preferenceSetting = Preferences.VAL_OUTPUT_FILENAME_NAME_DATE;
-
- String expectedFilename = "2000-01-02_03-04-05";
- if(!(Preferences.VAL_OUTPUT_FILENAME_LABEL.equals(""))) expectedFilename += "_";
- expectedFilename += Preferences.VAL_OUTPUT_FILENAME_LABEL+".gpx";
-
- doTestBuildGPXFilename(trackNameInDatabase, preferenceSetting, trackStartDate.getTime(), expectedFilename);
- }
-
- private void doTestBuildGPXFilename(String trackName, String desiredFormat, long trackStartDate, String expectedFilename) {
- when(mockPrefs.getString(KEY_OUTPUT_FILENAME, VAL_OUTPUT_FILENAME)).thenReturn(desiredFormat);
-
- String result = task.buildGPXFilename(createMockCursor(trackName, trackStartDate), temporaryFolder.getRoot());
-
- assertEquals(expectedFilename, result);
- }
-
- private Cursor createMockCursor(String trackName, long trackStartDate) {
- Cursor mockCursor = mock(Cursor.class);
- when(mockCursor.getColumnIndex(Schema.COL_NAME)).thenReturn(1);
- when(mockCursor.getString(1)).thenReturn(trackName);
-
- when(mockCursor.getColumnIndex(Schema.COL_START_DATE)).thenReturn(2);
- when(mockCursor.getLong(2)).thenReturn(trackStartDate);
-
- return mockCursor;
- }
-
- @Test
- public void testGetExportDirectoryWhenStorageIsWritableAndDirExists() throws Exception {
- // Mocking external storage state
- when(Environment.getExternalStorageState()).thenReturn(Environment.MEDIA_MOUNTED);
-
- // Mocking preferences and context
- when(mockPrefs.getString(any(), any())).thenReturn(OSMTracker.Preferences.VAL_STORAGE_DIR);
- when(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)).thenReturn(temporaryFolder.getRoot());
-
- var osmTrackerFolder = temporaryFolder.newFolder("osmtracker");
-
- // Creating task and invoking method
- File exportDirectory = task.getExportDirectory(new Date());
-
- // Verifying the directory path
- assertEquals(osmTrackerFolder.getAbsolutePath(), exportDirectory.getAbsolutePath());
- }
-
- @Test
- public void testGetExportDirectoryWhenStorageIsNotWritable() {
- // Mocking external storage state
- when(Environment.getExternalStorageState()).thenReturn(Environment.MEDIA_MOUNTED_READ_ONLY);
-
- // Verifying the exception
- assertThrows(ExportTrackException.class, () -> task.getExportDirectory(new Date()));
- }
-
- @Test
- public void testGetExportDirectoryWhenStorageIsWritableAndDirNotExists() throws Exception {
- // Mocking external storage state
- when(Environment.getExternalStorageState()).thenReturn(Environment.MEDIA_MOUNTED);
-
- // Mocking preferences and context
- when(mockPrefs.getString(any(), any())).thenReturn(OSMTracker.Preferences.VAL_STORAGE_DIR);
- when(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)).thenReturn(temporaryFolder.getRoot());
-
- // Creating task and invoking method
- File exportDirectory = task.getExportDirectory(new Date());
-
- var osmTrackerFolder = new File(temporaryFolder.getRoot(), OSMTracker.Preferences.VAL_STORAGE_DIR);
-
- // Verifying the directory path
- assertEquals(osmTrackerFolder.getAbsolutePath(), exportDirectory.getAbsolutePath());
- }
-
- @Test
- public void testGetExportDirectoryWhenDirDoesNotExistAndCreatesIt() throws Exception {
- // Mocking external storage state
- when(Environment.getExternalStorageState()).thenReturn(Environment.MEDIA_MOUNTED);
-
- // Mocking preferences and context
- when(mockPrefs.getString(any(), any())).thenReturn("NonExistentDir");
-
- // Creating task and invoking method
- File exportDirectory = task.getExportDirectory(new Date());
-
- // Verifying the directory creation
- assertEquals(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "NonExistentDir").getAbsolutePath(), exportDirectory.getAbsolutePath());
- }
-
- @Test
- public void testGetSanitizedTrackNameByStartDateWithValidTrackName() {
- // Mock track data
- Track mockTrack = new Track();
- mockTrack.setName("My/Track");
- when(mockDataHelper.getTrackByStartDate(any(Date.class))).thenReturn(mockTrack);
-
- // Execute the method
- String result = task.getSanitizedTrackNameByStartDate(new Date());
-
- // Verify the sanitized track name
- assertEquals("My_Track", result);
- }
-
- @Test
- public void testGetSanitizedTrackNameByStartDateWithEmptyTrackName() {
- // Mock track data
- Track mockTrack = new Track();
- mockTrack.setName("");
- when(mockDataHelper.getTrackByStartDate(any(Date.class))).thenReturn(mockTrack);
+ private Context context;
+ private DataHelper mockDataHelper;
+ private ExportToStorageTask task;
+ private SharedPreferences prefs;
+
+ // Standard test date: Jan 2nd, 2000, 03:04:05 UTC
+ private static final String DATE_STRING = "2000-01-02_03-04-05";
+ private static final String TRACK_NAME = "MyTrack";
+
+ @Before
+ public void setUp() {
+ context = ApplicationProvider.getApplicationContext();
+ mockDataHelper = mock(DataHelper.class);
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ // Reset preferences to prevent test-to-test leakage
+ prefs.edit().clear().apply();
+ task = new ExportToStorageTask(context, mockDataHelper, 1L);
+ }
+
+ // --- Filename Generation Tests ---
+
+ @Test
+ public void testBuildGPXFilename_OnlyTrackName() {
+ setupFilenamePreference(Preferences.VAL_OUTPUT_FILENAME_NAME);
+ Assert.assertEquals("MyTrack.gpx",
+ executeBuildFilename(TRACK_NAME, createDate()));
+ }
+
+ @Test
+ public void testBuildGPXFilename_TrackNameAndDate() {
+ setupFilenamePreference(Preferences.VAL_OUTPUT_FILENAME_NAME_DATE);
+ assertEquals("MyTrack_" + DATE_STRING + ".gpx",
+ executeBuildFilename(TRACK_NAME, createDate()));
+ }
+
+ @Test
+ public void testBuildGPXFilename_DateAndTrackName() {
+ setupFilenamePreference(Preferences.VAL_OUTPUT_FILENAME_DATE_NAME);
+ assertEquals(DATE_STRING + "_MyTrack" + ".gpx",
+ executeBuildFilename(TRACK_NAME, createDate()));
+ }
+
+ @Test
+ public void testBuildGPXFilename_OnlyDate() {
+ setupFilenamePreference(Preferences.VAL_OUTPUT_FILENAME_DATE);
+ assertEquals(DATE_STRING + ".gpx",
+ executeBuildFilename(TRACK_NAME, createDate()));
+ }
+
+ @Test
+ public void testBuildGPXFilename_Sanitization() {
+ String dirtyName = ":M/y*T@r~a\\c?k:";
+ setupFilenamePreference(Preferences.VAL_OUTPUT_FILENAME_NAME);
+ assertEquals(";M_y_T_r_a_c_k;.gpx",
+ executeBuildFilename(dirtyName, createDate()));
+ }
+
+ @Test
+ public void testBuildGPXFilename_FallbackToDateWhenNameEmpty() {
+ String emptyName = "";
+ setupFilenamePreference(Preferences.VAL_OUTPUT_FILENAME_NAME);
+ // Should fallback to the timestamp if name is missing
+ assertEquals(DATE_STRING + ".gpx",
+ executeBuildFilename(emptyName, createDate()));
+ }
+
+ // --- Export Directory Tests ---
+
+ @Test
+ public void testGetExportDirectory_CreatesMissingFolders() throws Exception {
+ ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED);
+ prefs.edit().putString(Preferences.KEY_STORAGE_DIR, "NewAppFolder").apply();
+
+ File result = task.getExportDirectory(new Date());
+
+ assertTrue("Folder should be created", result.exists());
+ assertTrue("Path should contain custom dir name", result.getAbsolutePath().contains("NewAppFolder"));
+ }
+
+ @Test
+ public void testGetExportDirectory_ThrowsWhenNotWritable() {
+ ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED_READ_ONLY);
+ assertThrows(ExportTrackException.class, () -> task.getBaseExportDirectory());
+ }
+
+ @Test
+ public void testGetSanitizedTrackName_ReplacesSlashes() {
+ Track mockTrack = new Track();
+ mockTrack.setName("Category/Sub/Track");
+ when(mockDataHelper.getTrackByStartDate(any(Date.class))).thenReturn(mockTrack);
+
+ String result = task.getSanitizedTrackNameByStartDate(new Date());
+ assertEquals("Category_Sub_Track", result);
+ }
+
+ // --- Internal Helpers ---
+
+ private void setupFilenamePreference(String format) {
+ prefs.edit()
+ .putString(Preferences.KEY_OUTPUT_FILENAME, format)
+ // Reset label for predictability
+ .putString(Preferences.KEY_OUTPUT_FILENAME_LABEL, "")
+ .apply();
+ }
+
+ private String executeBuildFilename(String name, Date date) {
+ return task.buildGPXFilename(createMockCursor(name, date.getTime()), context.getCacheDir());
+ }
+
+ private Cursor createMockCursor(String trackName, long trackStartDate) {
+ Cursor mockCursor = mock(Cursor.class);
+ when(mockCursor.getColumnIndex(Schema.COL_NAME)).thenReturn(1);
+ when(mockCursor.getString(1)).thenReturn(trackName);
+ when(mockCursor.getColumnIndex(Schema.COL_START_DATE)).thenReturn(2);
+ when(mockCursor.getLong(2)).thenReturn(trackStartDate);
+ return mockCursor;
+ }
+
+ /**
+ * Creates a UTC Date representing 2000-01-02 03:04:05.
+ */
+ private static Date createDate() {
+ Calendar cal = Calendar.getInstance(TimeZone.getDefault());
+ // Calendar months are 0-based (January is 0), so we subtract 1 from the input
+ cal.set(2000, Calendar.JANUARY, 2, 3, 4, 5);
+ cal.set(Calendar.MILLISECOND, 0);
+ return cal.getTime();
+
+ }
- // Execute the method
- String result = task.getSanitizedTrackNameByStartDate(new Date());
-
- // Verify the sanitized track name
- assertEquals("", result);
- }
-
- @Test
- public void testGetSanitizedTrackNameByStartDateWithNullTrackName() {
- // Mock track data
- Track mockTrack = new Track();
- mockTrack.setName(null);
- when(mockDataHelper.getTrackByStartDate(any(Date.class))).thenReturn(mockTrack);
-
- // Execute the method
- String result = task.getSanitizedTrackNameByStartDate(new Date());
-
- // Verify the sanitized track name
- assertNull(result);
- }
-
- @Test
- public void testGetSanitizedTrackNameByStartDateWithSpecialCharacters() {
- // Mock track data
- Track mockTrack = new Track();
- mockTrack.setName("/M/y/T/r/@/c/k/");
- when(mockDataHelper.getTrackByStartDate(any(Date.class))).thenReturn(mockTrack);
-
- // Execute the method
- String result = task.getSanitizedTrackNameByStartDate(new Date());
-
- // Verify the sanitized track name
- assertEquals("_M_y_T_r_@_c_k_", result);
- }
-
- @Test
- public void testGetSanitizedTrackNameByStartDateWithNoTrackFound() {
- // Mock no track data
- when(mockDataHelper.getTrackByStartDate(any(Date.class))).thenReturn(null);
-
- // Execute the method
- String result = task.getSanitizedTrackNameByStartDate(new Date());
-
- // Verify the sanitized track name
- assertEquals("", result);
- }
-
- @Test
- public void testConstructorCallsSuperclassConstructor() {
- long trackId = 1L;
-
- // Use a spy to verify the constructor call
- var taskSpy = new ExportToStorageTask(mockContext, trackId);
-
- assertTrue(taskSpy.exportMediaFiles());
- assertTrue(taskSpy.updateExportDate());
- }
-
- @Test
- public void testCreateDirectoryPerTrack() throws Exception{
- when(Environment.getExternalStorageState()).thenReturn(Environment.MEDIA_MOUNTED);
- when(mockPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true);
- when(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)).thenReturn(temporaryFolder.getRoot());
- when(mockPrefs.getString(any(), any())).thenReturn(OSMTracker.Preferences.VAL_STORAGE_DIR);
- Track mockTrack = new Track();
- mockTrack.setName("MyTrack");
- when(mockDataHelper.getTrackByStartDate(any(Date.class))).thenReturn(mockTrack);
-
- task.getExportDirectory(new Date());
- }
-
- @Test
- public void testCreateDirectoryPerTrackEmptyTrackname() throws Exception{
- when(Environment.getExternalStorageState()).thenReturn(Environment.MEDIA_MOUNTED);
- when(mockPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true);
- when(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)).thenReturn(temporaryFolder.getRoot());
- when(mockPrefs.getString(any(), any())).thenReturn(OSMTracker.Preferences.VAL_STORAGE_DIR);
- Track mockTrack = new Track();
- mockTrack.setName("");
- when(mockDataHelper.getTrackByStartDate(any(Date.class))).thenReturn(mockTrack);
-
- task.getExportDirectory(new Date());
- }
}
diff --git a/app/src/test/java/net/osmtracker/layout/DownloadCustomLayoutTaskTest.java b/app/src/test/java/net/osmtracker/layout/DownloadCustomLayoutTaskTest.java
index 9410e7c21..a81041611 100644
--- a/app/src/test/java/net/osmtracker/layout/DownloadCustomLayoutTaskTest.java
+++ b/app/src/test/java/net/osmtracker/layout/DownloadCustomLayoutTaskTest.java
@@ -3,81 +3,69 @@
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
-import android.util.Log;
import net.osmtracker.OSMTracker;
import net.osmtracker.db.DataHelper;
-import net.osmtracker.util.UnitTestUtils;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowEnvironment;
import java.io.File;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
-import static org.powermock.api.mockito.PowerMockito.mock;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-import static org.powermock.api.mockito.PowerMockito.when;
import androidx.preference.PreferenceManager;
+import androidx.test.core.app.ApplicationProvider;
-@RunWith(PowerMockRunner.class)
-@PrepareForTest({PreferenceManager.class, Environment.class, Log.class})
-@PowerMockIgnore("jdk.internal.reflect.*")
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 25)
public class DownloadCustomLayoutTaskTest {
DownloadCustomLayoutTask downloadCustomLayoutTask;
+ private Context context;
- Context mockContext;
- SharedPreferences mockPrefs;
-
- //FIXME: layout name and iso are coded.
String layoutName = "abc";
String iso = "en";
String expectedLayoutFilename = "abc_en.xml";
- public void setupMocks() {
- // Create SharedPreferences mock
- mockPrefs = mock(SharedPreferences.class);
- UnitTestUtils.setLayoutsTestingRepository(mockPrefs);
+ @Before
+ public void setUp() {
+ context = ApplicationProvider.getApplicationContext();
- // Create PreferenceManager mock
- mockContext = mock(Context.class);
- mockStatic(PreferenceManager.class);
- when(PreferenceManager.getDefaultSharedPreferences(mockContext)).thenReturn(mockPrefs);
- // external storage is writeable
- mockStatic(Environment.class);
- when(Environment.getExternalStorageState()).thenReturn(Environment.MEDIA_MOUNTED);
- // log
- mockStatic(Log.class);
+ // Setup real SharedPreferences logic
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ prefs.edit()
+ .putString(OSMTracker.Preferences.KEY_GITHUB_USERNAME, "labexp")
+ .putString(OSMTracker.Preferences.KEY_REPOSITORY_NAME, "osmtracker-android-layouts")
+ .putString(OSMTracker.Preferences.KEY_BRANCH_NAME, "for_tests")
+ .apply();
- downloadCustomLayoutTask = new DownloadCustomLayoutTask(mockContext);
- }
+ // Setup Environment Shadow
+ ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED);
- @Test
- public void downloadLayoutWithoutIconsTest() {
- setupMocks();
+ downloadCustomLayoutTask = new DownloadCustomLayoutTask(context);
+ }
- boolean result = downloadCustomLayoutTask.downloadLayout(layoutName,iso);
- assertEquals(true, result);
+ @Test
+ public void downloadLayoutWithoutIconsTest() {
+ boolean result = downloadCustomLayoutTask.downloadLayout(layoutName, iso);
+ assertTrue("Download should return true", result);
- // Check if layout was downloaded at .../osmtracker/layouts/abc_en.xml
- String expectedLayoutFilePath = mockContext.getExternalFilesDir(null)
- + OSMTracker.Preferences.VAL_STORAGE_DIR + File.separator
- + DataHelper.LAYOUTS_SUBDIR + File.separator
- + expectedLayoutFilename;
+ // Check if layout was downloaded at .../osmtracker/layouts/abc_en.xml
+ File layoutsDir = new File(context.getExternalFilesDir(null),
+ OSMTracker.Preferences.VAL_STORAGE_DIR + File.separator + DataHelper.LAYOUTS_SUBDIR);
- System.out.println(expectedLayoutFilePath);
- File layoutFile = new File(expectedLayoutFilePath);
- assertTrue(layoutFile.exists());
+ File layoutFile = new File(layoutsDir, expectedLayoutFilename);
- // Add N icons to abc layout and check if the N icons are downloaded
- // at ... /osmtracker/layouts/abc_icons.
+ System.out.println("Expected path: " + layoutFile.getAbsolutePath());
+ assertTrue("Layout file should exist at path", layoutFile.exists());
- }
+ // Add N icons to abc layout and check if the N icons are downloaded
+ // at ... /osmtracker/layouts/abc_icons.
+ }
}
diff --git a/app/src/test/java/net/osmtracker/util/CustomLayoutsUtilsTest.java b/app/src/test/java/net/osmtracker/util/CustomLayoutsUtilsTest.java
index 05faea75e..7d1b0052e 100644
--- a/app/src/test/java/net/osmtracker/util/CustomLayoutsUtilsTest.java
+++ b/app/src/test/java/net/osmtracker/util/CustomLayoutsUtilsTest.java
@@ -2,117 +2,77 @@
import android.content.Context;
import android.content.SharedPreferences;
-import android.content.res.AssetManager;
import net.osmtracker.OSMTracker;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
-import java.io.BufferedReader;
-import java.io.FileInputStream;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
-import java.util.Scanner;
import static org.junit.Assert.assertEquals;
-import static org.powermock.api.mockito.PowerMockito.mock;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-import static org.powermock.api.mockito.PowerMockito.when;
import androidx.preference.PreferenceManager;
+import androidx.test.core.app.ApplicationProvider;
-@RunWith(PowerMockRunner.class)
-@PrepareForTest(PreferenceManager.class)
-@PowerMockIgnore("jdk.internal.reflect.*")
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 25)
public class CustomLayoutsUtilsTest {
- Context mockContext;
- SharedPreferences mockPrefs;
- AssetManager mockAssetManager;
- InputStream resultStream;
- InputStream expectedStream;
-
- public void setupMocks() {
- // Create SharedPreferences mock
- mockPrefs = mock(SharedPreferences.class);
- when(mockPrefs.getString(OSMTracker.Preferences.KEY_UI_BUTTONS_LAYOUT,
- OSMTracker.Preferences.VAL_UI_BUTTONS_LAYOUT))
- .thenReturn("transporte publico");
-
- // Create PreferenceManager mock
- mockContext = mock(Context.class);
-
- mockStatic(PreferenceManager.class);
-
- when(PreferenceManager.getDefaultSharedPreferences(mockContext)).thenReturn(mockPrefs);
-
- mockAssetManager = mock(AssetManager.class);
-
- try {
- resultStream = new FileInputStream("./src/test/assets/gpx/gpx-test.gpx");
- expectedStream = new FileInputStream("./src/test/assets/gpx/gpx-test.gpx");
- when(mockContext.getAssets()).thenReturn(mockAssetManager);
- when(mockAssetManager.open("result.gpx")).thenReturn(resultStream);
- when(mockAssetManager.open("expected.gpx")).thenReturn(expectedStream);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- @Test
- public void convertFileName() {
- String result = CustomLayoutsUtils.convertFileName("public_transport.xml");
- String expected = "public transport";
- assertEquals(result, expected);
- }
-
- @Test
- public void unconvertFileName() {
- String result = CustomLayoutsUtils.unconvertFileName("public transport");
- String expected = "public_transport.xml";
- assertEquals(result, expected);
- }
-
- @Test
- public void createFileName() {
- String result = CustomLayoutsUtils.createFileName("public transport", "es");
- String expected = "public_transport_es.xml";
- assertEquals(result, expected);
- }
-
- @Test
- public void getStringFromStream() throws IOException {
- setupMocks();
-
- InputStream resultIs = mockAssetManager.open("result.gpx");
- String result = CustomLayoutsUtils.getStringFromStream(resultIs);
-
- String expected;
- try (InputStream expectedIs = mockAssetManager.open("expected.gpx");
- InputStreamReader expectedIsr = new InputStreamReader(expectedIs, StandardCharsets.UTF_8);
- BufferedReader expectedReader = new BufferedReader(expectedIsr)) {
- StringBuilder expectedBuilder = new StringBuilder();
- String line;
- while ((line = expectedReader.readLine()) != null) {
- expectedBuilder.append(line).append(System.lineSeparator());
- }
- expected = expectedBuilder.toString();
- }
- assertEquals("String should have same content", expected, result);
- }
-
- @Test
- public void getCurrentLayoutName() {
- setupMocks();
- String result = CustomLayoutsUtils.getCurrentLayoutName(mockContext);
- String expected = "transporte publico";
- assertEquals(result, expected);
- }
+ private Context context;
+ private SharedPreferences prefs;
+
+ @Before
+ public void setUp() {
+ context = ApplicationProvider.getApplicationContext();
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ // Ensure a clean state for every test
+ prefs.edit().clear().apply();
+ }
+
+ @Test
+ public void convertFileName() {
+ assertEquals("public transport", CustomLayoutsUtils.convertFileName("public_transport.xml"));
+ assertEquals("simple", CustomLayoutsUtils.convertFileName("simple.xml"));
+ }
+
+ @Test
+ public void unconvertFileName() {
+ assertEquals("public_transport.xml", CustomLayoutsUtils.unconvertFileName("public transport"));
+ }
+
+ @Test
+ public void createFileName() {
+ assertEquals("public_transport_es.xml", CustomLayoutsUtils.createFileName("public transport", "es"));
+ }
+
+ @Test
+ public void getStringFromStream() throws IOException {
+ String content = "GPX Test Content" + System.lineSeparator() + "Second Line";
+ InputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+ String result = CustomLayoutsUtils.getStringFromStream(inputStream);
+ assertEquals(content, result);
+ }
+
+ @Test
+ public void getCurrentLayoutName() {
+ // Set value in real Robolectric preferences
+ prefs.edit().putString(OSMTracker.Preferences.KEY_UI_BUTTONS_LAYOUT, "transporte publico").apply();
+ String result = CustomLayoutsUtils.getCurrentLayoutName(context);
+ assertEquals("transporte publico", result);
+ }
+
+ @Test
+ public void getCurrentLayoutName_ReturnsDefaultWhenEmpty() {
+ // Test fallback logic
+ String result = CustomLayoutsUtils.getCurrentLayoutName(context);
+ assertEquals(OSMTracker.Preferences.VAL_UI_BUTTONS_LAYOUT, result);
+ }
}
diff --git a/app/src/test/java/net/osmtracker/util/ThemeValidatorTest.java b/app/src/test/java/net/osmtracker/util/ThemeValidatorTest.java
index 8eeedd0a8..78be7b670 100644
--- a/app/src/test/java/net/osmtracker/util/ThemeValidatorTest.java
+++ b/app/src/test/java/net/osmtracker/util/ThemeValidatorTest.java
@@ -2,90 +2,81 @@
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
-import android.content.SharedPreferences.Editor;
import net.osmtracker.OSMTracker;
import net.osmtracker.R;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import static org.junit.Assert.*;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.verify;
-import static org.powermock.api.mockito.PowerMockito.mock;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-import static org.powermock.api.mockito.PowerMockito.when;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import androidx.preference.PreferenceManager;
+import androidx.test.core.app.ApplicationProvider;
-@RunWith(PowerMockRunner.class)
-@PrepareForTest(PreferenceManager.class)
-@PowerMockIgnore("jdk.internal.reflect.*")
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 25)
public class ThemeValidatorTest {
- Context mockContext;
- SharedPreferences mockPrefs;
- Resources mockRes;
- Editor mockEditor;
- /** Setup all the mocks(classes) that are used when calling the
- * ThemeValidator class with a selected theme
- * @param theme
- */
- public void setupMocks(String theme) {
+ private SharedPreferences realPrefs;
+ private Resources mockRes;
- mockPrefs = mock(SharedPreferences.class);
- when(mockPrefs.getString(OSMTracker.Preferences.KEY_UI_THEME,
- OSMTracker.Preferences.VAL_UI_THEME))
- .thenReturn(theme);
+ @Before
+ public void setUp() {
+ Context context = ApplicationProvider.getApplicationContext();
+ realPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ // Ensure a clean state for every test
+ realPrefs.edit().clear().commit();
+ mockRes = mock(Resources.class);
+ String[] themes = {
+ "net.osmtracker:style/DefaultTheme",
+ "net.osmtracker:style/DarkTheme",
+ "net.osmtracker:style/LightTheme",
+ "net.osmtracker:style/HighContrast"
+ };
+ when(mockRes.getStringArray(R.array.prefs_theme_values)).thenReturn(themes);
+ }
- String[] themes = { "net.osmtracker:style/DefaultTheme",
- "net.osmtracker:style/DarkTheme",
- "net.osmtracker:style/LightTheme",
- "net.osmtracker:style/HighContrast"};
-
- mockRes = mock(Resources.class);
- when(mockRes.getStringArray(R.array.prefs_theme_values))
- .thenReturn(themes);
-
-
- mockContext = mock(Context.class);
-
- mockStatic(PreferenceManager.class);
-
- when(PreferenceManager.getDefaultSharedPreferences(mockContext)).thenReturn(mockPrefs);
-
- mockEditor=mock(SharedPreferences.Editor.class);
-
- when(mockPrefs.edit())
- .thenReturn(mockEditor);
-
- }
@Test
public void validateDefaultTheme(){
- setupMocks("net.osmtracker:style/DefaultTheme");
- String result =ThemeValidator.getValidTheme(mockPrefs, mockRes);
+ // Set a valid theme in preferences
+ realPrefs.edit().putString(
+ OSMTracker.Preferences.KEY_UI_THEME,
+ "net.osmtracker:style/DefaultTheme")
+ .commit();
+
+ String result =ThemeValidator.getValidTheme(realPrefs, mockRes);
String expected = "net.osmtracker:style/DefaultTheme";
- assertEquals(result, expected);
+ assertEquals(expected, result);
}
/*Use a theme that is not included on the theme values array and also
* verify methods of the mocked editor so that the preferences are saved.*/
@Test
public void validateWrongTheme(){
-
- setupMocks("net.osmtracker:style/YellowTheme");
- String result =ThemeValidator.getValidTheme(mockPrefs, mockRes);
- String expected = "net.osmtracker:style/DefaultTheme";
- assertEquals(result, expected);
- verify(mockPrefs,atLeastOnce()).edit();
- verify(mockEditor,atLeastOnce()).putString(OSMTracker.Preferences.KEY_UI_THEME, OSMTracker.Preferences.VAL_UI_THEME);
- verify(mockEditor).commit();
- }
+ // Set an invalid theme in preferences
+ realPrefs.edit().putString(
+ OSMTracker.Preferences.KEY_UI_THEME,
+ "net.osmtracker:style/YellowTheme")
+ .commit();
+
+ // The validator should detect "YellowTheme" is missing from the Resources array
+ // and reset it to the default.
+ String result = ThemeValidator.getValidTheme(realPrefs, mockRes);
+ String expected = "net.osmtracker:style/DefaultTheme";
+
+ assertEquals("Should fallback to DefaultTheme", expected, result);
+
+ // Verify that the preference was actually updated/repaired in the storage
+ assertEquals("Preference should be repaired in storage",
+ expected, realPrefs.getString(OSMTracker.Preferences.KEY_UI_THEME, null));
+ }
}
diff --git a/app/src/test/java/net/osmtracker/util/URLCreatorTest.java b/app/src/test/java/net/osmtracker/util/URLCreatorTest.java
index 4b4888f9a..8aac745dc 100644
--- a/app/src/test/java/net/osmtracker/util/URLCreatorTest.java
+++ b/app/src/test/java/net/osmtracker/util/URLCreatorTest.java
@@ -1,83 +1,62 @@
package net.osmtracker.util;
import android.content.Context;
-import android.content.SharedPreferences;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import static org.junit.Assert.*;
-import static org.powermock.api.mockito.PowerMockito.mock;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-import static org.powermock.api.mockito.PowerMockito.when;
-import androidx.preference.PreferenceManager;
+import androidx.test.core.app.ApplicationProvider;
-
-@RunWith(PowerMockRunner.class)
-@PrepareForTest(PreferenceManager.class)
-@PowerMockIgnore("jdk.internal.reflect.*")
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 25)
public class URLCreatorTest {
- Context mockContext;
- SharedPreferences mockPrefs;
-
-
- public void setupMocks(){
- // Create SharedPreferences mock
- mockPrefs = mock(SharedPreferences.class);
-
- UnitTestUtils.setLayoutsDefaultRepository(mockPrefs);
+ private Context context;
- // Create PreferenceManager mock
- mockContext = mock(Context.class);
- mockStatic(PreferenceManager.class);
- when(PreferenceManager.getDefaultSharedPreferences(mockContext)).thenReturn(mockPrefs);
-
- }
+ @Before
+ public void setUp() {
+ context = ApplicationProvider.getApplicationContext();
+ }
@Test
public void createMetadataDirUrl() {
- setupMocks();
- String result = URLCreator.createMetadataDirUrl(mockContext);
- String expected = "https://api.github.com/repos/labexp/osmtracker-android-layouts/contents/layouts/metadata?ref=master";
- assertEquals(result, expected);
+ String result = URLCreator.createMetadataDirUrl(context);
+ String expected = "https://api.github.com/repos/labexp/osmtracker-android-layouts/contents/layouts/metadata?ref=master";
+ assertEquals(expected, result);
}
@Test
public void createMetadataFileURL() {
- setupMocks();
- String result = URLCreator.createMetadataFileURL(mockContext, "transporte_publico");
+ String result = URLCreator.createMetadataFileURL(context, "transporte_publico");
String expected = "https://raw.githubusercontent.com/labexp/osmtracker-android-layouts/master/layouts/metadata/transporte_publico.xml";
- assertEquals(result, expected);
+ assertEquals(expected, result);
}
@Test
public void createLayoutFileURL() {
- setupMocks();
- String result = URLCreator.createLayoutFileURL(mockContext, "hidrantes","es");
+ String result = URLCreator.createLayoutFileURL(context, "hidrantes","es");
String expected = "https://raw.githubusercontent.com/labexp/osmtracker-android-layouts/master/layouts/hidrantes/es.xml";
- assertEquals(result, expected);
+ assertEquals(expected, result);
}
@Test
public void createIconsDirUrl() {
- setupMocks();
- String result = URLCreator.createIconsDirUrl(mockContext, "hidrantes");
+ String result = URLCreator.createIconsDirUrl(context, "hidrantes");
String expected = "https://api.github.com/repos/labexp/osmtracker-android-layouts/contents/layouts/hidrantes/hidrantes_icons?ref=master";
- assertEquals(result, expected);
+ assertEquals(expected, result);
}
@Test
public void createTestURL() {
- setupMocks();
String result = URLCreator.createTestURL("labexp", "osmtracker-android-layouts", "master");
String expected = "https://api.github.com/repos/labexp/osmtracker-android-layouts/contents/layouts/metadata?ref=master";
- assertEquals(result, expected);
+ assertEquals(expected, result);
}
}
diff --git a/app/src/test/java/net/osmtracker/util/UnitTestUtils.java b/app/src/test/java/net/osmtracker/util/UnitTestUtils.java
deleted file mode 100644
index e45472c8f..000000000
--- a/app/src/test/java/net/osmtracker/util/UnitTestUtils.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package net.osmtracker.util;
-
-import android.content.SharedPreferences;
-
-import net.osmtracker.OSMTracker;
-
-import java.util.Date;
-
-import static org.powermock.api.mockito.PowerMockito.when;
-
-public class UnitTestUtils {
-
- public static String TESTING_GITHUB_USER = "labexp";
- public static String TESTING_GITHUB_REPOSITORY = "osmtracker-android-layouts";
- public static String TESTING_GITHUB_BRANCH = "for_tests";
-
- public static void setGithubRepositorySettings(SharedPreferences mockPrefs, String user,
- String repo, String branch) {
- when(mockPrefs.getString(OSMTracker.Preferences.KEY_GITHUB_USERNAME,
- OSMTracker.Preferences.VAL_GITHUB_USERNAME))
- .thenReturn(user);
-
- when(mockPrefs.getString(OSMTracker.Preferences.KEY_REPOSITORY_NAME,
- OSMTracker.Preferences.VAL_REPOSITORY_NAME))
- .thenReturn(repo);
-
- when(mockPrefs.getString(OSMTracker.Preferences.KEY_BRANCH_NAME,
- OSMTracker.Preferences.VAL_BRANCH_NAME))
- .thenReturn(branch);
- }
-
- public static void setLayoutsTestingRepository(SharedPreferences mockPrefs){
- setGithubRepositorySettings(mockPrefs, TESTING_GITHUB_USER, TESTING_GITHUB_REPOSITORY,
- TESTING_GITHUB_BRANCH);
- }
-
- public static void setLayoutsDefaultRepository(SharedPreferences mockPrefs){
- setGithubRepositorySettings(mockPrefs, OSMTracker.Preferences.VAL_GITHUB_USERNAME,
- OSMTracker.Preferences.VAL_REPOSITORY_NAME,
- OSMTracker.Preferences.VAL_BRANCH_NAME);
- }
-
- // This method is used to hide the weird modifications (offsets) that need to be made when creating a Date object
- // See why here https://docs.oracle.com/javase/8/docs/api/java/util/Date.html#setYear-int-
- public static Date createDateFrom(int year, int month, int day, int hour, int minute, int second) {
- return new Date(year-1900, month-1, day, hour, minute, second);
- }
-}
From 443098037e107c91349125b627aa5ee3e0a6ba0e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Thu, 22 Jan 2026 10:27:44 -0600
Subject: [PATCH 50/60] =?UTF-8?q?[Transifex]=20Updates=20for=20project=20O?=
=?UTF-8?q?SMTracker=20for=20Android=E2=84=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/src/main/res/values-el/strings.xml | 8 +++++++
app/src/main/res/values-es/strings.xml | 8 +++++++
app/src/main/res/values-nl/strings.xml | 8 +++++++
app/src/main/res/values-pl/strings.xml | 8 +++++++
app/src/main/res/values-pt-rBR/strings.xml | 8 +++++++
app/src/main/res/values-ru/strings.xml | 8 +++++++
app/src/main/res/values-sk/strings.xml | 8 +++++++
app/src/main/res/values-sv/strings.xml | 8 +++++++
.../res/values-zh-rTW/strings-preferences.xml | 16 ++++++++++++++
app/src/main/res/values-zh-rTW/strings.xml | 21 +++++++++++++++++++
10 files changed, 101 insertions(+)
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 7cc7add2d..4dc290f30 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -83,6 +83,8 @@
Ο εξυπηρετητής OSM απέδωσε σφάλμα: ({0}) μήνυμα {1}Σφάλμα επαλήθευσης. Θελε να εκκαθαρίσετε τα αποθηκευμένα πιστοποιητικά για το OSM;Αποστολή στο OpenStreetMap επιτυχής
+
+ ΑκύρωσηΗχογράφησηΦωτογράφιση
@@ -94,6 +96,12 @@
ΑκύρωσηΔιαγραφήΑκύρωση
+
+ Αποθήκευση
+ Διαγραφή
+ Ακύρωση
+ Διαγραφή
+ ΑκύρωσηΡυθμίσειςΣημεία
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 21e871f0d..a14147839 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -82,6 +82,8 @@
El servidor OSM ha devuelto un error: ({0}) mensaje {1}Error de autorización. ¿Desea borrar las credenciales de OSM guardadas?La subida a OpenStreetMap tuvo éxito
+
+ CancelarGrabar vozTomar foto
@@ -97,6 +99,12 @@
¿Eliminar este punto?EliminarCancelar
+
+ Guardar
+ Eliminar
+ Cancelar
+ Eliminar
+ CancelarConfiguraciónPuntos
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 3c786b6e6..b70ae59ef 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -83,6 +83,8 @@ Selecteer het in de lijst om door te gaan.Foutmelding van de OSM server: ({0}) bericht {1}Foute naam/wachtwoord. Wilt u de opgeslagen OpenStreetMap aanmeldgegevens wissen?Uploaden naar OpenStreetMap geslaagd
+
+ AnnulerenStemopnameFoto nemen
@@ -98,6 +100,12 @@ Selecteer het in de lijst om door te gaan.Dit referentiepunt verwijderen?VerwijderenAnnuleren
+
+ Opslaan
+ Verwijderen
+ Annuleren
+ Verwijderen
+ AnnulerenInstellingenReferentiepunten
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 4ec4eedb5..6d69f1b56 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -82,6 +82,8 @@
Serwer OSM zwrócił błąd ({0}) o treści {1}Błąd uwierzytelniania. Czy chcesz usunąć zapisane ustawienia uwierzytelniania OSM?Wysyłanie zakończone pomyślnie!
+
+ AnulowaćNagraj notkę
głosową
@@ -98,6 +100,12 @@ głosowąUsunąć ten punkt nawigacji?UsuwaćAnulować
+
+ Ratować
+ Usuwać
+ Anulować
+ Usuwać
+ AnulowaćUstawieniaPunkty nawigacji
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 5d7ac2ec9..53ff769bb 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -82,6 +82,8 @@
O servidor do OSM retornou um erro: ({0}) message {1}Erro de autorização. Gostaria de limpar as credencias do OpenStreetMaps salvas?Enviado com sucesso para o OpenStreetMap
+
+ CancelarGravar vozTirar foto
@@ -97,6 +99,12 @@
Apagar este ponto de referência?ApagarCancelar
+
+ Salvar
+ Apagar
+ Cancelar
+ Apagar
+ CancelarConfiguraçõesPontos de referência
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 5eeecd945..b653ab5ff 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -82,6 +82,8 @@
Сервер OSM вернул ошибку: ({0}) текст {1}Ошибка авторизации. Удалить сохраненные учетные данные OpenStreetMap?Загрузка на OpenStreetMap завершена
+
+ ОтменаЗапись аудиоСделать фото
@@ -97,6 +99,12 @@
Удалить эту точку?УдалитьОтмена
+
+ Сохранить
+ Удалить
+ Отмена
+ Удалить
+ ОтменаНастройкиПутевые точки
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 6d856b0d7..aaeda263e 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -82,6 +82,8 @@
Server OSM vrátil chybu: ({0}) správa o chybe {1}Chyba autorizácie. Chcete odstrániť poverenia pre OpenStreetMap uložené v zariadení?Odovzdávanie do OpenStreetMap bolo úspešné
+
+ ZrušiťHlasový záznamZachytiť fotografiu
@@ -97,6 +99,12 @@
Vymazať tento prechodný bod?OdstrániťZrušiť
+
+ Uložiť
+ Odstrániť
+ Zrušiť
+ Odstrániť
+ ZrušiťNastaveniaCestovné body
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 21e4e703f..9762f4e78 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -82,6 +82,8 @@
OSM-servern returnerade ett fel: ({0}) meddelande {1}Inloggningsfel. Vill du ta bort sparade inloggningsdata till OpenStreetMapUppladdning till OpenStreetMap klar
+
+ AvbokaRöstnoteringBildnotering
@@ -97,6 +99,12 @@
Vill du ta bort den här waypointen?RaderaAvboka
+
+ Spara
+ Radera
+ Avboka
+ Radera
+ AvbokaInställningarVägpunkter
diff --git a/app/src/main/res/values-zh-rTW/strings-preferences.xml b/app/src/main/res/values-zh-rTW/strings-preferences.xml
index 513d07a5c..c6f6f6ee5 100644
--- a/app/src/main/res/values-zh-rTW/strings-preferences.xml
+++ b/app/src/main/res/values-zh-rTW/strings-preferences.xml
@@ -1,5 +1,6 @@
+
設定GPS
@@ -11,6 +12,13 @@
忽略 GPS 的時間,改用 Android 系統時間記錄氣壓 [hPa]切換需要重新啟動軌跡記錄
+ 文字註解
+ 選取註解文字按鈕的值的儲存樣式
+
+ 路徑
+ OSM 註解
+ 全部
+ GPS 記錄頻率使用 0 會盡可能的記錄資料 (會影響電池續航力)秒
@@ -84,6 +92,14 @@
在 GPX 檔中填入 HDOP 近似值使用聲音當錄製語音期間可播放聲音
+ 軌跡可見度
+ 上傳到 openstreetmap.org 偏好的軌跡可見度
+
+ 私人
+ 可追蹤
+ 公開
+ 可識別
+ 重新設定 OSM 認證捨棄現存的 OSM 認證與權限資訊,強制 OSMTracker 重新取得它們您必須重新認證 OSMTracker 方能繼續上傳軌跡資料,確定嗎?
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 08732e854..3a9a6ab73 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -30,6 +30,7 @@
軌跡清單:航點:軌跡點:
+ 註記:您尚未有任何的軌跡紀錄資料。按壓來開始記錄新的軌跡。無法開始記錄新軌跡: {0}
@@ -67,6 +68,7 @@
描述標籤 (以半形逗號分隔)您必須填寫描述欄
+ 能見度私人公開可追蹤的
@@ -82,6 +84,14 @@
OSM 伺服器傳回錯誤: ({0}) 訊息 {1}認證錯誤。您想要清除現存的 OpenStreetMap 認證資料嗎?成功上傳至 OpenStreetMap
+
+ 上傳開放街圖註解
+ 上傳註解時發生錯誤
+ 註解文字
+ 上傳
+ 取消
+ 經由%1$s%2$s
+ 認證錯誤,如果你先前允許上傳軌跡的權限,你必須先清除已儲存的憑證,才有辦法認證 app 上傳軌跡以及註解。你想要清除已儲存的憑證嗎?錄製語音拍照
@@ -97,6 +107,17 @@
刪除這個路徑節點?刪除取消
+
+ 註解名稱/文字
+ 儲存
+ 刪除
+ 取消
+ 刪除註解
+ 刪除這個註解?
+ 刪除
+ 取消
+ 註解清單
+ 以 OSM 註解型式上傳設定航點
From 321d2614f135265b1715d39635cfc0cebb0cf75e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Thu, 22 Jan 2026 10:57:23 -0600
Subject: [PATCH 51/60] UI: Remove attention title in successful GPX upload to
OSM
---
.../main/java/net/osmtracker/osm/UploadToOpenStreetMapTask.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapTask.java b/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapTask.java
index 46d2c4a63..e4bb3c669 100644
--- a/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapTask.java
+++ b/app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapTask.java
@@ -124,7 +124,6 @@ protected void onPostExecute(Void result) {
DataHelper.setTrackUploadDate(trackId, System.currentTimeMillis(), activity.getContentResolver());
new AlertDialog.Builder(activity)
- .setTitle(android.R.string.dialog_alert_title)
.setIcon(android.R.drawable.ic_dialog_info)
.setMessage(R.string.osm_upload_sucess)
.setCancelable(true)
From 4986abf190b7431a3ab584905bb76caf78c1dd00 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Tue, 27 Jan 2026 13:13:25 -0600
Subject: [PATCH 52/60] Feature: Show note count in tracks list
---
.../activity/TrackListRVAdapter.java | 2 ++
.../net/osmtracker/db/TracklistAdapter.java | 4 ++-
app/src/main/res/layout/tracklist_item.xml | 25 +++++++++++++++++++
3 files changed, 30 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/net/osmtracker/activity/TrackListRVAdapter.java b/app/src/main/java/net/osmtracker/activity/TrackListRVAdapter.java
index 3f11a778b..230b51a16 100644
--- a/app/src/main/java/net/osmtracker/activity/TrackListRVAdapter.java
+++ b/app/src/main/java/net/osmtracker/activity/TrackListRVAdapter.java
@@ -52,6 +52,7 @@ public class TrackItemVH extends RecyclerView.ViewHolder
private final TextView vNameOrStartDate;
private final TextView vWps;
private final TextView vTps;
+ private final TextView vNotesCount;
private final ImageView vStatus;
private final ImageView vUploadStatus;
@@ -62,6 +63,7 @@ public TrackItemVH(View view) {
vNameOrStartDate = (TextView) view.findViewById(R.id.trackmgr_item_nameordate);
vWps = (TextView) view.findViewById(R.id.trackmgr_item_wps);
vTps = (TextView) view.findViewById(R.id.trackmgr_item_tps);
+ vNotesCount = view.findViewById(R.id.trackmgr_item_notes_count);
vStatus = (ImageView) view.findViewById(R.id.trackmgr_item_statusicon);
vUploadStatus = (ImageView) view.findViewById(R.id.trackmgr_item_upload_statusicon);
diff --git a/app/src/main/java/net/osmtracker/db/TracklistAdapter.java b/app/src/main/java/net/osmtracker/db/TracklistAdapter.java
index d99c4ced8..fe45edae6 100644
--- a/app/src/main/java/net/osmtracker/db/TracklistAdapter.java
+++ b/app/src/main/java/net/osmtracker/db/TracklistAdapter.java
@@ -54,6 +54,7 @@ private View bind(Cursor cursor, View v, Context context) {
TextView vNameOrStartDate = (TextView) v.findViewById(R.id.trackmgr_item_nameordate);
TextView vWps = (TextView) v.findViewById(R.id.trackmgr_item_wps);
TextView vTps = (TextView) v.findViewById(R.id.trackmgr_item_tps);
+ TextView vNotesCount = v.findViewById(R.id.trackmgr_item_notes_count);
ImageView vStatus = (ImageView) v.findViewById(R.id.trackmgr_item_statusicon);
ImageView vUploadStatus = (ImageView) v.findViewById(R.id.trackmgr_item_upload_statusicon);
@@ -86,10 +87,11 @@ private View bind(Cursor cursor, View v, Context context) {
String strTrackId = Long.toString(trackId);
vId.setText(strTrackId);
- // Bind WP count, TP count, name
+ // Bind WP count, TP count, Notes count, name
Track t = Track.build(trackId, cursor, context.getContentResolver(), false);
vTps.setText(Integer.toString(t.getTpCount()));
vWps.setText(Integer.toString(t.getWpCount()));
+ vNotesCount.setText(Integer.toString(t.getNoteCount()));
vNameOrStartDate.setText(t.getDisplayName());
return v;
diff --git a/app/src/main/res/layout/tracklist_item.xml b/app/src/main/res/layout/tracklist_item.xml
index 7818fe44c..6eff9b5fb 100644
--- a/app/src/main/res/layout/tracklist_item.xml
+++ b/app/src/main/res/layout/tracklist_item.xml
@@ -105,6 +105,31 @@
app:layout_constraintTop_toTopOf="@id/trackmgr_item_trackpoints"
app:layout_constraintBottom_toBottomOf="@id/trackmgr_item_trackpoints"/>
+
+
+
+
Date: Tue, 3 Feb 2026 17:56:20 -0600
Subject: [PATCH 53/60] Feature: Add a What's New slide to the App intro.
---
app/build.gradle | 1 +
.../java/net/osmtracker/activity/Intro.kt | 29 +++++++++++++++++--
app/src/main/res/values/strings.xml | 3 ++
3 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index a7b6d9f61..60b4726af 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -104,6 +104,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.preference:preference:1.2.0'
+ implementation 'androidx.preference:preference-ktx:1.2.1'
// Required -- JUnit 4 framework
testImplementation 'junit:junit:4.13.2'
diff --git a/app/src/main/java/net/osmtracker/activity/Intro.kt b/app/src/main/java/net/osmtracker/activity/Intro.kt
index 2b204f770..ff272334a 100644
--- a/app/src/main/java/net/osmtracker/activity/Intro.kt
+++ b/app/src/main/java/net/osmtracker/activity/Intro.kt
@@ -1,6 +1,8 @@
package net.osmtracker.activity
import android.os.Bundle
+import androidx.activity.enableEdgeToEdge
+import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import com.github.appintro.AppIntro
@@ -11,9 +13,24 @@ import androidx.core.content.edit
class Intro : AppIntro() {
override fun onCreate(savedInstanceState: Bundle?) {
+ // Enable Edge-to-Edge support. Must be called before super.onCreate()
+ enableEdgeToEdge()
super.onCreate(savedInstanceState)
// Make sure you don't call setContentView!
+ // Set the colors for the bottom bar elements
+ val activeColor = ContextCompat.getColor(this, R.color.colorAccent)
+ val inactiveColor = ContextCompat.getColor(this, R.color.colorPrimary)
+
+ setIndicatorColor(
+ selectedIndicatorColor = activeColor,
+ unselectedIndicatorColor = inactiveColor
+ )
+
+ setColorDoneText(activeColor)
+ setColorSkipButton(activeColor)
+ setNextArrowColor(activeColor)
+
// Call addSlide passing your Fragments.
// You can use AppIntroFragment to use a pre-built fragment
addSlide(AppIntroFragment.createInstance(
@@ -23,6 +40,14 @@ class Intro : AppIntro() {
description = getString(R.string.app_intro_slide1_description)
))
+ // Whats new Fragment
+ addSlide(AppIntroFragment.createInstance(
+ title = getString(R.string.app_intro_slide_whats_new_title),
+ imageDrawable = R.drawable.icon_100x100,
+ backgroundColorRes = R.color.appintro_background_color,
+ description = getString(R.string.app_intro_slide_whats_new_description)
+ ))
+
//TODO: change the image of slide number 2.
addSlide(AppIntroFragment.createInstance(
title = getString(R.string.app_intro_slide2_title),
@@ -40,8 +65,8 @@ class Intro : AppIntro() {
override fun onDonePressed(currentFragment: Fragment?) {
super.onDonePressed(currentFragment)
- // Decide what to do when the user clicks on "Done"
- PreferenceManager.getDefaultSharedPreferences(baseContext).edit {
+ // Use the KTX extension for cleaner SharedPreferences editing
+ PreferenceManager.getDefaultSharedPreferences(this).edit {
putBoolean(
OSMTracker.Preferences.KEY_DISPLAY_APP_INTRO,
false
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 381506590..277fe80e7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -274,6 +274,9 @@
Welcome to OSMTracker for Android™ 👋This App is free software that respects your freedom!
+
+ What\'s new in this version?
+ Now we can upload notes to OSM, and CyclOSM and OpenTopo map tiles are available again. Enjoy! Happy tracking 🗺 😎OSMTracker for Android will use your GPS location to record trackpoints and waypoints, even when the App is running in background.
From ed0d5eaf68183a0bcbd0d9c97fb2f0e57a4c7704 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Tue, 3 Feb 2026 18:35:13 -0600
Subject: [PATCH 54/60] =?UTF-8?q?[Transifex]=20Updates=20for=20project=20O?=
=?UTF-8?q?SMTracker=20for=20Android=E2=84=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../res/values-es/strings-preferences.xml | 7 +++++++
app/src/main/res/values-es/strings.xml | 13 +++++++++++++
.../res/values-pt-rBR/strings-preferences.xml | 19 +++++++++++++++++++
app/src/main/res/values-pt-rBR/strings.xml | 13 +++++++++++++
.../main/res/values/strings-preferences.xml | 1 -
5 files changed, 52 insertions(+), 1 deletion(-)
diff --git a/app/src/main/res/values-es/strings-preferences.xml b/app/src/main/res/values-es/strings-preferences.xml
index e0cce69d9..e3d20693b 100644
--- a/app/src/main/res/values-es/strings-preferences.xml
+++ b/app/src/main/res/values-es/strings-preferences.xml
@@ -11,6 +11,13 @@
Ignorar el reloj GPS y utilizar el reloj Android como marcas de tiempoRegistro de presión barométrica [hPa]Alternar requiere reiniciar la traza
+ Notas de texto
+ Escoja cómo se guardarán los valores del botón Nota de Texto de la disposición de botones
+
+ Waypoint
+ Nota de OSM
+ Ambas
+ Intervalo de registro de GPSUtilice 0 para el más breve posible (afecta a la vida de la batería)segundos
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index a14147839..43c47dedb 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -30,6 +30,7 @@
Lista de trazas:Puntos de ref.:P. de traza:
+ Notas:Usted no tiene ninguna traza.Presione para grabar una nueva traza.No se puede crear una nueva traza: {0}
@@ -67,6 +68,7 @@
Descripción: Etiquetas (delimitado por comas)Debe introducir una descripción
+ VisibilidadPrivadoPúblicoTrazable
@@ -83,7 +85,13 @@
Error de autorización. ¿Desea borrar las credenciales de OSM guardadas?La subida a OpenStreetMap tuvo éxito
+ Subir notas a OpenStreetMap
+ Error al subir la nota
+ Nota de texto
+ SubirCancelar
+ via %1$s %2$s
+ Error de autorización. Si usted previamente otorgó permisos a la aplicación para subir trazas, debe limpiar sus credenciales salvadas para autorizar la aplicación a subir trazas y notas. ¿Le gustaría limpiar sus credenciales de OpenStreetMap salvadas?Grabar vozTomar foto
@@ -100,11 +108,16 @@
EliminarCancelar
+ Texto/Nombre de la NotaGuardarEliminarCancelar
+ Eliminar nota
+ ¿Eliminar esta nota?EliminarCancelar
+ Lista de notas
+ Subir como nota de OSMConfiguraciónPuntos
diff --git a/app/src/main/res/values-pt-rBR/strings-preferences.xml b/app/src/main/res/values-pt-rBR/strings-preferences.xml
index 59c2a8999..ea7584e6c 100644
--- a/app/src/main/res/values-pt-rBR/strings-preferences.xml
+++ b/app/src/main/res/values-pt-rBR/strings-preferences.xml
@@ -11,12 +11,21 @@
Ignorar relógio do GPS e usar relógio do Android para os registros de tempoLog da pressão barométrica [hPa]Alternar requer o reinício da trilha
+ Notas de texto
+ Escolha como os valores do botão Texto da Nota nos layouts serão salvos.
+
+ Waypoint
+ Nota do OSM
+ Ambas
+ Intervalo de registros do GPSUse 0 para o menor possível (afeta a duração da bateria)segundos
+ O intervalo de registro de GPS não pode estar vazio.Distância de registro do GPSDistância mínima entre os pontos da trilha em metros, use 0 para o mais curto possívelmetros
+ A distância mínima entre os pontos de rastreamento não pode ser vazia.Interface do usuárioFonte padrão das imagensTirar foto com a câmera ou escolher da galeria?
@@ -58,6 +67,7 @@
Configurações GPXPasta do armazenamento nos documentosTerá efeito na próxima trilha (não na atual)
+ O valor da pasta de armazenamento não pode estar vazio.Uma pasta por trilhaSalvar cada trilha e arquivos associados em uma pasta própriaNome do arquivo para trilhas nomeadas
@@ -76,6 +86,14 @@
Preencha HDOP no GPX com um valor de aproximação de precisãoAtivar somTocar som quando gravação de voz começa e termina
+ Visibilidade da trilha
+ Visibilidade preferencial das trilhas enviadas para openstreetmap.org
+
+ Privado
+ Rastreável
+ Público
+ Identificável
+ Redefinir autenticação OSMEsquecer as credenciais e permissões OSM e forçar o OSMTracker a perguntar isso novamenteVocê vai ter que autorizar OSMTracker para carregar trilhas novamente. Tem certeza?
@@ -88,4 +106,5 @@
Exportar cabeçalho da bússolaDefine se e como os dados da bússola devem ser exportados para o arquivo GPX
+ Redefinir valor padrão
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 53ff769bb..5d6c8aae1 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -30,6 +30,7 @@
Lista de trilhas:Pontos de referência:Trajeto:
+ Notas:Você não tem nenhuma trilha.Pressione para gravar uma nova trilha.Impossível criar uma nova trilha: {0}
@@ -67,6 +68,7 @@
DescriçãoTags (separado com vírgula)Você precisa digitar uma descrição
+ VisibilidadePrivadoPúblicoRastreável
@@ -83,7 +85,13 @@
Erro de autorização. Gostaria de limpar as credencias do OpenStreetMaps salvas?Enviado com sucesso para o OpenStreetMap
+ Carregamento de notas do OpenStreetMap
+ Erro ao carregar a nota
+ Nota de texto
+ EnviarCancelar
+ via %1$s %2$s
+ Erro de autorização. Se você já concedeu permissão ao aplicativo para enviar trilhas, é necessário limpar suas credenciais salvas para autorizar o aplicativo a enviar trilhas e notas. Deseja limpar suas credenciais salvas do OpenStreetMap?Gravar vozTirar foto
@@ -100,11 +108,16 @@
ApagarCancelar
+ Nome/texto da notaSalvarApagarCancelar
+ Excluir nota
+ Apagar esta nota?ApagarCancelar
+ Lista de notas
+ Enviar como nota OSMConfiguraçõesPontos de referência
diff --git a/app/src/main/res/values/strings-preferences.xml b/app/src/main/res/values/strings-preferences.xml
index 41414b313..5b11dda53 100644
--- a/app/src/main/res/values/strings-preferences.xml
+++ b/app/src/main/res/values/strings-preferences.xml
@@ -1,6 +1,5 @@
-
SettingsGPS
From 9e873de3ed89e55c421669d81e32755bd1f2721e Mon Sep 17 00:00:00 2001
From: "transifex-integration[bot]"
<43880903+transifex-integration[bot]@users.noreply.github.com>
Date: Thu, 5 Feb 2026 19:22:38 -0600
Subject: [PATCH 55/60] =?UTF-8?q?[Transifex]=20Updates=20for=20project=20O?=
=?UTF-8?q?SMTracker=20for=20Android=E2=84=A2=20(#679)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Translate strings.xml in el
82% of minimum 80% translated source file: 'strings.xml'
on 'el'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate strings.xml in sk
82% of minimum 80% translated source file: 'strings.xml'
on 'sk'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate strings.xml in pl
91% of minimum 80% translated source file: 'strings.xml'
on 'pl'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate strings.xml in es
99% of minimum 80% translated source file: 'strings.xml'
on 'es'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate strings.xml in zh_TW
99% of minimum 80% translated source file: 'strings.xml'
on 'zh_TW'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate strings.xml in pt_BR
99% of minimum 80% translated source file: 'strings.xml'
on 'pt_BR'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate strings.xml in sv
93% of minimum 80% translated source file: 'strings.xml'
on 'sv'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate strings.xml in nl
93% of minimum 80% translated source file: 'strings.xml'
on 'nl'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate strings.xml in ru
93% of minimum 80% translated source file: 'strings.xml'
on 'ru'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate strings.xml in es
100% translated source file: 'strings.xml'
on 'es'.
* Translate strings.xml in pt_BR
100% translated source file: 'strings.xml'
on 'pt_BR'.
* Translate strings.xml in zh_TW
100% translated source file: 'strings.xml'
on 'zh_TW'.
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
---
app/src/main/res/values-el/strings.xml | 1 +
app/src/main/res/values-es/strings.xml | 3 +++
app/src/main/res/values-nl/strings.xml | 1 +
app/src/main/res/values-pl/strings.xml | 1 +
app/src/main/res/values-pt-rBR/strings.xml | 3 +++
app/src/main/res/values-ru/strings.xml | 1 +
app/src/main/res/values-sk/strings.xml | 1 +
app/src/main/res/values-sv/strings.xml | 1 +
app/src/main/res/values-zh-rTW/strings.xml | 3 +++
9 files changed, 15 insertions(+)
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 4dc290f30..a24bb55a4 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -233,6 +233,7 @@
Καλωσήρθατε στο OSMTracker για Android™ 👋Αυτή η εφαρμοφή είναι δωρεάν λογισμικό το οποίο σέβεται της ελευθερία σας!
+
Καλή ιχνηλάτηση 🗺 😎Το OSMTracker για Android θα χρησιμοποιήσει την τοποθεσία σας GPS για να καταγράψει σημεία ίχνους και σημεία διαδρομής, ακόμη κι αν εφαρμογή τρέχει στο παρασκήνιο.\nΤα δεδομένα σας δεν χρησιμοποιούνται για την υποστήριξη διαφημίσεων.
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 43c47dedb..850fcda16 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -274,6 +274,9 @@
Bienvenido a OSMTracker para Android ™ 👋¡Esta aplicación es un software libre que respeta tu libertad!
+
+ ¿Qué novedades hay en esta versión?
+ Ya podemos subir notas a OSM, y los mosaicos de mapas de CyclOSM y OpenTopo vuelven a estar disponibles. ¡Disfruten!Feliz trazado 🗺 😎OSMTracker para Android usará su ubicación GPS para registrar puntos de traza y puntos, incluso cuando la aplicación se está ejecutando en segundo plano.
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index b70ae59ef..81b82b032 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -262,6 +262,7 @@ Selecteer het in de lijst om door te gaan.Welkom bij OSMTracker voor Android™ 👋Deze app is gratis software die uw vrijheid respecteert!
+
Veel plezier met tracken! 🗺 😎OSMTracker voor Android zal uw gps-locatie gebruiken om trajectpunten en referentiepunten op te nemen, zelfs wanneer de app op de achtergrond actief is.
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 6d69f1b56..455903f58 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -257,6 +257,7 @@ głosowąWitaj w OSMTracker Android™ 👋Ta aplikacja jest wolnym oprogramowaniem i szanuje Twoją wolność!
+
Miłego zbierania śladów 🗺 😎OSMTracker Android będzie używał Twojej pozycji GPS, aby zarejestrować punkty trasy i punkty orientacyjne, nawet jeśli aplikacja działa w tle.
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 5d6c8aae1..10a00ceed 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -274,6 +274,9 @@
Bem-vindo ao OSMTracker para Android ™ 👋Este App é um software gratuito que respeita a sua liberdade!
+
+ O que há de novo nesta versão?
+ Agora podemos enviar notas para o OSM, e os mapas CyclOSM e OpenTopo estão disponíveis novamente. Aproveitem!Boas trilhas 🗺 😎OSMTracker para Android usará sua localização GPS para registrar trackpoints e waypoints, mesmo quando o aplicativo está sendo executado em segundo plano.
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index b653ab5ff..18c8cc061 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -261,6 +261,7 @@
Добро пожаловать в OSMTracker для Android ™ 👋Это свободное приложение, уважающее вашу свободу!
+
Удачного трекинга 🗺 😎OSMTracker для Android будет использовать ваше местоположение по GPS для записи точек трека и путевых точек, даже если приложение работает в фоновом режиме.
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index aaeda263e..e7d3773a7 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -236,5 +236,6 @@
+
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 9762f4e78..342f67df9 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -261,6 +261,7 @@
Välkommen till OSMTracker for Android™ 👋Den här appen är fri programvara som respekterar din frihet!
+
Lycka till med spårningen 🗺😎OSMTracker för Android kommer att använda din GPS-position för att spela in spårpunkter och vägpunkter, även när appen körs i bakgrunden.
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 3a9a6ab73..6a4f503a7 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -274,6 +274,9 @@
歡迎使用 OSMTracker for Android™👋這款 App 是尊重你自由的自由軟體
+
+ 新版本推出什麼新功能呢?
+ 如今我們能夠上傳註解到 OSM,恢復介接 CyclOSM 以及 OpenTopo 圖磚。請享用!記錄快樂🗺 😎OSMTracker for Android 會使用你的 GPS 位置來記錄航點和路徑,即便 App 在背景執行。
From b18f3a84c82e7ae291b2dd2c40e1aa4844cd8e06 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Sat, 7 Feb 2026 09:06:31 -0600
Subject: [PATCH 56/60] Increases version number to 72
---
app/build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/build.gradle b/app/build.gradle
index 60b4726af..48fa3eaaa 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,7 +15,7 @@ android {
multiDexEnabled true
// Version code should be increased after each release
- versionCode 71
+ versionCode 72
versionName new Date().format('yyyy.MM.dd')
testApplicationId "net.osmtracker.test"
From 5e51f1c35ca65a1dfbfb0c8c0e6e11b9bb5b38c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Guti=C3=A9rrez=20Alfaro?=
Date: Wed, 11 Feb 2026 10:06:02 -0600
Subject: [PATCH 57/60] Fix: Add fallback when pref value is not set
---
.../java/net/osmtracker/activity/Preferences.java | 11 +++++++++--
app/src/main/res/values/strings-preferences.xml | 2 ++
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/app/src/main/java/net/osmtracker/activity/Preferences.java b/app/src/main/java/net/osmtracker/activity/Preferences.java
index 9e637d2ab..d1b2cd54d 100644
--- a/app/src/main/java/net/osmtracker/activity/Preferences.java
+++ b/app/src/main/java/net/osmtracker/activity/Preferences.java
@@ -349,8 +349,15 @@ private void setupListPreference(String preferenceKey, String staticSummary) {
listPref.setSummaryProvider(preference -> {
ListPreference lp = (ListPreference) preference;
CharSequence entry = lp.getEntry();
- // Null check: entry might be null if no value is selected
- String displayValue = Objects.requireNonNull(entry).toString();
+
+ // Handle cases where no value has been selected yet. (backwards compatibility)
+ String displayValue;
+ if (entry == null || TextUtils.isEmpty(entry)) {
+ // Fallback text if no value is set.
+ displayValue = getString(R.string.prefs_not_set);
+ } else {
+ displayValue = entry.toString();
+ }
return displayValue + ".\n" + staticSummary;
});
}
diff --git a/app/src/main/res/values/strings-preferences.xml b/app/src/main/res/values/strings-preferences.xml
index 5b11dda53..88861e3cd 100644
--- a/app/src/main/res/values/strings-preferences.xml
+++ b/app/src/main/res/values/strings-preferences.xml
@@ -117,4 +117,6 @@
Export compass headingDefines if and how the compass data should be exported to the GPX fileReset default value
+ Not set
+
From 6563525cad7342cff3c410443246e4f12db59b08 Mon Sep 17 00:00:00 2001
From: "transifex-integration[bot]"
<43880903+transifex-integration[bot]@users.noreply.github.com>
Date: Wed, 11 Feb 2026 18:59:17 +0000
Subject: [PATCH 58/60] Translate strings-preferences.xml in es
100% translated source file: 'strings-preferences.xml'
on 'es'.
---
app/src/main/res/values-es/strings-preferences.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/src/main/res/values-es/strings-preferences.xml b/app/src/main/res/values-es/strings-preferences.xml
index e3d20693b..78be706ce 100644
--- a/app/src/main/res/values-es/strings-preferences.xml
+++ b/app/src/main/res/values-es/strings-preferences.xml
@@ -112,4 +112,5 @@
Exportar rumbo de la brújulaDefine el si y cómo los datos de brújula deben ser exportados al archivo GPXReestablecer valor predeterminado
+ No establecido
From 26c62a84b2b7729fc80b3b77ce4d431f7b38b41f Mon Sep 17 00:00:00 2001
From: "transifex-integration[bot]"
<43880903+transifex-integration[bot]@users.noreply.github.com>
Date: Wed, 11 Feb 2026 18:59:56 +0000
Subject: [PATCH 59/60] Translate strings-preferences.xml in pt_BR
100% translated source file: 'strings-preferences.xml'
on 'pt_BR'.
---
app/src/main/res/values-pt-rBR/strings-preferences.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/src/main/res/values-pt-rBR/strings-preferences.xml b/app/src/main/res/values-pt-rBR/strings-preferences.xml
index ea7584e6c..cf1b49d03 100644
--- a/app/src/main/res/values-pt-rBR/strings-preferences.xml
+++ b/app/src/main/res/values-pt-rBR/strings-preferences.xml
@@ -107,4 +107,5 @@
Exportar cabeçalho da bússolaDefine se e como os dados da bússola devem ser exportados para o arquivo GPXRedefinir valor padrão
+ Não definido
From 7285732ab81fa74a5428c17f7a224fb3882b785f Mon Sep 17 00:00:00 2001
From: "transifex-integration[bot]"
<43880903+transifex-integration[bot]@users.noreply.github.com>
Date: Wed, 11 Feb 2026 19:00:46 +0000
Subject: [PATCH 60/60] Translate strings-preferences.xml in zh_TW
98% of minimum 80% translated source file: 'strings-preferences.xml'
on 'zh_TW'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---
app/src/main/res/values-zh-rTW/strings-preferences.xml | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/src/main/res/values-zh-rTW/strings-preferences.xml b/app/src/main/res/values-zh-rTW/strings-preferences.xml
index c6f6f6ee5..f185f4c3b 100644
--- a/app/src/main/res/values-zh-rTW/strings-preferences.xml
+++ b/app/src/main/res/values-zh-rTW/strings-preferences.xml
@@ -1,6 +1,5 @@
-
設定GPS