diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 718e423b3..1ed8d7833 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -62,6 +62,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
CallVoidMethod(*onComplete, methodId);
+ jmethodID const runId = jni::GetMethodID(env, *onComplete, "run", "()V");
+ env->CallVoidMethod(*onComplete, runId);
+
+ ASSERT(g_framework, ("g_framework must be non-null"));
+
+ /*
+ * Add traffic sources for Android.
+ */
+ jclass configClass = env->FindClass("app/organicmaps/util/Config");
+ jmethodID const getTrafficLegacyEnabledId = jni::GetStaticMethodID(env, configClass,
+ "getTrafficLegacyEnabled", "()Z");
+ jmethodID const applyTrafficLegacyEnabledId = jni::GetStaticMethodID(env, configClass,
+ "applyTrafficLegacyEnabled", "(Z)V");
+ jmethodID const getTrafficAppsId = jni::GetStaticMethodID(env, configClass,
+ "getTrafficApps", "()[Ljava/lang/String;");
+ jmethodID const applyTrafficAppsId = jni::GetStaticMethodID(env, configClass,
+ "applyTrafficApps", "([Ljava/lang/String;)V");
+
+ env->CallStaticVoidMethod(configClass, applyTrafficLegacyEnabledId,
+ env->CallStaticBooleanMethod(configClass, getTrafficLegacyEnabledId));
+ env->CallStaticVoidMethod(configClass, applyTrafficAppsId,
+ (jobjectArray)env->CallStaticObjectMethod(configClass, getTrafficAppsId));
});
}
}
diff --git a/android/app/src/main/cpp/app/organicmaps/util/Config.cpp b/android/app/src/main/cpp/app/organicmaps/util/Config.cpp
index c934152b6..f0e3a4beb 100644
--- a/android/app/src/main/cpp/app/organicmaps/util/Config.cpp
+++ b/android/app/src/main/cpp/app/organicmaps/util/Config.cpp
@@ -152,4 +152,45 @@ extern "C"
frm()->SaveTrafficHttpUrl(jni::ToNativeString(env, value));
frm()->SetTrafficHttpUrl(jni::ToNativeString(env, value));
}
+
+ JNIEXPORT void JNICALL
+ Java_app_organicmaps_util_Config_applyTrafficLegacyEnabled(JNIEnv * env, jclass thiz,
+ jboolean value)
+ {
+ TrafficManager & tm = g_framework->GetTrafficManager();
+ tm.RemoveTraffSourceIf([](traffxml::TraffSource* source) {
+ if (traffxml::AndroidTraffSourceV0_7* traffSource = dynamic_cast(source))
+ {
+ traffSource->Close();
+ return true;
+ }
+ else
+ return false;
+ });
+ if (value)
+ traffxml::AndroidTraffSourceV0_7::Create(tm);
+ }
+
+ JNIEXPORT void JNICALL
+ Java_app_organicmaps_util_Config_applyTrafficApps(JNIEnv * env, jclass thiz, jobjectArray value)
+ {
+ jsize valueLen = env->GetArrayLength(value);
+ TrafficManager & tm = g_framework->GetTrafficManager();
+ tm.RemoveTraffSourceIf([](traffxml::TraffSource* source) {
+ if (traffxml::AndroidTraffSourceV0_8* traffSource = dynamic_cast(source))
+ {
+ traffSource->Close();
+ return true;
+ }
+ else
+ return false;
+ });
+ for (jsize i = 0; i < valueLen; i++)
+ {
+ jstring jAppId = (jstring)env->GetObjectArrayElement(value, i);
+ std::string appId = jni::ToNativeString(env, jAppId);
+ traffxml::AndroidTraffSourceV0_8::Create(tm, appId);
+ env->DeleteLocalRef(jAppId);
+ }
+ }
} // extern "C"
diff --git a/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java b/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java
index 1d7e3a2b7..3e50ea039 100644
--- a/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java
+++ b/android/app/src/main/java/app/organicmaps/settings/SettingsPrefsFragment.java
@@ -3,8 +3,11 @@ package app.organicmaps.settings;
import static app.organicmaps.leftbutton.LeftButtonsHolder.DISABLE_BUTTON_CODE;
import android.annotation.SuppressLint;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.view.View;
@@ -13,6 +16,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
+import androidx.preference.MultiSelectListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.TwoStatePreference;
@@ -38,6 +42,8 @@ import app.organicmaps.util.ThemeSwitcher;
import app.organicmaps.util.Utils;
import app.organicmaps.util.log.LogsManager;
import app.organicmaps.sdk.search.SearchRecents;
+import app.organicmaps.traffxml.AndroidTransport;
+
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.ArrayList;
@@ -46,6 +52,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements LanguagesFragment.Listener
{
@@ -69,6 +76,8 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
initTransliterationPrefsCallbacks();
initTrafficHttpEnabledPrefsCallbacks();
initTrafficHttpUrlPrefsCallbacks();
+ initTrafficAppsPrefs();
+ initTrafficLegacyEnabledPrefsCallbacks();
init3dModePrefsCallbacks();
initPerspectivePrefsCallbacks();
initAutoZoomPrefsCallbacks();
@@ -154,6 +163,36 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
pref.setSummary(summary);
}
+ private void updateTrafficAppsSummary()
+ {
+ final MultiSelectListPreference pref = getPreference(getString(R.string.pref_traffic_apps));
+ /*
+ * If the preference is disabled, it has not been initialized. This is the case if no TraFF
+ * apps were found. The code below would crash when trying to access the entries, and there
+ * is no need to update the summary if the setting cannot be changed.
+ */
+ if (!pref.isEnabled())
+ return;
+ String[] apps = Config.getTrafficApps();
+ if (apps.length == 0)
+ pref.setSummary(R.string.traffic_apps_none_selected);
+ else
+ {
+ String summary = "";
+ for (int i = 0; i < apps.length; i++)
+ {
+ if (i > 0)
+ summary = summary + ", ";
+ int index = pref.findIndexOfValue(apps[i]);
+ if (i >= 0)
+ summary = summary + pref.getEntries()[index];
+ else
+ summary = summary + apps[i];
+ }
+ pref.setSummary(summary);
+ }
+ }
+
private void updateRoutingSettingsPrefsSummary()
{
final Preference pref = getPreference(getString(R.string.prefs_routing));
@@ -182,6 +221,7 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
updateRoutingSettingsPrefsSummary();
updateMapLanguageCodeSummary();
updateTrafficHttpUrlSummary();
+ updateTrafficAppsSummary();
}
@Override
@@ -271,6 +311,61 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
});
}
+ private void initTrafficAppsPrefs()
+ {
+ final MultiSelectListPreference pref = getPreference(getString(R.string.pref_traffic_apps));
+
+ PackageManager pm = getContext().getPackageManager();
+ List receivers = pm.queryBroadcastReceivers(new Intent(AndroidTransport.ACTION_TRAFF_GET_CAPABILITIES), 0);
+
+ if (receivers == null || receivers.isEmpty())
+ {
+ pref.setSummary(R.string.traffic_apps_not_available);
+ pref.setEnabled(false);
+ return;
+ }
+
+ pref.setEnabled(true);
+
+ List entryList = new ArrayList<>(receivers.size());
+ List valueList = new ArrayList<>(receivers.size());
+
+ for (ResolveInfo receiver : receivers)
+ {
+ // friendly name
+ entryList.add(receiver.loadLabel(pm).toString());
+ // actual value (we just need the package name, broadcasts are sent to any receiver in the package)
+ valueList.add(receiver.activityInfo.applicationInfo.packageName);
+ }
+
+ pref.setEntries(entryList.toArray(new CharSequence[0]));
+ pref.setEntryValues(valueList.toArray(new CharSequence[0]));
+
+ pref.setOnPreferenceChangeListener((preference, newValue) -> {
+ // newValue is a Set, each item is a package ID
+ String[] apps = ((Set)newValue).toArray(new String[0]);
+ Config.setTrafficApps(apps);
+ updateTrafficAppsSummary();
+
+ return true;
+ });
+ }
+
+ private void initTrafficLegacyEnabledPrefsCallbacks()
+ {
+ final Preference pref = getPreference(getString(R.string.pref_traffic_legacy_enabled));
+
+ ((TwoStatePreference)pref).setChecked(Config.getTrafficLegacyEnabled());
+ pref.setOnPreferenceChangeListener((preference, newValue) -> {
+ final boolean oldVal = Config.getTrafficLegacyEnabled();
+ final boolean newVal = (Boolean) newValue;
+ if (oldVal != newVal)
+ Config.setTrafficLegacyEnabled(newVal);
+
+ return true;
+ });
+ }
+
private void initUseMobileDataPrefsCallbacks()
{
final ListPreference mobilePref = getPreference(getString(R.string.pref_use_mobile_data));
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
index 4729643c8..753875ec1 100644
--- a/android/app/src/main/java/app/organicmaps/traffxml/SourceImplV0_8.java
+++ b/android/app/src/main/java/app/organicmaps/traffxml/SourceImplV0_8.java
@@ -54,6 +54,12 @@ public class SourceImplV0_8 extends SourceImpl
}
context.registerReceiver(this, filter);
+
+ Bundle extras = new Bundle();
+ extras.putString(AndroidTransport.EXTRA_PACKAGE, context.getPackageName());
+ extras.putString(AndroidTransport.EXTRA_FILTER_LIST, filterList);
+ AndroidConsumer.sendTraffIntent(context, AndroidTransport.ACTION_TRAFF_SUBSCRIBE, null,
+ extras, packageName, Manifest.permission.ACCESS_COARSE_LOCATION, this);
}
/**
@@ -114,6 +120,8 @@ public class SourceImplV0_8 extends SourceImpl
else
Logger.e(this.getClass().getSimpleName(), String.format("subscription failed, %s",
AndroidTransport.formatTraffError(this.getResultCode())));
+ if (this.getResultCode() == AndroidTransport.RESULT_INTERNAL_ERROR)
+ Logger.e(this.getClass().getSimpleName(), "Make sure the TraFF source app has at least coarse location permission, even when running in background");
return;
}
Bundle extras = this.getResultExtras(true);
diff --git a/android/app/src/main/java/app/organicmaps/util/Config.java b/android/app/src/main/java/app/organicmaps/util/Config.java
index 418ebb7b2..06fcb6c85 100644
--- a/android/app/src/main/java/app/organicmaps/util/Config.java
+++ b/android/app/src/main/java/app/organicmaps/util/Config.java
@@ -55,6 +55,16 @@ public final class Config
* True if the first start animation has been seen.
*/
private static final String KEY_MISC_FIRST_START_DIALOG_SEEN = "FirstStartDialogSeen";
+
+ /**
+ * Whether feeds from legacy TraFF applications (TraFF 0.7, Android transport) are enabled.
+ */
+ private static final String KEY_TRAFFIC_LEGACY_ENABLED = "TrafficLegacyEnabled";
+
+ /**
+ * TraFF (0.8+) applications from which to request traffic data.
+ */
+ private static final String KEY_TRAFFIC_APPS = "TrafficApps";
private Config() {}
@@ -338,6 +348,38 @@ public final class Config
{
nativeSetTrafficHttpUrl(value);
}
+
+ public static String[] getTrafficApps()
+ {
+ String appString = getString(KEY_TRAFFIC_APPS, "");
+ if (appString.length() == 0)
+ return new String[0];
+ return appString.split(",");
+ }
+
+ public static void setTrafficApps(String[] value)
+ {
+ String valueString = "";
+ for (int i = 0; i < value.length; i++)
+ {
+ valueString = valueString + value[i];
+ if ((i + 1) < value.length)
+ valueString = valueString + ",";
+ }
+ setString(KEY_TRAFFIC_APPS, valueString);
+ applyTrafficApps(value);
+ }
+
+ public static boolean getTrafficLegacyEnabled()
+ {
+ return getBool(KEY_TRAFFIC_LEGACY_ENABLED, false);
+ }
+
+ public static void setTrafficLegacyEnabled(boolean value)
+ {
+ setBool(KEY_TRAFFIC_LEGACY_ENABLED, value);
+ applyTrafficLegacyEnabled(value);
+ }
public static boolean isNY()
{
@@ -496,4 +538,6 @@ public final class Config
private static native void nativeSetTrafficHttpEnabled(boolean value);
private static native String nativeGetTrafficHttpUrl();
private static native void nativeSetTrafficHttpUrl(String value);
+ private static native void applyTrafficApps(String[] value);
+ private static native void applyTrafficLegacyEnabled(boolean value);
}
diff --git a/android/app/src/main/res/values/donottranslate.xml b/android/app/src/main/res/values/donottranslate.xml
index c6294b18f..046304210 100644
--- a/android/app/src/main/res/values/donottranslate.xml
+++ b/android/app/src/main/res/values/donottranslate.xml
@@ -35,8 +35,11 @@
GeneralSettings
Navigation
Information
+ Traffic
TrafficHttpEnabled
TrafficHttpUrl
+ TrafficApps
+ TrafficLegacyEnabled
Transliteration
PowerManagment
KeepScreenOn
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index f26902f99..e28573b37 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -234,6 +234,7 @@
Information
Navigation
+ Traffic information
Zoom buttons
Display on the map
@@ -838,6 +839,16 @@
Traffic service URL
Not set
+
+ Use data from TraFF applications
+
+ No apps installed
+
+ No apps salected
+
+ Use data from legacy TraFF applications
+
+ When enabled, the app will receive and process traffic data from legacy TraFF applications.
Map data from OpenStreetMap
diff --git a/android/app/src/main/res/xml/prefs_main.xml b/android/app/src/main/res/xml/prefs_main.xml
index 625a86e74..421f7a4a1 100644
--- a/android/app/src/main/res/xml/prefs_main.xml
+++ b/android/app/src/main/res/xml/prefs_main.xml
@@ -119,18 +119,6 @@
app:singleLineTitle="false"
android:persistent="false"
android:order="19"/>
-
-
+
+
+
+
+
+
+
&pred)
+{
+ std::lock_guard lock(m_trafficSourceMutex);
+
+ for (auto it = m_trafficSources.begin(); it != m_trafficSources.end(); )
+ if (pred(it->get()))
+ m_trafficSources.erase(it);
+ else
+ ++it;
+}
+
bool TrafficManager::IsInvalidState() const
{
return m_state == TrafficState::NetworkError;
diff --git a/map/traffic_manager.hpp b/map/traffic_manager.hpp
index 7b4fbe08e..6e14bb4c7 100644
--- a/map/traffic_manager.hpp
+++ b/map/traffic_manager.hpp
@@ -190,6 +190,22 @@ public:
*/
void SetHttpTraffSource(bool enabled, std::string url);
+ /**
+ * @brief Removes all `TraffSource` instances which satisfy a predicate.
+ *
+ * This method iterates over all currently configured `TraffSource` instances and calls the
+ * caller-suppplied predicate function `pred` on each of them. If `pred` returns true, the source
+ * is removed, else it is kept.
+ *
+ * @todo For now, `pred` deliberately takes a non-const argument so we can do cleanup inside
+ * `pred`. If we manage to move any such cleanup into the destructor of the `TraffSource` subclass
+ * and get rid of any `Close()` methods in subclasses (which is preferable for other reasons as
+ * well), the argument can be made const.
+ *
+ * @param pred The predicate function, see description.
+ */
+ void RemoveTraffSourceIf(const std::function& pred);
+
/**
* @brief Starts the traffic manager.
*