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     private String getDefaultLocale() {
    359         final Locale locale = Locale.getDefault();
    360 
    361         // Note that the default locale might have an empty variant
    362         // or language, and we take care that the construction is
    363         // the same as {@link #getV1Locale} i.e no trailing delimiters
    364         // or spaces.
    365         String defaultLocale = locale.getISO3Language();
    366         if (TextUtils.isEmpty(defaultLocale)) {
    367             Log.w(TAG, "Default locale is empty.");
    368             return "";
    369         }
    370 
    371         if (!TextUtils.isEmpty(locale.getISO3Country())) {
    372             defaultLocale += LOCALE_DELIMITER + locale.getISO3Country();
    373         } else {
    374             // Do not allow locales of the form lang--variant with
    375             // an empty country.
    376             return defaultLocale;
    377         }
    378         if (!TextUtils.isEmpty(locale.getVariant())) {
    379             defaultLocale += LOCALE_DELIMITER + locale.getVariant();
    380         }
    381 
    382         return defaultLocale;
    383     }
    384 
    385     /**
    386      * Parses a comma separated list of engine locale preferences. The list is of the
    387      * form {@code "engine_name_1:locale_1,engine_name_2:locale2"} and so on and
    388      * so forth. Returns null if the list is empty, malformed or if there is no engine
    389      * specific preference in the list.
    390      */
    391     private static String parseEnginePrefFromList(String prefValue, String engineName) {
    392         if (TextUtils.isEmpty(prefValue)) {
    393             return null;
    394         }
    395 
    396         String[] prefValues = prefValue.split(",");
    397 
    398         for (String value : prefValues) {
    399             final int delimiter = value.indexOf(':');
    400             if (delimiter > 0) {
    401                 if (engineName.equals(value.substring(0, delimiter))) {
    402                     return value.substring(delimiter + 1);
    403                 }
    404             }
    405         }
    406 
    407         return null;
    408     }
    409 
    410     public synchronized void updateLocalePrefForEngine(String name, String newLocale) {
    411         final String prefList = Settings.Secure.getString(mContext.getContentResolver(),
    412                 Settings.Secure.TTS_DEFAULT_LOCALE);
    413         if (DBG) {
    414             Log.d(TAG, "updateLocalePrefForEngine(" + name + ", " + newLocale +
    415                     "), originally: " + prefList);
    416         }
    417 
    418         final String newPrefList = updateValueInCommaSeparatedList(prefList,
    419                 name, newLocale);
    420 
    421         if (DBG) Log.d(TAG, "updateLocalePrefForEngine(), writing: " + newPrefList.toString());
    422 
    423         Settings.Secure.putString(mContext.getContentResolver(),
    424                 Settings.Secure.TTS_DEFAULT_LOCALE, newPrefList.toString());
    425     }
    426 
    427     /**
    428      * Updates the value for a given key in a comma separated list of key value pairs,
    429      * each of which are delimited by a colon. If no value exists for the given key,
    430      * the kay value pair are appended to the end of the list.
    431      */
    432     private String updateValueInCommaSeparatedList(String list, String key,
    433             String newValue) {
    434         StringBuilder newPrefList = new StringBuilder();
    435         if (TextUtils.isEmpty(list)) {
    436             // If empty, create a new list with a single entry.
    437             newPrefList.append(key).append(':').append(newValue);
    438         } else {
    439             String[] prefValues = list.split(",");
    440             // Whether this is the first iteration in the loop.
    441             boolean first = true;
    442             // Whether we found the given key.
    443             boolean found = false;
    444             for (String value : prefValues) {
    445                 final int delimiter = value.indexOf(':');
    446                 if (delimiter > 0) {
    447                     if (key.equals(value.substring(0, delimiter))) {
    448                         if (first) {
    449                             first = false;
    450                         } else {
    451                             newPrefList.append(',');
    452                         }
    453                         found = true;
    454                         newPrefList.append(key).append(':').append(newValue);
    455                     } else {
    456                         if (first) {
    457                             first = false;
    458                         } else {
    459                             newPrefList.append(',');
    460                         }
    461                         // Copy across the entire key + value as is.
    462                         newPrefList.append(value);
    463                     }
    464                 }
    465             }
    466 
    467             if (!found) {
    468                 // Not found, but the rest of the keys would have been copied
    469                 // over already, so just append it to the end.
    470                 newPrefList.append(',');
    471                 newPrefList.append(key).append(':').append(newValue);
    472             }
    473         }
    474 
    475         return newPrefList.toString();
    476     }
    477 }
    478