• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, new long[] { 0L, 100L })203         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, new long[] {
204                 0L, 100L
205         });
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new long[] { 0L, 100L })206         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new long[] {
207                 0L, 100L
208         });
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, new long[] { 0L, 15L, 10L, 15L })209         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, new long[] {
210                 0L, 15L, 10L, 15L
211         });
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, new long[] { 0L, 15L, 10L, 15L })212         sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, new long[] {
213                 0L, 15L, 10L, 15L
214         });
sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, new long[] { 0L, 25L, 50L, 25L, 50L, 25L })215         sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, new long[] {
216                 0L, 25L, 50L, 25L, 50L, 25L
217         });
sVibrationPatterns.put(INDEX_SCREEN_ON, new long[] { 0L, 10L, 10L, 20L, 20L, 30L })218         sVibrationPatterns.put(INDEX_SCREEN_ON, new long[] {
219                 0L, 10L, 10L, 20L, 20L, 30L
220         });
sVibrationPatterns.put(INDEX_SCREEN_OFF, new long[] { 0L, 30L, 20L, 20L, 10L, 10L })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 {
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_CLICKED, R.raw.sound_view_clicked)229         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_CLICKED, R.raw.sound_view_clicked);
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, R.raw.sound_view_clicked)230         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, R.raw.sound_view_clicked);
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED, R.raw.sound_view_focused_or_selected)231         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED, R.raw.sound_view_focused_or_selected);
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, R.raw.sound_view_focused_or_selected)232         sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, R.raw.sound_view_focused_or_selected);
sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, R.raw.sound_window_state_changed)233         sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, R.raw.sound_window_state_changed);
sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound_screen_on)234         sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound_screen_on);
sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound_screen_off)235         sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound_screen_off);
sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound_ringer_silent)236         sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound_ringer_silent);
sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound_ringer_vibrate)237         sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound_ringer_vibrate);
sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound_ringer_normal)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
onServiceConnected()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
onUnbind(Intent intent)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      */
registerBroadCastReceiver()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      */
generateScreenOnOrOffUtternace(int feedbackIndex)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      */
configureForRingerMode(int ringerMode)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      */
setServiceInfo(int feedbackType)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
onAccessibilityEvent(AccessibilityEvent event)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
onInterrupt()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      */
formatUtterance(AccessibilityEvent event)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      */
getEventDataMappedStringResource(AccessibilityEvent event)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      */
computeLookupIndex(AccessibilityEvent event)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      */
playEarcon(int earconId)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