/documentation/ClockBackTutorial/ClockBack3/src/com/google/android/marvin/clockback/ClockBackService.java
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 17package com.google.android.marvin.clockback; 18 19import android.accessibilityservice.AccessibilityService; 20import android.accessibilityservice.AccessibilityServiceInfo; 21import android.app.Service; 22import android.content.BroadcastReceiver; 23import android.content.Context; 24import android.content.Intent; 25import android.content.IntentFilter; 26import android.media.AudioManager; 27import android.os.Handler; 28import android.os.Message; 29import android.speech.tts.TextToSpeech; 30import android.util.Log; 31import android.util.SparseArray; 32import android.view.accessibility.AccessibilityEvent; 33 34import java.util.List; 35 36/** 37 * This class is an {@link AccessibilityService} that provides custom feedback 38 * for the Clock application that comes by default with Android devices. It 39 * demonstrates the following key features of the Android accessibility APIs: 40 * <ol> 41 * <li> 42 * Simple demonstration of how to use the accessibility APIs. 43 * </li> 44 * <li> 45 * Hands-on example of various ways to utilize the accessibility API for 46 * providing alternative and complementary feedback. 47 * </li> 48 * <li> 49 * Providing application specific feedback - the service handles only 50 * accessibility events from the clock application. 51 * </li> 52 * <li> 53 * Providing dynamic, context-dependent feedback - feedback type changes 54 * depending on the ringer state.</li> 55 * <li> 56 * Application specific UI enhancement - application domain knowledge is 57 * utilized to enhance the provided feedback. 58 * </li> 59 * </ol> 60 * 61 * @author svetoslavganov@google.com (Svetoslav R. Ganov) 62 */ 63public class ClockBackService extends AccessibilityService { 64 65 /** Tag for logging from this service */ 66 private static final String LOG_TAG = "ClockBackService"; 67 68 // fields for configuring how the system handles this accessibility service 69 70 /** Minimal timeout between accessibility events we want to receive */ 71 private static final int EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80; 72 73 /** Packages we are interested in */ 74 // This works with AlarmClock and Clock whose package name changes in different releases 75 private static final String[] PACKAGE_NAMES = new String[] { 76 "com.android.alarmclock", "com.google.android.deskclock", "com.android.deskclock" 77 }; 78 79 // message types we are passing around 80 81 /** Speak */ 82 private static final int WHAT_SPEAK = 1; 83 84 /** Stop speaking */ 85 private static final int WHAT_STOP_SPEAK = 2; 86 87 /** Start the TTS service */ 88 private static final int WHAT_START_TTS = 3; 89 90 /** Stop the TTS service */ 91 private static final int WHAT_SHUTDOWN_TTS = 4; 92 93 /** Play an earcon */ 94 private static final int WHAT_PLAY_EARCON = 5; 95 96 /** Stop playing an earcon */ 97 private static final int WHAT_STOP_PLAY_EARCON = 6; 98 99 //screen state broadcast related constants 100 101 /** Feedback mapping index used as a key for the screen on broadcast */ 102 private static final int INDEX_SCREEN_ON = 0x00000100; 103 104 /** Feedback mapping index used as a key for the screen off broadcast */ 105 private static final int INDEX_SCREEN_OFF = 0x00000200; 106 107 // ringer mode change related constants 108 109 /** Feedback mapping index used as a key for normal ringer mode */ 110 private static final int INDEX_RINGER_NORMAL = 0x00000400; 111 112 /** Feedback mapping index used as a key for vibration ringer mode */ 113 private static final int INDEX_RINGER_VIBRATE = 0x00000800; 114 115 /** Feedback mapping index used as a key for silent ringer mode */ 116 private static final int INDEX_RINGER_SILENT = 0x00001000; 117 118 // speech related constants 119 120 /** 121 * The queuing mode we are using - interrupt a spoken utterance before 122 * speaking another one 123 */ 124 private static final int QUEUING_MODE_INTERRUPT = 2; 125 126 /** The empty string constant */ 127 private static final String SPACE = " "; 128 129 /** 130 * The class name of the number picker buttons with no text we want to 131 * announce in the Clock application. 132 */ 133 private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK = "android.widget.NumberPickerButton"; 134 135 /** 136 * The class name of the number picker buttons with no text we want to 137 * announce in the AlarmClock application. 138 */ 139 private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK = "com.android.internal.widget.NumberPickerButton"; 140 141 /** 142 * The class name of the edit text box for hours and minutes we want to 143 * better announce 144 */ 145 private static final String CLASS_NAME_EDIT_TEXT = "android.widget.EditText"; 146 147 /** 148 * Mapping from integer to string resource id where the keys are generated 149 * from the {@link AccessibilityEvent#getItemCount()} and 150 * {@link AccessibilityEvent#getCurrentItemIndex()} properties. 151 */ 152 private static final SparseArray<Integer> sPositionMappedStringResourceIds = new SparseArray<Integer>(); 153 static { 154 sPositionMappedStringResourceIds.put(11, R.string.value_plus); 155 sPositionMappedStringResourceIds.put(114, R.string.value_plus); 156 sPositionMappedStringResourceIds.put(112, R.string.value_minus); 157 sPositionMappedStringResourceIds.put(116, R.string.value_minus); 158 sPositionMappedStringResourceIds.put(111, R.string.value_hours); 159 sPositionMappedStringResourceIds.put(115, R.string.value_minutes); 160 } 161 162 /** Mapping from integers to raw sound resource ids */ 163 private static SparseArray<Integer> sSoundsResourceIds = new SparseArray<Integer>(); 164 static { 165 sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound6); 166 sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound7); 167 sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound8); 168 } 169 170 // sound pool related member fields 171 172 /** Mapping from integers to earcon names - dynamically populated. */ 173 private final SparseArray<String> mEarconNames = new SparseArray<String>(); 174 175 // auxiliary fields 176 177 /** 178 * Handle to this service to enable inner classes to access the {@link Context} 179 */ 180 private Context mContext; 181 182 /** Reusable instance for building utterances */ 183 private final StringBuilder mUtterance = new StringBuilder(); 184 185 // feedback providing services 186 187 /** The {@link TextToSpeech} used for speaking */ 188 private TextToSpeech mTts; 189 190 /** The {@link AudioManager} for detecting ringer state */ 191 private AudioManager mAudioManager; 192 193 /** Flag if the infrastructure is initialized */ 194 private boolean isInfrastructureInitialized; 195 196 /** {@link Handler} for executing messages on the service main thread */ 197 Handler mHandler = new Handler() { 198 @Override 199 public void handleMessage(Message message) { 200 switch (message.what) { 201 case WHAT_SPEAK: 202 String utterance = (String) message.obj; 203 mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null); 204 return; 205 case WHAT_STOP_SPEAK: 206 mTts.stop(); 207 return; 208 case WHAT_START_TTS: 209 mTts = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() { 210 @Override 211 public void onInit(int status) { 212 // register here since to add earcons the TTS must be initialized 213 // the receiver is called immediately with the current ringer mode 214 registerBroadCastReceiver(); 215 } 216 }); 217 return; 218 case WHAT_SHUTDOWN_TTS: 219 mTts.shutdown(); 220 return; 221 case WHAT_PLAY_EARCON: 222 int resourceId = message.arg1; 223 playEarcon(resourceId); 224 return; 225 case WHAT_STOP_PLAY_EARCON: 226 mTts.stop(); 227 return; 228 } 229 } 230 }; 231 232 /** 233 * {@link BroadcastReceiver} for receiving updates for our context - device 234 * state 235 */ 236 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 237 @Override 238 public void onReceive(Context context, Intent intent) { 239 String action = intent.getAction(); 240 241 if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { 242 int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, 243 AudioManager.RINGER_MODE_NORMAL); 244 configureForRingerMode(ringerMode); 245 } else if (Intent.ACTION_SCREEN_ON.equals(action)) { 246 provideScreenStateChangeFeedback(INDEX_SCREEN_ON); 247 } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { 248 provideScreenStateChangeFeedback(INDEX_SCREEN_OFF); 249 } else { 250 Log.w(LOG_TAG, "Registered for but not handling action " + action); 251 } 252 } 253 254 /** 255 * Provides feedback to announce the screen state change. Such a change 256 * is turning the screen on or off. 257 * 258 * @param feedbackIndex The index of the feedback in the statically 259 * mapped feedback resources. 260 */ 261 private void provideScreenStateChangeFeedback(int feedbackIndex) { 262 String utterance = generateScreenOnOrOffUtternace(feedbackIndex); 263 mHandler.obtainMessage(WHAT_SPEAK, utterance).sendToTarget(); 264 } 265 }; 266 267 @Override 268 public void onServiceConnected() { 269 if (isInfrastructureInitialized) { 270 return; 271 } 272 273 mContext = this; 274 275 // send a message to start the TTS 276 mHandler.sendEmptyMessage(WHAT_START_TTS); 277 278 // get the AudioManager and configure according the current ring mode 279 mAudioManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE); 280 281 setServiceInfo(AccessibilityServiceInfo.FEEDBACK_AUDIBLE); 282 283 // we are in an initialized state now 284 isInfrastructureInitialized = true; 285 } 286 287 @Override 288 public boolean onUnbind(Intent intent) { 289 if (isInfrastructureInitialized) { 290 // stop the TTS service 291 mHandler.sendEmptyMessage(WHAT_SHUTDOWN_TTS); 292 293 // unregister the intent broadcast receiver 294 if (mBroadcastReceiver != null) { 295 unregisterReceiver(mBroadcastReceiver); 296 } 297 298 // we are not in an initialized state anymore 299 isInfrastructureInitialized = false; 300 } 301 return false; 302 } 303 304 /** 305 * Registers the phone state observing broadcast receiver. 306 */ 307 private void registerBroadCastReceiver() { 308 //Create a filter with the broadcast intents we are interested in 309 IntentFilter filter = new IntentFilter(); 310 filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); 311 filter.addAction(Intent.ACTION_SCREEN_ON); 312 filter.addAction(Intent.ACTION_SCREEN_OFF); 313 // register for broadcasts of interest 314 registerReceiver(mBroadcastReceiver, filter, null, null); 315 } 316 317 /** 318 * Generates an utterance for announcing screen on and screen off. 319 * 320 * @param feedbackIndex The feedback index for looking up feedback value. 321 * @return The utterance. 322 */ 323 private String generateScreenOnOrOffUtternace(int feedbackIndex) { 324 // get the announce template 325 int resourceId = (feedbackIndex == INDEX_SCREEN_ON) ? R.string.template_screen_on 326 : R.string.template_screen_off; 327 String template = mContext.getString(resourceId); 328 329 // format the template with the ringer percentage 330 int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING); 331 int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING); 332 int volumePercent = (100 / maxRingerVolume) * currentRingerVolume; 333 334 // let us round to five so it sounds better 335 int adjustment = volumePercent % 10; 336 if (adjustment < 5) { 337 volumePercent -= adjustment; 338 } else if (adjustment > 5) { 339 volumePercent += (10 - adjustment); 340 } 341 342 return String.format(template, volumePercent); 343 } 344 345 /** 346 * Configures the service according to a ringer mode. 347 * 348 * @param ringerMode The device ringer mode. 349 */ 350 private void configureForRingerMode(int ringerMode) { 351 if (ringerMode == AudioManager.RINGER_MODE_SILENT) { 352 // use only an earcon to announce ringer state change 353 mHandler.obtainMessage(WHAT_PLAY_EARCON, INDEX_RINGER_SILENT, 0).sendToTarget(); 354 } else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) { 355 // use only an earcon to announce ringer state change 356 mHandler.obtainMessage(WHAT_PLAY_EARCON, INDEX_RINGER_VIBRATE, 0).sendToTarget(); 357 } else if (ringerMode == AudioManager.RINGER_MODE_NORMAL) { 358 // use only an earcon to announce ringer state change 359 mHandler.obtainMessage(WHAT_PLAY_EARCON, INDEX_RINGER_NORMAL, 0).sendToTarget(); 360 } 361 } 362 363 /** 364 * Sets the {@link AccessibilityServiceInfo} which informs the system how to 365 * handle this {@link AccessibilityService}. 366 * 367 * @param feedbackType The type of feedback this service will provide. </p> 368 * Note: The feedbackType parameter is an bitwise or of all 369 * feedback types this service would like to provide. 370 */ 371 private void setServiceInfo(int feedbackType) { 372 AccessibilityServiceInfo info = new AccessibilityServiceInfo(); 373 // we are interested in all types of accessibility events 374 info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK; 375 // we want to provide specific type of feedback 376 info.feedbackType = feedbackType; 377 // we want to receive events in a certain interval 378 info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS; 379 // we want to receive accessibility events only from certain packages 380 info.packageNames = PACKAGE_NAMES; 381 setServiceInfo(info); 382 } 383 384 @Override 385 public void onAccessibilityEvent(AccessibilityEvent event) { 386 Log.i(LOG_TAG, event.toString()); 387 388 mHandler.obtainMessage(WHAT_SPEAK, formatUtterance(event)).sendToTarget(); 389 } 390 391 @Override 392 public void onInterrupt() { 393 mHandler.obtainMessage(WHAT_STOP_SPEAK); 394 } 395 396 /** 397 * Formats an utterance from an {@link AccessibilityEvent}. 398 * 399 * @param event The event from which to format an utterance. 400 * @return The formatted utterance. 401 */ 402 private String formatUtterance(AccessibilityEvent event) { 403 StringBuilder utterance = mUtterance; 404 405 // clear the utterance before appending the formatted text 406 utterance.delete(0, utterance.length()); 407 408 List<CharSequence> eventText = event.getText(); 409 410 // We try to get the event text if such 411 if (!eventText.isEmpty()) { 412 for (CharSequence subText : eventText) { 413 utterance.append(subText); 414 utterance.append(SPACE); 415 } 416 417 // here we do a bit of enhancement of the UI presentation by using the semantic 418 // of the event source in the context of the Clock application 419 if (CLASS_NAME_EDIT_TEXT.equals(event.getClassName())) { 420 // if the source is an edit text box and we have a mapping based on 421 // its position in the items of the container parent of the event source 422 // we append that value as well. We say "XX hours" and "XX minutes". 423 String resourceValue = getPositionMappedStringResource(event.getItemCount(), 424 event.getCurrentItemIndex()); 425 if (resourceValue != null) { 426 utterance.append(resourceValue); 427 } 428 } 429 430 return utterance.toString(); 431 } 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 441 // No text and content description for the plus and minus buttons, so we lookup 442 // custom values based on the event's itemCount and currentItemIndex properties. 443 CharSequence className = event.getClassName(); 444 if (CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK.equals(className) 445 || CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK.equals(className)) { 446 String resourceValue = getPositionMappedStringResource(event.getItemCount(), 447 event.getCurrentItemIndex()); 448 utterance.append(resourceValue); 449 } 450 451 return utterance.toString(); 452 } 453 454 /** 455 * Returns a string resource mapped for a given position based on 456 * {@link AccessibilityEvent#getItemCount()} and 457 * {@link AccessibilityEvent#getCurrentItemIndex()} properties. 458 * 459 * @param itemCount The value of {@link AccessibilityEvent#getItemCount()}. 460 * @param currentItemIndex The value of 461 * {@link AccessibilityEvent#getCurrentItemIndex()}. 462 * @return The mapped string if such exists, null otherwise. 463 */ 464 private String getPositionMappedStringResource(int itemCount, int currentItemIndex) { 465 int lookupIndex = computeLookupIndex(itemCount, currentItemIndex); 466 int resourceId = sPositionMappedStringResourceIds.get(lookupIndex); 467 return getString(resourceId); 468 } 469 470 /** 471 * Computes an index for looking up the custom text for views with neither 472 * text not content description. The index is computed based on 473 * {@link AccessibilityEvent#getItemCount()} and 474 * {@link AccessibilityEvent#getCurrentItemIndex()} properties. 475 * 476 * @param itemCount The number of all items in the event source. 477 * @param currentItemIndex The index of the item source of the event. 478 * @return The lookup index. 479 */ 480 private int computeLookupIndex(int itemCount, int currentItemIndex) { 481 int lookupIndex = itemCount; 482 int divided = currentItemIndex; 483 484 while (divided > 0) { 485 lookupIndex *= 10; 486 divided /= 10; 487 } 488 489 return (lookupIndex += currentItemIndex); 490 } 491 492 /** 493 * Plays an earcon given its id. 494 * 495 * @param earconId The id of the earcon to be played. 496 */ 497 private void playEarcon(int earconId) { 498 String earconName = mEarconNames.get(earconId); 499 if (earconName == null) { 500 // we do not know the sound id, hence we need to load the sound 501 int resourceId = sSoundsResourceIds.get(earconId); 502 earconName = "[" + earconId + "]"; 503 mTts.addEarcon(earconName, getPackageName(), resourceId); 504 mEarconNames.put(earconId, earconName); 505 } 506 507 mTts.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null); 508 } 509}