From 4018423dca80f223005868b5c7cc59d01dbf06ee Mon Sep 17 00:00:00 2001
From: JoseAndresVargas
Date: Fri, 21 Nov 2025 15:09:41 -0600
Subject: [PATCH 1/4] scope and client_id change for notes auth
---
.../main/java/net/osmtracker/osm/OpenStreetMapConstants.java | 5 +++--
1 file changed, 3 insertions(+), 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 f51469320..2db037506 100644
--- a/app/src/main/java/net/osmtracker/osm/OpenStreetMapConstants.java
+++ b/app/src/main/java/net/osmtracker/osm/OpenStreetMapConstants.java
@@ -14,12 +14,13 @@ public static class Api {
}
public static class OAuth2 {
- public static final String CLIENT_ID_PROD = "6s8TuIQoPeq89ZWUFOXU7EZ-ZaCUVtUoNZFIKCMdU-E";
+ // 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_DEV = "94Ht-oVBJ2spydzfk18s1RV2z7NS98SBwMfzSCqLQLE"; // DEV
public static final String CLIENT_ID = (DEV_MODE) ? CLIENT_ID_DEV : CLIENT_ID_PROD;
- public static final String SCOPE = "write_gpx";
+ public static final String SCOPE = "write_gpx write_notes";
public static final String USER_AGENT = "OSMTracker for Android™";
public static class Urls {
From e4879db7b38abc6b2ac76305ca5b1e690a39ab98 Mon Sep 17 00:00:00 2001
From: JoseAndresVargas
Date: Fri, 21 Nov 2025 15:11:15 -0600
Subject: [PATCH 2/4] Implemented notes upload using osmapi
---
app/build.gradle | 6 +
app/src/main/AndroidManifest.xml | 1 +
.../activity/OpenStreetMapNotesUpload.java | 204 ++++++++++++++++++
.../osm/UploadToOpenStreetMapNotesTask.java | 178 +++++++++++++++
4 files changed, 389 insertions(+)
create mode 100644 app/src/main/java/net/osmtracker/activity/OpenStreetMapNotesUpload.java
create mode 100644 app/src/main/java/net/osmtracker/osm/UploadToOpenStreetMapNotesTask.java
diff --git a/app/build.gradle b/app/build.gradle
index 3caf91def..ac854fe64 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -85,6 +85,12 @@ dependencies {
exclude group: 'net.sf.kxml', module: 'kxml2'
exclude group: 'xmlpull', module: 'xmlpull'
}
+ // For upload notes to osm server
+ implementation ('de.westnordost:osmapi-notes:3.1'){
+ // Already included in Android
+ exclude group: 'net.sf.kxml', module: 'kxml2'
+ exclude group: 'xmlpull', module: 'xmlpull'
+ }
// App intro
implementation 'com.github.AppIntro:AppIntro:6.3.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4dc8627d4..451be00f4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -108,6 +108,7 @@
+ 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
+ * 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 5616dba04518e5054d96a275674dae85f0efe65d Mon Sep 17 00:00:00 2001
From: JoseAndresVargas
Date: Fri, 21 Nov 2025 15:12:10 -0600
Subject: [PATCH 3/4] Notes upload UI text added
---
app/src/main/res/layout/osm_note_upload.xml | 65 +++++++++++++++++++++
app/src/main/res/values/strings.xml | 10 ++++
2 files changed, 75 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 4b12833ab..6dda290f5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -27,6 +27,7 @@
Accur: Comp. head.:Comp. accur.:
+ Upload as OSM noteTrack manager
@@ -89,6 +90,15 @@
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 644131507a552f501458d17e1c29024ba775e3d4 Mon Sep 17 00:00:00 2001
From: JoseAndresVargas
Date: Fri, 21 Nov 2025 15:13:01 -0600
Subject: [PATCH 4/4] 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