diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt index 82d2a7d27..acef43d39 100644 --- a/android/app/src/main/cpp/CMakeLists.txt +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -17,6 +17,7 @@ set(SRC app/organicmaps/opengl/gl3stub.h app/organicmaps/platform/GuiThread.hpp app/organicmaps/platform/AndroidPlatform.hpp + app/organicmaps/traffxml/AndroidTraffSource.hpp app/organicmaps/util/Distance.hpp app/organicmaps/util/FeatureIdBuilder.hpp app/organicmaps/vulkan/android_vulkan_context_factory.hpp @@ -71,6 +72,8 @@ set(SRC app/organicmaps/platform/PThreadImpl.cpp app/organicmaps/platform/SecureStorage.cpp app/organicmaps/platform/SocketImpl.cpp + app/organicmaps/traffxml/AndroidTraffSource.cpp + app/organicmaps/traffxml/SourceImpl.cpp app/organicmaps/util/Config.cpp app/organicmaps/util/GeoUtils.cpp app/organicmaps/util/HttpClient.cpp diff --git a/android/app/src/main/cpp/app/organicmaps/traffxml/AndroidTraffSource.cpp b/android/app/src/main/cpp/app/organicmaps/traffxml/AndroidTraffSource.cpp new file mode 100644 index 000000000..ac2ca8109 --- /dev/null +++ b/android/app/src/main/cpp/app/organicmaps/traffxml/AndroidTraffSource.cpp @@ -0,0 +1,115 @@ +#include "AndroidTraffSource.hpp" + +#include "app/organicmaps/core/jni_helper.hpp" + +namespace traffxml { +void AndroidTraffSourceV0_7::Create(TraffSourceManager & manager) +{ + std::unique_ptr source = std::unique_ptr(new AndroidTraffSourceV0_7(manager)); + manager.RegisterSource(std::move(source)); +} + +AndroidTraffSourceV0_7::AndroidTraffSourceV0_7(TraffSourceManager & manager) + : TraffSource(manager) +{ + JNIEnv * env = jni::GetEnv(); + + static jclass const implClass = jni::GetGlobalClassRef(env, "app/organicmaps/traffxml/SourceImplV0_7"); + + static jmethodID const implConstructor = jni::GetConstructorID(env, implClass, "(Landroid/content/Context;J)V"); + + jlong nativeManager = reinterpret_cast(&manager); + + jobject implObject = env->NewObject( + implClass, implConstructor, android::Platform::Instance().GetContext(), nativeManager); + + m_implObject = env->NewGlobalRef(implObject); + + m_subscribeImpl = jni::GetMethodID(env, m_implObject, "subscribe", "(Ljava/lang/String;)V"); + m_unsubscribeImpl = jni::GetMethodID(env, m_implObject, "unsubscribe", "()V"); +} + +AndroidTraffSourceV0_7::~AndroidTraffSourceV0_7() +{ + jni::GetEnv()->DeleteGlobalRef(m_implObject); +} + +void AndroidTraffSourceV0_7::Close() +{ + Unsubscribe(); +} + +void AndroidTraffSourceV0_7::Subscribe(std::set & mwms) +{ + jni::GetEnv()->CallVoidMethod(m_implObject, m_subscribeImpl, nullptr); +} + +void AndroidTraffSourceV0_7::Unsubscribe() +{ + jni::GetEnv()->CallVoidMethod(m_implObject, m_unsubscribeImpl); +} + +void AndroidTraffSourceV0_8::Create(TraffSourceManager & manager, std::string const & packageId) +{ + std::unique_ptr source = std::unique_ptr(new AndroidTraffSourceV0_8(manager, packageId)); + manager.RegisterSource(std::move(source)); +} + +AndroidTraffSourceV0_8::AndroidTraffSourceV0_8(TraffSourceManager & manager, std::string const & packageId) + : TraffSource(manager) +{ + JNIEnv * env = jni::GetEnv(); + + static jclass const implClass = jni::GetGlobalClassRef(env, "app/organicmaps/traffxml/SourceImplV0_8"); + + static jmethodID const implConstructor = jni::GetConstructorID(env, implClass, "(Landroid/content/Context;JLjava/lang/String;)V"); + + jlong nativeManager = reinterpret_cast(&manager); + + jobject implObject = env->NewObject( + implClass, implConstructor, android::Platform::Instance().GetContext(), nativeManager, jni::ToJavaString(env, packageId)); + + m_implObject = env->NewGlobalRef(implObject); + + m_subscribeImpl = jni::GetMethodID(env, m_implObject, "subscribe", "(Ljava/lang/String;)V"); + m_changeSubscriptionImpl = jni::GetMethodID(env, m_implObject, "changeSubscription", "(Ljava/lang/String;)V"); + m_unsubscribeImpl = jni::GetMethodID(env, m_implObject, "unsubscribe", "()V"); + + // TODO packageId (if we need that at all here) +} + +AndroidTraffSourceV0_8::~AndroidTraffSourceV0_8() +{ + jni::GetEnv()->DeleteGlobalRef(m_implObject); +} + +void AndroidTraffSourceV0_8::Close() +{ + Unsubscribe(); +} + +void AndroidTraffSourceV0_8::Subscribe(std::set & mwms) +{ + JNIEnv * env = jni::GetEnv(); + std::string data = "\n" + + GetMwmFilters(mwms) + + ""; + + env->CallVoidMethod(m_implObject, m_subscribeImpl, jni::ToJavaString(env, data)); +} + +void AndroidTraffSourceV0_8::ChangeSubscription(std::set & mwms) +{ + JNIEnv * env = jni::GetEnv(); + std::string data = "\n" + + GetMwmFilters(mwms) + + ""; + + env->CallVoidMethod(m_implObject, m_changeSubscriptionImpl, jni::ToJavaString(env, data)); +} + +void AndroidTraffSourceV0_8::Unsubscribe() +{ + jni::GetEnv()->CallVoidMethod(m_implObject, m_unsubscribeImpl); +} +} // namespace traffxml diff --git a/android/app/src/main/cpp/app/organicmaps/traffxml/AndroidTraffSource.hpp b/android/app/src/main/cpp/app/organicmaps/traffxml/AndroidTraffSource.hpp new file mode 100644 index 000000000..05e04c659 --- /dev/null +++ b/android/app/src/main/cpp/app/organicmaps/traffxml/AndroidTraffSource.hpp @@ -0,0 +1,199 @@ +#pragma once + +#include "traffxml/traff_source.hpp" + +namespace traffxml +{ +/** + * @brief A TraFF source which relies on Android Binder for message delivery, using version 0.7 of the TraFF protocol. + * + * TraFF 0.7 does not support subscriptions. Messages are broadcast as the payload to a `FEED` intent. + */ +class AndroidTraffSourceV0_7 : public TraffSource +{ +public: + /** + * @brief Creates a new `AndroidTraffSourceV0_7` instance and registers it with the traffic manager. + * + * @param manager The traffic manager to register the new instance with + */ + static void Create(TraffSourceManager & manager); + + virtual ~AndroidTraffSourceV0_7() override; + + /** + * @brief Prepares the traffic source for unloading. + */ + // TODO do we need a close operation here? + // TODO move this to the parent class and override it here? + void Close(); + + /** + * @brief Subscribes to a traffic service. + * + * TraFF 0.7 does not support subscriptions. This implementation registers a broadcast receiver. + * + * @param mwms The MWMs for which data is needed (not used by this implementation). + */ + virtual void Subscribe(std::set & mwms) override; + + /** + * @brief Changes an existing traffic subscription. + * + * This implementation does nothing, as TraFF 0.7 does not support subscriptions. + * + * @param mwms The new set of MWMs for which data is needed. + */ + virtual void ChangeSubscription(std::set & mwms) override {}; + + /** + * @brief Unsubscribes from a traffic service we are subscribed to. + * + * TraFF 0.7 does not support subscriptions. This implementation unregisters the broadcast + * receiver which was registered by `Subscribe()`. + */ + virtual void Unsubscribe() override; + + /** + * @brief Whether this source should be polled. + * + * Prior to calling `Poll()` on a source, the caller should always first call `IsPollNeeded()` and + * poll the source only if the result is true. + * + * This implementation always returns false, as message delivery on Android uses `FEED` (push). + * + * @return true if the source should be polled, false if not. + */ + virtual bool IsPollNeeded() override { return false; }; + + /** + * @brief Polls the traffic service for updates. + * + * This implementation does nothing, as message delivery on Android uses `FEED` (push). + */ + virtual void Poll() override {}; + +protected: + /** + * @brief Constructs a new `AndroidTraffSourceV0_7`. + * @param manager The `TrafficSourceManager` instance to register the source with. + */ + AndroidTraffSourceV0_7(TraffSourceManager & manager); + +private: + // TODO “subscription” (i.e. broadcast receiver) state + + /** + * @brief The Java implementation class instance. + */ + jobject m_implObject; + + /** + * @brief The Java subscribe method. + */ + jmethodID m_subscribeImpl; + + /** + * @brief The Java unsubscribe method. + */ + jmethodID m_unsubscribeImpl; +}; + +/** + * @brief A TraFF source which relies on Android Binder for message delivery, using version 0.8 of the TraFF protocol. + * + * TraFF 0.8 supports subscriptions. Messages are announced through a `FEED` intent, whereupon the + * consumer can retrieve them from a content provider. + */ +class AndroidTraffSourceV0_8 : public TraffSource +{ +public: + /** + * @brief Creates a new `AndroidTraffSourceV0_8` instance and registers it with the traffic manager. + * + * @param manager The traffic manager to register the new instance with + * @param packageId The package ID of the app providing the TraFF source. + */ + static void Create(TraffSourceManager & manager, std::string const & packageId); + + virtual ~AndroidTraffSourceV0_8() override; + + /** + * @brief Prepares the traffic source for unloading. + * + * If there is still an active subscription, it unsubscribes, but without processing the result + * received from the service. Otherwise, teardown is a no-op. + */ + // TODO move this to the parent class and override it here? + void Close(); + + /** + * @brief Subscribes to a traffic service. + * + * @param mwms The MWMs for which data is needed. + */ + virtual void Subscribe(std::set & mwms) override; + + /** + * @brief Changes an existing traffic subscription. + * + * @param mwms The new set of MWMs for which data is needed. + */ + virtual void ChangeSubscription(std::set & mwms) override; + + /** + * @brief Unsubscribes from a traffic service we are subscribed to. + */ + virtual void Unsubscribe() override; + + /** + * @brief Whether this source should be polled. + * + * Prior to calling `Poll()` on a source, the caller should always first call `IsPollNeeded()` and + * poll the source only if the result is true. + * + * This implementation always returns false, as message delivery on Android uses `FEED` (push). + * + * @return true if the source should be polled, false if not. + */ + virtual bool IsPollNeeded() override { return false; }; + + /** + * @brief Polls the traffic service for updates. + * + * This implementation does nothing, as message delivery on Android uses `FEED` (push). + */ + virtual void Poll() override {}; + +protected: + /** + * @brief Constructs a new `AndroidTraffSourceV0_8`. + * @param manager The `TrafficSourceManager` instance to register the source with. + * @param packageId The package ID of the app providing the TraFF source. + */ + AndroidTraffSourceV0_8(TraffSourceManager & manager, std::string const & packageId); + +private: + // TODO subscription state + + /** + * @brief The Java implementation class instance. + */ + jobject m_implObject; + + /** + * @brief The Java subscribe method. + */ + jmethodID m_subscribeImpl; + + /** + * @brief The Java changeSubscription method. + */ + jmethodID m_changeSubscriptionImpl; + + /** + * @brief The Java unsubscribe method. + */ + jmethodID m_unsubscribeImpl; +}; +} // namespace traffxml diff --git a/android/app/src/main/cpp/app/organicmaps/traffxml/SourceImpl.cpp b/android/app/src/main/cpp/app/organicmaps/traffxml/SourceImpl.cpp new file mode 100644 index 000000000..45f89b2f5 --- /dev/null +++ b/android/app/src/main/cpp/app/organicmaps/traffxml/SourceImpl.cpp @@ -0,0 +1,34 @@ +// TODO which of the two do we need? (jni_helper includes jni) +//#include +#include "app/organicmaps/core/jni_helper.hpp" + +#include "traffxml/traff_source.hpp" +#include "traffxml/traff_model_xml.hpp" + +#include + +extern "C" +{ + JNIEXPORT void JNICALL + Java_app_organicmaps_traffxml_SourceImpl_onFeedReceivedImpl(JNIEnv * env, jclass thiz, jlong nativeManager, jstring feed) + { + std::string feedStd = jni::ToNativeString(env, feed); + pugi::xml_document document; + traffxml::TraffFeed parsedFeed; + + if (!document.load_string(feedStd.c_str())) + { + LOG(LWARNING, ("Feed is not a well-formed XML document")); + return; + } + + if (!traffxml::ParseTraff(document, std::nullopt, parsedFeed)) + { + LOG(LWARNING, ("Feed is not a valid TraFF feed")); + return; + } + + traffxml::TraffSourceManager & manager = *reinterpret_cast(nativeManager); + manager.ReceiveFeed(parsedFeed); + } +} diff --git a/android/app/src/main/java/app/organicmaps/traffxml/AndroidConsumer.java b/android/app/src/main/java/app/organicmaps/traffxml/AndroidConsumer.java new file mode 100644 index 000000000..1b8b19878 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/traffxml/AndroidConsumer.java @@ -0,0 +1,111 @@ +/* + * Copyright © 2017–2020 traffxml.org. + * + * Relicensed to CoMaps by the original author. + */ + +package app.organicmaps.traffxml; + +import java.util.List; + +import app.organicmaps.traffxml.Version; +import app.organicmaps.traffxml.AndroidTransport; + +import android.content.BroadcastReceiver; +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.net.Uri; +import android.os.Bundle; + +public class AndroidConsumer { + /** + * Creates an Intent filter which matches the Intents a TraFF consumer needs to receive. + * + *

Different filters are available for consumers implementing different versions of the TraFF + * specification. + * + * @param version The version of the TraFF specification (one of the constants in {@link org.traffxml.traff.Version}) + * + * @return An intent filter matching the necessary Intents + */ + public static IntentFilter createIntentFilter(int version) { + IntentFilter res = new IntentFilter(); + switch (version) { + case Version.V0_7: + res.addAction(AndroidTransport.ACTION_TRAFF_PUSH); + break; + case Version.V0_8: + res.addAction(AndroidTransport.ACTION_TRAFF_PUSH); + res.addDataScheme(AndroidTransport.CONTENT_SCHEMA); + try { + res.addDataType(AndroidTransport.MIME_TYPE_TRAFF); + } catch (MalformedMimeTypeException e) { + // as long as the constant is a well-formed MIME type, this exception never gets thrown + e.printStackTrace(); + } + break; + default: + throw new IllegalArgumentException("Invalid version code: " + version); + } + return res; + } + + /** + * Sends a TraFF intent to a source. + * + *

This encapsulates most of the low-level Android handling. + * + *

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. + * + *

All intents are sent as explicit ordered broadcasts. This means two things: + * + *

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. + * + *

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 org.traffxml.transport.android.AndroidTransport#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 intent 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 + */ + 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 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, + AndroidTransport.RESULT_INTERNAL_ERROR, // initialCode, + null, // initialData, + null); + } + } +} diff --git a/android/app/src/main/java/app/organicmaps/traffxml/AndroidTransport.java b/android/app/src/main/java/app/organicmaps/traffxml/AndroidTransport.java new file mode 100644 index 000000000..abb69d965 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/traffxml/AndroidTransport.java @@ -0,0 +1,222 @@ +/* + * Copyright © 2019–2020 traffxml.org. + * + * Relicensed to CoMaps by the original author. + */ + +package app.organicmaps.traffxml; + +public class AndroidTransport { + /** + * Intent to poll a peer for its capabilities. + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + */ + public static final String ACTION_TRAFF_GET_CAPABILITIES = "org.traffxml.traff.GET_CAPABILITIES"; + + /** + * Intent to send a heartbeat to a peer. + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + */ + public static final String ACTION_TRAFF_HEARTBEAT = "org.traffxml.traff.GET_HEARTBEAT"; + + /** + * Intent to poll a source for information. + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + * + *

Polling is a legacy feature on Android and deprecated in TraFF 0.8 (rather than polling, TraFF 0.8 + * applications query the content provider). Therefore, poll operations are subscriptionless, and the + * source should either reply with all messages it currently holds, or ignore the request. + */ + @Deprecated + public static final String ACTION_TRAFF_POLL = "org.traffxml.traff.POLL"; + + /** + * Intent for a push feed. + * + *

This is a broadcast intent. It can be used in different forms: + * + *

As of TraFF 0.8, it must be sent as an explicit broadcast and include the + * {@link #EXTRA_SUBSCRIPTION_ID} extra. The intent data must be a URI to the content provider from which + * the messages can be retrieved. The {@link #EXTRA_FEED} extra is not supported. The feed is part of a + * subscription and will contain only changes over feeds sent previously as part of the same + * subscription. + * + *

Legacy applications omit the {@link #EXTRA_SUBSCRIPTION_ID} extra and may send it as an implicit + * broadcast. If an application supports both legacy transport and TraFF 0.8 or later, it must include + * the {@link #EXTRA_PACKAGE} extra. The feed is sent in the {@link #EXTRA_FEED} extra, as legacy + * applications may not support content providers. If sent as a response to a subscriptionless poll, the + * source should include all messages it holds, else the set of messages included is at the discretion of + * the source. + * + *

Future applications may reintroduce unsolicited push operations for certain scenarios. + */ + public static final String ACTION_TRAFF_PUSH = "org.traffxml.traff.FEED"; + + /** + * Intent for a subscription request. + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + * + *

The filter list must be specified in the {@link #EXTRA_FILTER_LIST} extra. + * + *

The sender must indicate its package name in the {@link #EXTRA_PACKAGE} extra. + */ + public static final String ACTION_TRAFF_SUBSCRIBE = "org.traffxml.traff.SUBSCRIBE"; + + /** + * Intent for a subscription change request, + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + * + *

This intent must have {@link #EXTRA_SUBSCRIPTION_ID} set to the ID of an existing subscription between + * the calling consumer and the source which receives the broadcast. + * + *

The new filter list must be specified in the {@link #EXTRA_FILTER_LIST} extra. + */ + public static final String ACTION_TRAFF_SUBSCRIPTION_CHANGE = "org.traffxml.traff.SUBSCRIPTION_CHANGE"; + + /** + * Intent for an unsubscribe request, + * + *

This is a broadcast intent and must be sent as an explicit broadcast. + * + *

This intent must have {@link #EXTRA_SUBSCRIPTION_ID} set to the ID of an existing subscription between + * the calling consumer and the source which receives the broadcast. It signals that the consumer is no + * longer interested in receiving messages related to that subscription, and that the source should stop + * sending updates. Unsubscribing from a nonexistent subscription is a no-op. + */ + public static final String ACTION_TRAFF_UNSUBSCRIBE = "org.traffxml.traff.UNSUBSCRIBE"; + + /** + * Name for the column which holds the message data. + */ + public static final String COLUMN_DATA = "data"; + + /** + * Schema for TraFF content URIs. + */ + public static final String CONTENT_SCHEMA = "content"; + + /** + * String representations of TraFF result codes + */ + public 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)" + }; + + /** + * Extra which contains the capabilities of the peer. + * + *

This is a String extra. It contains a {@code capabilities} XML element. + */ + public static final String EXTRA_CAPABILITIES = "capabilities"; + + /** + * Extra which contains a TraFF feed. + * + *

This is a String extra. It contains a {@code feed} XML element. + * + *

The sender should be careful to keep the size of this extra low, as Android has a 1 MByte limit on all + * pending Binder transactions. However, there is no feedback to the sender about the capacity still + * available, or whether a request exceeds that limit. Therefore, senders should keep the size if each + * feed significantly below that limit. If necessary, they should split up a feed into multiple smaller + * ones and send them with a delay in between. + * + *

This mechanism is deprecated since TraFF 0.8 and peers are no longer required to support it. Peers + * which support TraFF 0.8 must rely on content providers for message transport. + */ + @Deprecated + public static final String EXTRA_FEED = "feed"; + + /** + * Extra which contains a filter list. + * + *

This is a String extra. It contains a {@code filter_list} XML element. + */ + public static final String EXTRA_FILTER_LIST = "filter_list"; + + /** + * Extra which contains the package name of the app sending it. + * + *

This is a String extra. + */ + public static final String EXTRA_PACKAGE = "package"; + + /** + * Extra which contains a subscription ID. + * + *

This is a String extra. + */ + public static final String EXTRA_SUBSCRIPTION_ID = "subscription_id"; + + /** + * Extra which contains the timeout duration for a subscription. + * + *

This is an integer extra. + */ + public static final String EXTRA_TIMEOUT = "timeout"; + + /** + * The MIME type for TraFF content providers. + */ + public static final String MIME_TYPE_TRAFF = "vnd.android.cursor.dir/org.traffxml.message"; + + /** + * The operation completed successfully. + */ + public static final int RESULT_OK = -1; + + /** + * An internal error prevented the recipient from fulfilling the request. + */ + public static final int RESULT_INTERNAL_ERROR = 7; + + /** + * A nonexistent operation was attempted, or an operation was attempted with incomplete or otherwise + * invalid data. + */ + public static final int RESULT_INVALID = 1; + + /** + * The subscription was rejected, and no messages will be sent. + */ + public static final int RESULT_SUBSCRIPTION_REJECTED = 2; + + /** + * The subscription was rejected because the source will never provide messages matching the selection. + */ + public static final int RESULT_NOT_COVERED = 3; + + /** + * The subscription was accepted but the source can only provide messages for parts of the selection. + */ + public static final int RESULT_PARTIALLY_COVERED = 4; + + /** + * The request failed because it refers to a subscription which does not exist between the source and + * consumer involved. + */ + public static final int RESULT_SUBSCRIPTION_UNKNOWN = 5; + + /** + * The request failed because the aggregator does not accept unsolicited push requests from the sensor. + */ + public static final int RESULT_PUSH_REJECTED = 6; + + public static String formatTraffError(int code) { + if ((code < 0) || (code >= ERROR_STRINGS.length)) + return String.format("unknown (%d)", code); + else + return ERROR_STRINGS[code]; + } +} diff --git a/android/app/src/main/java/app/organicmaps/traffxml/SourceImpl.java b/android/app/src/main/java/app/organicmaps/traffxml/SourceImpl.java new file mode 100644 index 000000000..9ef6e55e8 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/traffxml/SourceImpl.java @@ -0,0 +1,70 @@ +package app.organicmaps.traffxml; + +import android.content.BroadcastReceiver; +import android.content.Context; + +/** + * Abstract superclass for TraFF source implementations. + */ +public abstract class SourceImpl extends BroadcastReceiver +{ + /** + * Creates a new instance. + * + * @param context The application context + */ + public SourceImpl(Context context, long nativeManager) + { + super(); + this.context = context; + this.nativeManager = nativeManager; + } + + protected Context context; + + /** + * The native `TraffSourceManager` instance. + */ + protected long nativeManager; + + /** + * Subscribes to a traffic source. + * + * @param filterList The filter list in XML format + */ + public abstract void subscribe(String filterList); + + /** + * Changes an existing traffic subscription. + * + * @param filterList The filter list in XML format + */ + public abstract void changeSubscription(String filterList); + + /** + * Unsubscribes from a traffic source we are subscribed to. + */ + public abstract void unsubscribe(); + + /** + * Forwards a newly received TraFF feed to the traffic module for processing. + * + * Called when a TraFF feed is received. This is a wrapper around {@link #onFeedReceivedImpl(long, String)}. + * + * @param feed The TraFF feed + */ + protected void onFeedReceived(String feed) + { + onFeedReceivedImpl(nativeManager, feed); + } + + /** + * Forwards a newly received TraFF feed to the traffic module for processing. + * + * Called when a TraFF feed is received. + * + * @param nativeManager The native `TraffSourceManager` instance + * @param feed The TraFF feed + */ + protected static native void onFeedReceivedImpl(long nativeManager, String feed); +} diff --git a/android/app/src/main/java/app/organicmaps/traffxml/SourceImplV0_7.java b/android/app/src/main/java/app/organicmaps/traffxml/SourceImplV0_7.java new file mode 100644 index 000000000..981e2affc --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/traffxml/SourceImplV0_7.java @@ -0,0 +1,127 @@ +package app.organicmaps.traffxml; + +import java.util.ArrayList; +import java.util.List; + +import android.Manifest; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import app.organicmaps.util.log.Logger; + +/** + * Implementation for a TraFF 0.7 source. + */ +public class SourceImplV0_7 extends SourceImpl +{ + private PackageManager pm; + + /** + * Creates a new instance. + * + * @param context The application context + */ + public SourceImplV0_7(Context context, long nativeManager) + { + super(context, nativeManager); + // TODO Auto-generated constructor stub + } + + /** + * Subscribes to a traffic source. + * + * @param filterList The filter list in XML format + */ + @Override + public void subscribe(String filterList) + { + IntentFilter traffFilter07 = new IntentFilter(); + traffFilter07.addAction(AndroidTransport.ACTION_TRAFF_PUSH); + + this.context.registerReceiver(this, traffFilter07); + + // Broadcast a poll intent to all TraFF 0.7-only receivers + Intent outIntent = new Intent(AndroidTransport.ACTION_TRAFF_POLL); + pm = this.context.getPackageManager(); + List receivers07 = pm.queryBroadcastReceivers(outIntent, 0); + List receivers08 = pm.queryBroadcastReceivers(new Intent(AndroidTransport.ACTION_TRAFF_GET_CAPABILITIES), 0); + if (receivers07 != null) + { + /* + * Get receivers which support only TraFF 0.7 and poll them. + * If there are no TraFF 0.7 sources at the moment, we register the receiver nonetheless. + * That way, if any new sources are added during the session, we get any messages they send. + */ + if (receivers08 != null) + receivers07.removeAll(receivers08); + for (ResolveInfo receiver : receivers07) + { + ComponentName cn = new ComponentName(receiver.activityInfo.applicationInfo.packageName, + receiver.activityInfo.name); + outIntent = new Intent(AndroidTransport.ACTION_TRAFF_POLL); + outIntent.setComponent(cn); + this.context.sendBroadcast(outIntent, Manifest.permission.ACCESS_COARSE_LOCATION); + } + } + } + + /** + * Changes an existing traffic subscription. + * + * This implementation does nothing, as TraFF 0.7 does not support subscriptions. + * + * @param filterList The filter list in XML format + */ + @Override + public void changeSubscription(String filterList) + { + // NOP + } + + /** + * Unsubscribes from a traffic source we are subscribed to. + */ + @Override + public void unsubscribe() + { + this.context.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) + { + if (intent == null) + return; + + if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_PUSH)) + { + /* 0.7 feed */ + String packageName = intent.getStringExtra(AndroidTransport.EXTRA_PACKAGE); + /* + * If the feed comes from a TraFF 0.8+ source, skip it (this may happen with “bilingual” + * TraFF 0.7/0.8 sources). That ensures the only way to get information from such sources is + * through a TraFF 0.8 subscription. Fetching the list from scratch each time ensures that + * apps installed during runtime get considered.) + */ + if (packageName != null) + { + for (ResolveInfo info : pm.queryBroadcastReceivers(new Intent(AndroidTransport.ACTION_TRAFF_GET_CAPABILITIES), 0)) + if (packageName.equals(info.resolvePackageName)) + return; + } + String feed = intent.getStringExtra(AndroidTransport.EXTRA_FEED); + if (feed == null) + { + Logger.w(this.getClass().getSimpleName(), "empty feed, ignoring"); + } + else + { + onFeedReceived(feed); + } + } + } + +} diff --git a/android/app/src/main/java/app/organicmaps/traffxml/SourceImplV0_8.java b/android/app/src/main/java/app/organicmaps/traffxml/SourceImplV0_8.java new file mode 100644 index 000000000..4729643c8 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/traffxml/SourceImplV0_8.java @@ -0,0 +1,232 @@ +package app.organicmaps.traffxml; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import app.organicmaps.util.log.Logger; + +/** + * Implementation for a TraFF 0.8 source. + */ +public class SourceImplV0_8 extends SourceImpl +{ + + private String packageName; + private String subscriptionId = null; + + /** + * Creates a new instance. + * + * @param context The application context + * @param packageName The package name for the source + */ + public SourceImplV0_8(Context context, long nativeManager, String packageName) + { + super(context, nativeManager); + this.packageName = packageName; + } + + /** + * Subscribes to a traffic source. + * + * @param filterList The filter list in XML format + */ + @Override + public void subscribe(String filterList) + { + IntentFilter filter = new IntentFilter(); + filter.addAction(AndroidTransport.ACTION_TRAFF_PUSH); + filter.addDataScheme(AndroidTransport.CONTENT_SCHEMA); + try + { + filter.addDataType(AndroidTransport.MIME_TYPE_TRAFF); + } + catch (MalformedMimeTypeException e) + { + // as long as the constant is a well-formed MIME type, this exception never gets thrown + // TODO revisit logging + e.printStackTrace(); + } + + context.registerReceiver(this, filter); + } + + /** + * Changes an existing traffic subscription. + * + * @param filterList The filter list in XML format + */ + @Override + public void changeSubscription(String filterList) + { + Bundle extras = new Bundle(); + extras.putString(AndroidTransport.EXTRA_SUBSCRIPTION_ID, subscriptionId); + extras.putString(AndroidTransport.EXTRA_FILTER_LIST, filterList); + AndroidConsumer.sendTraffIntent(context, AndroidTransport.ACTION_TRAFF_SUBSCRIPTION_CHANGE, null, + extras, packageName, Manifest.permission.ACCESS_COARSE_LOCATION, this); + } + + /** + * Unsubscribes from a traffic source we are subscribed to. + */ + @Override + public void unsubscribe() + { + Bundle extras = new Bundle(); + extras.putString(AndroidTransport.EXTRA_SUBSCRIPTION_ID, subscriptionId); + AndroidConsumer.sendTraffIntent(this.context, AndroidTransport.ACTION_TRAFF_UNSUBSCRIBE, null, + extras, packageName, Manifest.permission.ACCESS_COARSE_LOCATION, this); + + this.context.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) + { + if (intent == null) + return; + + if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_PUSH)) + { + Uri uri = intent.getData(); + if (uri != null) + { + /* 0.8 feed */ + String subscriptionId = intent.getStringExtra(AndroidTransport.EXTRA_SUBSCRIPTION_ID); + if (subscriptionId.equals(this.subscriptionId)) + fetchMessages(context, uri); + } + else + { + Logger.w(this.getClass().getSimpleName(), "no URI in feed, ignoring"); + } // uri != null + } else if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_SUBSCRIBE)) { + if (this.getResultCode() != AndroidTransport.RESULT_OK) { + Bundle extras = this.getResultExtras(true); + if (extras != null) + Logger.e(this.getClass().getSimpleName(), String.format("subscription to %s failed, %s", + extras.getString(AndroidTransport.EXTRA_PACKAGE), AndroidTransport.formatTraffError(this.getResultCode()))); + else + Logger.e(this.getClass().getSimpleName(), String.format("subscription failed, %s", + AndroidTransport.formatTraffError(this.getResultCode()))); + return; + } + Bundle extras = this.getResultExtras(true); + String data = this.getResultData(); + String packageName = extras.getString(AndroidTransport.EXTRA_PACKAGE); + if (!this.packageName.equals(packageName)) + return; + String subscriptionId = extras.getString(AndroidTransport.EXTRA_SUBSCRIPTION_ID); + if (subscriptionId == null) { + Logger.e(this.getClass().getSimpleName(), + String.format("subscription to %s failed: no subscription ID returned", packageName)); + return; + } else if (packageName == null) { + Logger.e(this.getClass().getSimpleName(), "subscription failed: no package name"); + return; + } else if (data == null) { + Logger.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)); + this.subscriptionId = subscriptionId; + return; + } + Logger.d(this.getClass().getSimpleName(), + "subscription to " + packageName + " successful, ID: " + subscriptionId); + this.subscriptionId = subscriptionId; + fetchMessages(context, Uri.parse(data)); + } else if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_SUBSCRIPTION_CHANGE)) { + if (this.getResultCode() != AndroidTransport.RESULT_OK) { + Bundle extras = this.getResultExtras(true); + if (extras != null) + Logger.e(this.getClass().getSimpleName(), + String.format("subscription change for %s failed: %s", + extras.getString(AndroidTransport.EXTRA_SUBSCRIPTION_ID), + AndroidTransport.formatTraffError(this.getResultCode()))); + else + Logger.e(this.getClass().getSimpleName(), + String.format("subscription change failed: %s", + AndroidTransport.formatTraffError(this.getResultCode()))); + return; + } + Bundle extras = intent.getExtras(); + String data = this.getResultData(); + String subscriptionId = extras.getString(AndroidTransport.EXTRA_SUBSCRIPTION_ID); + if (subscriptionId == null) { + Logger.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 (!subscriptionId.equals(this.subscriptionId)) { + return; + } else if (data == null) { + Logger.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; + } + Logger.d(this.getClass().getSimpleName(), + "subscription change for " + subscriptionId + " successful"); + fetchMessages(context, Uri.parse(data)); + } else if (intent.getAction().equals(AndroidTransport.ACTION_TRAFF_UNSUBSCRIBE)) { + String subscriptionId = intent.getStringExtra(AndroidTransport.EXTRA_SUBSCRIPTION_ID); + if (subscriptionId.equals(this.subscriptionId)) + this.subscriptionId = null; + // TODO is there anything to do here? (Comment below is from Navit) + /* + * 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(AndroidTransport.ACTION_TRAFF_HEARTBEAT)) { + String subscriptionId = intent.getStringExtra(AndroidTransport.EXTRA_SUBSCRIPTION_ID); + if (subscriptionId.equals(this.subscriptionId)) { + Logger.d(this.getClass().getSimpleName(), + String.format("got a heartbeat from %s for subscription %s; sending result", + intent.getStringExtra(AndroidTransport.EXTRA_PACKAGE), subscriptionId)); + this.setResult(AndroidTransport.RESULT_OK, null, null); + } + } // intent.getAction() + // TODO Auto-generated method stub + + } + + /** + * Fetches TraFF 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[] {AndroidTransport.COLUMN_DATA}, null, null, null); + if (cursor == null) + return; + if (cursor.getCount() < 1) { + cursor.close(); + return; + } + StringBuilder builder = new StringBuilder("\n"); + while (cursor.moveToNext()) + builder.append(cursor.getString(cursor.getColumnIndex(AndroidTransport.COLUMN_DATA))).append("\n"); + builder.append(""); + cursor.close(); + onFeedReceived(builder.toString()); + } catch (Exception e) { + Logger.w(this.getClass().getSimpleName(), + String.format("Unable to fetch messages from %s", uri.toString()), e); + e.printStackTrace(); + } + } + +} diff --git a/android/app/src/main/java/app/organicmaps/traffxml/Version.java b/android/app/src/main/java/app/organicmaps/traffxml/Version.java new file mode 100644 index 000000000..8f3587ea0 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/traffxml/Version.java @@ -0,0 +1,18 @@ +/* + * Copyright © 2019–2020 traffxml.org. + * + * Relicensed to CoMaps by the original author. + */ + +package app.organicmaps.traffxml; + +/** + * Constants for versions. + */ +public class Version { + /** Version 0.7: introduced transport on Android. */ + public static final int V0_7 = 7; + + /** Version 0.8: introduced subscriptions and HTTP transport. */ + public static final int V0_8 = 8; +}