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