blob: 5fbd22e273e8446bea3847dfe1a8aac96d8bdf39 [file] [log] [blame]
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +01001/*
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 */
16package android.speech.tts;
17
Narayan Kamath4d034622011-06-16 12:43:46 +010018import org.xmlpull.v1.XmlPullParserException;
19
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +010020import android.content.ContentResolver;
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010021import android.content.Context;
22import android.content.Intent;
23import android.content.pm.ApplicationInfo;
24import android.content.pm.PackageManager;
Narayan Kamath4d034622011-06-16 12:43:46 +010025import android.content.pm.PackageManager.NameNotFoundException;
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010026import android.content.pm.ResolveInfo;
27import android.content.pm.ServiceInfo;
Narayan Kamath4d034622011-06-16 12:43:46 +010028import android.content.res.Resources;
29import android.content.res.TypedArray;
30import android.content.res.XmlResourceParser;
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +010031import static android.provider.Settings.Secure.getString;
32
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010033import android.provider.Settings;
34import android.speech.tts.TextToSpeech.Engine;
35import android.speech.tts.TextToSpeech.EngineInfo;
36import android.text.TextUtils;
Narayan Kamath4d034622011-06-16 12:43:46 +010037import android.util.AttributeSet;
38import android.util.Log;
39import android.util.Xml;
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010040
Narayan Kamath4d034622011-06-16 12:43:46 +010041import java.io.IOException;
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010042import java.util.ArrayList;
43import java.util.Collections;
44import java.util.Comparator;
45import java.util.List;
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +010046import java.util.Locale;
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010047
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 */
57public class TtsEngines {
Narayan Kamath4d034622011-06-16 12:43:46 +010058 private static final String TAG = "TtsEngines";
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +010059 private static final boolean DBG = false;
60
61 private static final String LOCALE_DELIMITER = "-";
Narayan Kamath4d034622011-06-16 12:43:46 +010062
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010063 private final Context mContext;
64
65 public TtsEngines(Context ctx) {
66 mContext = ctx;
67 }
68
69 /**
Narayan Kamath0e20fe52011-06-14 12:39:55 +010070 * @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}.
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010073 */
74 public String getDefaultEngine() {
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +010075 String engine = getString(mContext.getContentResolver(),
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010076 Settings.Secure.TTS_DEFAULT_SYNTH);
Narayan Kamath0e20fe52011-06-14 12:39:55 +010077 return isEngineInstalled(engine) ? engine : getHighestRankedEngineName();
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010078 }
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
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +0100139 private boolean isSystemEngine(ServiceInfo info) {
140 final ApplicationInfo appInfo = info.applicationInfo;
141 return appInfo != null && (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
142 }
143
Narayan Kamath0e20fe52011-06-14 12:39:55 +0100144 /**
Narayan Kamathc3edf2a2011-06-15 12:35:06 +0100145 * @return true if a given engine is installed on the system.
Narayan Kamath0e20fe52011-06-14 12:39:55 +0100146 */
Narayan Kamathc3edf2a2011-06-15 12:35:06 +0100147 public boolean isEngineInstalled(String engine) {
Narayan Kamath0e20fe52011-06-14 12:39:55 +0100148 if (engine == null) {
149 return false;
150 }
151
Narayan Kamathc3edf2a2011-06-15 12:35:06 +0100152 return getEngineInfo(engine) != null;
Narayan Kamath0e20fe52011-06-14 12:39:55 +0100153 }
154
Narayan Kamath4d034622011-06-16 12:43:46 +0100155 /**
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 *
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +0100186 * {@link com.android.internal.R.styleable#TextToSpeechEngine}
Narayan Kamath4d034622011-06-16 12:43:46 +0100187 */
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
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +0100238 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
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +0100256 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
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +0100283 /**
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;
Narayan Kamath39268ff2011-10-17 14:37:40 +0100347 } else {
348 return v1Locale;
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +0100349 }
Narayan Kamath39268ff2011-10-17 14:37:40 +0100350
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +0100351 if (!TextUtils.isEmpty(variant)) {
352 v1Locale += LOCALE_DELIMITER + variant;
353 }
354
355 return v1Locale;
356 }
357
Przemyslaw Szczepaniak40d51e72013-05-13 17:15:53 +0100358 /**
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() {
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +0100370 final Locale locale = Locale.getDefault();
371
Narayan Kamath39268ff2011-10-17 14:37:40 +0100372 // 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;
Narayan Kamathe5b8c4d2011-08-22 15:37:47 +0100394 }
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 }
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +0100488}