• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.accessibilityservice.AccessibilityServiceInfo;
20 import android.annotation.NonNull;
21 import android.app.AlertDialog;
22 import android.app.Dialog;
23 import android.app.DialogFragment;
24 import android.content.ActivityNotFoundException;
25 import android.content.ComponentName;
26 import android.content.DialogInterface;
27 import android.content.Intent;
28 import android.content.pm.ResolveInfo;
29 import android.content.pm.ServiceInfo;
30 import android.content.res.Resources;
31 import android.os.Bundle;
32 import android.provider.Settings;
33 import android.speech.tts.TextToSpeech;
34 import android.speech.tts.TextToSpeech.EngineInfo;
35 import android.speech.tts.TtsEngines;
36 import android.speech.tts.UtteranceProgressListener;
37 import android.text.TextUtils;
38 import android.text.TextUtils.SimpleStringSplitter;
39 import android.util.ArrayMap;
40 import android.util.Log;
41 import android.view.accessibility.AccessibilityManager;
42 
43 import com.android.tv.settings.R;
44 import com.android.tv.settings.dialog.Layout;
45 import com.android.tv.settings.dialog.SettingsLayoutActivity;
46 
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.Comparator;
50 import java.util.HashSet;
51 import java.util.List;
52 import java.util.Locale;
53 import java.util.Map;
54 import java.util.Objects;
55 import java.util.Set;
56 
57 import static android.provider.Settings.Secure.TTS_DEFAULT_RATE;
58 import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH;
59 
60 public class AccessibilityActivity extends SettingsLayoutActivity {
61 
62     private static final String TAG = "AccessibilityActivity";
63     private static final boolean DEBUG = false;
64 
65     private static final int GET_SAMPLE_TEXT = 1983;
66     private static final int VOICE_DATA_INTEGRITY_CHECK = 1977;
67 
68     private static final char ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ':';
69 
70     private static final int TTS_RATE_VERY_SLOW = 60;
71     private static final int TTS_RATE_SLOW = 80;
72     private static final int TTS_RATE_NORMAL = 100;
73     private static final int TTS_RATE_FAST = 150;
74     private static final int TTS_RATE_VERY_FAST = 200;
75 
76     private static final int ACTION_BASE_MASK = -1 << 20;
77 
78     private static final int ACTION_SERVICE_ENABLE_BASE = 1 << 20;
79 
80     private static final int ACTION_SERVICE_DISABLE_BASE = 2 << 20;
81 
82     private static final int ACTION_SPEAK_PASSWORD_BASE = 3 << 20;
83     private static final int ACTION_SPEAK_PASSWORD_ENABLE = ACTION_SPEAK_PASSWORD_BASE;
84     private static final int ACTION_SPEAK_PASSWORD_DISABLE = ACTION_SPEAK_PASSWORD_BASE + 1;
85 
86     private static final int ACTION_TTS_BASE = 4 << 20;
87     private static final int ACTION_PLAY_SAMPLE = ACTION_TTS_BASE;
88 
89     private static final int ACTION_TTS_ENGINE_BASE = 5 << 20;
90 
91     private static final int ACTION_TTS_LANGUAGE_BASE = 6 << 20;
92 
93     private static final int ACTION_TTS_RATE_BASE = 7 << 20;
94     private static final int ACTION_TTS_RATE_VERY_SLOW = ACTION_TTS_RATE_BASE + TTS_RATE_VERY_SLOW;
95     private static final int ACTION_TTS_RATE_SLOW = ACTION_TTS_RATE_BASE + TTS_RATE_SLOW;
96     private static final int ACTION_TTS_RATE_NORMAL = ACTION_TTS_RATE_BASE + TTS_RATE_NORMAL;
97     private static final int ACTION_TTS_RATE_FAST = ACTION_TTS_RATE_BASE + TTS_RATE_FAST;
98     private static final int ACTION_TTS_RATE_VERY_FAST = ACTION_TTS_RATE_BASE + TTS_RATE_VERY_FAST;
99 
100     private TextToSpeech mTts = null;
101     private TtsEngines mEnginesHelper = null;
102     private String mCurrentEngine;
103     private String mPreviousEngine;
104     private Intent mVoiceCheckData = null;
105     private String mVoiceCheckEngine;
106 
107     private final ArrayList<AccessibilityComponentHolder> mAccessibilityComponentHolders =
108             new ArrayList<>();
109 
110     private final ArrayList<Locale> mEngineLocales = new ArrayList<>();
111 
112     private final ArrayList<EngineInfo> mEngineInfos = new ArrayList<>();
113 
114     private final Layout.LayoutGetter mTtsLanguageLayoutGetter = new TtsLanguageLayoutGetter();
115 
116     /**
117      * The initialization listener used when we are initalizing the settings
118      * screen for the first time (as opposed to when a user changes his choice
119      * of engine).
120      */
121     private final TextToSpeech.OnInitListener mInitListener = new TextToSpeech.OnInitListener() {
122             @Override
123         public void onInit(int status) {
124             onInitEngine(status);
125         }
126     };
127 
128     /**
129      * The initialization listener used when the user changes his choice of
130      * engine (as opposed to when then screen is being initialized for the first
131      * time).
132      */
133     private final TextToSpeech.OnInitListener mUpdateListener = new TextToSpeech.OnInitListener() {
134         @Override
135         public void onInit(int status) {
136             onUpdateEngine(status);
137         }
138     };
139 
140     @Override
onCreate(Bundle savedInstanceState)141     public void onCreate(Bundle savedInstanceState) {
142         mTts = new TextToSpeech(getApplicationContext(), mInitListener);
143         mEnginesHelper = new TtsEngines(getApplicationContext());
144         mCurrentEngine = mTts.getCurrentEngine();
145 
146         checkVoiceData(mCurrentEngine);
147 
148         super.onCreate(savedInstanceState);
149     }
150 
151     @Override
createLayout()152     public Layout createLayout() {
153         final Resources res = getResources();
154         return new Layout()
155                 .breadcrumb(getString(R.string.header_category_preferences))
156                 .add(new Layout.Header.Builder(res)
157                         .title(R.string.system_accessibility)
158                         .icon(R.drawable.ic_settings_accessibility)
159                         .build()
160                         .add(getCaptionsAction())
161                         .add(getServicesHeader())
162                         // TODO b/18007521
163                         // uncomment when Talkback is able to support not speaking passwords aloud
164                         //.add(getSpeakPasswordsHeader())
165                         .add(getTtsHeader()));
166     }
167 
getCaptionsAction()168     private Layout.Action getCaptionsAction() {
169         final ComponentName comp = new ComponentName(this, CaptionSetupActivity.class);
170         final Intent captionsIntent = new Intent(Intent.ACTION_MAIN).setComponent(comp);
171 
172         return new Layout.Action.Builder(getResources(), captionsIntent)
173                 .title(R.string.accessibility_captions)
174                 .build();
175     }
176 
getServicesHeader()177     private Layout.Header getServicesHeader() {
178         final Resources res = getResources();
179         final Layout.Header header = new Layout.Header.Builder(res)
180                 .title(R.string.system_services)
181                 .build();
182 
183         final List<AccessibilityServiceInfo> installedServiceInfos = AccessibilityManager
184                 .getInstance(this).getInstalledAccessibilityServiceList();
185 
186         final Set<ComponentName> enabledServices = getEnabledServicesFromSettings();
187 
188         final boolean accessibilityEnabled = Settings.Secure.getInt(getContentResolver(),
189                 Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1;
190 
191         final int serviceInfoCount = installedServiceInfos.size();
192         mAccessibilityComponentHolders.clear();
193         mAccessibilityComponentHolders.ensureCapacity(serviceInfoCount);
194         for (int i = 0; i < serviceInfoCount; i++) {
195             final AccessibilityServiceInfo accInfo = installedServiceInfos.get(i);
196             final ServiceInfo serviceInfo = accInfo.getResolveInfo().serviceInfo;
197             final ComponentName componentName = new ComponentName(serviceInfo.packageName,
198                     serviceInfo.name);
199 
200             final boolean serviceEnabled = accessibilityEnabled
201                     && enabledServices.contains(componentName);
202 
203             final String title =
204                     accInfo.getResolveInfo().loadLabel(getPackageManager()).toString();
205 
206             final AccessibilityComponentHolder component =
207                     new AccessibilityComponentHolder(componentName, title, serviceEnabled);
208 
209             mAccessibilityComponentHolders.add(component);
210 
211             header.add(getServiceHeader(component, title,
212                     ACTION_SERVICE_ENABLE_BASE + i, ACTION_SERVICE_DISABLE_BASE + i));
213         }
214 
215         return header;
216     }
217 
getServiceHeader(AccessibilityComponentHolder componentHolder, String title, int enableActionId, int disableActionId)218     private Layout.Header getServiceHeader(AccessibilityComponentHolder componentHolder,
219             String title, int enableActionId, int disableActionId) {
220         final Resources res = getResources();
221 
222         final Layout.Action enableAction = new Layout.Action.Builder(res, enableActionId)
223                 .title(R.string.settings_on)
224                 .checked(componentHolder.getEnabledGetter())
225                 .build();
226         final Layout.Action disableAction = new Layout.Action.Builder(res, disableActionId)
227                 .title(R.string.settings_off)
228                 .checked(componentHolder.getDisabledGetter())
229                 .build();
230 
231         final ComponentName componentName = componentHolder.getComponentName();
232         final ComponentName settingsIntentComponent = getSettingsForService(componentName);
233 
234         if (settingsIntentComponent != null) {
235             final Intent settingsIntent = new Intent(Intent.ACTION_MAIN)
236                     .setComponent(settingsIntentComponent);
237 
238             return new Layout.Header.Builder(res)
239                     .title(title)
240                     .description(componentHolder.getStateStringGetter())
241                     .build()
242                     .add(new Layout.Header.Builder(res)
243                             .title(R.string.system_accessibility_status)
244                             .description(componentHolder.getStateStringGetter())
245                             .build()
246                             .add(enableAction)
247                             .add(disableAction))
248                     .add(new Layout.Action.Builder(res, settingsIntent)
249                             .title(R.string.system_accessibility_config)
250                             .build());
251         } else {
252             return new Layout.Header.Builder(res)
253                     .title(title)
254                     .description(componentHolder.getStateStringGetter())
255                     .build()
256                     .add(enableAction)
257                     .add(disableAction);
258         }
259     }
260 
getSpeakPasswordsHeader()261     private Layout.Header getSpeakPasswordsHeader() {
262         final boolean speakPasswordsEnabled = Settings.Secure.getInt(getContentResolver(),
263                 Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0;
264         return new Layout.Header.Builder(getResources())
265                 .title(R.string.system_speak_passwords)
266                 .build()
267                 .setSelectionGroup(new Layout.SelectionGroup.Builder()
268                         .add(getString(R.string.settings_on), null, ACTION_SPEAK_PASSWORD_ENABLE)
269                         .add(getString(R.string.settings_off), null, ACTION_SPEAK_PASSWORD_DISABLE)
270                         .select(speakPasswordsEnabled ?
271                                 ACTION_SPEAK_PASSWORD_ENABLE : ACTION_SPEAK_PASSWORD_DISABLE)
272                         .build());
273     }
274 
getTtsHeader()275     private Layout.Header getTtsHeader() {
276         final Resources res = getResources();
277         final Layout.Header header = new Layout.Header.Builder(res)
278                 .title(R.string.system_accessibility_tts_output)
279                 .build();
280         header.add(getTtsPreferredEngineHeader());
281         header.add(mTtsLanguageLayoutGetter);
282         if (mCurrentEngine != null) {
283             final Intent intent = new Intent(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
284             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
285             intent.setPackage(mCurrentEngine);
286 
287             header.add(new Layout.Action.Builder(res, intent)
288                     .title(R.string.system_install_voice_data)
289                     .build());
290         }
291         header.add(getTtsRateHeader());
292         header.add(new Layout.Action.Builder(res, ACTION_PLAY_SAMPLE)
293                 .title(R.string.system_play_sample)
294                 .build());
295         return header;
296     }
297 
getTtsPreferredEngineHeader()298     private Layout.Header getTtsPreferredEngineHeader() {
299         mEngineInfos.clear();
300         mEngineInfos.addAll(mEnginesHelper.getEngines());
301         final Layout.SelectionGroup.Builder engineBuilder =
302                 new Layout.SelectionGroup.Builder(mEngineInfos.size());
303         int index = 0;
304         for (final EngineInfo engineInfo : mEngineInfos) {
305             final int action = ACTION_TTS_ENGINE_BASE + index++;
306             engineBuilder.add(engineInfo.label, null, action);
307             if (TextUtils.equals(mCurrentEngine, engineInfo.name)) {
308                 engineBuilder.select(action);
309             }
310         }
311 
312         return new Layout.Header.Builder(getResources())
313             .title(R.string.system_preferred_engine)
314             .build()
315             .setSelectionGroup(engineBuilder.build());
316     }
317 
318     private class TtsLanguageLayoutGetter extends Layout.LayoutGetter {
319 
320         @Override
get()321         public Layout get() {
322             if (mVoiceCheckData == null) {
323                 return new Layout();
324             }
325 
326             final Layout.SelectionGroup.Builder languageBuilder =
327                     new Layout.SelectionGroup.Builder();
328 
329             final ArrayList<String> available = mVoiceCheckData.getStringArrayListExtra(
330                     TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
331             if (available != null) {
332                 final Map<String, Locale> langMap = new ArrayMap<>(available.size());
333                 for (final String lang : available) {
334                     final Locale locale = mEnginesHelper.parseLocaleString(lang);
335                     if (locale != null) {
336                         langMap.put(locale.getDisplayName(), locale);
337                     }
338                 }
339 
340                 final List<String> languages = new ArrayList<>(langMap.keySet());
341 
342                 Collections.sort(languages, new Comparator<String>() {
343                     @Override
344                     public int compare(String lhs, String rhs) {
345                         return lhs.compareToIgnoreCase(rhs);
346                     }
347                 });
348 
349                 final Locale currentLocale = mEnginesHelper.getLocalePrefForEngine(mCurrentEngine);
350                 mEngineLocales.clear();
351                 mEngineLocales.ensureCapacity(languages.size());
352                 int index = 0;
353                 for (final String langName : languages) {
354                     final int action = ACTION_TTS_LANGUAGE_BASE + index++;
355                     final Locale locale = langMap.get(langName);
356                     mEngineLocales.add(locale);
357                     languageBuilder.add(langName, null, action);
358                     if (Objects.equals(currentLocale, locale)) {
359                         languageBuilder.select(action);
360                     }
361                 }
362             }
363             final Resources res = getResources();
364             final Locale locale = mTts.getLanguage();
365             if (locale != null) {
366                 return new Layout().add(new Layout.Header.Builder(res)
367                         .title(R.string.system_language)
368                         .description(locale.getDisplayName())
369                         .build()
370                         .setSelectionGroup(languageBuilder.build()));
371             } else {
372                 return new Layout().add(new Layout.Header.Builder(res)
373                         .title(R.string.system_language)
374                         .build()
375                         .setSelectionGroup(languageBuilder.build()));
376             }
377         }
378     }
379 
getTtsRateHeader()380     private Layout.Header getTtsRateHeader() {
381         final int selectedRateAction = Settings.Secure.getInt(getContentResolver(),
382                 TTS_DEFAULT_RATE, TTS_RATE_NORMAL) + ACTION_TTS_RATE_BASE;
383         return new Layout.Header.Builder(getResources())
384                 .title(R.string.system_speech_rate)
385                 .build()
386                 .setSelectionGroup(new Layout.SelectionGroup.Builder()
387                         .add(getString(R.string.tts_rate_very_slow), null,
388                                 ACTION_TTS_RATE_VERY_SLOW)
389                         .add(getString(R.string.tts_rate_slow), null,
390                                 ACTION_TTS_RATE_SLOW)
391                         .add(getString(R.string.tts_rate_normal), null,
392                                 ACTION_TTS_RATE_NORMAL)
393                         .add(getString(R.string.tts_rate_fast), null,
394                                 ACTION_TTS_RATE_FAST)
395                         .add(getString(R.string.tts_rate_very_fast), null,
396                                 ACTION_TTS_RATE_VERY_FAST)
397                         .select(selectedRateAction)
398                         .build());
399     }
400 
getSettingsForService(@onNull ComponentName comp)401     private ComponentName getSettingsForService(@NonNull ComponentName comp) {
402         final List<AccessibilityServiceInfo> installedServiceInfos = AccessibilityManager
403                 .getInstance(this).getInstalledAccessibilityServiceList();
404 
405         for (AccessibilityServiceInfo accInfo : installedServiceInfos) {
406             final ServiceInfo serviceInfo = accInfo.getResolveInfo().serviceInfo;
407             if (serviceInfo.packageName.equals(comp.getPackageName()) &&
408                     serviceInfo.name.equals(comp.getClassName())) {
409                 final String settingsClassName = accInfo.getSettingsActivityName();
410                 if (!TextUtils.isEmpty(settingsClassName)) {
411                     return new ComponentName(comp.getPackageName(), settingsClassName);
412                 } else {
413                     return null;
414                 }
415             }
416         }
417         return null;
418     }
419 
getEnabledServicesFromSettings()420     private Set<ComponentName> getEnabledServicesFromSettings() {
421         String enabledServicesSetting = Settings.Secure.getString(getContentResolver(),
422                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
423         if (enabledServicesSetting == null) {
424             enabledServicesSetting = "";
425         }
426         Set<ComponentName> enabledServices = new HashSet<>();
427         SimpleStringSplitter colonSplitter = new SimpleStringSplitter(
428                 ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR);
429         colonSplitter.setString(enabledServicesSetting);
430         while (colonSplitter.hasNext()) {
431             String componentNameString = colonSplitter.next();
432             ComponentName enabledService = ComponentName.unflattenFromString(
433                     componentNameString);
434             if (enabledService != null) {
435                 enabledServices.add(enabledService);
436             }
437         }
438         return enabledServices;
439     }
440 
updateDefaultEngine(String engine)441     private void updateDefaultEngine(String engine) {
442         if (DEBUG) {
443             Log.d(TAG, "Updating default synth to : " + engine);
444         }
445 
446         // TODO Disable the "play sample text" preference and the speech
447         // rate preference while the engine is being swapped.
448 
449         // Keep track of the previous engine that was being used. So that
450         // we can reuse the previous engine.
451         //
452         // Note that if TextToSpeech#getCurrentEngine is not null, it means at
453         // the very least that we successfully bound to the engine service.
454         mPreviousEngine = mTts.getCurrentEngine();
455 
456         // Shut down the existing TTS engine.
457         try {
458             mTts.shutdown();
459             mTts = null;
460         } catch (Exception e) {
461             Log.e(TAG, "Error shutting down TTS engine" + e);
462         }
463 
464         // Connect to the new TTS engine.
465         // #onUpdateEngine (below) is called when the app binds successfully to the engine.
466         if (DEBUG) {
467             Log.d(TAG, "Updating engine : Attempting to connect to engine: " + engine);
468         }
469         mTts = new TextToSpeech(getApplicationContext(), mUpdateListener, engine);
470         setTtsUtteranceProgressListener();
471     }
472 
473     @Override
onActionClicked(Layout.Action action)474     public void onActionClicked(Layout.Action action) {
475         if (action.getIntent() != null) {
476             startActivity(action.getIntent());
477             return;
478         }
479         final int actionId = action.getId();
480         final int category = actionId & ACTION_BASE_MASK;
481         switch (category) {
482             case ACTION_SERVICE_ENABLE_BASE:
483                 handleServiceClick(actionId & ~ACTION_BASE_MASK, true);
484                 break;
485             case ACTION_SERVICE_DISABLE_BASE:
486                 handleServiceClick(actionId & ~ACTION_BASE_MASK, false);
487                 break;
488             case ACTION_SPEAK_PASSWORD_BASE:
489                 handleSpeakPasswordClick(actionId);
490                 break;
491             case ACTION_TTS_BASE:
492                 handleTtsClick(actionId);
493                 break;
494             case ACTION_TTS_ENGINE_BASE:
495                 handleTtsEngineClick(actionId & ~ACTION_BASE_MASK);
496                 break;
497             case ACTION_TTS_LANGUAGE_BASE:
498                 handleTtsLanguageClick(actionId & ~ACTION_BASE_MASK);
499                 break;
500             case ACTION_TTS_RATE_BASE:
501                 handleTtsRateClick(actionId & ~ACTION_BASE_MASK);
502                 break;
503         }
504     }
505 
handleServiceClick(int serviceIndex, boolean enable)506     private void handleServiceClick(int serviceIndex, boolean enable) {
507         AccessibilityComponentHolder holder = mAccessibilityComponentHolders.get(serviceIndex);
508         final ComponentName componentName = holder.getComponentName();
509         final String label = holder.getLabel();
510         final boolean currentlyEnabled = holder.isEnabled();
511 
512         if (enable == currentlyEnabled) {
513             onBackPressed();
514             return;
515         }
516 
517         if (enable) {
518             EnableServiceDialogFragment.getInstance(componentName, label)
519                     .show(getFragmentManager(), null);
520         } else {
521             DisableServiceDialogFragment.getInstance(componentName, label)
522                     .show(getFragmentManager(), null);
523         }
524     }
525 
526     public static class EnableServiceDialogFragment extends DialogFragment {
527 
528         private static final String ARG_COMPONENT_NAME = "componentName";
529         private static final String ARG_LABEL = "label";
530 
getInstance(ComponentName componentName, String label)531         public static EnableServiceDialogFragment getInstance(ComponentName componentName,
532                 String label) {
533             final EnableServiceDialogFragment fragment = new EnableServiceDialogFragment();
534             final Bundle args = new Bundle(2);
535             args.putParcelable(ARG_COMPONENT_NAME, componentName);
536             args.putString(ARG_LABEL, label);
537             fragment.setArguments(args);
538             return fragment;
539         }
540 
541         @Override
onCreateDialog(Bundle savedInstanceState)542         public Dialog onCreateDialog(Bundle savedInstanceState) {
543             final String label = getArguments().getString(ARG_LABEL);
544             return new AlertDialog.Builder(getActivity())
545                     .setTitle(getString(R.string.system_accessibility_service_on_confirm_title,
546                             label))
547                     .setMessage(getString(R.string.system_accessibility_service_on_confirm_desc,
548                             label))
549                     .setPositiveButton(R.string.agree, new DialogInterface.OnClickListener() {
550                         @Override
551                         public void onClick(DialogInterface dialog, int which) {
552                             final AccessibilityActivity activity =
553                                     (AccessibilityActivity) getActivity();
554                             final ComponentName componentName =
555                                     getArguments().getParcelable(ARG_COMPONENT_NAME);
556                             activity.setAccessibilityServiceState(componentName, true);
557                             dismiss();
558                             activity.onBackPressed();
559                         }
560                     })
561                     .setNegativeButton(R.string.disagree, null)
562                     .create();
563         }
564     }
565 
566     public static class DisableServiceDialogFragment extends DialogFragment {
567 
568         private static final String ARG_COMPONENT_NAME = "componentName";
569         private static final String ARG_LABEL = "label";
570 
571         public static DisableServiceDialogFragment getInstance(ComponentName componentName,
572                 String label) {
573             final DisableServiceDialogFragment fragment = new DisableServiceDialogFragment();
574             final Bundle args = new Bundle(2);
575             args.putParcelable(ARG_COMPONENT_NAME, componentName);
576             args.putString(ARG_LABEL, label);
577             fragment.setArguments(args);
578             return fragment;
579         }
580 
581         @Override
582         public Dialog onCreateDialog(Bundle savedInstanceState) {
583             final String label = getArguments().getString(ARG_LABEL);
584             return new AlertDialog.Builder(getActivity())
585                     .setTitle(getString(R.string.system_accessibility_service_off_confirm_title,
586                             label))
587                     .setMessage(getString(R.string.system_accessibility_service_off_confirm_desc,
588                             label))
589                     .setPositiveButton(R.string.settings_ok, new DialogInterface.OnClickListener() {
590                         @Override
591                         public void onClick(DialogInterface dialog, int which) {
592                             final AccessibilityActivity activity =
593                                     (AccessibilityActivity) getActivity();
594                             final ComponentName componentName =
595                                     getArguments().getParcelable(ARG_COMPONENT_NAME);
596                             activity.setAccessibilityServiceState(componentName, false);
597                             activity.onBackPressed();
598                         }
599                     })
600                     .setNegativeButton(R.string.settings_cancel, null)
601                     .create();
602         }
603     }
604 
605     private void handleSpeakPasswordClick(int actionId) {
606         switch (actionId) {
607             case ACTION_SPEAK_PASSWORD_ENABLE:
608                 Settings.Secure.putInt(getContentResolver(),
609                         Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 1);
610                 break;
611             case ACTION_SPEAK_PASSWORD_DISABLE:
612                 Settings.Secure.putInt(getContentResolver(),
613                         Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0);
614                 break;
615         }
616     }
617 
618     private void handleTtsClick(int actionId) {
619         switch (actionId) {
620             case ACTION_PLAY_SAMPLE:
621                 getSampleText();
622                 break;
623         }
624     }
625 
626     private void handleTtsEngineClick(int engineIndex) {
627         final EngineInfo info = mEngineInfos.get(engineIndex);
628         mCurrentEngine = info.name;
629         updateDefaultEngine(info.name);
630     }
631 
632     private void handleTtsLanguageClick(int languageIndex) {
633         updateLanguageTo(mEngineLocales.get(languageIndex));
634     }
635 
636     private void handleTtsRateClick(int rate) {
637         Settings.Secure.putInt(getContentResolver(), TTS_DEFAULT_RATE, rate);
638     }
639 
640     private Set<ComponentName> getInstalledServices() {
641         final Set<ComponentName> installedServices = new HashSet<>();
642         installedServices.clear();
643 
644         final List<AccessibilityServiceInfo> installedServiceInfos =
645                 AccessibilityManager.getInstance(this)
646                         .getInstalledAccessibilityServiceList();
647         if (installedServiceInfos == null) {
648             return installedServices;
649         }
650 
651         for (final AccessibilityServiceInfo info : installedServiceInfos) {
652             final ResolveInfo resolveInfo = info.getResolveInfo();
653             final ComponentName installedService = new ComponentName(
654                     resolveInfo.serviceInfo.packageName,
655                     resolveInfo.serviceInfo.name);
656             installedServices.add(installedService);
657         }
658         return installedServices;
659     }
660 
661     private void setAccessibilityServiceState(ComponentName toggledService, boolean enabled) {
662         // Parse the enabled services.
663         final Set<ComponentName> enabledServices = getEnabledServicesFromSettings();
664 
665         // Determine enabled services and accessibility state.
666         boolean accessibilityEnabled = false;
667         if (enabled) {
668             enabledServices.add(toggledService);
669             // Enabling at least one service enables accessibility.
670             accessibilityEnabled = true;
671         } else {
672             enabledServices.remove(toggledService);
673             // Check how many enabled and installed services are present.
674             final Set<ComponentName> installedServices = getInstalledServices();
675             for (ComponentName enabledService : enabledServices) {
676                 if (installedServices.contains(enabledService)) {
677                     // Disabling the last service disables accessibility.
678                     accessibilityEnabled = true;
679                     break;
680                 }
681             }
682         }
683 
684         // Update the enabled services setting.
685         final StringBuilder enabledServicesBuilder = new StringBuilder();
686         // Keep the enabled services even if they are not installed since we
687         // have no way to know whether the application restore process has
688         // completed. In general the system should be responsible for the
689         // clean up not settings.
690         for (ComponentName enabledService : enabledServices) {
691             enabledServicesBuilder.append(enabledService.flattenToString());
692             enabledServicesBuilder.append(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR);
693         }
694         final int enabledServicesBuilderLength = enabledServicesBuilder.length();
695         if (enabledServicesBuilderLength > 0) {
696             enabledServicesBuilder.deleteCharAt(enabledServicesBuilderLength - 1);
697         }
698         Settings.Secure.putString(getContentResolver(),
699                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
700                 enabledServicesBuilder.toString());
701 
702         // Update accessibility enabled.
703         Settings.Secure.putInt(getContentResolver(),
704                 Settings.Secure.ACCESSIBILITY_ENABLED, accessibilityEnabled ? 1 : 0);
705 
706         for (final AccessibilityComponentHolder holder : mAccessibilityComponentHolders) {
707             if (holder.getComponentName().equals(toggledService)) {
708                 holder.updateComponentState(enabled);
709             }
710         }
711     }
712 
713     /*
714      * Check whether the voice data for the engine is ok.
715      */
716     private void checkVoiceData(String engine) {
717         if (TextUtils.equals(mVoiceCheckEngine, engine)) {
718             return;
719         }
720         mVoiceCheckEngine = engine;
721         Intent intent = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
722         intent.setPackage(engine);
723         try {
724             if (DEBUG) {
725                 Log.d(TAG, "Updating engine: Checking voice data: " + intent.toUri(0));
726             }
727             startActivityForResult(intent, VOICE_DATA_INTEGRITY_CHECK);
728         } catch (ActivityNotFoundException ex) {
729             Log.e(TAG, "Failed to check TTS data, no activity found for " + intent + ")");
730         }
731     }
732 
733     /**
734      * Called when the TTS engine is initialized.
735      */
736     private void onInitEngine(int status) {
737         if (status == TextToSpeech.SUCCESS) {
738             if (DEBUG) {
739                 Log.d(TAG, "TTS engine for settings screen initialized.");
740             }
741         } else {
742             if (DEBUG) {
743                 Log.d(TAG, "TTS engine for settings screen failed to initialize successfully.");
744             }
745         }
746     }
747 
748     /*
749      * We have now bound to the TTS engine the user requested. We will
750      * attempt to check voice data for the engine if we successfully bound to it,
751      * or revert to the previous engine if we didn't.
752      */
753     private void onUpdateEngine(int status) {
754         if (status == TextToSpeech.SUCCESS) {
755             if (DEBUG) {
756                 Log.d(TAG, "Updating engine: Successfully bound to the engine: " +
757                         mTts.getCurrentEngine());
758             }
759             checkVoiceData(mTts.getCurrentEngine());
760         } else {
761             if (DEBUG) {
762                 Log.d(TAG, "Updating engine: Failed to bind to engine, reverting.");
763             }
764             if (mPreviousEngine != null) {
765                 // This is guaranteed to at least bind, since mPreviousEngine
766                 // would be
767                 // null if the previous bind to this engine failed.
768                 mTts = new TextToSpeech(getApplicationContext(), mInitListener,
769                         mPreviousEngine);
770                 setTtsUtteranceProgressListener();
771             }
772             mPreviousEngine = null;
773         }
774         onBackPressed();
775     }
776 
777     private void setTtsUtteranceProgressListener() {
778         if (mTts == null) {
779             return;
780         }
781         mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
782                 @Override
783             public void onStart(String utteranceId) {
784             }
785 
786                 @Override
787             public void onDone(String utteranceId) {
788             }
789 
790                 @Override
791             public void onError(String utteranceId) {
792                 Log.e(TAG, "Error while trying to synthesize sample text");
793             }
794         });
795     }
796 
797     private void updateLanguageTo(Locale locale) {
798         mEnginesHelper.updateLocalePrefForEngine(mCurrentEngine, locale);
799         if (mCurrentEngine.equals(mTts.getCurrentEngine())) {
800             // Null locale means "use system default"
801             mTts.setLanguage((locale != null) ? locale : Locale.getDefault());
802         }
803     }
804 
805     /**
806      * Called when voice data integrity check returns
807      */
808     @Override
809     public void onActivityResult(int requestCode, int resultCode, Intent data) {
810         if (requestCode == GET_SAMPLE_TEXT) {
811             onSampleTextReceived(resultCode, data);
812         } else if (requestCode == VOICE_DATA_INTEGRITY_CHECK) {
813             onVoiceDataIntegrityCheckDone(data);
814         }
815     }
816 
817     /**
818      * Ask the current default engine to return a string of sample text to be
819      * spoken to the user.
820      */
821     private void getSampleText() {
822         String currentEngine = mTts.getCurrentEngine();
823 
824         if (TextUtils.isEmpty(currentEngine))
825             currentEngine = mTts.getDefaultEngine();
826 
827         Locale defaultLocale = mTts.getDefaultLanguage();
828         if (defaultLocale == null) {
829             Log.e(TAG, "Failed to get default language from engine " + currentEngine);
830             return;
831         }
832         mTts.setLanguage(defaultLocale);
833 
834         // TODO: This is currently a hidden private API. The intent extras
835         // and the intent action should be made public if we intend to make this
836         // a public API. We fall back to using a canned set of strings if this
837         // doesn't work.
838         Intent intent = new Intent(TextToSpeech.Engine.ACTION_GET_SAMPLE_TEXT);
839 
840         intent.putExtra("language", defaultLocale.getLanguage());
841         intent.putExtra("country", defaultLocale.getCountry());
842         intent.putExtra("variant", defaultLocale.getVariant());
843         intent.setPackage(currentEngine);
844 
845         try {
846             if (DEBUG) {
847                 Log.d(TAG, "Getting sample text: " + intent.toUri(0));
848             }
849             startActivityForResult(intent, GET_SAMPLE_TEXT);
850         } catch (ActivityNotFoundException ex) {
851             Log.e(TAG, "Failed to get sample text, no activity found for " + intent + ")");
852         }
853     }
854 
855     private String getDefaultSampleString() {
856         if (mTts != null && mTts.getLanguage() != null) {
857             final String currentLang = mTts.getLanguage().getISO3Language();
858             final Resources res = getResources();
859             final String[] strings = res.getStringArray(R.array.tts_demo_strings);
860             final String[] langs = res.getStringArray(R.array.tts_demo_string_langs);
861 
862             for (int i = 0; i < strings.length; ++i) {
863                 if (langs[i].equals(currentLang)) {
864                     return strings[i];
865                 }
866             }
867         }
868         return null;
869     }
870 
871     private void onSampleTextReceived(int resultCode, Intent data) {
872         String sample = getDefaultSampleString();
873 
874         if (resultCode == TextToSpeech.LANG_AVAILABLE && data != null) {
875             if (data.getStringExtra("sampleText") != null) {
876                 sample = data.getStringExtra("sampleText");
877             }
878             if (DEBUG) {
879                 Log.d(TAG, "Got sample text: " + sample);
880             }
881         } else {
882             if (DEBUG) {
883                 Log.d(TAG, "Using default sample text :" + sample);
884             }
885         }
886 
887         if (sample != null && mTts != null) {
888             // The engine is guaranteed to have been initialized here
889             // because this preference is not enabled otherwise.
890 
891             final boolean networkRequired = isNetworkRequiredForSynthesis();
892             if (!networkRequired ||
893                     (mTts.isLanguageAvailable(mTts.getLanguage())
894                             >= TextToSpeech.LANG_AVAILABLE)) {
895                 mTts.speak(sample, TextToSpeech.QUEUE_FLUSH, null, "Sample");
896             } else {
897                 Log.w(TAG, "Network required for sample synthesis for requested language");
898                 // TODO displayNetworkAlert();
899             }
900         } else {
901             // TODO: Display an error here to the user.
902             Log.e(TAG, "Did not have a sample string for the requested language");
903         }
904     }
905 
906     private boolean isNetworkRequiredForSynthesis() {
907         Set<String> features = mTts.getFeatures(mTts.getLanguage());
908         return features.contains(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS) &&
909                 !features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
910     }
911 
912     /*
913      * Called when the voice data check is complete.
914      */
915     private void onVoiceDataIntegrityCheckDone(Intent data) {
916         final String engine = mTts.getCurrentEngine();
917 
918         if (engine == null) {
919             Log.e(TAG, "Voice data check complete, but no engine bound");
920             return;
921         }
922 
923         if (data == null) {
924             Log.e(TAG, "Engine failed voice data integrity check (null return)" +
925                     mTts.getCurrentEngine());
926             return;
927         }
928 
929         Settings.Secure.putString(getContentResolver(), TTS_DEFAULT_SYNTH, engine);
930 
931         mVoiceCheckData = data;
932 
933         mTtsLanguageLayoutGetter.refreshView();
934     }
935 
936     private class AccessibilityComponentHolder {
937         final ComponentName mComponentName;
938         final Layout.MutableBooleanGetter mEnabledGetter;
939         final Layout.MutableBooleanGetter mDisabledGetter;
940         final Layout.StringGetter mStateStringGetter;
941         final String mLabel;
942         boolean mEnabled;
943 
944         public AccessibilityComponentHolder(ComponentName componentName, String label,
945                 boolean enabled) {
946             mComponentName = componentName;
947             mEnabledGetter = new Layout.MutableBooleanGetter(enabled);
948             mDisabledGetter = new Layout.MutableBooleanGetter(!enabled);
949             mStateStringGetter = new Layout.StringGetter() {
950                 @Override
951                 public String get() {
952                     return getString(mEnabled ? R.string.settings_on : R.string.settings_off);
953                 }
954             };
955             mLabel = label;
956             mEnabled = enabled;
957         }
958 
959         public ComponentName getComponentName() {
960             return mComponentName;
961         }
962 
963         public Layout.MutableBooleanGetter getEnabledGetter() {
964             return mEnabledGetter;
965         }
966 
967         public Layout.MutableBooleanGetter getDisabledGetter() {
968             return mDisabledGetter;
969         }
970 
971         public Layout.StringGetter getStateStringGetter() {
972             return mStateStringGetter;
973         }
974 
975         public String getLabel() {
976             return mLabel;
977         }
978 
979         public boolean isEnabled() {
980             return mEnabled;
981         }
982 
983         public void updateComponentState(boolean enabled) {
984             mEnabled = enabled;
985             mStateStringGetter.refreshView();
986             mEnabledGetter.set(enabled);
987             mDisabledGetter.set(!enabled);
988         }
989     }
990 }
991