/documentation/ClockBackTutorial/ClockBack5/src/com/google/android/marvin/clockback/ClockBackService.java

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