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