PageRenderTime 43ms CodeModel.GetById 12ms app.highlight 25ms RepoModel.GetById 1ms app.codeStats 1ms

/documentation/ClockBackTutorial/ClockBack5/src/com/google/android/marvin/clockback/ClockBackService.java

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