1 /* 2 * Copyright (C) 2010 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.example.android.clockback; 18 19 import android.accessibilityservice.AccessibilityService; 20 import android.accessibilityservice.AccessibilityServiceInfo; 21 import android.app.Service; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.media.AudioManager; 27 import android.os.Handler; 28 import android.os.Message; 29 import android.os.Vibrator; 30 import android.speech.tts.TextToSpeech; 31 import android.util.Log; 32 import android.util.SparseArray; 33 import android.view.accessibility.AccessibilityEvent; 34 35 import java.util.List; 36 37 /** 38 * This class is an {@link AccessibilityService} that provides custom feedback 39 * for the Clock application that comes by default with Android devices. It 40 * demonstrates the following key features of the Android accessibility APIs: 41 * <ol> 42 * <li> 43 * Simple demonstration of how to use the accessibility APIs. 44 * </li> 45 * <li> 46 * Hands-on example of various ways to utilize the accessibility API for 47 * providing alternative and complementary feedback. 48 * </li> 49 * <li> 50 * Providing application specific feedback — the service handles only 51 * accessibility events from the clock application. 52 * </li> 53 * <li> 54 * Providing dynamic, context-dependent feedback — feedback type changes 55 * depending on the ringer state. 56 * </li> 57 * <li> 58 * Application specific UI enhancement - application domain knowledge is 59 * utilized to enhance the provided feedback. 60 * </li> 61 * </ol> 62 * <p> 63 * <strong> 64 * Note: This code sample will work only on devices shipped with the default Clock 65 * application. If you are running Android 1.6 of Android 2.0 you should enable first 66 * ClockBack and then TalkBack since in these releases accessibility services are 67 * notified in the order of registration. 68 * </strong> 69 * </p> 70 */ 71 public class ClockBackService extends AccessibilityService { 72 73 /** Tag for logging from this service. */ 74 private static final String LOG_TAG = "ClockBackService"; 75 76 // Fields for configuring how the system handles this accessibility service. 77 78 /** Minimal timeout between accessibility events we want to receive. */ 79 private static final int EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80; 80 81 /** Packages we are interested in. 82 * <p> 83 * <strong> 84 * Note: This code sample will work only on devices shipped with the 85 * default Clock application. 86 * </strong> 87 * </p> 88 */ 89 // This works with AlarmClock and Clock whose package name changes in different releases 90 private static final String[] PACKAGE_NAMES = new String[] { 91 "com.android.alarmclock", "com.google.android.deskclock", "com.android.deskclock" 92 }; 93 94 // Message types we are passing around. 95 96 /** Speak. */ 97 private static final int MESSAGE_SPEAK = 1; 98 99 /** Stop speaking. */ 100 private static final int MESSAGE_STOP_SPEAK = 2; 101 102 /** Start the TTS service. */ 103 private static final int MESSAGE_START_TTS = 3; 104 105 /** Stop the TTS service. */ 106 private static final int MESSAGE_SHUTDOWN_TTS = 4; 107 108 /** Play an earcon. */ 109 private static final int MESSAGE_PLAY_EARCON = 5; 110 111 /** Stop playing an earcon. */ 112 private static final int MESSAGE_STOP_PLAY_EARCON = 6; 113 114 /** Vibrate a pattern. */ 115 private static final int MESSAGE_VIBRATE = 7; 116 117 /** Stop vibrating. */ 118 private static final int MESSAGE_STOP_VIBRATE = 8; 119 120 // Screen state broadcast related constants. 121 122 /** Feedback mapping index used as a key for the screen-on broadcast. */ 123 private static final int INDEX_SCREEN_ON = 0x00000100; 124 125 /** Feedback mapping index used as a key for the screen-off broadcast. */ 126 private static final int INDEX_SCREEN_OFF = 0x00000200; 127 128 // Ringer mode change related constants. 129 130 /** Feedback mapping index used as a key for normal ringer mode. */ 131 private static final int INDEX_RINGER_NORMAL = 0x00000400; 132 133 /** Feedback mapping index used as a key for vibration ringer mode. */ 134 private static final int INDEX_RINGER_VIBRATE = 0x00000800; 135 136 /** Feedback mapping index used as a key for silent ringer mode. */ 137 private static final int INDEX_RINGER_SILENT = 0x00001000; 138 139 // Speech related constants. 140 141 /** 142 * The queuing mode we are using - interrupt a spoken utterance before 143 * speaking another one. 144 */ 145 private static final int QUEUING_MODE_INTERRUPT = 2; 146 147 /** The space string constant. */ 148 private static final String SPACE = " "; 149 150 /** 151 * The class name of the number picker buttons with no text we want to 152 * announce in the Clock application. 153 */ 154 private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK = "android.widget.NumberPickerButton"; 155 156 /** 157 * The class name of the number picker buttons with no text we want to 158 * announce in the AlarmClock application. 159 */ 160 private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK = "com.android.internal.widget.NumberPickerButton"; 161 162 /** 163 * The class name of the edit text box for hours and minutes we want to 164 * better announce. 165 */ 166 private static final String CLASS_NAME_EDIT_TEXT = "android.widget.EditText"; 167 168 /** 169 * Mapping from integer to string resource id where the keys are generated 170 * from the {@link AccessibilityEvent#getText()}, 171 * {@link AccessibilityEvent#getItemCount()} and 172 * {@link AccessibilityEvent#getCurrentItemIndex()} properties. 173 * <p> 174 * Note: In general, computing these mappings includes the widget position on 175 * the screen. This is fragile and should be used as a last resort since 176 * changing the layout could potentially change the widget position. This is 177 * a workaround since the widgets of interest are image buttons that do not 178 * have contentDescription attribute set (plus/minus buttons) or no other 179 * information in the accessibility event is available to distinguish them 180 * aside of their positions on the screen (hour/minute inputs).<br/> 181 * If you are owner of the target application (Clock in this case) you 182 * should add contentDescription attribute to all image buttons such that a 183 * screen reader knows how to speak them. For input fields (while not 184 * applicable for the hour and minute inputs since they are not empty) a 185 * hint text should be set to enable better announcement. 186 * </p> 187 */ 188 private static final SparseArray<Integer> sEventDataMappedStringResourceIds = new SparseArray<Integer>(); 189 static { 190 sEventDataMappedStringResourceIds.put(110, R.string.value_increase_hours); 191 sEventDataMappedStringResourceIds.put(1140, R.string.value_increase_minutes); 192 sEventDataMappedStringResourceIds.put(1120, R.string.value_decrease_hours); 193 sEventDataMappedStringResourceIds.put(1160, R.string.value_decrease_minutes); 194 sEventDataMappedStringResourceIds.put(1111, R.string.value_hour); 195 sEventDataMappedStringResourceIds.put(1110, R.string.value_hours); 196 sEventDataMappedStringResourceIds.put(1151, R.string.value_minute); 197 sEventDataMappedStringResourceIds.put(1150, R.string.value_minutes); 198 } 199 200 /** Mapping from integers to vibration patterns for haptic feedback. */ 201 private static final SparseArray<long[]> sVibrationPatterns = new SparseArray<long[]>(); 202 static { 203 sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, new long[] { 204 0L, 100L 205 }); 206 sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new long[] { 207 0L, 100L 208 }); 209 sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, new long[] { 210 0L, 15L, 10L, 15L 211 }); 212 sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, new long[] { 213 0L, 15L, 10L, 15L 214 }); 215 sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, new long[] { 216 0L, 25L, 50L, 25L, 50L, 25L 217 }); 218 sVibrationPatterns.put(INDEX_SCREEN_ON, new long[] { 219 0L, 10L, 10L, 20L, 20L, 30L 220 }); 221 sVibrationPatterns.put(INDEX_SCREEN_OFF, new long[] { 222 0L, 30L, 20L, 20L, 10L, 10L 223 }); 224 } 225 226 /** Mapping from integers to raw sound resource ids. */ 227 private static SparseArray<Integer> sSoundsResourceIds = new SparseArray<Integer>(); 228 static { 229 sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_CLICKED, R.raw.sound_view_clicked); 230 sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, R.raw.sound_view_clicked); 231 sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED, R.raw.sound_view_focused_or_selected); 232 sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, R.raw.sound_view_focused_or_selected); 233 sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, R.raw.sound_window_state_changed); 234 sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound_screen_on); 235 sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound_screen_off); 236 sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound_ringer_silent); 237 sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound_ringer_vibrate); 238 sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound_ringer_normal); 239 } 240 241 // Sound pool related member fields. 242 243 /** Mapping from integers to earcon names - dynamically populated. */ 244 private final SparseArray<String> mEarconNames = new SparseArray<String>(); 245 246 // Auxiliary fields. 247 248 /** 249 * Handle to this service to enable inner classes to access the {@link Context}. 250 */ 251 Context mContext; 252 253 /** The feedback this service is currently providing. */ 254 int mProvidedFeedbackType; 255 256 /** Reusable instance for building utterances. */ 257 private final StringBuilder mUtterance = new StringBuilder(); 258 259 // Feedback providing services. 260 261 /** The {@link TextToSpeech} used for speaking. */ 262 private TextToSpeech mTts; 263 264 /** The {@link AudioManager} for detecting ringer state. */ 265 private AudioManager mAudioManager; 266 267 /** Vibrator for providing haptic feedback. */ 268 private Vibrator mVibrator; 269 270 /** Flag if the infrastructure is initialized. */ 271 private boolean isInfrastructureInitialized; 272 273 /** {@link Handler} for executing messages on the service main thread. */ 274 Handler mHandler = new Handler() { 275 @Override 276 public void handleMessage(Message message) { 277 switch (message.what) { 278 case MESSAGE_SPEAK: 279 String utterance = (String) message.obj; 280 mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null); 281 return; 282 case MESSAGE_STOP_SPEAK: 283 mTts.stop(); 284 return; 285 case MESSAGE_START_TTS: 286 mTts = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() { 287 public void onInit(int status) { 288 // Register here since to add earcons the TTS must be initialized and 289 // the receiver is called immediately with the current ringer mode. 290 registerBroadCastReceiver(); 291 } 292 }); 293 return; 294 case MESSAGE_SHUTDOWN_TTS: 295 mTts.shutdown(); 296 return; 297 case MESSAGE_PLAY_EARCON: 298 int resourceId = message.arg1; 299 playEarcon(resourceId); 300 return; 301 case MESSAGE_STOP_PLAY_EARCON: 302 mTts.stop(); 303 return; 304 case MESSAGE_VIBRATE: 305 int key = message.arg1; 306 long[] pattern = sVibrationPatterns.get(key); 307 mVibrator.vibrate(pattern, -1); 308 return; 309 case MESSAGE_STOP_VIBRATE: 310 mVibrator.cancel(); 311 return; 312 } 313 } 314 }; 315 316 /** 317 * {@link BroadcastReceiver} for receiving updates for our context - device 318 * state. 319 */ 320 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 321 @Override 322 public void onReceive(Context context, Intent intent) { 323 String action = intent.getAction(); 324 325 if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { 326 int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, 327 AudioManager.RINGER_MODE_NORMAL); 328 configureForRingerMode(ringerMode); 329 } else if (Intent.ACTION_SCREEN_ON.equals(action)) { 330 provideScreenStateChangeFeedback(INDEX_SCREEN_ON); 331 } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { 332 provideScreenStateChangeFeedback(INDEX_SCREEN_OFF); 333 } else { 334 Log.w(LOG_TAG, "Registered for but not handling action " + action); 335 } 336 } 337 338 /** 339 * Provides feedback to announce the screen state change. Such a change 340 * is turning the screen on or off. 341 * 342 * @param feedbackIndex The index of the feedback in the statically 343 * mapped feedback resources. 344 */ 345 private void provideScreenStateChangeFeedback(int feedbackIndex) { 346 // We take a specific action depending on the feedback we currently provide. 347 switch (mProvidedFeedbackType) { 348 case AccessibilityServiceInfo.FEEDBACK_SPOKEN: 349 String utterance = generateScreenOnOrOffUtternace(feedbackIndex); 350 mHandler.obtainMessage(MESSAGE_SPEAK, utterance).sendToTarget(); 351 return; 352 case AccessibilityServiceInfo.FEEDBACK_AUDIBLE: 353 mHandler.obtainMessage(MESSAGE_PLAY_EARCON, feedbackIndex, 0).sendToTarget(); 354 return; 355 case AccessibilityServiceInfo.FEEDBACK_HAPTIC: 356 mHandler.obtainMessage(MESSAGE_VIBRATE, feedbackIndex, 0).sendToTarget(); 357 return; 358 default: 359 throw new IllegalStateException("Unexpected feedback type " 360 + mProvidedFeedbackType); 361 } 362 } 363 }; 364 365 @Override 366 public void onServiceConnected() { 367 if (isInfrastructureInitialized) { 368 return; 369 } 370 371 mContext = this; 372 373 // Send a message to start the TTS. 374 mHandler.sendEmptyMessage(MESSAGE_START_TTS); 375 376 // Get the vibrator service. 377 mVibrator = (Vibrator) getSystemService(Service.VIBRATOR_SERVICE); 378 379 // Get the AudioManager and configure according the current ring mode. 380 mAudioManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE); 381 // In Froyo the broadcast receiver for the ringer mode is called back with the 382 // current state upon registering but in Eclair this is not done so we poll here. 383 int ringerMode = mAudioManager.getRingerMode(); 384 configureForRingerMode(ringerMode); 385 386 // We are in an initialized state now. 387 isInfrastructureInitialized = true; 388 } 389 390 @Override 391 public boolean onUnbind(Intent intent) { 392 if (isInfrastructureInitialized) { 393 // Stop the TTS service. 394 mHandler.sendEmptyMessage(MESSAGE_SHUTDOWN_TTS); 395 396 // Unregister the intent broadcast receiver. 397 if (mBroadcastReceiver != null) { 398 unregisterReceiver(mBroadcastReceiver); 399 } 400 401 // We are not in an initialized state anymore. 402 isInfrastructureInitialized = false; 403 } 404 return false; 405 } 406 407 /** 408 * Registers the phone state observing broadcast receiver. 409 */ 410 private void registerBroadCastReceiver() { 411 // Create a filter with the broadcast intents we are interested in. 412 IntentFilter filter = new IntentFilter(); 413 filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); 414 filter.addAction(Intent.ACTION_SCREEN_ON); 415 filter.addAction(Intent.ACTION_SCREEN_OFF); 416 // Register for broadcasts of interest. 417 registerReceiver(mBroadcastReceiver, filter, null, null); 418 } 419 420 /** 421 * Generates an utterance for announcing screen on and screen off. 422 * 423 * @param feedbackIndex The feedback index for looking up feedback value. 424 * @return The utterance. 425 */ 426 private String generateScreenOnOrOffUtternace(int feedbackIndex) { 427 // Get the announce template. 428 int resourceId = (feedbackIndex == INDEX_SCREEN_ON) ? R.string.template_screen_on 429 : R.string.template_screen_off; 430 String template = mContext.getString(resourceId); 431 432 // Format the template with the ringer percentage. 433 int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING); 434 int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING); 435 int volumePercent = (100 / maxRingerVolume) * currentRingerVolume; 436 437 // Let us round to five so it sounds better. 438 int adjustment = volumePercent % 10; 439 if (adjustment < 5) { 440 volumePercent -= adjustment; 441 } else if (adjustment > 5) { 442 volumePercent += (10 - adjustment); 443 } 444 445 return String.format(template, volumePercent); 446 } 447 448 /** 449 * Configures the service according to a ringer mode. Possible 450 * configurations: 451 * <p> 452 * 1. {@link AudioManager#RINGER_MODE_SILENT}<br/> 453 * Goal: Provide only custom haptic feedback.<br/> 454 * Approach: Take over the haptic feedback by configuring this service to provide 455 * such and do so. This way the system will not call the default haptic 456 * feedback service KickBack.<br/> 457 * Take over the audible and spoken feedback by configuring this 458 * service to provide such feedback but not doing so. This way the system 459 * will not call the default spoken feedback service TalkBack and the 460 * default audible feedback service SoundBack. 461 * </p> 462 * <p> 463 * 2. {@link AudioManager#RINGER_MODE_VIBRATE}<br/> 464 * Goal: Provide custom audible and default haptic feedback.<br/> 465 * Approach: Take over the audible feedback and provide custom one.<br/> 466 * Take over the spoken feedback but do not provide such.<br/> 467 * Let some other service provide haptic feedback (KickBack). 468 * </p> 469 * <p> 470 * 3. {@link AudioManager#RINGER_MODE_NORMAL} 471 * Goal: Provide custom spoken, default audible and default haptic feedback.<br/> 472 * Approach: Take over the spoken feedback and provide custom one.<br/> 473 * Let some other services provide audible feedback (SounBack) and haptic 474 * feedback (KickBack). 475 * </p> 476 * Note: In the above description an assumption is made that all default feedback 477 * services are enabled. Such services are TalkBack, SoundBack, and KickBack. 478 * Also the feature of defining a service as the default for a given feedback 479 * type will be available in Android 2.2 and above. For previous releases the package 480 * specific accessibility service must be registered first i.e. checked in the 481 * settings. 482 * 483 * @param ringerMode The device ringer mode. 484 */ 485 private void configureForRingerMode(int ringerMode) { 486 if (ringerMode == AudioManager.RINGER_MODE_SILENT) { 487 // When the ringer is silent we want to provide only haptic feedback. 488 mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_HAPTIC; 489 490 // Take over the spoken and sound feedback so no such feedback is provided. 491 setServiceInfo(AccessibilityServiceInfo.FEEDBACK_HAPTIC 492 | AccessibilityServiceInfo.FEEDBACK_SPOKEN 493 | AccessibilityServiceInfo.FEEDBACK_AUDIBLE); 494 495 // Use only an earcon to announce ringer state change. 496 mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_SILENT, 0).sendToTarget(); 497 } else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) { 498 // When the ringer is vibrating we want to provide only audible feedback. 499 mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_AUDIBLE; 500 501 // Take over the spoken feedback so no spoken feedback is provided. 502 setServiceInfo(AccessibilityServiceInfo.FEEDBACK_AUDIBLE 503 | AccessibilityServiceInfo.FEEDBACK_SPOKEN); 504 505 // Use only an earcon to announce ringer state change. 506 mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_VIBRATE, 0).sendToTarget(); 507 } else if (ringerMode == AudioManager.RINGER_MODE_NORMAL) { 508 // When the ringer is ringing we want to provide spoken feedback 509 // overriding the default spoken feedback. 510 mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN; 511 setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN); 512 513 // Use only an earcon to announce ringer state change. 514 mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_NORMAL, 0).sendToTarget(); 515 } 516 } 517 518 /** 519 * Sets the {@link AccessibilityServiceInfo} which informs the system how to 520 * handle this {@link AccessibilityService}. 521 * 522 * @param feedbackType The type of feedback this service will provide. 523 * <p> 524 * Note: The feedbackType parameter is an bitwise or of all 525 * feedback types this service would like to provide. 526 * </p> 527 */ 528 private void setServiceInfo(int feedbackType) { 529 AccessibilityServiceInfo info = new AccessibilityServiceInfo(); 530 // We are interested in all types of accessibility events. 531 info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; 532 // We want to provide specific type of feedback. 533 info.feedbackType = feedbackType; 534 // We want to receive events in a certain interval. 535 info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS; 536 // We want to receive accessibility events only from certain packages. 537 info.packageNames = PACKAGE_NAMES; 538 setServiceInfo(info); 539 } 540 541 @Override 542 public void onAccessibilityEvent(AccessibilityEvent event) { 543 Log.i(LOG_TAG, mProvidedFeedbackType + " " + event.toString()); 544 545 // Here we act according to the feedback type we are currently providing. 546 if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) { 547 mHandler.obtainMessage(MESSAGE_SPEAK, formatUtterance(event)).sendToTarget(); 548 } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) { 549 mHandler.obtainMessage(MESSAGE_PLAY_EARCON, event.getEventType(), 0).sendToTarget(); 550 } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_HAPTIC) { 551 mHandler.obtainMessage(MESSAGE_VIBRATE, event.getEventType(), 0).sendToTarget(); 552 } else { 553 throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType); 554 } 555 } 556 557 @Override 558 public void onInterrupt() { 559 // Here we act according to the feedback type we are currently providing. 560 if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) { 561 mHandler.obtainMessage(MESSAGE_STOP_SPEAK).sendToTarget(); 562 } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) { 563 mHandler.obtainMessage(MESSAGE_STOP_PLAY_EARCON).sendToTarget(); 564 } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_HAPTIC) { 565 mHandler.obtainMessage(MESSAGE_STOP_VIBRATE).sendToTarget(); 566 } else { 567 throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType); 568 } 569 } 570 571 /** 572 * Formats an utterance from an {@link AccessibilityEvent}. 573 * 574 * @param event The event from which to format an utterance. 575 * @return The formatted utterance. 576 */ 577 private String formatUtterance(AccessibilityEvent event) { 578 StringBuilder utterance = mUtterance; 579 580 // Clear the utterance before appending the formatted text. 581 utterance.setLength(0); 582 583 List<CharSequence> eventText = event.getText(); 584 585 // We try to get the event text if such. 586 if (!eventText.isEmpty()) { 587 for (CharSequence subText : eventText) { 588 // Make 01 pronounced as 1 589 if (subText.charAt(0) =='0') { 590 subText = subText.subSequence(1, subText.length()); 591 } 592 utterance.append(subText); 593 utterance.append(SPACE); 594 } 595 596 // Here we do a bit of enhancement of the UI presentation by using the semantic 597 // of the event source in the context of the Clock application. 598 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED 599 && CLASS_NAME_EDIT_TEXT.equals(event.getClassName())) { 600 // If the source is an edit text box and we have a mapping based on 601 // its position in the items of the container parent of the event source 602 // we append that value as well. We say "XX hours" and "XX minutes". 603 String resourceValue = getEventDataMappedStringResource(event); 604 if (resourceValue != null) { 605 utterance.append(resourceValue); 606 } 607 } 608 609 return utterance.toString(); 610 } 611 612 // There is no event text but we try to get the content description which is 613 // an optional attribute for describing a view (typically used with ImageView). 614 CharSequence contentDescription = event.getContentDescription(); 615 if (contentDescription != null) { 616 utterance.append(contentDescription); 617 return utterance.toString(); 618 } 619 620 // No text and content description for the plus and minus buttons, so we lookup 621 // custom values based on the event's itemCount and currentItemIndex properties. 622 CharSequence className = event.getClassName(); 623 624 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED 625 && (CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK.equals(className) 626 || CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK.equals(className))) { 627 String resourceValue = getEventDataMappedStringResource(event); 628 utterance.append(resourceValue); 629 } 630 631 return utterance.toString(); 632 } 633 634 /** 635 * Returns a string resource mapped based on the accessibility event 636 * data, specifically the 637 * {@link AccessibilityEvent#getText()}, 638 * {@link AccessibilityEvent#getItemCount()}, and 639 * {@link AccessibilityEvent#getCurrentItemIndex()} properties. 640 * 641 * @param event The {@link AccessibilityEvent} to process. 642 * @return The mapped string if such exists, null otherwise. 643 */ 644 private String getEventDataMappedStringResource(AccessibilityEvent event) { 645 int lookupIndex = computeLookupIndex(event); 646 int resourceId = sEventDataMappedStringResourceIds.get(lookupIndex); 647 return getString(resourceId); 648 } 649 650 /** 651 * Computes an index for looking up the custom text for views which either 652 * do not have text/content description or the position information 653 * is the only oracle for deciding from which widget was an accessibility 654 * event generated. The index is computed based on 655 * {@link AccessibilityEvent#getText()}, 656 * {@link AccessibilityEvent#getItemCount()}, and 657 * {@link AccessibilityEvent#getCurrentItemIndex()} properties. 658 * 659 * @param event The event from which to compute the index. 660 * @return The lookup index. 661 */ 662 private int computeLookupIndex(AccessibilityEvent event) { 663 int lookupIndex = event.getItemCount(); 664 int divided = event.getCurrentItemIndex(); 665 666 while (divided > 0) { 667 lookupIndex *= 10; 668 divided /= 10; 669 } 670 671 lookupIndex += event.getCurrentItemIndex(); 672 lookupIndex *= 10; 673 674 // This is primarily for handling the zero hour/zero minutes cases 675 if (!event.getText().isEmpty() 676 && ("1".equals(event.getText().get(0).toString()) || "01".equals(event.getText() 677 .get(0).toString()))) { 678 lookupIndex++; 679 } 680 681 return lookupIndex; 682 } 683 684 /** 685 * Plays an earcon given its id. 686 * 687 * @param earconId The id of the earcon to be played. 688 */ 689 private void playEarcon(int earconId) { 690 String earconName = mEarconNames.get(earconId); 691 if (earconName == null) { 692 // We do not know the sound id, hence we need to load the sound. 693 int resourceId = sSoundsResourceIds.get(earconId); 694 earconName = "[" + earconId + "]"; 695 mTts.addEarcon(earconName, getPackageName(), resourceId); 696 mEarconNames.put(earconId, earconName); 697 } 698 699 mTts.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null); 700 } 701 } 702