woExecute onLoadLanguage in synthesis thread.

Previously, onLoadLanguage was executed without minding synthesis queue.
Now onLoadLanguage is queued item, so it won't be executed before the items
on the queue (previously TTS could receivecall to onLoadLanguage
while synthesizing in completly different language).

I've divided SpeechItem into ControlSpeechItem and UtteranceSpeechItem.
Utterance one dispatches callbacks about synthesis progress.

Bug: 7510063
Bug: 5351864
Change-Id: Ibd156b3cecb190e5c07c4451e61121127b54d51e
diff --git a/core/java/android/speech/tts/ITextToSpeechService.aidl b/core/java/android/speech/tts/ITextToSpeechService.aidl
index ab63187..580d52c 100644
--- a/core/java/android/speech/tts/ITextToSpeechService.aidl
+++ b/core/java/android/speech/tts/ITextToSpeechService.aidl
@@ -131,6 +131,8 @@
     /**
      * Notifies the engine that it should load a speech synthesis language.
      *
+     * @param caller a binder representing the identity of the calling
+     *        TextToSpeech object.
      * @param lang ISO-3 language code.
      * @param country ISO-3 country code. May be empty or null.
      * @param variant Language variant. May be empty or null.
@@ -141,13 +143,14 @@
      *         {@link TextToSpeech#LANG_MISSING_DATA}
      *         {@link TextToSpeech#LANG_NOT_SUPPORTED}.
      */
-    int loadLanguage(in String lang, in String country, in String variant);
+    int loadLanguage(in IBinder caller, in String lang, in String country, in String variant);
 
     /**
      * Sets the callback that will be notified when playback of utterance from the
      * given app are completed.
      *
-     * @param callingApp Package name for the app whose utterance the callback will handle.
+     * @param caller Instance a binder representing the identity of the calling
+     *        TextToSpeech object.
      * @param cb The callback.
      */
     void setCallback(in IBinder caller, ITextToSpeechCallback cb);
diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java
index 8b2a717..db4febe 100644
--- a/core/java/android/speech/tts/TextToSpeech.java
+++ b/core/java/android/speech/tts/TextToSpeech.java
@@ -1049,7 +1049,8 @@
                 // the available parts.
                 // Note that the language is not actually set here, instead it is cached so it
                 // will be associated with all upcoming utterances.
-                int result = service.loadLanguage(language, country, variant);
+
+                int result = service.loadLanguage(getCallerIdentity(), language, country, variant);
                 if (result >= LANG_AVAILABLE){
                     if (result < LANG_COUNTRY_VAR_AVAILABLE) {
                         variant = "";
diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java
index 3ebe029..99ea64d 100644
--- a/core/java/android/speech/tts/TextToSpeechService.java
+++ b/core/java/android/speech/tts/TextToSpeechService.java
@@ -128,6 +128,8 @@
      *
      * Can be called on multiple threads.
      *
+     * Its return values HAVE to be consistent with onLoadLanguage.
+     *
      * @param lang ISO-3 language code.
      * @param country ISO-3 country code. May be empty or null.
      * @param variant Language variant. May be empty or null.
@@ -162,6 +164,8 @@
      * at some point in the future.
      *
      * Can be called on multiple threads.
+     * In <= Android 4.2 (<= API 17) can be called on main and service binder threads.
+     * In > Android 4.2 (> API 17) can be called on main and synthesis threads.
      *
      * @param lang ISO-3 language code.
      * @param country ISO-3 country code. May be empty or null.
@@ -255,7 +259,6 @@
     }
 
     private class SynthHandler extends Handler {
-
         private SpeechItem mCurrentSpeechItem = null;
 
         public SynthHandler(Looper looper) {
@@ -274,7 +277,7 @@
 
         private synchronized SpeechItem maybeRemoveCurrentSpeechItem(Object callerIdentity) {
             if (mCurrentSpeechItem != null &&
-                    mCurrentSpeechItem.getCallerIdentity() == callerIdentity) {
+                    (mCurrentSpeechItem.getCallerIdentity() == callerIdentity)) {
                 SpeechItem current = mCurrentSpeechItem;
                 mCurrentSpeechItem = null;
                 return current;
@@ -295,7 +298,6 @@
             if (current != null) {
                 current.stop();
             }
-
             // The AudioPlaybackHandler will be destroyed by the caller.
         }
 
@@ -305,8 +307,15 @@
          * Called on a service binder thread.
          */
         public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {
+            UtteranceProgressDispatcher utterenceProgress = null;
+            if (speechItem instanceof UtteranceProgressDispatcher) {
+                utterenceProgress = (UtteranceProgressDispatcher) speechItem;
+            }
+
             if (!speechItem.isValid()) {
-                speechItem.dispatchOnError();
+                if (utterenceProgress != null) {
+                    utterenceProgress.dispatchOnError();
+                }
                 return TextToSpeech.ERROR;
             }
 
@@ -324,6 +333,7 @@
                 }
             };
             Message msg = Message.obtain(this, runnable);
+
             // The obj is used to remove all callbacks from the given app in
             // stopForApp(String).
             //
@@ -333,7 +343,9 @@
                 return TextToSpeech.SUCCESS;
             } else {
                 Log.w(TAG, "SynthThread has quit");
-                speechItem.dispatchOnError();
+                if (utterenceProgress != null) {
+                    utterenceProgress.dispatchOnError();
+                }
                 return TextToSpeech.ERROR;
             }
         }
@@ -369,7 +381,7 @@
         }
 
         public int stopAll() {
-            // Stop the current speech item unconditionally.
+            // Stop the current speech item unconditionally .
             SpeechItem current = setCurrentSpeechItem(null);
             if (current != null) {
                 current.stop();
@@ -392,7 +404,7 @@
     /**
      * An item in the synth thread queue.
      */
-    private abstract class SpeechItem implements UtteranceProgressDispatcher {
+    private abstract class SpeechItem {
         private final Object mCallerIdentity;
         protected final Bundle mParams;
         private final int mCallerUid;
@@ -411,6 +423,15 @@
             return mCallerIdentity;
         }
 
+
+        public int getCallerUid() {
+            return mCallerUid;
+        }
+
+        public int getCallerPid() {
+            return mCallerPid;
+        }
+
         /**
          * Checker whether the item is valid. If this method returns false, the item should not
          * be played.
@@ -435,6 +456,8 @@
             return playImpl();
         }
 
+        protected abstract int playImpl();
+
         /**
          * Stops the speech item.
          * Must not be called more than once.
@@ -451,6 +474,23 @@
             stopImpl();
         }
 
+        protected abstract void stopImpl();
+
+        protected synchronized boolean isStopped() {
+             return mStopped;
+        }
+    }
+
+    /**
+     * An item in the synth thread queue that process utterance.
+     */
+    private abstract class UtteranceSpeechItem extends SpeechItem
+        implements UtteranceProgressDispatcher  {
+
+        public UtteranceSpeechItem(Object caller, int callerUid, int callerPid, Bundle params) {
+            super(caller, callerUid, callerPid, params);
+        }
+
         @Override
         public void dispatchOnDone() {
             final String utteranceId = getUtteranceId();
@@ -475,22 +515,6 @@
             }
         }
 
-        public int getCallerUid() {
-            return mCallerUid;
-        }
-
-        public int getCallerPid() {
-            return mCallerPid;
-        }
-
-        protected synchronized boolean isStopped() {
-             return mStopped;
-        }
-
-        protected abstract int playImpl();
-
-        protected abstract void stopImpl();
-
         public int getStreamType() {
             return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM);
         }
@@ -518,9 +542,10 @@
         protected float getFloatParam(String key, float defaultValue) {
             return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue);
         }
+
     }
 
