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 {
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, new long[] { 0L, 100L })143         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, new long[] {
144                 0L, 100L
145         });
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new long[] { 0L, 100L })146         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new long[] {
147                 0L, 100L
148         });
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, new long[] { 0L, 15L, 10L, 15L })149         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, new long[] {
150                 0L, 15L, 10L, 15L
151         });
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, new long[] { 0L, 15L, 10L, 15L })152         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, new long[] {
153                 0L, 15L, 10L, 15L
154         });
sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, new long[] { 0L, 25L, 50L, 25L, 50L, 25L })155         sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, new long[] {
156                 0L, 25L, 50L, 25L, 50L, 25L
157         });
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, new long[] { 0L, 15L, 10L, 15L, 15L, 10L })158         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, new long[] {
159                 0L, 15L, 10L, 15L, 15L, 10L
160         });
sVibrationPatterns.put(INDEX_SCREEN_ON, new long[] { 0L, 10L, 10L, 20L, 20L, 30L })161         sVibrationPatterns.put(INDEX_SCREEN_ON, new long[] {
162                 0L, 10L, 10L, 20L, 20L, 30L
163         });
sVibrationPatterns.put(INDEX_SCREEN_OFF, new long[] { 0L, 30L, 20L, 20L, 10L, 10L })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 {
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_CLICKED, R.raw.sound_view_clicked)172         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_CLICKED,
173                 R.raw.sound_view_clicked);
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, R.raw.sound_view_clicked)174         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED,
175                 R.raw.sound_view_clicked);
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED, R.raw.sound_view_focused_or_selected)176         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED,
177                 R.raw.sound_view_focused_or_selected);
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, R.raw.sound_view_focused_or_selected)178         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED,
179                 R.raw.sound_view_focused_or_selected);
sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, R.raw.sound_window_state_changed)180         sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
181                 R.raw.sound_window_state_changed);
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, R.raw.sound_view_hover_enter)182         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
183                 R.raw.sound_view_hover_enter);
sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound_screen_on)184         sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound_screen_on);
sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound_screen_off)185         sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound_screen_off);
sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound_ringer_silent)186         sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound_ringer_silent);
sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound_ringer_vibrate)187         sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound_ringer_vibrate);
sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound_ringer_normal)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
onServiceConnected()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
onUnbind(Intent intent)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      */
registerBroadCastReceiver()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      */
generateScreenOnOrOffUtternace(int feedbackIndex)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      */
configureForRingerMode(int ringerMode)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      */
setServiceInfo(int feedbackType)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
onAccessibilityEvent(AccessibilityEvent event)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
onInterrupt()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      */
formatUtterance(AccessibilityEvent event)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      */
playEarcon(int earconId)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