Merge "Retry TTS speaking for a11y shortcut warning dialog" into rvc-dev
diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
index bb40465..5cdcab0 100644
--- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
+++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
@@ -412,8 +412,13 @@
      * Class to wrap TextToSpeech for shortcut dialog spoken feedback.
      */
     private class TtsPrompt implements TextToSpeech.OnInitListener {
+        private static final int RETRY_MILLIS = 1000;
+
         private final CharSequence mText;
+
+        private int mRetryCount = 3;
         private boolean mDismiss;
+        private boolean mLanguageReady = false;
         private TextToSpeech mTts;
 
         TtsPrompt(String serviceName) {
@@ -437,17 +442,15 @@
                 playNotificationTone();
                 return;
             }
-            mHandler.sendMessage(PooledLambda.obtainMessage(TtsPrompt::play, this));
+            mHandler.sendMessage(PooledLambda.obtainMessage(
+                    TtsPrompt::waitForTtsReady, this));
         }
 
         private void play() {
             if (mDismiss) {
                 return;
             }
-            int status = TextToSpeech.ERROR;
-            if (setLanguage(Locale.getDefault())) {
-                status = mTts.speak(mText, TextToSpeech.QUEUE_FLUSH, null, null);
-            }
+            final int status = mTts.speak(mText, TextToSpeech.QUEUE_FLUSH, null, null);
             if (status != TextToSpeech.SUCCESS) {
                 Slog.d(TAG, "Tts play fail");
                 playNotificationTone();
@@ -455,21 +458,42 @@
         }
 
         /**
-         * @return false if tts language is not available
+         * Waiting for tts is ready to speak. Trying again if tts language pack is not available
+         * or tts voice data is not installed yet.
          */
-        private boolean setLanguage(final Locale locale) {
-            int status = mTts.isLanguageAvailable(locale);
-            if (status == TextToSpeech.LANG_MISSING_DATA
-                    || status == TextToSpeech.LANG_NOT_SUPPORTED) {
-                return false;
+        private void waitForTtsReady() {
+            if (mDismiss) {
+                return;
             }
-            mTts.setLanguage(locale);
-            Voice voice = mTts.getVoice();
-            if (voice == null || (voice.getFeatures() != null && voice.getFeatures()
-                    .contains(TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED))) {
-                return false;
+            if (!mLanguageReady) {
+                final int status = mTts.setLanguage(Locale.getDefault());
+                // True if language is available and TTS#loadVoice has called once
+                // that trigger TTS service to start initialization.
+                mLanguageReady = status != TextToSpeech.LANG_MISSING_DATA
+                    && status != TextToSpeech.LANG_NOT_SUPPORTED;
             }
-            return true;
+            if (mLanguageReady) {
+                final Voice voice = mTts.getVoice();
+                final boolean voiceDataInstalled = voice != null
+                        && voice.getFeatures() != null
+                        && !voice.getFeatures().contains(
+                                TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED);
+                if (voiceDataInstalled) {
+                    mHandler.sendMessage(PooledLambda.obtainMessage(
+                            TtsPrompt::play, this));
+                    return;
+                }
+            }
+
+            if (mRetryCount == 0) {
+                Slog.d(TAG, "Tts not ready to speak.");
+                playNotificationTone();
+                return;
+            }
+            // Retry if TTS service not ready yet.
+            mRetryCount -= 1;
+            mHandler.sendMessageDelayed(PooledLambda.obtainMessage(
+                    TtsPrompt::waitForTtsReady, this), RETRY_MILLIS);
         }
     }
 
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
index bbf3b12..9af0ed0 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
@@ -36,6 +36,7 @@
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -85,7 +86,9 @@
 
 import java.lang.reflect.Field;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 
 @RunWith(AndroidJUnit4.class)
@@ -534,6 +537,36 @@
         verify(mRingtone).play();
     }
 
+    @Test
+    public void testOnAccessibilityShortcut_showsWarningDialog_ttsLongTimeInit_retrySpoken()
+            throws Exception {
+        configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
+        configureValidShortcutService();
+        configureTtsSpokenPromptEnabled();
+        configureHandlerCallbackInvocation();
+        AccessibilityShortcutController accessibilityShortcutController = getController();
+        Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0);
+        Set<String> features = new HashSet<>();
+        features.add(TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED);
+        doReturn(features, Collections.emptySet()).when(mVoice).getFeatures();
+        doReturn(TextToSpeech.LANG_NOT_SUPPORTED, TextToSpeech.LANG_AVAILABLE)
+                .when(mTextToSpeech).setLanguage(any());
+        accessibilityShortcutController.performAccessibilityShortcut();
+
+        verify(mAlertDialog).show();
+        ArgumentCaptor<TextToSpeech.OnInitListener> onInitCap = ArgumentCaptor.forClass(
+                TextToSpeech.OnInitListener.class);
+        verify(mFrameworkObjectProvider).getTextToSpeech(any(), onInitCap.capture());
+        onInitCap.getValue().onInit(TextToSpeech.SUCCESS);
+        verify(mTextToSpeech).speak(any(), eq(TextToSpeech.QUEUE_FLUSH), any(), any());
+        ArgumentCaptor<DialogInterface.OnDismissListener> onDismissCap = ArgumentCaptor.forClass(
+                DialogInterface.OnDismissListener.class);
+        verify(mAlertDialog).setOnDismissListener(onDismissCap.capture());
+        onDismissCap.getValue().onDismiss(mAlertDialog);
+        verify(mTextToSpeech).shutdown();
+        verify(mRingtone, times(0)).play();
+    }
+
     private void configureNoShortcutService() throws Exception {
         when(mAccessibilityManagerService
                 .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY))