blob: 2bfc6ea499827e65edd17ee0dcfca055d810b297 [file] [log] [blame]
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +01001/*
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
Fan Zhang1105a1a2017-07-28 17:18:44 -070019import static android.provider.Settings.Secure.TTS_DEFAULT_PITCH;
20import static android.provider.Settings.Secure.TTS_DEFAULT_RATE;
21import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH;
22
Fan Zhang31b21002019-01-16 13:49:47 -080023import android.app.settings.SettingsEnums;
Jason Monk39b46742015-09-10 15:52:51 -040024import android.content.ActivityNotFoundException;
25import android.content.ContentResolver;
Fan Zhang18a16822017-08-16 15:18:33 -070026import android.content.Context;
Jason Monk39b46742015-09-10 15:52:51 -040027import android.content.Intent;
28import android.os.Bundle;
Josh Imbriani1f248702019-06-13 17:41:52 -070029import android.os.UserHandle;
30import android.os.UserManager;
Josh Imbriani1f248702019-06-13 17:41:52 -070031import android.provider.Settings.Secure;
Jason Monk39b46742015-09-10 15:52:51 -040032import android.speech.tts.TextToSpeech;
33import android.speech.tts.TextToSpeech.EngineInfo;
34import android.speech.tts.TtsEngines;
35import android.speech.tts.UtteranceProgressListener;
Jason Monk39b46742015-09-10 15:52:51 -040036import android.text.TextUtils;
37import android.util.Log;
Niels Egbertse5017dc2016-12-12 13:29:41 +000038import android.util.Pair;
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +010039
Fan Zhang9ce4a1f2018-08-15 12:55:56 -070040import androidx.appcompat.app.AlertDialog;
41import androidx.preference.ListPreference;
42import androidx.preference.Preference;
43
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +010044import com.android.settings.R;
Fabrice Di Meglio263bcc82014-01-17 19:17:58 -080045import com.android.settings.SettingsActivity;
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +010046import com.android.settings.SettingsPreferenceFragment;
Josh Imbriani1f248702019-06-13 17:41:52 -070047import com.android.settings.Utils;
Raff Tsaiac3e0d02019-09-19 17:06:45 +080048import com.android.settings.search.BaseSearchIndexProvider;
Niels Egberts46f38572017-03-20 19:57:17 +000049import com.android.settings.widget.GearPreference;
Fan Zhang1105a1a2017-07-28 17:18:44 -070050import com.android.settings.widget.SeekBarPreference;
Tony Mantler0fcd6cb2018-03-26 15:17:25 -070051import com.android.settingslib.search.SearchIndexable;
tmfang8ad18fb2018-11-28 16:59:15 +080052import com.android.settingslib.widget.ActionButtonsPreference;
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +010053
Josh Imbriani7d6263a2019-06-25 08:44:21 -070054import java.text.Collator;
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +010055import java.util.ArrayList;
Fan Zhang1105a1a2017-07-28 17:18:44 -070056import java.util.Collections;
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +010057import java.util.HashMap;
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +010058import java.util.List;
59import java.util.Locale;
Shuhrat Dehkanovf3652162014-03-12 22:57:08 +090060import java.util.MissingResourceException;
Craig Mautner5a98d432014-06-28 14:25:28 -070061import java.util.Objects;
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +010062import java.util.Set;
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +010063
Tony Mantler0fcd6cb2018-03-26 15:17:25 -070064@SearchIndexable
Niels Egbertse5017dc2016-12-12 13:29:41 +000065public class TextToSpeechSettings extends SettingsPreferenceFragment
Niels Egberts46f38572017-03-20 19:57:17 +000066 implements Preference.OnPreferenceChangeListener,
Fan Zhang78ea7da2018-07-02 13:44:57 -070067 GearPreference.OnGearClickListener {
Niels Egbertse5017dc2016-12-12 13:29:41 +000068
69 private static final String STATE_KEY_LOCALE_ENTRIES = "locale_entries";
70 private static final String STATE_KEY_LOCALE_ENTRY_VALUES = "locale_entry_values";
71 private static final String STATE_KEY_LOCALE_VALUE = "locale_value";
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +010072
73 private static final String TAG = "TextToSpeechSettings";
74 private static final boolean DBG = false;
75
Niels Egberts46f38572017-03-20 19:57:17 +000076 /** Preference key for the TTS pitch selection slider. */
77 private static final String KEY_DEFAULT_PITCH = "tts_default_pitch";
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +010078
Niels Egberts46f38572017-03-20 19:57:17 +000079 /** Preference key for the TTS rate selection slider. */
80 private static final String KEY_DEFAULT_RATE = "tts_default_rate";
81
82 /** Engine picker. */
83 private static final String KEY_TTS_ENGINE_PREFERENCE = "tts_engine_preference";
84
85 /** Locale picker. */
86 private static final String KEY_ENGINE_LOCALE = "tts_default_lang";
87
88 /** Play/Reset buttons container. */
89 private static final String KEY_ACTION_BUTTONS = "action_buttons";
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +010090
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +010091 /**
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +010092 * These look like birth years, but they aren't mine. I'm much younger than this.
93 */
94 private static final int GET_SAMPLE_TEXT = 1983;
95 private static final int VOICE_DATA_INTEGRITY_CHECK = 1977;
96
Niels Egberts46f38572017-03-20 19:57:17 +000097 /**
98 * Speech rate value. This value should be kept in sync with the max value set in tts_settings
99 * xml.
100 */
101 private static final int MAX_SPEECH_RATE = 600;
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100102
Niels Egberts46f38572017-03-20 19:57:17 +0000103 private static final int MIN_SPEECH_RATE = 10;
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100104
Niels Egberts46f38572017-03-20 19:57:17 +0000105 /**
106 * Speech pitch value. TTS pitch value varies from 25 to 400, where 100 is the value for normal
107 * pitch. The max pitch value is set to 400, based on feedback from users and the GoogleTTS
108 * pitch variation range. The range for pitch is not set in stone and should be readjusted based
109 * on user need. This value should be kept in sync with the max value set in tts_settings xml.
110 */
111 private static final int MAX_SPEECH_PITCH = 400;
112
113 private static final int MIN_SPEECH_PITCH = 25;
114
115 private SeekBarPreference mDefaultPitchPref;
116 private SeekBarPreference mDefaultRatePref;
tmfang8ad18fb2018-11-28 16:59:15 +0800117 private ActionButtonsPreference mActionButtons;
Niels Egberts46f38572017-03-20 19:57:17 +0000118
119 private int mDefaultPitch = TextToSpeech.Engine.DEFAULT_PITCH;
120 private int mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE;
Niels Egbertse5017dc2016-12-12 13:29:41 +0000121
122 private int mSelectedLocaleIndex = -1;
123
124 /** The currently selected engine. */
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100125 private String mCurrentEngine;
126
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100127 private TextToSpeech mTts = null;
128 private TtsEngines mEnginesHelper = null;
129
Craig Mautner5a98d432014-06-28 14:25:28 -0700130 private String mSampleText = null;
Przemyslaw Szczepaniakeb306b42013-09-10 10:35:57 +0100131
Niels Egbertse5017dc2016-12-12 13:29:41 +0000132 private ListPreference mLocalePreference;
Niels Egbertse5017dc2016-12-12 13:29:41 +0000133
Przemyslaw Szczepaniakeb306b42013-09-10 10:35:57 +0100134 /**
135 * Default locale used by selected TTS engine, null if not connected to any engine.
136 */
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100137 private Locale mCurrentDefaultLocale;
Przemyslaw Szczepaniakeb306b42013-09-10 10:35:57 +0100138
139 /**
140 * List of available locals of selected TTS engine, as returned by
141 * {@link TextToSpeech.Engine#ACTION_CHECK_TTS_DATA} activity. If empty, then activity
142 * was not yet called.
143 */
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100144 private List<String> mAvailableStrLocals;
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100145
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100146 /**
147 * The initialization listener used when we are initalizing the settings
148 * screen for the first time (as opposed to when a user changes his choice
149 * of engine).
150 */
Rakesh Iyer52fd31c2018-10-26 12:45:38 -0700151 private final TextToSpeech.OnInitListener mInitListener = this::onInitEngine;
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100152
Josh Imbriani1f248702019-06-13 17:41:52 -0700153 /**
154 * A UserManager used to set settings for both person and work profiles for a user
155 */
156 private UserManager mUserManager;
157
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100158 @Override
Fan Zhang65076132016-08-08 10:25:13 -0700159 public int getMetricsCategory() {
Fan Zhang31b21002019-01-16 13:49:47 -0800160 return SettingsEnums.TTS_TEXT_TO_SPEECH;
Chris Wren8a963ba2015-03-20 10:29:14 -0400161 }
162
163 @Override
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100164 public void onCreate(Bundle savedInstanceState) {
165 super.onCreate(savedInstanceState);
166 addPreferencesFromResource(R.xml.tts_settings);
167
168 getActivity().setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
169
Niels Egbertse5017dc2016-12-12 13:29:41 +0000170 mEnginesHelper = new TtsEngines(getActivity().getApplicationContext());
shwetachaharbc170fd2016-02-12 17:17:33 +0000171
Niels Egbertse5017dc2016-12-12 13:29:41 +0000172 mLocalePreference = (ListPreference) findPreference(KEY_ENGINE_LOCALE);
173 mLocalePreference.setOnPreferenceChangeListener(this);
174
Niels Egberts46f38572017-03-20 19:57:17 +0000175 mDefaultPitchPref = (SeekBarPreference) findPreference(KEY_DEFAULT_PITCH);
176 mDefaultRatePref = (SeekBarPreference) findPreference(KEY_DEFAULT_RATE);
177
tmfang8ad18fb2018-11-28 16:59:15 +0800178 mActionButtons = ((ActionButtonsPreference) findPreference(KEY_ACTION_BUTTONS))
Fan Zhang1105a1a2017-07-28 17:18:44 -0700179 .setButton1Text(R.string.tts_play)
Fan Zhang1105a1a2017-07-28 17:18:44 -0700180 .setButton1OnClickListener(v -> speakSampleText())
181 .setButton1Enabled(false)
182 .setButton2Text(R.string.tts_reset)
Fan Zhang1105a1a2017-07-28 17:18:44 -0700183 .setButton2OnClickListener(v -> resetTts())
184 .setButton1Enabled(true);
Niels Egberts46f38572017-03-20 19:57:17 +0000185
Josh Imbriani1f248702019-06-13 17:41:52 -0700186 mUserManager = (UserManager) getActivity()
187 .getApplicationContext().getSystemService(Context.USER_SERVICE);
188
Niels Egbertse5017dc2016-12-12 13:29:41 +0000189 if (savedInstanceState == null) {
190 mLocalePreference.setEnabled(false);
191 mLocalePreference.setEntries(new CharSequence[0]);
192 mLocalePreference.setEntryValues(new CharSequence[0]);
193 } else {
194 // Repopulate mLocalePreference with saved state. Will be updated later with
195 // up-to-date values when checkTtsData() calls back with results.
196 final CharSequence[] entries =
197 savedInstanceState.getCharSequenceArray(STATE_KEY_LOCALE_ENTRIES);
198 final CharSequence[] entryValues =
199 savedInstanceState.getCharSequenceArray(STATE_KEY_LOCALE_ENTRY_VALUES);
200 final CharSequence value = savedInstanceState.getCharSequence(STATE_KEY_LOCALE_VALUE);
201
202 mLocalePreference.setEntries(entries);
203 mLocalePreference.setEntryValues(entryValues);
204 mLocalePreference.setValue(value != null ? value.toString() : null);
205 mLocalePreference.setEnabled(entries.length > 0);
206 }
207
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100208 mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener);
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100209
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +0100210 setTtsUtteranceProgressListener();
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100211 initSettings();
Przemyslaw Szczepaniak3b608582014-05-28 11:57:07 +0100212
213 // Prevent restarting the TTS connection on rotation
214 setRetainInstance(true);
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100215 }
216
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100217 @Override
218 public void onResume() {
219 super.onResume();
Rakesh Iyer52fd31c2018-10-26 12:45:38 -0700220 // We tend to change the summary contents of our widgets, which at higher text sizes causes
221 // them to resize, which results in the recyclerview smoothly animating them at inopportune
222 // times. Disable the animation so widgets snap to their positions rather than sliding
223 // around while the user is interacting with it.
224 getListView().getItemAnimator().setMoveDuration(0);
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100225
226 if (mTts == null || mCurrentDefaultLocale == null) {
227 return;
228 }
Niels Egbertse5017dc2016-12-12 13:29:41 +0000229 if (!mTts.getDefaultEngine().equals(mTts.getCurrentEngine())) {
230 try {
231 mTts.shutdown();
232 mTts = null;
233 } catch (Exception e) {
234 Log.e(TAG, "Error shutting down TTS engine" + e);
235 }
236 mTts = new TextToSpeech(getActivity().getApplicationContext(), mInitListener);
237 setTtsUtteranceProgressListener();
238 initSettings();
239 } else {
240 // Do set pitch correctly after it may have changed, and unlike speed, it doesn't change
241 // immediately.
242 final ContentResolver resolver = getContentResolver();
Fan Zhang18a16822017-08-16 15:18:33 -0700243 mTts.setPitch(android.provider.Settings.Secure.getInt(resolver, TTS_DEFAULT_PITCH,
244 TextToSpeech.Engine.DEFAULT_PITCH) / 100.0f);
Niels Egbertse5017dc2016-12-12 13:29:41 +0000245 }
246
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100247 Locale ttsDefaultLocale = mTts.getDefaultLanguage();
248 if (mCurrentDefaultLocale != null && !mCurrentDefaultLocale.equals(ttsDefaultLocale)) {
249 updateWidgetState(false);
250 checkDefaultLocale();
251 }
252 }
253
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +0100254 private void setTtsUtteranceProgressListener() {
255 if (mTts == null) {
256 return;
257 }
258 mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
259 @Override
Fan Zhang18a16822017-08-16 15:18:33 -0700260 public void onStart(String utteranceId) {
Rakesh Iyerf4627bb2018-11-02 12:34:52 -0700261 updateWidgetState(false);
Fan Zhang18a16822017-08-16 15:18:33 -0700262 }
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +0100263
264 @Override
Fan Zhang18a16822017-08-16 15:18:33 -0700265 public void onDone(String utteranceId) {
Rakesh Iyerf4627bb2018-11-02 12:34:52 -0700266 updateWidgetState(true);
Fan Zhang18a16822017-08-16 15:18:33 -0700267 }
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +0100268
269 @Override
270 public void onError(String utteranceId) {
271 Log.e(TAG, "Error while trying to synthesize sample text");
Rakesh Iyerf4627bb2018-11-02 12:34:52 -0700272 // Re-enable just in case, although there isn't much hope that following synthesis
273 // requests are going to succeed.
274 updateWidgetState(true);
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +0100275 }
276 });
277 }
278
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100279 @Override
280 public void onDestroy() {
281 super.onDestroy();
282 if (mTts != null) {
283 mTts.shutdown();
284 mTts = null;
285 }
286 }
287
Niels Egberts4b891422017-04-12 10:08:23 +0100288 @Override
289 public void onSaveInstanceState(Bundle outState) {
290 super.onSaveInstanceState(outState);
291
292 // Save the mLocalePreference values, so we can repopulate it with entries.
293 outState.putCharSequenceArray(STATE_KEY_LOCALE_ENTRIES,
294 mLocalePreference.getEntries());
295 outState.putCharSequenceArray(STATE_KEY_LOCALE_ENTRY_VALUES,
296 mLocalePreference.getEntryValues());
297 outState.putCharSequence(STATE_KEY_LOCALE_VALUE,
298 mLocalePreference.getValue());
299 }
300
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100301 private void initSettings() {
302 final ContentResolver resolver = getContentResolver();
303
Niels Egberts46f38572017-03-20 19:57:17 +0000304 // Set up the default rate and pitch.
305 mDefaultRate =
306 android.provider.Settings.Secure.getInt(
307 resolver, TTS_DEFAULT_RATE, TextToSpeech.Engine.DEFAULT_RATE);
308 mDefaultPitch =
309 android.provider.Settings.Secure.getInt(
310 resolver, TTS_DEFAULT_PITCH, TextToSpeech.Engine.DEFAULT_PITCH);
311
312 mDefaultRatePref.setProgress(getSeekBarProgressFromValue(KEY_DEFAULT_RATE, mDefaultRate));
313 mDefaultRatePref.setOnPreferenceChangeListener(this);
314 mDefaultRatePref.setMax(getSeekBarProgressFromValue(KEY_DEFAULT_RATE, MAX_SPEECH_RATE));
315
316 mDefaultPitchPref.setProgress(
317 getSeekBarProgressFromValue(KEY_DEFAULT_PITCH, mDefaultPitch));
318 mDefaultPitchPref.setOnPreferenceChangeListener(this);
319 mDefaultPitchPref.setMax(getSeekBarProgressFromValue(KEY_DEFAULT_PITCH, MAX_SPEECH_PITCH));
320
shwetachahar87f3e112016-04-15 14:25:07 +0100321 if (mTts != null) {
322 mCurrentEngine = mTts.getCurrentEngine();
Niels Egberts46f38572017-03-20 19:57:17 +0000323 mTts.setSpeechRate(mDefaultRate / 100.0f);
324 mTts.setPitch(mDefaultPitch / 100.0f);
shwetachahar87f3e112016-04-15 14:25:07 +0100325 }
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100326
Fabrice Di Meglio263bcc82014-01-17 19:17:58 -0800327 SettingsActivity activity = null;
328 if (getActivity() instanceof SettingsActivity) {
329 activity = (SettingsActivity) getActivity();
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100330 } else {
331 throw new IllegalStateException("TextToSpeechSettings used outside a " +
Fabrice Di Meglio263bcc82014-01-17 19:17:58 -0800332 "Settings");
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100333 }
334
Niels Egbertse5017dc2016-12-12 13:29:41 +0000335 if (mCurrentEngine != null) {
336 EngineInfo info = mEnginesHelper.getEngineInfo(mCurrentEngine);
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100337
Niels Egberts46f38572017-03-20 19:57:17 +0000338 Preference mEnginePreference = findPreference(KEY_TTS_ENGINE_PREFERENCE);
339 ((GearPreference) mEnginePreference).setOnGearClickListener(this);
Niels Egbertse5017dc2016-12-12 13:29:41 +0000340 mEnginePreference.setSummary(info.label);
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100341 }
342
343 checkVoiceData(mCurrentEngine);
344 }
345
346 /**
Niels Egberts46f38572017-03-20 19:57:17 +0000347 * The minimum speech pitch/rate value should be > 0 but the minimum value of a seekbar in
348 * android is fixed at 0. Therefore, we increment the seekbar progress with MIN_SPEECH_VALUE so
349 * that the minimum seekbar progress value is MIN_SPEECH_PITCH/RATE. SPEECH_VALUE =
350 * MIN_SPEECH_VALUE + SEEKBAR_PROGRESS
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100351 */
Niels Egberts46f38572017-03-20 19:57:17 +0000352 private int getValueFromSeekBarProgress(String preferenceKey, int progress) {
353 if (preferenceKey.equals(KEY_DEFAULT_RATE)) {
354 return MIN_SPEECH_RATE + progress;
355 } else if (preferenceKey.equals(KEY_DEFAULT_PITCH)) {
356 return MIN_SPEECH_PITCH + progress;
357 }
358 return progress;
359 }
360
361 /**
362 * Since we are appending the MIN_SPEECH value to the speech seekbar progress, the speech
363 * seekbar progress should be set to (speechValue - MIN_SPEECH value).
364 */
365 private int getSeekBarProgressFromValue(String preferenceKey, int value) {
366 if (preferenceKey.equals(KEY_DEFAULT_RATE)) {
367 return value - MIN_SPEECH_RATE;
368 } else if (preferenceKey.equals(KEY_DEFAULT_PITCH)) {
369 return value - MIN_SPEECH_PITCH;
370 }
371 return value;
372 }
373
374 /** Called when the TTS engine is initialized. */
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100375 public void onInitEngine(int status) {
376 if (status == TextToSpeech.SUCCESS) {
377 if (DBG) Log.d(TAG, "TTS engine for settings screen initialized.");
378 checkDefaultLocale();
Rakesh Iyer52fd31c2018-10-26 12:45:38 -0700379 getActivity().runOnUiThread(() -> mLocalePreference.setEnabled(true));
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100380 } else {
Fan Zhang18a16822017-08-16 15:18:33 -0700381 if (DBG) {
382 Log.d(TAG,
383 "TTS engine for settings screen failed to initialize successfully.");
384 }
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100385 updateWidgetState(false);
386 }
387 }
388
389 private void checkDefaultLocale() {
390 Locale defaultLocale = mTts.getDefaultLanguage();
391 if (defaultLocale == null) {
392 Log.e(TAG, "Failed to get default language from engine " + mCurrentEngine);
393 updateWidgetState(false);
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100394 return;
395 }
396
Przemyslaw Szczepaniak6ac8ddb2014-06-19 16:01:39 +0100397 // ISO-3166 alpha 3 country codes are out of spec. If we won't normalize,
398 // we may end up with English (USA)and German (DEU).
Craig Mautner5a98d432014-06-28 14:25:28 -0700399 final Locale oldDefaultLocale = mCurrentDefaultLocale;
Przemyslaw Szczepaniak6ac8ddb2014-06-19 16:01:39 +0100400 mCurrentDefaultLocale = mEnginesHelper.parseLocaleString(defaultLocale.toString());
Craig Mautner5a98d432014-06-28 14:25:28 -0700401 if (!Objects.equals(oldDefaultLocale, mCurrentDefaultLocale)) {
402 mSampleText = null;
403 }
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100404
405 int defaultAvailable = mTts.setLanguage(defaultLocale);
Craig Mautner5a98d432014-06-28 14:25:28 -0700406 if (evaluateDefaultLocale() && mSampleText == null) {
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100407 getSampleText();
408 }
409 }
410
411 private boolean evaluateDefaultLocale() {
Przemyslaw Szczepaniakeb306b42013-09-10 10:35:57 +0100412 // Check if we are connected to the engine, and CHECK_VOICE_DATA returned list
413 // of available languages.
414 if (mCurrentDefaultLocale == null || mAvailableStrLocals == null) {
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100415 return false;
416 }
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100417
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100418 boolean notInAvailableLangauges = true;
Shuhrat Dehkanovf3652162014-03-12 22:57:08 +0900419 try {
420 // Check if language is listed in CheckVoices Action result as available voice.
421 String defaultLocaleStr = mCurrentDefaultLocale.getISO3Language();
422 if (!TextUtils.isEmpty(mCurrentDefaultLocale.getISO3Country())) {
423 defaultLocaleStr += "-" + mCurrentDefaultLocale.getISO3Country();
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100424 }
Shuhrat Dehkanovf3652162014-03-12 22:57:08 +0900425 if (!TextUtils.isEmpty(mCurrentDefaultLocale.getVariant())) {
426 defaultLocaleStr += "-" + mCurrentDefaultLocale.getVariant();
427 }
428
429 for (String loc : mAvailableStrLocals) {
430 if (loc.equalsIgnoreCase(defaultLocaleStr)) {
Fan Zhang18a16822017-08-16 15:18:33 -0700431 notInAvailableLangauges = false;
432 break;
Shuhrat Dehkanovf3652162014-03-12 22:57:08 +0900433 }
434 }
435 } catch (MissingResourceException e) {
436 if (DBG) Log.wtf(TAG, "MissingResourceException", e);
Shuhrat Dehkanovf3652162014-03-12 22:57:08 +0900437 updateWidgetState(false);
438 return false;
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100439 }
440
Shuhrat Dehkanovf3652162014-03-12 22:57:08 +0900441 int defaultAvailable = mTts.setLanguage(mCurrentDefaultLocale);
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100442 if (defaultAvailable == TextToSpeech.LANG_NOT_SUPPORTED ||
443 defaultAvailable == TextToSpeech.LANG_MISSING_DATA ||
Przemyslaw Szczepaniakeb306b42013-09-10 10:35:57 +0100444 notInAvailableLangauges) {
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100445 if (DBG) Log.d(TAG, "Default locale for this TTS engine is not supported.");
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100446 updateWidgetState(false);
447 return false;
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100448 } else {
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100449 updateWidgetState(true);
450 return true;
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100451 }
452 }
453
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100454 /**
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100455 * Ask the current default engine to return a string of sample text to be
456 * spoken to the user.
457 */
458 private void getSampleText() {
459 String currentEngine = mTts.getCurrentEngine();
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100460
461 if (TextUtils.isEmpty(currentEngine)) currentEngine = mTts.getDefaultEngine();
462
463 // TODO: This is currently a hidden private API. The intent extras
464 // and the intent action should be made public if we intend to make this
465 // a public API. We fall back to using a canned set of strings if this
466 // doesn't work.
467 Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT);
468
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100469 intent.putExtra("language", mCurrentDefaultLocale.getLanguage());
470 intent.putExtra("country", mCurrentDefaultLocale.getCountry());
471 intent.putExtra("variant", mCurrentDefaultLocale.getVariant());
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100472 intent.setPackage(currentEngine);
473
474 try {
475 if (DBG) Log.d(TAG, "Getting sample text: " + intent.toUri(0));
476 startActivityForResult(intent, GET_SAMPLE_TEXT);
477 } catch (ActivityNotFoundException ex) {
478 Log.e(TAG, "Failed to get sample text, no activity found for " + intent + ")");
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100479 }
480 }
481
482 /**
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100483 * Called when voice data integrity check returns
484 */
485 @Override
486 public void onActivityResult(int requestCode, int resultCode, Intent data) {
487 if (requestCode == GET_SAMPLE_TEXT) {
488 onSampleTextReceived(resultCode, data);
489 } else if (requestCode == VOICE_DATA_INTEGRITY_CHECK) {
490 onVoiceDataIntegrityCheckDone(data);
Niels Egbertse5017dc2016-12-12 13:29:41 +0000491 if (resultCode != TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL) {
492 updateDefaultLocalePref(data);
493 }
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100494 }
495 }
496
Niels Egbertse5017dc2016-12-12 13:29:41 +0000497 private void updateDefaultLocalePref(Intent data) {
498 final ArrayList<String> availableLangs =
499 data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
500
501 final ArrayList<String> unavailableLangs =
502 data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_UNAVAILABLE_VOICES);
503
Niels Egbertse5017dc2016-12-12 13:29:41 +0000504 if (availableLangs == null || availableLangs.size() == 0) {
505 mLocalePreference.setEnabled(false);
506 return;
507 }
508 Locale currentLocale = null;
509 if (!mEnginesHelper.isLocaleSetToDefaultForEngine(mTts.getCurrentEngine())) {
510 currentLocale = mEnginesHelper.getLocalePrefForEngine(mTts.getCurrentEngine());
511 }
512
513 ArrayList<Pair<String, Locale>> entryPairs =
514 new ArrayList<Pair<String, Locale>>(availableLangs.size());
515 for (int i = 0; i < availableLangs.size(); i++) {
516 Locale locale = mEnginesHelper.parseLocaleString(availableLangs.get(i));
517 if (locale != null) {
518 entryPairs.add(new Pair<String, Locale>(locale.getDisplayName(), locale));
519 }
520 }
521
Josh Imbriani7d6263a2019-06-25 08:44:21 -0700522 // Get the primary locale and create a Collator to sort the strings
523 Locale userLocale = getResources().getConfiguration().getLocales().get(0);
524 Collator collator = Collator.getInstance(userLocale);
525
526 // Sort the list
527 Collections.sort(entryPairs, (lhs, rhs) -> collator.compare(lhs.first, rhs.first));
Niels Egbertse5017dc2016-12-12 13:29:41 +0000528
529 // Get two arrays out of one of pairs
530 mSelectedLocaleIndex = 0; // Will point to the R.string.tts_lang_use_system value
531 CharSequence[] entries = new CharSequence[availableLangs.size() + 1];
532 CharSequence[] entryValues = new CharSequence[availableLangs.size() + 1];
533
534 entries[0] = getActivity().getString(R.string.tts_lang_use_system);
535 entryValues[0] = "";
536
537 int i = 1;
538 for (Pair<String, Locale> entry : entryPairs) {
539 if (entry.second.equals(currentLocale)) {
540 mSelectedLocaleIndex = i;
541 }
542 entries[i] = entry.first;
543 entryValues[i++] = entry.second.toString();
544 }
545
546 mLocalePreference.setEntries(entries);
547 mLocalePreference.setEntryValues(entryValues);
548 mLocalePreference.setEnabled(true);
549 setLocalePreference(mSelectedLocaleIndex);
550 }
551
552 /** Set entry from entry table in mLocalePreference */
553 private void setLocalePreference(int index) {
554 if (index < 0) {
555 mLocalePreference.setValue("");
556 mLocalePreference.setSummary(R.string.tts_lang_not_selected);
557 } else {
558 mLocalePreference.setValueIndex(index);
559 mLocalePreference.setSummary(mLocalePreference.getEntries()[index]);
560 }
561 }
562
563
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100564 private String getDefaultSampleString() {
565 if (mTts != null && mTts.getLanguage() != null) {
Shuhrat Dehkanovf3652162014-03-12 22:57:08 +0900566 try {
567 final String currentLang = mTts.getLanguage().getISO3Language();
568 String[] strings = getActivity().getResources().getStringArray(
569 R.array.tts_demo_strings);
570 String[] langs = getActivity().getResources().getStringArray(
571 R.array.tts_demo_string_langs);
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100572
Shuhrat Dehkanovf3652162014-03-12 22:57:08 +0900573 for (int i = 0; i < strings.length; ++i) {
574 if (langs[i].equals(currentLang)) {
575 return strings[i];
576 }
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100577 }
Shuhrat Dehkanovf3652162014-03-12 22:57:08 +0900578 } catch (MissingResourceException e) {
579 if (DBG) Log.wtf(TAG, "MissingResourceException", e);
580 // Ignore and fall back to default sample string
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100581 }
582 }
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100583 return getString(R.string.tts_default_sample_string);
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100584 }
585
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +0100586 private boolean isNetworkRequiredForSynthesis() {
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100587 Set<String> features = mTts.getFeatures(mCurrentDefaultLocale);
Przemyslaw Szczepaniak0e01f122013-08-13 13:05:48 +0100588 if (features == null) {
Fan Zhang18a16822017-08-16 15:18:33 -0700589 return false;
Przemyslaw Szczepaniak0e01f122013-08-13 13:05:48 +0100590 }
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +0100591 return features.contains(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS) &&
592 !features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
593 }
594
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100595 private void onSampleTextReceived(int resultCode, Intent data) {
596 String sample = getDefaultSampleString();
597
598 if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) {
599 if (data != null && data.getStringExtra("sampleText") != null) {
600 sample = data.getStringExtra("sampleText");
601 }
602 if (DBG) Log.d(TAG, "Got sample text: " + sample);
603 } else {
604 if (DBG) Log.d(TAG, "Using default sample text :" + sample);
605 }
606
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100607 mSampleText = sample;
608 if (mSampleText != null) {
609 updateWidgetState(true);
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100610 } else {
Przemyslaw Szczepaniak4c85c1d2013-08-02 17:06:41 +0100611 Log.e(TAG, "Did not have a sample string for the requested language. Using default");
612 }
613 }
614
615 private void speakSampleText() {
616 final boolean networkRequired = isNetworkRequiredForSynthesis();
617 if (!networkRequired || networkRequired &&
618 (mTts.isLanguageAvailable(mCurrentDefaultLocale) >= TextToSpeech.LANG_AVAILABLE)) {
619 HashMap<String, String> params = new HashMap<String, String>();
620 params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "Sample");
621
622 mTts.speak(mSampleText, TextToSpeech.QUEUE_FLUSH, params);
623 } else {
624 Log.w(TAG, "Network required for sample synthesis for requested language");
625 displayNetworkAlert();
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100626 }
627 }
628
Przemyslaw Szczepaniak923187a2012-08-09 12:50:41 +0100629 @Override
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100630 public boolean onPreferenceChange(Preference preference, Object objValue) {
Niels Egberts46f38572017-03-20 19:57:17 +0000631 if (KEY_DEFAULT_RATE.equals(preference.getKey())) {
632 updateSpeechRate((Integer) objValue);
633 } else if (KEY_DEFAULT_PITCH.equals(preference.getKey())) {
634 updateSpeechPitchValue((Integer) objValue);
635 } else if (preference == mLocalePreference) {
Niels Egbertse5017dc2016-12-12 13:29:41 +0000636 String localeString = (String) objValue;
637 updateLanguageTo(
638 (!TextUtils.isEmpty(localeString)
639 ? mEnginesHelper.parseLocaleString(localeString)
640 : null));
641 checkDefaultLocale();
642 return true;
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100643 }
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100644 return true;
645 }
646
Niels Egbertse5017dc2016-12-12 13:29:41 +0000647 private void updateLanguageTo(Locale locale) {
648 int selectedLocaleIndex = -1;
649 String localeString = (locale != null) ? locale.toString() : "";
650 for (int i = 0; i < mLocalePreference.getEntryValues().length; i++) {
651 if (localeString.equalsIgnoreCase(mLocalePreference.getEntryValues()[i].toString())) {
652 selectedLocaleIndex = i;
653 break;
654 }
655 }
656
657 if (selectedLocaleIndex == -1) {
658 Log.w(TAG, "updateLanguageTo called with unknown locale argument");
659 return;
660 }
661 mLocalePreference.setSummary(mLocalePreference.getEntries()[selectedLocaleIndex]);
662 mSelectedLocaleIndex = selectedLocaleIndex;
663
664 mEnginesHelper.updateLocalePrefForEngine(mTts.getCurrentEngine(), locale);
665
666 // Null locale means "use system default"
667 mTts.setLanguage((locale != null) ? locale : Locale.getDefault());
668 }
669
Fan Zhang1105a1a2017-07-28 17:18:44 -0700670 private void resetTts() {
671 // Reset button.
672 int speechRateSeekbarProgress =
673 getSeekBarProgressFromValue(
674 KEY_DEFAULT_RATE, TextToSpeech.Engine.DEFAULT_RATE);
675 mDefaultRatePref.setProgress(speechRateSeekbarProgress);
676 updateSpeechRate(speechRateSeekbarProgress);
677 int pitchSeekbarProgress =
678 getSeekBarProgressFromValue(
679 KEY_DEFAULT_PITCH, TextToSpeech.Engine.DEFAULT_PITCH);
680 mDefaultPitchPref.setProgress(pitchSeekbarProgress);
681 updateSpeechPitchValue(pitchSeekbarProgress);
Niels Egbertse5017dc2016-12-12 13:29:41 +0000682 }
683
Niels Egberts46f38572017-03-20 19:57:17 +0000684 private void updateSpeechRate(int speechRateSeekBarProgress) {
685 mDefaultRate = getValueFromSeekBarProgress(KEY_DEFAULT_RATE, speechRateSeekBarProgress);
686 try {
Josh Imbriani1f248702019-06-13 17:41:52 -0700687 updateTTSSetting(TTS_DEFAULT_RATE, mDefaultRate);
Niels Egberts46f38572017-03-20 19:57:17 +0000688 if (mTts != null) {
689 mTts.setSpeechRate(mDefaultRate / 100.0f);
690 }
691 if (DBG) Log.d(TAG, "TTS default rate changed, now " + mDefaultRate);
692 } catch (NumberFormatException e) {
693 Log.e(TAG, "could not persist default TTS rate setting", e);
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100694 }
Niels Egberts46f38572017-03-20 19:57:17 +0000695 return;
696 }
697
698 private void updateSpeechPitchValue(int speechPitchSeekBarProgress) {
699 mDefaultPitch = getValueFromSeekBarProgress(KEY_DEFAULT_PITCH, speechPitchSeekBarProgress);
700 try {
Josh Imbriani1f248702019-06-13 17:41:52 -0700701 updateTTSSetting(TTS_DEFAULT_PITCH, mDefaultPitch);
Niels Egberts46f38572017-03-20 19:57:17 +0000702 if (mTts != null) {
703 mTts.setPitch(mDefaultPitch / 100.0f);
704 }
705 if (DBG) Log.d(TAG, "TTS default pitch changed, now" + mDefaultPitch);
706 } catch (NumberFormatException e) {
707 Log.e(TAG, "could not persist default TTS pitch setting", e);
708 }
709 return;
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100710 }
711
Josh Imbriani1f248702019-06-13 17:41:52 -0700712 private void updateTTSSetting(String key, int value) {
713 Secure.putInt(
714 getContentResolver(), key, value);
715 final int managedProfileUserId =
716 Utils.getManagedProfileId(mUserManager, UserHandle.myUserId());
717 if (managedProfileUserId != UserHandle.USER_NULL) {
718 Secure.putIntForUser(getContentResolver(), key, value, managedProfileUserId);
719 }
720 }
721
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100722 private void updateWidgetState(boolean enable) {
Rakesh Iyerf4627bb2018-11-02 12:34:52 -0700723 getActivity().runOnUiThread(() -> {
724 mActionButtons.setButton1Enabled(enable);
725 mDefaultRatePref.setEnabled(enable);
726 mDefaultPitchPref.setEnabled(enable);
727 });
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100728 }
729
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +0100730 private void displayNetworkAlert() {
731 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
Alan Viverette6bfec2d2014-06-10 13:41:07 -0700732 builder.setTitle(android.R.string.dialog_alert_title)
733 .setMessage(getActivity().getString(R.string.tts_engine_network_required))
734 .setCancelable(false)
735 .setPositiveButton(android.R.string.ok, null);
Przemyslaw Szczepaniak03b9f862012-10-23 16:39:02 +0100736
737 AlertDialog dialog = builder.create();
738 dialog.show();
739 }
740
Niels Egbertse5017dc2016-12-12 13:29:41 +0000741 /** Check whether the voice data for the engine is ok. */
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100742 private void checkVoiceData(String engine) {
743 Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
744 intent.setPackage(engine);
745 try {
746 if (DBG) Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0));
747 startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK);
748 } catch (ActivityNotFoundException ex) {
749 Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")");
750 }
751 }
752
Niels Egbertse5017dc2016-12-12 13:29:41 +0000753 /** The voice data check is complete. */
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100754 private void onVoiceDataIntegrityCheckDone(Intent data) {
755 final String engine = mTts.getCurrentEngine();
756
757 if (engine == null) {
758 Log.e(TAG, "Voice data check complete, but no engine bound");
759 return;
760 }
761
Fan Zhang18a16822017-08-16 15:18:33 -0700762 if (data == null) {
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100763 Log.e(TAG, "Engine failed voice data integrity check (null return)" +
764 mTts.getCurrentEngine());
765 return;
766 }
767
Fabrice Di Meglio263bcc82014-01-17 19:17:58 -0800768 android.provider.Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine);
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100769
Przemyslaw Szczepaniak6ada2d52013-08-06 13:55:52 +0100770 mAvailableStrLocals = data.getStringArrayListExtra(
Fan Zhang18a16822017-08-16 15:18:33 -0700771 TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
Przemyslaw Szczepaniakeb306b42013-09-10 10:35:57 +0100772 if (mAvailableStrLocals == null) {
773 Log.e(TAG, "Voice data check complete, but no available voices found");
774 // Set mAvailableStrLocals to empty list
775 mAvailableStrLocals = new ArrayList<String>();
776 }
777 if (evaluateDefaultLocale()) {
778 getSampleText();
779 }
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100780 }
Niels Egberts46f38572017-03-20 19:57:17 +0000781
782 @Override
783 public void onGearClick(GearPreference p) {
784 if (KEY_TTS_ENGINE_PREFERENCE.equals(p.getKey())) {
785 EngineInfo info = mEnginesHelper.getEngineInfo(mCurrentEngine);
786 final Intent settingsIntent = mEnginesHelper.getSettingsIntent(info.name);
tiansimingb65c4c32017-12-06 21:29:15 +0800787 if (settingsIntent != null) {
788 startActivity(settingsIntent);
789 } else {
790 Log.e(TAG, "settingsIntent is null");
791 }
Niels Egberts46f38572017-03-20 19:57:17 +0000792 }
793 }
794
Raff Tsaiac3e0d02019-09-19 17:06:45 +0800795 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
Raff Tsai1f30b1c2019-09-12 10:56:13 +0800796 new BaseSearchIndexProvider(R.xml.tts_settings);
Fan Zhang18a16822017-08-16 15:18:33 -0700797
Narayan Kamath0cfbb0f2011-08-18 17:55:41 +0100798}