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