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 @@ + + + + + + + + + + + + + + +