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 @@
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/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/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 { 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