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

http://eyes-free.googlecode.com/ · Java · 509 lines · 239 code · 77 blank · 193 comment · 35 complexity · 2520d45e14534518357b5a72a4ea295d 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. /** Play an earcon */
  80. private static final int WHAT_PLAY_EARCON = 5;
  81. /** Stop playing an earcon */
  82. private static final int WHAT_STOP_PLAY_EARCON = 6;
  83. //screen state broadcast related constants
  84. /** Feedback mapping index used as a key for the screen on broadcast */
  85. private static final int INDEX_SCREEN_ON = 0x00000100;
  86. /** Feedback mapping index used as a key for the screen off broadcast */
  87. private static final int INDEX_SCREEN_OFF = 0x00000200;
  88. // ringer mode change related constants
  89. /** Feedback mapping index used as a key for normal ringer mode */
  90. private static final int INDEX_RINGER_NORMAL = 0x00000400;
  91. /** Feedback mapping index used as a key for vibration ringer mode */
  92. private static final int INDEX_RINGER_VIBRATE = 0x00000800;
  93. /** Feedback mapping index used as a key for silent ringer mode */
  94. private static final int INDEX_RINGER_SILENT = 0x00001000;
  95. // speech related constants
  96. /**
  97. * The queuing mode we are using - interrupt a spoken utterance before
  98. * speaking another one
  99. */
  100. private static final int QUEUING_MODE_INTERRUPT = 2;
  101. /** The empty string constant */
  102. private static final String SPACE = " ";
  103. /**
  104. * The class name of the number picker buttons with no text we want to
  105. * announce in the Clock application.
  106. */
  107. private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK = "android.widget.NumberPickerButton";
  108. /**
  109. * The class name of the number picker buttons with no text we want to
  110. * announce in the AlarmClock application.
  111. */
  112. private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK = "com.android.internal.widget.NumberPickerButton";
  113. /**
  114. * The class name of the edit text box for hours and minutes we want to
  115. * better announce
  116. */
  117. private static final String CLASS_NAME_EDIT_TEXT = "android.widget.EditText";
  118. /**
  119. * Mapping from integer to string resource id where the keys are generated
  120. * from the {@link AccessibilityEvent#getItemCount()} and
  121. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  122. */
  123. private static final SparseArray<Integer> sPositionMappedStringResourceIds = new SparseArray<Integer>();
  124. static {
  125. sPositionMappedStringResourceIds.put(11, R.string.value_plus);
  126. sPositionMappedStringResourceIds.put(114, R.string.value_plus);
  127. sPositionMappedStringResourceIds.put(112, R.string.value_minus);
  128. sPositionMappedStringResourceIds.put(116, R.string.value_minus);
  129. sPositionMappedStringResourceIds.put(111, R.string.value_hours);
  130. sPositionMappedStringResourceIds.put(115, R.string.value_minutes);
  131. }
  132. /** Mapping from integers to raw sound resource ids */
  133. private static SparseArray<Integer> sSoundsResourceIds = new SparseArray<Integer>();
  134. static {
  135. sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound6);
  136. sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound7);
  137. sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound8);
  138. }
  139. // sound pool related member fields
  140. /** Mapping from integers to earcon names - dynamically populated. */
  141. private final SparseArray<String> mEarconNames = new SparseArray<String>();
  142. // auxiliary fields
  143. /**
  144. * Handle to this service to enable inner classes to access the {@link Context}
  145. */
  146. private Context mContext;
  147. /** Reusable instance for building utterances */
  148. private final StringBuilder mUtterance = new StringBuilder();
  149. // feedback providing services
  150. /** The {@link TextToSpeech} used for speaking */
  151. private TextToSpeech mTts;
  152. /** The {@link AudioManager} for detecting ringer state */
  153. private AudioManager mAudioManager;
  154. /** Flag if the infrastructure is initialized */
  155. private boolean isInfrastructureInitialized;
  156. /** {@link Handler} for executing messages on the service main thread */
  157. Handler mHandler = new Handler() {
  158. @Override
  159. public void handleMessage(Message message) {
  160. switch (message.what) {
  161. case WHAT_SPEAK:
  162. String utterance = (String) message.obj;
  163. mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null);
  164. return;
  165. case WHAT_STOP_SPEAK:
  166. mTts.stop();
  167. return;
  168. case WHAT_START_TTS:
  169. mTts = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() {
  170. @Override
  171. public void onInit(int status) {
  172. // register here since to add earcons the TTS must be initialized
  173. // the receiver is called immediately with the current ringer mode
  174. registerBroadCastReceiver();
  175. }
  176. });
  177. return;
  178. case WHAT_SHUTDOWN_TTS:
  179. mTts.shutdown();
  180. return;
  181. case WHAT_PLAY_EARCON:
  182. int resourceId = message.arg1;
  183. playEarcon(resourceId);
  184. return;
  185. case WHAT_STOP_PLAY_EARCON:
  186. mTts.stop();
  187. return;
  188. }
  189. }
  190. };
  191. /**
  192. * {@link BroadcastReceiver} for receiving updates for our context - device
  193. * state
  194. */
  195. private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
  196. @Override
  197. public void onReceive(Context context, Intent intent) {
  198. String action = intent.getAction();
  199. if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) {
  200. int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE,
  201. AudioManager.RINGER_MODE_NORMAL);
  202. configureForRingerMode(ringerMode);
  203. } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
  204. provideScreenStateChangeFeedback(INDEX_SCREEN_ON);
  205. } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
  206. provideScreenStateChangeFeedback(INDEX_SCREEN_OFF);
  207. } else {
  208. Log.w(LOG_TAG, "Registered for but not handling action " + action);
  209. }
  210. }
  211. /**
  212. * Provides feedback to announce the screen state change. Such a change
  213. * is turning the screen on or off.
  214. *
  215. * @param feedbackIndex The index of the feedback in the statically
  216. * mapped feedback resources.
  217. */
  218. private void provideScreenStateChangeFeedback(int feedbackIndex) {
  219. String utterance = generateScreenOnOrOffUtternace(feedbackIndex);
  220. mHandler.obtainMessage(WHAT_SPEAK, utterance).sendToTarget();
  221. }
  222. };
  223. @Override
  224. public void onServiceConnected() {
  225. if (isInfrastructureInitialized) {
  226. return;
  227. }
  228. mContext = this;
  229. // send a message to start the TTS
  230. mHandler.sendEmptyMessage(WHAT_START_TTS);
  231. // get the AudioManager and configure according the current ring mode
  232. mAudioManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE);
  233. setServiceInfo(AccessibilityServiceInfo.FEEDBACK_AUDIBLE);
  234. // we are in an initialized state now
  235. isInfrastructureInitialized = true;
  236. }
  237. @Override
  238. public boolean onUnbind(Intent intent) {
  239. if (isInfrastructureInitialized) {
  240. // stop the TTS service
  241. mHandler.sendEmptyMessage(WHAT_SHUTDOWN_TTS);
  242. // unregister the intent broadcast receiver
  243. if (mBroadcastReceiver != null) {
  244. unregisterReceiver(mBroadcastReceiver);
  245. }
  246. // we are not in an initialized state anymore
  247. isInfrastructureInitialized = false;
  248. }
  249. return false;
  250. }
  251. /**
  252. * Registers the phone state observing broadcast receiver.
  253. */
  254. private void registerBroadCastReceiver() {
  255. //Create a filter with the broadcast intents we are interested in
  256. IntentFilter filter = new IntentFilter();
  257. filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
  258. filter.addAction(Intent.ACTION_SCREEN_ON);
  259. filter.addAction(Intent.ACTION_SCREEN_OFF);
  260. // register for broadcasts of interest
  261. registerReceiver(mBroadcastReceiver, filter, null, null);
  262. }
  263. /**
  264. * Generates an utterance for announcing screen on and screen off.
  265. *
  266. * @param feedbackIndex The feedback index for looking up feedback value.
  267. * @return The utterance.
  268. */
  269. private String generateScreenOnOrOffUtternace(int feedbackIndex) {
  270. // get the announce template
  271. int resourceId = (feedbackIndex == INDEX_SCREEN_ON) ? R.string.template_screen_on
  272. : R.string.template_screen_off;
  273. String template = mContext.getString(resourceId);
  274. // format the template with the ringer percentage
  275. int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
  276. int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
  277. int volumePercent = (100 / maxRingerVolume) * currentRingerVolume;
  278. // let us round to five so it sounds better
  279. int adjustment = volumePercent % 10;
  280. if (adjustment < 5) {
  281. volumePercent -= adjustment;
  282. } else if (adjustment > 5) {
  283. volumePercent += (10 - adjustment);
  284. }
  285. return String.format(template, volumePercent);
  286. }
  287. /**
  288. * Configures the service according to a ringer mode.
  289. *
  290. * @param ringerMode The device ringer mode.
  291. */
  292. private void configureForRingerMode(int ringerMode) {
  293. if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
  294. // use only an earcon to announce ringer state change
  295. mHandler.obtainMessage(WHAT_PLAY_EARCON, INDEX_RINGER_SILENT, 0).sendToTarget();
  296. } else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
  297. // use only an earcon to announce ringer state change
  298. mHandler.obtainMessage(WHAT_PLAY_EARCON, INDEX_RINGER_VIBRATE, 0).sendToTarget();
  299. } else if (ringerMode == AudioManager.RINGER_MODE_NORMAL) {
  300. // use only an earcon to announce ringer state change
  301. mHandler.obtainMessage(WHAT_PLAY_EARCON, INDEX_RINGER_NORMAL, 0).sendToTarget();
  302. }
  303. }
  304. /**
  305. * Sets the {@link AccessibilityServiceInfo} which informs the system how to
  306. * handle this {@link AccessibilityService}.
  307. *
  308. * @param feedbackType The type of feedback this service will provide. </p>
  309. * Note: The feedbackType parameter is an bitwise or of all
  310. * feedback types this service would like to provide.
  311. */
  312. private void setServiceInfo(int feedbackType) {
  313. AccessibilityServiceInfo info = new AccessibilityServiceInfo();
  314. // we are interested in all types of accessibility events
  315. info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
  316. // we want to provide specific type of feedback
  317. info.feedbackType = feedbackType;
  318. // we want to receive events in a certain interval
  319. info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS;
  320. // we want to receive accessibility events only from certain packages
  321. info.packageNames = PACKAGE_NAMES;
  322. setServiceInfo(info);
  323. }
  324. @Override
  325. public void onAccessibilityEvent(AccessibilityEvent event) {
  326. Log.i(LOG_TAG, event.toString());
  327. mHandler.obtainMessage(WHAT_SPEAK, formatUtterance(event)).sendToTarget();
  328. }
  329. @Override
  330. public void onInterrupt() {
  331. mHandler.obtainMessage(WHAT_STOP_SPEAK);
  332. }
  333. /**
  334. * Formats an utterance from an {@link AccessibilityEvent}.
  335. *
  336. * @param event The event from which to format an utterance.
  337. * @return The formatted utterance.
  338. */
  339. private String formatUtterance(AccessibilityEvent event) {
  340. StringBuilder utterance = mUtterance;
  341. // clear the utterance before appending the formatted text
  342. utterance.delete(0, utterance.length());
  343. List<CharSequence> eventText = event.getText();
  344. // We try to get the event text if such
  345. if (!eventText.isEmpty()) {
  346. for (CharSequence subText : eventText) {
  347. utterance.append(subText);
  348. utterance.append(SPACE);
  349. }
  350. // here we do a bit of enhancement of the UI presentation by using the semantic
  351. // of the event source in the context of the Clock application
  352. if (CLASS_NAME_EDIT_TEXT.equals(event.getClassName())) {
  353. // if the source is an edit text box and we have a mapping based on
  354. // its position in the items of the container parent of the event source
  355. // we append that value as well. We say "XX hours" and "XX minutes".
  356. String resourceValue = getPositionMappedStringResource(event.getItemCount(),
  357. event.getCurrentItemIndex());
  358. if (resourceValue != null) {
  359. utterance.append(resourceValue);
  360. }
  361. }
  362. return utterance.toString();
  363. }
  364. // There is no event text but we try to get the content description which is
  365. // an optional attribute for describing a view (typically used with ImageView)
  366. CharSequence contentDescription = event.getContentDescription();
  367. if (contentDescription != null) {
  368. utterance.append(contentDescription);
  369. return utterance.toString();
  370. }
  371. // No text and content description for the plus and minus buttons, so we lookup
  372. // custom values based on the event's itemCount and currentItemIndex properties.
  373. CharSequence className = event.getClassName();
  374. if (CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK.equals(className)
  375. || CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK.equals(className)) {
  376. String resourceValue = getPositionMappedStringResource(event.getItemCount(),
  377. event.getCurrentItemIndex());
  378. utterance.append(resourceValue);
  379. }
  380. return utterance.toString();
  381. }
  382. /**
  383. * Returns a string resource mapped for a given position based on
  384. * {@link AccessibilityEvent#getItemCount()} and
  385. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  386. *
  387. * @param itemCount The value of {@link AccessibilityEvent#getItemCount()}.
  388. * @param currentItemIndex The value of
  389. * {@link AccessibilityEvent#getCurrentItemIndex()}.
  390. * @return The mapped string if such exists, null otherwise.
  391. */
  392. private String getPositionMappedStringResource(int itemCount, int currentItemIndex) {
  393. int lookupIndex = computeLookupIndex(itemCount, currentItemIndex);
  394. int resourceId = sPositionMappedStringResourceIds.get(lookupIndex);
  395. return getString(resourceId);
  396. }
  397. /**
  398. * Computes an index for looking up the custom text for views with neither
  399. * text not content description. The index is computed based on
  400. * {@link AccessibilityEvent#getItemCount()} and
  401. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  402. *
  403. * @param itemCount The number of all items in the event source.
  404. * @param currentItemIndex The index of the item source of the event.
  405. * @return The lookup index.
  406. */
  407. private int computeLookupIndex(int itemCount, int currentItemIndex) {
  408. int lookupIndex = itemCount;
  409. int divided = currentItemIndex;
  410. while (divided > 0) {
  411. lookupIndex *= 10;
  412. divided /= 10;
  413. }
  414. return (lookupIndex += currentItemIndex);
  415. }
  416. /**
  417. * Plays an earcon given its id.
  418. *
  419. * @param earconId The id of the earcon to be played.
  420. */
  421. private void playEarcon(int earconId) {
  422. String earconName = mEarconNames.get(earconId);
  423. if (earconName == null) {
  424. // we do not know the sound id, hence we need to load the sound
  425. int resourceId = sSoundsResourceIds.get(earconId);
  426. earconName = "[" + earconId + "]";
  427. mTts.addEarcon(earconName, getPackageName(), resourceId);
  428. mEarconNames.put(earconId, earconName);
  429. }
  430. mTts.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null);
  431. }
  432. }