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                         com.android.settingslib.R.string.tts_engine_security_warning,
156                         info.loadLabel()))
157                 .setCancelable(true)
158                 .setPositiveButton(android.R.string.ok, positiveOnClickListener)
159                 .setNegativeButton(android.R.string.cancel, null)
160                 .create();
161 
162         dialog.show();
163     }
164 
updateDefaultEngine(String engine)165     private void updateDefaultEngine(String engine) {
166         Log.d(TAG, "Updating default synth to : " + engine);
167 
168         // Step 1: Shut down the existing TTS engine.
169         Log.i(TAG, "Shutting down current tts engine");
170         if (mTts != null) {
171             // Keep track of the previous engine that was being used. So that
172             // we can reuse the previous engine.
173             //
174             // Note that if TextToSpeech#getCurrentEngine is not null, it means at
175             // the very least that we successfully bound to the engine service.
176             mPreviousEngine = mTts.getCurrentEngine();
177 
178             try {
179                 mTts.shutdown();
180                 mTts = null;
181             } catch (Exception e) {
182                 Log.e(TAG, "Error shutting down TTS engine" + e);
183             }
184         }
185 
186         // Step 2: Connect to the new TTS engine.
187         // Step 3 is continued on #onUpdateEngine (below) which is called when
188         // the app binds successfully to the engine.
189         Log.i(TAG, "Updating engine : Attempting to connect to engine: " + engine);
190         mTts = new TextToSpeech(mContext, mUpdateListener, engine);
191         Log.i(TAG, "Success");
192     }
193 
194     public static class EngineCandidateInfo extends CandidateInfo {
195         private final EngineInfo mEngineInfo;
196 
EngineCandidateInfo(EngineInfo engineInfo)197         EngineCandidateInfo(EngineInfo engineInfo) {
198             super(true /* enabled */);
199             mEngineInfo = engineInfo;
200         }
201 
202         @Override
loadLabel()203         public CharSequence loadLabel() {
204             return mEngineInfo.label;
205         }
206 
207         @Override
loadIcon()208         public Drawable loadIcon() {
209             return null;
210         }
211 
212         @Override
getKey()213         public String getKey() {
214             return mEngineInfo.name;
215         }
216 
isSystem()217         public boolean isSystem() {
218             return mEngineInfo.system;
219         }
220     }
221 
222     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
223             new BaseSearchIndexProvider(R.xml.tts_engine_picker);
224 }
225