Home | History | Annotate | Download | only in system
      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
    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
    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 
    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 
    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 
    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 
    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 
    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 
    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
    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 
    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 
    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 
    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 
    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
    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 
    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 
    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
    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