diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 7f012cb66f..d397d503c7 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -171,8 +171,7 @@ android:name="com.mapswithme.maps.MwmActivity" android:launchMode="singleTask" android:theme="@style/MwmTheme.MainActivity" - android:windowSoftInputMode="stateAlwaysHidden|adjustPan"> - + android:windowSoftInputMode="stateAlwaysHidden|adjustPan"/> EnableTurnNotifications(enable == JNI_TRUE ? true : false); + return frm()->EnableTurnNotifications(static_cast(enable)); } JNIEXPORT jboolean JNICALL Java_com_mapswithme_maps_sound_TtsPlayer_nativeAreTurnNotificationsEnabled(JNIEnv * env, jclass clazz) { - return frm()->AreTurnNotificationsEnabled() ? JNI_TRUE : JNI_FALSE; + return static_cast(frm()->AreTurnNotificationsEnabled()); } JNIEXPORT void JNICALL diff --git a/android/res/values-ru/strings.xml b/android/res/values-ru/strings.xml index 34e0b107b1..210c47c15e 100644 --- a/android/res/values-ru/strings.xml +++ b/android/res/values-ru/strings.xml @@ -327,6 +327,14 @@ Классическая тёмная Современная светлая + + Голосовые инструкции + + Подсказки о поворотах и прочих манёврах + + Язык подсказок + + Голосовые подсказки недоступны Расстояние Координаты Без категории diff --git a/android/res/values/donottranslate.xml b/android/res/values/donottranslate.xml index 6a24024b4f..397009c077 100644 --- a/android/res/values/donottranslate.xml +++ b/android/res/values/donottranslate.xml @@ -38,6 +38,8 @@ Settings MapsMePrefs MapStyle + TtsEnabled + TtsLanguage %1$s: %2$s %2$s :%1$s diff --git a/android/res/values/strings-tts.xml b/android/res/values/strings-tts.xml new file mode 100644 index 0000000000..39306a3bc3 --- /dev/null +++ b/android/res/values/strings-tts.xml @@ -0,0 +1,80 @@ + + + + + en + ru + pl + sv + tr + fr + nl + de + ar + el + it + cs + hu + ro + ja + da + es + fi + hi + hr + id + ko + pt + sk + sw + th + zh-TW:zh-Hant + zh-CN:zh-Hans + + + + English + Русский + Polski + Svenska + Türkçe + Français + Nederlands + Deutsch + العربية + Ελληνικά + Italiano + Čeština + Magyar + Română + 日本語 + Dansk + Español + Suomi + हिंदी + Hrvatski + Indonesia + 한국어 + Português + Slovenčina + Kiswahili + ภาษาไทย + 中文繁體 + 中文简体 + + diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml index f06619337e..df42fc3646 100644 --- a/android/res/values/strings.xml +++ b/android/res/values/strings.xml @@ -329,6 +329,14 @@ Classic dark Modern light + + Voice instructions + + Turn instructions + + Voice language + + TTS unavailable Distance Coordinates Unsorted diff --git a/android/res/xml/prefs_headers.xml b/android/res/xml/prefs_headers.xml index bbb02672c6..013d71a81f 100644 --- a/android/res/xml/prefs_headers.xml +++ b/android/res/xml/prefs_headers.xml @@ -4,6 +4,10 @@ android:title="@string/prefs_group_map" android:fragment="com.mapswithme.maps.settings.MapPrefsFragment"/> +
+
diff --git a/android/res/xml/prefs_route.xml b/android/res/xml/prefs_route.xml new file mode 100644 index 0000000000..5752c6acba --- /dev/null +++ b/android/res/xml/prefs_route.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/android/src/com/mapswithme/maps/MwmActivity.java b/android/src/com/mapswithme/maps/MwmActivity.java index 53b9cbeebd..1e886890bc 100644 --- a/android/src/com/mapswithme/maps/MwmActivity.java +++ b/android/src/com/mapswithme/maps/MwmActivity.java @@ -767,14 +767,13 @@ public class MwmActivity extends BaseMwmFragmentActivity { super.onStart(); - TtsPlayer.INSTANCE.reinitIfLocaleChanged(); if (!mIsFragmentContainer) popFragment(); } private void adjustZoomButtons(boolean routingActive) { - boolean show = (routingActive || Config.getShowZoomButtons()); + boolean show = (routingActive || Config.showZoomButtons()); UiUtils.showIf(show, mBtnZoomIn, mBtnZoomOut); if (!show) diff --git a/android/src/com/mapswithme/maps/MwmApplication.java b/android/src/com/mapswithme/maps/MwmApplication.java index 52dfc07e8a..7287cc9d11 100644 --- a/android/src/com/mapswithme/maps/MwmApplication.java +++ b/android/src/com/mapswithme/maps/MwmApplication.java @@ -1,16 +1,17 @@ package com.mapswithme.maps; +import android.app.Application; import android.content.SharedPreferences; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Environment; import android.preference.PreferenceManager; import android.util.Log; - import com.google.gson.Gson; import com.mapswithme.country.ActiveCountryTree; import com.mapswithme.country.CountryItem; import com.mapswithme.maps.background.Notifier; import com.mapswithme.maps.bookmarks.data.BookmarkManager; +import com.mapswithme.maps.sound.TtsPlayer; import com.mapswithme.util.Config; import com.mapswithme.util.Constants; import com.mapswithme.util.UiUtils; @@ -24,7 +25,8 @@ import com.parse.SaveCallback; import java.io.File; -public class MwmApplication extends android.app.Application implements ActiveCountryTree.ActiveCountryListener +public class MwmApplication extends Application + implements ActiveCountryTree.ActiveCountryListener { private final static String TAG = "MwmApplication"; @@ -32,32 +34,32 @@ public class MwmApplication extends android.app.Application implements ActiveCou private static final String PREF_PARSE_DEVICE_TOKEN = "ParseDeviceToken"; private static final String PREF_PARSE_INSTALLATION_ID = "ParseInstallationId"; - private static MwmApplication mSelf; + private static MwmApplication sSelf; + private static SharedPreferences sPrefs; private final Gson mGson = new Gson(); - private static SharedPreferences mPrefs; - private boolean mAreCountersInitialised; + private boolean mAreCountersInitialized; private boolean mIsFrameworkInitialized; public MwmApplication() { super(); - mSelf = this; + sSelf = this; } public static MwmApplication get() { - return mSelf; + return sSelf; } public static Gson gson() { - return mSelf.mGson; + return sSelf.mGson; } public static SharedPreferences prefs() { - return mPrefs; + return sPrefs; } @Override @@ -92,10 +94,10 @@ public class MwmApplication extends android.app.Application implements ActiveCou super.onCreate(); initParse(); - mPrefs = getSharedPreferences(getString(R.string.pref_file_name), MODE_PRIVATE); + sPrefs = getSharedPreferences(getString(R.string.pref_file_name), MODE_PRIVATE); } - public synchronized void initNativeCore() + public void initNativeCore() { if (mIsFrameworkInitialized) return; @@ -107,6 +109,7 @@ public class MwmApplication extends android.app.Application implements ActiveCou ActiveCountryTree.addListener(this); initNativeStrings(); BookmarkManager.getIcons(); // init BookmarkManager (automatically loads bookmarks) + TtsPlayer.INSTANCE.init(this); mIsFrameworkInitialized = true; } @@ -226,9 +229,9 @@ public class MwmApplication extends android.app.Application implements ActiveCou public void initCounters() { - if (!mAreCountersInitialised) + if (!mAreCountersInitialized) { - mAreCountersInitialised = true; + mAreCountersInitialized = true; Config.updateLaunchCounter(); PreferenceManager.setDefaultValues(this, R.xml.prefs_misc, false); } diff --git a/android/src/com/mapswithme/maps/search/SearchFragment.java b/android/src/com/mapswithme/maps/search/SearchFragment.java index 822128c07d..b6b68a3c3f 100644 --- a/android/src/com/mapswithme/maps/search/SearchFragment.java +++ b/android/src/com/mapswithme/maps/search/SearchFragment.java @@ -15,7 +15,6 @@ import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import com.mapswithme.country.ActiveCountryTree; import com.mapswithme.country.CountrySuggestFragment; import com.mapswithme.maps.Framework; @@ -24,7 +23,6 @@ import com.mapswithme.maps.R; import com.mapswithme.maps.base.BaseMwmFragment; import com.mapswithme.maps.base.OnBackPressListener; import com.mapswithme.maps.location.LocationHelper; -import com.mapswithme.maps.sound.TtsPlayer; import com.mapswithme.maps.widget.SearchToolbarController; import com.mapswithme.util.InputUtils; import com.mapswithme.util.Language; @@ -82,7 +80,7 @@ public class SearchFragment extends BaseMwmFragment } // TODO: This code only for demonstration purposes and will be removed soon - if (trySwitchOnTurnSound(query) || tryChangeMapStyle(query)) + if (tryChangeMapStyle(query)) return; runSearch(); @@ -333,20 +331,6 @@ public class SearchFragment extends BaseMwmFragment return true; } - - private boolean trySwitchOnTurnSound(String query) - { - final boolean sound = "?sound".equals(query); - final boolean nosound = "?nosound".equals(query); - - if (!sound && !nosound) - return false; - - hideSearch(); - TtsPlayer.INSTANCE.enable(sound); - - return sound; - } // FIXME END protected void showSingleResultOnMap(int resultIndex) diff --git a/android/src/com/mapswithme/maps/settings/MapPrefsFragment.java b/android/src/com/mapswithme/maps/settings/MapPrefsFragment.java index f2211f8bd3..83d7dc7682 100644 --- a/android/src/com/mapswithme/maps/settings/MapPrefsFragment.java +++ b/android/src/com/mapswithme/maps/settings/MapPrefsFragment.java @@ -87,7 +87,7 @@ public class MapPrefsFragment extends BaseXmlSettingsFragment }); pref = findPreference(getString(R.string.pref_show_zoom_buttons)); - ((SwitchPreference)pref).setChecked(Config.getShowZoomButtons()); + ((SwitchPreference)pref).setChecked(Config.showZoomButtons()); pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override diff --git a/android/src/com/mapswithme/maps/settings/RoutePrefsFragment.java b/android/src/com/mapswithme/maps/settings/RoutePrefsFragment.java new file mode 100644 index 0000000000..8c02fb4d86 --- /dev/null +++ b/android/src/com/mapswithme/maps/settings/RoutePrefsFragment.java @@ -0,0 +1,166 @@ +package com.mapswithme.maps.settings; + +import android.content.Intent; +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.SwitchPreference; +import android.speech.tts.TextToSpeech; +import android.support.annotation.NonNull; +import com.mapswithme.maps.R; +import com.mapswithme.maps.sound.LanguageData; +import com.mapswithme.maps.sound.TtsPlayer; +import com.mapswithme.util.Config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RoutePrefsFragment extends PreferenceFragment +{ + private static final int REQUEST_INSTALL_DATA = 1; + + private SwitchPreference mPrefEnabled; + private ListPreference mPrefLanguages; + + private final Map mLanguages = new HashMap<>(); + private LanguageData mCurrentLanguage; + private String mSelectedLanguage; + + private final Preference.OnPreferenceChangeListener mEnabledListener = new Preference.OnPreferenceChangeListener() + { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) + { + boolean set = (Boolean)newValue; + if (!set) + { + TtsPlayer.setEnabled(false); + mPrefLanguages.setEnabled(false); + return true; + } + + if (mCurrentLanguage != null && mCurrentLanguage.downloaded) + { + setLanguage(mCurrentLanguage); + return true; + } + + mPrefLanguages.setEnabled(true); + getPreferenceScreen().onItemClick(null, null, mPrefLanguages.getOrder(), 0); + mPrefLanguages.setEnabled(false); + return false; + } + }; + + private final Preference.OnPreferenceChangeListener mLangListener = new Preference.OnPreferenceChangeListener() + { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) + { + if (newValue == null) + return false; + + mSelectedLanguage = (String)newValue; + LanguageData lang = mLanguages.get(mSelectedLanguage); + if (lang == null) + return false; + + if (lang.downloaded) + setLanguage(lang); + else + startActivityForResult(new Intent(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA), REQUEST_INSTALL_DATA); + + return false; + } + }; + + private void enableListeners(boolean enable) + { + mPrefEnabled.setOnPreferenceChangeListener(enable ? mEnabledListener : null); + mPrefLanguages.setOnPreferenceChangeListener(enable ? mLangListener : null); + } + + private void setLanguage(@NonNull LanguageData lang) + { + Config.setTtsEnabled(true); + TtsPlayer.INSTANCE.setLanguage(lang); + mPrefLanguages.setSummary(lang.name); + + update(); + } + + private void update() + { + enableListeners(false); + + List languages = TtsPlayer.INSTANCE.refreshLanguages(); + mLanguages.clear(); + mCurrentLanguage = null; + + if (languages.isEmpty()) + { + mPrefEnabled.setChecked(false); + mPrefEnabled.setEnabled(false); + mPrefEnabled.setSummary(R.string.pref_tts_unavailable); + mPrefLanguages.setEnabled(false); + mPrefLanguages.setSummary(null); + + enableListeners(true); + return; + } + + mPrefEnabled.setChecked(TtsPlayer.INSTANCE.isEnabled()); + + final CharSequence[] entries = new CharSequence[languages.size()]; + final CharSequence[] values = new CharSequence[languages.size()]; + for (int i = 0; i < languages.size(); i++) + { + LanguageData lang = languages.get(i); + entries[i] = lang.name; + values[i] = lang.internalCode; + + mLanguages.put(lang.internalCode, lang); + } + + mPrefLanguages.setEntries(entries); + mPrefLanguages.setEntryValues(values); + + mCurrentLanguage = TtsPlayer.getSelectedLanguage(languages); + boolean available = (mCurrentLanguage != null && mCurrentLanguage.downloaded); + mPrefLanguages.setEnabled(available && TtsPlayer.INSTANCE.isEnabled()); + mPrefLanguages.setSummary(available ? mCurrentLanguage.name : null); + mPrefLanguages.setValue(available ? mCurrentLanguage.internalCode : null); + mPrefEnabled.setChecked(available && TtsPlayer.INSTANCE.isEnabled()); + + enableListeners(true); + } + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.prefs_route); + + mPrefEnabled = (SwitchPreference) findPreference(getString(R.string.pref_tts_enabled)); + mPrefLanguages = (ListPreference) findPreference(getString(R.string.pref_tts_language)); + update(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) + { + // Do not check resultCode here as it is always RESULT_CANCELED + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_INSTALL_DATA) + { + update(); + + LanguageData lang = mLanguages.get(mSelectedLanguage); + if (lang != null && lang.downloaded) + setLanguage(lang); + } + } +} diff --git a/android/src/com/mapswithme/maps/settings/SettingsActivity.java b/android/src/com/mapswithme/maps/settings/SettingsActivity.java index 0972e0ccce..6c08f10d44 100644 --- a/android/src/com/mapswithme/maps/settings/SettingsActivity.java +++ b/android/src/com/mapswithme/maps/settings/SettingsActivity.java @@ -62,6 +62,8 @@ public class SettingsActivity extends PreferenceActivity { if (header.id == R.id.group_map) Statistics.INSTANCE.trackSimpleNamedEvent(Statistics.EventName.Settings.GROUP_MAP); + else if (header.id == R.id.group_route) + Statistics.INSTANCE.trackSimpleNamedEvent(Statistics.EventName.Settings.GROUP_ROUTE); else if (header.id == R.id.group_misc) Statistics.INSTANCE.trackSimpleNamedEvent(Statistics.EventName.Settings.GROUP_MISC); else if (header.id == R.id.help) diff --git a/android/src/com/mapswithme/maps/sound/LanguageData.java b/android/src/com/mapswithme/maps/sound/LanguageData.java new file mode 100644 index 0000000000..20baddbc82 --- /dev/null +++ b/android/src/com/mapswithme/maps/sound/LanguageData.java @@ -0,0 +1,74 @@ +package com.mapswithme.maps.sound; + +import android.speech.tts.TextToSpeech; + +import java.util.Locale; + +/** + * {@code LanguageData} describes single voice language managed by {@link TtsPlayer}. + * Supported languages are listed in {@code strings-tts.xml} file, for details see comments there. + */ +public class LanguageData +{ + public static class NotAvailableException extends Exception { + public NotAvailableException(Locale locale) + { + super("Locale \"" + locale + "\" is not supported by current TTS engine"); + } + } + + public final Locale locale; + public final String name; + public final String internalCode; + public final boolean downloaded; + + public LanguageData(String line, String name, TextToSpeech tts) throws NotAvailableException + { + this.name = name; + + String[] parts = line.split(":"); + String code = (parts.length > 1 ? parts[1] : null); + + parts = parts[0].split("-"); + String language = parts[0]; + internalCode = (code == null ? language : code); + + String country = (parts.length > 1 ? parts[1] : ""); + locale = new Locale(language, country); + + int status = tts.isLanguageAvailable(locale); + if (status < TextToSpeech.LANG_MISSING_DATA) + throw new NotAvailableException(locale); + + downloaded = (status >= TextToSpeech.LANG_AVAILABLE); + } + + public boolean matchesLocale(Locale locale) + { + String lang = locale.getLanguage(); + if (!lang.equals(this.locale.getLanguage())) + return false; + + if ("zh".equals(lang) && "zh-Hant".equals(internalCode)) + { + // Chinese is a special case + String country = locale.getCountry(); + return "TW".equals(country) || + "MO".equals(country) || + "HK".equals(country); + } + + return true; + } + + public boolean matchesInternalCode(String internalCode) + { + return this.internalCode.equals(internalCode); + } + + @Override + public String toString() + { + return name + ": " + locale + ", internal: " + internalCode + (downloaded ? " - " : " - NOT ") + "downloaded"; + } +} diff --git a/android/src/com/mapswithme/maps/sound/TtsPlayer.java b/android/src/com/mapswithme/maps/sound/TtsPlayer.java index e9d1fceb1d..a2db35acb6 100644 --- a/android/src/com/mapswithme/maps/sound/TtsPlayer.java +++ b/android/src/com/mapswithme/maps/sound/TtsPlayer.java @@ -1,17 +1,36 @@ package com.mapswithme.maps.sound; +import android.content.Context; +import android.content.res.Resources; import android.speech.tts.TextToSpeech; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; -import android.widget.Toast; - import com.mapswithme.maps.Framework; import com.mapswithme.maps.MwmApplication; -import com.mapswithme.util.Language; +import com.mapswithme.maps.R; +import com.mapswithme.util.Config; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; - +/** + * {@code TtsPlayer} class manages available TTS voice languages. + * Single TTS language is described by {@link LanguageData} item. + *

+ * We support a set of languages listed in {@code strings-tts.xml} file. + * During loading each item in this list is marked as {@code downloaded} or {@code not downloaded}, + * 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 enabled and used. + */ public enum TtsPlayer { INSTANCE; @@ -19,160 +38,203 @@ public enum TtsPlayer private static final Locale DEFAULT_LOCALE = Locale.US; private static final float SPEECH_RATE = 1.2f; - // The both mTtts and mTtsLocale should be initialized before usage. private TextToSpeech mTts; - private Locale mTtsLocale; - private boolean mIsLocaleChanging; + private boolean mInitializing; - private final static String TAG = "TtsPlayer"; + // TTS is locked down due to absence of supported languages + private boolean mUnavailable; TtsPlayer() {} - public void reinitIfLocaleChanged() + private static @Nullable LanguageData findSupportedLanguage(String internalCode, List langs) { - if (!isValid()) - return; // TtsPlayer was not inited yet. + if (TextUtils.isEmpty(internalCode)) + return null; - final Locale locale = getDefaultLocale(); - if (!isLocaleEqual(locale)) - initTts(locale); + for (LanguageData lang : langs) + if (lang.matchesInternalCode(internalCode)) + return lang; + + return null; } - private Locale getDefaultLocale() + private static @Nullable LanguageData findSupportedLanguage(Locale locale, List langs) { - final Locale locale = Locale.getDefault(); - return locale == null ? DEFAULT_LOCALE : locale; + if (locale == null) + return null; + + for (LanguageData lang : langs) + if (lang.matchesLocale(locale)) + return lang; + + return null; } - private boolean isLocaleEqual(Locale locale) + private void setLanguageInternal(LanguageData lang) { - return locale.getLanguage().equals(mTtsLocale.getLanguage()) && - locale.getCountry().equals(mTtsLocale.getCountry()); + mTts.setLanguage(lang.locale); + nativeSetTurnNotificationsLocale(lang.internalCode); + Config.setTtsLanguage(lang.internalCode); } - private boolean isLocaleAvailable(Locale locale) + public void setLanguage(LanguageData lang) { - final int avail = mTts.isLanguageAvailable(locale); - return avail == TextToSpeech.LANG_AVAILABLE || avail == TextToSpeech.LANG_COUNTRY_AVAILABLE || - avail == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE; + if (lang != null) + setLanguageInternal(lang); } - private void initTts(final Locale locale) + private static @Nullable LanguageData getDefaultLanguage(List langs) { - if (mIsLocaleChanging) - return; // Preventing reiniting while creating TextToSpeech object. There's a small possibility a new locale is skipped. + LanguageData res; - if (mTts != null && mTtsLocale != null && mTtsLocale.equals(locale)) - return; - - mTtsLocale = null; - mIsLocaleChanging = true; - - if (mTts != null) + Locale defLocale = Locale.getDefault(); + if (defLocale != null) { - mTts.stop(); - mTts.shutdown(); + res = findSupportedLanguage(defLocale, langs); + if (res != null && res.downloaded) + return res; } - mTts = new TextToSpeech(MwmApplication.get(), new TextToSpeech.OnInitListener() + res = findSupportedLanguage(DEFAULT_LOCALE, langs); + if (res != null && res.downloaded) + return res; + + return null; + } + + public static @Nullable LanguageData getSelectedLanguage(List langs) + { + return findSupportedLanguage(Config.getTtsLanguage(), langs); + } + + private void lockDown() + { + mUnavailable = true; + setEnabled(false); + } + + public void init(Context context) + { + if (mTts != null || mInitializing || mUnavailable) + return; + + mInitializing = true; + mTts = new TextToSpeech(context, new TextToSpeech.OnInitListener() { @Override public void onInit(int status) { - // This method is called asynchronously. - mIsLocaleChanging = false; if (status == TextToSpeech.ERROR) { - Log.w(TAG, "Can't initialize TextToSpeech for locale " + locale.getLanguage() + " " + locale.getCountry()); - return; - } - - if (isLocaleAvailable(locale)) - { - Log.i(TAG, "The locale " + locale.getLanguage() + " " + locale.getCountry() + " will be used for TTS."); - mTtsLocale = locale; - } - else if (isLocaleAvailable(DEFAULT_LOCALE)) - { - Log.w(TAG, "TTS is not available for locale " + locale.getLanguage() + " " + locale.getCountry() + - ". The default locale " + DEFAULT_LOCALE.getLanguage() + " " + DEFAULT_LOCALE.getCountry() + " will be used."); - mTtsLocale = DEFAULT_LOCALE; - } - else - { - Log.w(TAG, "TTS is not available for locale " + locale.getLanguage() + " " + locale.getCountry() + - " and for the default locale " + DEFAULT_LOCALE.getLanguage() + " " + DEFAULT_LOCALE.getCountry() + - ". TTS will be switched off."); - mTtsLocale = null; - return; - } - - String localeTwine = Language.localeToTwineLanguage(mTtsLocale); - if (TextUtils.isEmpty(localeTwine)) - { - Log.w(TAG, "Cann't get a twine language name for the locale " + locale.getLanguage() + " " + locale.getCountry() + - ". TTS will be switched off."); - mTtsLocale = null; + Log.e("TtsPlayer", "Failed to initialize TextToSpeach"); + lockDown(); + mInitializing = false; return; } + refreshLanguages(); mTts.setSpeechRate(SPEECH_RATE); - nativeSetTurnNotificationsLocale(localeTwine); - mTts.setLanguage(mTtsLocale); - Log.i(TAG, "setLocaleIfAvailable() onInit nativeSetTurnNotificationsLocale(" + localeTwine + ")"); + mInitializing = false; } }); } - private boolean isValid() + public boolean isReady() { - return !mIsLocaleChanging && mTts != null && mTtsLocale != null; + return (mTts != null && !mUnavailable && !mInitializing); } private void speak(String textToSpeak) { - // @TODO(vbykoianko) removes these two toasts below when the test period is finished. - Toast.makeText(MwmApplication.get(), textToSpeak, Toast.LENGTH_SHORT).show(); - if (mTts.speak(textToSpeak, TextToSpeech.QUEUE_ADD, null) == TextToSpeech.ERROR) - { - Log.e(TAG, "TextToSpeech returns TextToSpeech.ERROR."); - Toast.makeText(MwmApplication.get(), "TTS error", Toast.LENGTH_SHORT).show(); - } + if (Config.isTtsEnabled()) + //noinspection deprecation + mTts.speak(textToSpeak, TextToSpeech.QUEUE_ADD, null); } public void playTurnNotifications() { // It's necessary to call Framework.nativeGenerateTurnNotifications() even if TtsPlayer is invalid. final String[] turnNotifications = Framework.nativeGenerateTurnNotifications(); - - if (turnNotifications != null && isValid()) + + if (turnNotifications != null && isReady()) for (String textToSpeak : turnNotifications) speak(textToSpeak); } public void stop() { - if(mTts != null) + if (isReady()) mTts.stop(); } public boolean isEnabled() { - return nativeAreTurnNotificationsEnabled(); + return (isReady() && nativeAreTurnNotificationsEnabled()); } - // Note. After a call of enable(true) the flag enabled in cpp core will be set. - // But later in onInit callback the initialization could fail. - // In that case isValid() returns false and every call of playTurnNotifications returns in the beginning. - public void enable(boolean enabled) + public static void setEnabled(boolean enabled) { - if (enabled && !isValid()) - initTts(getDefaultLocale()); + Config.setTtsEnabled(enabled); nativeEnableTurnNotifications(enabled); } + private void getUsableLanguages(List outList) + { + Resources resources = MwmApplication.get().getResources(); + String[] codes = resources.getStringArray(R.array.tts_languages_supported); + String[] names = resources.getStringArray(R.array.tts_language_names); + + for (int i = 0; i < codes.length; i++) + { + try + { + outList.add(new LanguageData(codes[i], names[i], mTts)); + } catch (LanguageData.NotAvailableException ignored) + {} + } + } + + private @Nullable LanguageData refreshLanguagesInternal(List outList) + { + getUsableLanguages(outList); + + if (outList.isEmpty()) + { + // 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) + { + // Default locale can not be used too + Config.setTtsEnabled(false); + return null; + } + + return res; + } + + public @NonNull List refreshLanguages() + { + List res = new ArrayList<>(); + if (mUnavailable || mTts == null) + return res; + + LanguageData lang = refreshLanguagesInternal(res); + setLanguage(lang); + + setEnabled(Config.isTtsEnabled()); + return res; + } + private native static void nativeEnableTurnNotifications(boolean enable); private native static boolean nativeAreTurnNotificationsEnabled(); - private native static void nativeSetTurnNotificationsLocale(String locale); + private native static void nativeSetTurnNotificationsLocale(String code); private native static String nativeGetTurnNotificationsLocale(); } diff --git a/android/src/com/mapswithme/util/Config.java b/android/src/com/mapswithme/util/Config.java index e8faa5bfdf..a30e61537d 100644 --- a/android/src/com/mapswithme/util/Config.java +++ b/android/src/com/mapswithme/util/Config.java @@ -13,6 +13,9 @@ public final class Config private static final String KEY_APP_LAST_SESSION_TIMESTAMP = "LastSessionTimestamp"; private static final String KEY_APP_FIRST_INSTALL_FLAVOR = "FirstInstallFlavor"; + private static final String KEY_TTS_ENABLED = "TtsEnabled"; + private static final String KEY_TTS_LANGUAGE = "TtsLanguage"; + private static final String KEY_PREF_ZOOM_BUTTONS = "ZoomButtonsEnabled"; private static final String KEY_PREF_STATISTICS = "StatisticsEnabled"; @@ -167,7 +170,27 @@ public final class Config incrementSessionNumber(); } - public static boolean getShowZoomButtons() + public static boolean isTtsEnabled() + { + return getBool(KEY_TTS_ENABLED, true); + } + + public static void setTtsEnabled(boolean enabled) + { + setBool(KEY_TTS_ENABLED, enabled); + } + + public static String getTtsLanguage() + { + return getString(KEY_TTS_LANGUAGE); + } + + public static void setTtsLanguage(String language) + { + setString(KEY_TTS_LANGUAGE, language); + } + + public static boolean showZoomButtons() { return getBool(KEY_PREF_ZOOM_BUTTONS, true); } diff --git a/android/src/com/mapswithme/util/Language.java b/android/src/com/mapswithme/util/Language.java index cc1ed97527..151ae76103 100644 --- a/android/src/com/mapswithme/util/Language.java +++ b/android/src/com/mapswithme/util/Language.java @@ -1,18 +1,14 @@ package com.mapswithme.util; import android.content.Context; -import android.text.TextUtils; -import android.util.Log; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; - import com.mapswithme.maps.MwmApplication; import java.util.Locale; public class Language { - private final static String TAG = "Language"; // Locale.getLanguage() returns even 3-letter codes, not that we need in the C++ core, // so we use locale itself, like zh_CN public static String getDefaultLocale() @@ -34,41 +30,4 @@ public class Language return getDefaultLocale(); } - - // Converts Locale to twine language name. - // If locale can be converted returns a twine language name. For example zh-Hans, ru, en and so on. - // If not returns an empty string. - public static String localeToTwineLanguage(Locale locale) - { - if (locale == null) - { - Log.e(TAG, "localeToTwineLanguage was called with null Locale."); - return ""; - } - - final String chinese = Locale.CHINESE.getLanguage(); - final String language = locale.getLanguage(); - final String country = locale.getCountry(); - - if (chinese == null || language == null || country == null) - { - Log.e(TAG, "Methods Locale.getLanguage or Locale.getCountry return null."); - return ""; - } - - if (chinese.equals(language)) - { - if (country.equals("TW") || country.equals("MO") || country.equals("HK")) - { - return "zh-Hant"; // Chinese traditional - } - return "zh-Hans"; // Chinese simplified - } - if (TextUtils.isEmpty(language)) - { - Log.e(TAG, "locale.getLanguage() returns null or empty string."); - return ""; - } - return language; - } } diff --git a/android/src/com/mapswithme/util/statistics/Statistics.java b/android/src/com/mapswithme/util/statistics/Statistics.java index 9ea9c640c3..d82a3154ad 100644 --- a/android/src/com/mapswithme/util/statistics/Statistics.java +++ b/android/src/com/mapswithme/util/statistics/Statistics.java @@ -70,6 +70,7 @@ public enum Statistics public static final String ABOUT = "Settings. About."; public static final String COPYRIGHT = "Settings. Copyright."; public static final String GROUP_MAP = "Settings. Group: map."; + public static final String GROUP_ROUTE = "Settings. Group: route."; public static final String GROUP_MISC = "Settings. Group: misc."; private Settings() {} diff --git a/data/sound-strings/in.json/localize.json b/data/sound-strings/id.json/localize.json similarity index 100% rename from data/sound-strings/in.json/localize.json rename to data/sound-strings/id.json/localize.json diff --git a/iphone/Maps/en.lproj/Localizable.strings b/iphone/Maps/en.lproj/Localizable.strings index 8759cea1a0..68b7c72667 100644 --- a/iphone/Maps/en.lproj/Localizable.strings +++ b/iphone/Maps/en.lproj/Localizable.strings @@ -508,6 +508,18 @@ /* «Map style» entry value */ "pref_map_style_modern_light" = "Modern light"; +/* Settings «Route» category: «Tts enabled» title */ +"pref_tts_enable_title" = "Voice instructions"; + +/* Settings «Route» category: «Tts enabled» summary */ +"pref_tts_enable_summary" = "Turn instructions"; + +/* Settings «Route» category: «Tts language» title */ +"pref_tts_language_title" = "Voice language"; + +/* Settings «Route» category: «Tts unavailable» subtitle */ +"pref_tts_unavailable" = "TTS unavailable"; + "placepage_distance" = "Distance"; "placepage_coordinates" = "Coordinates"; diff --git a/iphone/Maps/ru.lproj/Localizable.strings b/iphone/Maps/ru.lproj/Localizable.strings index 179fbb5350..12c4efc498 100644 --- a/iphone/Maps/ru.lproj/Localizable.strings +++ b/iphone/Maps/ru.lproj/Localizable.strings @@ -508,6 +508,18 @@ /* «Map style» entry value */ "pref_map_style_modern_light" = "Современная светлая"; +/* Settings «Route» category: «Tts enabled» title */ +"pref_tts_enable_title" = "Голосовые инструкции"; + +/* Settings «Route» category: «Tts enabled» summary */ +"pref_tts_enable_summary" = "Подсказки о поворотах и прочих манёврах"; + +/* Settings «Route» category: «Tts language» title */ +"pref_tts_language_title" = "Язык подсказок"; + +/* Settings «Route» category: «Tts unavailable» subtitle */ +"pref_tts_unavailable" = "Голосовые подсказки недоступны"; + "placepage_distance" = "Расстояние"; "placepage_coordinates" = "Координаты"; diff --git a/strings.txt b/strings.txt index 80a2159bb6..da267c86dc 100644 --- a/strings.txt +++ b/strings.txt @@ -5224,6 +5224,30 @@ en = Modern light ru = Современная светлая + [pref_tts_enable_title] + tags = android,ios + comment = Settings «Route» category: «Tts enabled» title + en = Voice instructions + ru = Голосовые инструкции + + [pref_tts_enable_summary] + tags = android,ios + comment = Settings «Route» category: «Tts enabled» summary + en = Turn instructions + ru = Подсказки о поворотах и прочих манёврах + + [pref_tts_language_title] + tags = android,ios + comment = Settings «Route» category: «Tts language» title + en = Voice language + ru = Язык подсказок + + [pref_tts_unavailable] + tags = android,ios + comment = Settings «Route» category: «Tts unavailable» subtitle + en = TTS unavailable + ru = Голосовые подсказки недоступны + [placepage_distance] en = Distance fr = Distance