1/*
2 * Copyright (C) 2012 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.android.server.accessibility;
18
19import android.animation.ObjectAnimator;
20import android.animation.TypeEvaluator;
21import android.animation.ValueAnimator;
22import android.content.BroadcastReceiver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.graphics.Rect;
27import android.graphics.Region;
28import android.os.AsyncTask;
29import android.os.Binder;
30import android.os.Handler;
31import android.os.Message;
32import android.os.SystemClock;
33import android.provider.Settings;
34import android.text.TextUtils;
35import android.util.Property;
36import android.util.Slog;
37import android.view.GestureDetector;
38import android.view.GestureDetector.SimpleOnGestureListener;
39import android.view.MagnificationSpec;
40import android.view.MotionEvent;
41import android.view.MotionEvent.PointerCoords;
42import android.view.MotionEvent.PointerProperties;
43import android.view.ScaleGestureDetector;
44import android.view.ScaleGestureDetector.OnScaleGestureListener;
45import android.view.View;
46import android.view.ViewConfiguration;
47import android.view.WindowManagerInternal;
48import android.view.accessibility.AccessibilityEvent;
49import android.view.animation.DecelerateInterpolator;
50
51import com.android.internal.os.SomeArgs;
52import com.android.server.LocalServices;
53
54import java.util.Locale;
55
56/**
57 * This class handles the screen magnification when accessibility is enabled.
58 * The behavior is as follows:
59 *
60 * 1. Triple tap toggles permanent screen magnification which is magnifying
61 *    the area around the location of the triple tap. One can think of the
62 *    location of the triple tap as the center of the magnified viewport.
63 *    For example, a triple tap when not magnified would magnify the screen
64 *    and leave it in a magnified state. A triple tapping when magnified would
65 *    clear magnification and leave the screen in a not magnified state.
66 *
67 * 2. Triple tap and hold would magnify the screen if not magnified and enable
68 *    viewport dragging mode until the finger goes up. One can think of this
69 *    mode as a way to move the magnified viewport since the area around the
70 *    moving finger will be magnified to fit the screen. For example, if the
71 *    screen was not magnified and the user triple taps and holds the screen
72 *    would magnify and the viewport will follow the user's finger. When the
73 *    finger goes up the screen will zoom out. If the same user interaction
74 *    is performed when the screen is magnified, the viewport movement will
75 *    be the same but when the finger goes up the screen will stay magnified.
76 *    In other words, the initial magnified state is sticky.
77 *
78 * 3. Pinching with any number of additional fingers when viewport dragging
79 *    is enabled, i.e. the user triple tapped and holds, would adjust the
80 *    magnification scale which will become the current default magnification
81 *    scale. The next time the user magnifies the same magnification scale
82 *    would be used.
83 *
84 * 4. When in a permanent magnified state the user can use two or more fingers
85 *    to pan the viewport. Note that in this mode the content is panned as
86 *    opposed to the viewport dragging mode in which the viewport is moved.
87 *
88 * 5. When in a permanent magnified state the user can use two or more
89 *    fingers to change the magnification scale which will become the current
90 *    default magnification scale. The next time the user magnifies the same
91 *    magnification scale would be used.
92 *
93 * 6. The magnification scale will be persisted in settings and in the cloud.
94 */
95public final class ScreenMagnifier implements WindowManagerInternal.MagnificationCallbacks,
96        EventStreamTransformation {
97
98    private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName();
99
100    private static final boolean DEBUG_STATE_TRANSITIONS = false;
101    private static final boolean DEBUG_DETECTING = false;
102    private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;
103    private static final boolean DEBUG_PANNING = false;
104    private static final boolean DEBUG_SCALING = false;
105    private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false;
106
107    private static final int STATE_DELEGATING = 1;
108    private static final int STATE_DETECTING = 2;
109    private static final int STATE_VIEWPORT_DRAGGING = 3;
110    private static final int STATE_MAGNIFIED_INTERACTION = 4;
111
112    private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;
113    private static final int MULTI_TAP_TIME_SLOP_ADJUSTMENT = 50;
114
115    private static final int MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED = 1;
116    private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 2;
117    private static final int MESSAGE_ON_USER_CONTEXT_CHANGED = 3;
118    private static final int MESSAGE_ON_ROTATION_CHANGED = 4;
119
120    private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1;
121
122    private static final int MY_PID = android.os.Process.myPid();
123
124    private final Rect mTempRect = new Rect();
125    private final Rect mTempRect1 = new Rect();
126
127    private final Context mContext;
128    private final WindowManagerInternal mWindowManager;
129    private final MagnificationController mMagnificationController;
130    private final ScreenStateObserver mScreenStateObserver;
131
132    private final DetectingStateHandler mDetectingStateHandler;
133    private final MagnifiedContentInteractonStateHandler mMagnifiedContentInteractonStateHandler;
134    private final StateViewportDraggingHandler mStateViewportDraggingHandler;
135
136    private final AccessibilityManagerService mAms;
137
138    private final int mTapTimeSlop = ViewConfiguration.getTapTimeout();
139    private final int mMultiTapTimeSlop =
140            ViewConfiguration.getDoubleTapTimeout() - MULTI_TAP_TIME_SLOP_ADJUSTMENT;
141    private final int mTapDistanceSlop;
142    private final int mMultiTapDistanceSlop;
143
144    private final long mLongAnimationDuration;
145
146    private final Region mMagnifiedBounds = new Region();
147
148    private EventStreamTransformation mNext;
149
150    private int mCurrentState;
151    private int mPreviousState;
152    private boolean mTranslationEnabledBeforePan;
153
154    private PointerCoords[] mTempPointerCoords;
155    private PointerProperties[] mTempPointerProperties;
156
157    private long mDelegatingStateDownTime;
158
159    private boolean mUpdateMagnificationSpecOnNextBoundsChange;
160
161    private final Handler mHandler = new Handler() {
162        @Override
163        public void handleMessage(Message message) {
164            switch (message.what) {
165                case MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED: {
166                    Region bounds = (Region) message.obj;
167                    handleOnMagnifiedBoundsChanged(bounds);
168                    bounds.recycle();
169                } break;
170                case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
171                    SomeArgs args = (SomeArgs) message.obj;
172                    final int left = args.argi1;
173                    final int top = args.argi2;
174                    final int right = args.argi3;
175                    final int bottom = args.argi4;
176                    handleOnRectangleOnScreenRequested(left, top, right, bottom);
177                    args.recycle();
178                } break;
179                case MESSAGE_ON_USER_CONTEXT_CHANGED: {
180                    handleOnUserContextChanged();
181                } break;
182                case MESSAGE_ON_ROTATION_CHANGED: {
183                    final int rotation = message.arg1;
184                    handleOnRotationChanged(rotation);
185                } break;
186            }
187        }
188    };
189
190    public ScreenMagnifier(Context context, int displayId, AccessibilityManagerService service) {
191        mContext = context;
192        mWindowManager = LocalServices.getService(WindowManagerInternal.class);
193        mAms = service;
194
195        mLongAnimationDuration = context.getResources().getInteger(
196                com.android.internal.R.integer.config_longAnimTime);
197        mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop();
198        mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
199
200        mDetectingStateHandler = new DetectingStateHandler();
201        mStateViewportDraggingHandler = new StateViewportDraggingHandler();
202        mMagnifiedContentInteractonStateHandler = new MagnifiedContentInteractonStateHandler(
203                context);
204
205        mMagnificationController = new MagnificationController(mLongAnimationDuration);
206        mScreenStateObserver = new ScreenStateObserver(context, mMagnificationController);
207
208        mWindowManager.setMagnificationCallbacks(this);
209
210        transitionToState(STATE_DETECTING);
211    }
212
213    @Override
214    public void onMagnifedBoundsChanged(Region bounds) {
215        Region newBounds = Region.obtain(bounds);
216        mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, newBounds).sendToTarget();
217        if (MY_PID != Binder.getCallingPid()) {
218            bounds.recycle();
219        }
220    }
221
222    private void handleOnMagnifiedBoundsChanged(Region bounds) {
223        // If there was a rotation we have to update the center of the magnified
224        // region since the old offset X/Y may be out of its acceptable range for
225        // the new display width and height.
226        if (mUpdateMagnificationSpecOnNextBoundsChange) {
227            mUpdateMagnificationSpecOnNextBoundsChange = false;
228            MagnificationSpec spec = mMagnificationController.getMagnificationSpec();
229            Rect magnifiedFrame = mTempRect;
230            mMagnifiedBounds.getBounds(magnifiedFrame);
231            final float scale = spec.scale;
232            final float centerX = (-spec.offsetX + magnifiedFrame.width() / 2) / scale;
233            final float centerY = (-spec.offsetY + magnifiedFrame.height() / 2) / scale;
234            mMagnificationController.setScaleAndMagnifiedRegionCenter(scale, centerX,
235                    centerY, false);
236        }
237        mMagnifiedBounds.set(bounds);
238        mAms.onMagnificationStateChanged();
239    }
240
241    @Override
242    public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) {
243        SomeArgs args = SomeArgs.obtain();
244        args.argi1 = left;
245        args.argi2 = top;
246        args.argi3 = right;
247        args.argi4 = bottom;
248        mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, args).sendToTarget();
249    }
250
251    private void handleOnRectangleOnScreenRequested(int left, int top, int right, int bottom) {
252        Rect magnifiedFrame = mTempRect;
253        mMagnifiedBounds.getBounds(magnifiedFrame);
254        if (!magnifiedFrame.intersects(left, top, right, bottom)) {
255            return;
256        }
257        Rect magnifFrameInScreenCoords = mTempRect1;
258        getMagnifiedFrameInContentCoords(magnifFrameInScreenCoords);
259        final float scrollX;
260        final float scrollY;
261        if (right - left > magnifFrameInScreenCoords.width()) {
262            final int direction = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault());
263            if (direction == View.LAYOUT_DIRECTION_LTR) {
264                scrollX = left - magnifFrameInScreenCoords.left;
265            } else {
266                scrollX = right - magnifFrameInScreenCoords.right;
267            }
268        } else if (left < magnifFrameInScreenCoords.left) {
269            scrollX = left - magnifFrameInScreenCoords.left;
270        } else if (right > magnifFrameInScreenCoords.right) {
271            scrollX = right - magnifFrameInScreenCoords.right;
272        } else {
273            scrollX = 0;
274        }
275        if (bottom - top > magnifFrameInScreenCoords.height()) {
276            scrollY = top - magnifFrameInScreenCoords.top;
277        } else if (top < magnifFrameInScreenCoords.top) {
278            scrollY = top - magnifFrameInScreenCoords.top;
279        } else if (bottom > magnifFrameInScreenCoords.bottom) {
280            scrollY = bottom - magnifFrameInScreenCoords.bottom;
281        } else {
282            scrollY = 0;
283        }
284        final float scale = mMagnificationController.getScale();
285        mMagnificationController.offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale);
286    }
287
288    @Override
289    public void onRotationChanged(int rotation) {
290        mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0).sendToTarget();
291    }
292
293    private void handleOnRotationChanged(int rotation) {
294        resetMagnificationIfNeeded();
295        if (mMagnificationController.isMagnifying()) {
296            mUpdateMagnificationSpecOnNextBoundsChange = true;
297        }
298    }
299
300    @Override
301    public void onUserContextChanged() {
302        mHandler.sendEmptyMessage(MESSAGE_ON_USER_CONTEXT_CHANGED);
303    }
304
305    private void handleOnUserContextChanged() {
306        resetMagnificationIfNeeded();
307    }
308
309    private void getMagnifiedFrameInContentCoords(Rect rect) {
310        MagnificationSpec spec = mMagnificationController.getMagnificationSpec();
311        mMagnifiedBounds.getBounds(rect);
312        rect.offset((int) -spec.offsetX, (int) -spec.offsetY);
313        rect.scale(1.0f / spec.scale);
314    }
315
316    private void resetMagnificationIfNeeded() {
317        if (mMagnificationController.isMagnifying()
318                && isScreenMagnificationAutoUpdateEnabled(mContext)) {
319            mMagnificationController.reset(true);
320        }
321    }
322
323    @Override
324    public void onMotionEvent(MotionEvent event, MotionEvent rawEvent,
325            int policyFlags) {
326        mMagnifiedContentInteractonStateHandler.onMotionEvent(event);
327        switch (mCurrentState) {
328            case STATE_DELEGATING: {
329                handleMotionEventStateDelegating(event, rawEvent, policyFlags);
330            } break;
331            case STATE_DETECTING: {
332                mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags);
333            } break;
334            case STATE_VIEWPORT_DRAGGING: {
335                mStateViewportDraggingHandler.onMotionEvent(event, policyFlags);
336            } break;
337            case STATE_MAGNIFIED_INTERACTION: {
338                // mMagnifiedContentInteractonStateHandler handles events only
339                // if this is the current state since it uses ScaleGestureDetecotr
340                // and a GestureDetector which need well formed event stream.
341            } break;
342            default: {
343                throw new IllegalStateException("Unknown state: " + mCurrentState);
344            }
345        }
346    }
347
348    @Override
349    public void onAccessibilityEvent(AccessibilityEvent event) {
350        if (mNext != null) {
351            mNext.onAccessibilityEvent(event);
352        }
353    }
354
355    @Override
356    public void setNext(EventStreamTransformation next) {
357        mNext = next;
358    }
359
360    @Override
361    public void clear() {
362        mCurrentState = STATE_DETECTING;
363        mDetectingStateHandler.clear();
364        mStateViewportDraggingHandler.clear();
365        mMagnifiedContentInteractonStateHandler.clear();
366        if (mNext != null) {
367            mNext.clear();
368        }
369    }
370
371    @Override
372    public void onDestroy() {
373        mScreenStateObserver.destroy();
374        mWindowManager.setMagnificationCallbacks(null);
375    }
376
377    private void handleMotionEventStateDelegating(MotionEvent event,
378            MotionEvent rawEvent, int policyFlags) {
379        switch (event.getActionMasked()) {
380            case MotionEvent.ACTION_DOWN: {
381                mDelegatingStateDownTime = event.getDownTime();
382            } break;
383            case MotionEvent.ACTION_UP: {
384                if (mDetectingStateHandler.mDelayedEventQueue == null) {
385                    transitionToState(STATE_DETECTING);
386                }
387            } break;
388        }
389        if (mNext != null) {
390            // If the event is within the magnified portion of the screen we have
391            // to change its location to be where the user thinks he is poking the
392            // UI which may have been magnified and panned.
393            final float eventX = event.getX();
394            final float eventY = event.getY();
395            if (mMagnificationController.isMagnifying()
396                    && mMagnifiedBounds.contains((int) eventX, (int) eventY)) {
397                final float scale = mMagnificationController.getScale();
398                final float scaledOffsetX = mMagnificationController.getOffsetX();
399                final float scaledOffsetY = mMagnificationController.getOffsetY();
400                final int pointerCount = event.getPointerCount();
401                PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
402                PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
403                for (int i = 0; i < pointerCount; i++) {
404                    event.getPointerCoords(i, coords[i]);
405                    coords[i].x = (coords[i].x - scaledOffsetX) / scale;
406                    coords[i].y = (coords[i].y - scaledOffsetY) / scale;
407                    event.getPointerProperties(i, properties[i]);
408                }
409                event = MotionEvent.obtain(event.getDownTime(),
410                        event.getEventTime(), event.getAction(), pointerCount, properties,
411                        coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(),
412                        event.getFlags());
413            }
414            // We cache some events to see if the user wants to trigger magnification.
415            // If no magnification is triggered we inject these events with adjusted
416            // time and down time to prevent subsequent transformations being confused
417            // by stale events. After the cached events, which always have a down, are
418            // injected we need to also update the down time of all subsequent non cached
419            // events. All delegated events cached and non-cached are delivered here.
420            event.setDownTime(mDelegatingStateDownTime);
421            mNext.onMotionEvent(event, rawEvent, policyFlags);
422        }
423    }
424
425    private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
426        final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
427        if (oldSize < size) {
428            PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
429            mTempPointerCoords = new PointerCoords[size];
430            if (oldTempPointerCoords != null) {
431                System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
432            }
433        }
434        for (int i = oldSize; i < size; i++) {
435            mTempPointerCoords[i] = new PointerCoords();
436        }
437        return mTempPointerCoords;
438    }
439
440    private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
441        final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0;
442        if (oldSize < size) {
443            PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
444            mTempPointerProperties = new PointerProperties[size];
445            if (oldTempPointerProperties != null) {
446                System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize);
447            }
448        }
449        for (int i = oldSize; i < size; i++) {
450            mTempPointerProperties[i] = new PointerProperties();
451        }
452        return mTempPointerProperties;
453    }
454
455    private void transitionToState(int state) {
456        if (DEBUG_STATE_TRANSITIONS) {
457            switch (state) {
458                case STATE_DELEGATING: {
459                    Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING");
460                } break;
461                case STATE_DETECTING: {
462                    Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING");
463                } break;
464                case STATE_VIEWPORT_DRAGGING: {
465                    Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING");
466                } break;
467                case STATE_MAGNIFIED_INTERACTION: {
468                    Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION");
469                } break;
470                default: {
471                    throw new IllegalArgumentException("Unknown state: " + state);
472                }
473            }
474        }
475        mPreviousState = mCurrentState;
476        mCurrentState = state;
477    }
478
479    private final class MagnifiedContentInteractonStateHandler
480            extends SimpleOnGestureListener implements OnScaleGestureListener {
481        private static final float MIN_SCALE = 1.3f;
482        private static final float MAX_SCALE = 5.0f;
483
484        private static final float SCALING_THRESHOLD = 0.3f;
485
486        private final ScaleGestureDetector mScaleGestureDetector;
487        private final GestureDetector mGestureDetector;
488
489        private float mInitialScaleFactor = -1;
490        private boolean mScaling;
491
492        public MagnifiedContentInteractonStateHandler(Context context) {
493            mScaleGestureDetector = new ScaleGestureDetector(context, this);
494            mScaleGestureDetector.setQuickScaleEnabled(false);
495            mGestureDetector = new GestureDetector(context, this);
496        }
497
498        public void onMotionEvent(MotionEvent event) {
499            mScaleGestureDetector.onTouchEvent(event);
500            mGestureDetector.onTouchEvent(event);
501            if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
502                return;
503            }
504            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
505                clear();
506                final float scale = Math.min(Math.max(mMagnificationController.getScale(),
507                        MIN_SCALE), MAX_SCALE);
508                if (scale != getPersistedScale()) {
509                    persistScale(scale);
510                }
511                if (mPreviousState == STATE_VIEWPORT_DRAGGING) {
512                    transitionToState(STATE_VIEWPORT_DRAGGING);
513                } else {
514                    transitionToState(STATE_DETECTING);
515                }
516            }
517        }
518
519        @Override
520        public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX,
521                float distanceY) {
522            if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
523                return true;
524            }
525            if (DEBUG_PANNING) {
526                Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX
527                        + " scrollY: " + distanceY);
528            }
529            mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY);
530            return true;
531        }
532
533        @Override
534        public boolean onScale(ScaleGestureDetector detector) {
535            if (!mScaling) {
536                if (mInitialScaleFactor < 0) {
537                    mInitialScaleFactor = detector.getScaleFactor();
538                } else {
539                    final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor;
540                    if (Math.abs(deltaScale) > SCALING_THRESHOLD) {
541                        mScaling = true;
542                        return true;
543                    }
544                }
545                return false;
546            }
547            final float newScale = mMagnificationController.getScale()
548                    * detector.getScaleFactor();
549            final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), MAX_SCALE);
550            if (DEBUG_SCALING) {
551                Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale);
552            }
553            mMagnificationController.setScale(normalizedNewScale, detector.getFocusX(),
554                    detector.getFocusY(), false);
555            return true;
556        }
557
558        @Override
559        public boolean onScaleBegin(ScaleGestureDetector detector) {
560            return (mCurrentState == STATE_MAGNIFIED_INTERACTION);
561        }
562
563        @Override
564        public void onScaleEnd(ScaleGestureDetector detector) {
565            clear();
566        }
567
568        private void clear() {
569            mInitialScaleFactor = -1;
570            mScaling = false;
571        }
572    }
573
574    private final class StateViewportDraggingHandler {
575        private boolean mLastMoveOutsideMagnifiedRegion;
576
577        private void onMotionEvent(MotionEvent event, int policyFlags) {
578            final int action = event.getActionMasked();
579            switch (action) {
580                case MotionEvent.ACTION_DOWN: {
581                    throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN");
582                }
583                case MotionEvent.ACTION_POINTER_DOWN: {
584                    clear();
585                    transitionToState(STATE_MAGNIFIED_INTERACTION);
586                } break;
587                case MotionEvent.ACTION_MOVE: {
588                    if (event.getPointerCount() != 1) {
589                        throw new IllegalStateException("Should have one pointer down.");
590                    }
591                    final float eventX = event.getX();
592                    final float eventY = event.getY();
593                    if (mMagnifiedBounds.contains((int) eventX, (int) eventY)) {
594                        if (mLastMoveOutsideMagnifiedRegion) {
595                            mLastMoveOutsideMagnifiedRegion = false;
596                            mMagnificationController.setMagnifiedRegionCenter(eventX,
597                                    eventY, true);
598                        } else {
599                            mMagnificationController.setMagnifiedRegionCenter(eventX,
600                                    eventY, false);
601                        }
602                    } else {
603                        mLastMoveOutsideMagnifiedRegion = true;
604                    }
605                } break;
606                case MotionEvent.ACTION_UP: {
607                    if (!mTranslationEnabledBeforePan) {
608                        mMagnificationController.reset(true);
609                    }
610                    clear();
611                    transitionToState(STATE_DETECTING);
612                } break;
613                case MotionEvent.ACTION_POINTER_UP: {
614                    throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP");
615                }
616            }
617        }
618
619        public void clear() {
620            mLastMoveOutsideMagnifiedRegion = false;
621        }
622    }
623
624    private final class DetectingStateHandler {
625
626        private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1;
627
628        private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
629
630        private static final int ACTION_TAP_COUNT = 3;
631
632        private MotionEventInfo mDelayedEventQueue;
633
634        private MotionEvent mLastDownEvent;
635        private MotionEvent mLastTapUpEvent;
636        private int mTapCount;
637
638        private final Handler mHandler = new Handler() {
639            @Override
640            public void handleMessage(Message message) {
641                final int type = message.what;
642                switch (type) {
643                    case MESSAGE_ON_ACTION_TAP_AND_HOLD: {
644                        MotionEvent event = (MotionEvent) message.obj;
645                        final int policyFlags = message.arg1;
646                        onActionTapAndHold(event, policyFlags);
647                    } break;
648                    case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
649                        transitionToState(STATE_DELEGATING);
650                        sendDelayedMotionEvents();
651                        clear();
652                    } break;
653                    default: {
654                        throw new IllegalArgumentException("Unknown message type: " + type);
655                    }
656                }
657            }
658        };
659
660        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
661            cacheDelayedMotionEvent(event, rawEvent, policyFlags);
662            final int action = event.getActionMasked();
663            switch (action) {
664                case MotionEvent.ACTION_DOWN: {
665                    mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
666                    if (!mMagnifiedBounds.contains((int) event.getX(),
667                            (int) event.getY())) {
668                        transitionToDelegatingStateAndClear();
669                        return;
670                    }
671                    if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null
672                            && GestureUtils.isMultiTap(mLastDownEvent, event,
673                                    mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
674                        Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD,
675                                policyFlags, 0, event);
676                        mHandler.sendMessageDelayed(message,
677                                ViewConfiguration.getLongPressTimeout());
678                    } else if (mTapCount < ACTION_TAP_COUNT) {
679                        Message message = mHandler.obtainMessage(
680                                MESSAGE_TRANSITION_TO_DELEGATING_STATE);
681                        mHandler.sendMessageDelayed(message, mMultiTapTimeSlop);
682                    }
683                    clearLastDownEvent();
684                    mLastDownEvent = MotionEvent.obtain(event);
685                } break;
686                case MotionEvent.ACTION_POINTER_DOWN: {
687                    if (mMagnificationController.isMagnifying()) {
688                        transitionToState(STATE_MAGNIFIED_INTERACTION);
689                        clear();
690                    } else {
691                        transitionToDelegatingStateAndClear();
692                    }
693                } break;
694                case MotionEvent.ACTION_MOVE: {
695                    if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) {
696                        final double distance = GestureUtils.computeDistance(mLastDownEvent,
697                                event, 0);
698                        if (Math.abs(distance) > mTapDistanceSlop) {
699                            transitionToDelegatingStateAndClear();
700                        }
701                    }
702                } break;
703                case MotionEvent.ACTION_UP: {
704                    if (mLastDownEvent == null) {
705                        return;
706                    }
707                    mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
708                    if (!mMagnifiedBounds.contains((int) event.getX(), (int) event.getY())) {
709                         transitionToDelegatingStateAndClear();
710                         return;
711                    }
712                    if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop,
713                            mTapDistanceSlop, 0)) {
714                        transitionToDelegatingStateAndClear();
715                        return;
716                    }
717                    if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent,
718                            event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
719                        transitionToDelegatingStateAndClear();
720                        return;
721                    }
722                    mTapCount++;
723                    if (DEBUG_DETECTING) {
724                        Slog.i(LOG_TAG, "Tap count:" + mTapCount);
725                    }
726                    if (mTapCount == ACTION_TAP_COUNT) {
727                        clear();
728                        onActionTap(event, policyFlags);
729                        return;
730                    }
731                    clearLastTapUpEvent();
732                    mLastTapUpEvent = MotionEvent.obtain(event);
733                } break;
734                case MotionEvent.ACTION_POINTER_UP: {
735                    /* do nothing */
736                } break;
737            }
738        }
739
740        public void clear() {
741            mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
742            mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
743            clearTapDetectionState();
744            clearDelayedMotionEvents();
745        }
746
747        private void clearTapDetectionState() {
748            mTapCount = 0;
749            clearLastTapUpEvent();
750            clearLastDownEvent();
751        }
752
753        private void clearLastTapUpEvent() {
754            if (mLastTapUpEvent != null) {
755                mLastTapUpEvent.recycle();
756                mLastTapUpEvent = null;
757            }
758        }
759
760        private void clearLastDownEvent() {
761            if (mLastDownEvent != null) {
762                mLastDownEvent.recycle();
763                mLastDownEvent = null;
764            }
765        }
766
767        private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
768                int policyFlags) {
769            MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent,
770                    policyFlags);
771            if (mDelayedEventQueue == null) {
772                mDelayedEventQueue = info;
773            } else {
774                MotionEventInfo tail = mDelayedEventQueue;
775                while (tail.mNext != null) {
776                    tail = tail.mNext;
777                }
778                tail.mNext = info;
779            }
780        }
781
782        private void sendDelayedMotionEvents() {
783            while (mDelayedEventQueue != null) {
784                MotionEventInfo info = mDelayedEventQueue;
785                mDelayedEventQueue = info.mNext;
786                final long offset = SystemClock.uptimeMillis() - info.mCachedTimeMillis;
787                MotionEvent event = obtainEventWithOffsetTimeAndDownTime(info.mEvent, offset);
788                MotionEvent rawEvent = obtainEventWithOffsetTimeAndDownTime(info.mRawEvent, offset);
789                ScreenMagnifier.this.onMotionEvent(event, rawEvent, info.mPolicyFlags);
790                event.recycle();
791                rawEvent.recycle();
792                info.recycle();
793            }
794        }
795
796        private MotionEvent obtainEventWithOffsetTimeAndDownTime(MotionEvent event, long offset) {
797            final int pointerCount = event.getPointerCount();
798            PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
799            PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
800            for (int i = 0; i < pointerCount; i++) {
801                event.getPointerCoords(i, coords[i]);
802                event.getPointerProperties(i, properties[i]);
803            }
804            final long downTime = event.getDownTime() + offset;
805            final long eventTime = event.getEventTime() + offset;
806            return MotionEvent.obtain(downTime, eventTime,
807                    event.getAction(), pointerCount, properties, coords,
808                    event.getMetaState(), event.getButtonState(),
809                    1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(),
810                    event.getSource(), event.getFlags());
811        }
812
813        private void clearDelayedMotionEvents() {
814            while (mDelayedEventQueue != null) {
815                MotionEventInfo info = mDelayedEventQueue;
816                mDelayedEventQueue = info.mNext;
817                info.recycle();
818            }
819        }
820
821        private void transitionToDelegatingStateAndClear() {
822            transitionToState(STATE_DELEGATING);
823            sendDelayedMotionEvents();
824            clear();
825        }
826
827        private void onActionTap(MotionEvent up, int policyFlags) {
828            if (DEBUG_DETECTING) {
829                Slog.i(LOG_TAG, "onActionTap()");
830            }
831            if (!mMagnificationController.isMagnifying()) {
832                mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
833                        up.getX(), up.getY(), true);
834            } else {
835                mMagnificationController.reset(true);
836            }
837        }
838
839        private void onActionTapAndHold(MotionEvent down, int policyFlags) {
840            if (DEBUG_DETECTING) {
841                Slog.i(LOG_TAG, "onActionTapAndHold()");
842            }
843            clear();
844            mTranslationEnabledBeforePan = mMagnificationController.isMagnifying();
845            mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
846                    down.getX(), down.getY(), true);
847            transitionToState(STATE_VIEWPORT_DRAGGING);
848        }
849    }
850
851    private void persistScale(final float scale) {
852        new AsyncTask<Void, Void, Void>() {
853            @Override
854            protected Void doInBackground(Void... params) {
855                Settings.Secure.putFloat(mContext.getContentResolver(),
856                        Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale);
857                return null;
858            }
859        }.execute();
860    }
861
862    private float getPersistedScale() {
863        return Settings.Secure.getFloat(mContext.getContentResolver(),
864                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
865                DEFAULT_MAGNIFICATION_SCALE);
866    }
867
868    private static boolean isScreenMagnificationAutoUpdateEnabled(Context context) {
869        return (Settings.Secure.getInt(context.getContentResolver(),
870                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
871                DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
872    }
873
874    private static final class MotionEventInfo {
875
876        private static final int MAX_POOL_SIZE = 10;
877
878        private static final Object sLock = new Object();
879        private static MotionEventInfo sPool;
880        private static int sPoolSize;
881
882        private MotionEventInfo mNext;
883        private boolean mInPool;
884
885        public MotionEvent mEvent;
886        public MotionEvent mRawEvent;
887        public int mPolicyFlags;
888        public long mCachedTimeMillis;
889
890        public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent,
891                int policyFlags) {
892            synchronized (sLock) {
893                MotionEventInfo info;
894                if (sPoolSize > 0) {
895                    sPoolSize--;
896                    info = sPool;
897                    sPool = info.mNext;
898                    info.mNext = null;
899                    info.mInPool = false;
900                } else {
901                    info = new MotionEventInfo();
902                }
903                info.initialize(event, rawEvent, policyFlags);
904                return info;
905            }
906        }
907
908        private void initialize(MotionEvent event, MotionEvent rawEvent,
909                int policyFlags) {
910            mEvent = MotionEvent.obtain(event);
911            mRawEvent = MotionEvent.obtain(rawEvent);
912            mPolicyFlags = policyFlags;
913            mCachedTimeMillis = SystemClock.uptimeMillis();
914        }
915
916        public void recycle() {
917            synchronized (sLock) {
918                if (mInPool) {
919                    throw new IllegalStateException("Already recycled.");
920                }
921                clear();
922                if (sPoolSize < MAX_POOL_SIZE) {
923                    sPoolSize++;
924                    mNext = sPool;
925                    sPool = this;
926                    mInPool = true;
927                }
928            }
929        }
930
931        private void clear() {
932            mEvent.recycle();
933            mEvent = null;
934            mRawEvent.recycle();
935            mRawEvent = null;
936            mPolicyFlags = 0;
937            mCachedTimeMillis = 0;
938        }
939    }
940
941    private final class MagnificationController {
942
943        private static final String PROPERTY_NAME_MAGNIFICATION_SPEC =
944                "magnificationSpec";
945
946        private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain();
947
948        private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain();
949
950        private final Rect mTempRect = new Rect();
951
952        private final ValueAnimator mTransformationAnimator;
953
954        public MagnificationController(long animationDuration) {
955            Property<MagnificationController, MagnificationSpec> property =
956                    Property.of(MagnificationController.class, MagnificationSpec.class,
957                    PROPERTY_NAME_MAGNIFICATION_SPEC);
958            TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() {
959                private final MagnificationSpec mTempTransformationSpec =
960                        MagnificationSpec.obtain();
961                @Override
962                public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec,
963                        MagnificationSpec toSpec) {
964                    MagnificationSpec result = mTempTransformationSpec;
965                    result.scale = fromSpec.scale
966                            + (toSpec.scale - fromSpec.scale) * fraction;
967                    result.offsetX = fromSpec.offsetX + (toSpec.offsetX - fromSpec.offsetX)
968                            * fraction;
969                    result.offsetY = fromSpec.offsetY + (toSpec.offsetY - fromSpec.offsetY)
970                            * fraction;
971                    return result;
972                }
973            };
974            mTransformationAnimator = ObjectAnimator.ofObject(this, property,
975                    evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec);
976            mTransformationAnimator.setDuration((long) (animationDuration));
977            mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f));
978        }
979
980        public boolean isMagnifying() {
981            return mCurrentMagnificationSpec.scale > 1.0f;
982        }
983
984        public void reset(boolean animate) {
985            if (mTransformationAnimator.isRunning()) {
986                mTransformationAnimator.cancel();
987            }
988            mCurrentMagnificationSpec.clear();
989            if (animate) {
990                animateMangificationSpec(mSentMagnificationSpec,
991                        mCurrentMagnificationSpec);
992            } else {
993                setMagnificationSpec(mCurrentMagnificationSpec);
994            }
995            Rect bounds = mTempRect;
996            bounds.setEmpty();
997            mAms.onMagnificationStateChanged();
998        }
999
1000        public float getScale() {
1001            return mCurrentMagnificationSpec.scale;
1002        }
1003
1004        public float getOffsetX() {
1005            return mCurrentMagnificationSpec.offsetX;
1006        }
1007
1008        public float getOffsetY() {
1009            return mCurrentMagnificationSpec.offsetY;
1010        }
1011
1012        public void setScale(float scale, float pivotX, float pivotY, boolean animate) {
1013            Rect magnifiedFrame = mTempRect;
1014            mMagnifiedBounds.getBounds(magnifiedFrame);
1015            MagnificationSpec spec = mCurrentMagnificationSpec;
1016            final float oldScale = spec.scale;
1017            final float oldCenterX = (-spec.offsetX + magnifiedFrame.width() / 2) / oldScale;
1018            final float oldCenterY = (-spec.offsetY + magnifiedFrame.height() / 2) / oldScale;
1019            final float normPivotX = (-spec.offsetX + pivotX) / oldScale;
1020            final float normPivotY = (-spec.offsetY + pivotY) / oldScale;
1021            final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale);
1022            final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale);
1023            final float centerX = normPivotX + offsetX;
1024            final float centerY = normPivotY + offsetY;
1025            setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate);
1026        }
1027
1028        public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) {
1029            setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.scale, centerX, centerY,
1030                    animate);
1031        }
1032
1033        public void offsetMagnifiedRegionCenter(float offsetX, float offsetY) {
1034            final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX;
1035            mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX,
1036                    getMinOffsetX()), 0);
1037            final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY;
1038            mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY,
1039                    getMinOffsetY()), 0);
1040            setMagnificationSpec(mCurrentMagnificationSpec);
1041        }
1042
1043        public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY,
1044                boolean animate) {
1045            if (Float.compare(mCurrentMagnificationSpec.scale, scale) == 0
1046                    && Float.compare(mCurrentMagnificationSpec.offsetX,
1047                            centerX) == 0
1048                    && Float.compare(mCurrentMagnificationSpec.offsetY,
1049                            centerY) == 0) {
1050                return;
1051            }
1052            if (mTransformationAnimator.isRunning()) {
1053                mTransformationAnimator.cancel();
1054            }
1055            if (DEBUG_MAGNIFICATION_CONTROLLER) {
1056                Slog.i(LOG_TAG, "scale: " + scale + " offsetX: " + centerX
1057                        + " offsetY: " + centerY);
1058            }
1059            updateMagnificationSpec(scale, centerX, centerY);
1060            if (animate) {
1061                animateMangificationSpec(mSentMagnificationSpec,
1062                        mCurrentMagnificationSpec);
1063            } else {
1064                setMagnificationSpec(mCurrentMagnificationSpec);
1065            }
1066            mAms.onMagnificationStateChanged();
1067        }
1068
1069        public void updateMagnificationSpec(float scale, float magnifiedCenterX,
1070                float magnifiedCenterY) {
1071            Rect magnifiedFrame = mTempRect;
1072            mMagnifiedBounds.getBounds(magnifiedFrame);
1073            mCurrentMagnificationSpec.scale = scale;
1074            final int viewportWidth = magnifiedFrame.width();
1075            final float nonNormOffsetX = viewportWidth / 2 - magnifiedCenterX * scale;
1076            mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX,
1077                    getMinOffsetX()), 0);
1078            final int viewportHeight = magnifiedFrame.height();
1079            final float nonNormOffsetY = viewportHeight / 2 - magnifiedCenterY * scale;
1080            mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY,
1081                    getMinOffsetY()), 0);
1082        }
1083
1084        private float getMinOffsetX() {
1085            Rect magnifiedFrame = mTempRect;
1086            mMagnifiedBounds.getBounds(magnifiedFrame);
1087            final float viewportWidth = magnifiedFrame.width();
1088            return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale;
1089        }
1090
1091        private float getMinOffsetY() {
1092            Rect magnifiedFrame = mTempRect;
1093            mMagnifiedBounds.getBounds(magnifiedFrame);
1094            final float viewportHeight = magnifiedFrame.height();
1095            return viewportHeight - viewportHeight * mCurrentMagnificationSpec.scale;
1096        }
1097
1098        private void animateMangificationSpec(MagnificationSpec fromSpec,
1099                MagnificationSpec toSpec) {
1100            mTransformationAnimator.setObjectValues(fromSpec, toSpec);
1101            mTransformationAnimator.start();
1102        }
1103
1104        public MagnificationSpec getMagnificationSpec() {
1105            return mSentMagnificationSpec;
1106        }
1107
1108        public void setMagnificationSpec(MagnificationSpec spec) {
1109            if (DEBUG_SET_MAGNIFICATION_SPEC) {
1110                Slog.i(LOG_TAG, "Sending: " + spec);
1111            }
1112            mSentMagnificationSpec.scale = spec.scale;
1113            mSentMagnificationSpec.offsetX = spec.offsetX;
1114            mSentMagnificationSpec.offsetY = spec.offsetY;
1115            mWindowManager.setMagnificationSpec(MagnificationSpec.obtain(spec));
1116        }
1117    }
1118
1119    private final class ScreenStateObserver extends BroadcastReceiver {
1120        private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1;
1121
1122        private final Context mContext;
1123        private final MagnificationController mMagnificationController;
1124
1125        private final Handler mHandler = new Handler() {
1126            @Override
1127            public void handleMessage(Message message) {
1128                 switch (message.what) {
1129                    case MESSAGE_ON_SCREEN_STATE_CHANGE: {
1130                        String action = (String) message.obj;
1131                        handleOnScreenStateChange(action);
1132                    } break;
1133                }
1134            }
1135        };
1136
1137        public ScreenStateObserver(Context context,
1138                MagnificationController magnificationController) {
1139            mContext = context;
1140            mMagnificationController = magnificationController;
1141            mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
1142        }
1143
1144        public void destroy() {
1145            mContext.unregisterReceiver(this);
1146        }
1147
1148        @Override
1149        public void onReceive(Context context, Intent intent) {
1150            mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE,
1151                    intent.getAction()).sendToTarget();
1152        }
1153
1154        private void handleOnScreenStateChange(String action) {
1155            if (mMagnificationController.isMagnifying()
1156                    && isScreenMagnificationAutoUpdateEnabled(mContext)) {
1157                mMagnificationController.reset(false);
1158            }
1159        }
1160    }
1161}
1162