PageRenderTime 36ms CodeModel.GetById 15ms app.highlight 16ms RepoModel.GetById 1ms app.codeStats 1ms

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

http://eyes-free.googlecode.com/
Java | 431 lines | 196 code | 65 blank | 170 comment | 23 complexity | 1c266b8ef01be4b87ec58ec7f11922c9 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.app.Service;
 22import android.content.BroadcastReceiver;
 23import android.content.Context;
 24import android.content.Intent;
 25import android.content.IntentFilter;
 26import android.media.AudioManager;
 27import android.os.Handler;
 28import android.os.Message;
 29import android.speech.tts.TextToSpeech;
 30import android.util.Log;
 31import android.util.SparseArray;
 32import android.view.accessibility.AccessibilityEvent;
 33
 34import java.util.List;
 35
 36/**
 37 * This class is an {@link AccessibilityService} that provides custom feedback
 38 * for the Clock application that comes by default with Android devices. It
 39 * demonstrates the following key features of the Android accessibility APIs:
 40 * <ol>
 41 *   <li>
 42 *     Simple demonstration of how to use the accessibility APIs.
 43 *   </li>
 44 *   <li>
 45 *     Hands-on example of various ways to utilize the accessibility API for
 46 *     providing alternative and complementary feedback.
 47 *   </li>
 48 *   <li>
 49 *     Providing application specific feedback - the service handles only
 50 *     accessibility events from the clock application.
 51 *   </li>
 52 *   <li>
 53 *     Providing dynamic, context-dependent feedback - feedback type changes
 54 *     depending on the ringer state.</li>
 55 *   <li>
 56 *     Application specific UI enhancement - application domain knowledge is
 57 *     utilized to enhance the provided feedback.
 58 *   </li>
 59 * </ol>
 60 *
 61 * @author svetoslavganov@google.com (Svetoslav R. Ganov)
 62 */
 63public class ClockBackService extends AccessibilityService {
 64
 65    /** Tag for logging from this service */
 66    private static final String LOG_TAG = "ClockBackService";
 67
 68    // fields for configuring how the system handles this accessibility service
 69
 70    /** Minimal timeout between accessibility events we want to receive */
 71    private static final int EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80;
 72
 73    /** Packages we are interested in */
 74    // This works with AlarmClock and Clock whose package name changes in different releases
 75    private static final String[] PACKAGE_NAMES = new String[] {
 76            "com.android.alarmclock", "com.google.android.deskclock", "com.android.deskclock"
 77    };
 78
 79    // message types we are passing around
 80
 81    /** Speak */
 82    private static final int WHAT_SPEAK = 1;
 83
 84    /** Stop speaking */
 85    private static final int WHAT_STOP_SPEAK = 2;
 86
 87    /** Start the TTS service */
 88    private static final int WHAT_START_TTS = 3;
 89
 90    /** Stop the TTS service */
 91    private static final int WHAT_SHUTDOWN_TTS = 4;
 92
 93     //screen state broadcast related constants
 94    
 95    /** Feedback mapping index used as a key for the screen on broadcast */
 96    private static final int INDEX_SCREEN_ON = 0x00000100;
 97
 98    /** Feedback mapping index used as a key for the screen off broadcast */
 99    private static final int INDEX_SCREEN_OFF = 0x00000200;
100
101    // speech related constants
102
103    /**
104     * The queuing mode we are using - interrupt a spoken utterance before
105     * speaking another one
106     */
107    private static final int QUEUING_MODE_INTERRUPT = 2;
108
109    /** The empty string constant */
110    private static final String SPACE = " ";
111
112    /**
113     * The class name of the number picker buttons with no text we want to
114     * announce in the Clock application.
115     */
116    private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK = "android.widget.NumberPickerButton";
117
118    /**
119     * The class name of the number picker buttons with no text we want to
120     * announce in the AlarmClock application.
121     */
122    private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK = "com.android.internal.widget.NumberPickerButton";
123
124    /**
125     * The class name of the edit text box for hours and minutes we want to
126     * better announce
127     */
128    private static final String CLASS_NAME_EDIT_TEXT = "android.widget.EditText";
129
130    /**
131     * Mapping from integer to string resource id where the keys are generated
132     * from the {@link AccessibilityEvent#getItemCount()} and
133     * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
134     */
135    private static final SparseArray<Integer> sPositionMappedStringResourceIds = new SparseArray<Integer>();
136    static {
137        sPositionMappedStringResourceIds.put(11, R.string.value_plus);
138        sPositionMappedStringResourceIds.put(114, R.string.value_plus);
139        sPositionMappedStringResourceIds.put(112, R.string.value_minus);
140        sPositionMappedStringResourceIds.put(116, R.string.value_minus);
141        sPositionMappedStringResourceIds.put(111, R.string.value_hours);
142        sPositionMappedStringResourceIds.put(115, R.string.value_minutes);
143    }
144
145    // auxiliary fields
146
147    /**
148     * Handle to this service to enable inner classes to access the {@link Context}
149     */
150    private Context mContext;
151
152    /** Reusable instance for building utterances */
153    private final StringBuilder mUtterance = new StringBuilder();
154
155    // feedback providing services
156
157    /** The {@link TextToSpeech} used for speaking */
158    private TextToSpeech mTts;
159
160    /** The {@link AudioManager} for detecting ringer state */
161    private AudioManager mAudioManager;
162
163    /** Flag if the infrastructure is initialized */
164    private boolean isInfrastructureInitialized;
165
166    /** {@link Handler} for executing messages on the service main thread */
167    Handler mHandler = new Handler() {
168        @Override
169        public void handleMessage(Message message) {
170            switch (message.what) {
171                case WHAT_SPEAK:
172                    String utterance = (String) message.obj;
173                    mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null);
174                    return;
175                case WHAT_STOP_SPEAK:
176                    mTts.stop();
177                    return;
178                case WHAT_START_TTS:
179                    mTts = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() {
180                        @Override
181                        public void onInit(int status) {
182                            // register here since to add earcons the TTS must be initialized
183                            // the receiver is called immediately with the current ringer mode
184                            registerBroadCastReceiver();
185                        }
186                    });
187                    return;
188                case WHAT_SHUTDOWN_TTS:
189                    mTts.shutdown();
190                    return;
191            }
192        }
193    };
194
195    /**
196     * {@link BroadcastReceiver} for receiving updates for our context - device
197     * state
198     */
199    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
200        @Override
201        public void onReceive(Context context, Intent intent) {
202            String action = intent.getAction();
203
204            if (Intent.ACTION_SCREEN_ON.equals(action)) {
205                provideScreenStateChangeFeedback(INDEX_SCREEN_ON);
206            } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
207                provideScreenStateChangeFeedback(INDEX_SCREEN_OFF);
208            } else {
209                Log.w(LOG_TAG, "Registered for but not handling action " + action);
210            }
211        }
212
213        /**
214         * Provides feedback to announce the screen state change. Such a change
215         * is turning the screen on or off.
216         * 
217         * @param feedbackIndex The index of the feedback in the statically
218         *            mapped feedback resources.
219         */
220        private void provideScreenStateChangeFeedback(int feedbackIndex) {
221            String utterance = generateScreenOnOrOffUtternace(feedbackIndex);
222            mHandler.obtainMessage(WHAT_SPEAK, utterance).sendToTarget();
223        }
224    };
225
226    @Override
227    public void onServiceConnected() {
228        if (isInfrastructureInitialized) {
229            return;
230        }
231
232        mContext = this;
233
234        // send a message to start the TTS
235        mHandler.sendEmptyMessage(WHAT_START_TTS);
236
237        // get the AudioManager and configure according the current ring mode
238        mAudioManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE);
239
240        setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
241
242        // we are in an initialized state now
243        isInfrastructureInitialized = true;
244    }
245
246    @Override
247    public boolean onUnbind(Intent intent) {
248        if (isInfrastructureInitialized) {
249            // stop the TTS service
250            mHandler.sendEmptyMessage(WHAT_SHUTDOWN_TTS);
251
252            // unregister the intent broadcast receiver
253            if (mBroadcastReceiver != null) {
254                unregisterReceiver(mBroadcastReceiver);
255            }
256
257            // we are not in an initialized state anymore
258            isInfrastructureInitialized = false;
259        }
260        return false;
261    }
262
263    /**
264     * Registers the phone state observing broadcast receiver.
265     */
266    private void registerBroadCastReceiver() {
267      //Create a filter with the broadcast intents we are interested in
268        IntentFilter filter = new IntentFilter();
269        filter.addAction(Intent.ACTION_SCREEN_ON);
270        filter.addAction(Intent.ACTION_SCREEN_OFF);
271        // register for broadcasts of interest
272        registerReceiver(mBroadcastReceiver, filter, null, null);
273    }
274
275    /**
276     * Generates an utterance for announcing screen on and screen off.
277     * 
278     * @param feedbackIndex The feedback index for looking up feedback value.
279     * @return The utterance.
280     */
281    private String generateScreenOnOrOffUtternace(int feedbackIndex) {
282        // get the announce template
283        int resourceId = (feedbackIndex == INDEX_SCREEN_ON) ? R.string.template_screen_on
284                : R.string.template_screen_off;
285        String template = mContext.getString(resourceId);
286
287        // format the template with the ringer percentage
288        int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
289        int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
290        int volumePercent = (100 / maxRingerVolume) * currentRingerVolume;
291
292        // let us round to five so it sounds better
293        int adjustment = volumePercent % 10;
294        if (adjustment < 5) {
295            volumePercent -= adjustment;
296        } else if (adjustment > 5) {
297            volumePercent += (10 - adjustment);
298        }
299
300        return String.format(template, volumePercent);
301    }
302    
303    /**
304     * Sets the {@link AccessibilityServiceInfo} which informs the system how to
305     * handle this {@link AccessibilityService}.
306     * 
307     * @param feedbackType The type of feedback this service will provide. </p>
308     *            Note: The feedbackType parameter is an bitwise or of all
309     *            feedback types this service would like to provide.
310     */
311    private void setServiceInfo(int feedbackType) {
312        AccessibilityServiceInfo info = new AccessibilityServiceInfo();
313        // we are interested in all types of accessibility events
314        info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
315        // we want to provide specific type of feedback
316        info.feedbackType = feedbackType;
317        // we want to receive events in a certain interval
318        info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS;
319        // we want to receive accessibility events only from certain packages
320        info.packageNames = PACKAGE_NAMES;
321        setServiceInfo(info);
322    }
323
324    @Override
325    public void onAccessibilityEvent(AccessibilityEvent event) {
326        Log.i(LOG_TAG, event.toString());
327
328        mHandler.obtainMessage(WHAT_SPEAK, formatUtterance(event)).sendToTarget();
329    }
330
331    @Override
332    public void onInterrupt() {
333        mHandler.obtainMessage(WHAT_STOP_SPEAK);
334    }
335
336    /**
337     * Formats an utterance from an {@link AccessibilityEvent}.
338     *
339     * @param event The event from which to format an utterance.
340     * @return The formatted utterance.
341     */
342    private String formatUtterance(AccessibilityEvent event) {
343        StringBuilder utterance = mUtterance;
344
345        // clear the utterance before appending the formatted text
346        utterance.delete(0, utterance.length());
347
348        List<CharSequence> eventText = event.getText();
349
350        // We try to get the event text if such
351        if (!eventText.isEmpty()) {
352            for (CharSequence subText : eventText) {
353                utterance.append(subText);
354                utterance.append(SPACE);
355            }
356
357            // here we do a bit of enhancement of the UI presentation by using the semantic
358            // of the event source in the context of the Clock application
359            if (CLASS_NAME_EDIT_TEXT.equals(event.getClassName())) {
360                // if the source is an edit text box and we have a mapping based on
361                // its position in the items of the container parent of the event source
362                // we append that value as well. We say "XX hours" and "XX minutes".
363                String resourceValue = getPositionMappedStringResource(event.getItemCount(),
364                        event.getCurrentItemIndex());
365                if (resourceValue != null) {
366                    utterance.append(resourceValue);
367                }
368            }
369
370            return utterance.toString();
371        }
372
373        // There is no event text but we try to get the content description which is
374        // an optional attribute for describing a view (typically used with ImageView)
375        CharSequence contentDescription = event.getContentDescription();
376        if (contentDescription != null) {
377            utterance.append(contentDescription);
378            return utterance.toString();
379        }
380
381        // No text and content description for the plus and minus buttons, so we lookup
382        // custom values based on the event's itemCount and currentItemIndex properties.
383        CharSequence className = event.getClassName();
384        if (CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK.equals(className)
385                || CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK.equals(className)) {
386            String resourceValue = getPositionMappedStringResource(event.getItemCount(),
387                    event.getCurrentItemIndex());
388            utterance.append(resourceValue);
389        }
390
391        return utterance.toString();
392    }
393
394    /**
395     * Returns a string resource mapped for a given position based on
396     * {@link AccessibilityEvent#getItemCount()} and
397     * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
398     * 
399     * @param itemCount The value of {@link AccessibilityEvent#getItemCount()}.
400     * @param currentItemIndex The value of
401     *            {@link AccessibilityEvent#getCurrentItemIndex()}.
402     * @return The mapped string if such exists, null otherwise.
403     */
404    private String getPositionMappedStringResource(int itemCount, int currentItemIndex) {
405        int lookupIndex = computeLookupIndex(itemCount, currentItemIndex);
406        int resourceId = sPositionMappedStringResourceIds.get(lookupIndex);
407        return getString(resourceId);
408    }
409
410    /**
411     * Computes an index for looking up the custom text for views with neither
412     * text not content description. The index is computed based on
413     * {@link AccessibilityEvent#getItemCount()} and
414     * {@link AccessibilityEvent#getCurrentItemIndex()} properties.
415     * 
416     * @param itemCount The number of all items in the event source.
417     * @param currentItemIndex The index of the item source of the event.
418     * @return The lookup index.
419     */
420    private int computeLookupIndex(int itemCount, int currentItemIndex) {
421        int lookupIndex = itemCount;
422        int divided = currentItemIndex;
423
424        while (divided > 0) {
425            lookupIndex *= 10;
426            divided /= 10;
427        }
428
429        return (lookupIndex += currentItemIndex);
430    }
431}