Home | History | Annotate | Download | only in accessibility
      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.apis.accessibility;
     18 
     19 import com.example.android.apis.R;
     20 
     21 import android.accessibilityservice.AccessibilityService;
     22 import android.accessibilityservice.AccessibilityServiceInfo;
     23 import android.app.Service;
     24 import android.content.BroadcastReceiver;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.IntentFilter;
     28 import android.media.AudioManager;
     29 import android.os.Handler;
     30 import android.os.Message;
     31 import android.os.Vibrator;
     32 import android.speech.tts.TextToSpeech;
     33 import android.util.Log;
     34 import android.util.SparseArray;
     35 import android.view.accessibility.AccessibilityEvent;
     36 
     37 import java.util.List;
     38 
     39 /**
     40  * This class is an {@link AccessibilityService} that provides custom feedback
     41  * for the Clock application that comes by default with Android devices. It
     42  * demonstrates the following key features of the Android accessibility APIs:
     43  * <ol>
     44  *   <li>
     45  *     Simple demonstration of how to use the accessibility APIs.
     46  *   </li>
     47  *   <li>
     48  *     Hands-on example of various ways to utilize the accessibility API for
     49  *     providing alternative and complementary feedback.
     50  *   </li>
     51  *   <li>
     52  *     Providing application specific feedback &mdash; the service handles only
     53  *     accessibility events from the clock application.
     54  *   </li>
     55  *   <li>
     56  *     Providing dynamic, context-dependent feedback &mdash; feedback type changes
     57  *     depending on the ringer state.
     58  *   </li>
     59  * </ol>
     60  */
     61 public class ClockBackService extends AccessibilityService {
     62 
     63     /** Tag for logging from this service. */
     64     private static final String LOG_TAG = "ClockBackService";
     65 
     66     // Fields for configuring how the system handles this accessibility service.
     67 
     68     /** Minimal timeout between accessibility events we want to receive. */
     69     private static final int EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80;
     70 
     71     /** Packages we are interested in.
     72      * <p>
     73      *   <strong>
     74      *   Note: This code sample will work only on devices shipped with the
     75      *   default Clock application.
     76      *   </strong>
     77      * </p>
     78      */
     79     // This works with AlarmClock and Clock whose package name changes in different releases
     80     private static final String[] PACKAGE_NAMES = new String[] {
     81             "com.android.alarmclock", "com.google.android.deskclock", "com.android.deskclock"
     82     };
     83 
     84     // Message types we are passing around.
     85 
     86     /** Speak. */
     87     private static final int MESSAGE_SPEAK = 1;
     88 
     89     /** Stop speaking. */
     90     private static final int MESSAGE_STOP_SPEAK = 2;
     91 
     92     /** Start the TTS service. */
     93     private static final int MESSAGE_START_TTS = 3;
     94 
     95     /** Stop the TTS service. */
     96     private static final int MESSAGE_SHUTDOWN_TTS = 4;
     97 
     98     /** Play an earcon. */
     99     private static final int MESSAGE_PLAY_EARCON = 5;
    100 
    101     /** Stop playing an earcon. */
    102     private static final int MESSAGE_STOP_PLAY_EARCON = 6;
    103 
    104     /** Vibrate a pattern. */
    105     private static final int MESSAGE_VIBRATE = 7;
    106 
    107     /** Stop vibrating. */
    108     private static final int MESSAGE_STOP_VIBRATE = 8;
    109 
    110     // Screen state broadcast related constants.
    111 
    112     /** Feedback mapping index used as a key for the screen-on broadcast. */
    113     private static final int INDEX_SCREEN_ON = 0x00000100;
    114 
    115     /** Feedback mapping index used as a key for the screen-off broadcast. */
    116     private static final int INDEX_SCREEN_OFF = 0x00000200;
    117 
    118     // Ringer mode change related constants.
    119 
    120     /** Feedback mapping index used as a key for normal ringer mode. */
    121     private static final int INDEX_RINGER_NORMAL = 0x00000400;
    122 
    123     /** Feedback mapping index used as a key for vibration ringer mode. */
    124     private static final int INDEX_RINGER_VIBRATE = 0x00000800;
    125 
    126     /** Feedback mapping index used as a key for silent ringer mode. */
    127     private static final int INDEX_RINGER_SILENT = 0x00001000;
    128 
    129     // Speech related constants.
    130 
    131     /**
    132      * The queuing mode we are using - interrupt a spoken utterance before
    133      * speaking another one.
    134      */
    135     private static final int QUEUING_MODE_INTERRUPT = 2;
    136 
    137     /** The space string constant. */
    138     private static final String SPACE = " ";
    139 
    140     /** Mapping from integers to vibration patterns for haptic feedback. */
    141     private static final SparseArray<long[]> sVibrationPatterns = new SparseArray<long[]>();
    142     static {
    143         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, new long[] {
    144                 0L, 100L
    145         });
    146         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new long[] {
    147                 0L, 100L
    148         });
    149         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, new long[] {
    150                 0L, 15L, 10L, 15L
    151         });
    152         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, new long[] {
    153                 0L, 15L, 10L, 15L
    154         });
    155         sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, new long[] {
    156                 0L, 25L, 50L, 25L, 50L, 25L
    157         });
    158         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, new long[] {
    159                 0L, 15L, 10L, 15L, 15L, 10L
    160         });
    161         sVibrationPatterns.put(INDEX_SCREEN_ON, new long[] {
    162                 0L, 10L, 10L, 20L, 20L, 30L
    163         });
    164         sVibrationPatterns.put(INDEX_SCREEN_OFF, new long[] {
    165                 0L, 30L, 20L, 20L, 10L, 10L
    166         });
    167     }
    168 
    169     /** Mapping from integers to raw sound resource ids. */
    170     private static SparseArray<Integer> sSoundsResourceIds = new SparseArray<Integer>();
    171     static {
    172         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_CLICKED,
    173                 R.raw.sound_view_clicked);
    174         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED,
    175                 R.raw.sound_view_clicked);
    176         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED,
    177                 R.raw.sound_view_focused_or_selected);
    178         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED,
    179                 R.raw.sound_view_focused_or_selected);
    180         sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
    181                 R.raw.sound_window_state_changed);
    182         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
    183                 R.raw.sound_view_hover_enter);
    184         sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound_screen_on);
    185         sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound_screen_off);
    186         sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound_ringer_silent);
    187         sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound_ringer_vibrate);
    188         sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound_ringer_normal);
    189     }
    190 
    191     // Sound pool related member fields.
    192 
    193     /** Mapping from integers to earcon names - dynamically populated. */
    194     private final SparseArray<String> mEarconNames = new SparseArray<String>();
    195 
    196     // Auxiliary fields.
    197 
    198     /**
    199      * Handle to this service to enable inner classes to access the {@link Context}.
    200      */
    201     Context mContext;
    202 
    203     /** The feedback this service is currently providing. */
    204     int mProvidedFeedbackType;
    205 
    206     /** Reusable instance for building utterances. */
    207     private final StringBuilder mUtterance = new StringBuilder();
    208 
    209     // Feedback providing services.
    210 
    211     /** The {@link TextToSpeech} used for speaking. */
    212     private TextToSpeech mTts;
    213 
    214     /** The {@link AudioManager} for detecting ringer state. */
    215     private AudioManager mAudioManager;
    216 
    217     /** Vibrator for providing haptic feedback. */
    218     private Vibrator mVibrator;
    219 
    220     /** Flag if the infrastructure is initialized. */
    221     private boolean isInfrastructureInitialized;
    222 
    223     /** {@link Handler} for executing messages on the service main thread. */
    224     Handler mHandler = new Handler() {
    225         @Override
    226         public void handleMessage(Message message) {
    227             switch (message.what) {
    228                 case MESSAGE_SPEAK:
    229                     String utterance = (String) message.obj;
    230                     mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null);
    231                     return;
    232                 case MESSAGE_STOP_SPEAK:
    233                     mTts.stop();
    234                     return;
    235                 case MESSAGE_START_TTS:
    236                     mTts = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() {
    237                         public void onInit(int status) {
    238                             // Register here since to add earcons the TTS must be initialized and
    239                             // the receiver is called immediately with the current ringer mode.
    240                             registerBroadCastReceiver();
    241                         }
    242                     });
    243                     return;
    244                 case MESSAGE_SHUTDOWN_TTS:
    245                     mTts.shutdown();
    246                     return;
    247                 case MESSAGE_PLAY_EARCON:
    248                     int resourceId = message.arg1;
    249                     playEarcon(resourceId);
    250                     return;
    251                 case MESSAGE_STOP_PLAY_EARCON:
    252                     mTts.stop();
    253                     return;
    254                 case MESSAGE_VIBRATE:
    255                     int key = message.arg1;
    256                     long[] pattern = sVibrationPatterns.get(key);
    257                     if (pattern != null) {
    258                         mVibrator.vibrate(pattern, -1);
    259                     }
    260                     return;
    261                 case MESSAGE_STOP_VIBRATE:
    262                     mVibrator.cancel();
    263                     return;
    264             }
    265         }
    266     };
    267 
    268     /**
    269      * {@link BroadcastReceiver} for receiving updates for our context - device
    270      * state.
    271      */
    272     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
    273         @Override
    274         public void onReceive(Context context, Intent intent) {
    275             String action = intent.getAction();
    276 
    277             if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) {
    278                 int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE,
    279                         AudioManager.RINGER_MODE_NORMAL);
    280                 configureForRingerMode(ringerMode);
    281             } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
    282                 provideScreenStateChangeFeedback(INDEX_SCREEN_ON);
    283             } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
    284                 provideScreenStateChangeFeedback(INDEX_SCREEN_OFF);
    285             } else {
    286                 Log.w(LOG_TAG, "Registered for but not handling action " + action);
    287             }
    288         }
    289 
    290         /**
    291          * Provides feedback to announce the screen state change. Such a change
    292          * is turning the screen on or off.
    293          *
    294          * @param feedbackIndex The index of the feedback in the statically
    295          *            mapped feedback resources.
    296          */
    297         private void provideScreenStateChangeFeedback(int feedbackIndex) {
    298             // We take a specific action depending on the feedback we currently provide.
    299             switch (mProvidedFeedbackType) {
    300                 case AccessibilityServiceInfo.FEEDBACK_SPOKEN:
    301                     String utterance = generateScreenOnOrOffUtternace(feedbackIndex);
    302                     mHandler.obtainMessage(MESSAGE_SPEAK, utterance).sendToTarget();
    303                     return;
    304                 case AccessibilityServiceInfo.FEEDBACK_AUDIBLE:
    305                     mHandler.obtainMessage(MESSAGE_PLAY_EARCON, feedbackIndex, 0).sendToTarget();
    306                     return;
    307                 case AccessibilityServiceInfo.FEEDBACK_HAPTIC:
    308                     mHandler.obtainMessage(MESSAGE_VIBRATE, feedbackIndex, 0).sendToTarget();
    309                     return;
    310                 default:
    311                     throw new IllegalStateException("Unexpected feedback type "
    312                             + mProvidedFeedbackType);
    313             }
    314         }
    315     };
    316 
    317     @Override
    318     public void onServiceConnected() {
    319         if (isInfrastructureInitialized) {
    320             return;
    321         }
    322 
    323         mContext = this;
    324 
    325         // Send a message to start the TTS.
    326         mHandler.sendEmptyMessage(MESSAGE_START_TTS);
    327 
    328         // Get the vibrator service.
    329         mVibrator = (Vibrator) getSystemService(Service.VIBRATOR_SERVICE);
    330 
    331         // Get the AudioManager and configure according the current ring mode.
    332         mAudioManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE);
    333         // In Froyo the broadcast receiver for the ringer mode is called back with the
    334         // current state upon registering but in Eclair this is not done so we poll here.
    335         int ringerMode = mAudioManager.getRingerMode();
    336         configureForRingerMode(ringerMode);
    337 
    338         // We are in an initialized state now.
    339         isInfrastructureInitialized = true;
    340     }
    341 
    342     @Override
    343     public boolean onUnbind(Intent intent) {
    344         if (isInfrastructureInitialized) {
    345             // Stop the TTS service.
    346             mHandler.sendEmptyMessage(MESSAGE_SHUTDOWN_TTS);
    347 
    348             // Unregister the intent broadcast receiver.
    349             if (mBroadcastReceiver != null) {
    350                 unregisterReceiver(mBroadcastReceiver);
    351             }
    352 
    353             // We are not in an initialized state anymore.
    354             isInfrastructureInitialized = false;
    355         }
    356         return false;
    357     }
    358 
    359     /**
    360      * Registers the phone state observing broadcast receiver.
    361      */
    362     private void registerBroadCastReceiver() {
    363         // Create a filter with the broadcast intents we are interested in.
    364         IntentFilter filter = new IntentFilter();
    365         filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
    366         filter.addAction(Intent.ACTION_SCREEN_ON);
    367         filter.addAction(Intent.ACTION_SCREEN_OFF);
    368         // Register for broadcasts of interest.
    369         registerReceiver(mBroadcastReceiver, filter, null, null);
    370     }
    371 
    372     /**
    373      * Generates an utterance for announcing screen on and screen off.
    374      *
    375      * @param feedbackIndex The feedback index for looking up feedback value.
    376      * @return The utterance.
    377      */
    378     private String generateScreenOnOrOffUtternace(int feedbackIndex) {
    379         // Get the announce template.
    380         int resourceId = (feedbackIndex == INDEX_SCREEN_ON) ? R.string.template_screen_on
    381                 : R.string.template_screen_off;
    382         String template = mContext.getString(resourceId);
    383 
    384         // Format the template with the ringer percentage.
    385         int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
    386         int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
    387         int volumePercent = (100 / maxRingerVolume) * currentRingerVolume;
    388 
    389         // Let us round to five so it sounds better.
    390         int adjustment = volumePercent % 10;
    391         if (adjustment < 5) {
    392             volumePercent -= adjustment;
    393         } else if (adjustment > 5) {
    394             volumePercent += (10 - adjustment);
    395         }
    396 
    397         return String.format(template, volumePercent);
    398     }
    399 
    400     /**
    401      * Configures the service according to a ringer mode. Possible
    402      * configurations:
    403      * <p>
    404      *   1. {@link AudioManager#RINGER_MODE_SILENT}<br/>
    405      *   Goal:     Provide only custom haptic feedback.<br/>
    406      *   Approach: Take over the haptic feedback by configuring this service to provide
    407      *             such and do so. This way the system will not call the default haptic
    408      *             feedback service KickBack.<br/>
    409      *             Take over the audible and spoken feedback by configuring this
    410      *             service to provide such feedback but not doing so. This way the system
    411      *             will not call the default spoken feedback service TalkBack and the
    412      *             default audible feedback service SoundBack.
    413      * </p>
    414      * <p>
    415      *   2. {@link AudioManager#RINGER_MODE_VIBRATE}<br/>
    416      *   Goal:     Provide custom audible and default haptic feedback.<br/>
    417      *   Approach: Take over the audible feedback and provide custom one.<br/>
    418      *             Take over the spoken feedback but do not provide such.<br/>
    419      *             Let some other service provide haptic feedback (KickBack).
    420      * </p>
    421      * <p>
    422      *   3. {@link AudioManager#RINGER_MODE_NORMAL}
    423      *   Goal:     Provide custom spoken, default audible and default haptic feedback.<br/>
    424      *   Approach: Take over the spoken feedback and provide custom one.<br/>
    425      *             Let some other services provide audible feedback (SounBack) and haptic
    426      *             feedback (KickBack).
    427      * </p>
    428      *
    429      * @param ringerMode The device ringer mode.
    430      */
    431     private void configureForRingerMode(int ringerMode) {
    432         if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
    433             // When the ringer is silent we want to provide only haptic feedback.
    434             mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_HAPTIC;
    435 
    436             // Take over the spoken and sound feedback so no such feedback is provided.
    437             setServiceInfo(AccessibilityServiceInfo.FEEDBACK_HAPTIC
    438                     | AccessibilityServiceInfo.FEEDBACK_SPOKEN
    439                     | AccessibilityServiceInfo.FEEDBACK_AUDIBLE);
    440 
    441             // Use only an earcon to announce ringer state change.
    442             mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_SILENT, 0).sendToTarget();
    443         } else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
    444             // When the ringer is vibrating we want to provide only audible feedback.
    445             mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_AUDIBLE;
    446 
    447             // Take over the spoken feedback so no spoken feedback is provided.
    448             setServiceInfo(AccessibilityServiceInfo.FEEDBACK_AUDIBLE
    449                     | AccessibilityServiceInfo.FEEDBACK_SPOKEN);
    450 
    451             // Use only an earcon to announce ringer state change.
    452             mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_VIBRATE, 0).sendToTarget();
    453         } else if (ringerMode == AudioManager.RINGER_MODE_NORMAL) {
    454             // When the ringer is ringing we want to provide spoken feedback
    455             // overriding the default spoken feedback.
    456             mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
    457             setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
    458 
    459             // Use only an earcon to announce ringer state change.
    460             mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_NORMAL, 0).sendToTarget();
    461         }
    462     }
    463 
    464     /**
    465      * Sets the {@link AccessibilityServiceInfo} which informs the system how to
    466      * handle this {@link AccessibilityService}.
    467      *
    468      * @param feedbackType The type of feedback this service will provide.
    469      * <p>
    470      *   Note: The feedbackType parameter is an bitwise or of all
    471      *   feedback types this service would like to provide.
    472      * </p>
    473      */
    474     private void setServiceInfo(int feedbackType) {
    475         AccessibilityServiceInfo info = new AccessibilityServiceInfo();
    476         // We are interested in all types of accessibility events.
    477         info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
    478         // We want to provide specific type of feedback.
    479         info.feedbackType = feedbackType;
    480         // We want to receive events in a certain interval.
    481         info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS;
    482         // We want to receive accessibility events only from certain packages.
    483         info.packageNames = PACKAGE_NAMES;
    484         setServiceInfo(info);
    485     }
    486 
    487     @Override
    488     public void onAccessibilityEvent(AccessibilityEvent event) {
    489         Log.i(LOG_TAG, mProvidedFeedbackType + " " + event.toString());
    490 
    491         // Here we act according to the feedback type we are currently providing.
    492         if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) {
    493             mHandler.obtainMessage(MESSAGE_SPEAK, formatUtterance(event)).sendToTarget();
    494         } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) {
    495             mHandler.obtainMessage(MESSAGE_PLAY_EARCON, event.getEventType(), 0).sendToTarget();
    496         } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_HAPTIC) {
    497             mHandler.obtainMessage(MESSAGE_VIBRATE, event.getEventType(), 0).sendToTarget();
    498         } else {
    499             throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType);
    500         }
    501     }
    502 
    503     @Override
    504     public void onInterrupt() {
    505         // Here we act according to the feedback type we are currently providing.
    506         if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) {
    507             mHandler.obtainMessage(MESSAGE_STOP_SPEAK).sendToTarget();
    508         } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) {
    509             mHandler.obtainMessage(MESSAGE_STOP_PLAY_EARCON).sendToTarget();
    510         } else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_HAPTIC) {
    511             mHandler.obtainMessage(MESSAGE_STOP_VIBRATE).sendToTarget();
    512         } else {
    513             throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType);
    514         }
    515     }
    516 
    517     /**
    518      * Formats an utterance from an {@link AccessibilityEvent}.
    519      *
    520      * @param event The event from which to format an utterance.
    521      * @return The formatted utterance.
    522      */
    523     private String formatUtterance(AccessibilityEvent event) {
    524         StringBuilder utterance = mUtterance;
    525 
    526         // Clear the utterance before appending the formatted text.
    527         utterance.setLength(0);
    528 
    529         List<CharSequence> eventText = event.getText();
    530 
    531         // We try to get the event text if such.
    532         if (!eventText.isEmpty()) {
    533             for (CharSequence subText : eventText) {
    534                 // Make 01 pronounced as 1
    535                 if (subText.charAt(0) =='0') {
    536                     subText = subText.subSequence(1, subText.length());
    537                 }
    538                 utterance.append(subText);
    539                 utterance.append(SPACE);
    540             }
    541 
    542             return utterance.toString();
    543         }
    544 
    545         // There is no event text but we try to get the content description which is
    546         // an optional attribute for describing a view (typically used with ImageView).
    547         CharSequence contentDescription = event.getContentDescription();
    548         if (contentDescription != null) {
    549             utterance.append(contentDescription);
    550             return utterance.toString();
    551         }
    552 
    553         return utterance.toString();
    554     }
    555 
    556     /**
    557      * Plays an earcon given its id.
    558      *
    559      * @param earconId The id of the earcon to be played.
    560      */
    561     private void playEarcon(int earconId) {
    562         String earconName = mEarconNames.get(earconId);
    563         if (earconName == null) {
    564             // We do not know the sound id, hence we need to load the sound.
    565             Integer resourceId = sSoundsResourceIds.get(earconId);
    566             if (resourceId != null) {
    567                 earconName = "[" + earconId + "]";
    568                 mTts.addEarcon(earconName, getPackageName(), resourceId);
    569                 mEarconNames.put(earconId, earconName);
    570             }
    571         }
    572 
    573         mTts.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null);
    574     }
    575 }
    576