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