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

http://eyes-free.googlecode.com/ · Java · 583 lines · 274 code · 82 blank · 227 comment · 48 complexity · 97fa4b35252559457b16e83463f01d3c 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(AccessibilityEvent.TYPE_VIEW_CLICKED, R.raw.sound1);
  136. sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED, R.raw.sound2);
  137. sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, R.raw.sound2);
  138. sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, R.raw.sound3);
  139. sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound4);
  140. sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound5);
  141. sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound6);
  142. sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound7);
  143. sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound8);
  144. }
  145. // sound pool related member fields
  146. /** Mapping from integers to earcon names - dynamically populated. */
  147. private final SparseArray<String> mEarconNames = new SparseArray<String>();
  148. // auxiliary fields
  149. /**
  150. * Handle to this service to enable inner classes to access the {@link Context}
  151. */
  152. private Context mContext;
  153. /** The feedback this service is currently providing */
  154. private int mProvidedFeedbackType;
  155. /** Reusable instance for building utterances */
  156. private final StringBuilder mUtterance = new StringBuilder();
  157. // feedback providing services
  158. /** The {@link TextToSpeech} used for speaking */
  159. private TextToSpeech mTts;
  160. /** The {@link AudioManager} for detecting ringer state */
  161. private AudioManager mAudioManager;
  162. /** Flag if the infrastructure is initialized */
  163. private boolean isInfrastructureInitialized;
  164. /** {@link Handler} for executing messages on the service main thread */
  165. Handler mHandler = new Handler() {
  166. @Override
  167. public void handleMessage(Message message) {
  168. switch (message.what) {
  169. case WHAT_SPEAK:
  170. String utterance = (String) message.obj;
  171. mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null);
  172. return;
  173. case WHAT_STOP_SPEAK:
  174. mTts.stop();
  175. return;
  176. case WHAT_START_TTS:
  177. mTts = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() {
  178. @Override
  179. public void onInit(int status) {
  180. // register here since to add earcons the TTS must be initialized
  181. // the receiver is called immediately with the current ringer mode
  182. registerBroadCastReceiver();
  183. }
  184. });
  185. return;
  186. case WHAT_SHUTDOWN_TTS:
  187. mTts.shutdown();
  188. return;
  189. case WHAT_PLAY_EARCON:
  190. int resourceId = message.arg1;
  191. playEarcon(resourceId);
  192. return;
  193. case WHAT_STOP_PLAY_EARCON:
  194. mTts.stop();
  195. return;
  196. }
  197. }
  198. };
  199. /**
  200. * {@link BroadcastReceiver} for receiving updates for our context - device
  201. * state
  202. */
  203. private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
  204. @Override
  205. public void onReceive(Context context, Intent intent) {
  206. String action = intent.getAction();
  207. if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) {
  208. int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE,
  209. AudioManager.RINGER_MODE_NORMAL);
  210. configureForRingerMode(ringerMode);
  211. } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
  212. provideScreenStateChangeFeedback(INDEX_SCREEN_ON);
  213. } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
  214. provideScreenStateChangeFeedback(INDEX_SCREEN_OFF);
  215. } else {
  216. Log.w(LOG_TAG, "Registered for but not handling action " + action);
  217. }
  218. }
  219. /**
  220. * Provides feedback to announce the screen state change. Such a change
  221. * is turning the screen on or off.
  222. *
  223. * @param feedbackIndex The index of the feedback in the statically
  224. * mapped feedback resources.
  225. */
  226. private void provideScreenStateChangeFeedback(int feedbackIndex) {
  227. // we take a specific action depending on the feedback we currently provide
  228. switch (mProvidedFeedbackType) {
  229. case AccessibilityServiceInfo.FEEDBACK_SPOKEN:
  230. String utterance = generateScreenOnOrOffUtternace(feedbackIndex);
  231. mHandler.obtainMessage(WHAT_SPEAK, utterance).sendToTarget();
  232. return;
  233. case AccessibilityServiceInfo.FEEDBACK_AUDIBLE:
  234. mHandler.obtainMessage(WHAT_PLAY_EARCON, feedbackIndex, 0).sendToTarget();
  235. return;
  236. default:
  237. throw new IllegalStateException("Unexpected feedback type "
  238. + mProvidedFeedbackType);
  239. }
  240. }
  241. };
  242. @Override
  243. public void onServiceConnected() {
  244. if (isInfrastructureInitialized) {
  245. return;
  246. }
  247. mContext = this;
  248. // send a message to start the TTS
  249. mHandler.sendEmptyMessage(WHAT_START_TTS);
  250. // get the AudioManager and configure according the current ring mode
  251. mAudioManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE);
  252. int ringerMode = mAudioManager.getRingerMode();
  253. configureForRingerMode(ringerMode);
  254. // we are in an initialized state now
  255. isInfrastructureInitialized = true;
  256. }
  257. @Override
  258. public boolean onUnbind(Intent intent) {
  259. if (isInfrastructureInitialized) {
  260. // stop the TTS service
  261. mHandler.sendEmptyMessage(WHAT_SHUTDOWN_TTS);
  262. // unregister the intent broadcast receiver
  263. if (mBroadcastReceiver != null) {
  264. unregisterReceiver(mBroadcastReceiver);
  265. }
  266. // we are not in an initialized state anymore
  267. isInfrastructureInitialized = false;
  268. }
  269. return false;
  270. }
  271. /**
  272. * Registers the phone state observing broadcast receiver.
  273. */
  274. private void registerBroadCastReceiver() {
  275. //Create a filter with the broadcast intents we are interested in
  276. IntentFilter filter = new IntentFilter();
  277. filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
  278. filter.addAction(Intent.ACTION_SCREEN_ON);
  279. filter.addAction(Intent.ACTION_SCREEN_OFF);
  280. // register for broadcasts of interest
  281. registerReceiver(mBroadcastReceiver, filter, null, null);
  282. }
  283. /**
  284. * Generates an utterance for announcing screen on and screen off.
  285. *
  286. * @param feedbackIndex The feedback index for looking up feedback value.
  287. * @return The utterance.
  288. */
  289. private String generateScreenOnOrOffUtternace(int feedbackIndex) {
  290. // get the announce template
  291. int resourceId = (feedbackIndex == INDEX_SCREEN_ON) ? R.string.template_screen_on
  292. : R.string.template_screen_off;
  293. String template = mContext.getString(resourceId);
  294. // format the template with the ringer percentage
  295. int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
  296. int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
  297. int volumePercent = (100 / maxRingerVolume) * currentRingerVolume;
  298. // let us round to five so it sounds better
  299. int adjustment = volumePercent % 10;
  300. if (adjustment < 5) {
  301. volumePercent -= adjustment;
  302. } else if (adjustment > 5) {
  303. volumePercent += (10 - adjustment);
  304. }
  305. return String.format(template, volumePercent);
  306. }
  307. /**
  308. * Configures the service according to a ringer mode. Possible
  309. * configurations:
  310. * </p>
  311. * 1. {@link AudioManager#RINGER_MODE_SILENT}</br>
  312. * Goal: For now same as the case below.</br>
  313. * Approach: For now same as the case below.
  314. * </p>
  315. * 2. {@link AudioManager#RINGER_MODE_VIBRATE}</br>
  316. * Goal: Provide custom audible and default haptic feedback.</p>
  317. * Approach: Take over the audible feedback and provide custom one.</p>
  318. * Take over the spoken feedback but do not provide such.</br>
  319. * Let some other service provide haptic feedback (KickBack).
  320. * </p>
  321. * 3. {@link AudioManager#RINGER_MODE_NORMAL}</p>
  322. * Goal: Provide custom spoken, default audible and default haptic feedback.</br>
  323. * Approach: Take over the spoken feedback and provide custom one.</br>
  324. * Let some other services provide audible feedback (SounBack) and haptic
  325. * feedback (KickBack).
  326. * </p>
  327. * Note: In the above description an assumption is made that all default feedback
  328. * services are enabled. Such services are TalkBack, SoundBack, and KickBack.
  329. * Also the feature of defining a service as the default for a given feedback
  330. * type will be available in Froyo and after. For previous releases the package
  331. * specific accessibility service must be registered first i.e. checked in the
  332. * settings.
  333. *
  334. * @param ringerMode The device ringer mode.
  335. */
  336. private void configureForRingerMode(int ringerMode) {
  337. if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
  338. // for now we handle this case as ringer vibrate
  339. // use only an earcon to announce ringer state change
  340. mHandler.obtainMessage(WHAT_PLAY_EARCON, INDEX_RINGER_SILENT, 0).sendToTarget();
  341. } else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
  342. // when the ringer is vibrating we want to provide only audible
  343. // feedback
  344. mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_AUDIBLE;
  345. // take over the spoken feedback so no spoken feedback is provided
  346. setServiceInfo(AccessibilityServiceInfo.FEEDBACK_AUDIBLE
  347. | AccessibilityServiceInfo.FEEDBACK_SPOKEN);
  348. // use only an earcon to announce ringer state change
  349. mHandler.obtainMessage(WHAT_PLAY_EARCON, INDEX_RINGER_VIBRATE, 0).sendToTarget();
  350. } else if (ringerMode == AudioManager.RINGER_MODE_NORMAL) {
  351. // when the ringer is ringing we want to provide spoken feedback
  352. // overriding the default spoken feedback
  353. mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
  354. setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
  355. // use only an earcon to announce ringer state change
  356. mHandler.obtainMessage(WHAT_PLAY_EARCON, INDEX_RINGER_NORMAL, 0).sendToTarget();
  357. }
  358. }
  359. /**
  360. * Sets the {@link AccessibilityServiceInfo} which informs the system how to
  361. * handle this {@link AccessibilityService}.
  362. *
  363. * @param feedbackType The type of feedback this service will provide. </p>
  364. * Note: The feedbackType parameter is an bitwise or of all
  365. * feedback types this service would like to provide.
  366. */
  367. private void setServiceInfo(int feedbackType) {
  368. AccessibilityServiceInfo info = new AccessibilityServiceInfo();
  369. // we are interested in all types of accessibility events
  370. info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
  371. // we want to provide specific type of feedback
  372. info.feedbackType = feedbackType;
  373. // we want to receive events in a certain interval
  374. info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS;
  375. // we want to receive accessibility events only from certain packages
  376. info.packageNames = PACKAGE_NAMES;
  377. setServiceInfo(info);
  378. }
  379. @Override
  380. public void onAccessibilityEvent(AccessibilityEvent event) {
  381. Log.i(LOG_TAG, mProvidedFeedbackType + " " + event.toString());
  382. // here we act according to the feedback type we are currently providing
  383. if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) {
  384. mHandler.obtainMessage(WHAT_SPEAK, formatUtterance(event)).sendToTarget();
  385. } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) {
  386. mHandler.obtainMessage(WHAT_PLAY_EARCON, event.getEventType(), 0).sendToTarget();
  387. } else {
  388. throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType);
  389. }
  390. }
  391. @Override
  392. public void onInterrupt() {
  393. // here we act according to the feedback type we are currently providing
  394. if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) {
  395. mHandler.obtainMessage(WHAT_STOP_SPEAK);
  396. } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) {
  397. mHandler.obtainMessage(WHAT_STOP_PLAY_EARCON);
  398. } else {
  399. throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType);
  400. }
  401. }
  402. /**
  403. * Formats an utterance from an {@link AccessibilityEvent}.
  404. *
  405. * @param event The event from which to format an utterance.
  406. * @return The formatted utterance.
  407. */
  408. private String formatUtterance(AccessibilityEvent event) {
  409. StringBuilder utterance = mUtterance;
  410. // clear the utterance before appending the formatted text
  411. utterance.delete(0, utterance.length());
  412. List<CharSequence> eventText = event.getText();
  413. // We try to get the event text if such
  414. if (!eventText.isEmpty()) {
  415. for (CharSequence subText : eventText) {
  416. utterance.append(subText);
  417. utterance.append(SPACE);
  418. }
  419. // here we do a bit of enhancement of the UI presentation by using the semantic
  420. // of the event source in the context of the Clock application
  421. if (CLASS_NAME_EDIT_TEXT.equals(event.getClassName())) {
  422. // if the source is an edit text box and we have a mapping based on
  423. // its position in the items of the container parent of the event source
  424. // we append that value as well. We say "XX hours" and "XX minutes".
  425. String resourceValue = getPositionMappedStringResource(event.getItemCount(),
  426. event.getCurrentItemIndex());
  427. if (resourceValue != null) {
  428. utterance.append(resourceValue);
  429. }
  430. }
  431. return utterance.toString();
  432. }
  433. // There is no event text but we try to get the content description which is
  434. // an optional attribute for describing a view (typically used with ImageView)
  435. CharSequence contentDescription = event.getContentDescription();
  436. if (contentDescription != null) {
  437. utterance.append(contentDescription);
  438. return utterance.toString();
  439. }
  440. // No text and content description for the plus and minus buttons, so we lookup
  441. // custom values based on the event's itemCount and currentItemIndex properties.
  442. CharSequence className = event.getClassName();
  443. if (CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK.equals(className)
  444. || CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK.equals(className)) {
  445. String resourceValue = getPositionMappedStringResource(event.getItemCount(),
  446. event.getCurrentItemIndex());
  447. utterance.append(resourceValue);
  448. }
  449. return utterance.toString();
  450. }
  451. /**
  452. * Returns a string resource mapped for a given position based on
  453. * {@link AccessibilityEvent#getItemCount()} and
  454. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  455. *
  456. * @param itemCount The value of {@link AccessibilityEvent#getItemCount()}.
  457. * @param currentItemIndex The value of
  458. * {@link AccessibilityEvent#getCurrentItemIndex()}.
  459. * @return The mapped string if such exists, null otherwise.
  460. */
  461. private String getPositionMappedStringResource(int itemCount, int currentItemIndex) {
  462. int lookupIndex = computeLookupIndex(itemCount, currentItemIndex);
  463. int resourceId = sPositionMappedStringResourceIds.get(lookupIndex);
  464. return getString(resourceId);
  465. }
  466. /**
  467. * Computes an index for looking up the custom text for views with neither
  468. * text not content description. The index is computed based on
  469. * {@link AccessibilityEvent#getItemCount()} and
  470. * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
  471. *
  472. * @param itemCount The number of all items in the event source.
  473. * @param currentItemIndex The index of the item source of the event.
  474. * @return The lookup index.
  475. */
  476. private int computeLookupIndex(int itemCount, int currentItemIndex) {
  477. int lookupIndex = itemCount;
  478. int divided = currentItemIndex;
  479. while (divided > 0) {
  480. lookupIndex *= 10;
  481. divided /= 10;
  482. }
  483. return (lookupIndex += currentItemIndex);
  484. }
  485. /**
  486. * Plays an earcon given its id.
  487. *
  488. * @param earconId The id of the earcon to be played.
  489. */
  490. private void playEarcon(int earconId) {
  491. String earconName = mEarconNames.get(earconId);
  492. if (earconName == null) {
  493. // we do not know the sound id, hence we need to load the sound
  494. int resourceId = sSoundsResourceIds.get(earconId);
  495. earconName = "[" + earconId + "]";
  496. mTts.addEarcon(earconName, getPackageName(), resourceId);
  497. mEarconNames.put(earconId, earconName);
  498. }
  499. mTts.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null);
  500. }
  501. }