TextToSpeechService.java revision 1b5637ee32c5d4e5d857fa86a1b1c1db23d027b7
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16package android.speech.tts;
17
18import android.app.Service;
19import android.content.Intent;
20import android.net.Uri;
21import android.os.Binder;
22import android.os.Bundle;
23import android.os.Handler;
24import android.os.HandlerThread;
25import android.os.IBinder;
26import android.os.Looper;
27import android.os.Message;
28import android.os.MessageQueue;
29import android.os.ParcelFileDescriptor;
30import android.os.RemoteCallbackList;
31import android.os.RemoteException;
32import android.provider.Settings;
33import android.speech.tts.TextToSpeech.Engine;
34import android.text.TextUtils;
35import android.util.Log;
36
37import java.io.FileOutputStream;
38import java.io.IOException;
39import java.util.ArrayList;
40import java.util.HashMap;
41import java.util.List;
42import java.util.Locale;
43import java.util.Map;
44import java.util.MissingResourceException;
45import java.util.Set;
46
47
48/**
49 * Abstract base class for TTS engine implementations. The following methods
50 * need to be implemented for V1 API ({@link TextToSpeech}) implementation.
51 * <ul>
52 * <li>{@link #onIsLanguageAvailable}</li>
53 * <li>{@link #onLoadLanguage}</li>
54 * <li>{@link #onGetLanguage}</li>
55 * <li>{@link #onSynthesizeText}</li>
56 * <li>{@link #onStop}</li>
57 * </ul>
58 * The first three deal primarily with language management, and are used to
59 * query the engine for it's support for a given language and indicate to it
60 * that requests in a given language are imminent.
61 *
62 * {@link #onSynthesizeText} is central to the engine implementation. The
63 * implementation should synthesize text as per the request parameters and
64 * return synthesized data via the supplied callback. This class and its helpers
65 * will then consume that data, which might mean queuing it for playback or writing
66 * it to a file or similar. All calls to this method will be on a single thread,
67 * which will be different from the main thread of the service. Synthesis must be
68 * synchronous which means the engine must NOT hold on to the callback or call any
69 * methods on it after the method returns.
70 *
71 * {@link #onStop} tells the engine that it should stop
72 * all ongoing synthesis, if any. Any pending data from the current synthesis
73 * will be discarded.
74 *
75 * {@link #onGetLanguage} is not required as of JELLYBEAN_MR2 (API 18) and later, it is only
76 * called on earlier versions of Android.
77 * <p>
78 * In order to fully support the V2 API ({@link TextToSpeechClient}),
79 * these methods must be implemented:
80 * <ul>
81 * <li>{@link #onSynthesizeTextV2}</li>
82 * <li>{@link #checkVoicesInfo}</li>
83 * <li>{@link #onVoicesInfoChange}</li>
84 * <li>{@link #implementsV2API}</li>
85 * </ul>
86 * In addition {@link #implementsV2API} has to return true.
87 * <p>
88 * If the service does not implement these methods and {@link #implementsV2API} returns false,
89 * then the V2 API will be provided by converting V2 requests ({@link #onSynthesizeTextV2})
90 * to V1 requests ({@link #onSynthesizeText}). On service setup, all of the available device
91 * locales will be fed to {@link #onIsLanguageAvailable} to check if they are supported.
92 * If they are, embedded and/or network voices will be created depending on the result of
93 * {@link #onGetFeaturesForLanguage}.
94 * <p>
95 * Note that a V2 service will still receive requests from V1 clients and has to implement all
96 * of the V1 API methods.
97 */
98public abstract class TextToSpeechService extends Service {
99
100    private static final boolean DBG = false;
101    private static final String TAG = "TextToSpeechService";
102
103    private static final String SYNTH_THREAD_NAME = "SynthThread";
104
105    private SynthHandler mSynthHandler;
106    // A thread and it's associated handler for playing back any audio
107    // associated with this TTS engine. Will handle all requests except synthesis
108    // to file requests, which occur on the synthesis thread.
109    private AudioPlaybackHandler mAudioPlaybackHandler;
110    private TtsEngines mEngineHelper;
111
112    private CallbackMap mCallbacks;
113    private String mPackageName;
114
115    private final Object mVoicesInfoLock = new Object();
116
117    private List<VoiceInfo> mVoicesInfoList;
118    private Map<String, VoiceInfo> mVoicesInfoLookup;
119
120    @Override
121    public void onCreate() {
122        if (DBG) Log.d(TAG, "onCreate()");
123        super.onCreate();
124
125        SynthThread synthThread = new SynthThread();
126        synthThread.start();
127        mSynthHandler = new SynthHandler(synthThread.getLooper());
128
129        mAudioPlaybackHandler = new AudioPlaybackHandler();
130        mAudioPlaybackHandler.start();
131
132        mEngineHelper = new TtsEngines(this);
133
134        mCallbacks = new CallbackMap();
135
136        mPackageName = getApplicationInfo().packageName;
137
138        String[] defaultLocale = getSettingsLocale();
139
140        // Load default language
141        onLoadLanguage(defaultLocale[0], defaultLocale[1], defaultLocale[2]);
142    }
143
144    @Override
145    public void onDestroy() {
146        if (DBG) Log.d(TAG, "onDestroy()");
147
148        // Tell the synthesizer to stop
149        mSynthHandler.quit();
150        // Tell the audio playback thread to stop.
151        mAudioPlaybackHandler.quit();
152        // Unregister all callbacks.
153        mCallbacks.kill();
154
155        super.onDestroy();
156    }
157
158    /**
159     * Checks whether the engine supports a given language.
160     *
161     * Can be called on multiple threads.
162     *
163     * Its return values HAVE to be consistent with onLoadLanguage.
164     *
165     * @param lang ISO-3 language code.
166     * @param country ISO-3 country code. May be empty or null.
167     * @param variant Language variant. May be empty or null.
168     * @return Code indicating the support status for the locale.
169     *         One of {@link TextToSpeech#LANG_AVAILABLE},
170     *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
171     *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
172     *         {@link TextToSpeech#LANG_MISSING_DATA}
173     *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
174     */
175    protected abstract int onIsLanguageAvailable(String lang, String country, String variant);
176
177    /**
178     * Returns the language, country and variant currently being used by the TTS engine.
179     *
180     * This method will be called only on Android 4.2 and before (API <= 17). In later versions
181     * this method is not called by the Android TTS framework.
182     *
183     * Can be called on multiple threads.
184     *
185     * @return A 3-element array, containing language (ISO 3-letter code),
186     *         country (ISO 3-letter code) and variant used by the engine.
187     *         The country and variant may be {@code ""}. If country is empty, then variant must
188     *         be empty too.
189     * @see Locale#getISO3Language()
190     * @see Locale#getISO3Country()
191     * @see Locale#getVariant()
192     */
193    protected abstract String[] onGetLanguage();
194
195    /**
196     * Notifies the engine that it should load a speech synthesis language. There is no guarantee
197     * that this method is always called before the language is used for synthesis. It is merely
198     * a hint to the engine that it will probably get some synthesis requests for this language
199     * at some point in the future.
200     *
201     * Can be called on multiple threads.
202     * In <= Android 4.2 (<= API 17) can be called on main and service binder threads.
203     * In > Android 4.2 (> API 17) can be called on main and synthesis threads.
204     *
205     * @param lang ISO-3 language code.
206     * @param country ISO-3 country code. May be empty or null.
207     * @param variant Language variant. May be empty or null.
208     * @return Code indicating the support status for the locale.
209     *         One of {@link TextToSpeech#LANG_AVAILABLE},
210     *         {@link TextToSpeech#LANG_COUNTRY_AVAILABLE},
211     *         {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE},
212     *         {@link TextToSpeech#LANG_MISSING_DATA}
213     *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
214     */
215    protected abstract int onLoadLanguage(String lang, String country, String variant);
216
217    /**
218     * Notifies the service that it should stop any in-progress speech synthesis.
219     * This method can be called even if no speech synthesis is currently in progress.
220     *
221     * Can be called on multiple threads, but not on the synthesis thread.
222     */
223    protected abstract void onStop();
224
225    /**
226     * Tells the service to synthesize speech from the given text. This method
227     * should block until the synthesis is finished. Used for requests from V1
228     * clients ({@link android.speech.tts.TextToSpeech}). Called on the synthesis
229     * thread.
230     *
231     * @param request The synthesis request.
232     * @param callback The callback that the engine must use to make data
233     *            available for playback or for writing to a file.
234     */
235    protected abstract void onSynthesizeText(SynthesisRequest request,
236            SynthesisCallback callback);
237
238    /**
239     * Check the available voices data and return an immutable list of the available voices.
240     * The output of this method will be passed to clients to allow them to configure synthesis
241     * requests.
242     *
243     * Can be called on multiple threads.
244     *
245     * The result of this method will be saved and served to all TTS clients. If a TTS service wants
246     * to update the set of available voices, it should call the {@link #forceVoicesInfoCheck()}
247     * method.
248     */
249    protected List<VoiceInfo> checkVoicesInfo() {
250        if (implementsV2API()) {
251            throw new IllegalStateException("For proper V2 API implementation this method has to" +
252                    "  be implemented");
253        }
254
255        // V2 to V1 interface adapter. This allows using V2 client interface on V1-only services.
256        Bundle defaultParams = new Bundle();
257        defaultParams.putFloat(TextToSpeechClient.Params.SPEECH_PITCH, 1.0f);
258        // Speech speed <= 0 makes it use a system wide setting
259        defaultParams.putFloat(TextToSpeechClient.Params.SPEECH_SPEED, 0.0f);
260
261        // Enumerate all locales and check if they are available
262        ArrayList<VoiceInfo> voicesInfo = new ArrayList<VoiceInfo>();
263        int id = 0;
264        for (Locale locale : Locale.getAvailableLocales()) {
265            int expectedStatus = TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE;
266            if (locale.getVariant().isEmpty()) {
267                if (locale.getCountry().isEmpty()) {
268                    expectedStatus = TextToSpeech.LANG_AVAILABLE;
269                } else {
270                    expectedStatus = TextToSpeech.LANG_COUNTRY_AVAILABLE;
271                }
272            }
273            try {
274                int localeStatus = onIsLanguageAvailable(locale.getISO3Language(),
275                        locale.getISO3Country(), locale.getVariant());
276                if (localeStatus != expectedStatus) {
277                    continue;
278                }
279            } catch (MissingResourceException e) {
280                // Ignore locale without iso 3 codes
281                continue;
282            }
283
284            Set<String> features = onGetFeaturesForLanguage(locale.getISO3Language(),
285                        locale.getISO3Country(), locale.getVariant());
286
287            VoiceInfo.Builder builder = new VoiceInfo.Builder();
288            builder.setLatency(VoiceInfo.LATENCY_NORMAL);
289            builder.setQuality(VoiceInfo.QUALITY_NORMAL);
290            builder.setLocale(locale);
291            builder.setParamsWithDefaults(defaultParams);
292
293            if (features == null || features.contains(
294                    TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS)) {
295                builder.setName(locale.toString() + "-embedded");
296                builder.setRequiresNetworkConnection(false);
297                voicesInfo.add(builder.build());
298            }
299
300            if (features != null && features.contains(
301                    TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS)) {
302                builder.setName(locale.toString() + "-network");
303                builder.setRequiresNetworkConnection(true);
304                voicesInfo.add(builder.build());
305            }
306        }
307
308        return voicesInfo;
309    }
310
311    /**
312     * Tells the synthesis thread that it should reload voice data.
313     * There's a high probability that the underlying set of available voice data has changed.
314     * Called only on the synthesis thread.
315     */
316    protected void onVoicesInfoChange() {
317
318    }
319
320    /**
321     * Tells the service to synthesize speech from the given text. This method
322     * should block until the synthesis is finished. Used for requests from V2
323     * client {@link android.speech.tts.TextToSpeechClient}. Called on the
324     * synthesis thread.
325     *
326     * @param request The synthesis request.
327     * @param callback The callback the the engine must use to make data
328     *            available for playback or for writing to a file.
329     */
330    protected void onSynthesizeTextV2(SynthesisRequestV2 request,
331            VoiceInfo selectedVoice,
332            SynthesisCallback callback) {
333        if (implementsV2API()) {
334            throw new IllegalStateException("For proper V2 API implementation this method has to" +
335                    "  be implemented");
336        }
337
338        // Convert to V1 params
339        int speechRate = (int) (request.getVoiceParams().getFloat(
340                TextToSpeechClient.Params.SPEECH_SPEED, 1.0f) * 100);
341        int speechPitch = (int) (request.getVoiceParams().getFloat(
342                TextToSpeechClient.Params.SPEECH_PITCH, 1.0f) * 100);
343
344        // Provide adapter to V1 API
345        Bundle params = new Bundle();
346        params.putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, request.getUtteranceId());
347        params.putInt(TextToSpeech.Engine.KEY_PARAM_PITCH, speechPitch);
348        params.putInt(TextToSpeech.Engine.KEY_PARAM_RATE, speechRate);
349        if (selectedVoice.getRequiresNetworkConnection()) {
350            params.putString(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS, "true");
351        } else {
352            params.putString(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS, "true");
353        }
354
355        String noWarning = request.getMarkup().getParameter(Utterance.KEY_NO_WARNING_ON_FALLBACK);
356        if (noWarning == null || noWarning.equals("false")) {
357            Log.w("TextToSpeechService", "The synthesis engine does not support Markup, falling " +
358                                         "back to the given plain text.");
359        }
360
361        // Build V1 request
362        SynthesisRequest requestV1 = new SynthesisRequest(request.getText(), params);
363        Locale locale = selectedVoice.getLocale();
364        requestV1.setLanguage(locale.getISO3Language(), locale.getISO3Country(),
365                locale.getVariant());
366        requestV1.setSpeechRate(speechRate);
367        requestV1.setPitch(speechPitch);
368
369        // Synthesize using V1 interface
370        onSynthesizeText(requestV1, callback);
371    }
372
373    /**
374     * If true, this service implements proper V2 TTS API service. If it's false,
375     * V2 API will be provided through adapter.
376     */
377    protected boolean implementsV2API() {
378        return false;
379    }
380
381    /**
382     * Queries the service for a set of features supported for a given language.
383     *
384     * Can be called on multiple threads.
385     *
386     * @param lang ISO-3 language code.
387     * @param country ISO-3 country code. May be empty or null.
388     * @param variant Language variant. May be empty or null.
389     * @return A list of features supported for the given language.
390     */
391    protected Set<String> onGetFeaturesForLanguage(String lang, String country, String variant) {
392        return null;
393    }
394
395    private List<VoiceInfo> getVoicesInfo() {
396        synchronized (mVoicesInfoLock) {
397            if (mVoicesInfoList == null) {
398                // Get voices. Defensive copy to make sure TTS engine won't alter the list.
399                mVoicesInfoList = new ArrayList<VoiceInfo>(checkVoicesInfo());
400                // Build lookup map
401                mVoicesInfoLookup = new HashMap<String, VoiceInfo>((int) (
402                        mVoicesInfoList.size()*1.5f));
403                for (VoiceInfo voiceInfo : mVoicesInfoList) {
404                    VoiceInfo prev = mVoicesInfoLookup.put(voiceInfo.getName(), voiceInfo);
405                    if (prev != null) {
406                        Log.e(TAG, "Duplicate name (" + voiceInfo.getName() + ") of the voice ");
407                    }
408                }
409            }
410            return mVoicesInfoList;
411        }
412    }
413
414    public VoiceInfo getVoicesInfoWithName(String name) {
415        synchronized (mVoicesInfoLock) {
416            if (mVoicesInfoLookup != null) {
417                return mVoicesInfoLookup.get(name);
418            }
419        }
420        return null;
421    }
422
423    /**
424     * Force TTS service to reevaluate the set of available languages. Will result in
425     * a call to {@link #checkVoicesInfo()} on the same thread, {@link #onVoicesInfoChange}
426     * on the synthesizer thread and callback to
427     * {@link TextToSpeechClient.ConnectionCallbacks#onEngineStatusChange} of all connected
428     * TTS clients.
429     *
430     * Use this method only if you know that set of available languages changed.
431     *
432     * Can be called on multiple threads.
433     */
434    public void forceVoicesInfoCheck() {
435        synchronized (mVoicesInfoLock) {
436            List<VoiceInfo> old = mVoicesInfoList;
437
438            mVoicesInfoList = null; // Force recreation of voices info list
439            getVoicesInfo();
440
441            if (mVoicesInfoList == null) {
442                throw new IllegalStateException("This method applies only to services " +
443                        "supporting V2 TTS API. This services doesn't support V2 TTS API.");
444            }
445
446            if (old != null) {
447                // Flush all existing items, and inform synthesis thread about the change.
448                mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_FLUSH,
449                        new VoicesInfoChangeItem());
450                // TODO: Handle items that may be added to queue after SynthesizerRestartItem
451                // but before client reconnection
452                // Disconnect all of them
453                mCallbacks.dispatchVoicesInfoChange(mVoicesInfoList);
454            }
455        }
456    }
457
458    private int getDefaultSpeechRate() {
459        return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE);
460    }
461
462    private String[] getSettingsLocale() {
463        final Locale locale = mEngineHelper.getLocalePrefForEngine(mPackageName);
464        return TtsEngines.toOldLocaleStringFormat(locale);
465    }
466
467    private int getSecureSettingInt(String name, int defaultValue) {
468        return Settings.Secure.getInt(getContentResolver(), name, defaultValue);
469    }
470
471    /**
472     * Synthesizer thread. This thread is used to run {@link SynthHandler}.
473     */
474    private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler {
475
476        private boolean mFirstIdle = true;
477
478        public SynthThread() {
479            super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_DEFAULT);
480        }
481
482        @Override
483        protected void onLooperPrepared() {
484            getLooper().getQueue().addIdleHandler(this);
485        }
486
487        @Override
488        public boolean queueIdle() {
489            if (mFirstIdle) {
490                mFirstIdle = false;
491            } else {
492                broadcastTtsQueueProcessingCompleted();
493            }
494            return true;
495        }
496
497        private void broadcastTtsQueueProcessingCompleted() {
498            Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED);
499            if (DBG) Log.d(TAG, "Broadcasting: " + i);
500            sendBroadcast(i);
501        }
502    }
503
504    private class SynthHandler extends Handler {
505        private SpeechItem mCurrentSpeechItem = null;
506
507        public SynthHandler(Looper looper) {
508            super(looper);
509        }
510
511        private synchronized SpeechItem getCurrentSpeechItem() {
512            return mCurrentSpeechItem;
513        }
514
515        private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) {
516            SpeechItem old = mCurrentSpeechItem;
517            mCurrentSpeechItem = speechItem;
518            return old;
519        }
520
521        private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) {
522            if (mCurrentSpeechItem != null &&
523                    (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) {
524                SpeechItem current = mCurrentSpeechItem;
525                mCurrentSpeechItem = null;
526                return current;
527            }
528
529            return null;
530        }
531
532        public boolean isSpeaking() {
533            return getCurrentSpeechItem() != null;
534        }
535
536        public void quit() {
537            // Don't process any more speech items
538            getLooper().quit();
539            // Stop the current speech item
540            SpeechItem current = setCurrentSpeechItem(null);
541            if (current != null) {
542                current.stop();
543            }
544            // The AudioPlaybackHandler will be destroyed by the caller.
545        }
546
547        /**
548         * Adds a speech item to the queue.
549         *
550         * Called on a service binder thread.
551         */
552        public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
553            UtteranceProgressDispatcher utterenceProgress = null;
554            if (speechItem instanceof UtteranceProgressDispatcher) {
555                utterenceProgress = (UtteranceProgressDispatcher) speechItem;
556            }
557
558            if (!speechItem.isValid()) {
559                if (utterenceProgress != null) {
560                    utterenceProgress.dispatchOnError(
561                            TextToSpeechClient.Status.ERROR_INVALID_REQUEST);
562                }
563                return TextToSpeech.ERROR;
564            }
565
566            if (queueMode == TextToSpeech.QUEUE_FLUSH) {
567                stopForApp(speechItem.getCallerIdentity());
568            } else if (queueMode == TextToSpeech.QUEUE_DESTROY) {
569                stopAll();
570            }
571            Runnable runnable = new Runnable() {
572                @Override
573                public void run() {
574                    setCurrentSpeechItem(speechItem);
575                    speechItem.play();
576                    setCurrentSpeechItem(null);
577                }
578            };
579            Message msg = Message.obtain(this, runnable);
580
581            // The obj is used to remove all callbacks from the given app in
582            // stopForApp(String).
583            //
584            // Note that this string is interned, so the == comparison works.
585            msg.obj = speechItem.getCallerIdentity();
586
587            if (sendMessage(msg)) {
588                return TextToSpeech.SUCCESS;
589            } else {
590                Log.w(TAG, "SynthThread has quit");
591                if (utterenceProgress != null) {
592                    utterenceProgress.dispatchOnError(TextToSpeechClient.Status.ERROR_SERVICE);
593                }
594                return TextToSpeech.ERROR;
595            }
596        }
597
598        /**
599         * Stops all speech output and removes any utterances still in the queue for
600         * the calling app.
601         *
602         * Called on a service binder thread.
603         */
604        public int stopForApp(Object callerIdentity) {
605            if (callerIdentity == null) {
606                return TextToSpeech.ERROR;
607            }
608
609            removeCallbacksAndMessages(callerIdentity);
610            // This stops writing data to the file / or publishing
611            // items to the audio playback handler.
612            //
613            // Note that the current speech item must be removed only if it
614            // belongs to the callingApp, else the item will be "orphaned" and
615            // not stopped correctly if a stop request comes along for the item
616            // from the app it belongs to.
617            SpeechItem current = maybeRemoveCurrentSpeechItem(callerIdentity);
618            if (current != null) {
619                current.stop();
620            }
621
622            // Remove any enqueued audio too.
623            mAudioPlaybackHandler.stopForApp(callerIdentity);
624
625            return TextToSpeech.SUCCESS;
626        }
627
628        public int stopAll() {
629            // Stop the current speech item unconditionally .
630            SpeechItem current = setCurrentSpeechItem(null);
631            if (current != null) {
632                current.stop();
633            }
634            // Remove all other items from the queue.
635            removeCallbacksAndMessages(null);
636            // Remove all pending playback as well.
637            mAudioPlaybackHandler.stop();
638
639            return TextToSpeech.SUCCESS;
640        }
641    }
642
643    interface UtteranceProgressDispatcher {
644        public void dispatchOnFallback();
645        public void dispatchOnStop();
646        public void dispatchOnSuccess();
647        public void dispatchOnStart();
648        public void dispatchOnError(int errorCode);
649    }
650
651    /**
652     * An item in the synth thread queue.
653     */
654    private abstract class SpeechItem {
655        private final Object mCallerIdentity;
656        private final int mCallerUid;
657        private final int mCallerPid;
658        private boolean mStarted = false;
659        private boolean mStopped = false;
660
661        public SpeechItem(Object caller, int callerUid, int callerPid) {
662            mCallerIdentity = caller;
663            mCallerUid = callerUid;
664            mCallerPid = callerPid;
665        }
666
667        public Object getCallerIdentity() {
668            return mCallerIdentity;
669        }
670
671
672        public int getCallerUid() {
673            return mCallerUid;
674        }
675
676        public int getCallerPid() {
677            return mCallerPid;
678        }
679
680        /**
681         * Checker whether the item is valid. If this method returns false, the item should not
682         * be played.
683         */
684        public abstract boolean isValid();
685
686        /**
687         * Plays the speech item. Blocks until playback is finished.
688         * Must not be called more than once.
689         *
690         * Only called on the synthesis thread.
691         */
692        public void play() {
693            synchronized (this) {
694                if (mStarted) {
695                    throw new IllegalStateException("play() called twice");
696                }
697                mStarted = true;
698            }
699            playImpl();
700        }
701
702        protected abstract void playImpl();
703
704        /**
705         * Stops the speech item.
706         * Must not be called more than once.
707         *
708         * Can be called on multiple threads,  but not on the synthesis thread.
709         */
710        public void stop() {
711            synchronized (this) {
712                if (mStopped) {
713                    throw new IllegalStateException("stop() called twice");
714                }
715                mStopped = true;
716            }
717            stopImpl();
718        }
719
720        protected abstract void stopImpl();
721
722        protected synchronized boolean isStopped() {
723             return mStopped;
724        }
725    }
726
727    /**
728     * An item in the synth thread queue that process utterance (and call back to client about
729     * progress).
730     */
731    private abstract class UtteranceSpeechItem extends SpeechItem
732        implements UtteranceProgressDispatcher  {
733
734        public UtteranceSpeechItem(Object caller, int callerUid, int callerPid) {
735            super(caller, callerUid, callerPid);
736        }
737
738        @Override
739        public void dispatchOnSuccess() {
740            final String utteranceId = getUtteranceId();
741            if (utteranceId != null) {
742                mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId);
743            }
744        }
745
746        @Override
747        public void dispatchOnStop() {
748            final String utteranceId = getUtteranceId();
749            if (utteranceId != null) {
750                mCallbacks.dispatchOnStop(getCallerIdentity(), utteranceId);
751            }
752        }
753
754        @Override
755        public void dispatchOnFallback() {
756            final String utteranceId = getUtteranceId();
757            if (utteranceId != null) {
758                mCallbacks.dispatchOnFallback(getCallerIdentity(), utteranceId);
759            }
760        }
761
762        @Override
763        public void dispatchOnStart() {
764            final String utteranceId = getUtteranceId();
765            if (utteranceId != null) {
766                mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId);
767            }
768        }
769
770        @Override
771        public void dispatchOnError(int errorCode) {
772            final String utteranceId = getUtteranceId();
773            if (utteranceId != null) {
774                mCallbacks.dispatchOnError(getCallerIdentity(), utteranceId, errorCode);
775            }
776        }
777
778        abstract public String getUtteranceId();
779
780        String getStringParam(Bundle params, String key, String defaultValue) {
781            return params == null ? defaultValue : params.getString(key, defaultValue);
782        }
783
784        int getIntParam(Bundle params, String key, int defaultValue) {
785            return params == null ? defaultValue : params.getInt(key, defaultValue);
786        }
787
788        float getFloatParam(Bundle params, String key, float defaultValue) {
789            return params == null ? defaultValue : params.getFloat(key, defaultValue);
790        }
791    }
792
793    /**
794     * UtteranceSpeechItem for V1 API speech items. V1 API speech items keep
795     * synthesis parameters in a single Bundle passed as parameter. This class
796     * allow subclasses to access them conveniently.
797     */
798    private abstract class SpeechItemV1 extends UtteranceSpeechItem {
799        protected final Bundle mParams;
800
801        SpeechItemV1(Object callerIdentity, int callerUid, int callerPid,
802                Bundle params) {
803            super(callerIdentity, callerUid, callerPid);
804            mParams = params;
805        }
806
807        boolean hasLanguage() {
808            return !TextUtils.isEmpty(getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, null));
809        }
810
811        int getSpeechRate() {
812            return getIntParam(mParams, Engine.KEY_PARAM_RATE, getDefaultSpeechRate());
813        }
814
815        int getPitch() {
816            return getIntParam(mParams, Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH);
817        }
818
819        @Override
820        public String getUtteranceId() {
821            return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null);
822        }
823
824        int getStreamType() {
825            return getIntParam(mParams, Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM);
826        }
827
828        float getVolume() {
829            return getFloatParam(mParams, Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME);
830        }
831
832        float getPan() {
833            return getFloatParam(mParams, Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN);
834        }
835    }
836
837    class SynthesisSpeechItemV2 extends UtteranceSpeechItem {
838        private final SynthesisRequestV2 mSynthesisRequest;
839        private AbstractSynthesisCallback mSynthesisCallback;
840        private final EventLoggerV2 mEventLogger;
841
842        public SynthesisSpeechItemV2(Object callerIdentity, int callerUid, int callerPid,
843                SynthesisRequestV2 synthesisRequest) {
844            super(callerIdentity, callerUid, callerPid);
845
846            mSynthesisRequest = synthesisRequest;
847            mEventLogger = new EventLoggerV2(synthesisRequest, callerUid, callerPid,
848                    mPackageName);
849
850            updateSpeechSpeedParam(synthesisRequest);
851        }
852
853        private void updateSpeechSpeedParam(SynthesisRequestV2 synthesisRequest) {
854            Bundle voiceParams = mSynthesisRequest.getVoiceParams();
855
856            // Inject default speech speed if needed
857            if (voiceParams.containsKey(TextToSpeechClient.Params.SPEECH_SPEED)) {
858                if (voiceParams.getFloat(TextToSpeechClient.Params.SPEECH_SPEED) <= 0) {
859                    voiceParams.putFloat(TextToSpeechClient.Params.SPEECH_SPEED,
860                            getDefaultSpeechRate() / 100.0f);
861                }
862            }
863        }
864
865        /**
866         * Estimate of the character count equivalent of a Markup instance. Calculated
867         * by summing the characters of all Markups of type "text". Each other node
868         * is counted as a single character, as the character count of other nodes
869         * is non-trivial to calculate and we don't want to accept arbitrarily large
870         * requests.
871         */
872        private int estimateSynthesisLengthFromMarkup(Markup m) {
873            int size = 0;
874            if (m.getType() != null &&
875                m.getType().equals("text") &&
876                m.getParameter("text") != null) {
877                size += m.getParameter("text").length();
878            } else if (m.getType() == null ||
879                       !m.getType().equals("utterance")) {
880                size += 1;
881            }
882            for (Markup nested : m.getNestedMarkups()) {
883                size += estimateSynthesisLengthFromMarkup(nested);
884            }
885            return size;
886        }
887
888        @Override
889        public boolean isValid() {
890            if (mSynthesisRequest.getMarkup() == null) {
891                Log.e(TAG, "No markup in request.");
892                return false;
893            }
894            String type = mSynthesisRequest.getMarkup().getType();
895            if (type == null) {
896                Log.w(TAG, "Top level markup node should have type \"utterance\", not null");
897                return false;
898            } else if (!type.equals("utterance")) {
899                Log.w(TAG, "Top level markup node should have type \"utterance\" instead of " +
900                            "\"" + type + "\"");
901                return false;
902            }
903
904            int estimate = estimateSynthesisLengthFromMarkup(mSynthesisRequest.getMarkup());
905            if (estimate >= TextToSpeech.getMaxSpeechInputLength()) {
906                Log.w(TAG, "Text too long: estimated size of text was " + estimate + " chars.");
907                return false;
908            }
909
910            if (estimate <= 0) {
911                Log.e(TAG, "null synthesis text");
912                return false;
913            }
914
915            return true;
916        }
917
918        @Override
919        protected void playImpl() {
920            AbstractSynthesisCallback synthesisCallback;
921            if (mEventLogger != null) {
922                mEventLogger.onRequestProcessingStart();
923            }
924            synchronized (this) {
925                // stop() might have been called before we enter this
926                // synchronized block.
927                if (isStopped()) {
928                    return;
929                }
930                mSynthesisCallback = createSynthesisCallback();
931                synthesisCallback = mSynthesisCallback;
932            }
933
934            // Get voice info
935            VoiceInfo voiceInfo = getVoicesInfoWithName(mSynthesisRequest.getVoiceName());
936            if (voiceInfo != null) {
937                // Primary voice
938                TextToSpeechService.this.onSynthesizeTextV2(mSynthesisRequest, voiceInfo,
939                        synthesisCallback);
940            } else {
941                Log.e(TAG, "Unknown voice name:" + mSynthesisRequest.getVoiceName());
942                synthesisCallback.error(TextToSpeechClient.Status.ERROR_INVALID_REQUEST);
943            }
944
945            // Fix for case where client called .start() & .error(), but did not called .done()
946            if (!synthesisCallback.hasFinished()) {
947                synthesisCallback.done();
948            }
949        }
950
951        @Override
952        protected void stopImpl() {
953            AbstractSynthesisCallback synthesisCallback;
954            synchronized (this) {
955                synthesisCallback = mSynthesisCallback;
956            }
957            if (synthesisCallback != null) {
958                // If the synthesis callback is null, it implies that we haven't
959                // entered the synchronized(this) block in playImpl which in
960                // turn implies that synthesis would not have started.
961                synthesisCallback.stop();
962                TextToSpeechService.this.onStop();
963            }
964        }
965
966        protected AbstractSynthesisCallback createSynthesisCallback() {
967            return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(),
968                    mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger,
969                    implementsV2API());
970        }
971
972        private int getStreamType() {
973            return getIntParam(mSynthesisRequest.getAudioParams(),
974                    TextToSpeechClient.Params.AUDIO_PARAM_STREAM,
975                    Engine.DEFAULT_STREAM);
976        }
977
978        private float getVolume() {
979            return getFloatParam(mSynthesisRequest.getAudioParams(),
980                    TextToSpeechClient.Params.AUDIO_PARAM_VOLUME,
981                    Engine.DEFAULT_VOLUME);
982        }
983
984        private float getPan() {
985            return getFloatParam(mSynthesisRequest.getAudioParams(),
986                    TextToSpeechClient.Params.AUDIO_PARAM_PAN,
987                    Engine.DEFAULT_PAN);
988        }
989
990        @Override
991        public String getUtteranceId() {
992            return mSynthesisRequest.getUtteranceId();
993        }
994    }
995
996    private class SynthesisToFileOutputStreamSpeechItemV2 extends SynthesisSpeechItemV2 {
997        private final FileOutputStream mFileOutputStream;
998
999        public SynthesisToFileOutputStreamSpeechItemV2(Object callerIdentity, int callerUid,
1000                int callerPid,
1001                SynthesisRequestV2 synthesisRequest,
1002                FileOutputStream fileOutputStream) {
1003            super(callerIdentity, callerUid, callerPid, synthesisRequest);
1004            mFileOutputStream = fileOutputStream;
1005        }
1006
1007        @Override
1008        protected AbstractSynthesisCallback createSynthesisCallback() {
1009            return new FileSynthesisCallback(mFileOutputStream.getChannel(),
1010                    this, getCallerIdentity(), implementsV2API());
1011        }
1012
1013        @Override
1014        protected void playImpl() {
1015            super.playImpl();
1016            try {
1017              mFileOutputStream.close();
1018            } catch(IOException e) {
1019              Log.w(TAG, "Failed to close output file", e);
1020            }
1021        }
1022    }
1023
1024    private class AudioSpeechItemV2 extends UtteranceSpeechItem {
1025        private final AudioPlaybackQueueItem mItem;
1026        private final Bundle mAudioParams;
1027        private final String mUtteranceId;
1028
1029        public AudioSpeechItemV2(Object callerIdentity, int callerUid, int callerPid,
1030                String utteranceId, Bundle audioParams, Uri uri) {
1031            super(callerIdentity, callerUid, callerPid);
1032            mUtteranceId = utteranceId;
1033            mAudioParams = audioParams;
1034            mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(),
1035                    TextToSpeechService.this, uri, getStreamType());
1036        }
1037
1038        @Override
1039        public boolean isValid() {
1040            return true;
1041        }
1042
1043        @Override
1044        protected void playImpl() {
1045            mAudioPlaybackHandler.enqueue(mItem);
1046        }
1047
1048        @Override
1049        protected void stopImpl() {
1050            // Do nothing.
1051        }
1052
1053        protected int getStreamType() {
1054            return mAudioParams.getInt(TextToSpeechClient.Params.AUDIO_PARAM_STREAM);
1055        }
1056
1057        public String getUtteranceId() {
1058            return mUtteranceId;
1059        }
1060    }
1061
1062
1063    class SynthesisSpeechItemV1 extends SpeechItemV1 {
1064        // Never null.
1065        private final String mText;
1066        private final SynthesisRequest mSynthesisRequest;
1067        private final String[] mDefaultLocale;
1068        // Non null after synthesis has started, and all accesses
1069        // guarded by 'this'.
1070        private AbstractSynthesisCallback mSynthesisCallback;
1071        private final EventLoggerV1 mEventLogger;
1072        private final int mCallerUid;
1073
1074        public SynthesisSpeechItemV1(Object callerIdentity, int callerUid, int callerPid,
1075                Bundle params, String text) {
1076            super(callerIdentity, callerUid, callerPid, params);
1077            mText = text;
1078            mCallerUid = callerUid;
1079            mSynthesisRequest = new SynthesisRequest(mText, mParams);
1080            mDefaultLocale = getSettingsLocale();
1081            setRequestParams(mSynthesisRequest);
1082            mEventLogger = new EventLoggerV1(mSynthesisRequest, callerUid, callerPid,
1083                    mPackageName);
1084        }
1085
1086        public String getText() {
1087            return mText;
1088        }
1089
1090        @Override
1091        public boolean isValid() {
1092            if (mText == null) {
1093                Log.e(TAG, "null synthesis text");
1094                return false;
1095            }
1096            if (mText.length() >= TextToSpeech.getMaxSpeechInputLength()) {
1097                Log.w(TAG, "Text too long: " + mText.length() + " chars");
1098                return false;
1099            }
1100            return true;
1101        }
1102
1103        @Override
1104        protected void playImpl() {
1105            AbstractSynthesisCallback synthesisCallback;
1106            mEventLogger.onRequestProcessingStart();
1107            synchronized (this) {
1108                // stop() might have been called before we enter this
1109                // synchronized block.
1110                if (isStopped()) {
1111                    return;
1112                }
1113                mSynthesisCallback = createSynthesisCallback();
1114                synthesisCallback = mSynthesisCallback;
1115            }
1116
1117            TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);
1118
1119            // Fix for case where client called .start() & .error(), but did not called .done()
1120            if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) {
1121                synthesisCallback.done();
1122            }
1123        }
1124
1125        protected AbstractSynthesisCallback createSynthesisCallback() {
1126            return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(),
1127                    mAudioPlaybackHandler, this, getCallerIdentity(), mEventLogger, false);
1128        }
1129
1130        private void setRequestParams(SynthesisRequest request) {
1131            request.setLanguage(getLanguage(), getCountry(), getVariant());
1132            request.setSpeechRate(getSpeechRate());
1133            request.setCallerUid(mCallerUid);
1134            request.setPitch(getPitch());
1135        }
1136
1137        @Override
1138        protected void stopImpl() {
1139            AbstractSynthesisCallback synthesisCallback;
1140            synchronized (this) {
1141                synthesisCallback = mSynthesisCallback;
1142            }
1143            if (synthesisCallback != null) {
1144                // If the synthesis callback is null, it implies that we haven't
1145                // entered the synchronized(this) block in playImpl which in
1146                // turn implies that synthesis would not have started.
1147                synthesisCallback.stop();
1148                TextToSpeechService.this.onStop();
1149            }
1150        }
1151
1152        private String getCountry() {
1153            if (!hasLanguage()) return mDefaultLocale[1];
1154            return getStringParam(mParams, Engine.KEY_PARAM_COUNTRY, "");
1155        }
1156
1157        private String getVariant() {
1158            if (!hasLanguage()) return mDefaultLocale[2];
1159            return getStringParam(mParams, Engine.KEY_PARAM_VARIANT, "");
1160        }
1161
1162        public String getLanguage() {
1163            return getStringParam(mParams, Engine.KEY_PARAM_LANGUAGE, mDefaultLocale[0]);
1164        }
1165    }
1166
1167    private class SynthesisToFileOutputStreamSpeechItemV1 extends SynthesisSpeechItemV1 {
1168        private final FileOutputStream mFileOutputStream;
1169
1170        public SynthesisToFileOutputStreamSpeechItemV1(Object callerIdentity, int callerUid,
1171                int callerPid, Bundle params, String text, FileOutputStream fileOutputStream) {
1172            super(callerIdentity, callerUid, callerPid, params, text);
1173            mFileOutputStream = fileOutputStream;
1174        }
1175
1176        @Override
1177        protected AbstractSynthesisCallback createSynthesisCallback() {
1178            return new FileSynthesisCallback(mFileOutputStream.getChannel(),
1179                    this, getCallerIdentity(), false);
1180        }
1181
1182        @Override
1183        protected void playImpl() {
1184            dispatchOnStart();
1185            super.playImpl();
1186            try {
1187              mFileOutputStream.close();
1188            } catch(IOException e) {
1189              Log.w(TAG, "Failed to close output file", e);
1190            }
1191        }
1192    }
1193
1194    private class AudioSpeechItemV1 extends SpeechItemV1 {
1195        private final AudioPlaybackQueueItem mItem;
1196
1197        public AudioSpeechItemV1(Object callerIdentity, int callerUid, int callerPid,
1198                Bundle params, Uri uri) {
1199            super(callerIdentity, callerUid, callerPid, params);
1200            mItem = new AudioPlaybackQueueItem(this, getCallerIdentity(),
1201                    TextToSpeechService.this, uri, getStreamType());
1202        }
1203
1204        @Override
1205        public boolean isValid() {
1206            return true;
1207        }
1208
1209        @Override
1210        protected void playImpl() {
1211            mAudioPlaybackHandler.enqueue(mItem);
1212        }
1213
1214        @Override
1215        protected void stopImpl() {
1216            // Do nothing.
1217        }
1218
1219        @Override
1220        public String getUtteranceId() {
1221            return getStringParam(mParams, Engine.KEY_PARAM_UTTERANCE_ID, null);
1222        }
1223    }
1224
1225    private class SilenceSpeechItem extends UtteranceSpeechItem {
1226        private final long mDuration;
1227        private final String mUtteranceId;
1228
1229        public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid,
1230                String utteranceId, long duration) {
1231            super(callerIdentity, callerUid, callerPid);
1232            mUtteranceId = utteranceId;
1233            mDuration = duration;
1234        }
1235
1236        @Override
1237        public boolean isValid() {
1238            return true;
1239        }
1240
1241        @Override
1242        protected void playImpl() {
1243            mAudioPlaybackHandler.enqueue(new SilencePlaybackQueueItem(
1244                    this, getCallerIdentity(), mDuration));
1245        }
1246
1247        @Override
1248        protected void stopImpl() {
1249
1250        }
1251
1252        @Override
1253        public String getUtteranceId() {
1254            return mUtteranceId;
1255        }
1256    }
1257
1258    /**
1259     * Call {@link TextToSpeechService#onVoicesInfoChange} on synthesis thread.
1260     */
1261    private class VoicesInfoChangeItem extends SpeechItem {
1262        public VoicesInfoChangeItem() {
1263            super(null, 0, 0); // It's never initiated by an user
1264        }
1265
1266        @Override
1267        public boolean isValid() {
1268            return true;
1269        }
1270
1271        @Override
1272        protected void playImpl() {
1273            TextToSpeechService.this.onVoicesInfoChange();
1274        }
1275
1276        @Override
1277        protected void stopImpl() {
1278            // No-op
1279        }
1280    }
1281
1282    /**
1283     * Call {@link TextToSpeechService#onLoadLanguage} on synth thread.
1284     */
1285    private class LoadLanguageItem extends SpeechItem {
1286        private final String mLanguage;
1287        private final String mCountry;
1288        private final String mVariant;
1289
1290        public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid,
1291                String language, String country, String variant) {
1292            super(callerIdentity, callerUid, callerPid);
1293            mLanguage = language;
1294            mCountry = country;
1295            mVariant = variant;
1296        }
1297
1298        @Override
1299        public boolean isValid() {
1300            return true;
1301        }
1302
1303        @Override
1304        protected void playImpl() {
1305            TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant);
1306        }
1307
1308        @Override
1309        protected void stopImpl() {
1310            // No-op
1311        }
1312    }
1313
1314    @Override
1315    public IBinder onBind(Intent intent) {
1316        if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) {
1317            return mBinder;
1318        }
1319        return null;
1320    }
1321
1322    /**
1323     * Binder returned from {@code #onBind(Intent)}. The methods in this class can be
1324     * called called from several different threads.
1325     */
1326    // NOTE: All calls that are passed in a calling app are interned so that
1327    // they can be used as message objects (which are tested for equality using ==).
1328    private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() {
1329        @Override
1330        public int speak(IBinder caller, String text, int queueMode, Bundle params) {
1331            if (!checkNonNull(caller, text, params)) {
1332                return TextToSpeech.ERROR;
1333            }
1334
1335            SpeechItem item = new SynthesisSpeechItemV1(caller,
1336                    Binder.getCallingUid(), Binder.getCallingPid(), params, text);
1337            return mSynthHandler.enqueueSpeechItem(queueMode, item);
1338        }
1339
1340        @Override
1341        public int synthesizeToFileDescriptor(IBinder caller, String text, ParcelFileDescriptor
1342                fileDescriptor, Bundle params) {
1343            if (!checkNonNull(caller, text, fileDescriptor, params)) {
1344                return TextToSpeech.ERROR;
1345            }
1346
1347            // In test env, ParcelFileDescriptor instance may be EXACTLY the same
1348            // one that is used by client. And it will be closed by a client, thus
1349            // preventing us from writing anything to it.
1350            final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd(
1351                    fileDescriptor.detachFd());
1352
1353            SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV1(caller,
1354                    Binder.getCallingUid(), Binder.getCallingPid(), params, text,
1355                    new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor));
1356            return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
1357        }
1358
1359        @Override
1360        public int playAudio(IBinder caller, Uri audioUri, int queueMode, Bundle params) {
1361            if (!checkNonNull(caller, audioUri, params)) {
1362                return TextToSpeech.ERROR;
1363            }
1364
1365            SpeechItem item = new AudioSpeechItemV1(caller,
1366                    Binder.getCallingUid(), Binder.getCallingPid(), params, audioUri);
1367            return mSynthHandler.enqueueSpeechItem(queueMode, item);
1368        }
1369
1370        @Override
1371        public int playSilence(IBinder caller, long duration, int queueMode, String utteranceId) {
1372            if (!checkNonNull(caller)) {
1373                return TextToSpeech.ERROR;
1374            }
1375
1376            SpeechItem item = new SilenceSpeechItem(caller,
1377                    Binder.getCallingUid(), Binder.getCallingPid(), utteranceId, duration);
1378            return mSynthHandler.enqueueSpeechItem(queueMode, item);
1379        }
1380
1381        @Override
1382        public boolean isSpeaking() {
1383            return mSynthHandler.isSpeaking() || mAudioPlaybackHandler.isSpeaking();
1384        }
1385
1386        @Override
1387        public int stop(IBinder caller) {
1388            if (!checkNonNull(caller)) {
1389                return TextToSpeech.ERROR;
1390            }
1391
1392            return mSynthHandler.stopForApp(caller);
1393        }
1394
1395        @Override
1396        public String[] getLanguage() {
1397            return onGetLanguage();
1398        }
1399
1400        @Override
1401        public String[] getClientDefaultLanguage() {
1402            return getSettingsLocale();
1403        }
1404
1405        /*
1406         * If defaults are enforced, then no language is "available" except
1407         * perhaps the default language selected by the user.
1408         */
1409        @Override
1410        public int isLanguageAvailable(String lang, String country, String variant) {
1411            if (!checkNonNull(lang)) {
1412                return TextToSpeech.ERROR;
1413            }
1414
1415            return onIsLanguageAvailable(lang, country, variant);
1416        }
1417
1418        @Override
1419        public String[] getFeaturesForLanguage(String lang, String country, String variant) {
1420            Set<String> features = onGetFeaturesForLanguage(lang, country, variant);
1421            String[] featuresArray = null;
1422            if (features != null) {
1423                featuresArray = new String[features.size()];
1424                features.toArray(featuresArray);
1425            } else {
1426                featuresArray = new String[0];
1427            }
1428            return featuresArray;
1429        }
1430
1431        /*
1432         * There is no point loading a non default language if defaults
1433         * are enforced.
1434         */
1435        @Override
1436        public int loadLanguage(IBinder caller, String lang, String country, String variant) {
1437            if (!checkNonNull(lang)) {
1438                return TextToSpeech.ERROR;
1439            }
1440            int retVal = onIsLanguageAvailable(lang, country, variant);
1441
1442            if (retVal == TextToSpeech.LANG_AVAILABLE ||
1443                    retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE ||
1444                    retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
1445
1446                SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(),
1447                        Binder.getCallingPid(), lang, country, variant);
1448
1449                if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) !=
1450                        TextToSpeech.SUCCESS) {
1451                    return TextToSpeech.ERROR;
1452                }
1453            }
1454            return retVal;
1455        }
1456
1457        @Override
1458        public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
1459            // Note that passing in a null callback is a valid use case.
1460            if (!checkNonNull(caller)) {
1461                return;
1462            }
1463
1464            mCallbacks.setCallback(caller, cb);
1465        }
1466
1467        private String intern(String in) {
1468            // The input parameter will be non null.
1469            return in.intern();
1470        }
1471
1472        private boolean checkNonNull(Object... args) {
1473            for (Object o : args) {
1474                if (o == null) return false;
1475            }
1476            return true;
1477        }
1478
1479        @Override
1480        public List<VoiceInfo> getVoicesInfo() {
1481            return TextToSpeechService.this.getVoicesInfo();
1482        }
1483
1484        @Override
1485        public int speakV2(IBinder callingInstance,
1486                SynthesisRequestV2 request) {
1487            if (!checkNonNull(callingInstance, request)) {
1488                return TextToSpeech.ERROR;
1489            }
1490
1491            SpeechItem item = new SynthesisSpeechItemV2(callingInstance,
1492                    Binder.getCallingUid(), Binder.getCallingPid(), request);
1493            return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
1494        }
1495
1496        @Override
1497        public int synthesizeToFileDescriptorV2(IBinder callingInstance,
1498                ParcelFileDescriptor fileDescriptor,
1499                SynthesisRequestV2 request) {
1500            if (!checkNonNull(callingInstance, request, fileDescriptor)) {
1501                return TextToSpeech.ERROR;
1502            }
1503
1504            // In test env, ParcelFileDescriptor instance may be EXACTLY the same
1505            // one that is used by client. And it will be closed by a client, thus
1506            // preventing us from writing anything to it.
1507            final ParcelFileDescriptor sameFileDescriptor = ParcelFileDescriptor.adoptFd(
1508                    fileDescriptor.detachFd());
1509
1510            SpeechItem item = new SynthesisToFileOutputStreamSpeechItemV2(callingInstance,
1511                    Binder.getCallingUid(), Binder.getCallingPid(), request,
1512                    new ParcelFileDescriptor.AutoCloseOutputStream(sameFileDescriptor));
1513            return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
1514
1515        }
1516
1517        @Override
1518        public int playAudioV2(
1519                IBinder callingInstance, Uri audioUri, String utteranceId,
1520                Bundle systemParameters) {
1521            if (!checkNonNull(callingInstance, audioUri, systemParameters)) {
1522                return TextToSpeech.ERROR;
1523            }
1524
1525            SpeechItem item = new AudioSpeechItemV2(callingInstance,
1526                    Binder.getCallingUid(), Binder.getCallingPid(), utteranceId, systemParameters,
1527                    audioUri);
1528            return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item);
1529        }
1530    };
1531
1532    private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {
1533        private final HashMap<IBinder, ITextToSpeechCallback> mCallerToCallback
1534                = new HashMap<IBinder, ITextToSpeechCallback>();
1535
1536        public void setCallback(IBinder caller, ITextToSpeechCallback cb) {
1537            synchronized (mCallerToCallback) {
1538                ITextToSpeechCallback old;
1539                if (cb != null) {
1540                    register(cb, caller);
1541                    old = mCallerToCallback.put(caller, cb);
1542                } else {
1543                    old = mCallerToCallback.remove(caller);
1544                }
1545                if (old != null && old != cb) {
1546                    unregister(old);
1547                }
1548            }
1549        }
1550
1551        public void dispatchOnFallback(Object callerIdentity, String utteranceId) {
1552            ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1553            if (cb == null) return;
1554            try {
1555                cb.onFallback(utteranceId);
1556            } catch (RemoteException e) {
1557                Log.e(TAG, "Callback onFallback failed: " + e);
1558            }
1559        }
1560
1561        public void dispatchOnStop(Object callerIdentity, String utteranceId) {
1562            ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1563            if (cb == null) return;
1564            try {
1565                cb.onStop(utteranceId);
1566            } catch (RemoteException e) {
1567                Log.e(TAG, "Callback onStop failed: " + e);
1568            }
1569        }
1570
1571        public void dispatchOnSuccess(Object callerIdentity, String utteranceId) {
1572            ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1573            if (cb == null) return;
1574            try {
1575                cb.onSuccess(utteranceId);
1576            } catch (RemoteException e) {
1577                Log.e(TAG, "Callback onDone failed: " + e);
1578            }
1579        }
1580
1581        public void dispatchOnStart(Object callerIdentity, String utteranceId) {
1582            ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1583            if (cb == null) return;
1584            try {
1585                cb.onStart(utteranceId);
1586            } catch (RemoteException e) {
1587                Log.e(TAG, "Callback onStart failed: " + e);
1588            }
1589
1590        }
1591
1592        public void dispatchOnError(Object callerIdentity, String utteranceId,
1593                int errorCode) {
1594            ITextToSpeechCallback cb = getCallbackFor(callerIdentity);
1595            if (cb == null) return;
1596            try {
1597                cb.onError(utteranceId, errorCode);
1598            } catch (RemoteException e) {
1599                Log.e(TAG, "Callback onError failed: " + e);
1600            }
1601        }
1602
1603        @Override
1604        public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) {
1605            IBinder caller = (IBinder) cookie;
1606            synchronized (mCallerToCallback) {
1607                mCallerToCallback.remove(caller);
1608            }
1609            //mSynthHandler.stopForApp(caller);
1610        }
1611
1612        @Override
1613        public void kill() {
1614            synchronized (mCallerToCallback) {
1615                mCallerToCallback.clear();
1616                super.kill();
1617            }
1618        }
1619
1620        public void dispatchVoicesInfoChange(List<VoiceInfo> voicesInfo) {
1621            synchronized (mCallerToCallback) {
1622                for (ITextToSpeechCallback callback : mCallerToCallback.values()) {
1623                    try {
1624                        callback.onVoicesInfoChange(voicesInfo);
1625                    } catch (RemoteException e) {
1626                        Log.e(TAG, "Failed to request reconnect", e);
1627                    }
1628                }
1629            }
1630        }
1631
1632        private ITextToSpeechCallback getCallbackFor(Object caller) {
1633            ITextToSpeechCallback cb;
1634            IBinder asBinder = (IBinder) caller;
1635            synchronized (mCallerToCallback) {
1636                cb = mCallerToCallback.get(asBinder);
1637            }
1638
1639            return cb;
1640        }
1641    }
1642}
1643