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