/ClockBack/src/com/google/android/marvin/clockback/ClockBackService.java

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