PageRenderTime 33ms CodeModel.GetById 10ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 0ms

/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
 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}