Home | History | Annotate | Download | only in tts
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 package android.speech.tts;
     17 
     18 import org.xmlpull.v1.XmlPullParserException;
     19 
     20 import android.content.ContentResolver;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.pm.ApplicationInfo;
     24 import android.content.pm.PackageManager;
     25 import android.content.pm.PackageManager.NameNotFoundException;
     26 import android.content.pm.ResolveInfo;
     27 import android.content.pm.ServiceInfo;
     28 import android.content.res.Resources;
     29 import android.content.res.TypedArray;
     30 import android.content.res.XmlResourceParser;
     31 import static android.provider.Settings.Secure.getString;
     32 
     33 import android.provider.Settings;
     34 import android.speech.tts.TextToSpeech.Engine;
     35 import android.speech.tts.TextToSpeech.EngineInfo;
     36 import android.text.TextUtils;
     37 import android.util.AttributeSet;
     38 import android.util.Log;
     39 import android.util.Xml;
     40 
     41 import java.io.IOException;
     42 import java.util.ArrayList;
     43 import java.util.Collections;
     44 import java.util.Comparator;
     45 import java.util.List;
     46 import java.util.Locale;
     47 
     48 /**
     49  * Support class for querying the list of available engines
     50  * on the device and deciding which one to use etc.
     51  *
     52  * Comments in this class the use the shorthand "system engines" for engines that
     53  * are a part of the system image.
     54  *
     55  * @hide
     56  */
     57 public class TtsEngines {
     58     private static final String TAG = "TtsEngines";
     59     private static final boolean DBG = false;
     60 
     61     private static final String LOCALE_DELIMITER = "-";
     62 
     63     private final Context mContext;
     64 
     65     public TtsEngines(Context ctx) {
     66         mContext = ctx;
     67     }
     68 
     69     /**
     70      * @return the default TTS engine. If the user has set a default, and the engine
     71      *         is available on the device, the default is returned. Otherwise,
     72      *         the highest ranked engine is returned as per {@link EngineInfoComparator}.
     73      */
     74     public String getDefaultEngine() {
     75         String engine = getString(mContext.getContentResolver(),
     76                 Settings.Secure.TTS_DEFAULT_SYNTH);
     77         return isEngineInstalled(engine) ? engine : getHighestRankedEngineName();
     78     }
     79 
     80     /**
     81      * @return the package name of the highest ranked system engine, {@code null}
     82      *         if no TTS engines were present in the system image.
     83      */
     84     public String getHighestRankedEngineName() {
     85         final List<EngineInfo> engines = getEngines();
     86 
     87         if (engines.size() > 0 && engines.get(0).system) {
     88             return engines.get(0).name;
     89         }
     90 
     91         return null;
     92     }
     93 
     94     /**
     95      * Returns the engine info for a given engine name. Note that engines are
     96      * identified by their package name.
     97      */
     98     public EngineInfo getEngineInfo(String packageName) {
     99         PackageManager pm = mContext.getPackageManager();
    100         Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE);
    101         intent.setPackage(packageName);
    102         List<ResolveInfo> resolveInfos = pm.queryIntentServices(intent,
    103                 PackageManager.MATCH_DEFAULT_ONLY);
    104         // Note that the current API allows only one engine per
    105         // package name. Since the "engine name" is the same as
    106         // the package name.
    107         if (resolveInfos != null && resolveInfos.size() == 1) {
    108             return getEngineInfo(resolveInfos.get(0), pm);
    109         }
    110 
    111         return null;
    112     }
    113 
    114     /**
    115      * Gets a list of all installed TTS engines.
    116      *
    117      * @return A list of engine info objects. The list can be empty, but never {@code null}.
    118      */
    119     public List<EngineInfo> getEngines() {
    120         PackageManager pm = mContext.getPackageManager();
    121         Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE);
    122         List<ResolveInfo> resolveInfos =
    123                 pm.queryIntentServices(intent, PackageManager.MATCH_DEFAULT_ONLY);
    124         if (resolveInfos == null) return Collections.emptyList();
    125 
    126         List<EngineInfo> engines = new ArrayList<EngineInfo>(resolveInfos.size());
    127 
    128         for (ResolveInfo resolveInfo : resolveInfos) {
    129             EngineInfo engine = getEngineInfo(resolveInfo, pm);
    130             if (engine != null) {
    131                 engines.add(engine);
    132             }
    133         }
    134         Collections.sort(engines, EngineInfoComparator.INSTANCE);
    135 
    136         return engines;
    137     }
    138 
    139     private boolean isSystemEngine(ServiceInfo info) {
    140         final ApplicationInfo appInfo = info.applicationInfo;
    141         return appInfo != null && (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
    142     }
    143 
    144     /**
    145      * @return true if a given engine is installed on the system.
    146      */
    147     public boolean isEngineInstalled(String engine) {
    148         if (engine == null) {
    149             return false;
    150         }
    151 
    152         return getEngineInfo(engine) != null;
    153     }
    154 
    155     /**
    156      * @return an intent that can launch the settings activity for a given tts engine.
    157      */
    158     public Intent getSettingsIntent(String engine) {
    159         PackageManager pm = mContext.getPackageManager();
    160         Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE);
    161         intent.setPackage(engine);
    162         List<ResolveInfo> resolveInfos = pm.queryIntentServices(intent,
    163                 PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA);
    164         // Note that the current API allows only one engine per
    165         // package name. Since the "engine name" is the same as
    166         // the package name.
    167         if (resolveInfos != null && resolveInfos.size() == 1) {
    168             ServiceInfo service = resolveInfos.get(0).serviceInfo;
    169             if (service != null) {
    170                 final String settings = settingsActivityFromServiceInfo(service, pm);
    171                 if (settings != null) {
    172                     Intent i = new Intent();
    173                     i.setClassName(engine, settings);
    174                     return i;
    175                 }
    176             }
    177         }
    178 
    179         return null;
    180     }
    181 
    182     /**
    183      * The name of the XML tag that text to speech engines must use to
    184      * declare their meta data.
    185      *
    186      * {@link com.android.internal.R.styleable#TextToSpeechEngine}
    187      */
    188     private static final String XML_TAG_NAME = "tts-engine";
    189 
    190     private String settingsActivityFromServiceInfo(ServiceInfo si, PackageManager pm) {
    191         XmlResourceParser parser = null;
    192         try {
    193             parser = si.loadXmlMetaData(pm, TextToSpeech.Engine.SERVICE_META_DATA);
    194             if (parser == null) {
    195                 Log.w(TAG, "No meta-data found for :" + si);
    196                 return null;
    197             }
    198 
    199             final Resources res = pm.getResourcesForApplication(si.applicationInfo);
    200 
    201             int type;
    202             while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT) {
    203                 if (type == XmlResourceParser.START_TAG) {
    204                     if (!XML_TAG_NAME.equals(parser.getName())) {
    205                         Log.w(TAG, "Package " + si + " uses unknown tag :"
    206                                 + parser.getName());
    207                         return null;
    208                     }
    209 
    210                     final AttributeSet attrs = Xml.asAttributeSet(parser);
    211                     final TypedArray array = res.obtainAttributes(attrs,
    212                             com.android.internal.R.styleable.TextToSpeechEngine);
    213                     final String settings = array.getString(
    214                             com.android.internal.R.styleable.TextToSpeechEngine_settingsActivity);
    215                     array.recycle();
    216 
    217                     return settings;
    218                 }
    219             }
    220 
    221             return null;
    222         } catch (NameNotFoundException e) {
    223             Log.w(TAG, "Could not load resources for : " + si);
    224             return null;
    225         } catch (XmlPullParserException e) {
    226             Log.w(TAG, "Error parsing metadata for " + si + ":" + e);
    227             return null;
    228         } catch (IOException e) {
    229             Log.w(TAG, "Error parsing metadata for " + si + ":" + e);
    230             return null;
    231         } finally {
    232             if (parser != null) {
    233                 parser.close();
    234             }
    235         }
    236     }
    237 
    238     private EngineInfo getEngineInfo(ResolveInfo resolve, PackageManager pm) {
    239         ServiceInfo service = resolve.serviceInfo;
    240         if (service != null) {
    241             EngineInfo engine = new EngineInfo();
    242             // Using just the package name isn't great, since it disallows having
    243             // multiple engines in the same package, but that's what the existing API does.
    244             engine.name = service.packageName;
    245             CharSequence label = service.loadLabel(pm);
    246             engine.label = TextUtils.isEmpty(label) ? engine.name : label.toString();
    247             engine.icon = service.getIconResource();
    248             engine.priority = resolve.priority;
    249             engine.system = isSystemEngine(service);
    250             return engine;
    251         }
    252 
    253         return null;
    254     }
    255 
    256     private static class EngineInfoComparator implements Comparator<EngineInfo> {
    257         private EngineInfoComparator() { }
    258 
    259         static EngineInfoComparator INSTANCE = new EngineInfoComparator();
    260 
    261         /**
    262          * Engines that are a part of the system image are always lesser
    263          * than those that are not. Within system engines / non system engines
    264          * the engines are sorted in order of their declared priority.
    265          */
    266         @Override
    267         public int compare(EngineInfo lhs, EngineInfo rhs) {
    268             if (lhs.system && !rhs.system) {
    269                 return -1;
    270             } else if (rhs.system && !lhs.system) {
    271                 return 1;
    272             } else {
    273                 // Either both system engines, or both non system
    274                 // engines.
    275                 //
    276                 // Note, this isn't a typo. Higher priority numbers imply
    277                 // higher priority, but are "lower" in the sort order.
    278                 return rhs.priority - lhs.priority;
    279             }
    280         }
    281     }
    282 
    283     /**
    284      * Returns the locale string for a given TTS engine. Attempts to read the
    285      * value from {@link Settings.Secure#TTS_DEFAULT_LOCALE}, failing which the
    286      * old style value from {@link Settings.Secure#TTS_DEFAULT_LANG} is read. If
    287      * both these values are empty, the default phone locale is returned.
    288      *
    289      * @param engineName the engine to return the locale for.
    290      * @return the locale string preference for this engine. Will be non null
    291      *         and non empty.
    292      */
    293     public String getLocalePrefForEngine(String engineName) {
    294         String locale = parseEnginePrefFromList(
    295                 getString(mContext.getContentResolver(), Settings.Secure.TTS_DEFAULT_LOCALE),
    296                 engineName);
    297 
    298         if (TextUtils.isEmpty(locale)) {
    299             // The new style setting is unset, attempt to return the old style setting.
    300             locale = getV1Locale();
    301         }
    302 
    303         if (DBG) Log.d(TAG, "getLocalePrefForEngine(" + engineName + ")= " + locale);
    304 
    305         return locale;
    306     }
    307 
    308     /**
    309      * Parses a locale preference value delimited by {@link #LOCALE_DELIMITER}.
    310      * Varies from {@link String#split} in that it will always return an array
    311      * of length 3 with non null values.
    312      */
    313     public static String[] parseLocalePref(String pref) {
    314         String[] returnVal = new String[] { "", "", ""};
    315         if (!TextUtils.isEmpty(pref)) {
    316             String[] split = pref.split(LOCALE_DELIMITER);
    317             System.arraycopy(split, 0, returnVal, 0, split.length);
    318         }
    319 
    320         if (DBG) Log.d(TAG, "parseLocalePref(" + returnVal[0] + "," + returnVal[1] +
    321                 "," + returnVal[2] +")");
    322 
    323         return returnVal;
    324     }
    325 
    326     /**
    327      * @return the old style locale string constructed from
    328      *         {@link Settings.Secure#TTS_DEFAULT_LANG},
    329      *         {@link Settings.Secure#TTS_DEFAULT_COUNTRY} and
    330      *         {@link Settings.Secure#TTS_DEFAULT_VARIANT}. If no such locale is set,
    331      *         then return the default phone locale.
    332      */
    333     private String getV1Locale() {
    334         final ContentResolver cr = mContext.getContentResolver();
    335 
    336         final String lang = Settings.Secure.getString(cr, Settings.Secure.TTS_DEFAULT_LANG);
    337         final String country = Settings.Secure.getString(cr, Settings.Secure.TTS_DEFAULT_COUNTRY);
    338         final String variant = Settings.Secure.getString(cr, Settings.Secure.TTS_DEFAULT_VARIANT);
    339 
    340         if (TextUtils.isEmpty(lang)) {
    341             return getDefaultLocale();
    342         }
    343 
    344         String v1Locale = lang;
    345         if (!TextUtils.isEmpty(country)) {
    346             v1Locale += LOCALE_DELIMITER + country;
    347         } else {
    348             return v1Locale;
    349         }
    350 
    351         if (!TextUtils.isEmpty(variant)) {
    352             v1Locale += LOCALE_DELIMITER + variant;
    353         }
    354 
    355         return v1Locale;
    356     }
    357 
    358     /**
    359      * Return the default device locale in form of 3 letter codes delimited by
    360      * {@link #LOCALE_DELIMITER}:
    361      * <ul>
    362      *   <li> "ISO 639-2/T language code" if locale have no country entry</li>
    363      *   <li> "ISO 639-2/T language code{@link #LOCALE_DELIMITER}ISO 3166 country code "
    364      *     if locale have no variant entry</li>
    365      *   <li> "ISO 639-2/T language code{@link #LOCALE_DELIMITER}ISO 3166 country code
    366      *     {@link #LOCALE_DELIMITER} variant" if locale have variant entry</li>
    367      * </ul>
    368      */
    369     public String getDefaultLocale() {
    370         final Locale locale = Locale.getDefault();
    371 
    372         // Note that the default locale might have an empty variant
    373         // or language, and we take care that the construction is
    374         // the same as {@link #getV1Locale} i.e no trailing delimiters
    375         // or spaces.
    376         String defaultLocale = locale.getISO3Language();
    377         if (TextUtils.isEmpty(defaultLocale)) {
    378             Log.w(TAG, "Default locale is empty.");
    379             return "";
    380         }
    381 
    382         if (!TextUtils.isEmpty(locale.getISO3Country())) {
    383             defaultLocale += LOCALE_DELIMITER + locale.getISO3Country();
    384         } else {
    385             // Do not allow locales of the form lang--variant with
    386             // an empty country.
    387             return defaultLocale;
    388         }
    389         if (!TextUtils.isEmpty(locale.getVariant())) {
    390             defaultLocale += LOCALE_DELIMITER + locale.getVariant();
    391         }
    392 
    393         return defaultLocale;
    394     }
    395 
    396     /**
    397      * Parses a comma separated list of engine locale preferences. The list is of the
    398      * form {@code "engine_name_1:locale_1,engine_name_2:locale2"} and so on and
    399      * so forth. Returns null if the list is empty, malformed or if there is no engine
    400      * specific preference in the list.
    401      */
    402     private static String parseEnginePrefFromList(String prefValue, String engineName) {
    403         if (TextUtils.isEmpty(prefValue)) {
    404             return null;
    405         }
    406 
    407         String[] prefValues = prefValue.split(",");
    408 
    409         for (String value : prefValues) {
    410             final int delimiter = value.indexOf(':');
    411             if (delimiter > 0) {
    412                 if (engineName.equals(value.substring(0, delimiter))) {
    413                     return value.substring(delimiter + 1);
    414                 }
    415             }
    416         }
    417 
    418         return null;
    419     }
    420 
    421     public synchronized void updateLocalePrefForEngine(String name, String newLocale) {
    422         final String prefList = Settings.Secure.getString(mContext.getContentResolver(),
    423                 Settings.Secure.TTS_DEFAULT_LOCALE);
    424         if (DBG) {
    425             Log.d(TAG, "updateLocalePrefForEngine(" + name + ", " + newLocale +
    426                     "), originally: " + prefList);
    427         }
    428 
    429         final String newPrefList = updateValueInCommaSeparatedList(prefList,
    430                 name, newLocale);
    431 
    432         if (DBG) Log.d(TAG, "updateLocalePrefForEngine(), writing: " + newPrefList.toString());
    433 
    434         Settings.Secure.putString(mContext.getContentResolver(),
    435                 Settings.Secure.TTS_DEFAULT_LOCALE, newPrefList.toString());
    436     }
    437 
    438     /**
    439      * Updates the value for a given key in a comma separated list of key value pairs,
    440      * each of which are delimited by a colon. If no value exists for the given key,
    441      * the kay value pair are appended to the end of the list.
    442      */
    443     private String updateValueInCommaSeparatedList(String list, String key,
    444             String newValue) {
    445         StringBuilder newPrefList = new StringBuilder();
    446         if (TextUtils.isEmpty(list)) {
    447             // If empty, create a new list with a single entry.
    448             newPrefList.append(key).append(':').append(newValue);
    449         } else {
    450             String[] prefValues = list.split(",");
    451             // Whether this is the first iteration in the loop.
    452             boolean first = true;
    453             // Whether we found the given key.
    454             boolean found = false;
    455             for (String value : prefValues) {
    456                 final int delimiter = value.indexOf(':');
    457                 if (delimiter > 0) {
    458                     if (key.equals(value.substring(0, delimiter))) {
    459                         if (first) {
    460                             first = false;
    461                         } else {
    462                             newPrefList.append(',');
    463                         }
    464                         found = true;
    465                         newPrefList.append(key).append(':').append(newValue);
    466                     } else {
    467                         if (first) {
    468                             first = false;
    469                         } else {
    470                             newPrefList.append(',');
    471                         }
    472                         // Copy across the entire key + value as is.
    473                         newPrefList.append(value);
    474                     }
    475                 }
    476             }
    477 
    478             if (!found) {
    479                 // Not found, but the rest of the keys would have been copied
    480                 // over already, so just append it to the end.
    481                 newPrefList.append(',');
    482                 newPrefList.append(key).append(':').append(newValue);
    483             }
    484         }
    485 
    486         return newPrefList.toString();
    487     }
    488 }
    489