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