1 package com.android.settings.tts; 2 3 import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH; 4 5 import android.app.settings.SettingsEnums; 6 import android.content.Context; 7 import android.content.DialogInterface; 8 import android.graphics.drawable.Drawable; 9 import android.os.Bundle; 10 import android.speech.tts.TextToSpeech; 11 import android.speech.tts.TextToSpeech.EngineInfo; 12 import android.speech.tts.TtsEngines; 13 import android.util.Log; 14 15 import androidx.appcompat.app.AlertDialog; 16 17 import com.android.settings.R; 18 import com.android.settings.search.BaseSearchIndexProvider; 19 import com.android.settings.widget.RadioButtonPickerFragment; 20 import com.android.settingslib.search.SearchIndexable; 21 import com.android.settingslib.widget.CandidateInfo; 22 23 import java.util.ArrayList; 24 import java.util.HashMap; 25 import java.util.List; 26 import java.util.Map; 27 28 @SearchIndexable 29 public class TtsEnginePreferenceFragment extends RadioButtonPickerFragment { 30 private static final String TAG = "TtsEnginePrefFragment"; 31 32 /** 33 * The previously selected TTS engine. Useful for rollbacks if the users choice is not loaded or 34 * fails a voice integrity check. 35 */ 36 private String mPreviousEngine; 37 38 private TextToSpeech mTts = null; 39 private TtsEngines mEnginesHelper = null; 40 private Context mContext; 41 private Map<String, EngineCandidateInfo> mEngineMap; 42 /** 43 * The initialization listener used when the user changes his choice of engine (as opposed to 44 * when then screen is being initialized for the first time). 45 */ 46 private final TextToSpeech.OnInitListener mUpdateListener = 47 new TextToSpeech.OnInitListener() { 48 @Override 49 public void onInit(int status) { 50 onUpdateEngine(status); 51 } 52 }; 53 54 @Override onCreate(Bundle savedInstanceState)55 public void onCreate(Bundle savedInstanceState) { 56 mContext = getContext().getApplicationContext(); 57 mEnginesHelper = new TtsEngines(mContext); 58 mEngineMap = new HashMap<>(); 59 mTts = new TextToSpeech(mContext, null); 60 61 super.onCreate(savedInstanceState); 62 } 63 64 @Override onDestroy()65 public void onDestroy() { 66 super.onDestroy(); 67 if (mTts != null) { 68 mTts.shutdown(); 69 mTts = null; 70 } 71 } 72 73 @Override getMetricsCategory()74 public int getMetricsCategory() { 75 return SettingsEnums.TTS_ENGINE_SETTINGS; 76 } 77 78 /** 79 * Step 3: We have now bound to the TTS engine the user requested. We will attempt to check 80 * voice data for the engine if we successfully bound to it, or revert to the previous engine if 81 * we didn't. 82 */ onUpdateEngine(int status)83 public void onUpdateEngine(int status) { 84 if (status == TextToSpeech.SUCCESS) { 85 Log.d(TAG, "Updating engine: Successfully bound to the engine: " 86 + mTts.getCurrentEngine()); 87 android.provider.Settings.Secure.putString( 88 mContext.getContentResolver(), TTS_DEFAULT_SYNTH, mTts.getCurrentEngine()); 89 } else { 90 Log.d(TAG, "Updating engine: Failed to bind to engine, reverting."); 91 if (mPreviousEngine != null) { 92 // This is guaranteed to at least bind, since mPreviousEngine would be 93 // null if the previous bind to this engine failed. 94 mTts = new TextToSpeech(mContext, null, mPreviousEngine); 95 updateCheckedState(mPreviousEngine); 96 } 97 mPreviousEngine = null; 98 } 99 } 100 101 @Override onRadioButtonConfirmed(String selectedKey)102 protected void onRadioButtonConfirmed(String selectedKey) { 103 final EngineCandidateInfo info = mEngineMap.get(selectedKey); 104 // Should we alert user? if that's true, delay making engine current one. 105 if (shouldDisplayDataAlert(info)) { 106 displayDataAlert(info, (dialog, which) -> { 107 setDefaultKey(selectedKey); 108 }); 109 } else { 110 // Privileged engine, set it current 111 setDefaultKey(selectedKey); 112 } 113 } 114 115 @Override getCandidates()116 protected List<? extends CandidateInfo> getCandidates() { 117 final List<EngineCandidateInfo> infos = new ArrayList<>(); 118 final List<EngineInfo> engines = mEnginesHelper.getEngines(); 119 for (EngineInfo engine : engines) { 120 final EngineCandidateInfo info = new EngineCandidateInfo(engine); 121 infos.add(info); 122 mEngineMap.put(engine.name, info); 123 } 124 return infos; 125 } 126 127 @Override getDefaultKey()128 protected String getDefaultKey() { 129 return mEnginesHelper.getDefaultEngine(); 130 } 131 132 @Override setDefaultKey(String key)133 protected boolean setDefaultKey(String key) { 134 updateDefaultEngine(key); 135 updateCheckedState(key); 136 return true; 137 } 138 139 @Override getPreferenceScreenResId()140 protected int getPreferenceScreenResId() { 141 return R.xml.tts_engine_picker; 142 } 143 shouldDisplayDataAlert(EngineCandidateInfo info)144 private boolean shouldDisplayDataAlert(EngineCandidateInfo info) { 145 return !info.isSystem(); 146 } 147 displayDataAlert(EngineCandidateInfo info, DialogInterface.OnClickListener positiveOnClickListener)148 private void displayDataAlert(EngineCandidateInfo info, 149 DialogInterface.OnClickListener positiveOnClickListener) { 150 Log.i(TAG, "Displaying data alert for :" + info.getKey()); 151 152 final AlertDialog dialog = new AlertDialog.Builder(getPrefContext()) 153 .setTitle(android.R.string.dialog_alert_title) 154 .setMessage(mContext.getString( 155 R.string.tts_engine_security_warning, info.loadLabel())) 156 .setCancelable(true) 157 .setPositiveButton(android.R.string.ok, positiveOnClickListener) 158 .setNegativeButton(android.R.string.cancel, null) 159 .create(); 160 161 dialog.show(); 162 } 163 updateDefaultEngine(String engine)164 private void updateDefaultEngine(String engine) { 165 Log.d(TAG, "Updating default synth to : " + engine); 166 167 // Keep track of the previous engine that was being used. So that 168 // we can reuse the previous engine. 169 // 170 // Note that if TextToSpeech#getCurrentEngine is not null, it means at 171 // the very least that we successfully bound to the engine service. 172 mPreviousEngine = mTts.getCurrentEngine(); 173 174 // Step 1: Shut down the existing TTS engine. 175 Log.i(TAG, "Shutting down current tts engine"); 176 if (mTts != null) { 177 try { 178 mTts.shutdown(); 179 mTts = null; 180 } catch (Exception e) { 181 Log.e(TAG, "Error shutting down TTS engine" + e); 182 } 183 } 184 185 // Step 2: Connect to the new TTS engine. 186 // Step 3 is continued on #onUpdateEngine (below) which is called when 187 // the app binds successfully to the engine. 188 Log.i(TAG, "Updating engine : Attempting to connect to engine: " + engine); 189 mTts = new TextToSpeech(mContext, mUpdateListener, engine); 190 Log.i(TAG, "Success"); 191 } 192 193 public static class EngineCandidateInfo extends CandidateInfo { 194 private final EngineInfo mEngineInfo; 195 EngineCandidateInfo(EngineInfo engineInfo)196 EngineCandidateInfo(EngineInfo engineInfo) { 197 super(true /* enabled */); 198 mEngineInfo = engineInfo; 199 } 200 201 @Override loadLabel()202 public CharSequence loadLabel() { 203 return mEngineInfo.label; 204 } 205 206 @Override loadIcon()207 public Drawable loadIcon() { 208 return null; 209 } 210 211 @Override getKey()212 public String getKey() { 213 return mEngineInfo.name; 214 } 215 isSystem()216 public boolean isSystem() { 217 return mEngineInfo.system; 218 } 219 } 220 221 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 222 new BaseSearchIndexProvider(R.xml.tts_engine_picker); 223 } 224