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

http://eyes-free.googlecode.com/ · Java · 431 lines · 196 code · 65 blank · 170 comment · 23 complexity · 1c266b8ef01be4b87ec58ec7f11922c9 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.speech.tts.TextToSpeech;
  28. import android.util.Log;
  29. import android.util.SparseArray;
  30. import android.view.accessibility.AccessibilityEvent;
  31. import java.util.List;
  32. /**
  33. * This class is an {@link AccessibilityService} that provides custom feedback
  34. * for the Clock application that comes by default with Android devices. It
  35. * demonstrates the following key features of the Android accessibility APIs:
  36. * <ol>
  37. * <li>
  38. * Simple demonstration of how to use the accessibility APIs.
  39. * </li>
  40. * <li>
  41. * Hands-on example of various ways to utilize the accessibility API for
  42. * providing alternative and complementary feedback.
  43. * </li>
  44. * <li>
  45. * Providing application specific feedback - the service handles only
  46. * accessibility events from the clock application.
  47. * </li>
  48. * <li>
  49. * Providing dynamic, context-dependent feedback - feedback type changes
  50. * depending on the ringer state.</li>
  51. * <li>
  52. * Application specific UI enhancement - application domain knowledge is
  53. * utilized to enhance the provided feedback.
  54. * </li>
  55. * </ol>
  56. *
  57. * @author svetoslavganov@google.com (Svetoslav R. Ganov)
  58. */
  59. public class ClockBackService extends AccessibilityService {
  60. /** Tag for logging from this service */
  61. private static final String LOG_TAG = "ClockBackService";
  62. // fields for configuring how the system handles this accessibility service
  63. /** Minimal timeout between accessibility events we want to receive */
  64. private static final int EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80;
  65. /** Packages we are interested in */
  66. // This works with AlarmClock and Clock whose package name changes in different releases
  67. private static final String[] PACKAGE_NAMES = new String[] {
  68. "com.android.alarmclock", "com.google.android.deskclock", "com.android.deskclock"
  69. };
  70. // message types we are passing around
  71. /** Speak */
  72. private static final int WHAT_SPEAK = 1;
  73. /** Stop speaking */
  74. private static final int WHAT_STOP_SPEAK = 2;
  75. /** Start the TTS service */
  76. private static final int WHAT_START_TTS = 3;
  77. /** Stop the TTS service */
  78. private static final int WHAT_SHUTDOWN_TTS = 4;
  79. //screen state broadcast related constants
  80. /** Feedback mapping index used as a key for the screen on broadcast */
  81. private static final int INDEX_SCREEN_ON = 0x00000100;
  82. /** Feedback mapping index used as a key for the screen off broadcast */
  83. private static final int INDEX_SCREEN_OFF = 0x00000200;
  84. // speech related constants
  85. /**
  86. * The queuing mode we are using - interrupt a spoken utterance before
  87. * speaking another one
  88. */
  89. private static final int QUEUING_MODE_INTERRUPT = 2;
  90. /** The empty string constant */
  91. private static final String SPACE = " ";
  92. /**
  93. * The class name of the number picker buttons with no text we want to
  94. * announce in the Clock application.
  95. */
  96. private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK = "android.widget.NumberPickerButton";
  97. /**
  98. * The class name of the number picker buttons with no text we want to
  99. * announce in the AlarmClock application.
  100. */
  101. private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK = "com.android.internal.widget.NumberPickerButton";
  102. /**
  103. * The class name of the edit text box for hours and minutes we want to
  104. * better announce
  105. */
  106. private static final String CLASS_NAME_EDIT_TEXT = "android.widget.EditText";
  107. /**
  108. * Mapping from integer to string resource id where the keys are generated
  109. * from the {@link AccessibilityEvent#getItemCount()} and
  110. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  111. */
  112. private static final SparseArray<Integer> sPositionMappedStringResourceIds = new SparseArray<Integer>();
  113. static {
  114. sPositionMappedStringResourceIds.put(11, R.string.value_plus);
  115. sPositionMappedStringResourceIds.put(114, R.string.value_plus);
  116. sPositionMappedStringResourceIds.put(112, R.string.value_minus);
  117. sPositionMappedStringResourceIds.put(116, R.string.value_minus);
  118. sPositionMappedStringResourceIds.put(111, R.string.value_hours);
  119. sPositionMappedStringResourceIds.put(115, R.string.value_minutes);
  120. }
  121. // auxiliary fields
  122. /**
  123. * Handle to this service to enable inner classes to access the {@link Context}
  124. */
  125. private Context mContext;
  126. /** Reusable instance for building utterances */
  127. private final StringBuilder mUtterance = new StringBuilder();
  128. // feedback providing services
  129. /** The {@link TextToSpeech} used for speaking */
  130. private TextToSpeech mTts;
  131. /** The {@link AudioManager} for detecting ringer state */
  132. private AudioManager mAudioManager;
  133. /** Flag if the infrastructure is initialized */
  134. private boolean isInfrastructureInitialized;
  135. /** {@link Handler} for executing messages on the service main thread */
  136. Handler mHandler = new Handler() {
  137. @Override
  138. public void handleMessage(Message message) {
  139. switch (message.what) {
  140. case WHAT_SPEAK:
  141. String utterance = (String) message.obj;
  142. mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null);
  143. return;
  144. case WHAT_STOP_SPEAK:
  145. mTts.stop();
  146. return;
  147. case WHAT_START_TTS:
  148. mTts = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() {
  149. @Override
  150. public void onInit(int status) {
  151. // register here since to add earcons the TTS must be initialized
  152. // the receiver is called immediately with the current ringer mode
  153. registerBroadCastReceiver();
  154. }
  155. });
  156. return;
  157. case WHAT_SHUTDOWN_TTS:
  158. mTts.shutdown();
  159. return;
  160. }
  161. }
  162. };
  163. /**
  164. * {@link BroadcastReceiver} for receiving updates for our context - device
  165. * state
  166. */
  167. private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
  168. @Override
  169. public void onReceive(Context context, Intent intent) {
  170. String action = intent.getAction();
  171. if (Intent.ACTION_SCREEN_ON.equals(action)) {
  172. provideScreenStateChangeFeedback(INDEX_SCREEN_ON);
  173. } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
  174. provideScreenStateChangeFeedback(INDEX_SCREEN_OFF);
  175. } else {
  176. Log.w(LOG_TAG, "Registered for but not handling action " + action);
  177. }
  178. }
  179. /**
  180. * Provides feedback to announce the screen state change. Such a change
  181. * is turning the screen on or off.
  182. *
  183. * @param feedbackIndex The index of the feedback in the statically
  184. * mapped feedback resources.
  185. */
  186. private void provideScreenStateChangeFeedback(int feedbackIndex) {
  187. String utterance = generateScreenOnOrOffUtternace(feedbackIndex);
  188. mHandler.obtainMessage(WHAT_SPEAK, utterance).sendToTarget();
  189. }
  190. };
  191. @Override
  192. public void onServiceConnected() {
  193. if (isInfrastructureInitialized) {
  194. return;
  195. }
  196. mContext = this;
  197. // send a message to start the TTS
  198. mHandler.sendEmptyMessage(WHAT_START_TTS);
  199. // get the AudioManager and configure according the current ring mode
  200. mAudioManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE);
  201. setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
  202. // we are in an initialized state now
  203. isInfrastructureInitialized = true;
  204. }
  205. @Override
  206. public boolean onUnbind(Intent intent) {
  207. if (isInfrastructureInitialized) {
  208. // stop the TTS service
  209. mHandler.sendEmptyMessage(WHAT_SHUTDOWN_TTS);
  210. // unregister the intent broadcast receiver
  211. if (mBroadcastReceiver != null) {
  212. unregisterReceiver(mBroadcastReceiver);
  213. }
  214. // we are not in an initialized state anymore
  215. isInfrastructureInitialized = false;
  216. }
  217. return false;
  218. }
  219. /**
  220. * Registers the phone state observing broadcast receiver.
  221. */
  222. private void registerBroadCastReceiver() {
  223. //Create a filter with the broadcast intents we are interested in
  224. IntentFilter filter = new IntentFilter();
  225. filter.addAction(Intent.ACTION_SCREEN_ON);
  226. filter.addAction(Intent.ACTION_SCREEN_OFF);
  227. // register for broadcasts of interest
  228. registerReceiver(mBroadcastReceiver, filter, null, null);
  229. }
  230. /**
  231. * Generates an utterance for announcing screen on and screen off.
  232. *
  233. * @param feedbackIndex The feedback index for looking up feedback value.
  234. * @return The utterance.
  235. */
  236. private String generateScreenOnOrOffUtternace(int feedbackIndex) {
  237. // get the announce template
  238. int resourceId = (feedbackIndex == INDEX_SCREEN_ON) ? R.string.template_screen_on
  239. : R.string.template_screen_off;
  240. String template = mContext.getString(resourceId);
  241. // format the template with the ringer percentage
  242. int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
  243. int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
  244. int volumePercent = (100 / maxRingerVolume) * currentRingerVolume;
  245. // let us round to five so it sounds better
  246. int adjustment = volumePercent % 10;
  247. if (adjustment < 5) {
  248. volumePercent -= adjustment;
  249. } else if (adjustment > 5) {
  250. volumePercent += (10 - adjustment);
  251. }
  252. return String.format(template, volumePercent);
  253. }
  254. /**
  255. * Sets the {@link AccessibilityServiceInfo} which informs the system how to
  256. * handle this {@link AccessibilityService}.
  257. *
  258. * @param feedbackType The type of feedback this service will provide. </p>
  259. * Note: The feedbackType parameter is an bitwise or of all
  260. * feedback types this service would like to provide.
  261. */
  262. private void setServiceInfo(int feedbackType) {
  263. AccessibilityServiceInfo info = new AccessibilityServiceInfo();
  264. // we are interested in all types of accessibility events
  265. info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
  266. // we want to provide specific type of feedback
  267. info.feedbackType = feedbackType;
  268. // we want to receive events in a certain interval
  269. info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS;
  270. // we want to receive accessibility events only from certain packages
  271. info.packageNames = PACKAGE_NAMES;
  272. setServiceInfo(info);
  273. }
  274. @Override
  275. public void onAccessibilityEvent(AccessibilityEvent event) {
  276. Log.i(LOG_TAG, event.toString());
  277. mHandler.obtainMessage(WHAT_SPEAK, formatUtterance(event)).sendToTarget();
  278. }
  279. @Override
  280. public void onInterrupt() {
  281. mHandler.obtainMessage(WHAT_STOP_SPEAK);
  282. }
  283. /**
  284. * Formats an utterance from an {@link AccessibilityEvent}.
  285. *
  286. * @param event The event from which to format an utterance.
  287. * @return The formatted utterance.
  288. */
  289. private String formatUtterance(AccessibilityEvent event) {
  290. StringBuilder utterance = mUtterance;
  291. // clear the utterance before appending the formatted text
  292. utterance.delete(0, utterance.length());
  293. List<CharSequence> eventText = event.getText();
  294. // We try to get the event text if such
  295. if (!eventText.isEmpty()) {
  296. for (CharSequence subText : eventText) {
  297. utterance.append(subText);
  298. utterance.append(SPACE);
  299. }
  300. // here we do a bit of enhancement of the UI presentation by using the semantic
  301. // of the event source in the context of the Clock application
  302. if (CLASS_NAME_EDIT_TEXT.equals(event.getClassName())) {
  303. // if the source is an edit text box and we have a mapping based on
  304. // its position in the items of the container parent of the event source
  305. // we append that value as well. We say "XX hours" and "XX minutes".
  306. String resourceValue = getPositionMappedStringResource(event.getItemCount(),
  307. event.getCurrentItemIndex());
  308. if (resourceValue != null) {
  309. utterance.append(resourceValue);
  310. }
  311. }
  312. return utterance.toString();
  313. }
  314. // There is no event text but we try to get the content description which is
  315. // an optional attribute for describing a view (typically used with ImageView)
  316. CharSequence contentDescription = event.getContentDescription();
  317. if (contentDescription != null) {
  318. utterance.append(contentDescription);
  319. return utterance.toString();
  320. }
  321. // No text and content description for the plus and minus buttons, so we lookup
  322. // custom values based on the event's itemCount and currentItemIndex properties.
  323. CharSequence className = event.getClassName();
  324. if (CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK.equals(className)
  325. || CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK.equals(className)) {
  326. String resourceValue = getPositionMappedStringResource(event.getItemCount(),
  327. event.getCurrentItemIndex());
  328. utterance.append(resourceValue);
  329. }
  330. return utterance.toString();
  331. }
  332. /**
  333. * Returns a string resource mapped for a given position based on
  334. * {@link AccessibilityEvent#getItemCount()} and
  335. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  336. *
  337. * @param itemCount The value of {@link AccessibilityEvent#getItemCount()}.
  338. * @param currentItemIndex The value of
  339. * {@link AccessibilityEvent#getCurrentItemIndex()}.
  340. * @return The mapped string if such exists, null otherwise.
  341. */
  342. private String getPositionMappedStringResource(int itemCount, int currentItemIndex) {
  343. int lookupIndex = computeLookupIndex(itemCount, currentItemIndex);
  344. int resourceId = sPositionMappedStringResourceIds.get(lookupIndex);
  345. return getString(resourceId);
  346. }
  347. /**
  348. * Computes an index for looking up the custom text for views with neither
  349. * text not content description. The index is computed based on
  350. * {@link AccessibilityEvent#getItemCount()} and
  351. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  352. *
  353. * @param itemCount The number of all items in the event source.
  354. * @param currentItemIndex The index of the item source of the event.
  355. * @return The lookup index.
  356. */
  357. private int computeLookupIndex(int itemCount, int currentItemIndex) {
  358. int lookupIndex = itemCount;
  359. int divided = currentItemIndex;
  360. while (divided > 0) {
  361. lookupIndex *= 10;
  362. divided /= 10;
  363. }
  364. return (lookupIndex += currentItemIndex);
  365. }
  366. }