From 0dbbb422e2bf66475c601cb50ba13eb7602ebeed Mon Sep 17 00:00:00 2001 From: Dieter Hsu Date: Wed, 12 Feb 2020 14:52:59 +0800 Subject: [PATCH] Retry TTS speaking for a11y shortcut warning dialog TTS engine might fetch data over the network or prepare data when binding tts service at first time after boot that may take a few seconds. Because the dialog is shown only once and on-demand for enableing accessibility shortcut, it's unable to speak successfully unless others have bound tts ahead of time. Bug: 139887992 Test: 1. adb shell settings delete secure accessibility_shortcut_dialog_shown 2. manual reboot, long press both vol keys and tts speaks Test: Add 2nd user and long press both vol keys and tts speaks Test: atest AccessibilityShortcutControllerTest Change-Id: I6290f0a64d1a51aa46f13e7d88a15a096ddb0fdc Merged-In: I6290f0a64d1a51aa46f13e7d88a15a096ddb0fdc --- .../AccessibilityShortcutController.java | 58 +++++++++++++------ .../AccessibilityShortcutControllerTest.java | 33 +++++++++++ 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java index bb404654b741b..5cdcab0298773 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -412,8 +412,13 @@ public class AccessibilityShortcutController { * 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 @@ public class AccessibilityShortcutController { 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 @@ public class AccessibilityShortcutController { } /** - * @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 bbf3b12d9b7d0..9af0ed0ab8261 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.anyObject; 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 org.mockito.invocation.InvocationOnMock; 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 @@ public class AccessibilityShortcutControllerTest { 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 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 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 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))