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