TtsEngines.java revision 4d03462b374dfc080f0c7c78d458c102a26be5c6
1a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich/*
2a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich * Copyright (C) 2011 The Android Open Source Project
3a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich *
4a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich * use this file except in compliance with the License. You may obtain a copy of
6a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich * the License at
7a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich *
8a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich * http://www.apache.org/licenses/LICENSE-2.0
9a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich *
10a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich * Unless required by applicable law or agreed to in writing, software
11a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich * License for the specific language governing permissions and limitations under
14a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich * the License.
15a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich */
16a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevichpackage android.speech.tts;
17a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevich
18a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevichimport org.xmlpull.v1.XmlPullParserException;
19655a7c081f83b8351ed5f11a6c6accd9458293a8Ben Cheng
20655a7c081f83b8351ed5f11a6c6accd9458293a8Ben Chengimport android.content.Context;
21655a7c081f83b8351ed5f11a6c6accd9458293a8Ben Chengimport android.content.Intent;
22655a7c081f83b8351ed5f11a6c6accd9458293a8Ben Chengimport android.content.pm.ApplicationInfo;
23a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevichimport android.content.pm.PackageManager;
24655a7c081f83b8351ed5f11a6c6accd9458293a8Ben Chengimport android.content.pm.PackageManager.NameNotFoundException;
25655a7c081f83b8351ed5f11a6c6accd9458293a8Ben Chengimport android.content.pm.ResolveInfo;
26655a7c081f83b8351ed5f11a6c6accd9458293a8Ben Chengimport android.content.pm.ServiceInfo;
27655a7c081f83b8351ed5f11a6c6accd9458293a8Ben Chengimport android.content.res.Resources;
28a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevichimport android.content.res.TypedArray;
29655a7c081f83b8351ed5f11a6c6accd9458293a8Ben Chengimport android.content.res.XmlResourceParser;
30d7db594b8d1dab36b711bd887a9dd21675c87243Tao Baoimport android.provider.Settings;
31d7db594b8d1dab36b711bd887a9dd21675c87243Tao Baoimport android.speech.tts.TextToSpeech.Engine;
32d7db594b8d1dab36b711bd887a9dd21675c87243Tao Baoimport android.speech.tts.TextToSpeech.EngineInfo;
33a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevichimport android.text.TextUtils;
34d7db594b8d1dab36b711bd887a9dd21675c87243Tao Baoimport android.util.AttributeSet;
35d7db594b8d1dab36b711bd887a9dd21675c87243Tao Baoimport android.util.Log;
36d7db594b8d1dab36b711bd887a9dd21675c87243Tao Baoimport android.util.Xml;
37655a7c081f83b8351ed5f11a6c6accd9458293a8Ben Cheng
38a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevichimport java.io.IOException;
39a67e4de6620b570dabe0f92985228af0d0204f2cNick Kralevichimport 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 {
54    private static final String TAG = "TtsEngines";
55
56    private final Context mContext;
57
58    public TtsEngines(Context ctx) {
59        mContext = ctx;
60    }
61
62    /**
63     * @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}.
66     */
67    public String getDefaultEngine() {
68        String engine = Settings.Secure.getString(mContext.getContentResolver(),
69                Settings.Secure.TTS_DEFAULT_SYNTH);
70        return isEngineInstalled(engine) ? engine : getHighestRankedEngineName();
71    }
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
132    // TODO: Used only by the settings app. Remove once
133    // the settings UI change has been finalized.
134    public boolean isEngineEnabled(String engine) {
135        return isEngineInstalled(engine);
136    }
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
143    /**
144     * @return true if a given engine is installed on the system.
145     */
146    public boolean isEngineInstalled(String engine) {
147        if (engine == null) {
148            return false;
149        }
150
151        return getEngineInfo(engine) != null;
152    }
153
154    /**
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
237    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
255    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}
283