PageRenderTime 39ms CodeModel.GetById 10ms app.highlight 23ms RepoModel.GetById 1ms app.codeStats 0ms

/widgets/access-shim/src/com/googlecode/eyesfree/widget/AccessibleFrameLayout.java

http://eyes-free.googlecode.com/
Java | 532 lines | 342 code | 95 blank | 95 comment | 56 complexity | 62910d66064555f2424d257b68e20fed MD5 | raw file
  1/*
  2 * Copyright (C) 2011 Google Inc.
  3 *
  4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5 * use this file except in compliance with the License. You may obtain a copy of
  6 * 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, WITHOUT
 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 13 * License for the specific language governing permissions and limitations under
 14 * the License.
 15 */
 16
 17package com.googlecode.eyesfree.widget;
 18
 19import android.app.Instrumentation;
 20import android.content.ContentResolver;
 21import android.content.Context;
 22import android.graphics.Canvas;
 23import android.graphics.Matrix;
 24import android.graphics.Paint;
 25import android.graphics.Rect;
 26import android.os.Handler;
 27import android.os.SystemClock;
 28import android.os.Vibrator;
 29import android.provider.Settings;
 30import android.speech.tts.TextToSpeech;
 31import android.view.GestureDetector;
 32import android.view.KeyEvent;
 33import android.view.MotionEvent;
 34import android.view.MotionEvent.PointerCoords;
 35import android.view.View;
 36import android.view.ViewGroup;
 37import android.view.ViewParent;
 38import android.view.ViewTreeObserver;
 39import android.view.accessibility.AccessibilityEvent;
 40import android.widget.AbsListView;
 41import android.widget.FrameLayout;
 42
 43/**
 44 * @author alanv@google.com (Alan Viverette)
 45 */
 46public class AccessibleFrameLayout extends FrameLayout {
 47    private static final boolean ENABLE_VIBRATE = false;
 48
 49    private final Handler mHandler;
 50    private final Instrumentation mInstrumentation;
 51    private final GestureDetector mDetector;
 52    private final Vibrator mVibrator;
 53    private final Paint mPaint;
 54    private final TextToSpeech mTTS;
 55    private final Rect mSelectedRect;
 56    private final boolean mCompatibilityMode;
 57
 58    private View mSelectedView;
 59
 60    private boolean mExplorationEnabled;
 61    private boolean mSpeechAvailable;
 62
 63    // private static final long[] mFocusGainedFocusablePattern = new long[] {
 64    // 0, 100 };
 65    private static final long[] mFocusLostFocusablePattern = new long[] { 0, 50 };
 66    // private static final long[] mFocusGainedPattern = new long[] { 0, 50 };
 67    private static final long[] mFocusLostPattern = new long[] { 0, 15 };
 68
 69    public AccessibleFrameLayout(Context context) {
 70        super(context);
 71
 72        mHandler = new Handler();
 73        mInstrumentation = new Instrumentation();
 74
 75        if (ENABLE_VIBRATE) {
 76            mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
 77        } else {
 78            mVibrator = null;
 79        }
 80
 81        mDetector = new GestureDetector(context, gestureListener);
 82        mDetector.setIsLongpressEnabled(false);
 83
 84        mSelectedView = null;
 85        mSelectedRect = new Rect();
 86
 87        mPaint = new Paint();
 88        mPaint.setStyle(Paint.Style.STROKE);
 89        mPaint.setStrokeJoin(Paint.Join.ROUND);
 90        mPaint.setStrokeWidth(3);
 91        mPaint.setColor(0xFF8CD2FF);
 92
 93        mSelectedView = null;
 94        mSpeechAvailable = false;
 95
 96        mTTS = new TextToSpeech(context, ttsInit);
 97
 98        updateExplorationEnabled();
 99
100        getViewTreeObserver().addOnGlobalFocusChangeListener(focusChangeListener);
101
102        boolean compatibilityMode = true;
103
104        try {
105            Class.forName("android.view.MotionEvent.PointerCoords");
106            compatibilityMode = false;
107        } catch (ClassNotFoundException e) {
108            e.printStackTrace();
109        }
110
111        mCompatibilityMode = compatibilityMode;
112    }
113
114    @Override
115    public void getHitRect(Rect rect) {
116        getWindowVisibleDisplayFrame(rect);
117    }
118
119    @Override
120    public void onWindowFocusChanged(boolean hasWindowFocus) {
121        super.onWindowFocusChanged(hasWindowFocus);
122
123        if (hasWindowFocus) {
124            updateExplorationEnabled();
125        }
126
127        if (mExplorationEnabled) {
128            if (hasWindowFocus) {
129                // Update focus since it may have changed.
130                View newFocusedView = findFocus();
131                setSelectedView(newFocusedView, false);
132            } else {
133                setSelectedView(null, false);
134            }
135        }
136    }
137
138    private void updateExplorationEnabled() {
139        ContentResolver resolver = getContext().getContentResolver();
140        int enabled = Settings.Secure.getInt(resolver, Settings.Secure.ACCESSIBILITY_ENABLED, -1);
141
142        mExplorationEnabled = (enabled == 1);
143    }
144
145    @Override
146    protected void onDetachedFromWindow() {
147        super.onDetachedFromWindow();
148
149        mTTS.shutdown();
150    }
151
152    @Override
153    protected void dispatchDraw(Canvas canvas) {
154        super.dispatchDraw(canvas);
155
156        if (mSelectedView != null) {
157            getGlobalVisibleRect(mSelectedRect);
158
159            int offsetTop = mSelectedRect.top;
160            int offsetLeft = mSelectedRect.left;
161
162            mSelectedView.getGlobalVisibleRect(mSelectedRect);
163
164            if (offsetTop > 0 || offsetLeft > 0) {
165                int saveCount = canvas.save();
166                Matrix matrix = canvas.getMatrix();
167                matrix.postTranslate(offsetLeft, offsetTop);
168                canvas.setMatrix(matrix);
169                canvas.drawRect(mSelectedRect, mPaint);
170                canvas.restoreToCount(saveCount);
171            } else {
172                canvas.drawRect(mSelectedRect, mPaint);
173            }
174        }
175    }
176
177    /**
178     * Inserts this frame between a ViewGroup and its children by removing all
179     * child views from the parent view, adding them to this frame, and then
180     * adding this frame to the parent view.
181     *
182     * @param parent The parent view into which this frame will be inserted.
183     */
184    public void inject(ViewGroup parent) {
185        int count = parent.getChildCount();
186
187        while (parent.getChildCount() > 0) {
188            View child = parent.getChildAt(0);
189            parent.removeViewAt(0);
190            addView(child);
191        }
192
193        parent.addView(this);
194    }
195
196    /**
197     * Strips the last pointer from a {@link MotionEvent} and returns the
198     * modified event. Does not modify the original event.
199     *
200     * @param ev The MotionEvent to modify.
201     * @return The modified MotionEvent.
202     */
203    private MotionEvent stripLastPointer(MotionEvent ev) {
204        ev.getPointerCount();
205
206        int removePointer = ev.getPointerCount() - 1;
207        int removePointerId = ev.getPointerId(removePointer);
208
209        long downTime = ev.getDownTime();
210        long eventTime = ev.getEventTime();
211        int action = ev.getAction();
212        int pointers = ev.getPointerCount() - 1;
213        int[] pointerIds = new int[pointers];
214        int metaState = ev.getMetaState();
215        float xPrecision = ev.getXPrecision();
216        float yPrecision = ev.getYPrecision();
217        int deviceId = ev.getDeviceId();
218        int edgeFlags = ev.getEdgeFlags();
219
220        switch (ev.getActionMasked()) {
221            case MotionEvent.ACTION_POINTER_DOWN:
222            case MotionEvent.ACTION_POINTER_UP:
223                action -= 0x100;
224                if (pointers == 1) {
225                    action -= 0x5;
226                }
227                break;
228        }
229
230        MotionEvent event = null;
231
232        if (mCompatibilityMode) {
233            float x = ev.getX();
234            float y = ev.getY();
235            float pressure = ev.getPressure();
236            float size = ev.getSize();
237
238            event = MotionEvent.obtain(downTime, eventTime, action, pointers, x, y, pressure, size,
239                    metaState, xPrecision, yPrecision, deviceId, edgeFlags);
240        } else {
241            PointerCoords[] pointerCoords = new PointerCoords[pointers];
242            int source = ev.getSource();
243            int flags = ev.getFlags();
244
245            for (int i = 0; i < pointers; i++) {
246                pointerIds[i] = ev.getPointerId(i);
247                pointerCoords[i] = new PointerCoords();
248
249                ev.getPointerCoords(i, pointerCoords[i]);
250            }
251
252            event = MotionEvent.obtain(downTime, eventTime, action, pointers, pointerIds,
253                    pointerCoords, metaState, xPrecision, yPrecision, deviceId, edgeFlags, source,
254                    flags);
255        }
256
257        return event;
258    }
259
260    @Override
261    public boolean dispatchTouchEvent(MotionEvent ev) {
262        if (mExplorationEnabled) {
263            int pointers = ev.getPointerCount();
264
265            if (ev.getPointerCount() == 1) {
266                ViewGroup target = this;
267
268                if (ev.getAction() == MotionEvent.ACTION_OUTSIDE) {
269                    ev.setAction(MotionEvent.ACTION_DOWN);
270                    mDetector.onTouchEvent(ev);
271                    ev.setAction(MotionEvent.ACTION_UP);
272                }
273
274                mDetector.onTouchEvent(ev);
275
276                return true;
277            }
278
279            ev = stripLastPointer(ev);
280        }
281
282        return super.dispatchTouchEvent(ev);
283    }
284
285    /**
286     * Emulates a tap event positioned at the center of the selected view. No-op
287     * if no view is selected.
288     */
289    private void tapSelectedView() {
290        if (mSelectedView == null) {
291            return;
292        }
293
294        final float centerX = mSelectedRect.exactCenterX();
295        final float centerY = mSelectedRect.exactCenterY();
296        final long currTime = SystemClock.uptimeMillis();
297
298        MotionEvent down = MotionEvent.obtain(
299                currTime, currTime, MotionEvent.ACTION_DOWN, centerX, centerY, 0);
300        MotionEvent up = MotionEvent.obtain(
301                currTime, currTime, MotionEvent.ACTION_UP, centerX, centerY, 0);
302
303        super.dispatchTouchEvent(down);
304        super.dispatchTouchEvent(up);
305    }
306
307    /**
308     * Emulates a directional pad event based on the given flick direction.
309     *
310     * @param direction A flick direction constant.
311     */
312    private void changeFocus(int direction) {
313        int keyCode = KeyEvent.KEYCODE_UNKNOWN;
314
315        switch (direction) {
316            case FlickGestureListener.FLICK_UP:
317                keyCode = KeyEvent.KEYCODE_DPAD_UP;
318                break;
319            case FlickGestureListener.FLICK_RIGHT:
320                keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
321                break;
322            case FlickGestureListener.FLICK_DOWN:
323                keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
324                break;
325            case FlickGestureListener.FLICK_LEFT:
326                keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
327                break;
328            default:
329                // Invalid flick constant.
330                return;
331        }
332
333        if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
334            final int keyCodeFinal = keyCode;
335
336            // Exit from touch mode as gracefully as possible.
337            if (mSelectedView != null) {
338                ViewParent parent = mSelectedView.getParent();
339
340                mSelectedView.requestFocusFromTouch();
341
342                // If the selected view belongs to a list, make sure it's
343                // selected within the list (since it's not focusable).
344                // TODO(alanv): Check whether the prior call was successful?
345                if (parent instanceof AbsListView) {
346                    AbsListView listParent = (AbsListView) parent;
347                    int position = listParent.getPositionForView(mSelectedView);
348                    listParent.setSelection(position);
349                }
350            } else {
351                requestFocusFromTouch();
352            }
353
354            // We have to send the key event on a separate thread, then return
355            // to the main thread to synchronize the selected view with focus.
356            new Thread() {
357                @Override
358                public void run() {
359                    mInstrumentation.sendKeyDownUpSync(keyCodeFinal);
360
361                    mHandler.post(new Runnable() {
362                        @Override
363                        public void run() {
364                            View newFocusedView = findFocus();
365                            setSelectedView(newFocusedView, false);
366                        }
367                    });
368                }
369            }.start();
370        }
371    }
372
373    /**
374     * Searches for a selectable view under the given frame-relative
375     * coordinates.
376     *
377     * @param x Frame-relative X coordinate.
378     * @param y Frame-relative Y coordinate.
379     */
380    private void setSelectionAtPoint(int x, int y) {
381        View selection = SelectionFinder.getSelectionAtPoint(mSelectedView, this, x, y);
382
383        setSelectedView(selection, true);
384    }
385
386    /**
387     * Sets the selected view and optionally announces it through TalkBack.
388     *
389     * @param selectedView The {@link View} to set as the current selection.
390     * @param announce Set to <code>true</code> to announce selection changes.
391     */
392    private void setSelectedView(View selectedView, boolean announce) {
393        if (mSelectedView == selectedView) {
394            return;
395        }
396
397        if (mSelectedView != null) {
398            announceSelectionLost(mSelectedView);
399        }
400
401        if (selectedView != null) {
402            if (selectedView instanceof AbsListView) {
403                AbsListView absListView = (AbsListView) selectedView;
404                View item = absListView.getSelectedView();
405
406                if (item != null) {
407                    selectedView = item;
408                } else {
409                    // We don't want to select list containers, so if there's no
410                    // selected element then we'll just select nothing.
411                    selectedView = null;
412                }
413            }
414
415            if (selectedView != null && announce) {
416                announceSelectionGained(selectedView);
417            }
418        }
419
420        mSelectedView = selectedView;
421
422        invalidate();
423    }
424
425    /**
426     * Updates the selection rectangle and attempts to shift focus away from the
427     * provided view. Clears active TTS.
428     *
429     * @param view
430     */
431    private void announceSelectionLost(View view) {
432        // TODO(alanv): Add an additional TYPE_VIEW_HOVER_OFF event type that
433        // fires a KickBack vibration and (probably) clears active TalkBack
434        // utterances.
435
436        if (mSpeechAvailable) {
437            mTTS.speak("", TextToSpeech.QUEUE_FLUSH, null);
438        }
439
440        if (mVibrator != null) {
441            if (view.isFocusable()) {
442                mVibrator.vibrate(mFocusLostFocusablePattern, -1);
443            } else {
444                mVibrator.vibrate(mFocusLostPattern, -1);
445            }
446        }
447
448        mSelectedRect.setEmpty();
449    }
450
451    /**
452     * Updates the selection rectangle and attempts to shift focus to the
453     * provided view. If the view is not focusable, fires an AccessibilityEvent
454     * so that it is read aloud.
455     *
456     * @param view The view which has gained selection.
457     * @return Returns the view that actually gained selection.
458     */
459    private View announceSelectionGained(View view) {
460        // TODO(alanv): Add an additional TYPE_VIEW_HOVER event type with a
461        // different KickBack response. Otherwise everything looks like a
462        // button.
463
464        if (mSpeechAvailable) {
465            mTTS.speak("", TextToSpeech.QUEUE_FLUSH, null);
466        }
467
468        view.getGlobalVisibleRect(mSelectedRect);
469
470        // If the view is focusable, request focus. This will automatically read
471        // the the view's content description using TalkBack (if enabled).
472        // if (view.requestFocusFromTouch()) {
473        // return view.findFocus();
474        // }
475
476        // If the view is not focusable, force it to send an AccessibilityEvent.
477        // TODO(alanv): This seems to retain the contentDescription from a
478        // previous event.
479        view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
480
481        return view;
482    }
483
484    private final FlickGestureListener gestureListener = new FlickGestureListener() {
485        @Override
486        protected boolean onSingleTap(float x, float y, float rawX, float rawY) {
487            setSelectionAtPoint((int) x, (int) y);
488
489            return true;
490        }
491
492        @Override
493        protected boolean onDoubleTap(float x, float y, float rawX, float rawY) {
494            tapSelectedView();
495
496            return true;
497        }
498
499        @Override
500        protected boolean onFlick(float x, float y, float rawX, float rawY, int direction) {
501            changeFocus(direction);
502
503            return true;
504        }
505
506        @Override
507        protected boolean onMove(float x, float y, float rawX, float rawY) {
508            setSelectionAtPoint((int) x, (int) y);
509
510            return true;
511        }
512    };
513
514    private final TextToSpeech.OnInitListener ttsInit = new TextToSpeech.OnInitListener() {
515        @Override
516        public void onInit(int status) {
517            if (status == TextToSpeech.SUCCESS) {
518                mSpeechAvailable = true;
519            }
520        }
521    };
522
523    private final ViewTreeObserver.OnGlobalFocusChangeListener focusChangeListener =
524            new ViewTreeObserver.OnGlobalFocusChangeListener() {
525                @Override
526                public void onGlobalFocusChanged(View oldFocus, View newFocus) {
527                    if (newFocus != null) {
528                        setSelectedView(newFocus, false);
529                    }
530                }
531            };
532}