Merge pull request #354 from trashkalmar/tts-settings

[android] TTS settings and refactoring.
This commit is contained in:
Dmitry Yunitsky 2015-11-10 19:51:43 +03:00
commit ef1cf23f6a
23 changed files with 605 additions and 171 deletions

View file

@ -171,8 +171,7 @@
android:name="com.mapswithme.maps.MwmActivity"
android:launchMode="singleTask"
android:theme="@style/MwmTheme.MainActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
</activity>
android:windowSoftInputMode="stateAlwaysHidden|adjustPan"/>
<activity
android:name="com.mapswithme.country.DownloadActivity"

View file

@ -13,13 +13,13 @@ extern "C"
JNIEXPORT void JNICALL
Java_com_mapswithme_maps_sound_TtsPlayer_nativeEnableTurnNotifications(JNIEnv * env, jclass thiz, jboolean enable)
{
return frm()->EnableTurnNotifications(enable == JNI_TRUE ? true : false);
return frm()->EnableTurnNotifications(static_cast<bool>(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<jboolean>(frm()->AreTurnNotificationsEnabled());
}
JNIEXPORT void JNICALL

View file

@ -327,6 +327,14 @@
<string name="pref_map_style_classic_dark">Классическая тёмная</string>
<!-- «Map style» entry value -->
<string name="pref_map_style_modern_light">Современная светлая</string>
<!-- Settings «Route» category: «Tts enabled» title -->
<string name="pref_tts_enable_title">Голосовые инструкции</string>
<!-- Settings «Route» category: «Tts enabled» summary -->
<string name="pref_tts_enable_summary">Подсказки о поворотах и прочих манёврах</string>
<!-- Settings «Route» category: «Tts language» title -->
<string name="pref_tts_language_title">Язык подсказок</string>
<!-- Settings «Route» category: «Tts unavailable» subtitle -->
<string name="pref_tts_unavailable">Голосовые подсказки недоступны</string>
<string name="placepage_distance">Расстояние</string>
<string name="placepage_coordinates">Координаты</string>
<string name="placepage_unsorted">Без категории</string>

View file

@ -38,6 +38,8 @@
<string name="pref_settings" translatable="false">Settings</string>
<string name="pref_file_name" translatable="false">MapsMePrefs</string>
<string name="pref_map_style" translatable="false">MapStyle</string>
<string name="pref_tts_enabled" translatable="false">TtsEnabled</string>
<string name="pref_tts_language" translatable="false">TtsLanguage</string>
<string name="notification_ticker_ltr" translatable="false">%1$s: %2$s</string>
<string name="notification_ticker_rtl" translatable="false">%2$s :%1$s</string>

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Supported voice languages. Should have language[-country][:code] format.
Where:
language - two-letter ISO 639-1 language code.
country - two-letter ISO 3166-1 (alpha-2) country code.
code - internal language code to be set in the core.
If not specified, language2 is used.
Chinese is an exception. Matching is based on country code:
zho_TW, zho_MO and zho_HK correspond to zh-Hant.
Otherwise we consider that this is zh-Hans.
TODO: Move language list to core.
-->
<string-array name="tts_languages_supported"
translatable="false">
<item>en</item>
<item>ru</item>
<item>pl</item>
<item>sv</item>
<item>tr</item>
<item>fr</item>
<item>nl</item>
<item>de</item>
<item>ar</item>
<item>el</item>
<item>it</item>
<item>cs</item>
<item>hu</item>
<item>ro</item>
<item>ja</item>
<item>da</item>
<item>es</item>
<item>fi</item>
<item>hi</item>
<item>hr</item>
<item>id</item>
<item>ko</item>
<item>pt</item>
<item>sk</item>
<item>sw</item>
<item>th</item>
<item>zh-TW:zh-Hant</item>
<item>zh-CN:zh-Hans</item>
</string-array>
<string-array name="tts_language_names"
translatable="false">
<item>English</item>
<item>Русский</item>
<item>Polski</item>
<item>Svenska</item>
<item>Türkçe</item>
<item>Français</item>
<item>Nederlands</item>
<item>Deutsch</item>
<item>العربية</item>
<item>Ελληνικά</item>
<item>Italiano</item>
<item>Čeština</item>
<item>Magyar</item>
<item>Română</item>
<item>日本語</item>
<item>Dansk</item>
<item>Español</item>
<item>Suomi</item>
<item>हिंदी</item>
<item>Hrvatski</item>
<item>Indonesia</item>
<item>한국어</item>
<item>Português</item>
<item>Slovenčina</item>
<item>Kiswahili</item>
<item>ภาษาไทย</item>
<item>中文繁體</item>
<item>中文简体</item>
</string-array>
</resources>

View file

@ -329,6 +329,14 @@
<string name="pref_map_style_classic_dark">Classic dark</string>
<!-- «Map style» entry value -->
<string name="pref_map_style_modern_light">Modern light</string>
<!-- Settings «Route» category: «Tts enabled» title -->
<string name="pref_tts_enable_title">Voice instructions</string>
<!-- Settings «Route» category: «Tts enabled» summary -->
<string name="pref_tts_enable_summary">Turn instructions</string>
<!-- Settings «Route» category: «Tts language» title -->
<string name="pref_tts_language_title">Voice language</string>
<!-- Settings «Route» category: «Tts unavailable» subtitle -->
<string name="pref_tts_unavailable">TTS unavailable</string>
<string name="placepage_distance">Distance</string>
<string name="placepage_coordinates">Coordinates</string>
<string name="placepage_unsorted">Unsorted</string>

View file

@ -4,6 +4,10 @@
android:title="@string/prefs_group_map"
android:fragment="com.mapswithme.maps.settings.MapPrefsFragment"/>
<header android:id="@+id/group_route"
android:title="@string/prefs_group_route"
android:fragment="com.mapswithme.maps.settings.RoutePrefsFragment"/>
<header android:id="@+id/group_misc"
android:title="@string/prefs_group_misc"
android:fragment="com.mapswithme.maps.settings.MiscPrefsFragment"/>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:persistent="false">
<SwitchPreference android:key="@string/pref_tts_enabled"
android:title="@string/pref_tts_enable_title"
android:summary="@string/pref_tts_enable_summary"
android:switchTextOn=""
android:switchTextOff=""/>
<ListPreference android:key="@string/pref_tts_language"
android:title="@string/pref_tts_language_title"/>
</PreferenceScreen>

View file

@ -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)

View file

@ -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);
}

View file

@ -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)

View file

@ -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

View file

@ -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<String, LanguageData> 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<LanguageData> 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);
}
}
}

View file

@ -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)

View file

@ -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";
}
}

View file

@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<LanguageData> 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<LanguageData> 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<LanguageData> 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<LanguageData> 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<LanguageData> 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<LanguageData> 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<LanguageData> refreshLanguages()
{
List<LanguageData> 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();
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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() {}

View file

@ -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";

View file

@ -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" = "Координаты";

View file

@ -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