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