• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 Google Inc.
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.tts;
17 
18 import android.app.Service;
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.SharedPreferences;
23 import android.content.pm.ActivityInfo;
24 import android.content.pm.PackageManager;
25 import android.content.pm.PackageManager.NameNotFoundException;
26 import android.content.pm.ResolveInfo;
27 import android.database.Cursor;
28 import android.media.AudioManager;
29 import android.media.MediaPlayer;
30 import android.media.MediaPlayer.OnCompletionListener;
31 import android.net.Uri;
32 import android.os.IBinder;
33 import android.os.RemoteCallbackList;
34 import android.os.RemoteException;
35 import android.preference.PreferenceManager;
36 import android.speech.tts.ITts.Stub;
37 import android.speech.tts.ITtsCallback;
38 import android.speech.tts.TextToSpeech;
39 import android.util.Log;
40 
41 import java.io.File;
42 import java.io.IOException;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Locale;
48 import java.util.concurrent.locks.ReentrantLock;
49 import java.util.concurrent.TimeUnit;
50 
51 
52 /**
53  * @hide Synthesizes speech from text. This is implemented as a service so that
54  *       other applications can call the TTS without needing to bundle the TTS
55  *       in the build.
56  *
57  */
58 public class TtsService extends Service implements OnCompletionListener {
59 
60     private static class SpeechItem {
61         public static final int TEXT = 0;
62         public static final int EARCON = 1;
63         public static final int SILENCE = 2;
64         public static final int TEXT_TO_FILE = 3;
65         public String mText = "";
66         public ArrayList<String> mParams = null;
67         public int mType = TEXT;
68         public long mDuration = 0;
69         public String mFilename = null;
70         public String mCallingApp = "";
71 
SpeechItem(String source, String text, ArrayList<String> params, int itemType)72         public SpeechItem(String source, String text, ArrayList<String> params, int itemType) {
73             mText = text;
74             mParams = params;
75             mType = itemType;
76             mCallingApp = source;
77         }
78 
SpeechItem(String source, long silenceTime, ArrayList<String> params)79         public SpeechItem(String source, long silenceTime, ArrayList<String> params) {
80             mDuration = silenceTime;
81             mParams = params;
82             mType = SILENCE;
83             mCallingApp = source;
84         }
85 
SpeechItem(String source, String text, ArrayList<String> params, int itemType, String filename)86         public SpeechItem(String source, String text, ArrayList<String> params,
87                 int itemType, String filename) {
88             mText = text;
89             mParams = params;
90             mType = itemType;
91             mFilename = filename;
92             mCallingApp = source;
93         }
94 
95     }
96 
97     /**
98      * Contains the information needed to access a sound resource; the name of
99      * the package that contains the resource and the resID of the resource
100      * within that package.
101      */
102     private static class SoundResource {
103         public String mSourcePackageName = null;
104         public int mResId = -1;
105         public String mFilename = null;
106 
SoundResource(String packageName, int id)107         public SoundResource(String packageName, int id) {
108             mSourcePackageName = packageName;
109             mResId = id;
110             mFilename = null;
111         }
112 
SoundResource(String file)113         public SoundResource(String file) {
114             mSourcePackageName = null;
115             mResId = -1;
116             mFilename = file;
117         }
118     }
119     // If the speech queue is locked for more than 5 seconds, something has gone
120     // very wrong with processSpeechQueue.
121     private static final int SPEECHQUEUELOCK_TIMEOUT = 5000;
122     private static final int MAX_SPEECH_ITEM_CHAR_LENGTH = 4000;
123     private static final int MAX_FILENAME_LENGTH = 250;
124     // TODO use the TTS stream type when available
125     private static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_MUSIC;
126     // TODO use TextToSpeech.DEFAULT_SYNTH once it is unhidden
127     private static final String DEFAULT_SYNTH = "com.svox.pico";
128     private static final String ACTION = "android.intent.action.START_TTS_SERVICE";
129     private static final String CATEGORY = "android.intent.category.TTS";
130     private static final String PKGNAME = "android.tts";
131     protected static final String SERVICE_TAG = "TtsService";
132 
133     private final RemoteCallbackList<ITtsCallback> mCallbacks
134             = new RemoteCallbackList<ITtsCallback>();
135 
136     private HashMap<String, ITtsCallback> mCallbacksMap;
137 
138     private Boolean mIsSpeaking;
139     private Boolean mSynthBusy;
140     private ArrayList<SpeechItem> mSpeechQueue;
141     private HashMap<String, SoundResource> mEarcons;
142     private HashMap<String, SoundResource> mUtterances;
143     private MediaPlayer mPlayer;
144     private SpeechItem mCurrentSpeechItem;
145     private HashMap<SpeechItem, Boolean> mKillList; // Used to ensure that in-flight synth calls
146                                                     // are killed when stop is used.
147     private TtsService mSelf;
148 
149     private ContentResolver mResolver;
150 
151     // lock for the speech queue (mSpeechQueue) and the current speech item (mCurrentSpeechItem)
152     private final ReentrantLock speechQueueLock = new ReentrantLock();
153     private final ReentrantLock synthesizerLock = new ReentrantLock();
154 
155     private static SynthProxy sNativeSynth = null;
156     private String currentSpeechEngineSOFile = "";
157 
158     @Override
onCreate()159     public void onCreate() {
160         super.onCreate();
161         Log.v("TtsService", "TtsService.onCreate()");
162 
163         mResolver = getContentResolver();
164 
165         currentSpeechEngineSOFile = "";
166         setEngine(getDefaultEngine());
167 
168         mSelf = this;
169         mIsSpeaking = false;
170         mSynthBusy = false;
171 
172         mEarcons = new HashMap<String, SoundResource>();
173         mUtterances = new HashMap<String, SoundResource>();
174         mCallbacksMap = new HashMap<String, android.speech.tts.ITtsCallback>();
175 
176         mSpeechQueue = new ArrayList<SpeechItem>();
177         mPlayer = null;
178         mCurrentSpeechItem = null;
179         mKillList = new HashMap<SpeechItem, Boolean>();
180 
181         setDefaultSettings();
182     }
183 
184     @Override
onDestroy()185     public void onDestroy() {
186         super.onDestroy();
187 
188         killAllUtterances();
189 
190         // Don't hog the media player
191         cleanUpPlayer();
192 
193         if (sNativeSynth != null) {
194             sNativeSynth.shutdown();
195         }
196         sNativeSynth = null;
197 
198         // Unregister all callbacks.
199         mCallbacks.kill();
200 
201         Log.v(SERVICE_TAG, "onDestroy() completed");
202     }
203 
204 
setEngine(String enginePackageName)205     private int setEngine(String enginePackageName) {
206         String soFilename = "";
207         if (isDefaultEnforced()) {
208             enginePackageName = getDefaultEngine();
209         }
210 
211         // Make sure that the engine has been allowed by the user
212         if (!enginePackageName.equals(DEFAULT_SYNTH)) {
213             String[] enabledEngines = android.provider.Settings.Secure.getString(mResolver,
214                     android.provider.Settings.Secure.TTS_ENABLED_PLUGINS).split(" ");
215             boolean isEnabled = false;
216             for (int i=0; i<enabledEngines.length; i++) {
217                 if (enabledEngines[i].equals(enginePackageName)) {
218                     isEnabled = true;
219                     break;
220                 }
221             }
222             if (!isEnabled) {
223                 // Do not use an engine that the user has not enabled; fall back
224                 // to using the default synthesizer.
225                 enginePackageName = DEFAULT_SYNTH;
226             }
227         }
228 
229         // The SVOX TTS is an exception to how the TTS packaging scheme works
230         // because it is part of the system and not a 3rd party add-on; thus
231         // its binary is actually located under /system/lib/
232         if (enginePackageName.equals(DEFAULT_SYNTH)) {
233             soFilename = "/system/lib/libttspico.so";
234         } else {
235             // Find the package
236             Intent intent = new Intent("android.intent.action.START_TTS_ENGINE");
237             intent.setPackage(enginePackageName);
238             ResolveInfo[] enginesArray = new ResolveInfo[0];
239             PackageManager pm = getPackageManager();
240             List <ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, 0);
241             if ((resolveInfos == null) || resolveInfos.isEmpty()) {
242                 Log.e(SERVICE_TAG, "Invalid TTS Engine Package: " + enginePackageName);
243                 return TextToSpeech.ERROR;
244             }
245             enginesArray = resolveInfos.toArray(enginesArray);
246             // Generate the TTS .so filename from the package
247             ActivityInfo aInfo = enginesArray[0].activityInfo;
248             soFilename = aInfo.name.replace(aInfo.packageName + ".", "") + ".so";
249             soFilename = soFilename.toLowerCase();
250             soFilename = "/data/data/" + aInfo.packageName + "/lib/libtts" + soFilename;
251         }
252 
253         if (currentSpeechEngineSOFile.equals(soFilename)) {
254             return TextToSpeech.SUCCESS;
255         }
256 
257         File f = new File(soFilename);
258         if (!f.exists()) {
259             Log.e(SERVICE_TAG, "Invalid TTS Binary: " + soFilename);
260             return TextToSpeech.ERROR;
261         }
262 
263         if (sNativeSynth != null) {
264             sNativeSynth.stopSync();
265             sNativeSynth.shutdown();
266             sNativeSynth = null;
267         }
268 
269         // Load the engineConfig from the plugin if it has any special configuration
270         // to be loaded. By convention, if an engine wants the TTS framework to pass
271         // in any configuration, it must put it into its content provider which has the URI:
272         // content://<packageName>.providers.SettingsProvider
273         // That content provider must provide a Cursor which returns the String that
274         // is to be passed back to the native .so file for the plugin when getString(0) is
275         // called on it.
276         // Note that the TTS framework does not care what this String data is: it is something
277         // that comes from the engine plugin and is consumed only by the engine plugin itself.
278         String engineConfig = "";
279         Cursor c = getContentResolver().query(Uri.parse("content://" + enginePackageName
280                 + ".providers.SettingsProvider"), null, null, null, null);
281         if (c != null){
282             c.moveToFirst();
283             engineConfig = c.getString(0);
284             c.close();
285         }
286         sNativeSynth = new SynthProxy(soFilename, engineConfig);
287         currentSpeechEngineSOFile = soFilename;
288         return TextToSpeech.SUCCESS;
289     }
290 
291 
292 
setDefaultSettings()293     private void setDefaultSettings() {
294         setLanguage("", this.getDefaultLanguage(), getDefaultCountry(), getDefaultLocVariant());
295 
296         // speech rate
297         setSpeechRate("", getDefaultRate());
298     }
299 
300 
isDefaultEnforced()301     private boolean isDefaultEnforced() {
302         return (android.provider.Settings.Secure.getInt(mResolver,
303                     android.provider.Settings.Secure.TTS_USE_DEFAULTS,
304                     TextToSpeech.Engine.USE_DEFAULTS)
305                 == 1 );
306     }
307 
getDefaultEngine()308     private String getDefaultEngine() {
309         String defaultEngine = android.provider.Settings.Secure.getString(mResolver,
310                 android.provider.Settings.Secure.TTS_DEFAULT_SYNTH);
311         if (defaultEngine == null) {
312             return TextToSpeech.Engine.DEFAULT_SYNTH;
313         } else {
314             return defaultEngine;
315         }
316     }
317 
getDefaultRate()318     private int getDefaultRate() {
319         return android.provider.Settings.Secure.getInt(mResolver,
320                 android.provider.Settings.Secure.TTS_DEFAULT_RATE,
321                 TextToSpeech.Engine.DEFAULT_RATE);
322     }
323 
getDefaultPitch()324     private int getDefaultPitch() {
325         // Pitch is not user settable; the default pitch is always 100.
326         return 100;
327     }
328 
getDefaultLanguage()329     private String getDefaultLanguage() {
330         String defaultLang = android.provider.Settings.Secure.getString(mResolver,
331                 android.provider.Settings.Secure.TTS_DEFAULT_LANG);
332         if (defaultLang == null) {
333             // no setting found, use the current Locale to determine the default language
334             return Locale.getDefault().getISO3Language();
335         } else {
336             return defaultLang;
337         }
338     }
339 
340 
getDefaultCountry()341     private String getDefaultCountry() {
342         String defaultCountry = android.provider.Settings.Secure.getString(mResolver,
343                 android.provider.Settings.Secure.TTS_DEFAULT_COUNTRY);
344         if (defaultCountry == null) {
345             // no setting found, use the current Locale to determine the default country
346             return Locale.getDefault().getISO3Country();
347         } else {
348             return defaultCountry;
349         }
350     }
351 
352 
getDefaultLocVariant()353     private String getDefaultLocVariant() {
354         String defaultVar = android.provider.Settings.Secure.getString(mResolver,
355                 android.provider.Settings.Secure.TTS_DEFAULT_VARIANT);
356         if (defaultVar == null) {
357             // no setting found, use the current Locale to determine the default variant
358             return Locale.getDefault().getVariant();
359         } else {
360             return defaultVar;
361         }
362     }
363 
364 
setSpeechRate(String callingApp, int rate)365     private int setSpeechRate(String callingApp, int rate) {
366         int res = TextToSpeech.ERROR;
367         try {
368             if (isDefaultEnforced()) {
369                 res = sNativeSynth.setSpeechRate(getDefaultRate());
370             } else {
371                 res = sNativeSynth.setSpeechRate(rate);
372             }
373         } catch (NullPointerException e) {
374             // synth will become null during onDestroy()
375             res = TextToSpeech.ERROR;
376         }
377         return res;
378     }
379 
380 
setPitch(String callingApp, int pitch)381     private int setPitch(String callingApp, int pitch) {
382         int res = TextToSpeech.ERROR;
383         try {
384             res = sNativeSynth.setPitch(pitch);
385         } catch (NullPointerException e) {
386             // synth will become null during onDestroy()
387             res = TextToSpeech.ERROR;
388         }
389         return res;
390     }
391 
392 
isLanguageAvailable(String lang, String country, String variant)393     private int isLanguageAvailable(String lang, String country, String variant) {
394         int res = TextToSpeech.LANG_NOT_SUPPORTED;
395         try {
396             res = sNativeSynth.isLanguageAvailable(lang, country, variant);
397         } catch (NullPointerException e) {
398             // synth will become null during onDestroy()
399             res = TextToSpeech.LANG_NOT_SUPPORTED;
400         }
401         return res;
402     }
403 
404 
getLanguage()405     private String[] getLanguage() {
406         try {
407             return sNativeSynth.getLanguage();
408         } catch (Exception e) {
409             return null;
410         }
411     }
412 
413 
setLanguage(String callingApp, String lang, String country, String variant)414     private int setLanguage(String callingApp, String lang, String country, String variant) {
415         Log.v(SERVICE_TAG, "TtsService.setLanguage(" + lang + ", " + country + ", " + variant + ")");
416         int res = TextToSpeech.ERROR;
417         try {
418             if (isDefaultEnforced()) {
419                 res = sNativeSynth.setLanguage(getDefaultLanguage(), getDefaultCountry(),
420                         getDefaultLocVariant());
421             } else {
422                 res = sNativeSynth.setLanguage(lang, country, variant);
423             }
424         } catch (NullPointerException e) {
425             // synth will become null during onDestroy()
426             res = TextToSpeech.ERROR;
427         }
428         return res;
429     }
430 
431 
432     /**
433      * Adds a sound resource to the TTS.
434      *
435      * @param text
436      *            The text that should be associated with the sound resource
437      * @param packageName
438      *            The name of the package which has the sound resource
439      * @param resId
440      *            The resource ID of the sound within its package
441      */
addSpeech(String callingApp, String text, String packageName, int resId)442     private void addSpeech(String callingApp, String text, String packageName, int resId) {
443         mUtterances.put(text, new SoundResource(packageName, resId));
444     }
445 
446     /**
447      * Adds a sound resource to the TTS.
448      *
449      * @param text
450      *            The text that should be associated with the sound resource
451      * @param filename
452      *            The filename of the sound resource. This must be a complete
453      *            path like: (/sdcard/mysounds/mysoundbite.mp3).
454      */
addSpeech(String callingApp, String text, String filename)455     private void addSpeech(String callingApp, String text, String filename) {
456         mUtterances.put(text, new SoundResource(filename));
457     }
458 
459     /**
460      * Adds a sound resource to the TTS as an earcon.
461      *
462      * @param earcon
463      *            The text that should be associated with the sound resource
464      * @param packageName
465      *            The name of the package which has the sound resource
466      * @param resId
467      *            The resource ID of the sound within its package
468      */
addEarcon(String callingApp, String earcon, String packageName, int resId)469     private void addEarcon(String callingApp, String earcon, String packageName, int resId) {
470         mEarcons.put(earcon, new SoundResource(packageName, resId));
471     }
472 
473     /**
474      * Adds a sound resource to the TTS as an earcon.
475      *
476      * @param earcon
477      *            The text that should be associated with the sound resource
478      * @param filename
479      *            The filename of the sound resource. This must be a complete
480      *            path like: (/sdcard/mysounds/mysoundbite.mp3).
481      */
addEarcon(String callingApp, String earcon, String filename)482     private void addEarcon(String callingApp, String earcon, String filename) {
483         mEarcons.put(earcon, new SoundResource(filename));
484     }
485 
486     /**
487      * Speaks the given text using the specified queueing mode and parameters.
488      *
489      * @param text
490      *            The text that should be spoken
491      * @param queueMode
492      *            TextToSpeech.TTS_QUEUE_FLUSH for no queue (interrupts all previous utterances),
493      *            TextToSpeech.TTS_QUEUE_ADD for queued
494      * @param params
495      *            An ArrayList of parameters. This is not implemented for all
496      *            engines.
497      */
speak(String callingApp, String text, int queueMode, ArrayList<String> params)498     private int speak(String callingApp, String text, int queueMode, ArrayList<String> params) {
499         Log.v(SERVICE_TAG, "TTS service received " + text);
500         if (queueMode == TextToSpeech.QUEUE_FLUSH) {
501             stop(callingApp);
502         } else if (queueMode == 2) {
503             stopAll(callingApp);
504         }
505         mSpeechQueue.add(new SpeechItem(callingApp, text, params, SpeechItem.TEXT));
506         if (!mIsSpeaking) {
507             processSpeechQueue();
508         }
509         return TextToSpeech.SUCCESS;
510     }
511 
512     /**
513      * Plays the earcon using the specified queueing mode and parameters.
514      *
515      * @param earcon
516      *            The earcon that should be played
517      * @param queueMode
518      *            TextToSpeech.TTS_QUEUE_FLUSH for no queue (interrupts all previous utterances),
519      *            TextToSpeech.TTS_QUEUE_ADD for queued
520      * @param params
521      *            An ArrayList of parameters. This is not implemented for all
522      *            engines.
523      */
playEarcon(String callingApp, String earcon, int queueMode, ArrayList<String> params)524     private int playEarcon(String callingApp, String earcon, int queueMode,
525             ArrayList<String> params) {
526         if (queueMode == TextToSpeech.QUEUE_FLUSH) {
527             stop(callingApp);
528         } else if (queueMode == 2) {
529             stopAll(callingApp);
530         }
531         mSpeechQueue.add(new SpeechItem(callingApp, earcon, params, SpeechItem.EARCON));
532         if (!mIsSpeaking) {
533             processSpeechQueue();
534         }
535         return TextToSpeech.SUCCESS;
536     }
537 
538     /**
539      * Stops all speech output and removes any utterances still in the queue for the calling app.
540      */
stop(String callingApp)541     private int stop(String callingApp) {
542         int result = TextToSpeech.ERROR;
543         boolean speechQueueAvailable = false;
544         try{
545             speechQueueAvailable =
546                     speechQueueLock.tryLock(SPEECHQUEUELOCK_TIMEOUT, TimeUnit.MILLISECONDS);
547             if (speechQueueAvailable) {
548                 Log.i(SERVICE_TAG, "Stopping");
549                 for (int i = mSpeechQueue.size() - 1; i > -1; i--){
550                     if (mSpeechQueue.get(i).mCallingApp.equals(callingApp)){
551                         mSpeechQueue.remove(i);
552                     }
553                 }
554                 if ((mCurrentSpeechItem != null) &&
555                      mCurrentSpeechItem.mCallingApp.equals(callingApp)) {
556                     try {
557                         result = sNativeSynth.stop();
558                     } catch (NullPointerException e1) {
559                         // synth will become null during onDestroy()
560                         result = TextToSpeech.ERROR;
561                     }
562                     mKillList.put(mCurrentSpeechItem, true);
563                     if (mPlayer != null) {
564                         try {
565                             mPlayer.stop();
566                         } catch (IllegalStateException e) {
567                             // Do nothing, the player is already stopped.
568                         }
569                     }
570                     mIsSpeaking = false;
571                     mCurrentSpeechItem = null;
572                 } else {
573                     result = TextToSpeech.SUCCESS;
574                 }
575                 Log.i(SERVICE_TAG, "Stopped");
576             } else {
577                 Log.e(SERVICE_TAG, "TTS stop(): queue locked longer than expected");
578                 result = TextToSpeech.ERROR;
579             }
580         } catch (InterruptedException e) {
581           Log.e(SERVICE_TAG, "TTS stop: tryLock interrupted");
582           e.printStackTrace();
583         } finally {
584             // This check is needed because finally will always run; even if the
585             // method returns somewhere in the try block.
586             if (speechQueueAvailable) {
587                 speechQueueLock.unlock();
588             }
589             return result;
590         }
591     }
592 
593 
594     /**
595      * Stops all speech output, both rendered to a file and directly spoken, and removes any
596      * utterances still in the queue globally. Files that were being written are deleted.
597      */
598     @SuppressWarnings("finally")
killAllUtterances()599     private int killAllUtterances() {
600         int result = TextToSpeech.ERROR;
601         boolean speechQueueAvailable = false;
602 
603         try {
604             speechQueueAvailable = speechQueueLock.tryLock(SPEECHQUEUELOCK_TIMEOUT,
605                     TimeUnit.MILLISECONDS);
606             if (speechQueueAvailable) {
607                 // remove every single entry in the speech queue
608                 mSpeechQueue.clear();
609 
610                 // clear the current speech item
611                 if (mCurrentSpeechItem != null) {
612                     result = sNativeSynth.stopSync();
613                     mKillList.put(mCurrentSpeechItem, true);
614                     mIsSpeaking = false;
615 
616                     // was the engine writing to a file?
617                     if (mCurrentSpeechItem.mType == SpeechItem.TEXT_TO_FILE) {
618                         // delete the file that was being written
619                         if (mCurrentSpeechItem.mFilename != null) {
620                             File tempFile = new File(mCurrentSpeechItem.mFilename);
621                             Log.v(SERVICE_TAG, "Leaving behind " + mCurrentSpeechItem.mFilename);
622                             if (tempFile.exists()) {
623                                 Log.v(SERVICE_TAG, "About to delete "
624                                         + mCurrentSpeechItem.mFilename);
625                                 if (tempFile.delete()) {
626                                     Log.v(SERVICE_TAG, "file successfully deleted");
627                                 }
628                             }
629                         }
630                     }
631 
632                     mCurrentSpeechItem = null;
633                 }
634             } else {
635                 Log.e(SERVICE_TAG, "TTS killAllUtterances(): queue locked longer than expected");
636                 result = TextToSpeech.ERROR;
637             }
638         } catch (InterruptedException e) {
639             Log.e(SERVICE_TAG, "TTS killAllUtterances(): tryLock interrupted");
640             result = TextToSpeech.ERROR;
641         } finally {
642             // This check is needed because finally will always run, even if the
643             // method returns somewhere in the try block.
644             if (speechQueueAvailable) {
645                 speechQueueLock.unlock();
646             }
647             return result;
648         }
649     }
650 
651 
652     /**
653      * Stops all speech output and removes any utterances still in the queue globally, except
654      * those intended to be synthesized to file.
655      */
stopAll(String callingApp)656     private int stopAll(String callingApp) {
657         int result = TextToSpeech.ERROR;
658         boolean speechQueueAvailable = false;
659         try{
660             speechQueueAvailable =
661                     speechQueueLock.tryLock(SPEECHQUEUELOCK_TIMEOUT, TimeUnit.MILLISECONDS);
662             if (speechQueueAvailable) {
663                 for (int i = mSpeechQueue.size() - 1; i > -1; i--){
664                     if (mSpeechQueue.get(i).mType != SpeechItem.TEXT_TO_FILE){
665                         mSpeechQueue.remove(i);
666                     }
667                 }
668                 if ((mCurrentSpeechItem != null) &&
669                     ((mCurrentSpeechItem.mType != SpeechItem.TEXT_TO_FILE) ||
670                       mCurrentSpeechItem.mCallingApp.equals(callingApp))) {
671                     try {
672                         result = sNativeSynth.stop();
673                     } catch (NullPointerException e1) {
674                         // synth will become null during onDestroy()
675                         result = TextToSpeech.ERROR;
676                     }
677                     mKillList.put(mCurrentSpeechItem, true);
678                     if (mPlayer != null) {
679                         try {
680                             mPlayer.stop();
681                         } catch (IllegalStateException e) {
682                             // Do nothing, the player is already stopped.
683                         }
684                     }
685                     mIsSpeaking = false;
686                     mCurrentSpeechItem = null;
687                 } else {
688                     result = TextToSpeech.SUCCESS;
689                 }
690                 Log.i(SERVICE_TAG, "Stopped all");
691             } else {
692                 Log.e(SERVICE_TAG, "TTS stopAll(): queue locked longer than expected");
693                 result = TextToSpeech.ERROR;
694             }
695         } catch (InterruptedException e) {
696           Log.e(SERVICE_TAG, "TTS stopAll: tryLock interrupted");
697           e.printStackTrace();
698         } finally {
699             // This check is needed because finally will always run; even if the
700             // method returns somewhere in the try block.
701             if (speechQueueAvailable) {
702                 speechQueueLock.unlock();
703             }
704             return result;
705         }
706     }
707 
onCompletion(MediaPlayer arg0)708     public void onCompletion(MediaPlayer arg0) {
709         // mCurrentSpeechItem may become null if it is stopped at the same
710         // time it completes.
711         SpeechItem currentSpeechItemCopy = mCurrentSpeechItem;
712         if (currentSpeechItemCopy != null) {
713             String callingApp = currentSpeechItemCopy.mCallingApp;
714             ArrayList<String> params = currentSpeechItemCopy.mParams;
715             String utteranceId = "";
716             if (params != null) {
717                 for (int i = 0; i < params.size() - 1; i = i + 2) {
718                     String param = params.get(i);
719                     if (param.equals(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID)) {
720                         utteranceId = params.get(i + 1);
721                     }
722                 }
723             }
724             if (utteranceId.length() > 0) {
725                 dispatchUtteranceCompletedCallback(utteranceId, callingApp);
726             }
727         }
728         processSpeechQueue();
729     }
730 
playSilence(String callingApp, long duration, int queueMode, ArrayList<String> params)731     private int playSilence(String callingApp, long duration, int queueMode,
732             ArrayList<String> params) {
733         if (queueMode == TextToSpeech.QUEUE_FLUSH) {
734             stop(callingApp);
735         }
736         mSpeechQueue.add(new SpeechItem(callingApp, duration, params));
737         if (!mIsSpeaking) {
738             processSpeechQueue();
739         }
740         return TextToSpeech.SUCCESS;
741     }
742 
silence(final SpeechItem speechItem)743     private void silence(final SpeechItem speechItem) {
744         class SilenceThread implements Runnable {
745             public void run() {
746                 String utteranceId = "";
747                 if (speechItem.mParams != null){
748                     for (int i = 0; i < speechItem.mParams.size() - 1; i = i + 2){
749                         String param = speechItem.mParams.get(i);
750                         if (param.equals(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID)){
751                             utteranceId = speechItem.mParams.get(i+1);
752                         }
753                     }
754                 }
755                 try {
756                     Thread.sleep(speechItem.mDuration);
757                 } catch (InterruptedException e) {
758                     e.printStackTrace();
759                 } finally {
760                     if (utteranceId.length() > 0){
761                         dispatchUtteranceCompletedCallback(utteranceId, speechItem.mCallingApp);
762                     }
763                     processSpeechQueue();
764                 }
765             }
766         }
767         Thread slnc = (new Thread(new SilenceThread()));
768         slnc.setPriority(Thread.MIN_PRIORITY);
769         slnc.start();
770     }
771 
speakInternalOnly(final SpeechItem speechItem)772     private void speakInternalOnly(final SpeechItem speechItem) {
773         class SynthThread implements Runnable {
774             public void run() {
775                 boolean synthAvailable = false;
776                 String utteranceId = "";
777                 try {
778                     synthAvailable = synthesizerLock.tryLock();
779                     if (!synthAvailable) {
780                         mSynthBusy = true;
781                         Thread.sleep(100);
782                         Thread synth = (new Thread(new SynthThread()));
783                         synth.start();
784                         mSynthBusy = false;
785                         return;
786                     }
787                     int streamType = DEFAULT_STREAM_TYPE;
788                     String language = "";
789                     String country = "";
790                     String variant = "";
791                     String speechRate = "";
792                     String engine = "";
793                     String pitch = "";
794                     if (speechItem.mParams != null){
795                         for (int i = 0; i < speechItem.mParams.size() - 1; i = i + 2){
796                             String param = speechItem.mParams.get(i);
797                             if (param != null) {
798                                 if (param.equals(TextToSpeech.Engine.KEY_PARAM_RATE)) {
799                                     speechRate = speechItem.mParams.get(i+1);
800                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_LANGUAGE)){
801                                     language = speechItem.mParams.get(i+1);
802                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_COUNTRY)){
803                                     country = speechItem.mParams.get(i+1);
804                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_VARIANT)){
805                                     variant = speechItem.mParams.get(i+1);
806                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID)){
807                                     utteranceId = speechItem.mParams.get(i+1);
808                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_STREAM)) {
809                                     try {
810                                         streamType
811                                                 = Integer.parseInt(speechItem.mParams.get(i + 1));
812                                     } catch (NumberFormatException e) {
813                                         streamType = DEFAULT_STREAM_TYPE;
814                                     }
815                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_ENGINE)) {
816                                     engine = speechItem.mParams.get(i + 1);
817                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_PITCH)) {
818                                     pitch = speechItem.mParams.get(i + 1);
819                                 }
820                             }
821                         }
822                     }
823                     // Only do the synthesis if it has not been killed by a subsequent utterance.
824                     if (mKillList.get(speechItem) == null) {
825                         if (engine.length() > 0) {
826                             setEngine(engine);
827                         } else {
828                             setEngine(getDefaultEngine());
829                         }
830                         if (language.length() > 0){
831                             setLanguage("", language, country, variant);
832                         } else {
833                             setLanguage("", getDefaultLanguage(), getDefaultCountry(),
834                                     getDefaultLocVariant());
835                         }
836                         if (speechRate.length() > 0){
837                             setSpeechRate("", Integer.parseInt(speechRate));
838                         } else {
839                             setSpeechRate("", getDefaultRate());
840                         }
841                         if (pitch.length() > 0){
842                             setPitch("", Integer.parseInt(pitch));
843                         } else {
844                             setPitch("", getDefaultPitch());
845                         }
846                         try {
847                             sNativeSynth.speak(speechItem.mText, streamType);
848                         } catch (NullPointerException e) {
849                             // synth will become null during onDestroy()
850                             Log.v(SERVICE_TAG, " null synth, can't speak");
851                         }
852                     }
853                 } catch (InterruptedException e) {
854                     Log.e(SERVICE_TAG, "TTS speakInternalOnly(): tryLock interrupted");
855                     e.printStackTrace();
856                 } finally {
857                     // This check is needed because finally will always run;
858                     // even if the
859                     // method returns somewhere in the try block.
860                     if (utteranceId.length() > 0){
861                         dispatchUtteranceCompletedCallback(utteranceId, speechItem.mCallingApp);
862                     }
863                     if (synthAvailable) {
864                         synthesizerLock.unlock();
865                         processSpeechQueue();
866                     }
867                 }
868             }
869         }
870         Thread synth = (new Thread(new SynthThread()));
871         synth.setPriority(Thread.MAX_PRIORITY);
872         synth.start();
873     }
874 
synthToFileInternalOnly(final SpeechItem speechItem)875     private void synthToFileInternalOnly(final SpeechItem speechItem) {
876         class SynthThread implements Runnable {
877             public void run() {
878                 boolean synthAvailable = false;
879                 String utteranceId = "";
880                 Log.i(SERVICE_TAG, "Synthesizing to " + speechItem.mFilename);
881                 try {
882                     synthAvailable = synthesizerLock.tryLock();
883                     if (!synthAvailable) {
884                         synchronized (this) {
885                             mSynthBusy = true;
886                         }
887                         Thread.sleep(100);
888                         Thread synth = (new Thread(new SynthThread()));
889                         synth.start();
890                         synchronized (this) {
891                             mSynthBusy = false;
892                         }
893                         return;
894                     }
895                     String language = "";
896                     String country = "";
897                     String variant = "";
898                     String speechRate = "";
899                     String engine = "";
900                     String pitch = "";
901                     if (speechItem.mParams != null){
902                         for (int i = 0; i < speechItem.mParams.size() - 1; i = i + 2){
903                             String param = speechItem.mParams.get(i);
904                             if (param != null) {
905                                 if (param.equals(TextToSpeech.Engine.KEY_PARAM_RATE)) {
906                                     speechRate = speechItem.mParams.get(i+1);
907                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_LANGUAGE)){
908                                     language = speechItem.mParams.get(i+1);
909                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_COUNTRY)){
910                                     country = speechItem.mParams.get(i+1);
911                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_VARIANT)){
912                                     variant = speechItem.mParams.get(i+1);
913                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID)){
914                                     utteranceId = speechItem.mParams.get(i+1);
915                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_ENGINE)) {
916                                     engine = speechItem.mParams.get(i + 1);
917                                 } else if (param.equals(TextToSpeech.Engine.KEY_PARAM_PITCH)) {
918                                     pitch = speechItem.mParams.get(i + 1);
919                                 }
920                             }
921                         }
922                     }
923                     // Only do the synthesis if it has not been killed by a subsequent utterance.
924                     if (mKillList.get(speechItem) == null){
925                         if (engine.length() > 0) {
926                             setEngine(engine);
927                         } else {
928                             setEngine(getDefaultEngine());
929                         }
930                         if (language.length() > 0){
931                             setLanguage("", language, country, variant);
932                         } else {
933                             setLanguage("", getDefaultLanguage(), getDefaultCountry(),
934                                     getDefaultLocVariant());
935                         }
936                         if (speechRate.length() > 0){
937                             setSpeechRate("", Integer.parseInt(speechRate));
938                         } else {
939                             setSpeechRate("", getDefaultRate());
940                         }
941                         if (pitch.length() > 0){
942                             setPitch("", Integer.parseInt(pitch));
943                         } else {
944                             setPitch("", getDefaultPitch());
945                         }
946                         try {
947                             sNativeSynth.synthesizeToFile(speechItem.mText, speechItem.mFilename);
948                         } catch (NullPointerException e) {
949                             // synth will become null during onDestroy()
950                             Log.v(SERVICE_TAG, " null synth, can't synthesize to file");
951                         }
952                     }
953                 } catch (InterruptedException e) {
954                     Log.e(SERVICE_TAG, "TTS synthToFileInternalOnly(): tryLock interrupted");
955                     e.printStackTrace();
956                 } finally {
957                     // This check is needed because finally will always run;
958                     // even if the
959                     // method returns somewhere in the try block.
960                     if (utteranceId.length() > 0){
961                         dispatchUtteranceCompletedCallback(utteranceId, speechItem.mCallingApp);
962                     }
963                     if (synthAvailable) {
964                         synthesizerLock.unlock();
965                         processSpeechQueue();
966                     }
967                 }
968             }
969         }
970         Thread synth = (new Thread(new SynthThread()));
971         synth.setPriority(Thread.MAX_PRIORITY);
972         synth.start();
973     }
974 
getSoundResource(SpeechItem speechItem)975     private SoundResource getSoundResource(SpeechItem speechItem) {
976         SoundResource sr = null;
977         String text = speechItem.mText;
978         if (speechItem.mType == SpeechItem.SILENCE) {
979             // Do nothing if this is just silence
980         } else if (speechItem.mType == SpeechItem.EARCON) {
981             sr = mEarcons.get(text);
982         } else {
983             sr = mUtterances.get(text);
984         }
985         return sr;
986     }
987 
broadcastTtsQueueProcessingCompleted()988     private void broadcastTtsQueueProcessingCompleted(){
989         Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED);
990         sendBroadcast(i);
991     }
992 
993 
dispatchUtteranceCompletedCallback(String utteranceId, String packageName)994     private void dispatchUtteranceCompletedCallback(String utteranceId, String packageName) {
995         ITtsCallback cb = mCallbacksMap.get(packageName);
996         if (cb == null){
997             return;
998         }
999         Log.v(SERVICE_TAG, "TTS callback: dispatch started");
1000         // Broadcast to all clients the new value.
1001         final int N = mCallbacks.beginBroadcast();
1002         try {
1003             cb.utteranceCompleted(utteranceId);
1004         } catch (RemoteException e) {
1005             // The RemoteCallbackList will take care of removing
1006             // the dead object for us.
1007         }
1008         mCallbacks.finishBroadcast();
1009         Log.v(SERVICE_TAG, "TTS callback: dispatch completed to " + N);
1010     }
1011 
splitCurrentTextIfNeeded(SpeechItem currentSpeechItem)1012     private SpeechItem splitCurrentTextIfNeeded(SpeechItem currentSpeechItem){
1013         if (currentSpeechItem.mText.length() < MAX_SPEECH_ITEM_CHAR_LENGTH){
1014             return currentSpeechItem;
1015         } else {
1016             String callingApp = currentSpeechItem.mCallingApp;
1017             ArrayList<SpeechItem> splitItems = new ArrayList<SpeechItem>();
1018             int start = 0;
1019             int end = start + MAX_SPEECH_ITEM_CHAR_LENGTH - 1;
1020             String splitText;
1021             SpeechItem splitItem;
1022             while (end < currentSpeechItem.mText.length()){
1023                 splitText = currentSpeechItem.mText.substring(start, end);
1024                 splitItem = new SpeechItem(callingApp, splitText, null, SpeechItem.TEXT);
1025                 splitItems.add(splitItem);
1026                 start = end;
1027                 end = start + MAX_SPEECH_ITEM_CHAR_LENGTH - 1;
1028             }
1029             splitText = currentSpeechItem.mText.substring(start);
1030             splitItem = new SpeechItem(callingApp, splitText, null, SpeechItem.TEXT);
1031             splitItems.add(splitItem);
1032             mSpeechQueue.remove(0);
1033             for (int i = splitItems.size() - 1; i >= 0; i--){
1034                 mSpeechQueue.add(0, splitItems.get(i));
1035             }
1036             return mSpeechQueue.get(0);
1037         }
1038     }
1039 
processSpeechQueue()1040     private void processSpeechQueue() {
1041         boolean speechQueueAvailable = false;
1042         synchronized (this) {
1043             if (mSynthBusy){
1044                 // There is already a synth thread waiting to run.
1045                 return;
1046             }
1047         }
1048         try {
1049             speechQueueAvailable =
1050                     speechQueueLock.tryLock(SPEECHQUEUELOCK_TIMEOUT, TimeUnit.MILLISECONDS);
1051             if (!speechQueueAvailable) {
1052                 Log.e(SERVICE_TAG, "processSpeechQueue - Speech queue is unavailable.");
1053                 return;
1054             }
1055             if (mSpeechQueue.size() < 1) {
1056                 mIsSpeaking = false;
1057                 mKillList.clear();
1058                 broadcastTtsQueueProcessingCompleted();
1059                 return;
1060             }
1061 
1062             mCurrentSpeechItem = mSpeechQueue.get(0);
1063             mIsSpeaking = true;
1064             SoundResource sr = getSoundResource(mCurrentSpeechItem);
1065             // Synth speech as needed - synthesizer should call
1066             // processSpeechQueue to continue running the queue
1067             Log.v(SERVICE_TAG, "TTS processing: " + mCurrentSpeechItem.mText);
1068             if (sr == null) {
1069                 if (mCurrentSpeechItem.mType == SpeechItem.TEXT) {
1070                     mCurrentSpeechItem = splitCurrentTextIfNeeded(mCurrentSpeechItem);
1071                     speakInternalOnly(mCurrentSpeechItem);
1072                 } else if (mCurrentSpeechItem.mType == SpeechItem.TEXT_TO_FILE) {
1073                     synthToFileInternalOnly(mCurrentSpeechItem);
1074                 } else {
1075                     // This is either silence or an earcon that was missing
1076                     silence(mCurrentSpeechItem);
1077                 }
1078             } else {
1079                 cleanUpPlayer();
1080                 if (sr.mSourcePackageName == PKGNAME) {
1081                     // Utterance is part of the TTS library
1082                     mPlayer = MediaPlayer.create(this, sr.mResId);
1083                 } else if (sr.mSourcePackageName != null) {
1084                     // Utterance is part of the app calling the library
1085                     Context ctx;
1086                     try {
1087                         ctx = this.createPackageContext(sr.mSourcePackageName, 0);
1088                     } catch (NameNotFoundException e) {
1089                         e.printStackTrace();
1090                         mSpeechQueue.remove(0); // Remove it from the queue and
1091                         // move on
1092                         mIsSpeaking = false;
1093                         return;
1094                     }
1095                     mPlayer = MediaPlayer.create(ctx, sr.mResId);
1096                 } else {
1097                     // Utterance is coming from a file
1098                     mPlayer = MediaPlayer.create(this, Uri.parse(sr.mFilename));
1099                 }
1100 
1101                 // Check if Media Server is dead; if it is, clear the queue and
1102                 // give up for now - hopefully, it will recover itself.
1103                 if (mPlayer == null) {
1104                     mSpeechQueue.clear();
1105                     mIsSpeaking = false;
1106                     return;
1107                 }
1108                 mPlayer.setOnCompletionListener(this);
1109                 try {
1110                     mPlayer.setAudioStreamType(getStreamTypeFromParams(mCurrentSpeechItem.mParams));
1111                     mPlayer.start();
1112                 } catch (IllegalStateException e) {
1113                     mSpeechQueue.clear();
1114                     mIsSpeaking = false;
1115                     cleanUpPlayer();
1116                     return;
1117                 }
1118             }
1119             if (mSpeechQueue.size() > 0) {
1120                 mSpeechQueue.remove(0);
1121             }
1122         } catch (InterruptedException e) {
1123           Log.e(SERVICE_TAG, "TTS processSpeechQueue: tryLock interrupted");
1124           e.printStackTrace();
1125         } finally {
1126             // This check is needed because finally will always run; even if the
1127             // method returns somewhere in the try block.
1128             if (speechQueueAvailable) {
1129                 speechQueueLock.unlock();
1130             }
1131         }
1132     }
1133 
getStreamTypeFromParams(ArrayList<String> paramList)1134     private int getStreamTypeFromParams(ArrayList<String> paramList) {
1135         int streamType = DEFAULT_STREAM_TYPE;
1136         if (paramList == null) {
1137             return streamType;
1138         }
1139         for (int i = 0; i < paramList.size() - 1; i = i + 2) {
1140             String param = paramList.get(i);
1141             if ((param != null) && (param.equals(TextToSpeech.Engine.KEY_PARAM_STREAM))) {
1142                 try {
1143                     streamType = Integer.parseInt(paramList.get(i + 1));
1144                 } catch (NumberFormatException e) {
1145                     streamType = DEFAULT_STREAM_TYPE;
1146                 }
1147             }
1148         }
1149         return streamType;
1150     }
1151 
cleanUpPlayer()1152     private void cleanUpPlayer() {
1153         if (mPlayer != null) {
1154             mPlayer.release();
1155             mPlayer = null;
1156         }
1157     }
1158 
1159     /**
1160      * Synthesizes the given text to a file using the specified parameters.
1161      *
1162      * @param text
1163      *            The String of text that should be synthesized
1164      * @param params
1165      *            An ArrayList of parameters. The first element of this array
1166      *            controls the type of voice to use.
1167      * @param filename
1168      *            The string that gives the full output filename; it should be
1169      *            something like "/sdcard/myappsounds/mysound.wav".
1170      * @return A boolean that indicates if the synthesis can be started
1171      */
synthesizeToFile(String callingApp, String text, ArrayList<String> params, String filename)1172     private boolean synthesizeToFile(String callingApp, String text, ArrayList<String> params,
1173             String filename) {
1174         // Don't allow a filename that is too long
1175         if (filename.length() > MAX_FILENAME_LENGTH) {
1176             return false;
1177         }
1178         // Don't allow anything longer than the max text length; since this
1179         // is synthing to a file, don't even bother splitting it.
1180         if (text.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH){
1181             return false;
1182         }
1183         // Check that the output file can be created
1184         try {
1185             File tempFile = new File(filename);
1186             if (tempFile.exists()) {
1187                 Log.v("TtsService", "File " + filename + " exists, deleting.");
1188                 tempFile.delete();
1189             }
1190             if (!tempFile.createNewFile()) {
1191                 Log.e("TtsService", "Unable to synthesize to file: can't create " + filename);
1192                 return false;
1193             }
1194             tempFile.delete();
1195         } catch (IOException e) {
1196             Log.e("TtsService", "Can't create " + filename + " due to exception " + e);
1197             return false;
1198         }
1199         mSpeechQueue.add(new SpeechItem(callingApp, text, params, SpeechItem.TEXT_TO_FILE, filename));
1200         if (!mIsSpeaking) {
1201             processSpeechQueue();
1202         }
1203         return true;
1204     }
1205 
1206     @Override
onBind(Intent intent)1207     public IBinder onBind(Intent intent) {
1208         if (ACTION.equals(intent.getAction())) {
1209             for (String category : intent.getCategories()) {
1210                 if (category.equals(CATEGORY)) {
1211                     return mBinder;
1212                 }
1213             }
1214         }
1215         return null;
1216     }
1217 
1218     private final android.speech.tts.ITts.Stub mBinder = new Stub() {
1219 
1220         public int registerCallback(String packageName, ITtsCallback cb) {
1221             if (cb != null) {
1222                 mCallbacks.register(cb);
1223                 mCallbacksMap.put(packageName, cb);
1224                 return TextToSpeech.SUCCESS;
1225             }
1226             return TextToSpeech.ERROR;
1227         }
1228 
1229         public int unregisterCallback(String packageName, ITtsCallback cb) {
1230             if (cb != null) {
1231                 mCallbacksMap.remove(packageName);
1232                 mCallbacks.unregister(cb);
1233                 return TextToSpeech.SUCCESS;
1234             }
1235             return TextToSpeech.ERROR;
1236         }
1237 
1238         /**
1239          * Speaks the given text using the specified queueing mode and
1240          * parameters.
1241          *
1242          * @param text
1243          *            The text that should be spoken
1244          * @param queueMode
1245          *            TextToSpeech.TTS_QUEUE_FLUSH for no queue (interrupts all previous utterances)
1246          *            TextToSpeech.TTS_QUEUE_ADD for queued
1247          * @param params
1248          *            An ArrayList of parameters. The first element of this
1249          *            array controls the type of voice to use.
1250          */
1251         public int speak(String callingApp, String text, int queueMode, String[] params) {
1252             ArrayList<String> speakingParams = new ArrayList<String>();
1253             if (params != null) {
1254                 speakingParams = new ArrayList<String>(Arrays.asList(params));
1255             }
1256             return mSelf.speak(callingApp, text, queueMode, speakingParams);
1257         }
1258 
1259         /**
1260          * Plays the earcon using the specified queueing mode and parameters.
1261          *
1262          * @param earcon
1263          *            The earcon that should be played
1264          * @param queueMode
1265          *            TextToSpeech.TTS_QUEUE_FLUSH for no queue (interrupts all previous utterances)
1266          *            TextToSpeech.TTS_QUEUE_ADD for queued
1267          * @param params
1268          *            An ArrayList of parameters.
1269          */
1270         public int playEarcon(String callingApp, String earcon, int queueMode, String[] params) {
1271             ArrayList<String> speakingParams = new ArrayList<String>();
1272             if (params != null) {
1273                 speakingParams = new ArrayList<String>(Arrays.asList(params));
1274             }
1275             return mSelf.playEarcon(callingApp, earcon, queueMode, speakingParams);
1276         }
1277 
1278         /**
1279          * Plays the silence using the specified queueing mode and parameters.
1280          *
1281          * @param duration
1282          *            The duration of the silence that should be played
1283          * @param queueMode
1284          *            TextToSpeech.TTS_QUEUE_FLUSH for no queue (interrupts all previous utterances)
1285          *            TextToSpeech.TTS_QUEUE_ADD for queued
1286          * @param params
1287          *            An ArrayList of parameters.
1288          */
1289         public int playSilence(String callingApp, long duration, int queueMode, String[] params) {
1290             ArrayList<String> speakingParams = new ArrayList<String>();
1291             if (params != null) {
1292                 speakingParams = new ArrayList<String>(Arrays.asList(params));
1293             }
1294             return mSelf.playSilence(callingApp, duration, queueMode, speakingParams);
1295         }
1296 
1297         /**
1298          * Stops all speech output and removes any utterances still in the
1299          * queue.
1300          */
1301         public int stop(String callingApp) {
1302             return mSelf.stop(callingApp);
1303         }
1304 
1305         /**
1306          * Returns whether or not the TTS is speaking.
1307          *
1308          * @return Boolean to indicate whether or not the TTS is speaking
1309          */
1310         public boolean isSpeaking() {
1311             return (mSelf.mIsSpeaking && (mSpeechQueue.size() < 1));
1312         }
1313 
1314         /**
1315          * Adds a sound resource to the TTS.
1316          *
1317          * @param text
1318          *            The text that should be associated with the sound resource
1319          * @param packageName
1320          *            The name of the package which has the sound resource
1321          * @param resId
1322          *            The resource ID of the sound within its package
1323          */
1324         public void addSpeech(String callingApp, String text, String packageName, int resId) {
1325             mSelf.addSpeech(callingApp, text, packageName, resId);
1326         }
1327 
1328         /**
1329          * Adds a sound resource to the TTS.
1330          *
1331          * @param text
1332          *            The text that should be associated with the sound resource
1333          * @param filename
1334          *            The filename of the sound resource. This must be a
1335          *            complete path like: (/sdcard/mysounds/mysoundbite.mp3).
1336          */
1337         public void addSpeechFile(String callingApp, String text, String filename) {
1338             mSelf.addSpeech(callingApp, text, filename);
1339         }
1340 
1341         /**
1342          * Adds a sound resource to the TTS as an earcon.
1343          *
1344          * @param earcon
1345          *            The text that should be associated with the sound resource
1346          * @param packageName
1347          *            The name of the package which has the sound resource
1348          * @param resId
1349          *            The resource ID of the sound within its package
1350          */
1351         public void addEarcon(String callingApp, String earcon, String packageName, int resId) {
1352             mSelf.addEarcon(callingApp, earcon, packageName, resId);
1353         }
1354 
1355         /**
1356          * Adds a sound resource to the TTS as an earcon.
1357          *
1358          * @param earcon
1359          *            The text that should be associated with the sound resource
1360          * @param filename
1361          *            The filename of the sound resource. This must be a
1362          *            complete path like: (/sdcard/mysounds/mysoundbite.mp3).
1363          */
1364         public void addEarconFile(String callingApp, String earcon, String filename) {
1365             mSelf.addEarcon(callingApp, earcon, filename);
1366         }
1367 
1368         /**
1369          * Sets the speech rate for the TTS. Note that this will only have an
1370          * effect on synthesized speech; it will not affect pre-recorded speech.
1371          *
1372          * @param speechRate
1373          *            The speech rate that should be used
1374          */
1375         public int setSpeechRate(String callingApp, int speechRate) {
1376             return mSelf.setSpeechRate(callingApp, speechRate);
1377         }
1378 
1379         /**
1380          * Sets the pitch for the TTS. Note that this will only have an
1381          * effect on synthesized speech; it will not affect pre-recorded speech.
1382          *
1383          * @param pitch
1384          *            The pitch that should be used for the synthesized voice
1385          */
1386         public int setPitch(String callingApp, int pitch) {
1387             return mSelf.setPitch(callingApp, pitch);
1388         }
1389 
1390         /**
1391          * Returns the level of support for the specified language.
1392          *
1393          * @param lang  the three letter ISO language code.
1394          * @param country  the three letter ISO country code.
1395          * @param variant  the variant code associated with the country and language pair.
1396          * @return one of TTS_LANG_NOT_SUPPORTED, TTS_LANG_MISSING_DATA, TTS_LANG_AVAILABLE,
1397          *      TTS_LANG_COUNTRY_AVAILABLE, TTS_LANG_COUNTRY_VAR_AVAILABLE as defined in
1398          *      android.speech.tts.TextToSpeech.
1399          */
1400         public int isLanguageAvailable(String lang, String country, String variant,
1401                 String[] params) {
1402             for (int i = 0; i < params.length - 1; i = i + 2){
1403                 String param = params[i];
1404                 if (param != null) {
1405                     if (param.equals(TextToSpeech.Engine.KEY_PARAM_ENGINE)) {
1406                         mSelf.setEngine(params[i + 1]);
1407                         break;
1408                     }
1409                 }
1410             }
1411             return mSelf.isLanguageAvailable(lang, country, variant);
1412         }
1413 
1414         /**
1415          * Returns the currently set language / country / variant strings representing the
1416          * language used by the TTS engine.
1417          * @return null is no language is set, or an array of 3 string containing respectively
1418          *      the language, country and variant.
1419          */
1420         public String[] getLanguage() {
1421             return mSelf.getLanguage();
1422         }
1423 
1424         /**
1425          * Sets the speech rate for the TTS, which affects the synthesized voice.
1426          *
1427          * @param lang  the three letter ISO language code.
1428          * @param country  the three letter ISO country code.
1429          * @param variant  the variant code associated with the country and language pair.
1430          */
1431         public int setLanguage(String callingApp, String lang, String country, String variant) {
1432             return mSelf.setLanguage(callingApp, lang, country, variant);
1433         }
1434 
1435         /**
1436          * Synthesizes the given text to a file using the specified
1437          * parameters.
1438          *
1439          * @param text
1440          *            The String of text that should be synthesized
1441          * @param params
1442          *            An ArrayList of parameters. The first element of this
1443          *            array controls the type of voice to use.
1444          * @param filename
1445          *            The string that gives the full output filename; it should
1446          *            be something like "/sdcard/myappsounds/mysound.wav".
1447          * @return A boolean that indicates if the synthesis succeeded
1448          */
1449         public boolean synthesizeToFile(String callingApp, String text, String[] params,
1450                 String filename) {
1451             ArrayList<String> speakingParams = new ArrayList<String>();
1452             if (params != null) {
1453                 speakingParams = new ArrayList<String>(Arrays.asList(params));
1454             }
1455             return mSelf.synthesizeToFile(callingApp, text, speakingParams, filename);
1456         }
1457 
1458         /**
1459          * Sets the speech synthesis engine for the TTS by specifying its packagename
1460          *
1461          * @param packageName  the packageName of the speech synthesis engine (ie, "com.svox.pico")
1462          *
1463          * @return SUCCESS or ERROR as defined in android.speech.tts.TextToSpeech.
1464          */
1465         public int setEngineByPackageName(String packageName) {
1466             return mSelf.setEngine(packageName);
1467         }
1468 
1469         /**
1470          * Returns the packagename of the default speech synthesis engine.
1471          *
1472          * @return Packagename of the TTS engine that the user has chosen as their default.
1473          */
1474         public String getDefaultEngine() {
1475             return mSelf.getDefaultEngine();
1476         }
1477 
1478         /**
1479          * Returns whether or not the user is forcing their defaults to override the
1480          * Text-To-Speech settings set by applications.
1481          *
1482          * @return Whether or not defaults are enforced.
1483          */
1484         public boolean areDefaultsEnforced() {
1485             return mSelf.isDefaultEnforced();
1486         }
1487 
1488     };
1489 
1490 }
1491