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