Home | History | Annotate | Download | only in clockback
      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 
     17 package com.example.android.clockback;
     18 
     19 import android.accessibilityservice.AccessibilityService;
     20 import android.accessibilityservice.AccessibilityServiceInfo;
     21 import android.app.Service;
     22 import android.content.BroadcastReceiver;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.IntentFilter;
     26 import android.media.AudioManager;
     27 import android.os.Handler;
     28 import android.os.Message;
     29 import android.os.Vibrator;
     30 import android.speech.tts.TextToSpeech;
     31 import android.util.Log;
     32 import android.util.SparseArray;
     33 import android.view.accessibility.AccessibilityEvent;
     34 
     35 import 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  * <p>
     63  *   <strong>
     64  *     Note: This code sample will work only on devices shipped with the default Clock
     65  *     application. If you are running Android 1.6 of Android 2.0 you should enable first
     66  *     ClockBack and then TalkBack since in these releases accessibility services are
     67  *     notified in the order of registration.
     68  *   </strong>
     69  * </p>
     70  */
     71 public class ClockBackService extends AccessibilityService {
     72 
     73     /** Tag for logging from this service. */
     74     private static final String LOG_TAG = "ClockBackService";
     75 
     76     // Fields for configuring how the system handles this accessibility service.
     77 
     78     /** Minimal timeout between accessibility events we want to receive. */
     79     private static final int EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80;
     80 
     81     /** Packages we are interested in.
     82      * <p>
     83      *   <strong>
     84      *   Note: This code sample will work only on devices shipped with the
     85      *   default Clock application.
     86      *   </strong>
     87      * </p>
     88      */
     89     // This works with AlarmClock and Clock whose package name changes in different releases
     90     private static final String[] PACKAGE_NAMES = new String[] {
     91             "com.android.alarmclock", "com.google.android.deskclock", "com.android.deskclock"
     92     };
     93 
     94     // Message types we are passing around.
     95 
     96     /** Speak. */
     97     private static final int MESSAGE_SPEAK = 1;
     98 
     99     /** Stop speaking. */
    100     private static final int MESSAGE_STOP_SPEAK = 2;
    101 
    102     /** Start the TTS service. */
    103     private static final int MESSAGE_START_TTS = 3;
    104 
    105     /** Stop the TTS service. */
    106     private static final int MESSAGE_SHUTDOWN_TTS = 4;
    107 
    108     /** Play an earcon. */
    109     private static final int MESSAGE_PLAY_EARCON = 5;
    110 
    111     /** Stop playing an earcon. */
    112     private static final int MESSAGE_STOP_PLAY_EARCON = 6;
    113 
    114     /** Vibrate a pattern. */
    115     private static final int MESSAGE_VIBRATE = 7;
    116 
    117     /** Stop vibrating. */
    118     private static final int MESSAGE_STOP_VIBRATE = 8;
    119 
    120     // Screen state broadcast related constants.
    121 
    122     /** Feedback mapping index used as a key for the screen-on broadcast. */
    123     private static final int INDEX_SCREEN_ON = 0x00000100;
    124 
    125     /** Feedback mapping index used as a key for the screen-off broadcast. */
    126     private static final int INDEX_SCREEN_OFF = 0x00000200;
    127 
    128     // Ringer mode change related constants.
    129 
    130     /** Feedback mapping index used as a key for normal ringer mode. */
    131     private static final int INDEX_RINGER_NORMAL = 0x00000400;
    132 
    133     /** Feedback mapping index used as a key for vibration ringer mode. */
    134     private static final int INDEX_RINGER_VIBRATE = 0x00000800;
    135 
    136     /** Feedback mapping index used as a key for silent ringer mode. */
    137     private static final int INDEX_RINGER_SILENT = 0x00001000;
    138 
    139     // Speech related constants.
    140 
    141     /**
    142      * The queuing mode we are using - interrupt a spoken utterance before
    143      * speaking another one.
    144      */
    145     private static final int QUEUING_MODE_INTERRUPT = 2;
    146 
    147     /** The space string constant. */
    148     private static final String SPACE = " ";
    149 
    150     /**
    151      * The class name of the number picker buttons with no text we want to
    152      * announce in the Clock application.
    153      */
    154     private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK = "android.widget.NumberPickerButton";
    155 
    156     /**
    157      * The class name of the number picker buttons with no text we want to
    158      * announce in the AlarmClock application.
    159      */
    160     private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK = "com.android.internal.widget.NumberPickerButton";
    161 
    162     /**
    163      * The class name of the edit text box for hours and minutes we want to
    164      * better announce.
    165      */
    166     private static final String CLASS_NAME_EDIT_TEXT = "android.widget.EditText";
    167 
    168     /**
    169      * Mapping from integer to string resource id where the keys are generated
    170      * from the {@link AccessibilityEvent#getText()},
    171      * {@link AccessibilityEvent#getItemCount()} and
    172      * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
    173      * <p>
    174      * Note: In general, computing these mappings includes the widget position on
    175      * the screen. This is fragile and should be used as a last resort since
    176      * changing the layout could potentially change the widget position. This is
    177      * a workaround since the widgets of interest are image buttons that do not
    178      * have contentDescription attribute set (plus/minus buttons) or no other
    179      * information in the accessibility event is available to distinguish them
    180      * aside of their positions on the screen (hour/minute inputs).<br/>
    181      * If you are owner of the target application (Clock in this case) you
    182      * should add contentDescription attribute to all image buttons such that a
    183      * screen reader knows how to speak them. For input fields (while not
    184      * applicable for the hour and minute inputs since they are not empty) a
    185      * hint text should be set to enable better announcement.
    186      * </p>
    187      */
    188     private static final SparseArray<Integer> sEventDataMappedStringResourceIds = new SparseArray<Integer>();
    189     static {
    190         sEventDataMappedStringResourceIds.put(110, R.string.value_increase_hours);
    191         sEventDataMappedStringResourceIds.put(1140, R.string.value_increase_minutes);
    192         sEventDataMappedStringResourceIds.put(1120, R.string.value_decrease_hours);
    193         sEventDataMappedStringResourceIds.put(1160, R.string.value_decrease_minutes);
    194         sEventDataMappedStringResourceIds.put(1111, R.string.value_hour);
    195         sEventDataMappedStringResourceIds.put(1110, R.string.value_hours);
    196         sEventDataMappedStringResourceIds.put(1151, R.string.value_minute);
    197         sEventDataMappedStringResourceIds.put(1150, R.string.value_minutes);
    198     }
    199 
    200     /** Mapping from integers to vibration patterns for haptic feedback. */
    201     private static final SparseArray<long[]> sVibrationPatterns = new SparseArray<long[]>();
    202     static {
    203         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, new long[] {
    204                 0L, 100L
    205         });
    206         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new long[] {
    207                 0L, 100L
    208         });
    209         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, new long[] {
    210                 0L, 15L, 10L, 15L
    211         });
    212         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, new long[] {
    213                 0L, 15L, 10L, 15L
    214         });
    215         sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, new long[] {
    216                 0L, 25L, 50L, 25L, 50L, 25L
    217         });
    218         sVibrationPatterns.put(INDEX_SCREEN_ON, new long[] {
    219                 0L, 10L, 10L, 20L, 20L, 30L
    220         });
    221         sVibrationPatterns.put(INDEX_SCREEN_OFF, new long[] {
    222                 0L, 30L, 20L, 20L, 10L, 10L
    223         });
    224     }
    225 
    226     /** Mapping from integers to raw sound resource ids. */
    227     private static SparseArray<Integer> sSoundsResourceIds = new SparseArray<Integer>();
    228     static {
    229         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_CLICKED, R.raw.sound_view_clicked);
    230         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, R.raw.sound_view_clicked);
    231         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED, R.raw.sound_view_focused_or_selected);
    232         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, R.raw.sound_view_focused_or_selected);
    233         sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, R.raw.sound_window_state_changed);
    234         sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound_screen_on);
    235         sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound_screen_off);
    236         sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound_ringer_silent);
    237         sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound_ringer_vibrate);
    238         sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound_ringer_normal);
    239     }
    240 
    241     // Sound pool related member fields.
    242 
    243     /** Mapping from integers to earcon names - dynamically populated. */
    244     private final SparseArray<String> mEarconNames = new SparseArray<String>();
    245 
    246     // Auxiliary fields.
    247 
    248     /**
    249      * Handle to this service to enable inner classes to access the {@link Context}.
    250      */
    251     Context mContext;
    252 
    253     /** The feedback this service is currently providing. */
    254     int mProvidedFeedbackType;
    255 
    256     /** Reusable instance for building utterances. */
    257     private final StringBuilder mUtterance = new StringBuilder();
    258 
    259     // Feedback providing services.
    260 
    261     /** The {@link TextToSpeech} used for speaking. */
    262     private TextToSpeech mTts;
    263 
    264     /** The {@link AudioManager} for detecting ringer state. */
    265     private AudioManager mAudioManager;
    266 
    267     /** Vibrator for providing haptic feedback. */
    268     private Vibrator mVibrator;
    269 
    270     /** Flag if the infrastructure is initialized. */
    271     private boolean isInfrastructureInitialized;
    272 
    273     /** {@link Handler} for executing messages on the service main thread. */
    274     Handler mHandler = new Handler() {
    275         @Override
    276         public void handleMessage(Message message) {
    277             switch (message.what) {
    278                 case MESSAGE_SPEAK:
    279                     String utterance = (String) message.obj;
    280                     mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null);
    281                     return;
    282                 case MESSAGE_STOP_SPEAK:
    283                     mTts.stop();
    284                     return;
    285                 case MESSAGE_START_TTS:
    286                     mTts = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() {
    287                         public void onInit(int status) {
    288                             // Register here since to add earcons the TTS must be initialized and
    289                             // the receiver is called immediately with the current ringer mode.
    290                             registerBroadCastReceiver();
    291                         }
    292                     });
    293                     return;
    294                 case MESSAGE_SHUTDOWN_TTS:
    295                     mTts.shutdown();
    296                     return;
    297                 case MESSAGE_PLAY_EARCON:
    298                     int resourceId = message.arg1;
    299                     playEarcon(resourceId);
    300                     return;
    301                 case MESSAGE_STOP_PLAY_EARCON:
    302                     mTts.stop();
    303                     return;
    304                 case MESSAGE_VIBRATE:
    305                     int key = message.arg1;
    306                     long[] pattern = sVibrationPatterns.get(key);
    307                     mVibrator.vibrate(pattern, -1);
    308                     return;
    309                 case MESSAGE_STOP_VIBRATE:
    310                     mVibrator.cancel();
    311                     return;
    312             }
    313         }
    314     };
    315 
    316     /**
    317      * {@link BroadcastReceiver} for receiving updates for our context - device
    318      * state.
    319      */
    320     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
    321         @Override
    322         public void onReceive(Context context, Intent intent) {
    323             String action = intent.getAction();
    324 
    325             if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) {
    326                 int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE,
    327                         AudioManager.RINGER_MODE_NORMAL);
    328                 configureForRingerMode(ringerMode);
    329             } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
    330                 provideScreenStateChangeFeedback(INDEX_SCREEN_ON);
    331             } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
    332                 provideScreenStateChangeFeedback(INDEX_SCREEN_OFF);
    333             } else {
    334                 Log.w(LOG_TAG, "Registered for but not handling action " + action);
    335             }
    336         }
    337 
    338         /**
    339          * Provides feedback to announce the screen state change. Such a change
    340          * is turning the screen on or off.
    341          *
    342          * @param feedbackIndex The index of the feedback in the statically
    343          *            mapped feedback resources.
    344          */
    345         private void provideScreenStateChangeFeedback(int feedbackIndex) {
    346             // We take a specific action depending on the feedback we currently provide.
    347             switch (mProvidedFeedbackType) {
    348                 case AccessibilityServiceInfo.FEEDBACK_SPOKEN:
    349                     String utterance = generateScreenOnOrOffUtternace(feedbackIndex);
    350                     mHandler.obtainMessage(MESSAGE_SPEAK, utterance).sendToTarget();
    351                     return;
    352                 case AccessibilityServiceInfo.FEEDBACK_AUDIBLE:
    353                     mHandler.obtainMessage(MESSAGE_PLAY_EARCON, feedbackIndex, 0).sendToTarget();
    354                     return;
    355                 case AccessibilityServiceInfo.FEEDBACK_HAPTIC:
    356                     mHandler.obtainMessage(MESSAGE_VIBRATE, feedbackIndex, 0).sendToTarget();
    357                     return;
    358                 default:
    359                     throw new IllegalStateException("Unexpected feedback type "
    360                             + mProvidedFeedbackType);
    361             }
    362         }
    363     };
    364 
    365     @Override
    366     public void onServiceConnected() {
    367         if (isInfrastructureInitialized) {
    368             return;
    369         }
    370 
    371         mContext = this;
    372 
    373         // Send a message to start the TTS.
    374         mHandler.sendEmptyMessage(MESSAGE_START_TTS);
    375 
    376         // Get the vibrator service.
    377         mVibrator = (Vibrator) getSystemService(Service.VIBRATOR_SERVICE);
    378 
    379         // Get the AudioManager and configure according the current ring mode.
    380         mAudioManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE);
    381         // In Froyo the broadcast receiver for the ringer mode is called back with the
    382         // current state upon registering but in Eclair this is not done so we poll here.
    383         int ringerMode = mAudioManager.getRingerMode();
    384         configureForRingerMode(ringerMode);
    385 
    386         // We are in an initialized state now.
    387         isInfrastructureInitialized = true;
    388     }
    389 
    390     @Override
    391     public boolean onUnbind(Intent intent) {
    392         if (isInfrastructureInitialized) {
    393             // Stop the TTS service.
    394             mHandler.sendEmptyMessage(MESSAGE_SHUTDOWN_TTS);
    395 
    396             // Unregister the intent broadcast receiver.
    397             if (mBroadcastReceiver != null) {
    398                 unregisterReceiver(mBroadcastReceiver);
    399             }
    400 
    401             // We are not in an initialized state anymore.
    402             isInfrastructureInitialized = false;
    403         }
    404         return false;
    405     }
    406 
    407     /**
    408      * Registers the phone state observing broadcast receiver.
    409      */
    410     private void registerBroadCastReceiver() {
    411         // Create a filter with the broadcast intents we are interested in.
    412         IntentFilter filter = new IntentFilter();
    413         filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
    414         filter.addAction(Intent.ACTION_SCREEN_ON);
    415         filter.addAction(Intent.ACTION_SCREEN_OFF);
    416         // Register for broadcasts of interest.
    417         registerReceiver(mBroadcastReceiver, filter, null, null);
    418     }
    419 
    420     /**
    421      * Generates an utterance for announcing screen on and screen off.
    422      *
    423      * @param feedbackIndex The feedback index for looking up feedback value.
    424      * @return The utterance.
    425      */
    426     private String generateScreenOnOrOffUtternace(int feedbackIndex) {
    427         // Get the announce template.
    428         int resourceId = (feedbackIndex == INDEX_SCREEN_ON) ? R.string.template_screen_on
    429                 : R.string.template_screen_off;
    430         String template = mContext.getString(resourceId);
    431 
    432         // Format the template with the ringer percentage.
    433         int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
    434         int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
    435         int volumePercent = (100 / maxRingerVolume) * currentRingerVolume;
    436 
    437         // Let us round to five so it sounds better.
    438         int adjustment = volumePercent % 10;
    439         if (adjustment < 5) {
    440             volumePercent -= adjustment;
    441         } else if (adjustment > 5) {
    442             volumePercent += (10 - adjustment);
    443         }
    444 
    445         return String.format(template, volumePercent);
    446     }
    447 
    448     /**
    449      * Configures the service according to a ringer mode. Possible
    450      * configurations:
    451      * <p>
    452      *   1. {@link AudioManager#RINGER_MODE_SILENT}<br/>
    453      *   Goal:     Provide only custom haptic feedback.<br/>
    454      *   Approach: Take over the haptic feedback by configuring this service to provide
    455      *             such and do so. This way the system will not call the default haptic
    456      *             feedback service KickBack.<br/>
    457      *             Take over the audible and spoken feedback by configuring this
    458      *             service to provide such feedback but not doing so. This way the system
    459      *             will not call the default spoken feedback service TalkBack and the
    460      *             default audible feedback service SoundBack.
    461      * </p>
    462      * <p>
    463      *   2. {@link AudioManager#RINGER_MODE_VIBRATE}<br/>
    464      *   Goal:     Provide custom audible and default haptic feedback.<br/>
    465      *   Approach: Take over the audible feedback and provide custom one.<br/>
    466      *             Take over the spoken feedback but do not provide such.<br/>
    467      *             Let some other service provide haptic feedback (KickBack).
    468      * </p>
    469      * <p>
    470      *   3. {@link AudioManager#RINGER_MODE_NORMAL}
    471      *   Goal:     Provide custom spoken, default audible and default haptic feedback.<br/>
    472      *   Approach: Take over the spoken feedback and provide custom one.<br/>
    473      *             Let some other services provide audible feedback (SounBack) and haptic
    474      *             feedback (KickBack).
    475      * </p>
    476      * Note: In the above description an assumption is made that all default feedback
    477      *       services are enabled. Such services are TalkBack, SoundBack, and KickBack.
    478      *       Also the feature of defining a service as the default for a given feedback
    479      *       type will be available in Android 2.2 and above. For previous releases the package
    480      *       specific accessibility service must be registered first i.e. checked in the
    481      *       settings.
    482      *
    483      * @param ringerMode The device ringer mode.
    484      */
    485     private void configureForRingerMode(int ringerMode) {
    486         if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
    487             // When the ringer is silent we want to provide only haptic feedback.
    488             mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_HAPTIC;
    489 
    490             // Take over the spoken and sound feedback so no such feedback is provided.
    491             setServiceInfo(AccessibilityServiceInfo.FEEDBACK_HAPTIC
    492                     | AccessibilityServiceInfo.FEEDBACK_SPOKEN
    493                     | AccessibilityServiceInfo.FEEDBACK_AUDIBLE);
    494 
    495             // Use only an earcon to announce ringer state change.
    496             mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_SILENT, 0).sendToTarget();
    497         } else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
    498             // When the ringer is vibrating we want to provide only audible feedback.
    499             mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_AUDIBLE;
    500 
    501             // Take over the spoken feedback so no spoken feedback is provided.
    502             setServiceInfo(AccessibilityServiceInfo.FEEDBACK_AUDIBLE
    503                     | AccessibilityServiceInfo.FEEDBACK_SPOKEN);
    504 
    505             // Use only an earcon to announce ringer state change.
    506             mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_VIBRATE, 0).sendToTarget();
    507         } else if (ringerMode == AudioManager.RINGER_MODE_NORMAL) {
    508             // When the ringer is ringing we want to provide spoken feedback
    509             // overriding the default spoken feedback.
    510             mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
    511             setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
    512 
    513             // Use only an earcon to announce ringer state change.
    514             mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_NORMAL, 0).sendToTarget();
    515         }
    516     }
    517 
    518     /**
    519      * Sets the {@link AccessibilityServiceInfo} which informs the system how to
    520      * handle this {@link AccessibilityService}.
    521      *
    522      * @param feedbackType The type of feedback this service will provide.
    523      * <p>
    524      *   Note: The feedbackType parameter is an bitwise or of all
    525      *   feedback types this service would like to provide.
    526      * </p>
    527      */
    528     private void setServiceInfo(int feedbackType) {
    529         AccessibilityServiceInfo info = new AccessibilityServiceInfo();
    530         // We are interested in all types of accessibility events.
    531         info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
    532         // We want to provide specific type of feedback.
    533         info.feedbackType = feedbackType;
    534         // We want to receive events in a certain interval.
    535         info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS;
    536         // We want to receive accessibility events only from certain packages.
    537         info.packageNames = PACKAGE_NAMES;
    538         setServiceInfo(info);
    539     }
    540 
    541     @Override
    542     public void onAccessibilityEvent(AccessibilityEvent event) {
    543         Log.i(LOG_TAG, mProvidedFeedbackType + " " + event.toString());
    544 
    545         // Here we act according to the feedback type we are currently providing.
    546         if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) {
    547             mHandler.obtainMessage(MESSAGE_SPEAK, formatUtterance(event)).sendToTarget();
    548         } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) {
    549             mHandler.obtainMessage(MESSAGE_PLAY_EARCON, event.getEventType(), 0).sendToTarget();
    550         } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_HAPTIC) {
    551             mHandler.obtainMessage(MESSAGE_VIBRATE, event.getEventType(), 0).sendToTarget();
    552         } else {
    553             throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType);
    554         }
    555     }
    556 
    557     @Override
    558     public void onInterrupt() {
    559         // Here we act according to the feedback type we are currently providing.
    560         if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) {
    561             mHandler.obtainMessage(MESSAGE_STOP_SPEAK).sendToTarget();
    562         } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) {
    563             mHandler.obtainMessage(MESSAGE_STOP_PLAY_EARCON).sendToTarget();
    564         } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_HAPTIC) {
    565             mHandler.obtainMessage(MESSAGE_STOP_VIBRATE).sendToTarget();
    566         } else {
    567             throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType);
    568         }
    569     }
    570 
    571     /**
    572      * Formats an utterance from an {@link AccessibilityEvent}.
    573      *
    574      * @param event The event from which to format an utterance.
    575      * @return The formatted utterance.
    576      */
    577     private String formatUtterance(AccessibilityEvent event) {
    578         StringBuilder utterance = mUtterance;
    579 
    580         // Clear the utterance before appending the formatted text.
    581         utterance.setLength(0);
    582 
    583         List<CharSequence> eventText = event.getText();
    584 
    585         // We try to get the event text if such.
    586         if (!eventText.isEmpty()) {
    587             for (CharSequence subText : eventText) {
    588                 // Make 01 pronounced as 1
    589                 if (subText.charAt(0) =='0') {
    590                     subText = subText.subSequence(1, subText.length());
    591                 }
    592                 utterance.append(subText);
    593                 utterance.append(SPACE);
    594             }
    595 
    596             // Here we do a bit of enhancement of the UI presentation by using the semantic
    597             // of the event source in the context of the Clock application.
    598             if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
    599                     && CLASS_NAME_EDIT_TEXT.equals(event.getClassName())) {
    600                 // If the source is an edit text box and we have a mapping based on
    601                 // its position in the items of the container parent of the event source
    602                 // we append that value as well. We say "XX hours" and "XX minutes".
    603                 String resourceValue = getEventDataMappedStringResource(event);
    604                 if (resourceValue != null) {
    605                     utterance.append(resourceValue);
    606                 }
    607             }
    608 
    609             return utterance.toString();
    610         }
    611 
    612         // There is no event text but we try to get the content description which is
    613         // an optional attribute for describing a view (typically used with ImageView).
    614         CharSequence contentDescription = event.getContentDescription();
    615         if (contentDescription != null) {
    616             utterance.append(contentDescription);
    617             return utterance.toString();
    618         }
    619 
    620         // No text and content description for the plus and minus buttons, so we lookup
    621         // custom values based on the event's itemCount and currentItemIndex properties.
    622         CharSequence className = event.getClassName();
    623 
    624         if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
    625                 && (CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK.equals(className)
    626                 || CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK.equals(className))) {
    627             String resourceValue = getEventDataMappedStringResource(event);
    628             utterance.append(resourceValue);
    629         }
    630 
    631         return utterance.toString();
    632     }
    633 
    634     /**
    635      * Returns a string resource mapped based on the accessibility event
    636      * data, specifically the
    637      * {@link AccessibilityEvent#getText()},
    638      * {@link AccessibilityEvent#getItemCount()}, and
    639      * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
    640      *
    641      * @param event The {@link AccessibilityEvent} to process.
    642      * @return The mapped string if such exists, null otherwise.
    643      */
    644     private String getEventDataMappedStringResource(AccessibilityEvent event) {
    645         int lookupIndex = computeLookupIndex(event);
    646         int resourceId = sEventDataMappedStringResourceIds.get(lookupIndex);
    647         return getString(resourceId);
    648     }
    649 
    650     /**
    651      * Computes an index for looking up the custom text for views which either
    652      * do not have text/content description or the position information
    653      * is the only oracle for deciding from which widget was an accessibility
    654      * event generated. The index is computed based on
    655      * {@link AccessibilityEvent#getText()},
    656      * {@link AccessibilityEvent#getItemCount()}, and
    657      * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
    658      *
    659      * @param event The event from which to compute the index.
    660      * @return The lookup index.
    661      */
    662     private int computeLookupIndex(AccessibilityEvent event) {
    663         int lookupIndex = event.getItemCount();
    664         int divided = event.getCurrentItemIndex();
    665 
    666         while (divided > 0) {
    667             lookupIndex *= 10;
    668             divided /= 10;
    669         }
    670 
    671         lookupIndex += event.getCurrentItemIndex();
    672         lookupIndex *= 10;
    673 
    674         // This is primarily for handling the zero hour/zero minutes cases
    675         if (!event.getText().isEmpty()
    676                 && ("1".equals(event.getText().get(0).toString()) || "01".equals(event.getText()
    677                         .get(0).toString()))) {
    678             lookupIndex++;
    679         }
    680 
    681         return lookupIndex;
    682     }
    683 
    684     /**
    685      * Plays an earcon given its id.
    686      *
    687      * @param earconId The id of the earcon to be played.
    688      */
    689     private void playEarcon(int earconId) {
    690         String earconName = mEarconNames.get(earconId);
    691         if (earconName == null) {
    692             // We do not know the sound id, hence we need to load the sound.
    693             int resourceId = sSoundsResourceIds.get(earconId);
    694             earconName = "[" + earconId + "]";
    695             mTts.addEarcon(earconName, getPackageName(), resourceId);
    696             mEarconNames.put(earconId, earconName);
    697         }
    698 
    699         mTts.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null);
    700     }
    701 }
    702