PageRenderTime 73ms CodeModel.GetById 11ms app.highlight 56ms RepoModel.GetById 1ms app.codeStats 0ms

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