1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.tv.settings.system; 18 19 import android.content.ActivityNotFoundException; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.os.Bundle; 25 import android.speech.tts.TextToSpeech; 26 import android.speech.tts.TtsEngines; 27 import android.text.TextUtils; 28 import android.util.Log; 29 import android.util.Pair; 30 31 import androidx.annotation.Keep; 32 import androidx.annotation.NonNull; 33 import androidx.preference.ListPreference; 34 import androidx.preference.Preference; 35 import androidx.preference.PreferenceScreen; 36 37 import com.android.internal.logging.nano.MetricsProto; 38 import com.android.tv.settings.R; 39 import com.android.tv.settings.SettingsPreferenceFragment; 40 41 import java.util.ArrayList; 42 import java.util.Locale; 43 44 /** 45 * The text-to-speech engine settings screen in TV Settings. 46 */ 47 @Keep 48 public class TtsEngineSettingsFragment extends SettingsPreferenceFragment implements 49 Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener { 50 private static final String TAG = "TtsEngineSettings"; 51 private static final boolean DBG = false; 52 53 /** 54 * Key for the name of the TTS engine passed in to the engine 55 * settings fragment {@link TtsEngineSettingsFragment}. 56 */ 57 private static final String ARG_ENGINE_NAME = "engineName"; 58 59 /** 60 * Key for the label of the TTS engine passed in to the engine 61 * settings fragment. This is used as the title of the fragment 62 * {@link TtsEngineSettingsFragment}. 63 */ 64 private static final String ARG_ENGINE_LABEL = "engineLabel"; 65 66 /** 67 * Key for the voice data data passed in to the engine settings 68 * fragmetn {@link TtsEngineSettingsFragment}. 69 */ 70 private static final String ARG_VOICES = "voices"; 71 72 73 private static final String KEY_ENGINE_LOCALE = "tts_default_lang"; 74 private static final String KEY_ENGINE_SETTINGS = "tts_engine_settings"; 75 private static final String KEY_INSTALL_DATA = "tts_install_data"; 76 77 private static final String STATE_KEY_LOCALE_ENTRIES = "locale_entries"; 78 private static final String STATE_KEY_LOCALE_ENTRY_VALUES= "locale_entry_values"; 79 private static final String STATE_KEY_LOCALE_VALUE = "locale_value"; 80 81 private static final int VOICE_DATA_INTEGRITY_CHECK = 1977; 82 83 private TtsEngines mEnginesHelper; 84 private ListPreference mLocalePreference; 85 private Preference mEngineSettingsPreference; 86 private Preference mInstallVoicesPreference; 87 private Intent mVoiceDataDetails; 88 89 private TextToSpeech mTts; 90 91 private int mSelectedLocaleIndex = -1; 92 93 private final TextToSpeech.OnInitListener mTtsInitListener = new TextToSpeech.OnInitListener() { 94 @Override 95 public void onInit(int status) { 96 if (status != TextToSpeech.SUCCESS) { 97 getFragmentManager().popBackStack(); 98 } else { 99 getActivity().runOnUiThread(new Runnable() { 100 @Override 101 public void run() { 102 mLocalePreference.setEnabled(true); 103 } 104 }); 105 } 106 } 107 }; 108 109 private final BroadcastReceiver mLanguagesChangedReceiver = new BroadcastReceiver() { 110 @Override 111 public void onReceive(Context context, Intent intent) { 112 // Installed or uninstalled some data packs 113 if (TextToSpeech.Engine.ACTION_TTS_DATA_INSTALLED.equals(intent.getAction())) { 114 checkTtsData(); 115 } 116 } 117 }; 118 prepareArgs(@onNull Bundle args, String engineName, String engineLabel, Intent voiceCheckData)119 public static void prepareArgs(@NonNull Bundle args, String engineName, String engineLabel, 120 Intent voiceCheckData) { 121 args.clear(); 122 123 args.putString(ARG_ENGINE_NAME, engineName); 124 args.putString(ARG_ENGINE_LABEL, engineLabel); 125 args.putParcelable(ARG_VOICES, voiceCheckData); 126 } 127 TtsEngineSettingsFragment()128 public TtsEngineSettingsFragment() {} 129 130 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)131 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 132 133 addPreferencesFromResource(R.xml.tts_engine_settings); 134 135 final PreferenceScreen screen = getPreferenceScreen(); 136 screen.setTitle(getEngineLabel()); 137 screen.setKey(getEngineName()); 138 139 mLocalePreference = (ListPreference) findPreference(KEY_ENGINE_LOCALE); 140 mLocalePreference.setOnPreferenceChangeListener(this); 141 mEngineSettingsPreference = findPreference(KEY_ENGINE_SETTINGS); 142 mEngineSettingsPreference.setOnPreferenceClickListener(this); 143 mInstallVoicesPreference = findPreference(KEY_INSTALL_DATA); 144 mInstallVoicesPreference.setOnPreferenceClickListener(this); 145 146 mEngineSettingsPreference.setTitle(getResources().getString( 147 R.string.tts_engine_settings_title, getEngineLabel())); 148 final Intent settingsIntent = mEnginesHelper.getSettingsIntent(getEngineName()); 149 mEngineSettingsPreference.setIntent(settingsIntent); 150 if (settingsIntent == null) { 151 mEngineSettingsPreference.setEnabled(false); 152 } 153 mInstallVoicesPreference.setEnabled(false); 154 155 if (savedInstanceState == null) { 156 mLocalePreference.setEnabled(false); 157 mLocalePreference.setEntries(new CharSequence[0]); 158 mLocalePreference.setEntryValues(new CharSequence[0]); 159 } else { 160 // Repopulate mLocalePreference with saved state. Will be updated later with 161 // up-to-date values when checkTtsData() calls back with results. 162 final CharSequence[] entries = 163 savedInstanceState.getCharSequenceArray(STATE_KEY_LOCALE_ENTRIES); 164 final CharSequence[] entryValues = 165 savedInstanceState.getCharSequenceArray(STATE_KEY_LOCALE_ENTRY_VALUES); 166 final CharSequence value = 167 savedInstanceState.getCharSequence(STATE_KEY_LOCALE_VALUE); 168 169 mLocalePreference.setEntries(entries); 170 mLocalePreference.setEntryValues(entryValues); 171 mLocalePreference.setValue(value != null ? value.toString() : null); 172 mLocalePreference.setEnabled(entries.length > 0); 173 } 174 175 } 176 177 @Override onCreate(Bundle savedInstanceState)178 public void onCreate(Bundle savedInstanceState) { 179 mEnginesHelper = new TtsEngines(getActivity()); 180 181 super.onCreate(savedInstanceState); 182 183 mVoiceDataDetails = getArguments().getParcelable(ARG_VOICES); 184 185 mTts = new TextToSpeech(getActivity().getApplicationContext(), mTtsInitListener, 186 getEngineName()); 187 188 // Check if data packs changed 189 checkTtsData(); 190 191 getActivity().registerReceiver(mLanguagesChangedReceiver, 192 new IntentFilter(TextToSpeech.Engine.ACTION_TTS_DATA_INSTALLED)); 193 } 194 195 @Override onDestroy()196 public void onDestroy() { 197 getActivity().unregisterReceiver(mLanguagesChangedReceiver); 198 mTts.shutdown(); 199 super.onDestroy(); 200 } 201 202 @Override onSaveInstanceState(Bundle outState)203 public void onSaveInstanceState(Bundle outState) { 204 super.onSaveInstanceState(outState); 205 206 // Save the mLocalePreference values, so we can repopulate it with entries. 207 outState.putCharSequenceArray(STATE_KEY_LOCALE_ENTRIES, 208 mLocalePreference.getEntries()); 209 outState.putCharSequenceArray(STATE_KEY_LOCALE_ENTRY_VALUES, 210 mLocalePreference.getEntryValues()); 211 outState.putCharSequence(STATE_KEY_LOCALE_VALUE, 212 mLocalePreference.getValue()); 213 } 214 checkTtsData()215 private void checkTtsData() { 216 Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); 217 intent.setPackage(getEngineName()); 218 try { 219 if (DBG) Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0)); 220 startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK); 221 } catch (ActivityNotFoundException ex) { 222 Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")"); 223 } 224 } 225 226 @Override onActivityResult(int requestCode, int resultCode, Intent data)227 public void onActivityResult(int requestCode, int resultCode, Intent data) { 228 if (requestCode == VOICE_DATA_INTEGRITY_CHECK) { 229 if (resultCode != TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL) { 230 updateVoiceDetails(data); 231 } else { 232 Log.e(TAG, "CheckVoiceData activity failed"); 233 } 234 } 235 } 236 updateVoiceDetails(Intent data)237 private void updateVoiceDetails(Intent data) { 238 if (data == null){ 239 Log.e(TAG, "Engine failed voice data integrity check (null return)" + 240 mTts.getCurrentEngine()); 241 return; 242 } 243 mVoiceDataDetails = data; 244 245 if (DBG) Log.d(TAG, "Parsing voice data details, data: " + mVoiceDataDetails.toUri(0)); 246 247 final ArrayList<String> available = mVoiceDataDetails.getStringArrayListExtra( 248 TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); 249 final ArrayList<String> unavailable = mVoiceDataDetails.getStringArrayListExtra( 250 TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES); 251 252 if (unavailable != null && unavailable.size() > 0) { 253 mInstallVoicesPreference.setEnabled(true); 254 } else { 255 mInstallVoicesPreference.setEnabled(false); 256 } 257 258 if (available == null){ 259 Log.e(TAG, "TTS data check failed (available == null)."); 260 mLocalePreference.setEnabled(false); 261 } else { 262 updateDefaultLocalePref(available); 263 } 264 } 265 updateDefaultLocalePref(ArrayList<String> availableLangs)266 private void updateDefaultLocalePref(ArrayList<String> availableLangs) { 267 if (availableLangs == null || availableLangs.size() == 0) { 268 mLocalePreference.setEnabled(false); 269 return; 270 } 271 Locale currentLocale = null; 272 if (!mEnginesHelper.isLocaleSetToDefaultForEngine(getEngineName())) { 273 currentLocale = mEnginesHelper.getLocalePrefForEngine(getEngineName()); 274 } 275 276 ArrayList<Pair<String, Locale>> entryPairs = 277 new ArrayList<>(availableLangs.size()); 278 for (int i = 0; i < availableLangs.size(); i++) { 279 Locale locale = mEnginesHelper.parseLocaleString(availableLangs.get(i)); 280 if (locale != null){ 281 entryPairs.add(new Pair<>(locale.getDisplayName(), locale)); 282 } 283 } 284 285 // Sort it 286 entryPairs.sort((lhs, rhs) -> lhs.first.compareToIgnoreCase(rhs.first)); 287 288 // Get two arrays out of one of pairs 289 mSelectedLocaleIndex = 0; // Will point to the R.string.tts_lang_use_system value 290 CharSequence[] entries = new CharSequence[availableLangs.size()+1]; 291 CharSequence[] entryValues = new CharSequence[availableLangs.size()+1]; 292 293 entries[0] = getString(R.string.tts_lang_use_system); 294 entryValues[0] = ""; 295 296 int i = 1; 297 for (Pair<String, Locale> entry : entryPairs) { 298 if (entry.second.equals(currentLocale)) { 299 mSelectedLocaleIndex = i; 300 } 301 entries[i] = entry.first; 302 entryValues[i++] = entry.second.toString(); 303 } 304 305 mLocalePreference.setEntries(entries); 306 mLocalePreference.setEntryValues(entryValues); 307 mLocalePreference.setEnabled(true); 308 setLocalePreference(mSelectedLocaleIndex); 309 } 310 311 /** Set entry from entry table in mLocalePreference */ setLocalePreference(int index)312 private void setLocalePreference(int index) { 313 if (index < 0) { 314 mLocalePreference.setValue(""); 315 mLocalePreference.setSummary(R.string.tts_lang_not_selected); 316 } else { 317 mLocalePreference.setValueIndex(index); 318 mLocalePreference.setSummary(mLocalePreference.getEntries()[index]); 319 } 320 } 321 322 /** 323 * Ask the current default engine to launch the matching INSTALL_TTS_DATA activity 324 * so the required TTS files are properly installed. 325 */ installVoiceData()326 private void installVoiceData() { 327 if (TextUtils.isEmpty(getEngineName())) return; 328 Intent intent = new Intent(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA); 329 intent.setPackage(getEngineName()); 330 try { 331 startActivity(intent); 332 } catch (ActivityNotFoundException ex) { 333 Log.e(TAG, "Failed to install TTS data, no activity found for " + intent + ")"); 334 } 335 } 336 337 @Override onPreferenceClick(Preference preference)338 public boolean onPreferenceClick(Preference preference) { 339 if (preference == mInstallVoicesPreference) { 340 installVoiceData(); 341 return true; 342 } 343 344 return false; 345 } 346 347 @Override onPreferenceChange(Preference preference, Object newValue)348 public boolean onPreferenceChange(Preference preference, Object newValue) { 349 if (preference == mLocalePreference) { 350 String localeString = (String) newValue; 351 updateLanguageTo((!TextUtils.isEmpty(localeString) ? 352 mEnginesHelper.parseLocaleString(localeString) : null)); 353 return true; 354 } 355 return false; 356 } 357 updateLanguageTo(Locale locale)358 private void updateLanguageTo(Locale locale) { 359 int selectedLocaleIndex = -1; 360 String localeString = (locale != null) ? locale.toString() : ""; 361 for (int i=0; i < mLocalePreference.getEntryValues().length; i++) { 362 if (localeString.equalsIgnoreCase(mLocalePreference.getEntryValues()[i].toString())) { 363 selectedLocaleIndex = i; 364 break; 365 } 366 } 367 368 if (selectedLocaleIndex == -1) { 369 Log.w(TAG, "updateLanguageTo called with unknown locale argument"); 370 return; 371 } 372 mLocalePreference.setSummary(mLocalePreference.getEntries()[selectedLocaleIndex]); 373 mSelectedLocaleIndex = selectedLocaleIndex; 374 375 mEnginesHelper.updateLocalePrefForEngine(getEngineName(), locale); 376 377 if (getEngineName().equals(mTts.getCurrentEngine())) { 378 // Null locale means "use system default" 379 mTts.setLanguage((locale != null) ? locale : Locale.getDefault()); 380 } 381 } 382 getEngineName()383 private String getEngineName() { 384 return getArguments().getString(ARG_ENGINE_NAME); 385 } 386 getEngineLabel()387 private String getEngineLabel() { 388 return getArguments().getString(ARG_ENGINE_LABEL); 389 } 390 391 @Override getMetricsCategory()392 public int getMetricsCategory() { 393 return MetricsProto.MetricsEvent.TTS_ENGINE_SETTINGS; 394 } 395 } 396