PageRenderTime 41ms CodeModel.GetById 15ms app.highlight 20ms RepoModel.GetById 0ms app.codeStats 1ms

/ime/aimelib/src/com/google/android/marvin/aime/AccessibleInputMethodService.java

http://eyes-free.googlecode.com/
Java | 647 lines | 329 code | 93 blank | 225 comment | 73 complexity | fd6d76123e3ae073e63408e650f8ebe3 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.aime;
 18
 19import com.google.android.marvin.aime.usercommands.UserCommandHandler;
 20
 21import android.content.res.Configuration;
 22import android.content.res.Resources;
 23import android.database.ContentObserver;
 24import android.inputmethodservice.InputMethodService;
 25import android.os.Handler;
 26import android.provider.Settings;
 27import android.view.KeyEvent;
 28import android.view.MotionEvent;
 29import android.view.accessibility.AccessibilityManager;
 30import android.view.inputmethod.EditorInfo;
 31import android.view.inputmethod.InputConnection;
 32
 33import java.util.Locale;
 34
 35/**
 36 * Accessible InputMethodService. Provides handle to
 37 * {@link AccessibleInputConnection}, for powerful navigation capabilities. It
 38 * also fires accessibility events. Extend this class to make your IME
 39 * accessible. It overrides default behaviour of trackball and dpad for
 40 * improving accessibility.<br>
 41 * <br>
 42 * Call {@link #setGranularity(int)} and {@link #setAction(int)}, everytime
 43 * current granularity or action of IME changes, to reflect change in trackball
 44 * and dpad behavior.<br>
 45 * Default <code>granularity</code> is {@link TextNavigation#GRANULARITY_CHAR}
 46 * and default <code>action</code> is {@link TextNavigation#ACTION_MOVE} for
 47 * trackball and dpad motion.
 48 *
 49 * @author hiteshk@google.com (Hitesh Khandelwal)
 50 * @author alanv@google.com (Alan Viverette)
 51 */
 52public abstract class AccessibleInputMethodService extends InputMethodService {
 53    /** Whether the trackball can be used to control granularity. */
 54    private static final boolean ENABLE_TRACKBALL = false;
 55
 56    /** List of characters ignored by word iterator. */
 57    private final char[] ignoredCharForWords = {
 58        ' '
 59    };
 60
 61    /** String to speak when granularity changes. */
 62    private String mGranularitySet;
 63
 64    /** String to speak when ALT key is pressed. */
 65    private String mAltString;
 66
 67    /** String to speak when SHIFT key is pressed. */
 68    private String mShiftString;
 69
 70    /** Strings used to describe granularity changes. */
 71    private String[] mGranularityTypes;
 72
 73    /** String to speak when action changes. */
 74    private String mActionSet;
 75
 76    /** Strings used to describe action changes. */
 77    private String[] mActionTypes;
 78
 79    /** Current granularity (unit type). */
 80    private int mGranularity = TextNavigation.GRANULARITY_CHAR;
 81
 82    /** Current action set. */
 83    private int mAction = TextNavigation.ACTION_MOVE;
 84
 85    /** Handle to AccessibleInputConnection. */
 86    private AccessibleInputConnection mAIC = null;
 87
 88    /** Handle to base InputConnection. */
 89    private InputConnection mIC = null;
 90
 91    /** Stored key down event. */
 92    private KeyEvent mPreviousDpadDownEvent;
 93
 94    /** Stored meta key down event. */
 95    private KeyEvent mPreviousMetaDownEvent;
 96
 97    /** Whether accessibility is enabled. */
 98    private boolean mAccessibilityEnabled;
 99
100    /** Whether KEYCODE_UP or KEYCODE_DOWN was just pressed. */
101    private boolean mWasUpDownPressed;
102
103    private UserCommandHandler mUserCommandHandler;
104    private AccessibilityManager mAccessibilityManager;
105
106    @Override
107    public void onCreate() {
108        super.onCreate();
109
110        final Resources res = getResources();
111        mGranularityTypes = res.getStringArray(R.array.granularity_types);
112        mGranularitySet = res.getString(R.string.set_granularity);
113        mActionTypes = res.getStringArray(R.array.action_types);
114        mActionSet = res.getString(R.string.set_action);
115        mAltString = res.getString(R.string.alt_pressed);
116        mShiftString = res.getString(R.string.shift_pressed);
117
118        mPreviousDpadDownEvent = null;
119        mWasUpDownPressed = false;
120
121        mUserCommandHandler = new UserCommandHandler(this);
122        mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
123
124        // Register content observer to receive accessibility status changes.
125        getContentResolver().registerContentObserver(
126                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_ENABLED), false,
127                mAccessibilityObserver);
128
129        updateAccessibilityState();
130    }
131
132    @Override
133    public void onDestroy() {
134        super.onDestroy();
135
136        mUserCommandHandler.release();
137    }
138
139    /**
140     * Returns an {@link AccessibleInputConnection} bound to the current
141     * {@link InputConnection}.
142     *
143     * @return an instance of AccessibleInputConnection
144     */
145    @Override
146    public AccessibleInputConnection getCurrentInputConnection() {
147        InputConnection currentIC = super.getCurrentInputConnection();
148
149        if (currentIC == null) {
150            mAIC = null;
151            return null;
152        }
153
154        if (mAIC == null || (mIC != null && mIC != currentIC)) {
155            mIC = currentIC;
156            mAIC = new AccessibleInputConnection(this, mIC, true, ignoredCharForWords);
157        }
158
159        return mAIC;
160    }
161
162    @Override
163    public void onStartInputView(EditorInfo info, boolean restarting) {
164        super.onStartInputView(info, restarting);
165
166        if (mAccessibilityManager.isEnabled()) {
167            mAccessibilityManager.interrupt();
168        }
169    }
170
171    @Override
172    public void onFinishInput() {
173        super.onFinishInput();
174
175        // Reset state when leaving input field.
176        mWasUpDownPressed = false;
177    }
178
179    /**
180     * Overrides default trackball behavior:
181     * <ul>
182     * <li>Up/down: Increases/decreases text navigation granularity</li>
183     * <li>Left/right: Moves to previous/next unit of text</li>
184     * </ul>
185     * <br>
186     * Moving the trackball far to the left or right results in moving by
187     * multiple units.
188     * <p>
189     * If one of the following conditions is met, default behavior is preserved:
190     * <ul>
191     * <li>No input connection available</li>
192     * <li>Input view is hidden</li>
193     * <li>Not currently editing text</li>
194     * <li>Cannot move in the specified direction</li>
195     * </ul>
196     * </p>
197     */
198    @SuppressWarnings("unused")
199    @Override
200    public boolean onTrackballEvent(MotionEvent event) {
201        if (!ENABLE_TRACKBALL)
202            return false;
203
204        AccessibleInputConnection aic = getCurrentInputConnection();
205        if (aic == null || !isInputViewShown()) {
206            return super.onTrackballEvent(event);
207        }
208
209        float x = event.getX();
210        float absX = Math.abs(event.getX());
211        float y = event.getY();
212        float absY = Math.abs(event.getY());
213
214        if (absY > 2 * absX && absY >= 0.75) {
215            // Up and down switch granularities, but this is less common so it's
216            // less sensitive.
217
218            if (y < 0) {
219                adjustGranularity(1);
220            } else {
221                adjustGranularity(-1);
222            }
223        } else {
224            // If they moved the trackball really far, move by more than one but
225            // only announce for the last move.
226
227            int count = Math.max(1, (int) Math.floor(absX + 0.25));
228            boolean isNext = (x > 0);
229            boolean isShiftPressed = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0;
230
231            moveUnit(mGranularity, count, isNext, isShiftPressed);
232        }
233
234        return true;
235    }
236
237    /**
238     * Overrides default directional pad behavior:
239     * <ul>
240     * <li>Up/down: Increases/decreases text navigation granularity</li>
241     * <li>Left/right: Moves to previous/next unit of text</li>
242     * </ul>
243     * <p>
244     * If one of the following conditions is met, default behavior is preserved:
245     * <ul>
246     * <li>No input connection available</li>
247     * <li>Input view is hidden</li>
248     * <li>Not currently editing text</li>
249     * <li>Cannot move in the specified direction</li>
250     * </ul>
251     * </p>
252     */
253    @Override
254    public boolean onKeyUp(int keyCode, KeyEvent event) {
255        if (mUserCommandHandler.onKeyUp(event)) {
256            return true;
257        }
258
259        final AccessibleInputConnection aic = getCurrentInputConnection();
260        if (aic == null || !aic.hasExtractedText()) {
261            return super.onKeyUp(keyCode, event);
262        }
263
264        final KeyEvent downEvent = mPreviousDpadDownEvent;
265        mPreviousDpadDownEvent = null;
266
267        final KeyEvent metaDownEvent = mPreviousMetaDownEvent;
268        mPreviousMetaDownEvent = null;
269
270        if (downEvent != null) {
271            boolean captureEvent = false;
272
273            switch (downEvent.getKeyCode()) {
274                case KeyEvent.KEYCODE_DPAD_LEFT:
275                    if (!event.isAltPressed()) {
276                        captureEvent = previousUnit(mGranularity, 1, event.isShiftPressed());
277                    } else {
278                        mWasUpDownPressed = true;
279                    }
280                    break;
281                case KeyEvent.KEYCODE_DPAD_RIGHT:
282                    if (!event.isAltPressed()) {
283                        captureEvent = nextUnit(mGranularity, 1, event.isShiftPressed());
284                    } else {
285                        mWasUpDownPressed = true;
286                    }
287                    break;
288                case KeyEvent.KEYCODE_DPAD_UP:
289                    if (event.isAltPressed()) {
290                        adjustGranularity(1);
291                        captureEvent = true;
292                    } else {
293                        mWasUpDownPressed = true;
294                    }
295                    break;
296                case KeyEvent.KEYCODE_DPAD_DOWN:
297                    if (event.isAltPressed()) {
298                        adjustGranularity(-1);
299                        captureEvent = true;
300                    } else {
301                        mWasUpDownPressed = true;
302                    }
303                    break;
304            }
305
306            if (captureEvent) {
307                return true;
308            }
309        }
310
311        // If we didn't capture the meta event, attempt to send the previous
312        // meta down event and then preserve default behavior.
313        if (metaDownEvent != null) {
314            if (!super.onKeyDown(metaDownEvent.getKeyCode(), metaDownEvent)) {
315                aic.sendKeyEvent(metaDownEvent);
316            }
317        }
318
319        // If we didn't capture the event, attempt to send the previous down
320        // event and then preserve default behavior.
321        if (downEvent != null) {
322            if (!super.onKeyDown(downEvent.getKeyCode(), downEvent)) {
323                aic.sendKeyEvent(downEvent);
324            }
325        }
326
327        if (!super.onKeyUp(keyCode, event)) {
328            aic.sendKeyEvent(event);
329        }
330
331        return true;
332    }
333
334    /**
335     * Captures and stores directional pad events. If onKeyUp() preserves
336     * default behavior, the original down event will be released.
337     */
338    @Override
339    public boolean onKeyDown(int keyCode, KeyEvent event) {
340        if (mUserCommandHandler.onKeyDown(event)) {
341            return true;
342        }
343
344        AccessibleInputConnection aic = getCurrentInputConnection();
345        if (aic == null || !aic.hasExtractedText()) {
346            return super.onKeyDown(keyCode, event);
347        }
348
349        // If we've captured a meta key, capture all subsequent keys.
350        if (mPreviousMetaDownEvent != null) {
351            mPreviousDpadDownEvent = event;
352            return true;
353        }
354
355        switch (event.getKeyCode()) {
356            case KeyEvent.KEYCODE_DPAD_DOWN:
357            case KeyEvent.KEYCODE_DPAD_UP:
358            case KeyEvent.KEYCODE_DPAD_LEFT:
359            case KeyEvent.KEYCODE_DPAD_RIGHT:
360                mPreviousDpadDownEvent = event;
361                return true;
362            case KeyEvent.KEYCODE_ALT_LEFT:
363            case KeyEvent.KEYCODE_ALT_RIGHT: {
364                mAIC.trySendAccessiblityEvent(mAltString);
365                mPreviousMetaDownEvent = event;
366                return true;
367            }
368            case KeyEvent.KEYCODE_SHIFT_LEFT:
369            case KeyEvent.KEYCODE_SHIFT_RIGHT: {
370                mAIC.trySendAccessiblityEvent(mShiftString);
371                mPreviousMetaDownEvent = event;
372                return true;
373            }
374            default:
375                return super.onKeyDown(keyCode, event);
376        }
377    }
378
379    /**
380     * Moves forward <code>count</code> units using the current granularity and
381     * action. Returns <code>true</code> if successful. Moving can fail if the
382     * carat is already at the end of the text or if there is no available input
383     * connection.
384     *
385     * @param granularity The granularity with which to move.
386     * @param count The number of units to move.
387     * @param isShiftPressed <code>true</code> if the shift key is pressed.
388     * @return <code>true</code> if successful.
389     * @see AccessibleInputMethodService#setGranularity(int)
390     * @see AccessibleInputMethodService#setAction(int)
391     */
392    protected boolean nextUnit(int granularity, int count, boolean isShiftPressed) {
393        return moveUnit(granularity, count, true, isShiftPressed);
394    }
395
396    /**
397     * Moves backward <code>count</code> units using the current granularity and
398     * action. Returns <code>true</code> if successful. Moving can fail if the
399     * carat is already at the beginning of the text or if there is no available
400     * input connection.
401     *
402     * @param granularity The granularity with which to move.
403     * @param count The number of units to move.
404     * @param isShiftPressed <code>true</code> if the shift key is pressed.
405     * @return <code>true</code> if successful.
406     * @see AccessibleInputMethodService#setGranularity(int)
407     * @see AccessibleInputMethodService#setAction(int)
408     */
409    protected boolean previousUnit(int granularity, int count, boolean isShiftPressed) {
410        return moveUnit(granularity, count, false, isShiftPressed);
411    }
412
413    /**
414     * Moves <code>count</code> units in the specified direction using the
415     * current granularity and action.
416     *
417     * @param count The number of units to move.
418     * @param forward <code>true</code> to move <code>count</code> units
419     *            forward, <code>false</code> to move backward.
420     * @param isShiftPressed <code>true</code> if the shift key is pressed.
421     * @return <code>true</code> if successful or <code>false</code> if no input
422     *         connection was available or the movement failed
423     */
424    private boolean moveUnit(int granularity, int count, boolean forward, boolean isShiftPressed) {
425        // If the input connection is null or count is 0, no-op.
426        AccessibleInputConnection inputConnection = getCurrentInputConnection();
427        if (inputConnection == null || !inputConnection.hasExtractedText()) {
428            return false;
429        } else if (count == 0) {
430            return true;
431        }
432
433        // If the shift key is held down, force ACTION_EXTEND mode.
434        int action = (isShiftPressed ? TextNavigation.ACTION_EXTEND : mAction);
435
436        // Disable sending accessibility events while we send multiple events,
437        // then announce only the last event.
438        boolean savedSendAccessibilityEvents = inputConnection.isSendAccessibilityEvents();
439        inputConnection.setSendAccessibilityEvents(false);
440        for (int i = 0; i < count - 1; i++) {
441            if (forward) {
442                inputConnection.next(granularity, action);
443            } else {
444                inputConnection.previous(granularity, action);
445            }
446        }
447        inputConnection.setSendAccessibilityEvents(savedSendAccessibilityEvents);
448
449        // Obtain the new position from the final event. If the position is
450        // null, we failed to move and should return false.
451        Position newPosition = null;
452        if (forward) {
453            newPosition = inputConnection.next(granularity, action);
454        } else {
455            newPosition = inputConnection.previous(mGranularity, action);
456        }
457        return (newPosition != null);
458    }
459
460    /**
461     * Adjusts granularity up or down. Returns <code>true</code> if granularity
462     * is set to a different value. Wraps around if granularity is already at
463     * the minimum or maximum setting.
464     *
465     * @param direction The direction in which to change granularity.
466     * @return <code>true</code> if granularity is set to a different value.
467     * @see TextNavigation#NUM_GRANULARITY_TYPES
468     */
469    protected boolean adjustGranularity(int direction) {
470        int oldGranularity = getGranularity();
471        int granularity = oldGranularity + direction;
472
473        if (granularity < 0) {
474            granularity += TextNavigation.NUM_GRANULARITY_TYPES;
475        } else if (granularity >= TextNavigation.NUM_GRANULARITY_TYPES) {
476            granularity -= TextNavigation.NUM_GRANULARITY_TYPES;
477        }
478
479        setGranularity(granularity);
480
481        return (oldGranularity != granularity);
482    }
483
484    /**
485     * Sets granularity (unit type) for text navigation.
486     *
487     * @param granularity Value could be {@link TextNavigation#GRANULARITY_CHAR}
488     *            , {@link TextNavigation#GRANULARITY_WORD},
489     *            {@link TextNavigation#GRANULARITY_SENTENCE},
490     *            {@link TextNavigation#GRANULARITY_PARAGRAPH} or
491     *            {@link TextNavigation#GRANULARITY_ENTIRE_TEXT}
492     * @return <code>true</code> if granularity changed
493     */
494    public boolean setGranularity(int granularity) {
495        AccessibleInputConnection.checkValidGranularity(granularity);
496        String speak = String.format(mGranularitySet, mGranularityTypes[granularity]);
497        getCurrentInputConnection().trySendAccessiblityEvent(speak);
498
499        if (mGranularity == granularity) {
500            return false;
501        }
502
503        mGranularity = granularity;
504        onGranularityChanged(granularity);
505
506        return true;
507    }
508
509    /**
510     * Returns the current granularity.
511     *
512     * @return the current granularity
513     * @see AccessibleInputMethodService#setGranularity(int)
514     */
515    public int getGranularity() {
516        return mGranularity;
517    }
518
519    /**
520     * Callback for change in granularity. Override this method to update any
521     * internal state or GUI of IME.
522     *
523     * @param granularity The type of granularity.
524     * @see TextNavigation#GRANULARITY_CHAR
525     * @see TextNavigation#GRANULARITY_WORD
526     * @see TextNavigation#GRANULARITY_SENTENCE
527     * @see TextNavigation#GRANULARITY_PARAGRAPH
528     * @see TextNavigation#GRANULARITY_ENTIRE_TEXT
529     */
530    public void onGranularityChanged(int granularity) {
531    }
532
533    /**
534     * Sets action to be performed on the current selection.
535     *
536     * @param action Value could be either {@link TextNavigation#ACTION_MOVE} or
537     *            {@link TextNavigation#ACTION_EXTEND}.
538     */
539    public boolean setAction(int action) {
540        AccessibleInputConnection.checkValidAction(action);
541
542        if (mAction == action) {
543            return false;
544        }
545
546        mAction = action;
547        String speak = String.format(mActionSet, mActionTypes[action]);
548        getCurrentInputConnection().trySendAccessiblityEvent(speak);
549        onActionChanged(action);
550        return true;
551    }
552
553    /**
554     * Returns the current action.
555     *
556     * @return the current action
557     * @see AccessibleInputMethodService#setAction(int)
558     */
559    public int getAction() {
560        return mAction;
561    }
562
563    /**
564     * Callback for change in action. Override this method to update any
565     * internal state or GUI of IME.
566     *
567     * @param action The type of action.
568     * @see TextNavigation#ACTION_MOVE
569     * @see TextNavigation#ACTION_EXTEND
570     */
571    public void onActionChanged(int action) {
572    }
573
574    /**
575     * Returns whether accessibility is enabled.
576     *
577     * @return whether accessibility is enabled
578     */
579    protected boolean isAccessibilityEnabled() {
580        return mAccessibilityEnabled;
581    }
582
583    /**
584     * Updates the current accessibility enabled state.
585     */
586    private void updateAccessibilityState() {
587        mAccessibilityEnabled = (Settings.Secure.getInt(getContentResolver(),
588                Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1);
589
590        // Reset text navigation when accessibility is disabled.
591        if (!mAccessibilityEnabled) {
592            mAction = TextNavigation.ACTION_MOVE;
593            mGranularity = TextNavigation.GRANULARITY_CHAR;
594        }
595    }
596
597    /**
598     * Callback for change in accessibility enabled state.
599     *
600     * @param accessibilityEnabled
601     */
602    protected void onAccessibilityChanged(boolean accessibilityEnabled) {
603        // Placeholder.
604    }
605
606    @Override
607    public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd,
608            int candidatesStart, int candidatesEnd) {
609        super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart,
610                candidatesEnd);
611
612        if (mWasUpDownPressed) {
613            mWasUpDownPressed = false;
614
615            android.util.Log.e("AIME", "updown was pressed, speaking");
616            mAIC.speakCurrentUnit(mGranularity);
617        }
618    }
619
620    /**
621     * This handler is used by the {@link ContentObserver} below.
622     */
623    private final Handler mHandler = new Handler();
624
625    /**
626     * This observer listens for changes in the accessibility enabled state.
627     */
628    private final ContentObserver mAccessibilityObserver = new ContentObserver(mHandler) {
629        @Override
630        public void onChange(boolean selfChange) {
631            if (selfChange) {
632                return;
633            }
634
635            updateAccessibilityState();
636
637            // Force a configuration change.
638            Configuration newConfig = new Configuration();
639            newConfig.setToDefaults();
640            newConfig.locale = Locale.getDefault();
641            Settings.System.getConfiguration(getContentResolver(), newConfig);
642            onConfigurationChanged(newConfig);
643
644            onAccessibilityChanged(mAccessibilityEnabled);
645        }
646    };
647}