[android] refactor: TTS refactored.

This commit is contained in:
Alexander Marchuk 2015-10-22 20:39:23 +03:00
parent ec1de77a33
commit b194d3d271
12 changed files with 325 additions and 165 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

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Supported voices. Should have language2[-country2][:code] format.
Where:
language2 - two-letter ISO 639-1 language code.
country2 - 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 ISO 3166-1 (alpha-3) country code:
zho_TWN, zho_MAC and zho_HKG 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>TODO: Indonesian</item>
<item>한국어</item>
<item>Português</item>
<item>Slovenčina</item>
<item>Kiswahili</item>
<item>ไทย</item>
<item>TODO: Chinese Traditional</item>
<item>TODO: Chinese Simplified</item>
</string-array>
</resources>

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,71 @@
package com.mapswithme.maps.sound;
import java.util.Locale;
public class LanguageData
{
public final Locale locale;
public final String name;
public final String internalCode;
private int mStatus;
private LanguageData(String name, Locale locale, String internalCode)
{
this.locale = locale;
this.name = name;
this.internalCode = internalCode;
}
static LanguageData parse(String line, String name)
{
String[] parts = line.split(":");
String internalCode = (parts.length > 1 ? parts[1] : null);
parts = parts[0].split("-");
String language = parts[0];
String country = (parts.length > 1 ? parts[1] : "");
Locale locale = new Locale(language, country.toUpperCase());
return new LanguageData(name, locale, (internalCode == null ? language : internalCode));
}
void setStatus(int status)
{
mStatus = status;
}
public int getStatus()
{
return mStatus;
}
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 "(" + mStatus + ") " + name + ": " + locale + ", internal: " + internalCode;
}
}

View file

@ -1,14 +1,18 @@
package com.mapswithme.maps.sound;
import android.content.Context;
import android.content.res.Resources;
import android.speech.tts.TextToSpeech;
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;
@ -16,143 +20,148 @@ public enum TtsPlayer
{
INSTANCE;
private static final String TAG = "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 lack of supported languages
private boolean mUnavailable;
TtsPlayer() {}
public void reinitIfLocaleChanged()
private @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 @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 LanguageData getDefaultLanguage(List<LanguageData> langs)
{
if (mIsLocaleChanging)
return; // Preventing reiniting while creating TextToSpeech object. There's a small possibility a new locale is skipped.
Locale defLocale = Locale.getDefault();
if (defLocale == null)
defLocale = DEFAULT_LOCALE;
if (mTts != null && mTtsLocale != null && mTtsLocale.equals(locale))
return;
mTtsLocale = null;
mIsLocaleChanging = true;
if (mTts != null)
LanguageData lang = findSupportedLanguage(defLocale, langs);
if (lang == null)
{
mTts.stop();
mTts.shutdown();
// No supported languages found, lock down TTS :(
mUnavailable = true;
Config.setTtsEnabled(false);
}
mTts = new TextToSpeech(MwmApplication.get(), new TextToSpeech.OnInitListener()
return lang;
}
public LanguageData getSelectedLanguage(List<LanguageData> langs)
{
return findSupportedLanguage(Config.getTtsLanguage(), langs);
}
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());
Log.e(TAG, "Failed to initialize TextToSpeach");
mUnavailable = true;
mInitializing = false;
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;
}
List<LanguageData> langs = getAvailableLanguages(false);
LanguageData defLang = getDefaultLanguage(langs);
LanguageData lang = getSelectedLanguage(langs);
if (lang == null)
lang = defLang;
String localeTwine = Language.localeToTwineLanguage(mTtsLocale);
if (TextUtils.isEmpty(localeTwine))
if (lang != null)
{
Log.w(TAG, "Cann't get a twine language name for the locale " + locale.getLanguage() + " " + locale.getCountry() +
". TTS will be switched off.");
mTtsLocale = null;
return;
String curLangCode = Config.getTtsLanguage();
if (defLang != null && !defLang.matchesInternalCode(curLangCode))
{
// Selected TTS locale does not match current defaults. And it was NOT set by user.
// Assume that the current locale was equal to old default one.
// So, let the new default locale be current.
if (!Config.isTtsLanguageSetByUser())
lang = findSupportedLanguage(defLang.internalCode, langs);
}
setLanguage(lang);
}
mTts.setSpeechRate(SPEECH_RATE);
nativeSetTurnNotificationsLocale(localeTwine);
mTts.setLanguage(mTtsLocale);
Log.i(TAG, "setLocaleIfAvailable() onInit nativeSetTurnNotificationsLocale(" + localeTwine + ")");
setEnabled(Config.isTtsEnabled());
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();
}
//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();
}
@ -161,18 +170,40 @@ public enum TtsPlayer
return 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 void setEnabled(boolean enabled)
{
if (enabled && !isValid())
initTts(getDefaultLocale());
nativeEnableTurnNotifications(enabled);
}
public List<LanguageData> getAvailableLanguages(boolean includeDownloadable)
{
List<LanguageData> res = new ArrayList<>();
if (mUnavailable || mTts == null)
return res;
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++)
{
LanguageData lang = LanguageData.parse(codes[i], names[i]);
int status = mTts.isLanguageAvailable(lang.locale);
int requiredStatus = (includeDownloadable ? TextToSpeech.LANG_MISSING_DATA
: TextToSpeech.LANG_AVAILABLE);
if (status >= requiredStatus)
{
lang.setStatus(status);
res.add(lang);
}
}
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,10 @@ 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_TTS_LANGUAGE_SET_BY_USER = "TtsLanguageSetByUser";
private static final String KEY_PREF_ZOOM_BUTTONS = "ZoomButtonsEnabled";
private static final String KEY_PREF_STATISTICS = "StatisticsEnabled";
@ -167,7 +171,37 @@ 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 boolean isTtsLanguageSetByUser()
{
return getBool(KEY_TTS_LANGUAGE_SET_BY_USER);
}
public static void setTtsLanguageSetByUser()
{
setBool(KEY_TTS_LANGUAGE_SET_BY_USER, true);
}
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;
}
}