1 /*
2  * Copyright (C) 2014 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 
17 package com.android.tv.settings.system;
18 
19 import com.android.internal.view.RotationPolicy;
20 import com.android.tv.settings.ActionBehavior;
21 import com.android.tv.settings.BaseSettingsActivity;
22 
23 import static android.provider.Settings.Secure.TTS_DEFAULT_RATE;
24 import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH;
25 import com.android.tv.settings.ActionKey;
26 import com.android.tv.settings.R;
27 import com.android.tv.settings.system.DeveloperOptionsActivity.MyApplicationInfo;
28 import com.android.tv.settings.util.SettingsHelper;
29 import com.android.tv.settings.dialog.old.Action;
30 import com.android.tv.settings.dialog.old.ActionAdapter;
31 import com.android.tv.settings.dialog.old.ContentFragment;
32 import com.android.tv.settings.util.SettingsHelper;
33 
34 import android.accessibilityservice.AccessibilityServiceInfo;
35 import android.content.ActivityNotFoundException;
36 import android.content.ComponentName;
37 import android.content.Context;
38 import android.view.accessibility.AccessibilityManager;
39 import android.widget.TextView;
40 import android.content.Intent;
41 import android.content.pm.ResolveInfo;
42 import android.content.pm.ServiceInfo;
43 import android.os.Bundle;
44 import android.provider.Settings;
45 import android.speech.tts.TextToSpeech;
46 import android.text.TextUtils.SimpleStringSplitter;
47 import android.speech.tts.TtsEngines;
48 import android.util.Log;
49 import android.util.Pair;
50 import android.speech.tts.TextToSpeech.EngineInfo;
51 import android.speech.tts.UtteranceProgressListener;
52 import android.text.TextUtils;
53 
54 import java.util.ArrayList;
55 import java.util.List;
56 import java.util.Collections;
57 import java.util.Comparator;
58 import java.util.HashMap;
59 import java.util.HashSet;
60 import java.util.Locale;
61 import java.util.Set;
62 
63 public class AccessibilityActivity extends BaseSettingsActivity implements ActionAdapter.Listener {
64 
65     private static final String TAG = "AccessibilityActivity";
66     private static final boolean DEBUG = false;
67 
68     private static final int GET_SAMPLE_TEXT = 1983;
69     private static final int VOICE_DATA_INTEGRITY_CHECK = 1977;
70 
71     private static final char ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ':';
72 
73     private SettingsHelper mHelper;
74 
75     private boolean mTtsSettingsEnabled;
76     private TextToSpeech mTts = null;
77     private TtsEngines mEnginesHelper = null;
78     private String mCurrentEngine;
79     private String mPreviousEngine;
80     private Intent mVoiceCheckData;
81 
82     private String mServiceSettingTitle;
83     private String mSelectedServiceComponent;
84     private String mSelectedServiceSettings;
85     private boolean mSelectedServiceEnabled;
86 
87     /**
88      * The initialization listener used when we are initalizing the settings
89      * screen for the first time (as opposed to when a user changes his choice
90      * of engine).
91      */
92     private final TextToSpeech.OnInitListener mInitListener = new TextToSpeech.OnInitListener() {
93             @Override
94         public void onInit(int status) {
95             onInitEngine(status);
96         }
97     };
98 
99     /**
100      * The initialization listener used when the user changes his choice of
101      * engine (as opposed to when then screen is being initialized for the first
102      * time).
103      */
104     private final TextToSpeech.OnInitListener mUpdateListener = new TextToSpeech.OnInitListener() {
105         @Override
106         public void onInit(int status) {
107             onUpdateEngine(status);
108         }
109     };
110 
111     @Override
onCreate(Bundle savedInstanceState)112     public void onCreate(Bundle savedInstanceState) {
113         mResources = getResources();
114         mHelper = new SettingsHelper(this);
115         mActions = new ArrayList<Action>();
116 
117         mTts = new TextToSpeech(getApplicationContext(), mInitListener);
118         mEnginesHelper = new TtsEngines(getApplicationContext());
119         initSettings();
120 
121         super.onCreate(savedInstanceState);
122     }
123 
124     @Override
refreshActionList()125     protected void refreshActionList() {
126         mActions.clear();
127         switch ((ActionType) mState) {
128             case ACCESSIBILITY_OVERVIEW:
129                 mActions.add(ActionType.ACCESSIBILITY_CAPTIONS.toAction(mResources));
130                 mActions.add(ActionType.ACCESSIBILITY_SERVICES.toAction(mResources));
131                 // TODO b/18007521
132                 // uncomment when Talkback is able to support not speaking passwords aloud
133                 //mActions.add(ActionType.ACCESSIBILITY_SPEAK_PASSWORDS.toAction(mResources,
134                 //        mHelper.getSecureStatusIntSetting(
135                 //                Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD)));
136                 mActions.add(ActionType.ACCESSIBILITY_TTS_OUTPUT.toAction(mResources,
137                                 getDisplayNameForEngine(mTts.getCurrentEngine())));
138                 break;
139             case ACCESSIBILITY_SERVICES:
140                 mActions = getInstalledServicesActions();
141                 break;
142             case ACCESSIBILITY_SERVICES_SETTINGS:
143                 mActions.add(ActionType.ACCESSIBILITY_SERVICES_STATUS.toAction(mResources,
144                         mHelper.getStatusStringFromBoolean(mSelectedServiceEnabled)));
145                 if (mSelectedServiceSettings != null) {
146                     mActions.add(ActionType.ACCESSIBILITY_SERVICE_CONFIG.toAction(mResources));
147                 }
148                 break;
149             case ACCESSIBILITY_SERVICES_STATUS:
150                 mActions = getEnableActions(mServiceSettingTitle, mSelectedServiceEnabled);
151                 break;
152             case ACCESSIBILITY_SERVICES_CONFIRM_ON:
153                 mActions.add(ActionType.AGREE.toAction(mResources));
154                 mActions.add(ActionType.DISAGREE.toAction(mResources));
155                 break;
156             case ACCESSIBILITY_SERVICES_CONFIRM_OFF:
157                 mActions.add(ActionType.OK.toAction(mResources));
158                 mActions.add(ActionType.CANCEL.toAction(mResources));
159                 break;
160             case ACCESSIBILITY_SPEAK_PASSWORDS:
161                 mActions = getEnableActions(((ActionType) mState).name(), getProperty());
162                 break;
163             case ACCESSIBILITY_TTS_OUTPUT:
164                 mActions.add(ActionType.ACCESSIBILITY_PREFERRED_ENGINE.toAction(
165                         mResources, getDisplayNameForEngine(mTts.getCurrentEngine())));
166                 if (mTtsSettingsEnabled) {
167                     if (mTts.getLanguage() != null) {
168                         mActions.add(ActionType.ACCESSIBILITY_LANGUAGE.toAction(
169                                 mResources, mTts.getLanguage().getDisplayName()));
170                     } else {
171                         mActions.add(ActionType.ACCESSIBILITY_LANGUAGE.toAction(
172                                 mResources, " "));
173                     }
174                 }
175                 mActions.add(ActionType.ACCESSIBILITY_INSTALL_VOICE_DATA.toAction(mResources));
176                 mActions.add(ActionType.ACCESSIBILITY_SPEECH_RATE.toAction(
177                         mResources, getTtsRate(mHelper.getSecureIntSetting(
178                                 TTS_DEFAULT_RATE, getString(R.string.tts_rate_default_value)))));
179                 if (mTtsSettingsEnabled) {
180                     mActions.add(ActionType.ACCESSIBILITY_PLAY_SAMPLE.toAction(mResources));
181                 }
182                 break;
183             case ACCESSIBILITY_PREFERRED_ENGINE:
184                 mActions = getEngines();
185                 break;
186             case ACCESSIBILITY_LANGUAGE:
187                 final ArrayList<String> available = mVoiceCheckData.getStringArrayListExtra(
188                         TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
189                 if (available != null && available.size() > 0) {
190                     mActions = updateDefaultLocalePref(available);
191                     if (mTts.getLanguage() != null) {
192                         String currLang = getLanguageString(mTts.getLanguage());
193                         checkSelectedAction(mActions, currLang);
194                     }
195                 }
196                 break;
197             case ACCESSIBILITY_SPEECH_RATE:
198                 mActions = Action.createActionsFromArrays(
199                         mResources.getStringArray(R.array.tts_rate_values),
200                         mResources.getStringArray(R.array.tts_rate_entries));
201                 checkSelectedAction(mActions, mHelper.getSecureIntSetting(
202                         TTS_DEFAULT_RATE, getString(R.string.tts_rate_default_value)));
203                 break;
204             default:
205                 break;
206         }
207     }
208 
209     @Override
updateView()210     protected void updateView() {
211         refreshActionList();
212         switch ((ActionType) mState) {
213             case ACCESSIBILITY_SERVICES_SETTINGS:
214                 setView(mServiceSettingTitle,
215                         ((ActionType) getPrevState()).getTitle(mResources),
216                         null, R.drawable.ic_settings_accessibility);
217                 break;
218             case ACCESSIBILITY_SERVICES_STATUS:
219                 setView(((ActionType) mState).getTitle(mResources), mServiceSettingTitle,
220                         null,
221                         R.drawable.ic_settings_accessibility);
222                 return;
223             case ACCESSIBILITY_SERVICES_CONFIRM_ON:
224                 String onTitle = getString(
225                         R.string.system_accessibility_service_on_confirm_title,
226                         mServiceSettingTitle);
227                 setView(onTitle, mServiceSettingTitle,
228                         getString(R.string.system_accessibility_service_on_confirm_desc,
229                                 mServiceSettingTitle), R.drawable.ic_settings_accessibility);
230                 return;
231             case ACCESSIBILITY_SERVICES_CONFIRM_OFF:
232                 String offTitle = getString(
233                         R.string.system_accessibility_service_off_confirm_title,
234                         mServiceSettingTitle);
235                 setView(offTitle, mServiceSettingTitle,
236                         getString(R.string.system_accessibility_service_off_confirm_desc,
237                                 mServiceSettingTitle), R.drawable.ic_settings_accessibility);
238                 return;
239             default:
240                 setView(((ActionType) mState).getTitle(mResources),
241                         getPrevState() != null ?
242                         ((ActionType) getPrevState()).getTitle(mResources) :
243                         getString(R.string.settings_app_name),
244                         ((ActionType) mState).getDescription(mResources),
245                         R.drawable.ic_settings_accessibility);
246         }
247     }
248 
getTtsRate(String value)249     private String getTtsRate(String value) {
250         String[] values = mResources.getStringArray(R.array.tts_rate_values);
251         String[] entries = mResources.getStringArray(R.array.tts_rate_entries);
252         for (int index = 0; index < values.length; ++index) {
253             if (values[index].equals(value)) {
254                 return entries[index];
255             }
256         }
257         return "";
258     }
259 
getInstalledServicesActions()260     private ArrayList<Action> getInstalledServicesActions() {
261         ArrayList<Action> actions = new ArrayList<Action>();
262         final List<AccessibilityServiceInfo> installedServiceInfos = AccessibilityManager
263                 .getInstance(this).getInstalledAccessibilityServiceList();
264 
265         Set<ComponentName> enabledServices = getEnabledServicesFromSettings(this);
266 
267         final boolean accessibilityEnabled = Settings.Secure.getInt(getContentResolver(),
268                 Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1;
269 
270         for (AccessibilityServiceInfo accInfo : installedServiceInfos) {
271             ServiceInfo serviceInfo = accInfo.getResolveInfo().serviceInfo;
272             ComponentName componentName = new ComponentName(serviceInfo.packageName,
273                     serviceInfo.name);
274             final boolean serviceEnabled = accessibilityEnabled
275                     && enabledServices.contains(componentName);
276             String title = accInfo.getResolveInfo().loadLabel(getPackageManager()).toString();
277             actions.add(new Action.Builder()
278                     .key(componentName.flattenToString())
279                     .title(title)
280                     .description(mHelper.getStatusStringFromBoolean(serviceEnabled))
281                     .build());
282         }
283         return actions;
284     }
285 
getSettingsForService(String serviceComponentName)286     private String getSettingsForService(String serviceComponentName) {
287         final List<AccessibilityServiceInfo> installedServiceInfos = AccessibilityManager
288                 .getInstance(this).getInstalledAccessibilityServiceList();
289         ComponentName comp = ComponentName.unflattenFromString(serviceComponentName);
290 
291         if (comp != null) {
292             for (AccessibilityServiceInfo accInfo : installedServiceInfos) {
293                 ServiceInfo serviceInfo = accInfo.getResolveInfo().serviceInfo;
294                 if (serviceInfo.packageName.equals(comp.getPackageName()) &&
295                         serviceInfo.name.equals(comp.getClassName())) {
296                     String settingsClassName = accInfo.getSettingsActivityName();
297                     if (!TextUtils.isEmpty(settingsClassName)) {
298                         ComponentName settingsComponent =
299                                 new ComponentName(comp.getPackageName(), settingsClassName);
300                         return settingsComponent.flattenToString();
301                     } else {
302                         return null;
303                     }
304                 }
305             }
306         }
307         return null;
308     }
309 
getEnabledServicesFromSettings(Context context)310     private static Set<ComponentName> getEnabledServicesFromSettings(Context context) {
311         String enabledServicesSetting = Settings.Secure.getString(context.getContentResolver(),
312                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
313         if (enabledServicesSetting == null) {
314             enabledServicesSetting = "";
315         }
316         Set<ComponentName> enabledServices = new HashSet<ComponentName>();
317         SimpleStringSplitter colonSplitter = new SimpleStringSplitter(
318                 ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR);
319         colonSplitter.setString(enabledServicesSetting);
320         while (colonSplitter.hasNext()) {
321             String componentNameString = colonSplitter.next();
322             ComponentName enabledService = ComponentName.unflattenFromString(
323                     componentNameString);
324             if (enabledService != null) {
325                 enabledServices.add(enabledService);
326             }
327         }
328         return enabledServices;
329     }
330 
getEnableActions(String type, boolean enabled)331     private ArrayList<Action> getEnableActions(String type, boolean enabled) {
332         ArrayList<Action> actions = new ArrayList<Action>();
333         actions.add(ActionBehavior.ON.toAction(ActionBehavior.getOnKey(type), mResources, enabled));
334         actions.add(ActionBehavior.OFF.toAction(ActionBehavior.getOffKey(type), mResources,
335                 !enabled));
336         return actions;
337     }
338 
checkSelectedAction(ArrayList<Action> actions, String selectedKey)339     private void checkSelectedAction(ArrayList<Action> actions, String selectedKey) {
340         for (Action action : actions) {
341             if (action.getKey().equalsIgnoreCase(selectedKey)) {
342                 action.setChecked(true);
343                 break;
344             }
345         }
346     }
347 
getLanguageString(Locale lang)348     private String getLanguageString(Locale lang) {
349         if (lang.getLanguage().isEmpty())
350             return "";
351 
352         StringBuilder builder = new StringBuilder();
353         builder.append(lang.getLanguage());
354 
355         if (!lang.getCountry().isEmpty()) {
356             builder.append('-');
357             builder.append(lang.getCountry());
358         }
359 
360         if (!lang.getVariant().isEmpty()) {
361             builder.append('-');
362             builder.append(lang.getVariant());
363         }
364 
365         return builder.toString();
366     }
367 
updateDefaultEngine(String engine)368     private void updateDefaultEngine(String engine) {
369         if (DEBUG) {
370             Log.d(TAG, "Updating default synth to : " + engine);
371         }
372 
373         // TODO Disable the "play sample text" preference and the speech
374         // rate preference while the engine is being swapped.
375 
376         // Keep track of the previous engine that was being used. So that
377         // we can reuse the previous engine.
378         //
379         // Note that if TextToSpeech#getCurrentEngine is not null, it means at
380         // the very least that we successfully bound to the engine service.
381         mPreviousEngine = mTts.getCurrentEngine();
382 
383         // Step 1: Shut down the existing TTS engine.
384         if (mTts != null) {
385             try {
386                 mTts.shutdown();
387                 mTts = null;
388             } catch (Exception e) {
389                 Log.e(TAG, "Error shutting down TTS engine" + e);
390             }
391         }
392 
393         // Step 2: Connect to the new TTS engine.
394         // Step 3 is continued on #onUpdateEngine (below) which is called when
395         // the app binds successfully to the engine.
396         if (DEBUG) {
397             Log.d(TAG, "Updating engine : Attempting to connect to engine: " + engine);
398         }
399         mTts = new TextToSpeech(getApplicationContext(), mUpdateListener, engine);
400         setTtsUtteranceProgressListener();
401     }
402 
403     @Override
onActionClicked(Action action)404     public void onActionClicked(Action action) {
405         /*
406          * For list preferences
407          */
408         final String key = action.getKey();
409         final String title = action.getTitle();
410         switch((ActionType)mState){
411             case ACCESSIBILITY_SERVICES:
412                 mServiceSettingTitle = action.getTitle();
413                 mSelectedServiceComponent = action.getKey();
414                 mSelectedServiceEnabled = mHelper.getStatusFromString(action.getDescription());
415                 mSelectedServiceSettings = getSettingsForService(mSelectedServiceComponent);
416                 if (mSelectedServiceSettings != null) {
417                     // Service provides a settings component, so go to the Status/Settings screen
418                     setState(ActionType.ACCESSIBILITY_SERVICES_SETTINGS, true);
419                 } else {
420                     // Service does not provide Settings, so go straight to Enable/Disable
421                     setState(ActionType.ACCESSIBILITY_SERVICES_STATUS, true);
422                 }
423                 return;
424             case ACCESSIBILITY_PREFERRED_ENGINE:
425                 mCurrentEngine = key;
426                 updateDefaultEngine(mCurrentEngine);
427                 // Delay the goBack here until we are done binding to the service, so we have
428                 // the Language data available for the previous screen.
429                 return;
430             case ACCESSIBILITY_LANGUAGE:
431                 updateLanguageTo(
432                         !TextUtils.isEmpty(key) ? mEnginesHelper.parseLocaleString(key) : null);
433                 goBack();
434                 return;
435             case ACCESSIBILITY_SPEECH_RATE:
436                 mHelper.setSecureIntValueSetting(TTS_DEFAULT_RATE, key);
437                 goBack();
438                 return;
439         }
440 
441         /*
442          * For regular states
443          */
444         ActionKey<ActionType, ActionBehavior> actionKey = new ActionKey<ActionType, ActionBehavior>(
445                 ActionType.class, ActionBehavior.class, action.getKey());
446         final ActionType type = actionKey.getType();
447         if (type != null) {
448             switch (type) {
449                 case ACCESSIBILITY_PLAY_SAMPLE:
450                     getSampleText();
451                     return;
452                 case ACCESSIBILITY_INSTALL_VOICE_DATA:
453                     installVoiceData();
454                     return;
455                 case ACCESSIBILITY_SERVICE_CONFIG: {
456                     ComponentName comp = ComponentName.unflattenFromString(
457                             mSelectedServiceSettings);
458                     Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent(comp);
459                     startActivity(settingsIntent);
460                     return;
461                 }
462                 case ACCESSIBILITY_CAPTIONS: {
463                     ComponentName comp = new ComponentName(this, CaptionSetupActivity.class);
464                     Intent captionsIntent = new Intent(Intent.ACTION_MAIN).setComponent(comp);
465                     startActivity(captionsIntent);
466                     return;
467                 }
468                 case AGREE:
469                     setProperty(true); // Agreed to turn ON service
470                     return;
471                 case DISAGREE:
472                     setProperty(false); // Disagreed to turn service ON
473                     return;
474                 case OK:
475                     setProperty(false); // ok to STOP Service
476                     return;
477                 case CANCEL:
478                     goBack(); // Cancelled request to STOP service
479                     return;
480                 default:
481             }
482         }
483 
484         final ActionBehavior behavior = actionKey.getBehavior();
485         switch (behavior) {
486             case ON:
487                 setProperty(true);
488                 return;
489             case OFF:
490                 setProperty(false);
491                 return;
492             default:
493         }
494         setState(type, true);
495     }
496 
497     @Override
setProperty(boolean enable)498     protected void setProperty(boolean enable) {
499         switch ((ActionType) mState) {
500             case ACCESSIBILITY_SPEAK_PASSWORDS:
501                 mHelper.setSecureIntSetting(Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, enable);
502                 break;
503             case ACCESSIBILITY_SERVICES_STATUS:
504                 // Accessibility Service ON/OFF requires an extra confirmation screen.
505                 if (enable) {
506                     setState(ActionType.ACCESSIBILITY_SERVICES_CONFIRM_ON, true);
507                 } else {
508                     if (mSelectedServiceEnabled) {
509                         setState(ActionType.ACCESSIBILITY_SERVICES_CONFIRM_OFF, true);
510                     } else {
511                         goBack();
512                     }
513                 }
514                 return;
515             case ACCESSIBILITY_SERVICES_CONFIRM_ON:
516                 setAccessibilityServiceState(mSelectedServiceComponent, enable);
517                 mSelectedServiceEnabled = enable;
518                 // go back twice: Remove the ON/OFF screen from the stack
519                 goBack();
520                 break;
521             case ACCESSIBILITY_SERVICES_CONFIRM_OFF:
522                 setAccessibilityServiceState(mSelectedServiceComponent, enable);
523                 mSelectedServiceEnabled = enable;
524                 // go back twice: Remove the ON/OFF screen from the stack
525                 goBack();
526                 break;
527         }
528         goBack();
529     }
530 
getProperty()531     private boolean getProperty() {
532         if ((ActionType) mState == ActionType.ACCESSIBILITY_SPEAK_PASSWORDS) {
533             return mHelper.getSecureIntValueSettingToBoolean(
534                     Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD);
535         }
536         return false;
537     }
538 
getInstalledServices()539     private Set<ComponentName> getInstalledServices() {
540         Set<ComponentName> installedServices = new HashSet<ComponentName>();
541         installedServices.clear();
542 
543         List<AccessibilityServiceInfo> installedServiceInfos =
544                 AccessibilityManager.getInstance(this)
545                         .getInstalledAccessibilityServiceList();
546         if (installedServiceInfos == null) {
547             return installedServices;
548         }
549 
550         final int installedServiceInfoCount = installedServiceInfos.size();
551         for (int i = 0; i < installedServiceInfoCount; i++) {
552             ResolveInfo resolveInfo = installedServiceInfos.get(i).getResolveInfo();
553             ComponentName installedService = new ComponentName(
554                     resolveInfo.serviceInfo.packageName,
555                     resolveInfo.serviceInfo.name);
556             installedServices.add(installedService);
557         }
558         return installedServices;
559     }
560 
setAccessibilityServiceState(String preferenceKey, boolean enabled)561     public void setAccessibilityServiceState(String preferenceKey, boolean enabled) {
562         // Parse the enabled services.
563         Set<ComponentName> enabledServices = getEnabledServicesFromSettings(this);
564 
565         // Determine enabled services and accessibility state.
566         ComponentName toggledService = ComponentName.unflattenFromString(preferenceKey);
567         boolean accessibilityEnabled = false;
568         if (enabled) {
569             enabledServices.add(toggledService);
570             // Enabling at least one service enables accessibility.
571             accessibilityEnabled = true;
572         } else {
573             enabledServices.remove(toggledService);
574             // Check how many enabled and installed services are present.
575             Set<ComponentName> installedServices = getInstalledServices();
576             for (ComponentName enabledService : enabledServices) {
577                 if (installedServices.contains(enabledService)) {
578                     // Disabling the last service disables accessibility.
579                     accessibilityEnabled = true;
580                     break;
581                 }
582             }
583         }
584 
585         // Update the enabled services setting.
586         StringBuilder enabledServicesBuilder = new StringBuilder();
587         // Keep the enabled services even if they are not installed since we
588         // have no way to know whether the application restore process has
589         // completed. In general the system should be responsible for the
590         // clean up not settings.
591         for (ComponentName enabledService : enabledServices) {
592             enabledServicesBuilder.append(enabledService.flattenToString());
593             enabledServicesBuilder.append(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR);
594         }
595         final int enabledServicesBuilderLength = enabledServicesBuilder.length();
596         if (enabledServicesBuilderLength > 0) {
597             enabledServicesBuilder.deleteCharAt(enabledServicesBuilderLength - 1);
598         }
599         Settings.Secure.putString(getContentResolver(),
600                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
601                 enabledServicesBuilder.toString());
602 
603         // Update accessibility enabled.
604         Settings.Secure.putInt(getContentResolver(),
605                 Settings.Secure.ACCESSIBILITY_ENABLED, accessibilityEnabled ? 1 : 0);
606     }
607 
getEngines()608     private ArrayList<Action> getEngines() {
609         ArrayList<Action> actions = new ArrayList<Action>();
610         List<EngineInfo> engines = mEnginesHelper.getEngines();
611         int totalEngine = engines.size();
612         for (int i = 0; i < totalEngine; i++) {
613             Action action = new Action.Builder()
614                     .key(engines.get(i).name)
615                     .title(engines.get(i).label)
616                     .build();
617             actions.add(action);
618         }
619         mCurrentEngine = mTts.getCurrentEngine();
620         checkVoiceData(mCurrentEngine);
621         return actions;
622     }
623 
getDisplayNameForEngine(String enginePackageName)624     private String getDisplayNameForEngine(String enginePackageName) {
625         List<EngineInfo> engines = mEnginesHelper.getEngines();
626         int totalEngine = engines.size();
627         for (int i = 0; i < totalEngine; i++) {
628             if (engines.get(i).name.equals(enginePackageName)) {
629                 return engines.get(i).label;
630             }
631         }
632         // Not found, return package name then
633         return enginePackageName;
634     }
635 
636     /*
637      * Check whether the voice data for the engine is ok.
638      */
checkVoiceData(String engine)639     private void checkVoiceData(String engine) {
640         Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
641         intent.setPackage(engine);
642         try {
643             if (DEBUG) {
644                 Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0));
645             }
646             startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK);
647         } catch (ActivityNotFoundException ex) {
648             Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")");
649         }
650     }
651 
652     /**
653      * Called when the TTS engine is initialized.
654      */
onInitEngine(int status)655     public void onInitEngine(int status) {
656         if (status == TextToSpeech.SUCCESS) {
657             if (DEBUG) {
658                 Log.d(TAG, "TTS engine for settings screen initialized.");
659             }
660         } else {
661             if (DEBUG) {
662                 Log.d(TAG, "TTS engine for settings screen failed to initialize successfully.");
663             }
664         }
665     }
666 
initSettings()667     private void initSettings() {
668         mCurrentEngine = mTts.getCurrentEngine();
669 
670         checkVoiceData(mCurrentEngine);
671     }
672 
673     /*
674      * Step 3: We have now bound to the TTS engine the user requested. We will
675      * attempt to check voice data for the engine if we successfully bound to it,
676      * or revert to the previous engine if we didn't.
677      */
onUpdateEngine(int status)678     public void onUpdateEngine(int status) {
679         if (status == TextToSpeech.SUCCESS) {
680             if (DEBUG) {
681                 Log.d(TAG, "Updating engine: Successfully bound to the engine: " +
682                         mTts.getCurrentEngine());
683             }
684             checkVoiceData(mTts.getCurrentEngine());
685         } else {
686             if (DEBUG) {
687                 Log.d(TAG, "Updating engine: Failed to bind to engine, reverting.");
688             }
689             if (mPreviousEngine != null) {
690                 // This is guaranteed to at least bind, since mPreviousEngine
691                 // would be
692                 // null if the previous bind to this engine failed.
693                 mTts = new TextToSpeech(getApplicationContext(), mInitListener,
694                         mPreviousEngine);
695                 setTtsUtteranceProgressListener();
696             }
697             mPreviousEngine = null;
698         }
699         goBack();
700     }
701 
setTtsUtteranceProgressListener()702     private void setTtsUtteranceProgressListener() {
703         if (mTts == null) {
704             return;
705         }
706         mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
707                 @Override
708             public void onStart(String utteranceId) {
709             }
710 
711                 @Override
712             public void onDone(String utteranceId) {
713             }
714 
715                 @Override
716             public void onError(String utteranceId) {
717                 Log.e(TAG, "Error while trying to synthesize sample text");
718             }
719         });
720     }
721 
updateDefaultLocalePref(ArrayList<String> availableLangs)722     private ArrayList<Action> updateDefaultLocalePref(ArrayList<String> availableLangs) {
723         ArrayList<Action> actions = new ArrayList<Action>();
724         Locale currentLocale = mEnginesHelper.getLocalePrefForEngine(mCurrentEngine);
725 
726         ArrayList<Pair<String, Locale>> entryPairs =
727                 new ArrayList<Pair<String, Locale>>(availableLangs.size());
728         for (int i = 0; i < availableLangs.size(); i++) {
729             Locale locale = mEnginesHelper.parseLocaleString(availableLangs.get(i));
730             if (locale != null) {
731                 entryPairs.add(new Pair<String, Locale>(
732                         locale.getDisplayName(), locale));
733             }
734         }
735 
736         // Sort it
737         Collections.sort(entryPairs, new Comparator<Pair<String, Locale>>() {
738                 @Override
739             public int compare(Pair<String, Locale> lhs, Pair<String, Locale> rhs) {
740                 return lhs.first.compareToIgnoreCase(rhs.first);
741             }
742         });
743 
744         // Get two arrays out of one of pairs
745         int selectedLanguageIndex = -1;
746         int i = 0;
747         for (Pair<String, Locale> entry : entryPairs) {
748             if (entry.second.equals(currentLocale)) {
749                 selectedLanguageIndex = i;
750             }
751             Action action = new Action.Builder()
752                     .key(entry.second.toString())
753                     .title(entry.first).build();
754             actions.add(action);
755         }
756         return actions;
757     }
758 
updateLanguageTo(Locale locale)759     private void updateLanguageTo(Locale locale) {
760         mEnginesHelper.updateLocalePrefForEngine(mCurrentEngine, locale);
761         if (mCurrentEngine.equals(mTts.getCurrentEngine())) {
762             // Null locale means "use system default"
763             mTts.setLanguage((locale != null) ? locale : Locale.getDefault());
764         }
765     }
766 
767     /**
768      * Called when voice data integrity check returns
769      */
770     @Override
onActivityResult(int requestCode, int resultCode, Intent data)771     public void onActivityResult(int requestCode, int resultCode, Intent data) {
772         if (requestCode == GET_SAMPLE_TEXT) {
773             onSampleTextReceived(resultCode, data);
774         } else if (requestCode == VOICE_DATA_INTEGRITY_CHECK) {
775             onVoiceDataIntegrityCheckDone(data);
776         }
777     }
778 
779     /**
780      * Ask the current default engine to return a string of sample text to be
781      * spoken to the user.
782      */
getSampleText()783     private void getSampleText() {
784         String currentEngine = mTts.getCurrentEngine();
785 
786         if (TextUtils.isEmpty(currentEngine))
787             currentEngine = mTts.getDefaultEngine();
788 
789         Locale defaultLocale = mTts.getDefaultLanguage();
790         if (defaultLocale == null) {
791             Log.e(TAG, "Failed to get default language from engine " + currentEngine);
792             return;
793         }
794         mTts.setLanguage(defaultLocale);
795 
796         // TODO: This is currently a hidden private API. The intent extras
797         // and the intent action should be made public if we intend to make this
798         // a public API. We fall back to using a canned set of strings if this
799         // doesn't work.
800         Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT);
801 
802         intent.putExtra("language", defaultLocale.getLanguage());
803         intent.putExtra("country", defaultLocale.getCountry());
804         intent.putExtra("variant", defaultLocale.getVariant());
805         intent.setPackage(currentEngine);
806 
807         try {
808             if (DEBUG) {
809                 Log.d(TAG, "Getting sample text: " + intent.toUri(0));
810             }
811             startActivityForResult(intent, GET_SAMPLE_TEXT);
812         } catch (ActivityNotFoundException ex) {
813             Log.e(TAG, "Failed to get sample text, no activity found for " + intent + ")");
814         }
815     }
816 
getDefaultSampleString()817     private String getDefaultSampleString() {
818         if (mTts != null && mTts.getLanguage() != null) {
819             final String currentLang = mTts.getLanguage().getISO3Language();
820             String[] strings = mResources.getStringArray(R.array.tts_demo_strings);
821             String[] langs = mResources.getStringArray(R.array.tts_demo_string_langs);
822 
823             for (int i = 0; i < strings.length; ++i) {
824                 if (langs[i].equals(currentLang)) {
825                     return strings[i];
826                 }
827             }
828         }
829         return null;
830     }
831 
onSampleTextReceived(int resultCode, Intent data)832     private void onSampleTextReceived(int resultCode, Intent data) {
833         String sample = getDefaultSampleString();
834 
835         if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) {
836             if (data != null && data.getStringExtra("sampleText") != null) {
837                 sample = data.getStringExtra("sampleText");
838             }
839             if (DEBUG) {
840                 Log.d(TAG, "Got sample text: " + sample);
841             }
842         } else {
843             if (DEBUG) {
844                 Log.d(TAG, "Using default sample text :" + sample);
845             }
846         }
847 
848         if (sample != null && mTts != null) {
849             // The engine is guaranteed to have been initialized here
850             // because this preference is not enabled otherwise.
851 
852             final boolean networkRequired = isNetworkRequiredForSynthesis();
853             if (!networkRequired || networkRequired &&
854                     (mTts.isLanguageAvailable(mTts.getLanguage()) >= TextToSpeech.LANG_AVAILABLE)) {
855                 HashMap<String, String> params = new HashMap<String, String>();
856                 params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "Sample");
857 
858                 mTts.speak(sample, TextToSpeech.QUEUE_FLUSH, params);
859             } else {
860                 Log.w(TAG, "Network required for sample synthesis for requested language");
861                 // TODO displayNetworkAlert();
862             }
863         } else {
864             // TODO: Display an error here to the user.
865             Log.e(TAG, "Did not have a sample string for the requested language");
866         }
867     }
868 
isNetworkRequiredForSynthesis()869     private boolean isNetworkRequiredForSynthesis() {
870         Set<String> features = mTts.getFeatures(mTts.getLanguage());
871         return features.contains(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS) &&
872                 !features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
873     }
874 
875     /*
876      * Step 5: The voice data check is complete.
877      */
onVoiceDataIntegrityCheckDone(Intent data)878     private void onVoiceDataIntegrityCheckDone(Intent data) {
879         final String engine = mTts.getCurrentEngine();
880 
881         if (engine == null) {
882             Log.e(TAG, "Voice data check complete, but no engine bound");
883             return;
884         }
885 
886         if (data == null) {
887             Log.e(TAG, "Engine failed voice data integrity check (null return)" +
888                     mTts.getCurrentEngine());
889             return;
890         }
891 
892         Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine);
893 
894         setVoiceDataDetails(data);
895     }
896 
setVoiceDataDetails(Intent data)897     public void setVoiceDataDetails(Intent data) {
898         mVoiceCheckData = data;
899         // This might end up running before getView above, in which
900         // case mSettingsIcon && mRadioButton will be null. In this case
901         // getView will set the right values.
902         if (mVoiceCheckData != null) {
903             mTtsSettingsEnabled = true;
904         } else {
905             mTtsSettingsEnabled = false;
906         }
907     }
908 
909     /**
910      * Ask the current default engine to launch the matching INSTALL_TTS_DATA
911      * activity so the required TTS files are properly installed.
912      */
installVoiceData()913     private void installVoiceData() {
914         if (TextUtils.isEmpty(mCurrentEngine)) {
915             return;
916         }
917         Intent intent = new Intent(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
918         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
919         intent.setPackage(mCurrentEngine);
920         try {
921             Log.v(TAG, "Installing voice data: " + intent.toUri(0));
922             startActivity(intent);
923         } catch (ActivityNotFoundException ex) {
924             Log.e(TAG, "Failed to install TTS data, no acitivty found for " + intent + ")");
925         }
926     }
927 
928     @Override
getInitialState()929     protected Object getInitialState() {
930         return ActionType.ACCESSIBILITY_OVERVIEW;
931     }
932 }
933