PageRenderTime 47ms CodeModel.GetById 15ms app.highlight 25ms RepoModel.GetById 2ms app.codeStats 0ms

/ClockBack/src/com/google/android/marvin/clockback/ClockBackService.java

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