diff --git a/android/app/src/main/java/app/organicmaps/MwmActivity.java b/android/app/src/main/java/app/organicmaps/MwmActivity.java index 0ba3f0077..5c6f6b80d 100644 --- a/android/app/src/main/java/app/organicmaps/MwmActivity.java +++ b/android/app/src/main/java/app/organicmaps/MwmActivity.java @@ -107,6 +107,7 @@ import app.organicmaps.sdk.routing.RoutingOptions; import app.organicmaps.sdk.search.SearchEngine; import app.organicmaps.sdk.settings.RoadType; import app.organicmaps.sdk.settings.UnitLocale; +import app.organicmaps.sdk.sound.TtsPlayer; import app.organicmaps.sdk.util.Config; import app.organicmaps.sdk.util.LocationUtils; import app.organicmaps.sdk.util.PowerManagment; @@ -132,7 +133,6 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.textview.MaterialTextView; - import java.util.ArrayList; import java.util.Objects; @@ -1813,6 +1813,18 @@ public class MwmActivity extends BaseMwmFragmentActivity return false; } + private void deliverTtsMessage() + { + if (Config.isTtsMessageDelivered()) + return; + + String navigationStartMessage = getResources().getString(R.string.navigation_start_tts_message); + navigationStartMessage += TtsPlayer.INSTANCE.getLanguageDisplayName(); + Toast.makeText(this, navigationStartMessage, Toast.LENGTH_LONG).show(); + + Config.setTtsMessageDelivered(); + } + private boolean showStartPointNotice() { final RoutingController controller = RoutingController.get(); @@ -2189,6 +2201,8 @@ public class MwmActivity extends BaseMwmFragmentActivity if (!showRoutingDisclaimer()) return; + deliverTtsMessage(); + closeFloatingPanels(); setFullscreen(false); RoutingController.get().start(); diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 2273c8658..f91f649fb 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -932,6 +932,7 @@ Share Track Delete %s? No text-to-speech engine found, check the app settings + "Starting Navigation, voice instruction language: " unknown Type 2 (no cable) Type 2 (w/ cable) diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/sound/TtsPlayer.java b/android/sdk/src/main/java/app/organicmaps/sdk/sound/TtsPlayer.java index 2c6a12716..d9903bf3d 100644 --- a/android/sdk/src/main/java/app/organicmaps/sdk/sound/TtsPlayer.java +++ b/android/sdk/src/main/java/app/organicmaps/sdk/sound/TtsPlayer.java @@ -1,6 +1,7 @@ package app.organicmaps.sdk.sound; import android.content.Context; +import android.content.res.Configuration; import android.database.ContentObserver; import android.media.AudioManager; import android.os.Bundle; @@ -14,6 +15,8 @@ import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import androidx.core.os.ConfigurationCompat; +import androidx.core.os.LocaleListCompat; import androidx.media.AudioAttributesCompat; import androidx.media.AudioFocusRequestCompat; import androidx.media.AudioManagerCompat; @@ -23,6 +26,7 @@ import app.organicmaps.sdk.util.log.Logger; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Set; /** * {@code TtsPlayer} class manages available TTS voice languages. @@ -33,9 +37,9 @@ import java.util.Locale; * unsupported voices are excluded. *

* At startup we check whether currently selected language is in our list of supported voices and its data is - * downloaded. If not, we check system default locale. If failed, the same check is made for English language. Finally, - * if mentioned checks fail we manually disable TTS, so the user must go to the settings and select preferred voice - * language by hand.

If no core supported languages can be used by the system, TTS is locked down and can not be + * downloaded. If not, we check system default locale. If failed, the same check is made for other system locales. + * If those fail too, we check for English language. Then, as a final resort, all installed TTS locales are checked. + *

If no core supported languages can be used by the system, TTS is locked down and can not be * enabled and used. */ public enum TtsPlayer @@ -78,6 +82,8 @@ public enum TtsPlayer // TTS is locked down due to absence of supported languages private boolean mUnavailable; + private LocaleListCompat mInstalledSystemLocales; + TtsPlayer() {} private static @Nullable LanguageData findSupportedLanguage(String internalCode, List langs) @@ -126,28 +132,57 @@ public enum TtsPlayer return (lang != null && setLanguageInternal(lang)); } - private static @Nullable LanguageData getDefaultLanguage(List langs) + public static @Nullable LanguageData getSelectedLanguage(List langs) + { + return findSupportedLanguage(Config.TTS.getLanguage(), langs); + } + + private @Nullable LanguageData getSystemLanguage(List langs) { LanguageData res; + // Try default system locale Locale defLocale = Locale.getDefault(); - if (defLocale != null) + res = findSupportedLanguage(defLocale, langs); + if (res != null && res.downloaded) + return res; + + // Try other installed system locales + for (int i = 0; i < mInstalledSystemLocales.size(); i++) { - res = findSupportedLanguage(defLocale, langs); + Locale loc = mInstalledSystemLocales.get(i); + res = findSupportedLanguage(loc, langs); + if (res != null && res.downloaded) + return res; + } + return null; + } + + private @Nullable LanguageData getTTSLanguage(List langs) + { + LanguageData res; + + // Try all TTS installed languages + Set ttsLocales = mTts.getAvailableLanguages(); + for (Locale loc : ttsLocales) + { + res = findSupportedLanguage(loc, langs); if (res != null && res.downloaded) return res; } - res = findSupportedLanguage(DEFAULT_LOCALE, langs); - if (res != null && res.downloaded) - return res; - return null; } - public static @Nullable LanguageData getSelectedLanguage(List langs) + private static @Nullable LanguageData getDefaultLanguage(List langs) { - return findSupportedLanguage(Config.TTS.getLanguage(), langs); + LanguageData res; + + // Try default app locale (en.US) + res = findSupportedLanguage(DEFAULT_LOCALE, langs); + if (res != null && res.downloaded) + return res; + return null; } private void lockDown() @@ -167,6 +202,10 @@ public enum TtsPlayer // TextToSpeech.OnInitListener() can be called from a non-main thread // on LineageOS '20.0-20231127-RELEASE-thyme' 'Xiaomi/thyme/thyme'. // https://github.com/organicmaps/organicmaps/issues/6903 + + Configuration config = context.getResources().getConfiguration(); + mInstalledSystemLocales = ConfigurationCompat.getLocales(config); + mTts = new TextToSpeech(context, status -> UiThread.run(() -> { if (status == TextToSpeech.ERROR) { @@ -239,6 +278,17 @@ public enum TtsPlayer return (INSTANCE.mTts != null && !INSTANCE.mUnavailable && !INSTANCE.mInitializing); } + public Locale getVoiceLocale() + { + return mTts.getVoice().getLocale(); + } + + public String getLanguageDisplayName() + { + Locale locale = getVoiceLocale(); + return locale.getDisplayName(locale); + } + public void speak(String textToSpeak) { if (Config.TTS.isEnabled()) @@ -328,24 +378,49 @@ public enum TtsPlayer if (outList.isEmpty()) { - // No supported languages found, lock down TTS :( + Logger.d("TtsPlayer", "No supported languages found, lock down TTS :( "); lockDown(); return null; } LanguageData res = getSelectedLanguage(outList); - if (res == null || !res.downloaded) - // Selected locale is not available or not downloaded - res = getDefaultLanguage(outList); - - if (res == null || !res.downloaded) + if (res != null && res.downloaded) { - // Default locale can not be used too - Config.TTS.setEnabled(false); - return null; + Logger.d("TtsPlayer", "Selected locale " + res.internalCode + " will be used for TTS"); + return res; + } + Logger.d("TtsPlayer", "Selected locale " + Config.TTS.getLanguage() + + " is not available or not downloaded, trying system locales..."); + + res = getSystemLanguage(outList); + if (res != null && res.downloaded) + { + Logger.d("TtsPlayer", "System locale " + res.internalCode + " will be used for TTS"); + return res; + } + Logger.d("TtsPlayer", + "None of the system locales are available, or they are not downloaded, trying default locale..."); + + res = getDefaultLanguage(outList); + if (res != null && res.downloaded) + { + Logger.d("TtsPlayer", "Default locale " + res.internalCode + " will be used for TTS"); + return res; + } + Logger.d("TtsPlayer", + "Default locale " + DEFAULT_LOCALE + " can not be used either, trying all installed TTS locales..."); + + res = getTTSLanguage(outList); + if (res != null && res.downloaded) + { + Logger.d("TtsPlayer", "TTS locale " + res.internalCode + " will be used for TTS"); + return res; } - return res; + Logger.d("TtsPlayer", + "None of the TTS engine locales are available, or they are not downloaded, disabling TTS :( "); + Config.TTS.setEnabled(false); + return null; } public @NonNull List refreshLanguages() diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/util/Config.java b/android/sdk/src/main/java/app/organicmaps/sdk/util/Config.java index e540ee577..74bfe8d7d 100644 --- a/android/sdk/src/main/java/app/organicmaps/sdk/util/Config.java +++ b/android/sdk/src/main/java/app/organicmaps/sdk/util/Config.java @@ -36,6 +36,7 @@ public final class Config private static final String KEY_PREF_USE_GS = "UseGoogleServices"; private static final String KEY_MISC_DISCLAIMER_ACCEPTED = "IsDisclaimerApproved"; + private static final String KEY_MISC_TTS_MESSAGE_DELIVERED = "TtsMessageDelivered"; private static final String KEY_MISC_LOCATION_REQUESTED = "LocationRequested"; private static final String KEY_MISC_USE_MOBILE_DATA = "UseMobileData"; private static final String KEY_MISC_USE_MOBILE_DATA_TIMESTAMP = "UseMobileDataTimestamp"; @@ -237,6 +238,16 @@ public final class Config setBool(KEY_MISC_DISCLAIMER_ACCEPTED); } + public static boolean isTtsMessageDelivered() + { + return getBool(KEY_MISC_TTS_MESSAGE_DELIVERED); + } + + public static void setTtsMessageDelivered() + { + setBool(KEY_MISC_TTS_MESSAGE_DELIVERED); + } + public static boolean isLocationRequested() { return getBool(KEY_MISC_LOCATION_REQUESTED);