PageRenderTime 46ms CodeModel.GetById 8ms app.highlight 32ms RepoModel.GetById 1ms app.codeStats 0ms

/documentation/ClockBackTutorial/ClockBack1/src/com/google/android/marvin/clockback/ClockBackService.java

http://eyes-free.googlecode.com/
Java | 330 lines | 142 code | 51 blank | 137 comment | 13 complexity | b177728111f06d8da2955ea3d2dda914 MD5 | raw file
  1/*
  2 * Copyright (C) 2010 The Android Open Source Project
  3 *
  4 * Licensed under the Apache License, Version 2.0 (the "License");
  5 * you may not use this file except in compliance with the License.
  6 * You may obtain a copy of the License at
  7 *
  8 *      http://www.apache.org/licenses/LICENSE-2.0
  9 *
 10 * Unless required by applicable law or agreed to in writing, software
 11 * distributed under the License is distributed on an "AS IS" BASIS,
 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 * See the License for the specific language governing permissions and
 14 * limitations under the License.
 15 */
 16
 17package com.google.android.marvin.clockback;
 18
 19import android.accessibilityservice.AccessibilityService;
 20import android.accessibilityservice.AccessibilityServiceInfo;
 21import android.content.Context;
 22import android.content.Intent;
 23import android.os.Handler;
 24import android.os.Message;
 25import android.speech.tts.TextToSpeech;
 26import android.util.Log;
 27import android.util.SparseArray;
 28import android.view.accessibility.AccessibilityEvent;
 29
 30import java.util.List;
 31
 32/**
 33 * This class is an {@link AccessibilityService} that provides custom feedback
 34 * for the Clock application that comes by default with Android devices. It
 35 * demonstrates the following key features of the Android accessibility APIs:
 36 * <ol>
 37 *   <li>
 38 *     Simple demonstration of how to use the accessibility APIs.
 39 *   </li>
 40 *   <li>
 41 *     Hands-on example of various ways to utilize the accessibility API for
 42 *     providing alternative and complementary feedback.
 43 *   </li>
 44 *   <li>
 45 *     Providing application specific feedback - the service handles only
 46 *     accessibility events from the clock application.
 47 *   </li>
 48 *   <li>
 49 *     Providing dynamic, context-dependent feedback - feedback type changes
 50 *     depending on the ringer state.</li>
 51 *   <li>
 52 *     Application specific UI enhancement - application domain knowledge is
 53 *     utilized to enhance the provided feedback.
 54 *   </li>
 55 * </ol>
 56 *
 57 * @author svetoslavganov@google.com (Svetoslav R. Ganov)
 58 */
 59public class ClockBackService extends AccessibilityService {
 60
 61    /** Tag for logging from this service */
 62    private static final String LOG_TAG = "ClockBackService";
 63
 64    // fields for configuring how the system handles this accessibility service
 65
 66    /** Minimal timeout between accessibility events we want to receive */
 67    private static final int EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80;
 68
 69    /** Packages we are interested in */
 70    // This works with AlarmClock and Clock whose package name changes in different releases
 71    private static final String[] PACKAGE_NAMES = new String[] {
 72            "com.android.alarmclock", "com.google.android.deskclock", "com.android.deskclock"
 73    };
 74
 75    // message types we are passing around
 76
 77    /** Speak */
 78    private static final int WHAT_SPEAK = 1;
 79
 80    /** Stop speaking */
 81    private static final int WHAT_STOP_SPEAK = 2;
 82
 83    /** Start the TTS service */
 84    private static final int WHAT_START_TTS = 3;
 85
 86    /** Stop the TTS service */
 87    private static final int WHAT_SHUTDOWN_TTS = 4;
 88
 89    // speech related constants
 90
 91    /**
 92     * The queuing mode we are using - interrupt a spoken utterance before
 93     * speaking another one
 94     */
 95    private static final int QUEUING_MODE_INTERRUPT = 2;
 96
 97    /** The empty string constant */
 98    private static final String SPACE = " ";
 99
100    /**
101     * The class name of the number picker buttons with no text we want to
102     * announce in the Clock application.
103     */
104    private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK = "android.widget.NumberPickerButton";
105
106    /**
107     * The class name of the number picker buttons with no text we want to
108     * announce in the AlarmClock application.
109     */
110    private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK = "com.android.internal.widget.NumberPickerButton";
111
112    /**
113     * The class name of the edit text box for hours and minutes we want to
114     * better announce
115     */
116    private static final String CLASS_NAME_EDIT_TEXT = "android.widget.EditText";
117
118    /**
119     * Mapping from integer to string resource id where the keys are generated
120     * from the {@link AccessibilityEvent#getItemCount()} and
121     * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
122     */
123    private static final SparseArray<Integer> sPositionMappedStringResourceIds = new SparseArray<Integer>();
124    static {
125        sPositionMappedStringResourceIds.put(11, R.string.value_plus);
126        sPositionMappedStringResourceIds.put(114, R.string.value_plus);
127        sPositionMappedStringResourceIds.put(112, R.string.value_minus);
128        sPositionMappedStringResourceIds.put(116, R.string.value_minus);
129        sPositionMappedStringResourceIds.put(111, R.string.value_hours);
130        sPositionMappedStringResourceIds.put(115, R.string.value_minutes);
131    }
132
133    // auxiliary fields
134
135    /**
136     * Handle to this service to enable inner classes to access the {@link Context}
137     */
138    private Context mContext;
139
140    /** Reusable instance for building utterances */
141    private final StringBuilder mUtterance = new StringBuilder();
142
143    // feedback providing services
144
145    /** The {@link TextToSpeech} used for speaking */
146    private TextToSpeech mTts;
147
148    /** Flag if the infrastructure is initialized */
149    private boolean isInfrastructureInitialized;
150
151    /** {@link Handler} for executing messages on the service main thread */
152    Handler mHandler = new Handler() {
153        @Override
154        public void handleMessage(Message message) {
155            switch (message.what) {
156                case WHAT_SPEAK:
157                    String utterance = (String) message.obj;
158                    mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null);
159                    return;
160                case WHAT_STOP_SPEAK:
161                    mTts.stop();
162                    return;
163                case WHAT_START_TTS:
164                    mTts = new TextToSpeech(mContext, null);
165                    return;
166                case WHAT_SHUTDOWN_TTS:
167                    mTts.shutdown();
168                    return;
169            }
170        }
171    };
172    
173    @Override
174    public void onServiceConnected() {
175        if (isInfrastructureInitialized) {
176            return;
177        }
178
179        mContext = this;
180
181        // send a message to start the TTS
182        mHandler.sendEmptyMessage(WHAT_START_TTS);
183
184        setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
185
186        // we are in an initialized state now
187        isInfrastructureInitialized = true;
188    }
189
190    @Override
191    public boolean onUnbind(Intent intent) {
192        if (isInfrastructureInitialized) {
193            // stop the TTS service
194            mHandler.sendEmptyMessage(WHAT_SHUTDOWN_TTS);
195
196            // we are not in an initialized state anymore
197            isInfrastructureInitialized = false;
198        }
199        return false;
200    }
201    
202    /**
203     * Sets the {@link AccessibilityServiceInfo} which informs the system how to
204     * handle this {@link AccessibilityService}.
205     * 
206     * @param feedbackType The type of feedback this service will provide. </p>
207     *            Note: The feedbackType parameter is an bitwise or of all
208     *            feedback types this service would like to provide.
209     */
210    private void setServiceInfo(int feedbackType) {
211        AccessibilityServiceInfo info = new AccessibilityServiceInfo();
212        // we are interested in all types of accessibility events
213        info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
214        // we want to provide specific type of feedback
215        info.feedbackType = feedbackType;
216        // we want to receive events in a certain interval
217        info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS;
218        // we want to receive accessibility events only from certain packages
219        info.packageNames = PACKAGE_NAMES;
220        setServiceInfo(info);
221    }
222
223    @Override
224    public void onAccessibilityEvent(AccessibilityEvent event) {
225        Log.i(LOG_TAG, event.toString());
226
227        mHandler.obtainMessage(WHAT_SPEAK, formatUtterance(event)).sendToTarget();
228    }
229
230    @Override
231    public void onInterrupt() {
232        mHandler.obtainMessage(WHAT_STOP_SPEAK);
233    }
234
235    /**
236     * Formats an utterance from an {@link AccessibilityEvent}.
237     *
238     * @param event The event from which to format an utterance.
239     * @return The formatted utterance.
240     */
241    private String formatUtterance(AccessibilityEvent event) {
242        StringBuilder utterance = mUtterance;
243
244        // clear the utterance before appending the formatted text
245        utterance.delete(0, utterance.length());
246
247        List<CharSequence> eventText = event.getText();
248
249        // We try to get the event text if such
250        if (!eventText.isEmpty()) {
251            for (CharSequence subText : eventText) {
252                utterance.append(subText);
253                utterance.append(SPACE);
254            }
255
256            // here we do a bit of enhancement of the UI presentation by using the semantic
257            // of the event source in the context of the Clock application
258            if (CLASS_NAME_EDIT_TEXT.equals(event.getClassName())) {
259                // if the source is an edit text box and we have a mapping based on
260                // its position in the items of the container parent of the event source
261                // we append that value as well. We say "XX hours" and "XX minutes".
262                String resourceValue = getPositionMappedStringResource(event.getItemCount(),
263                        event.getCurrentItemIndex());
264                if (resourceValue != null) {
265                    utterance.append(resourceValue);
266                }
267            }
268
269            return utterance.toString();
270        }
271
272        // There is no event text but we try to get the content description which is
273        // an optional attribute for describing a view (typically used with ImageView)
274        CharSequence contentDescription = event.getContentDescription();
275        if (contentDescription != null) {
276            utterance.append(contentDescription);
277            return utterance.toString();
278        }
279
280        // No text and content description for the plus and minus buttons, so we lookup
281        // custom values based on the event's itemCount and currentItemIndex properties.
282        CharSequence className = event.getClassName();
283        if (CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK.equals(className)
284                || CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK.equals(className)) {
285            String resourceValue = getPositionMappedStringResource(event.getItemCount(),
286                    event.getCurrentItemIndex());
287            utterance.append(resourceValue);
288        }
289
290        return utterance.toString();
291    }
292
293    /**
294     * Returns a string resource mapped for a given position based on
295     * {@link AccessibilityEvent#getItemCount()} and
296     * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
297     * 
298     * @param itemCount The value of {@link AccessibilityEvent#getItemCount()}.
299     * @param currentItemIndex The value of
300     *            {@link AccessibilityEvent#getCurrentItemIndex()}.
301     * @return The mapped string if such exists, null otherwise.
302     */
303    private String getPositionMappedStringResource(int itemCount, int currentItemIndex) {
304        int lookupIndex = computeLookupIndex(itemCount, currentItemIndex);
305        int resourceId = sPositionMappedStringResourceIds.get(lookupIndex);
306        return getString(resourceId);
307    }
308
309    /**
310     * Computes an index for looking up the custom text for views with neither
311     * text not content description. The index is computed based on
312     * {@link AccessibilityEvent#getItemCount()} and
313     * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
314     * 
315     * @param itemCount The number of all items in the event source.
316     * @param currentItemIndex The index of the item source of the event.
317     * @return The lookup index.
318     */
319    private int computeLookupIndex(int itemCount, int currentItemIndex) {
320        int lookupIndex = itemCount;
321        int divided = currentItemIndex;
322
323        while (divided > 0) {
324            lookupIndex *= 10;
325            divided /= 10;
326        }
327
328        return (lookupIndex += currentItemIndex);
329    }
330}