From 7021575af7578fc54c46c61e13835941183b652f Mon Sep 17 00:00:00 2001 From: lains Date: Wed, 23 Oct 2019 21:38:52 +0200 Subject: Add/Android Supporting sending & receiving geo: intents (#812) --- navit/android.c | 27 +-- navit/android/AndroidManifest.xml | 7 + navit/android/res/values/strings.xml | 1 + .../android/src/org/navitproject/navit/Navit.java | 190 ++++++++++++++++----- .../src/org/navitproject/navit/NavitGraphics.java | 96 ++++++++--- navit/coord.h | 8 +- 6 files changed, 248 insertions(+), 81 deletions(-) diff --git a/navit/android.c b/navit/android.c index f35cb274a..def3a6f22 100644 --- a/navit/android.c +++ b/navit/android.c @@ -11,6 +11,7 @@ #include "callback.h" #include "country.h" #include "projection.h" +#include "coord.h" #include "map.h" #include "mapset.h" #include "navit_nls.h" @@ -321,7 +322,7 @@ JNIEXPORT jobjectArray JNICALL Java_org_navitproject_navit_NavitGraphics_getAllC JNIEXPORT jstring JNICALL Java_org_navitproject_navit_NavitGraphics_getCoordForPoint( JNIEnv* env, - jobject thiz, jint x, jint y, jboolean absolute_coord) { + jobject thiz, jint x, jint y, jboolean absoluteCoord) { jstring return_string = NULL; @@ -343,13 +344,13 @@ JNIEXPORT jstring JNICALL Java_org_navitproject_navit_NavitGraphics_getCoordForP pc.pro = transform_get_projection(transform); char coord_str[32]; - if (absolute_coord) { + if (absoluteCoord) { pcoord_format_absolute(&pc, coord_str, sizeof(coord_str), ","); } else { pcoord_format_degree_short(&pc, coord_str, sizeof(coord_str), " "); } - dbg(lvl_error,"Display point x=%d y=%d is \"%s\"",x,y,coord_str); + dbg(lvl_debug,"Display point x=%d y=%d is \"%s\"",x,y,coord_str); return_string = (*env)->NewStringUTF(env,coord_str); return return_string; @@ -458,12 +459,13 @@ JNIEXPORT jint JNICALL Java_org_navitproject_navit_NavitGraphics_callbackMessage pc.pro = transform_get_projection(transform); char coord_str[32]; - //pcoord_format_short(&pc, coord_str, sizeof(coord_str), " "); pcoord_format_degree_short(&pc, coord_str, sizeof(coord_str), " "); + dbg(lvl_debug,"Setting destination to %s",coord_str); // start navigation asynchronous navit_set_destination(attr.u.navit, &pc, coord_str, 1); } + break; case 3: { // navigate to geo position char *name; @@ -478,14 +480,14 @@ JNIEXPORT jint JNICALL Java_org_navitproject_navit_NavitGraphics_callbackMessage char *p; char *stopstring; - // lat - p = strtok(parse_str, "#"); + // latitude + p = strtok (parse_str,"#"); g.lat = strtof(p, &stopstring); - // lon - p = strtok(NULL, "#"); + // longitude + p = strtok (NULL, "#"); g.lng = strtof(p, &stopstring); - // description - name = strtok(NULL, "#"); + // description/name of the place identified by lat and long + name = strtok (NULL, "#"); dbg(lvl_debug, "lat=%f", g.lat); dbg(lvl_debug, "lng=%f", g.lng); @@ -499,14 +501,14 @@ JNIEXPORT jint JNICALL Java_org_navitproject_navit_NavitGraphics_callbackMessage pc.y = c.y; pc.pro = projection_mg; char coord_str[32]; - if (!name || *name == '\0') { + if (!name || *name == '\0') { /* When name is an empty string, use the geo coord instead */ pcoord_format_degree_short(&pc, coord_str, sizeof(coord_str), " "); name = coord_str; } // start navigation asynchronous navit_set_destination(attr.u.navit, &pc, name, 1); - break; } + break; default: dbg(lvl_error, "Unknown command: %d", channel); } @@ -514,7 +516,6 @@ JNIEXPORT jint JNICALL Java_org_navitproject_navit_NavitGraphics_callbackMessage return ret; } - static char *postal_str(struct search_list_result *res, int level) { char *ret=NULL; if (res->town->common.postal) diff --git a/navit/android/AndroidManifest.xml b/navit/android/AndroidManifest.xml index e8f153fd4..2bcdb4e19 100644 --- a/navit/android/AndroidManifest.xml +++ b/navit/android/AndroidManifest.xml @@ -16,12 +16,19 @@ android:theme="@style/NavitTheme"> + + + + + diff --git a/navit/android/res/values/strings.xml b/navit/android/res/values/strings.xml index 44ed1483b..5ce99e52a 100644 --- a/navit/android/res/values/strings.xml +++ b/navit/android/res/values/strings.xml @@ -31,6 +31,7 @@ Position Route to here View + Use position with Delete this map? diff --git a/navit/android/src/org/navitproject/navit/Navit.java b/navit/android/src/org/navitproject/navit/Navit.java index 87ad5d9a8..2fa2f312d 100644 --- a/navit/android/src/org/navitproject/navit/Navit.java +++ b/navit/android/src/org/navitproject/navit/Navit.java @@ -71,14 +71,86 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; - public class Navit extends Activity { + /** + * Nested class storing the intent that was sent to the main navit activity at startup. + **/ + private class StartupIntent { + /** + * Constructor. + * + * @param intent The intent to store in this object + **/ + public StartupIntent(Intent intent) { + mStartupIntent = intent; + mStartupIntentTimestamp = System.currentTimeMillis(); + } + + /** + * Check if the encapsulated intent still valid or too old. + * + * @return true if the encapsulated intent is recent enough + **/ + public boolean isRecentEnough() { + if (mStartupIntent == null) { + return false; + } + /* We consider the intent is valid for 4s */ + return (System.currentTimeMillis() <= getExpirationTimeMillis()); + } + + /** + * Compute the system time when the stored intent will become invalid. + * + * @return The system time for invalidation (in ms) + **/ + private long getExpirationTimeMillis() { + if (mStartupIntent == null) { + return 0; + } + /* We give 4s to navit to process the intent */ + return mStartupIntentTimestamp + 4000L; + } + + /** + * Getter for the encapsulated intent. + * + * @return The encapsulated intent + **/ + public Intent getIntent() { + return mStartupIntent; + } + + /** + * Represent this object as a string. + * + * @return A string containing the summary of the data we store here + **/ + public String toString() { + if (mStartupIntent == null) { + return "{null}"; + } else { + String validForStr; + long remainingValidity = getExpirationTimeMillis() - System.currentTimeMillis(); + if (remainingValidity < 0) { + validForStr = "(expired since " + -remainingValidity + "ms)"; + } else { + validForStr = "(valid for " + remainingValidity + "ms)"; + } + return "{ act=" + mStartupIntent.getAction() + " data=" + mStartupIntent.getDataString() + + " " + validForStr + " }"; + } + } + + private Intent mStartupIntent; /*!< The intent we store */ + private long mStartupIntentTimestamp; /*!< A timestamp (in ms) for when mStartupIntent was recorded */ + } + public static DisplayMetrics sMetrics; public static boolean sShowSoftKeyboardShowing; - private static Intent sStartupIntent; - private static long sStartupIntentTimestamp; + private static StartupIntent sStartupIntent; private static final int MY_PERMISSIONS_REQ_FINE_LOC = 103; private static final int NavitDownloaderSelectMap_id = 967; private static final int NavitAddressSearch_id = 70; @@ -214,6 +286,9 @@ public class Navit extends Activity { return true; } + /** + * Show the first start infoxbox (presentation of navit and link website for more info). + **/ private void showInfos() { SharedPreferences settings = getSharedPreferences(NavitAppConfig.NAVIT_PREFS, MODE_PRIVATE); boolean firstStart = settings.getBoolean("firstStart", true); @@ -260,22 +335,21 @@ public class Navit extends Activity { @Override public void onCreate(Bundle savedInstanceState) { + Log.d(TAG, "OnCreate"); super.onCreate(savedInstanceState); windowSetup(); mDialogs = new NavitDialogs(this); - // only take arguments here, onResume gets called all the time (e.g. when screenblanks, etc.) - Navit.sStartupIntent = this.getIntent(); - // hack! Remember time stamps, and only allow 4 secs. later in onResume to set target! - Navit.sStartupIntentTimestamp = System.currentTimeMillis(); - Log.d(TAG, "**1**A " + sStartupIntent.getAction()); - Log.d(TAG, "**1**D " + sStartupIntent.getDataString()); + /* Only store the startup intent, onResume() gets called all the time (e.g. when screenblanks, etc.) and + will process this intent later on if needed */ + sStartupIntent = new StartupIntent(this.getIntent()); + Log.d(TAG, "Recording intent " + sStartupIntent.toString()); createNotificationChannel(); buildNotification(); verifyPermissions(); - // get the local language ------------- + // get the local language Locale locale = Locale.getDefault(); String lang = locale.getLanguage(); String langc = lang; @@ -425,6 +499,13 @@ public class Navit extends Activity { Log.d(TAG, "OnStart"); } + @Override + public void onNewIntent(Intent intent) { + Log.d(TAG, "OnNewIntent"); + sStartupIntent = new StartupIntent(intent); + Log.d(TAG, "Recording intent " + sStartupIntent.toString()); + } + @Override public void onResume() { super.onResume(); @@ -435,24 +516,33 @@ public class Navit extends Activity { // intent_data = "google.navigation:q=48.25676,16.643"; // intent_data = "google.navigation:ll=48.25676,16.643&q=blabla-strasse"; // intent_data = "google.navigation:ll=48.25676,16.643"; + // intent_data = "geo:48.25676,16.643"; if (sStartupIntent != null) { - if (System.currentTimeMillis() <= Navit.sStartupIntentTimestamp + 4000L) { - Log.d(TAG, "**2**A " + sStartupIntent.getAction()); - Log.d(TAG, "**2**D " + sStartupIntent.getDataString()); - String naviScheme = sStartupIntent.getScheme(); - if (naviScheme != null && naviScheme.equals("google.navigation")) { - parseNavigationURI(sStartupIntent.getData().getSchemeSpecificPart()); + Log.d(TAG, "Using stored startup intent " + sStartupIntent.toString()); + if (sStartupIntent.isRecentEnough()) { + Intent startupIntent = sStartupIntent.getIntent(); + String naviScheme = startupIntent.getScheme(); + if (naviScheme != null) { + if (naviScheme.equals("google.navigation")) { + parseNavigationURI(startupIntent.getData().getSchemeSpecificPart()); + } else if (naviScheme.equals("geo") + && startupIntent.getAction().equals("android.intent.action.VIEW")) { + invokeCallbackOnGeo(startupIntent.getData().getSchemeSpecificPart(), + NavitGraphics.MsgType.CLB_SET_DESTINATION, + ""); + } } } else { - Log.e(TAG, "timestamp for navigate_to expired! not using data"); + Log.e(TAG, "timestamp for startup intent expired! not using data"); } + sStartupIntent = null; } } @Override public void onPause() { super.onPause(); - Log.d(TAG, "onPause"); + Log.d(TAG, "OnPause"); } @Override @@ -476,6 +566,42 @@ public class Navit extends Activity { } } + /** + * Invoke NavitGraphics.sCallbackHandler on a geographical position + * + * @param geoString A string containing the target geographical position with a format like "48.25676,16.643" + * @param msgType The type of message to send to the callback (see NavitGraphics.MsgType for possible values) + * @param name The name/label to associate to the geographical position + **/ + private void invokeCallbackOnGeo(String geoString, NavitGraphics.MsgType msgType, String name) { + String[] geo = geoString.split(","); + if (geo.length == 2) { + try { + Bundle b = new Bundle(); + Float lat = Float.valueOf(geo[0]); + Float lon = Float.valueOf(geo[1]); + b.putFloat("lat", lat); + b.putFloat("lon", lon); + b.putString("q", name); + Message msg = Message.obtain(NavitGraphics.sCallbackHandler, + msgType.ordinal()); + + msg.setData(b); + msg.sendToTarget(); + Log.d(TAG, "target found (b): " + geoString); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } else { + Log.w(TAG, "Ignoring invalid geo string: " + geoString); + } + } + + /** + * Parse google navigation URIs (usually starting with "google.navigation:") and take the appropriate actions + * + * @param schemeSpecificPart A string containing the URI scheme, for example "ll=48.25676,16.643&q=blabla-strasse" + **/ private void parseNavigationURI(String schemeSpecificPart) { String[] naviData = schemeSpecificPart.split("&"); Pattern p = Pattern.compile("(.*)=(.*)"); @@ -492,39 +618,17 @@ public class Navit extends Activity { // c: google.navigation:ll=48.25676,16.643 // b: google.navigation:q=48.25676,16.643 - float lat; - float lon; - Bundle b = new Bundle(); - String geoString = params.get("ll"); + String address = null; if (geoString != null) { - String address = params.get("q"); - if (address != null) { - b.putString("q", address); - } + address = params.get("q"); } else { geoString = params.get("q"); } if (geoString != null) { if (geoString.matches("^[+-]{0,1}\\d+(|\\.\\d*),[+-]{0,1}\\d+(|\\.\\d*)$")) { - String[] geo = geoString.split(","); - if (geo.length == 2) { - try { - lat = Float.valueOf(geo[0]); - lon = Float.valueOf(geo[1]); - b.putFloat("lat", lat); - b.putFloat("lon", lon); - Message msg = Message.obtain(NavitGraphics.sCallbackHandler, - NavitGraphics.MsgType.CLB_SET_DESTINATION.ordinal()); - - msg.setData(b); - msg.sendToTarget(); - Log.i(TAG, "target found (b): " + geoString); - } catch (NumberFormatException e) { - e.printStackTrace(); - } - } + invokeCallbackOnGeo(geoString, NavitGraphics.MsgType.CLB_SET_DESTINATION, address); } else { start_targetsearch_from_intent(geoString); } diff --git a/navit/android/src/org/navitproject/navit/NavitGraphics.java b/navit/android/src/org/navitproject/navit/NavitGraphics.java index 6aee53cf5..cd6ce6944 100644 --- a/navit/android/src/org/navitproject/navit/NavitGraphics.java +++ b/navit/android/src/org/navitproject/navit/NavitGraphics.java @@ -41,6 +41,7 @@ import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Message; +import android.os.Parcelable; import android.support.annotation.RequiresApi; import android.support.v4.view.ViewConfigurationCompat; import android.util.Log; @@ -151,6 +152,9 @@ class NavitGraphics { static final int ZOOM = 2; static final int PRESSED = 3; PointF mPressedPosition = null; + // mContextMenuMapViewIntent is the ACTION_VIEW intent for a geo coordinates. + // it is used when clicking on view in the map contextual menu + Intent mContextMenuMapViewIntent = null; NavitView(Context context) { super(context); @@ -197,6 +201,56 @@ class NavitGraphics { return insets; } + /** + * Create an intent for a view action of a point provided by its x and y position on the display. + * + * @param x The x coordinates of the point on the display + * @param y The y coordinates of the point on the display + * + * @return An intent to start to view the specified point on a third-party app on Android (can be null if a + * view action is not possible) + **/ + protected Intent getViewIntentForDisplayPoint(int x, int y) { + Intent result = null; + + /* Check if there is at least one application that can process a geo intent... */ + String selectedPointCoord = getCoordForPoint(x, y, true); + Uri intentUri = Uri.parse("geo:" + selectedPointCoord); + Intent defaultShareIntent = new Intent(Intent.ACTION_VIEW, intentUri); + + List customShareIntentList = new ArrayList(); + List intentTargetAppList; + intentTargetAppList = this.getContext().getPackageManager().queryIntentActivities(defaultShareIntent, 0); + + String selfPackageName = this.getContext().getPackageName(); /* aka: "org.navitproject.navit" */ + + if (!intentTargetAppList.isEmpty()) { + for (ResolveInfo resolveInfo : intentTargetAppList) { + String packageName = resolveInfo.activityInfo.packageName; + Intent copiedIntent = new Intent(Intent.ACTION_VIEW, intentUri); + if (!packageName.equals(selfPackageName)) { + Log.d(TAG, "Adding package \"" + packageName + "\" to app chooser"); + copiedIntent.setPackage(packageName); + copiedIntent.setClassName( + resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name); + customShareIntentList.add(copiedIntent); + } else { + Log.d(TAG, "Excluding ourselves (package " + packageName + ") from intent targets"); + } + } + if (customShareIntentList.size() > 0) { + result = Intent.createChooser(customShareIntentList.remove(customShareIntentList.size() - 1), + NavitAppConfig.getTstring(R.string.use_position_with)); + result.putExtra(Intent.EXTRA_INITIAL_INTENTS, + customShareIntentList.toArray(new Parcelable[customShareIntentList.size()])); + Log.d(TAG, "Preparing action intent (" + customShareIntentList.size() + 1 + + " candidate apps) to view selected coord: " + selectedPointCoord); + } + } + return result; + } + private static final int MENU_DRIVE_HERE = 1; private static final int MENU_VIEW = 2; private static final int MENU_CANCEL = 3; @@ -208,39 +262,39 @@ class NavitGraphics { menu.setHeaderTitle(NavitAppConfig.getTstring(R.string.position_popup_title) + " " + clickCoord); menu.add(1, MENU_DRIVE_HERE, NONE, NavitAppConfig.getTstring(R.string.position_popup_drive_here)) .setOnMenuItemClickListener(this); - Uri intentUri = Uri.parse("geo:" + getCoordForPoint((int)mPressedPosition.x, - (int)mPressedPosition.y, true)); - Intent mContextMenuMapViewIntent = new Intent(Intent.ACTION_VIEW, intentUri); - - PackageManager packageManager = this.getContext().getPackageManager(); - List activities = packageManager.queryIntentActivities(mContextMenuMapViewIntent, - PackageManager.MATCH_DEFAULT_ONLY); - boolean isIntentSafe = (activities.size() > 0); // at least one candidate receiver - if (isIntentSafe) { // add view with external app option - menu.add(1, MENU_VIEW, NONE, NavitAppConfig.getTstring(R.string.position_popup_view)) - .setOnMenuItemClickListener(this); + mContextMenuMapViewIntent = getViewIntentForDisplayPoint((int)mPressedPosition.x, (int)mPressedPosition.y); + if (mContextMenuMapViewIntent != null) { + menu.add(1, MENU_VIEW, NONE, + NavitAppConfig.getTstring(R.string.position_popup_view)).setOnMenuItemClickListener(this); } else { - Log.w(TAG, "No application available to handle ACTION_VIEW intent, option not displayed"); + Log.w(TAG, "No application available to handle ACTION_VIEW intent, view option not displayed"); } - menu.add(1, MENU_CANCEL, NONE, getTstring(R.string.cancel)).setOnMenuItemClickListener(this); + menu.add(1, MENU_CANCEL, NONE, + NavitAppConfig.getTstring(R.string.cancel)).setOnMenuItemClickListener(this); } @Override public boolean onMenuItemClick(MenuItem item) { int itemId = item.getItemId(); + if (itemId != MENU_VIEW) { + /* Destroy any previous map view intent if the user didn't select the MENU_VIEW action */ + mContextMenuMapViewIntent = null; + } if (itemId == MENU_DRIVE_HERE) { Message msg = Message.obtain(sCallbackHandler, MsgType.CLB_SET_DISPLAY_DESTINATION.ordinal(), (int) mPressedPosition.x, (int) mPressedPosition.y); msg.sendToTarget(); } else if (itemId == MENU_VIEW) { - Uri intentUri = Uri.parse("geo:" + getCoordForPoint((int) mPressedPosition.x, - (int) mPressedPosition.y, true)); - Intent mContextMenuMapViewIntent = new Intent(Intent.ACTION_VIEW, intentUri); - mContextMenuMapViewIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - if (mContextMenuMapViewIntent.resolveActivity(this.getContext().getPackageManager()) != null) { - this.getContext().startActivity(mContextMenuMapViewIntent); + if (mContextMenuMapViewIntent != null) { + mContextMenuMapViewIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + if (mContextMenuMapViewIntent.resolveActivity(this.getContext().getPackageManager()) != null) { + this.getContext().startActivity(mContextMenuMapViewIntent); + } else { + Log.w(TAG, "View menu selected but intent is not handled by any application. Ignoring..."); + } + mContextMenuMapViewIntent = null; /* Destoy the intent once it has been used */ } else { - Log.w(TAG, "ACTION_VIEW intent is not handled by any application, discarding..."); + Log.e(TAG, "User clicked on view on menu but intent was null. Discarding..."); } } return true; @@ -669,7 +723,7 @@ class NavitGraphics { private native void motionCallback(long id, int x, int y); - private native String getCoordForPoint(int x, int y, boolean absolutCoord); + private native String getCoordForPoint(int x, int y, boolean absoluteCoord); static native String[][] getAllCountries(); diff --git a/navit/coord.h b/navit/coord.h index cad6a86e8..f3601830d 100644 --- a/navit/coord.h +++ b/navit/coord.h @@ -113,13 +113,13 @@ struct coord_geo_cart { enum coord_format { /** - * Degrees with absolute decimal places (positive or negative) - * ie -20.500000 -110.500000 - */ + * Degrees with absolute decimal places (positive or negative) + * ie -20.500000 -110.500000 + */ DEGREES_DECIMAL_ABSOLUTE, /** - * Degrees with decimal places. + * Degrees with decimal places (positive with heading) * ie 20.500000°N 110.500000°E */ DEGREES_DECIMAL, -- cgit v1.2.1