Write unit tests for android.speech.tts.

Change-Id: Ie5b3b4cdd13be2babee9a44bae00da179b372d12
diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java
index cd065ec..7a174af 100755
--- a/core/java/android/speech/tts/TextToSpeech.java
+++ b/core/java/android/speech/tts/TextToSpeech.java
@@ -486,6 +486,11 @@
     private final Object mStartLock = new Object();
 
     private String mRequestedEngine;
+    // Whether to initialize this TTS object with the default engine,
+    // if the requested engine is not available. Valid only if mRequestedEngine
+    // is not null. Used only for testing, though potentially useful API wise
+    // too.
+    private final boolean mUseFallback;
     private final Map<String, Uri> mEarcons;
     private final Map<String, Uri> mUtterances;
     private final Bundle mParams = new Bundle();
@@ -519,7 +524,7 @@
      * @param engine Package name of the TTS engine to use.
      */
     public TextToSpeech(Context context, OnInitListener listener, String engine) {
-        this(context, listener, engine, null);
+        this(context, listener, engine, null, true);
     }
 
     /**
@@ -529,10 +534,11 @@
      * @hide
      */
     public TextToSpeech(Context context, OnInitListener listener, String engine,
-            String packageName) {
+            String packageName, boolean useFallback) {
         mContext = context;
         mInitListener = listener;
         mRequestedEngine = engine;
+        mUseFallback = useFallback;
 
         mEarcons = new HashMap<String, Uri>();
         mUtterances = new HashMap<String, Uri>();
@@ -567,10 +573,21 @@
 
     private int initTts() {
         // Step 1: Try connecting to the engine that was requested.
-        if (mRequestedEngine != null && mEnginesHelper.isEngineInstalled(mRequestedEngine)) {
-            if (connectToEngine(mRequestedEngine)) {
-                mCurrentEngine = mRequestedEngine;
-                return SUCCESS;
+        if (mRequestedEngine != null) {
+            if (mEnginesHelper.isEngineInstalled(mRequestedEngine)) {
+                if (connectToEngine(mRequestedEngine)) {
+                    mCurrentEngine = mRequestedEngine;
+                    return SUCCESS;
+                } else if (!mUseFallback) {
+                    mCurrentEngine = null;
+                    dispatchOnInit(ERROR);
+                    return ERROR;
+                }
+            } else if (!mUseFallback) {
+                Log.i(TAG, "Requested engine not installed: " + mRequestedEngine);
+                mCurrentEngine = null;
+                dispatchOnInit(ERROR);
+                return ERROR;
             }
         }
 
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index fa82c46..4c81767 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -1322,7 +1322,7 @@
                 final String packageName = ctx.getPackageName();
                 if (packageName != null) {
                     mTextToSpeech = new TextToSpeech(getContext(), null, null,
-                            packageName + ".**webview**");
+                            packageName + ".**webview**", true);
                     addJavascriptInterface(mTextToSpeech, ALIAS_ACCESSIBILITY_JS_INTERFACE);
                 }
             }
diff --git a/tests/TtsTests/Android.mk b/tests/TtsTests/Android.mk
new file mode 100644
index 0000000..e049c90
--- /dev/null
+++ b/tests/TtsTests/Android.mk
@@ -0,0 +1,28 @@
+#
+# Copyright (C) 2011 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_STATIC_JAVA_LIBRARIES := littlemock
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_PACKAGE_NAME := TtsTests
+
+include $(BUILD_PACKAGE)
diff --git a/tests/TtsTests/AndroidManifest.xml b/tests/TtsTests/AndroidManifest.xml
new file mode 100644
index 0000000..b6d5111
--- /dev/null
+++ b/tests/TtsTests/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2011 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.speech.tts">
+    <application>
+        <uses-library android:name="android.test.runner" />
+
+
+        <service android:name=".MockableTextToSpeechService"
+                 android:label="Mockable Text-to-speech Service">
+            <intent-filter>
+                <action android:name="android.intent.action.TTS_SERVICE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </service>
+
+        <activity android:name=".MockableCheckVoiceData"
+                  android:theme="@android:style/Theme.NoDisplay">
+            <intent-filter>
+                <action android:name="android.speech.tts.engine.CHECK_TTS_DATA" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+    </application>
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+                     android:targetPackage="com.android.speech.tts"
+                     android:label="Tests for android.speech.tts" />
+</manifest>
diff --git a/tests/TtsTests/src/com/android/speech/tts/MockableCheckVoiceData.java b/tests/TtsTests/src/com/android/speech/tts/MockableCheckVoiceData.java
new file mode 100644
index 0000000..0ab8ed6
--- /dev/null
+++ b/tests/TtsTests/src/com/android/speech/tts/MockableCheckVoiceData.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.speech.tts;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.speech.tts.TextToSpeech;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MockableCheckVoiceData extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        MockableTextToSpeechService.IDelegate delegate =
+                MockableTextToSpeechService.getMocker();
+
+        ArrayList<String> availableLangs = delegate.getAvailableVoices();
+        ArrayList<String> unavailableLangs = delegate.getUnavailableVoices();
+
+        final Intent returnVal = new Intent();
+
+        // Returns early.
+        if (availableLangs == null) {
+            setResult(TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL, returnVal);
+            finish();
+            return;
+        }
+
+        returnVal.putStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES,
+                    availableLangs);
+
+        if (unavailableLangs != null && unavailableLangs.size() > 0) {
+            returnVal.putStringArrayListExtra(TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES,
+                    unavailableLangs);
+        }
+
+        setResult(TextToSpeech.Engine.CHECK_VOICE_DATA_PASS, returnVal);
+        finish();
+    }
+
+}
diff --git a/tests/TtsTests/src/com/android/speech/tts/MockableTextToSpeechService.java b/tests/TtsTests/src/com/android/speech/tts/MockableTextToSpeechService.java
new file mode 100644
index 0000000..20648a4
--- /dev/null
+++ b/tests/TtsTests/src/com/android/speech/tts/MockableTextToSpeechService.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.speech.tts;
+
+import android.speech.tts.SynthesisCallback;
+import android.speech.tts.SynthesisRequest;
+import android.speech.tts.TextToSpeechService;
+
+import java.util.ArrayList;
+
+public class MockableTextToSpeechService extends TextToSpeechService {
+
+    private static IDelegate sDelegate;
+
+    public static void setMocker(IDelegate delegate) {
+        sDelegate = delegate;
+    }
+
+    static IDelegate getMocker() {
+        return sDelegate;
+    }
+
+    @Override
+    protected int onIsLanguageAvailable(String lang, String country, String variant) {
+        return sDelegate.onIsLanguageAvailable(lang, country, variant);
+    }
+
+    @Override
+    protected String[] onGetLanguage() {
+        return sDelegate.onGetLanguage();
+    }
+
+    @Override
+    protected int onLoadLanguage(String lang, String country, String variant) {
+        return sDelegate.onLoadLanguage(lang, country, variant);
+    }
+
+    @Override
+    protected void onStop() {
+        sDelegate.onStop();
+    }
+
+    @Override
+    protected void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback) {
+        sDelegate.onSynthesizeText(request, callback);
+    }
+
+    public static interface IDelegate {
+        int onIsLanguageAvailable(String lang, String country, String variant);
+
+        String[] onGetLanguage();
+
+        int onLoadLanguage(String lang, String country, String variant);
+
+        void onStop();
+
+        void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback);
+
+        ArrayList<String> getAvailableVoices();
+
+        ArrayList<String> getUnavailableVoices();
+    }
+
+}
diff --git a/tests/TtsTests/src/com/android/speech/tts/TextToSpeechTests.java b/tests/TtsTests/src/com/android/speech/tts/TextToSpeechTests.java
new file mode 100644
index 0000000..b736e9f
--- /dev/null
+++ b/tests/TtsTests/src/com/android/speech/tts/TextToSpeechTests.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.speech.tts;
+
+import android.speech.tts.SynthesisCallback;
+import android.speech.tts.SynthesisRequest;
+import android.speech.tts.TextToSpeech;
+import android.test.InstrumentationTestCase;
+
+import com.android.speech.tts.MockableTextToSpeechService.IDelegate;
+import com.google.testing.littlemock.ArgumentCaptor;
+import com.google.testing.littlemock.Behaviour;
+import com.google.testing.littlemock.LittleMock;
+import junit.framework.Assert;
+
+import java.util.Locale;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class TextToSpeechTests extends InstrumentationTestCase {
+    private static final String MOCK_ENGINE = "com.android.speech.tts";
+    private static final String MOCK_PACKAGE = "com.android.speech.tts.__testpackage__";
+
+    private TextToSpeech mTts;
+
+    @Override
+    public void setUp() throws Exception {
+        IDelegate passThrough = LittleMock.mock(IDelegate.class);
+        MockableTextToSpeechService.setMocker(passThrough);
+
+        blockingInitAndVerify(MOCK_ENGINE, TextToSpeech.SUCCESS);
+        assertEquals(MOCK_ENGINE, mTts.getCurrentEngine());
+    }
+
+
+    @Override
+    public void tearDown() {
+        if (mTts != null) {
+            mTts.shutdown();
+        }
+    }
+
+    public void testEngineInitialized() throws Exception {
+        // Fail on an engine that doesn't exist.
+        blockingInitAndVerify("__DOES_NOT_EXIST__", TextToSpeech.ERROR);
+
+        // Also, the "current engine" must be null
+        assertNull(mTts.getCurrentEngine());
+    }
+
+    public void testSetLanguage_delegation() {
+        IDelegate delegate = LittleMock.mock(IDelegate.class);
+        MockableTextToSpeechService.setMocker(delegate);
+
+        // Test 1 :Tests that calls to onLoadLanguage( ) are delegated through to the
+        // service without any caching or intermediate steps.
+        mTts.setLanguage(new Locale("eng", "USA", "variant"));
+        LittleMock.verify(delegate, LittleMock.times(1)).onLoadLanguage(
+                "eng", "USA", "variant");
+    }
+
+    public void testSetLanguage_availableLanguage() throws Exception {
+        IDelegate delegate = LittleMock.mock(IDelegate.class);
+        MockableTextToSpeechService.setMocker(delegate);
+
+        // ---------------------------------------------------------
+        // Test 2 : Tests that when the language is successfully set
+        // like above (returns LANG_COUNTRY_AVAILABLE). That the
+        // request language changes from that point on.
+        LittleMock.doReturn(TextToSpeech.LANG_COUNTRY_AVAILABLE).when(delegate).onLoadLanguage(
+                "eng", "USA", "variant");
+        mTts.setLanguage(new Locale("eng", "USA", "variant"));
+        blockingCallSpeak("foo bar", delegate);
+        ArgumentCaptor<SynthesisRequest> req = LittleMock.createCaptor();
+        LittleMock.verify(delegate, LittleMock.times(1)).onSynthesizeText(req.capture(),
+                LittleMock.<SynthesisCallback>anyObject());
+
+        assertEquals("eng", req.getValue().getLanguage());
+        assertEquals("USA", req.getValue().getCountry());
+        assertEquals("", req.getValue().getVariant());
+    }
+
+    public void testSetLanguage_unavailableLanguage() throws Exception {
+        IDelegate delegate = LittleMock.mock(IDelegate.class);
+        MockableTextToSpeechService.setMocker(delegate);
+
+        // ---------------------------------------------------------
+        // TEST 3 : Tests that the language that is set does not change when the
+        // engine reports it could not load the specified language.
+        LittleMock.doReturn(TextToSpeech.LANG_NOT_SUPPORTED).when(
+                delegate).onLoadLanguage("fra", "FRA", "");
+        mTts.setLanguage(Locale.FRANCE);
+        blockingCallSpeak("le fou barre", delegate);
+        ArgumentCaptor<SynthesisRequest> req2 = LittleMock.createCaptor();
+        LittleMock.verify(delegate, LittleMock.times(1)).onSynthesizeText(req2.capture(),
+                        LittleMock.<SynthesisCallback>anyObject());
+
+        // The params are basically unchanged.
+        assertEquals("eng", req2.getValue().getLanguage());
+        assertEquals("USA", req2.getValue().getCountry());
+        assertEquals("", req2.getValue().getVariant());
+    }
+
+
+    public void testGetLanguage_invalidReturnValues() {
+        IDelegate delegate = LittleMock.mock(IDelegate.class);
+        MockableTextToSpeechService.setMocker(delegate);
+
+        // Test1: Simple end to end test. Ensure that bad return values
+        // are dealt with appropriately.
+        LittleMock.doReturn(null).when(delegate).onGetLanguage();
+        Locale returnVal = mTts.getLanguage();
+        assertNull(returnVal);
+
+
+        // Bad value 2. An array of length < 3.
+        LittleMock.doReturn(new String[] {"eng", "usa"}).when(delegate).onGetLanguage();
+        returnVal = mTts.getLanguage();
+        assertNull(returnVal);
+    }
+
+    public void testGetLanguage_validReturnValues() {
+        IDelegate delegate = LittleMock.mock(IDelegate.class);
+        MockableTextToSpeechService.setMocker(delegate);
+
+        // A correct value.
+        LittleMock.doReturn(new String[] {"eng", "usa", ""}).when(delegate).onGetLanguage();
+        Locale returnVal = mTts.getLanguage();
+
+        // Note: This is not the same as Locale.US . Well tough luck for
+        // being the only component of the entire framework that standardized
+        // three letter country and language codes.
+        assertEquals(new Locale("eng", "USA", ""), returnVal);
+    }
+
+    public void testIsLanguageAvailable() {
+        IDelegate delegate = LittleMock.mock(IDelegate.class);
+        MockableTextToSpeechService.setMocker(delegate);
+
+        // Test1: Simple end to end test.
+        LittleMock.doReturn(TextToSpeech.LANG_COUNTRY_AVAILABLE).when(
+                delegate).onIsLanguageAvailable("eng", "USA", "");
+
+        assertEquals(TextToSpeech.LANG_COUNTRY_AVAILABLE, mTts.isLanguageAvailable(Locale.US));
+        LittleMock.verify(delegate, LittleMock.times(1)).onIsLanguageAvailable(
+                "eng", "USA", "");
+    }
+
+
+    private void blockingCallSpeak(String speech, IDelegate mock) throws
+            InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        doCountDown(latch).when(mock).onSynthesizeText(LittleMock.<SynthesisRequest>anyObject(),
+                LittleMock.<SynthesisCallback>anyObject());
+        mTts.speak(speech, TextToSpeech.QUEUE_ADD, null);
+
+        awaitCountDown(latch, 5, TimeUnit.SECONDS);
+    }
+
+    private void blockingInitAndVerify(final String engine, int errorCode) throws
+            InterruptedException {
+        TextToSpeech.OnInitListener listener = LittleMock.mock(
+                TextToSpeech.OnInitListener.class);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        doCountDown(latch).when(listener).onInit(errorCode);
+
+        mTts = new TextToSpeech(getInstrumentation().getTargetContext(),
+                listener, engine, MOCK_PACKAGE, false /* use fallback package */);
+
+        awaitCountDown(latch, 5, TimeUnit.SECONDS);
+    }
+
+    public interface CountDownBehaviour extends Behaviour {
+        /** Used to mock methods that return a result. */
+        Behaviour andReturn(Object result);
+    }
+
+    public static CountDownBehaviour doCountDown(final CountDownLatch latch) {
+        return new CountDownBehaviour() {
+            @Override
+            public <T> T when(T mock) {
+                return LittleMock.doAnswer(new Callable<Void>() {
+                    @Override
+                    public Void call() throws Exception {
+                        latch.countDown();
+                        return null;
+                    }
+                }).when(mock);
+            }
+
+            @Override
+            public Behaviour andReturn(final Object result) {
+                return new Behaviour() {
+                    @Override
+                    public <T> T when(T mock) {
+                        return LittleMock.doAnswer(new Callable<Object>() {
+                            @Override
+                            public Object call() throws Exception {
+                                latch.countDown();
+                                return result;
+                            }
+                        }).when(mock);
+                    }
+                };
+            }
+        };
+    }
+
+    public static void awaitCountDown(CountDownLatch latch, long timeout, TimeUnit unit)
+            throws InterruptedException {
+        Assert.assertTrue("Waited too long for method call", latch.await(timeout, unit));
+    }
+}