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 17 package com.example.android.apis.accessibility; 18 19 import com.example.android.apis.R; 20 21 import android.accessibilityservice.AccessibilityService; 22 import android.accessibilityservice.AccessibilityServiceInfo; 23 import android.app.Service; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.media.AudioManager; 29 import android.os.Handler; 30 import android.os.Message; 31 import android.os.Vibrator; 32 import android.speech.tts.TextToSpeech; 33 import android.util.Log; 34 import android.util.SparseArray; 35 import android.view.accessibility.AccessibilityEvent; 36 37 import java.util.List; 38 39 /** 40 * This class is an {@link AccessibilityService} that provides custom feedback 41 * for the Clock application that comes by default with Android devices. It 42 * demonstrates the following key features of the Android accessibility APIs: 43 * <ol> 44 * <li> 45 * Simple demonstration of how to use the accessibility APIs. 46 * </li> 47 * <li> 48 * Hands-on example of various ways to utilize the accessibility API for 49 * providing alternative and complementary feedback. 50 * </li> 51 * <li> 52 * Providing application specific feedback — the service handles only 53 * accessibility events from the clock application. 54 * </li> 55 * <li> 56 * Providing dynamic, context-dependent feedback — feedback type changes 57 * depending on the ringer state. 58 * </li> 59 * </ol> 60 */ 61 public class ClockBackService extends AccessibilityService { 62 63 /** Tag for logging from this service. */ 64 private static final String LOG_TAG = "ClockBackService"; 65 66 // Fields for configuring how the system handles this accessibility service. 67 68 /** Minimal timeout between accessibility events we want to receive. */ 69 private static final int EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80; 70 71 /** Packages we are interested in. 72 * <p> 73 * <strong> 74 * Note: This code sample will work only on devices shipped with the 75 * default Clock application. 76 * </strong> 77 * </p> 78 */ 79 // This works with AlarmClock and Clock whose package name changes in different releases 80 private static final String[] PACKAGE_NAMES = new String[] { 81 "com.android.alarmclock", "com.google.android.deskclock", "com.android.deskclock" 82 }; 83 84 // Message types we are passing around. 85 86 /** Speak. */ 87 private static final int MESSAGE_SPEAK = 1; 88 89 /** Stop speaking. */ 90 private static final int MESSAGE_STOP_SPEAK = 2; 91 92 /** Start the TTS service. */ 93 private static final int MESSAGE_START_TTS = 3; 94 95 /** Stop the TTS service. */ 96 private static final int MESSAGE_SHUTDOWN_TTS = 4; 97 98 /** Play an earcon. */ 99 private static final int MESSAGE_PLAY_EARCON = 5; 100 101 /** Stop playing an earcon. */ 102 private static final int MESSAGE_STOP_PLAY_EARCON = 6; 103 104 /** Vibrate a pattern. */ 105 private static final int MESSAGE_VIBRATE = 7; 106 107 /** Stop vibrating. */ 108 private static final int MESSAGE_STOP_VIBRATE = 8; 109 110 // Screen state broadcast related constants. 111 112 /** Feedback mapping index used as a key for the screen-on broadcast. */ 113 private static final int INDEX_SCREEN_ON = 0x00000100; 114 115 /** Feedback mapping index used as a key for the screen-off broadcast. */ 116 private static final int INDEX_SCREEN_OFF = 0x00000200; 117 118 // Ringer mode change related constants. 119 120 /** Feedback mapping index used as a key for normal ringer mode. */ 121 private static final int INDEX_RINGER_NORMAL = 0x00000400; 122 123 /** Feedback mapping index used as a key for vibration ringer mode. */ 124 private static final int INDEX_RINGER_VIBRATE = 0x00000800; 125 126 /** Feedback mapping index used as a key for silent ringer mode. */ 127 private static final int INDEX_RINGER_SILENT = 0x00001000; 128 129 // Speech related constants. 130 131 /** 132 * The queuing mode we are using - interrupt a spoken utterance before 133 * speaking another one. 134 */ 135 private static final int QUEUING_MODE_INTERRUPT = 2; 136 137 /** The space string constant. */ 138 private static final String SPACE = " "; 139 140 /** Mapping from integers to vibration patterns for haptic feedback. */ 141 private static final SparseArray<long[]> sVibrationPatterns = new SparseArray<long[]>(); 142 static { 143 sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, new long[] { 144 0L, 100L 145 }); 146 sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new long[] { 147 0L, 100L 148 }); 149 sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, new long[] { 150 0L, 15L, 10L, 15L 151 }); 152 sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, new long[] { 153 0L, 15L, 10L, 15L 154 }); 155 sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, new long[] { 156 0L, 25L, 50L, 25L, 50L, 25L 157 }); 158 sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, new long[] { 159 0L, 15L, 10L, 15L, 15L, 10L 160 }); 161 sVibrationPatterns.put(INDEX_SCREEN_ON, new long[] { 162 0L, 10L, 10L, 20L, 20L, 30L 163 }); 164 sVibrationPatterns.put(INDEX_SCREEN_OFF, new long[] { 165 0L, 30L, 20L, 20L, 10L, 10L 166 }); 167 } 168 169 /** Mapping from integers to raw sound resource ids. */ 170 private static SparseArray<Integer> sSoundsResourceIds = new SparseArray<Integer>(); 171 static { 172 sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_CLICKED, 173 R.raw.sound_view_clicked); 174 sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, 175 R.raw.sound_view_clicked); 176 sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED, 177 R.raw.sound_view_focused_or_selected); 178 sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, 179 R.raw.sound_view_focused_or_selected); 180 sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 181 R.raw.sound_window_state_changed); 182 sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, 183 R.raw.sound_view_hover_enter); 184 sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound_screen_on); 185 sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound_screen_off); 186 sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound_ringer_silent); 187 sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound_ringer_vibrate); 188 sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound_ringer_normal); 189 } 190 191 // Sound pool related member fields. 192 193 /** Mapping from integers to earcon names - dynamically populated. */ 194 private final SparseArray<String> mEarconNames = new SparseArray<String>(); 195 196 // Auxiliary fields. 197 198 /** 199 * Handle to this service to enable inner classes to access the {@link Context}. 200 */ 201 Context mContext; 202 203 /** The feedback this service is currently providing. */ 204 int mProvidedFeedbackType; 205 206 /** Reusable instance for building utterances. */ 207 private final StringBuilder mUtterance = new StringBuilder(); 208 209 // Feedback providing services. 210 211 /** The {@link TextToSpeech} used for speaking. */ 212 private TextToSpeech mTts; 213 214 /** The {@link AudioManager} for detecting ringer state. */ 215 private AudioManager mAudioManager; 216 217 /** Vibrator for providing haptic feedback. */ 218 private Vibrator mVibrator; 219 220 /** Flag if the infrastructure is initialized. */ 221 private boolean isInfrastructureInitialized; 222 223 /** {@link Handler} for executing messages on the service main thread. */ 224 Handler mHandler = new Handler() { 225 @Override 226 public void handleMessage(Message message) { 227 switch (message.what) { 228 case MESSAGE_SPEAK: 229 String utterance = (String) message.obj; 230 mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null); 231 return; 232 case MESSAGE_STOP_SPEAK: 233 mTts.stop(); 234 return; 235 case MESSAGE_START_TTS: 236 mTts = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() { 237 public void onInit(int status) { 238 // Register here since to add earcons the TTS must be initialized and 239 // the receiver is called immediately with the current ringer mode. 240 registerBroadCastReceiver(); 241 } 242 }); 243 return; 244 case MESSAGE_SHUTDOWN_TTS: 245 mTts.shutdown(); 246 return; 247 case MESSAGE_PLAY_EARCON: 248 int resourceId = message.arg1; 249 playEarcon(resourceId); 250 return; 251 case MESSAGE_STOP_PLAY_EARCON: 252 mTts.stop(); 253 return; 254 case MESSAGE_VIBRATE: 255 int key = message.arg1; 256 long[] pattern = sVibrationPatterns.get(key); 257 if (pattern != null) { 258 mVibrator.vibrate(pattern, -1); 259 } 260 return; 261 case MESSAGE_STOP_VIBRATE: 262 mVibrator.cancel(); 263 return; 264 } 265 } 266 }; 267 268 /** 269 * {@link BroadcastReceiver} for receiving updates for our context - device 270 * state. 271 */ 272 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 273 @Override 274 public void onReceive(Context context, Intent intent) { 275 String action = intent.getAction(); 276 277 if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { 278 int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, 279 AudioManager.RINGER_MODE_NORMAL); 280 configureForRingerMode(ringerMode); 281 } else if (Intent.ACTION_SCREEN_ON.equals(action)) { 282 provideScreenStateChangeFeedback(INDEX_SCREEN_ON); 283 } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { 284 provideScreenStateChangeFeedback(INDEX_SCREEN_OFF); 285 } else { 286 Log.w(LOG_TAG, "Registered for but not handling action " + action); 287 } 288 } 289 290 /** 291 * Provides feedback to announce the screen state change. Such a change 292 * is turning the screen on or off. 293 * 294 * @param feedbackIndex The index of the feedback in the statically 295 * mapped feedback resources. 296 */ 297 private void provideScreenStateChangeFeedback(int feedbackIndex) { 298 // We take a specific action depending on the feedback we currently provide. 299 switch (mProvidedFeedbackType) { 300 case AccessibilityServiceInfo.FEEDBACK_SPOKEN: 301 String utterance = generateScreenOnOrOffUtternace(feedbackIndex); 302 mHandler.obtainMessage(MESSAGE_SPEAK, utterance).sendToTarget(); 303 return; 304 case AccessibilityServiceInfo.FEEDBACK_AUDIBLE: 305 mHandler.obtainMessage(MESSAGE_PLAY_EARCON, feedbackIndex, 0).sendToTarget(); 306 return; 307 case AccessibilityServiceInfo.FEEDBACK_HAPTIC: 308 mHandler.obtainMessage(MESSAGE_VIBRATE, feedbackIndex, 0).sendToTarget(); 309 return; 310 default: 311 throw new IllegalStateException("Unexpected feedback type " 312 + mProvidedFeedbackType); 313 } 314 } 315 }; 316 317 @Override 318 public void onServiceConnected() { 319 if (isInfrastructureInitialized) { 320 return; 321 } 322 323 mContext = this; 324 325 // Send a message to start the TTS. 326 mHandler.sendEmptyMessage(MESSAGE_START_TTS); 327 328 // Get the vibrator service. 329 mVibrator = (Vibrator) getSystemService(Service.VIBRATOR_SERVICE); 330 331 // Get the AudioManager and configure according the current ring mode. 332 mAudioManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE); 333 // In Froyo the broadcast receiver for the ringer mode is called back with the 334 // current state upon registering but in Eclair this is not done so we poll here. 335 int ringerMode = mAudioManager.getRingerMode(); 336 configureForRingerMode(ringerMode); 337 338 // We are in an initialized state now. 339 isInfrastructureInitialized = true; 340 } 341 342 @Override 343 public boolean onUnbind(Intent intent) { 344 if (isInfrastructureInitialized) { 345 // Stop the TTS service. 346 mHandler.sendEmptyMessage(MESSAGE_SHUTDOWN_TTS); 347 348 // Unregister the intent broadcast receiver. 349 if (mBroadcastReceiver != null) { 350 unregisterReceiver(mBroadcastReceiver); 351 } 352 353 // We are not in an initialized state anymore. 354 isInfrastructureInitialized = false; 355 } 356 return false; 357 } 358 359 /** 360 * Registers the phone state observing broadcast receiver. 361 */ 362 private void registerBroadCastReceiver() { 363 // Create a filter with the broadcast intents we are interested in. 364 IntentFilter filter = new IntentFilter(); 365 filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); 366 filter.addAction(Intent.ACTION_SCREEN_ON); 367 filter.addAction(Intent.ACTION_SCREEN_OFF); 368 // Register for broadcasts of interest. 369 registerReceiver(mBroadcastReceiver, filter, null, null); 370 } 371 372 /** 373 * Generates an utterance for announcing screen on and screen off. 374 * 375 * @param feedbackIndex The feedback index for looking up feedback value. 376 * @return The utterance. 377 */ 378 private String generateScreenOnOrOffUtternace(int feedbackIndex) { 379 // Get the announce template. 380 int resourceId = (feedbackIndex == INDEX_SCREEN_ON) ? R.string.template_screen_on 381 : R.string.template_screen_off; 382 String template = mContext.getString(resourceId); 383 384 // Format the template with the ringer percentage. 385 int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING); 386 int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING); 387 int volumePercent = (100 / maxRingerVolume) * currentRingerVolume; 388 389 // Let us round to five so it sounds better. 390 int adjustment = volumePercent % 10; 391 if (adjustment < 5) { 392 volumePercent -= adjustment; 393 } else if (adjustment > 5) { 394 volumePercent += (10 - adjustment); 395 } 396 397 return String.format(template, volumePercent); 398 } 399 400 /** 401 * Configures the service according to a ringer mode. Possible 402 * configurations: 403 * <p> 404 * 1. {@link AudioManager#RINGER_MODE_SILENT}<br/> 405 * Goal: Provide only custom haptic feedback.<br/> 406 * Approach: Take over the haptic feedback by configuring this service to provide 407 * such and do so. This way the system will not call the default haptic 408 * feedback service KickBack.<br/> 409 * Take over the audible and spoken feedback by configuring this 410 * service to provide such feedback but not doing so. This way the system 411 * will not call the default spoken feedback service TalkBack and the 412 * default audible feedback service SoundBack. 413 * </p> 414 * <p> 415 * 2. {@link AudioManager#RINGER_MODE_VIBRATE}<br/> 416 * Goal: Provide custom audible and default haptic feedback.<br/> 417 * Approach: Take over the audible feedback and provide custom one.<br/> 418 * Take over the spoken feedback but do not provide such.<br/> 419 * Let some other service provide haptic feedback (KickBack). 420 * </p> 421 * <p> 422 * 3. {@link AudioManager#RINGER_MODE_NORMAL} 423 * Goal: Provide custom spoken, default audible and default haptic feedback.<br/> 424 * Approach: Take over the spoken feedback and provide custom one.<br/> 425 * Let some other services provide audible feedback (SounBack) and haptic 426 * feedback (KickBack). 427 * </p> 428 * 429 * @param ringerMode The device ringer mode. 430 */ 431 private void configureForRingerMode(int ringerMode) { 432 if (ringerMode == AudioManager.RINGER_MODE_SILENT) { 433 // When the ringer is silent we want to provide only haptic feedback. 434 mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_HAPTIC; 435 436 // Take over the spoken and sound feedback so no such feedback is provided. 437 setServiceInfo(AccessibilityServiceInfo.FEEDBACK_HAPTIC 438 | AccessibilityServiceInfo.FEEDBACK_SPOKEN 439 | AccessibilityServiceInfo.FEEDBACK_AUDIBLE); 440 441 // Use only an earcon to announce ringer state change. 442 mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_SILENT, 0).sendToTarget(); 443 } else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) { 444 // When the ringer is vibrating we want to provide only audible feedback. 445 mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_AUDIBLE; 446 447 // Take over the spoken feedback so no spoken feedback is provided. 448 setServiceInfo(AccessibilityServiceInfo.FEEDBACK_AUDIBLE 449 | AccessibilityServiceInfo.FEEDBACK_SPOKEN); 450 451 // Use only an earcon to announce ringer state change. 452 mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_VIBRATE, 0).sendToTarget(); 453 } else if (ringerMode == AudioManager.RINGER_MODE_NORMAL) { 454 // When the ringer is ringing we want to provide spoken feedback 455 // overriding the default spoken feedback. 456 mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN; 457 setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN); 458 459 // Use only an earcon to announce ringer state change. 460 mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_NORMAL, 0).sendToTarget(); 461 } 462 } 463 464 /** 465 * Sets the {@link AccessibilityServiceInfo} which informs the system how to 466 * handle this {@link AccessibilityService}. 467 * 468 * @param feedbackType The type of feedback this service will provide. 469 * <p> 470 * Note: The feedbackType parameter is an bitwise or of all 471 * feedback types this service would like to provide. 472 * </p> 473 */ 474 private void setServiceInfo(int feedbackType) { 475 AccessibilityServiceInfo info = new AccessibilityServiceInfo(); 476 // We are interested in all types of accessibility events. 477 info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; 478 // We want to provide specific type of feedback. 479 info.feedbackType = feedbackType; 480 // We want to receive events in a certain interval. 481 info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS; 482 // We want to receive accessibility events only from certain packages. 483 info.packageNames = PACKAGE_NAMES; 484 setServiceInfo(info); 485 } 486 487 @Override 488 public void onAccessibilityEvent(AccessibilityEvent event) { 489 Log.i(LOG_TAG, mProvidedFeedbackType + " " + event.toString()); 490 491 // Here we act according to the feedback type we are currently providing. 492 if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) { 493 mHandler.obtainMessage(MESSAGE_SPEAK, formatUtterance(event)).sendToTarget(); 494 } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) { 495 mHandler.obtainMessage(MESSAGE_PLAY_EARCON, event.getEventType(), 0).sendToTarget(); 496 } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_HAPTIC) { 497 mHandler.obtainMessage(MESSAGE_VIBRATE, event.getEventType(), 0).sendToTarget(); 498 } else { 499 throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType); 500 } 501 } 502 503 @Override 504 public void onInterrupt() { 505 // Here we act according to the feedback type we are currently providing. 506 if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) { 507 mHandler.obtainMessage(MESSAGE_STOP_SPEAK).sendToTarget(); 508 } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) { 509 mHandler.obtainMessage(MESSAGE_STOP_PLAY_EARCON).sendToTarget(); 510 } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_HAPTIC) { 511 mHandler.obtainMessage(MESSAGE_STOP_VIBRATE).sendToTarget(); 512 } else { 513 throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType); 514 } 515 } 516 517 /** 518 * Formats an utterance from an {@link AccessibilityEvent}. 519 * 520 * @param event The event from which to format an utterance. 521 * @return The formatted utterance. 522 */ 523 private String formatUtterance(AccessibilityEvent event) { 524 StringBuilder utterance = mUtterance; 525 526 // Clear the utterance before appending the formatted text. 527 utterance.setLength(0); 528 529 List<CharSequence> eventText = event.getText(); 530 531 // We try to get the event text if such. 532 if (!eventText.isEmpty()) { 533 for (CharSequence subText : eventText) { 534 // Make 01 pronounced as 1 535 if (subText.charAt(0) =='0') { 536 subText = subText.subSequence(1, subText.length()); 537 } 538 utterance.append(subText); 539 utterance.append(SPACE); 540 } 541 542 return utterance.toString(); 543 } 544 545 // There is no event text but we try to get the content description which is 546 // an optional attribute for describing a view (typically used with ImageView). 547 CharSequence contentDescription = event.getContentDescription(); 548 if (contentDescription != null) { 549 utterance.append(contentDescription); 550 return utterance.toString(); 551 } 552 553 return utterance.toString(); 554 } 555 556 /** 557 * Plays an earcon given its id. 558 * 559 * @param earconId The id of the earcon to be played. 560 */ 561 private void playEarcon(int earconId) { 562 String earconName = mEarconNames.get(earconId); 563 if (earconName == null) { 564 // We do not know the sound id, hence we need to load the sound. 565 Integer resourceId = sSoundsResourceIds.get(earconId); 566 if (resourceId != null) { 567 earconName = "[" + earconId + "]"; 568 mTts.addEarcon(earconName, getPackageName(), resourceId); 569 mEarconNames.put(earconId, earconName); 570 } 571 } 572 573 mTts.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null); 574 } 575 } 576