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

http://eyes-free.googlecode.com/ · Java · 330 lines · 142 code · 51 blank · 137 comment · 13 complexity · b177728111f06d8da2955ea3d2dda914 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.content.Context;
  20. import android.content.Intent;
  21. import android.os.Handler;
  22. import android.os.Message;
  23. import android.speech.tts.TextToSpeech;
  24. import android.util.Log;
  25. import android.util.SparseArray;
  26. import android.view.accessibility.AccessibilityEvent;
  27. import java.util.List;
  28. /**
  29. * This class is an {@link AccessibilityService} that provides custom feedback
  30. * for the Clock application that comes by default with Android devices. It
  31. * demonstrates the following key features of the Android accessibility APIs:
  32. * <ol>
  33. * <li>
  34. * Simple demonstration of how to use the accessibility APIs.
  35. * </li>
  36. * <li>
  37. * Hands-on example of various ways to utilize the accessibility API for
  38. * providing alternative and complementary feedback.
  39. * </li>
  40. * <li>
  41. * Providing application specific feedback - the service handles only
  42. * accessibility events from the clock application.
  43. * </li>
  44. * <li>
  45. * Providing dynamic, context-dependent feedback - feedback type changes
  46. * depending on the ringer state.</li>
  47. * <li>
  48. * Application specific UI enhancement - application domain knowledge is
  49. * utilized to enhance the provided feedback.
  50. * </li>
  51. * </ol>
  52. *
  53. * @author svetoslavganov@google.com (Svetoslav R. Ganov)
  54. */
  55. public class ClockBackService extends AccessibilityService {
  56. /** Tag for logging from this service */
  57. private static final String LOG_TAG = "ClockBackService";
  58. // fields for configuring how the system handles this accessibility service
  59. /** Minimal timeout between accessibility events we want to receive */
  60. private static final int EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80;
  61. /** Packages we are interested in */
  62. // This works with AlarmClock and Clock whose package name changes in different releases
  63. private static final String[] PACKAGE_NAMES = new String[] {
  64. "com.android.alarmclock", "com.google.android.deskclock", "com.android.deskclock"
  65. };
  66. // message types we are passing around
  67. /** Speak */
  68. private static final int WHAT_SPEAK = 1;
  69. /** Stop speaking */
  70. private static final int WHAT_STOP_SPEAK = 2;
  71. /** Start the TTS service */
  72. private static final int WHAT_START_TTS = 3;
  73. /** Stop the TTS service */
  74. private static final int WHAT_SHUTDOWN_TTS = 4;
  75. // speech related constants
  76. /**
  77. * The queuing mode we are using - interrupt a spoken utterance before
  78. * speaking another one
  79. */
  80. private static final int QUEUING_MODE_INTERRUPT = 2;
  81. /** The empty string constant */
  82. private static final String SPACE = " ";
  83. /**
  84. * The class name of the number picker buttons with no text we want to
  85. * announce in the Clock application.
  86. */
  87. private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK = "android.widget.NumberPickerButton";
  88. /**
  89. * The class name of the number picker buttons with no text we want to
  90. * announce in the AlarmClock application.
  91. */
  92. private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK = "com.android.internal.widget.NumberPickerButton";
  93. /**
  94. * The class name of the edit text box for hours and minutes we want to
  95. * better announce
  96. */
  97. private static final String CLASS_NAME_EDIT_TEXT = "android.widget.EditText";
  98. /**
  99. * Mapping from integer to string resource id where the keys are generated
  100. * from the {@link AccessibilityEvent#getItemCount()} and
  101. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  102. */
  103. private static final SparseArray<Integer> sPositionMappedStringResourceIds = new SparseArray<Integer>();
  104. static {
  105. sPositionMappedStringResourceIds.put(11, R.string.value_plus);
  106. sPositionMappedStringResourceIds.put(114, R.string.value_plus);
  107. sPositionMappedStringResourceIds.put(112, R.string.value_minus);
  108. sPositionMappedStringResourceIds.put(116, R.string.value_minus);
  109. sPositionMappedStringResourceIds.put(111, R.string.value_hours);
  110. sPositionMappedStringResourceIds.put(115, R.string.value_minutes);
  111. }
  112. // auxiliary fields
  113. /**
  114. * Handle to this service to enable inner classes to access the {@link Context}
  115. */
  116. private Context mContext;
  117. /** Reusable instance for building utterances */
  118. private final StringBuilder mUtterance = new StringBuilder();
  119. // feedback providing services
  120. /** The {@link TextToSpeech} used for speaking */
  121. private TextToSpeech mTts;
  122. /** Flag if the infrastructure is initialized */
  123. private boolean isInfrastructureInitialized;
  124. /** {@link Handler} for executing messages on the service main thread */
  125. Handler mHandler = new Handler() {
  126. @Override
  127. public void handleMessage(Message message) {
  128. switch (message.what) {
  129. case WHAT_SPEAK:
  130. String utterance = (String) message.obj;
  131. mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null);
  132. return;
  133. case WHAT_STOP_SPEAK:
  134. mTts.stop();
  135. return;
  136. case WHAT_START_TTS:
  137. mTts = new TextToSpeech(mContext, null);
  138. return;
  139. case WHAT_SHUTDOWN_TTS:
  140. mTts.shutdown();
  141. return;
  142. }
  143. }
  144. };
  145. @Override
  146. public void onServiceConnected() {
  147. if (isInfrastructureInitialized) {
  148. return;
  149. }
  150. mContext = this;
  151. // send a message to start the TTS
  152. mHandler.sendEmptyMessage(WHAT_START_TTS);
  153. setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
  154. // we are in an initialized state now
  155. isInfrastructureInitialized = true;
  156. }
  157. @Override
  158. public boolean onUnbind(Intent intent) {
  159. if (isInfrastructureInitialized) {
  160. // stop the TTS service
  161. mHandler.sendEmptyMessage(WHAT_SHUTDOWN_TTS);
  162. // we are not in an initialized state anymore
  163. isInfrastructureInitialized = false;
  164. }
  165. return false;
  166. }
  167. /**
  168. * Sets the {@link AccessibilityServiceInfo} which informs the system how to
  169. * handle this {@link AccessibilityService}.
  170. *
  171. * @param feedbackType The type of feedback this service will provide. </p>
  172. * Note: The feedbackType parameter is an bitwise or of all
  173. * feedback types this service would like to provide.
  174. */
  175. private void setServiceInfo(int feedbackType) {
  176. AccessibilityServiceInfo info = new AccessibilityServiceInfo();
  177. // we are interested in all types of accessibility events
  178. info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
  179. // we want to provide specific type of feedback
  180. info.feedbackType = feedbackType;
  181. // we want to receive events in a certain interval
  182. info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS;
  183. // we want to receive accessibility events only from certain packages
  184. info.packageNames = PACKAGE_NAMES;
  185. setServiceInfo(info);
  186. }
  187. @Override
  188. public void onAccessibilityEvent(AccessibilityEvent event) {
  189. Log.i(LOG_TAG, event.toString());
  190. mHandler.obtainMessage(WHAT_SPEAK, formatUtterance(event)).sendToTarget();
  191. }
  192. @Override
  193. public void onInterrupt() {
  194. mHandler.obtainMessage(WHAT_STOP_SPEAK);
  195. }
  196. /**
  197. * Formats an utterance from an {@link AccessibilityEvent}.
  198. *
  199. * @param event The event from which to format an utterance.
  200. * @return The formatted utterance.
  201. */
  202. private String formatUtterance(AccessibilityEvent event) {
  203. StringBuilder utterance = mUtterance;
  204. // clear the utterance before appending the formatted text
  205. utterance.delete(0, utterance.length());
  206. List<CharSequence> eventText = event.getText();
  207. // We try to get the event text if such
  208. if (!eventText.isEmpty()) {
  209. for (CharSequence subText : eventText) {
  210. utterance.append(subText);
  211. utterance.append(SPACE);
  212. }
  213. // here we do a bit of enhancement of the UI presentation by using the semantic
  214. // of the event source in the context of the Clock application
  215. if (CLASS_NAME_EDIT_TEXT.equals(event.getClassName())) {
  216. // if the source is an edit text box and we have a mapping based on
  217. // its position in the items of the container parent of the event source
  218. // we append that value as well. We say "XX hours" and "XX minutes".
  219. String resourceValue = getPositionMappedStringResource(event.getItemCount(),
  220. event.getCurrentItemIndex());
  221. if (resourceValue != null) {
  222. utterance.append(resourceValue);
  223. }
  224. }
  225. return utterance.toString();
  226. }
  227. // There is no event text but we try to get the content description which is
  228. // an optional attribute for describing a view (typically used with ImageView)
  229. CharSequence contentDescription = event.getContentDescription();
  230. if (contentDescription != null) {
  231. utterance.append(contentDescription);
  232. return utterance.toString();
  233. }
  234. // No text and content description for the plus and minus buttons, so we lookup
  235. // custom values based on the event's itemCount and currentItemIndex properties.
  236. CharSequence className = event.getClassName();
  237. if (CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK.equals(className)
  238. || CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK.equals(className)) {
  239. String resourceValue = getPositionMappedStringResource(event.getItemCount(),
  240. event.getCurrentItemIndex());
  241. utterance.append(resourceValue);
  242. }
  243. return utterance.toString();
  244. }
  245. /**
  246. * Returns a string resource mapped for a given position based on
  247. * {@link AccessibilityEvent#getItemCount()} and
  248. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  249. *
  250. * @param itemCount The value of {@link AccessibilityEvent#getItemCount()}.
  251. * @param currentItemIndex The value of
  252. * {@link AccessibilityEvent#getCurrentItemIndex()}.
  253. * @return The mapped string if such exists, null otherwise.
  254. */
  255. private String getPositionMappedStringResource(int itemCount, int currentItemIndex) {
  256. int lookupIndex = computeLookupIndex(itemCount, currentItemIndex);
  257. int resourceId = sPositionMappedStringResourceIds.get(lookupIndex);
  258. return getString(resourceId);
  259. }
  260. /**
  261. * Computes an index for looking up the custom text for views with neither
  262. * text not content description. The index is computed based on
  263. * {@link AccessibilityEvent#getItemCount()} and
  264. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  265. *
  266. * @param itemCount The number of all items in the event source.
  267. * @param currentItemIndex The index of the item source of the event.
  268. * @return The lookup index.
  269. */
  270. private int computeLookupIndex(int itemCount, int currentItemIndex) {
  271. int lookupIndex = itemCount;
  272. int divided = currentItemIndex;
  273. while (divided > 0) {
  274. lookupIndex *= 10;
  275. divided /= 10;
  276. }
  277. return (lookupIndex += currentItemIndex);
  278. }
  279. }