blob: 5f0cb74b1fb0f4b7226908a381779a43bea8480a [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 Kamathd3ee2fa2011-06-10 16:19:52 +010020import android.content.Context;
21import android.content.Intent;
22import android.content.pm.ApplicationInfo;
23import android.content.pm.PackageManager;
Narayan Kamath4d034622011-06-16 12:43:46 +010024import android.content.pm.PackageManager.NameNotFoundException;
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010025import android.content.pm.ResolveInfo;
26import android.content.pm.ServiceInfo;
Narayan Kamath4d034622011-06-16 12:43:46 +010027import android.content.res.Resources;
28import android.content.res.TypedArray;
29import android.content.res.XmlResourceParser;
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010030import android.provider.Settings;
31import android.speech.tts.TextToSpeech.Engine;
32import android.speech.tts.TextToSpeech.EngineInfo;
33import android.text.TextUtils;
Narayan Kamath4d034622011-06-16 12:43:46 +010034import android.util.AttributeSet;
35import android.util.Log;
36import android.util.Xml;
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010037
Narayan Kamath4d034622011-06-16 12:43:46 +010038import java.io.IOException;
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010039import java.util.ArrayList;
40import java.util.Collections;
41import java.util.Comparator;
42import java.util.List;
43
44/**
45 * Support class for querying the list of available engines
46 * on the device and deciding which one to use etc.
47 *
48 * Comments in this class the use the shorthand "system engines" for engines that
49 * are a part of the system image.
50 *
51 * @hide
52 */
53public class TtsEngines {
Narayan Kamath4d034622011-06-16 12:43:46 +010054 private static final String TAG = "TtsEngines";
55
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010056 private final Context mContext;
57
58 public TtsEngines(Context ctx) {
59 mContext = ctx;
60 }
61
62 /**
Narayan Kamath0e20fe52011-06-14 12:39:55 +010063 * @return the default TTS engine. If the user has set a default, and the engine
64 * is available on the device, the default is returned. Otherwise,
65 * the highest ranked engine is returned as per {@link EngineInfoComparator}.
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010066 */
67 public String getDefaultEngine() {
68 String engine = Settings.Secure.getString(mContext.getContentResolver(),
69 Settings.Secure.TTS_DEFAULT_SYNTH);
Narayan Kamath0e20fe52011-06-14 12:39:55 +010070 return isEngineInstalled(engine) ? engine : getHighestRankedEngineName();
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +010071 }
72
73 /**
74 * @return the package name of the highest ranked system engine, {@code null}
75 * if no TTS engines were present in the system image.
76 */
77 public String getHighestRankedEngineName() {
78 final List<EngineInfo> engines = getEngines();
79
80 if (engines.size() > 0 && engines.get(0).system) {
81 return engines.get(0).name;
82 }
83
84 return null;
85 }
86
87 /**
88 * Returns the engine info for a given engine name. Note that engines are
89 * identified by their package name.
90 */
91 public EngineInfo getEngineInfo(String packageName) {
92 PackageManager pm = mContext.getPackageManager();
93 Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE);
94 intent.setPackage(packageName);
95 List<ResolveInfo> resolveInfos = pm.queryIntentServices(intent,
96 PackageManager.MATCH_DEFAULT_ONLY);
97 // Note that the current API allows only one engine per
98 // package name. Since the "engine name" is the same as
99 // the package name.
100 if (resolveInfos != null && resolveInfos.size() == 1) {
101 return getEngineInfo(resolveInfos.get(0), pm);
102 }
103
104 return null;
105 }
106
107 /**
108 * Gets a list of all installed TTS engines.
109 *
110 * @return A list of engine info objects. The list can be empty, but never {@code null}.
111 */
112 public List<EngineInfo> getEngines() {
113 PackageManager pm = mContext.getPackageManager();
114 Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE);
115 List<ResolveInfo> resolveInfos =
116 pm.queryIntentServices(intent, PackageManager.MATCH_DEFAULT_ONLY);
117 if (resolveInfos == null) return Collections.emptyList();
118
119 List<EngineInfo> engines = new ArrayList<EngineInfo>(resolveInfos.size());
120
121 for (ResolveInfo resolveInfo : resolveInfos) {
122 EngineInfo engine = getEngineInfo(resolveInfo, pm);
123 if (engine != null) {
124 engines.add(engine);
125 }
126 }
127 Collections.sort(engines, EngineInfoComparator.INSTANCE);
128
129 return engines;
130 }
131
Narayan Kamathc3edf2a2011-06-15 12:35:06 +0100132 // TODO: Used only by the settings app. Remove once
133 // the settings UI change has been finalized.
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +0100134 public boolean isEngineEnabled(String engine) {
Narayan Kamathc3edf2a2011-06-15 12:35:06 +0100135 return isEngineInstalled(engine);
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +0100136 }
137
138 private boolean isSystemEngine(ServiceInfo info) {
139 final ApplicationInfo appInfo = info.applicationInfo;
140 return appInfo != null && (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
141 }
142
Narayan Kamath0e20fe52011-06-14 12:39:55 +0100143 /**
Narayan Kamathc3edf2a2011-06-15 12:35:06 +0100144 * @return true if a given engine is installed on the system.
Narayan Kamath0e20fe52011-06-14 12:39:55 +0100145 */
Narayan Kamathc3edf2a2011-06-15 12:35:06 +0100146 public boolean isEngineInstalled(String engine) {
Narayan Kamath0e20fe52011-06-14 12:39:55 +0100147 if (engine == null) {
148 return false;
149 }
150
Narayan Kamathc3edf2a2011-06-15 12:35:06 +0100151 return getEngineInfo(engine) != null;
Narayan Kamath0e20fe52011-06-14 12:39:55 +0100152 }
153
Narayan Kamath4d034622011-06-16 12:43:46 +0100154 /**
155 * @return an intent that can launch the settings activity for a given tts engine.
156 */
157 public Intent getSettingsIntent(String engine) {
158 PackageManager pm = mContext.getPackageManager();
159 Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE);
160 intent.setPackage(engine);
161 List<ResolveInfo> resolveInfos = pm.queryIntentServices(intent,
162 PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA);
163 // Note that the current API allows only one engine per
164 // package name. Since the "engine name" is the same as
165 // the package name.
166 if (resolveInfos != null && resolveInfos.size() == 1) {
167 ServiceInfo service = resolveInfos.get(0).serviceInfo;
168 if (service != null) {
169 final String settings = settingsActivityFromServiceInfo(service, pm);
170 if (settings != null) {
171 Intent i = new Intent();
172 i.setClassName(engine, settings);
173 return i;
174 }
175 }
176 }
177
178 return null;
179 }
180
181 /**
182 * The name of the XML tag that text to speech engines must use to
183 * declare their meta data.
184 *
185 * {@link com.android.internal.R.styleable.TextToSpeechEngine}
186 */
187 private static final String XML_TAG_NAME = "tts-engine";
188
189 private String settingsActivityFromServiceInfo(ServiceInfo si, PackageManager pm) {
190 XmlResourceParser parser = null;
191 try {
192 parser = si.loadXmlMetaData(pm, TextToSpeech.Engine.SERVICE_META_DATA);
193 if (parser == null) {
194 Log.w(TAG, "No meta-data found for :" + si);
195 return null;
196 }
197
198 final Resources res = pm.getResourcesForApplication(si.applicationInfo);
199
200 int type;
201 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT) {
202 if (type == XmlResourceParser.START_TAG) {
203 if (!XML_TAG_NAME.equals(parser.getName())) {
204 Log.w(TAG, "Package " + si + " uses unknown tag :"
205 + parser.getName());
206 return null;
207 }
208
209 final AttributeSet attrs = Xml.asAttributeSet(parser);
210 final TypedArray array = res.obtainAttributes(attrs,
211 com.android.internal.R.styleable.TextToSpeechEngine);
212 final String settings = array.getString(
213 com.android.internal.R.styleable.TextToSpeechEngine_settingsActivity);
214 array.recycle();
215
216 return settings;
217 }
218 }
219
220 return null;
221 } catch (NameNotFoundException e) {
222 Log.w(TAG, "Could not load resources for : " + si);
223 return null;
224 } catch (XmlPullParserException e) {
225 Log.w(TAG, "Error parsing metadata for " + si + ":" + e);
226 return null;
227 } catch (IOException e) {
228 Log.w(TAG, "Error parsing metadata for " + si + ":" + e);
229 return null;
230 } finally {
231 if (parser != null) {
232 parser.close();
233 }
234 }
235 }
236
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +0100237 private EngineInfo getEngineInfo(ResolveInfo resolve, PackageManager pm) {
238 ServiceInfo service = resolve.serviceInfo;
239 if (service != null) {
240 EngineInfo engine = new EngineInfo();
241 // Using just the package name isn't great, since it disallows having
242 // multiple engines in the same package, but that's what the existing API does.
243 engine.name = service.packageName;
244 CharSequence label = service.loadLabel(pm);
245 engine.label = TextUtils.isEmpty(label) ? engine.name : label.toString();
246 engine.icon = service.getIconResource();
247 engine.priority = resolve.priority;
248 engine.system = isSystemEngine(service);
249 return engine;
250 }
251
252 return null;
253 }
254
Narayan Kamathd3ee2fa2011-06-10 16:19:52 +0100255 private static class EngineInfoComparator implements Comparator<EngineInfo> {
256 private EngineInfoComparator() { }
257
258 static EngineInfoComparator INSTANCE = new EngineInfoComparator();
259
260 /**
261 * Engines that are a part of the system image are always lesser
262 * than those that are not. Within system engines / non system engines
263 * the engines are sorted in order of their declared priority.
264 */
265 @Override
266 public int compare(EngineInfo lhs, EngineInfo rhs) {
267 if (lhs.system && !rhs.system) {
268 return -1;
269 } else if (rhs.system && !lhs.system) {
270 return 1;
271 } else {
272 // Either both system engines, or both non system
273 // engines.
274 //
275 // Note, this isn't a typo. Higher priority numbers imply
276 // higher priority, but are "lower" in the sort order.
277 return rhs.priority - lhs.priority;
278 }
279 }
280 }
281
282}