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