-    class SynthesisSpeechItem extends SpeechItem {
+    class SynthesisSpeechItem extends UtteranceSpeechItem {
         // Never null.
         private final String mText;
         private final SynthesisRequest mSynthesisRequest;
@@ -657,7 +682,7 @@
         }
     }
 
-    private class AudioSpeechItem extends SpeechItem {
+    private class AudioSpeechItem extends UtteranceSpeechItem {
         private final AudioPlaybackQueueItem mItem;
         public AudioSpeechItem(Object callerIdentity, int callerUid, int callerPid,
                 Bundle params, Uri uri) {
@@ -683,7 +708,7 @@
         }
     }
 
-    private class SilenceSpeechItem extends SpeechItem {
+    private class SilenceSpeechItem extends UtteranceSpeechItem {
         private final long mDuration;
 
         public SilenceSpeechItem(Object callerIdentity, int callerUid, int callerPid,
@@ -710,6 +735,41 @@
         }
     }
 
+    private class LoadLanguageItem extends SpeechItem {
+        private final String mLanguage;
+        private final String mCountry;
+        private final String mVariant;
+
+        public LoadLanguageItem(Object callerIdentity, int callerUid, int callerPid,
+                Bundle params, String language, String country, String variant) {
+            super(callerIdentity, callerUid, callerPid, params);
+            mLanguage = language;
+            mCountry = country;
+            mVariant = variant;
+        }
+
+        @Override
+        public boolean isValid() {
+            return true;
+        }
+
+        @Override
+        protected int playImpl() {
+            int result = TextToSpeechService.this.onLoadLanguage(mLanguage, mCountry, mVariant);
+            if (result == TextToSpeech.LANG_AVAILABLE ||
+                    result == TextToSpeech.LANG_COUNTRY_AVAILABLE ||
+                    result == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
+                return TextToSpeech.SUCCESS;
+            }
+            return TextToSpeech.ERROR;
+        }
+
+        @Override
+        protected void stopImpl() {
+            // No-op
+        }
+    }
+
     @Override
     public IBinder onBind(Intent intent) {
         if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) {
@@ -821,12 +881,25 @@
          * are enforced.
          */
         @Override
-        public int loadLanguage(String lang, String country, String variant) {
+        public int loadLanguage(IBinder caller, String lang, String country, String variant) {
             if (!checkNonNull(lang)) {
                 return TextToSpeech.ERROR;
             }
+            int retVal = onIsLanguageAvailable(lang, country, variant);
 
-            return onLoadLanguage(lang, country, variant);
+            if (retVal == TextToSpeech.LANG_AVAILABLE ||
+                    retVal == TextToSpeech.LANG_COUNTRY_AVAILABLE ||
+                    retVal == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
+
+                SpeechItem item = new LoadLanguageItem(caller, Binder.getCallingUid(),
+                    Binder.getCallingPid(), null, lang, country, variant);
+
+                if (mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item) !=
+                        TextToSpeech.SUCCESS) {
+                    return TextToSpeech.ERROR;
+                }
+            }
+            return retVal;
         }
 
         @Override