diff options
Diffstat (limited to 'navit/android/src')
-rw-r--r-- | navit/android/src/org/navitproject/navit/Navit.java | 14 | ||||
-rw-r--r-- | navit/android/src/org/navitproject/navit/NavitTraff.java | 361 |
2 files changed, 353 insertions, 22 deletions
diff --git a/navit/android/src/org/navitproject/navit/Navit.java b/navit/android/src/org/navitproject/navit/Navit.java index bacc15213..011acd9fe 100644 --- a/navit/android/src/org/navitproject/navit/Navit.java +++ b/navit/android/src/org/navitproject/navit/Navit.java @@ -248,10 +248,15 @@ public class Navit extends Activity { private void verifyPermissions() { if (ContextCompat.checkSelfPermission(this, - Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + return; + } else if (ContextCompat.checkSelfPermission(this, + Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED) { + return; + } else { Log.d(TAG,"ask for permission(s)"); ActivityCompat.requestPermissions(this, new String[] { - Manifest.permission.ACCESS_FINE_LOCATION}, MY_PERMISSIONS_REQ_FINE_LOC); + Manifest.permission.ACCESS_BACKGROUND_LOCATION}, MY_PERMISSIONS_REQ_FINE_LOC); } } @@ -728,7 +733,7 @@ public class Navit extends Activity { if (resultCode == RESULT_OK) { String newDir = data.getStringExtra(FileBrowserActivity.returnDirectoryParameter); Log.d(TAG, "selected path= " + newDir); - if (!newDir.contains("/navit")) { + if (!(newDir.contains("/navit") || newDir.contains("/org.navitproject.navit"))) { newDir = newDir + "/navit/"; } else { newDir = newDir + "/"; @@ -774,7 +779,8 @@ public class Navit extends Activity { private void setMapLocation() { Intent fileExploreIntent = new Intent(this,FileBrowserActivity.class); fileExploreIntent - .putExtra(FileBrowserActivity.startDirectoryParameter, "/mnt") + .putExtra(FileBrowserActivity.startDirectoryParameter, + getApplicationContext().getExternalFilesDir(null).toString()) .setAction(FileBrowserActivity.INTENT_ACTION_SELECT_DIR); startActivityForResult(fileExploreIntent,NavitSelectStorage_id); } diff --git a/navit/android/src/org/navitproject/navit/NavitTraff.java b/navit/android/src/org/navitproject/navit/NavitTraff.java index c82d7d293..fd499b70b 100644 --- a/navit/android/src/org/navitproject/navit/NavitTraff.java +++ b/navit/android/src/org/navitproject/navit/NavitTraff.java @@ -25,11 +25,18 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; import android.util.Log; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; /** * The TraFF receiver implementation. @@ -39,11 +46,46 @@ import java.util.List; */ public class NavitTraff extends BroadcastReceiver { + private static final String ACTION_TRAFF_GET_CAPABILITIES = "org.traffxml.traff.GET_CAPABILITIES"; + private static final String ACTION_TRAFF_HEARTBEAT = "org.traffxml.traff.HEARTBEAT"; private static final String ACTION_TRAFF_FEED = "org.traffxml.traff.FEED"; private static final String ACTION_TRAFF_POLL = "org.traffxml.traff.POLL"; + private static final String ACTION_TRAFF_SUBSCRIBE = "org.traffxml.traff.SUBSCRIBE"; + private static final String ACTION_TRAFF_SUBSCRIPTION_CHANGE = "org.traffxml.traff.SUBSCRIPTION_CHANGE"; + private static final String ACTION_TRAFF_UNSUBSCRIBE = "org.traffxml.traff.UNSUBSCRIBE"; + private static final String COLUMN_DATA = "data"; + private static final String CONTENT_SCHEMA = "content"; + private static final String[] ERROR_STRINGS = { + "unknown (0)", + "invalid request (1)", + "subscription rejected by the source (2)", + "requested area not covered (3)", + "requested area partially covered (4)", + "subscription ID not recognized by the source (5)", + "unknown (6)", + "source reported an internal error (7)" + }; + private static final String EXTRA_CAPABILITIES = "capabilities"; private static final String EXTRA_FEED = "feed"; + private static final String EXTRA_FILTER_LIST = "filter_list"; + private static final String EXTRA_PACKAGE = "package"; + private static final String EXTRA_SUBSCRIPTION_ID = "subscription_id"; + private static final String MIME_TYPE_TRAFF = "vnd.android.cursor.dir/org.traffxml.message"; + private static final int RESULT_OK = -1; + private static final int RESULT_INTERNAL_ERROR = 7; + private static final int RESULT_INVALID = 1; + private static final int RESULT_SUBSCRIPTION_REJECTED = 2; + private static final int RESULT_NOT_COVERED = 3; + private static final int RESULT_PARTIALLY_COVERED = 4; + private static final int RESULT_SUBSCRIPTION_UNKNOWN = 5; + private static final String TAG = "NavitTraff"; private final long mCbid; + private final Context context; + + /** Active subscriptions (key is the subscription ID, value is the package ID). */ + private Map<String, String> subscriptions = new HashMap<String, String>(); + /** * Forwards a newly received TraFF feed to the traffic module for processing. * @@ -65,39 +107,322 @@ public class NavitTraff extends BroadcastReceiver { */ NavitTraff(Context context, long cbid) { this.mCbid = cbid; + this.context = context.getApplicationContext(); + + /* An intent filter for TraFF 0.7 events. */ + IntentFilter traffFilter07 = new IntentFilter(); + traffFilter07.addAction(ACTION_TRAFF_FEED); - /* An intent filter for TraFF events. */ - IntentFilter traffFilter = new IntentFilter(); - traffFilter.addAction(ACTION_TRAFF_FEED); - traffFilter.addAction(ACTION_TRAFF_POLL); + /* An intent filter for TraFF 0.8 events. */ + IntentFilter traffFilter08 = new IntentFilter(); + traffFilter08.addAction(ACTION_TRAFF_FEED); + traffFilter08.addDataScheme(CONTENT_SCHEMA); + try { + traffFilter08.addDataType(MIME_TYPE_TRAFF); + } catch (MalformedMimeTypeException e) { + // as long as the constant is a well-formed MIME type, this exception never gets thrown + e.printStackTrace(); + } - context.registerReceiver(this, traffFilter); - /* TODO unregister receiver on exit */ + this.context.registerReceiver(this, traffFilter07); + this.context.registerReceiver(this, traffFilter08); - /* Broadcast a poll intent */ + /* Broadcast a poll intent to all TraFF 0.7-only receivers */ Intent outIntent = new Intent(ACTION_TRAFF_POLL); - PackageManager pm = context.getPackageManager(); - List<ResolveInfo> receivers = pm.queryBroadcastReceivers(outIntent, 0); - if (receivers != null) { - for (ResolveInfo receiver : receivers) { + PackageManager pm = this.context.getPackageManager(); + List<ResolveInfo> receivers07 = pm.queryBroadcastReceivers(outIntent, 0); + List<ResolveInfo> receivers08 = pm.queryBroadcastReceivers(new Intent(ACTION_TRAFF_GET_CAPABILITIES), 0); + if (receivers07 != null) { + /* get receivers which support only TraFF 0.7 and poll them */ + if (receivers08 != null) + receivers07.removeAll(receivers08); + for (ResolveInfo receiver : receivers07) { ComponentName cn = new ComponentName(receiver.activityInfo.applicationInfo.packageName, receiver.activityInfo.name); outIntent = new Intent(ACTION_TRAFF_POLL); outIntent.setComponent(cn); - context.sendBroadcast(outIntent, Manifest.permission.ACCESS_COARSE_LOCATION); + this.context.sendBroadcast(outIntent, Manifest.permission.ACCESS_COARSE_LOCATION); + } + } + } + + void close() { + for (Map.Entry<String, String> subscription : subscriptions.entrySet()) { + Bundle extras = new Bundle(); + extras.putString(EXTRA_SUBSCRIPTION_ID, subscription.getKey()); + sendTraffIntent(this.context, ACTION_TRAFF_UNSUBSCRIBE, null, extras, subscription.getValue(), + Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + this.context.unregisterReceiver(this); + } + + void onFilterUpdate(String filterList) { + /* change existing subscriptions */ + for (Map.Entry<String, String> entry : subscriptions.entrySet()) { + Log.d(TAG, String.format("changing subscription %s (%s)", entry.getKey(), entry.getValue())); + Bundle extras = new Bundle(); + extras.putString(EXTRA_SUBSCRIPTION_ID, entry.getKey()); + extras.putString(EXTRA_FILTER_LIST, filterList); + sendTraffIntent(context, ACTION_TRAFF_SUBSCRIPTION_CHANGE, null, extras, + entry.getValue(), + Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + + /* set up missing subscriptions */ + PackageManager pm = this.context.getPackageManager(); + List<ResolveInfo> receivers = pm.queryBroadcastReceivers(new Intent(ACTION_TRAFF_GET_CAPABILITIES), 0); + if (receivers != null) { + /* filter out receivers to which we are already subscribed */ + Iterator<ResolveInfo> iter = receivers.iterator(); + while (iter.hasNext()) { + ResolveInfo receiver = iter.next(); + if (subscriptions.containsValue(receiver.activityInfo.applicationInfo.packageName)) + iter.remove(); + } + + for (ResolveInfo receiver : receivers) { + Log.d(TAG, "subscribing to " + receiver.activityInfo.applicationInfo.packageName); + Bundle extras = new Bundle(); + extras.putString(EXTRA_PACKAGE, context.getPackageName()); + extras.putString(EXTRA_FILTER_LIST, filterList); + sendTraffIntent(context, ACTION_TRAFF_SUBSCRIBE, null, extras, + receiver.activityInfo.applicationInfo.packageName, + Manifest.permission.ACCESS_COARSE_LOCATION, this); } } } @Override public void onReceive(Context context, Intent intent) { - if ((intent != null) && (intent.getAction().equals(ACTION_TRAFF_FEED))) { - String feed = intent.getStringExtra(EXTRA_FEED); - if (feed == null) { - Log.w(this.getClass().getSimpleName(), "empty feed, ignoring"); - } else { - onFeedReceived(mCbid, feed); + if (intent != null) { + if (intent.getAction().equals(ACTION_TRAFF_FEED)) { + Uri uri = intent.getData(); + if (uri != null) { + /* 0.8 feed */ + String subscriptionId = intent.getStringExtra(EXTRA_SUBSCRIPTION_ID); + if (subscriptions.containsKey(subscriptionId)) + fetchMessages(context, uri); + else { + /* + * If we don’t recognize the subscription, skip processing and unsubscribe. + * Note: if EXTRA_PACKAGE is not set, sendTraffIntent() sends the request to every + * manifest-declared receiver which handles the request. + */ + Log.d(TAG, + String.format("got a feed from %s for unknown subscription %s, URI %s; unsubscribing", + intent.getStringExtra(EXTRA_PACKAGE), subscriptionId, uri)); + Bundle extras = new Bundle(); + extras.putString(EXTRA_SUBSCRIPTION_ID, subscriptionId); + sendTraffIntent(context, ACTION_TRAFF_UNSUBSCRIBE, null, extras, + intent.getStringExtra(EXTRA_PACKAGE), + Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + } else { + /* 0.7 feed */ + String packageName = intent.getStringExtra(EXTRA_PACKAGE); + /* + * If the feed comes from a TraFF 0.8+ source and we are subscribed, skip it. + * As a side effect of the current implementation, if a “bilingual” TraFF 0.7/0.8 + * source sends a broadcast feed before we have subscribed to it, we would process + * the whole feed first, and then subscribe to a subset of that data. + * If that turns out to be an issue, we would need to detect TraFF 0.8-capable + * sources and discard broadcast feeds from them. + */ + if ((packageName != null) && subscriptions.containsValue(packageName)) + return; + String feed = intent.getStringExtra(EXTRA_FEED); + if (feed == null) { + Log.w(this.getClass().getSimpleName(), "empty feed, ignoring"); + } else { + onFeedReceived(mCbid, feed); + } + } // uri != null + } else if (intent.getAction().equals(ACTION_TRAFF_SUBSCRIBE)) { + if (this.getResultCode() != RESULT_OK) { + Bundle extras = this.getResultExtras(true); + if (extras != null) + Log.e(this.getClass().getSimpleName(), String.format("subscription to %s failed, %s", + extras.getString(EXTRA_PACKAGE), formatTraffError(this.getResultCode()))); + else + Log.e(this.getClass().getSimpleName(), String.format("subscription failed, %s", + formatTraffError(this.getResultCode()))); + return; + } + Bundle extras = this.getResultExtras(true); + String data = this.getResultData(); + String packageName = extras.getString(EXTRA_PACKAGE); + String subscriptionId = extras.getString(EXTRA_SUBSCRIPTION_ID); + if (subscriptionId == null) { + Log.e(this.getClass().getSimpleName(), + String.format("subscription to %s failed: no subscription ID returned", packageName)); + return; + } else if (packageName == null) { + Log.e(this.getClass().getSimpleName(), "subscription failed: no package name"); + return; + } else if (data == null) { + Log.w(this.getClass().getSimpleName(), + String.format("subscription to %s successful (ID: %s) but no content URI was supplied. " + + "This is an issue with the source and may result in delayed message retrieval.", + packageName, subscriptionId)); + subscriptions.put(subscriptionId, packageName); + return; + } + Log.d(TAG, "subscription to " + packageName + " successful, ID: " + subscriptionId); + subscriptions.put(subscriptionId, packageName); + fetchMessages(context, Uri.parse(data)); + } else if (intent.getAction().equals(ACTION_TRAFF_SUBSCRIPTION_CHANGE)) { + if (this.getResultCode() != RESULT_OK) { + Bundle extras = this.getResultExtras(true); + if (extras != null) + Log.e(this.getClass().getSimpleName(), + String.format("subscription change for %s failed: %s", + extras.getString(EXTRA_SUBSCRIPTION_ID), + formatTraffError(this.getResultCode()))); + else + Log.e(this.getClass().getSimpleName(), + String.format("subscription change failed: %s", + formatTraffError(this.getResultCode()))); + return; + } + Bundle extras = intent.getExtras(); + String data = this.getResultData(); + String subscriptionId = extras.getString(EXTRA_SUBSCRIPTION_ID); + if (subscriptionId == null) { + Log.w(this.getClass().getSimpleName(), + "subscription change successful but the source did not specify the subscription ID. " + + "This is an issue with the source and may result in delayed message retrieval. " + + "URI: " + data); + return; + } else if (data == null) { + Log.w(this.getClass().getSimpleName(), + String.format("subscription change for %s successful but no content URI was supplied. " + + "This is an issue with the source and may result in delayed message retrieval.", + subscriptionId)); + return; + } else if (!subscriptions.containsKey(subscriptionId)) { + Log.e(this.getClass().getSimpleName(), + "subscription change failed: unknown subscription ID " + subscriptionId); + return; + } + Log.d(TAG, "subscription change for " + subscriptionId + " successful"); + fetchMessages(context, Uri.parse(data)); + } else if (intent.getAction().equals(ACTION_TRAFF_UNSUBSCRIBE)) { + /* + * If we ever unsubscribe for reasons other than that we are shutting down or got a feed for + * a subscription we don’t recognize, or if we start keeping a persistent list of + * subscriptions, we need to delete the subscription from our list. Until then, there is + * nothing to do here: either the subscription isn’t in the list, or we are about to shut + * down and the whole list is about to get discarded. + */ + } else if (intent.getAction().equals(ACTION_TRAFF_HEARTBEAT)) { + String subscriptionId = intent.getStringExtra(EXTRA_SUBSCRIPTION_ID); + if (subscriptions.containsKey(subscriptionId)) { + Log.d(TAG, + String.format("got a heartbeat from %s for subscription %s; sending result", + intent.getStringExtra(EXTRA_PACKAGE), subscriptionId)); + this.setResult(RESULT_OK, null, null); + } else { + /* + * If we don’t recognize the subscription, skip reply and unsubscribe. + * Note: if EXTRA_PACKAGE is not set, sendTraffIntent() sends the request to every + * manifest-declared receiver which handles the request. + */ + Log.d(TAG, + String.format("got a heartbeat from %s for unknown subscription %s; unsubscribing", + intent.getStringExtra(EXTRA_PACKAGE), subscriptionId)); + Bundle extras = new Bundle(); + extras.putString(EXTRA_SUBSCRIPTION_ID, subscriptionId); + sendTraffIntent(context, ACTION_TRAFF_UNSUBSCRIBE, null, extras, + intent.getStringExtra(EXTRA_PACKAGE), + Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + } // intent.getAction() + } // intent != null + } + + /** + * Fetches messages from a content provider. + * + * @param context The context to use for the content resolver + * @param uri The content provider URI + */ + private void fetchMessages(Context context, Uri uri) { + try { + Cursor cursor = context.getContentResolver().query(uri, new String[] {COLUMN_DATA}, null, null, null); + if (cursor == null) + return; + if (cursor.getCount() < 1) { + cursor.close(); + return; } + StringBuilder builder = new StringBuilder("<feed>\n"); + while (cursor.moveToNext()) + builder.append(cursor.getString(cursor.getColumnIndex(COLUMN_DATA))).append("\n"); + builder.append("</feed>"); + cursor.close(); + onFeedReceived(mCbid, builder.toString()); + } catch (Exception e) { + Log.w(TAG, String.format("Unable to fetch messages from %s", uri.toString()), e); + e.printStackTrace(); } } + + /** + * Sends a TraFF intent to a source. This encapsulates most of the low-level Android handling. + * + * <p>If the recipient specified in {@code packageName} declares multiple receivers for the intent in its + * manifest, a separate intent will be delivered to each of them. The intent will not be delivered to + * receivers registered at runtime. + * + * <p>All intents are sent as explicit ordered broadcasts. This means two things: + * + * <p>Any app which declares a matching receiver in its manifest will be woken up to process the intent. + * This works even with certain Android 7 builds which restrict intent delivery to apps which are not + * currently running. + * + * <p>It is safe for the recipient to unconditionally set result data. If the recipient does not set result + * data, the result will have a result code of {@link #RESULT_INTERNAL_ERROR}, no data and no extras. + * + * @param context The context + * @param action The intent action. + * @param data The intent data (for TraFF, this is the content provider URI), or null + * @param extras The extras for the intent + * @param packageName The package name for the recipient, or null to deliver the intent to all matching receivers + * @param receiverPermission A permission which the recipient must hold, or null if not required + * @param resultReceiver A BroadcastReceiver which will receive the result for the intent + */ + /* From traff-consumer-android, by the same author and re-licensed under GPL2 for Navit */ + public static void sendTraffIntent(Context context, String action, Uri data, Bundle extras, String packageName, + String receiverPermission, BroadcastReceiver resultReceiver) { + Intent outIntent = new Intent(action); + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> receivers = pm.queryBroadcastReceivers(outIntent, 0); + if (receivers != null) + for (ResolveInfo receiver : receivers) { + if ((packageName != null) && !packageName.equals(receiver.activityInfo.applicationInfo.packageName)) + continue; + ComponentName cn = new ComponentName(receiver.activityInfo.applicationInfo.packageName, + receiver.activityInfo.name); + outIntent = new Intent(action); + if (data != null) + outIntent.setData(data); + if (extras != null) + outIntent.putExtras(extras); + outIntent.setComponent(cn); + context.sendOrderedBroadcast(outIntent, + receiverPermission, + resultReceiver, + null, // scheduler, + RESULT_INTERNAL_ERROR, // initialCode, + null, // initialData, + null); + } + } + + private static String formatTraffError(int code) { + if ((code < 0) || (code >= ERROR_STRINGS.length)) + return String.format("unknown (%d)", code); + else + return ERROR_STRINGS[code]; + } } |