ScreenMagnifier.java revision 662538957f12a7b7c534382a6a4f306d5777375b
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.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.ObjectAnimator;
22import android.animation.TypeEvaluator;
23import android.animation.ValueAnimator;
24import android.content.Context;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.PixelFormat;
28import android.graphics.PointF;
29import android.graphics.PorterDuff.Mode;
30import android.graphics.Rect;
31import android.graphics.drawable.Drawable;
32import android.hardware.display.DisplayManager;
33import android.hardware.display.DisplayManager.DisplayListener;
34import android.os.AsyncTask;
35import android.os.Handler;
36import android.os.Message;
37import android.os.RemoteException;
38import android.os.ServiceManager;
39import android.provider.Settings;
40import android.util.MathUtils;
41import android.util.Property;
42import android.util.Slog;
43import android.view.Display;
44import android.view.DisplayInfo;
45import android.view.Gravity;
46import android.view.IDisplayContentChangeListener;
47import android.view.IWindowManager;
48import android.view.MotionEvent;
49import android.view.MotionEvent.PointerCoords;
50import android.view.MotionEvent.PointerProperties;
51import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener;
52import android.view.Surface;
53import android.view.View;
54import android.view.ViewConfiguration;
55import android.view.ViewGroup;
56import android.view.WindowInfo;
57import android.view.WindowManager;
58import android.view.WindowManagerPolicy;
59import android.view.accessibility.AccessibilityEvent;
60import android.view.animation.DecelerateInterpolator;
61import android.view.animation.Interpolator;
62
63import com.android.internal.R;
64import com.android.internal.os.SomeArgs;
65
66import java.util.ArrayList;
67
68/**
69 * This class handles the screen magnification when accessibility is enabled.
70 * The behavior is as follows:
71 *
72 * 1. Triple tap toggles permanent screen magnification which is magnifying
73 *    the area around the location of the triple tap. One can think of the
74 *    location of the triple tap as the center of the magnified viewport.
75 *    For example, a triple tap when not magnified would magnify the screen
76 *    and leave it in a magnified state. A triple tapping when magnified would
77 *    clear magnification and leave the screen in a not magnified state.
78 *
79 * 2. Triple tap and hold would magnify the screen if not magnified and enable
80 *    viewport dragging mode until the finger goes up. One can think of this
81 *    mode as a way to move the magnified viewport since the area around the
82 *    moving finger will be magnified to fit the screen. For example, if the
83 *    screen was not magnified and the user triple taps and holds the screen
84 *    would magnify and the viewport will follow the user's finger. When the
85 *    finger goes up the screen will clear zoom out. If the same user interaction
86 *    is performed when the screen is magnified, the viewport movement will
87 *    be the same but when the finger goes up the screen will stay magnified.
88 *    In other words, the initial magnified state is sticky.
89 *
90 * 3. Pinching with any number of additional fingers when viewport dragging
91 *    is enabled, i.e. the user triple tapped and holds, would adjust the
92 *    magnification scale which will become the current default magnification
93 *    scale. The next time the user magnifies the same magnification scale
94 *    would be used.
95 *
96 * 4. When in a permanent magnified state the user can use two or more fingers
97 *    to pan the viewport. Note that in this mode the content is panned as
98 *    opposed to the viewport dragging mode in which the viewport is moved.
99 *
100 * 5. When in a permanent magnified state the user can use three or more
101 *    fingers to change the magnification scale which will become the current
102 *    default magnification scale. The next time the user magnifies the same
103 *    magnification scale would be used.
104 *
105 * 6. The magnification scale will be persisted in settings and in the cloud.
106 */
107public final class ScreenMagnifier implements EventStreamTransformation {
108
109    private static final boolean DEBUG_STATE_TRANSITIONS = false;
110    private static final boolean DEBUG_DETECTING = false;
111    private static final boolean DEBUG_TRANSFORMATION = false;
112    private static final boolean DEBUG_PANNING = false;
113    private static final boolean DEBUG_SCALING = false;
114    private static final boolean DEBUG_VIEWPORT_WINDOW = false;
115    private static final boolean DEBUG_WINDOW_TRANSITIONS = false;
116    private static final boolean DEBUG_ROTATION = false;
117    private static final boolean DEBUG_GESTURE_DETECTOR = false;
118    private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false;
119
120    private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName();
121
122    private static final int STATE_DELEGATING = 1;
123    private static final int STATE_DETECTING = 2;
124    private static final int STATE_VIEWPORT_DRAGGING = 3;
125    private static final int STATE_MAGNIFIED_INTERACTION = 4;
126
127    private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;
128    private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1;
129    private static final float DEFAULT_WINDOW_ANIMATION_SCALE = 1.0f;
130
131    private final IWindowManager mWindowManagerService = IWindowManager.Stub.asInterface(
132            ServiceManager.getService("window"));
133    private final WindowManager mWindowManager;
134    private final DisplayProvider mDisplayProvider;
135
136    private final DetectingStateHandler mDetectingStateHandler = new DetectingStateHandler();
137    private final GestureDetector mGestureDetector;
138    private final StateViewportDraggingHandler mStateViewportDraggingHandler =
139            new StateViewportDraggingHandler();
140
141    private final Interpolator mInterpolator = new DecelerateInterpolator(2.5f);
142
143    private final MagnificationController mMagnificationController;
144    private final DisplayContentObserver mDisplayContentObserver;
145    private final Viewport mViewport;
146
147    private final int mTapTimeSlop = ViewConfiguration.getTapTimeout();
148    private final int mMultiTapTimeSlop = ViewConfiguration.getDoubleTapTimeout();
149    private final int mTapDistanceSlop;
150    private final int mMultiTapDistanceSlop;
151
152    private final int mShortAnimationDuration;
153    private final int mLongAnimationDuration;
154    private final float mWindowAnimationScale;
155
156    private final Context mContext;
157
158    private EventStreamTransformation mNext;
159
160    private int mCurrentState;
161    private int mPreviousState;
162    private boolean mTranslationEnabledBeforePan;
163
164    private PointerCoords[] mTempPointerCoords;
165    private PointerProperties[] mTempPointerProperties;
166
167    public ScreenMagnifier(Context context) {
168        mContext = context;
169        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
170
171        mShortAnimationDuration = context.getResources().getInteger(
172                com.android.internal.R.integer.config_shortAnimTime);
173        mLongAnimationDuration = context.getResources().getInteger(
174                com.android.internal.R.integer.config_longAnimTime);
175        mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop();
176        mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
177        mWindowAnimationScale = Settings.System.getFloat(context.getContentResolver(),
178                Settings.System.WINDOW_ANIMATION_SCALE, DEFAULT_WINDOW_ANIMATION_SCALE);
179
180        mMagnificationController = new MagnificationController(mShortAnimationDuration);
181        mDisplayProvider = new DisplayProvider(context, mWindowManager);
182        mViewport = new Viewport(mContext, mWindowManager, mWindowManagerService,
183                mDisplayProvider, mInterpolator, mShortAnimationDuration);
184        mDisplayContentObserver = new DisplayContentObserver(mContext, mViewport,
185                mMagnificationController, mWindowManagerService, mDisplayProvider,
186                mLongAnimationDuration, mWindowAnimationScale);
187
188        mGestureDetector = new GestureDetector(context);
189
190        transitionToState(STATE_DETECTING);
191    }
192
193    @Override
194    public void onMotionEvent(MotionEvent event, int policyFlags) {
195        mGestureDetector.onMotionEvent(event);
196        switch (mCurrentState) {
197            case STATE_DELEGATING: {
198                handleMotionEventStateDelegating(event, policyFlags);
199            } break;
200            case STATE_DETECTING: {
201                mDetectingStateHandler.onMotionEvent(event, policyFlags);
202            } break;
203            case STATE_VIEWPORT_DRAGGING: {
204                mStateViewportDraggingHandler.onMotionEvent(event, policyFlags);
205            } break;
206            case STATE_MAGNIFIED_INTERACTION: {
207                // Handled by the gesture detector. Since the detector
208                // needs all touch events to work properly we cannot
209                // call it only for this state.
210            } break;
211            default: {
212                throw new IllegalStateException("Unknown state: " + mCurrentState);
213            }
214        }
215    }
216
217    @Override
218    public void onAccessibilityEvent(AccessibilityEvent event) {
219        if (mNext != null) {
220            mNext.onAccessibilityEvent(event);
221        }
222    }
223
224    @Override
225    public void setNext(EventStreamTransformation next) {
226        mNext = next;
227    }
228
229    @Override
230    public void clear() {
231        mCurrentState = STATE_DETECTING;
232        mDetectingStateHandler.clear();
233        mStateViewportDraggingHandler.clear();
234        mGestureDetector.clear();
235        if (mNext != null) {
236            mNext.clear();
237        }
238    }
239
240    @Override
241    public void onDestroy() {
242        mMagnificationController.setScaleAndMagnifiedRegionCenter(1.0f,
243                0, 0, true);
244        mViewport.setFrameShown(false, true);
245        mDisplayProvider.destroy();
246        mDisplayContentObserver.destroy();
247    }
248
249    private void handleMotionEventStateDelegating(MotionEvent event, int policyFlags) {
250        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
251            if (mDetectingStateHandler.mDelayedEventQueue == null) {
252                transitionToState(STATE_DETECTING);
253            }
254        }
255        if (mNext != null) {
256            // If the event is within the magnified portion of the screen we have
257            // to change its location to be where the user thinks he is poking the
258            // UI which may have been magnified and panned.
259            final float eventX = event.getX();
260            final float eventY = event.getY();
261            if (mMagnificationController.isMagnifying()
262                    && mViewport.getBounds().contains((int) eventX, (int) eventY)) {
263                final float scale = mMagnificationController.getScale();
264                final float scaledOffsetX = mMagnificationController.getScaledOffsetX();
265                final float scaledOffsetY = mMagnificationController.getScaledOffsetY();
266                final int pointerCount = event.getPointerCount();
267                PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
268                PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
269                for (int i = 0; i < pointerCount; i++) {
270                    event.getPointerCoords(i, coords[i]);
271                    coords[i].x = (coords[i].x - scaledOffsetX) / scale;
272                    coords[i].y = (coords[i].y - scaledOffsetY) / scale;
273                    event.getPointerProperties(i, properties[i]);
274                }
275                event = MotionEvent.obtain(event.getDownTime(),
276                        event.getEventTime(), event.getAction(), pointerCount, properties,
277                        coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(),
278                        event.getFlags());
279            }
280            mNext.onMotionEvent(event, policyFlags);
281        }
282    }
283
284    private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
285        final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
286        if (oldSize < size) {
287            PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
288            mTempPointerCoords = new PointerCoords[size];
289            if (oldTempPointerCoords != null) {
290                System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
291            }
292        }
293        for (int i = oldSize; i < size; i++) {
294            mTempPointerCoords[i] = new PointerCoords();
295        }
296        return mTempPointerCoords;
297    }
298
299    private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
300        final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0;
301        if (oldSize < size) {
302            PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
303            mTempPointerProperties = new PointerProperties[size];
304            if (oldTempPointerProperties != null) {
305                System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize);
306            }
307        }
308        for (int i = oldSize; i < size; i++) {
309            mTempPointerProperties[i] = new PointerProperties();
310        }
311        return mTempPointerProperties;
312    }
313
314    private void transitionToState(int state) {
315        if (DEBUG_STATE_TRANSITIONS) {
316            switch (state) {
317                case STATE_DELEGATING: {
318                    Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING");
319                } break;
320                case STATE_DETECTING: {
321                    Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING");
322                } break;
323                case STATE_VIEWPORT_DRAGGING: {
324                    Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING");
325                } break;
326                case STATE_MAGNIFIED_INTERACTION: {
327                    Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION");
328                } break;
329                default: {
330                    throw new IllegalArgumentException("Unknown state: " + state);
331                }
332            }
333        }
334        mPreviousState = mCurrentState;
335        mCurrentState = state;
336    }
337
338    private final class GestureDetector implements OnScaleGestureListener {
339        private static final float MIN_SCALE = 1.3f;
340        private static final float MAX_SCALE = 5.0f;
341
342        private static final float DETECT_SCALING_THRESHOLD = 0.30f;
343        private static final int DETECT_PANNING_THRESHOLD_DIP = 30;
344
345        private final float mScaledDetectPanningThreshold;
346
347        private final ScaleGestureDetector mScaleGestureDetector;
348
349        private final PointF mPrevFocus = new PointF(Float.NaN, Float.NaN);
350        private final PointF mInitialFocus = new PointF(Float.NaN, Float.NaN);
351
352        private float mCurrScale = Float.NaN;
353        private float mCurrScaleFactor = 1.0f;
354        private float mPrevScaleFactor = 1.0f;
355        private float mCurrPan;
356        private float mPrevPan;
357
358        private float mScaleFocusX = Float.NaN;
359        private float mScaleFocusY = Float.NaN;
360
361        private boolean mScaling;
362        private boolean mPanning;
363
364        public GestureDetector(Context context) {
365            final float density = context.getResources().getDisplayMetrics().density;
366            mScaledDetectPanningThreshold = DETECT_PANNING_THRESHOLD_DIP * density;
367            mScaleGestureDetector = new ScaleGestureDetector(this);
368        }
369
370        public void onMotionEvent(MotionEvent event) {
371            mScaleGestureDetector.onTouchEvent(event);
372            switch (mCurrentState) {
373                case STATE_DETECTING:
374                case STATE_DELEGATING:
375                case STATE_VIEWPORT_DRAGGING: {
376                    return;
377                }
378            }
379            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
380                clear();
381                final float scale = mMagnificationController.getScale();
382                if (scale != getPersistedScale()) {
383                    persistScale(scale);
384                }
385                if (mPreviousState == STATE_VIEWPORT_DRAGGING) {
386                    transitionToState(STATE_VIEWPORT_DRAGGING);
387                } else {
388                    transitionToState(STATE_DETECTING);
389                }
390            }
391        }
392
393        @Override
394        public boolean onScale(ScaleGestureDetector detector) {
395            switch (mCurrentState) {
396                case STATE_DETECTING:
397                case STATE_DELEGATING:
398                case STATE_VIEWPORT_DRAGGING: {
399                    return true;
400                }
401                case STATE_MAGNIFIED_INTERACTION: {
402                    mCurrScaleFactor = mScaleGestureDetector.getScaleFactor();
403                    final float scaleDelta = Math.abs(1.0f - mCurrScaleFactor * mPrevScaleFactor);
404                    if (DEBUG_GESTURE_DETECTOR) {
405                        Slog.i(LOG_TAG, "scaleDelta: " + scaleDelta);
406                    }
407                    if (!mScaling && scaleDelta > DETECT_SCALING_THRESHOLD) {
408                        mScaling = true;
409                        clearContextualState();
410                        return true;
411                    }
412                    if (mScaling) {
413                        performScale(detector);
414                    }
415                    mCurrPan = (float) MathUtils.dist(
416                            mScaleGestureDetector.getFocusX(),
417                            mScaleGestureDetector.getFocusY(),
418                            mInitialFocus.x, mInitialFocus.y);
419                    final float panDelta = mCurrPan + mPrevPan;
420                    if (DEBUG_GESTURE_DETECTOR) {
421                        Slog.i(LOG_TAG, "panDelta: " + panDelta);
422                    }
423                    if (!mPanning && panDelta > mScaledDetectPanningThreshold) {
424                        mPanning = true;
425                        clearContextualState();
426                        return true;
427                    }
428                    if (mPanning) {
429                        performPan(detector);
430                    }
431                } break;
432            }
433            return false;
434        }
435
436        @Override
437        public boolean onScaleBegin(ScaleGestureDetector detector) {
438            mPrevScaleFactor *= mCurrScaleFactor;
439            mCurrScale = Float.NaN;
440            mPrevPan += mCurrPan;
441            mPrevFocus.x = mInitialFocus.x = detector.getFocusX();
442            mPrevFocus.y = mInitialFocus.y = detector.getFocusY();
443            return true;
444        }
445
446        @Override
447        public void onScaleEnd(ScaleGestureDetector detector) {
448            clearContextualState();
449        }
450
451        public void clear() {
452            clearContextualState();
453            mScaling = false;
454            mPanning = false;
455        }
456
457        private void clearContextualState() {
458            mCurrScaleFactor = 1.0f;
459            mPrevScaleFactor = 1.0f;
460            mPrevPan = 0;
461            mCurrPan = 0;
462            mInitialFocus.set(Float.NaN, Float.NaN);
463            mPrevFocus.set(Float.NaN, Float.NaN);
464            mCurrScale = Float.NaN;
465            mScaleFocusX = Float.NaN;
466            mScaleFocusY = Float.NaN;
467        }
468
469        private void performPan(ScaleGestureDetector detector) {
470            if (Float.compare(mPrevFocus.x, Float.NaN) == 0
471                    && Float.compare(mPrevFocus.y, Float.NaN) == 0) {
472                mPrevFocus.set(detector.getFocusX(), detector.getFocusY());
473                return;
474            }
475            final float scale = mMagnificationController.getScale();
476            final float scrollX = (detector.getFocusX() - mPrevFocus.x) / scale;
477            final float scrollY = (detector.getFocusY() - mPrevFocus.y) / scale;
478            final float centerX = mMagnificationController.getMagnifiedRegionCenterX()
479                    - scrollX;
480            final float centerY = mMagnificationController.getMagnifiedRegionCenterY()
481                    - scrollY;
482            if (DEBUG_PANNING) {
483                Slog.i(LOG_TAG, "Panned content by scrollX: " + scrollX
484                        + " scrollY: " + scrollY);
485            }
486            mMagnificationController.setMagnifiedRegionCenter(centerX, centerY, false);
487            mPrevFocus.set(detector.getFocusX(), detector.getFocusY());
488        }
489
490        private void performScale(ScaleGestureDetector detector) {
491            if (Float.compare(mCurrScale, Float.NaN) == 0) {
492                mCurrScale = mMagnificationController.getScale();
493                return;
494            }
495            final float totalScaleFactor = mPrevScaleFactor * detector.getScaleFactor();
496            final float newScale = mCurrScale * totalScaleFactor;
497            final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE),
498                    MAX_SCALE);
499            if (DEBUG_SCALING) {
500                Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale);
501            }
502            if (Float.compare(mScaleFocusX, Float.NaN) == 0
503                    && Float.compare(mScaleFocusY, Float.NaN) == 0) {
504                mScaleFocusX = detector.getFocusX();
505                mScaleFocusY = detector.getFocusY();
506            }
507            mMagnificationController.setScale(normalizedNewScale, mScaleFocusX,
508                    mScaleFocusY, false);
509        }
510    }
511
512    private final class StateViewportDraggingHandler {
513        private boolean mLastMoveOutsideMagnifiedRegion;
514
515        private void onMotionEvent(MotionEvent event, int policyFlags) {
516            final int action = event.getActionMasked();
517            switch (action) {
518                case MotionEvent.ACTION_DOWN: {
519                    throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN");
520                }
521                case MotionEvent.ACTION_POINTER_DOWN: {
522                    clear();
523                    transitionToState(STATE_MAGNIFIED_INTERACTION);
524                } break;
525                case MotionEvent.ACTION_MOVE: {
526                    if (event.getPointerCount() != 1) {
527                        throw new IllegalStateException("Should have one pointer down.");
528                    }
529                    final float eventX = event.getX();
530                    final float eventY = event.getY();
531                    if (mViewport.getBounds().contains((int) eventX, (int) eventY)) {
532                        if (mLastMoveOutsideMagnifiedRegion) {
533                            mLastMoveOutsideMagnifiedRegion = false;
534                            mMagnificationController.setMagnifiedRegionCenter(eventX,
535                                    eventY, true);
536                        } else {
537                            mMagnificationController.setMagnifiedRegionCenter(eventX,
538                                    eventY, false);
539                        }
540                    } else {
541                        mLastMoveOutsideMagnifiedRegion = true;
542                    }
543                } break;
544                case MotionEvent.ACTION_UP: {
545                    if (!mTranslationEnabledBeforePan) {
546                        mMagnificationController.reset(true);
547                        mViewport.setFrameShown(false, true);
548                    }
549                    clear();
550                    transitionToState(STATE_DETECTING);
551                } break;
552                case MotionEvent.ACTION_POINTER_UP: {
553                    throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP");
554                }
555            }
556        }
557
558        public void clear() {
559            mLastMoveOutsideMagnifiedRegion = false;
560        }
561    }
562
563    private final class DetectingStateHandler {
564
565        private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1;
566
567        private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
568
569        private static final int ACTION_TAP_COUNT = 3;
570
571        private MotionEventInfo mDelayedEventQueue;
572
573        private MotionEvent mLastDownEvent;
574        private MotionEvent mLastTapUpEvent;
575        private int mTapCount;
576
577        private final Handler mHandler = new Handler() {
578            @Override
579            public void handleMessage(Message message) {
580                final int type = message.what;
581                switch (type) {
582                    case MESSAGE_ON_ACTION_TAP_AND_HOLD: {
583                        MotionEvent event = (MotionEvent) message.obj;
584                        final int policyFlags = message.arg1;
585                        onActionTapAndHold(event, policyFlags);
586                    } break;
587                    case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
588                        transitionToState(STATE_DELEGATING);
589                        sendDelayedMotionEvents();
590                        clear();
591                    } break;
592                    default: {
593                        throw new IllegalArgumentException("Unknown message type: " + type);
594                    }
595                }
596            }
597        };
598
599        public void onMotionEvent(MotionEvent event, int policyFlags) {
600            cacheDelayedMotionEvent(event, policyFlags);
601            final int action = event.getActionMasked();
602            switch (action) {
603                case MotionEvent.ACTION_DOWN: {
604                    mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
605                    if (!mViewport.getBounds().contains((int) event.getX(),
606                            (int) event.getY())) {
607                        transitionToDelegatingStateAndClear();
608                        return;
609                    }
610                    if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null
611                            && GestureUtils.isMultiTap(mLastDownEvent, event,
612                                    mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
613                        Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD,
614                                policyFlags, 0, event);
615                        mHandler.sendMessageDelayed(message,
616                                ViewConfiguration.getLongPressTimeout());
617                    } else if (mTapCount < ACTION_TAP_COUNT) {
618                        Message message = mHandler.obtainMessage(
619                                MESSAGE_TRANSITION_TO_DELEGATING_STATE);
620                        mHandler.sendMessageDelayed(message, mTapTimeSlop + mMultiTapDistanceSlop);
621                    }
622                    clearLastDownEvent();
623                    mLastDownEvent = MotionEvent.obtain(event);
624                } break;
625                case MotionEvent.ACTION_POINTER_DOWN: {
626                    if (mMagnificationController.isMagnifying()) {
627                        transitionToState(STATE_MAGNIFIED_INTERACTION);
628                        clear();
629                    } else {
630                        transitionToDelegatingStateAndClear();
631                    }
632                } break;
633                case MotionEvent.ACTION_MOVE: {
634                    if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) {
635                        final double distance = GestureUtils.computeDistance(mLastDownEvent,
636                                event, 0);
637                        if (Math.abs(distance) > mTapDistanceSlop) {
638                            transitionToDelegatingStateAndClear();
639                        }
640                    }
641                } break;
642                case MotionEvent.ACTION_UP: {
643                    if (mLastDownEvent == null) {
644                        return;
645                    }
646                    mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
647                    if (!mViewport.getBounds().contains((int) event.getX(), (int) event.getY())) {
648                         transitionToDelegatingStateAndClear();
649                         return;
650                    }
651                    if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop,
652                            mTapDistanceSlop, 0)) {
653                        transitionToDelegatingStateAndClear();
654                        return;
655                    }
656                    if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent,
657                            event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
658                        transitionToDelegatingStateAndClear();
659                        return;
660                    }
661                    mTapCount++;
662                    if (DEBUG_DETECTING) {
663                        Slog.i(LOG_TAG, "Tap count:" + mTapCount);
664                    }
665                    if (mTapCount == ACTION_TAP_COUNT) {
666                        clear();
667                        onActionTap(event, policyFlags);
668                        return;
669                    }
670                    clearLastTapUpEvent();
671                    mLastTapUpEvent = MotionEvent.obtain(event);
672                } break;
673                case MotionEvent.ACTION_POINTER_UP: {
674                    /* do nothing */
675                } break;
676            }
677        }
678
679        public void clear() {
680            mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
681            mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
682            clearTapDetectionState();
683            clearDelayedMotionEvents();
684        }
685
686        private void clearTapDetectionState() {
687            mTapCount = 0;
688            clearLastTapUpEvent();
689            clearLastDownEvent();
690        }
691
692        private void clearLastTapUpEvent() {
693            if (mLastTapUpEvent != null) {
694                mLastTapUpEvent.recycle();
695                mLastTapUpEvent = null;
696            }
697        }
698
699        private void clearLastDownEvent() {
700            if (mLastDownEvent != null) {
701                mLastDownEvent.recycle();
702                mLastDownEvent = null;
703            }
704        }
705
706        private void cacheDelayedMotionEvent(MotionEvent event, int policyFlags) {
707            MotionEventInfo info = MotionEventInfo.obtain(event, policyFlags);
708            if (mDelayedEventQueue == null) {
709                mDelayedEventQueue = info;
710            } else {
711                MotionEventInfo tail = mDelayedEventQueue;
712                while (tail.mNext != null) {
713                    tail = tail.mNext;
714                }
715                tail.mNext = info;
716            }
717        }
718
719        private void sendDelayedMotionEvents() {
720            while (mDelayedEventQueue != null) {
721                MotionEventInfo info = mDelayedEventQueue;
722                mDelayedEventQueue = info.mNext;
723                ScreenMagnifier.this.onMotionEvent(info.mEvent, info.mPolicyFlags);
724                info.recycle();
725            }
726        }
727
728        private void clearDelayedMotionEvents() {
729            while (mDelayedEventQueue != null) {
730                MotionEventInfo info = mDelayedEventQueue;
731                mDelayedEventQueue = info.mNext;
732                info.recycle();
733            }
734        }
735
736        private void transitionToDelegatingStateAndClear() {
737            transitionToState(STATE_DELEGATING);
738            sendDelayedMotionEvents();
739            clear();
740        }
741
742        private void onActionTap(MotionEvent up, int policyFlags) {
743            if (DEBUG_DETECTING) {
744                Slog.i(LOG_TAG, "onActionTap()");
745            }
746            if (!mMagnificationController.isMagnifying()) {
747                mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
748                        up.getX(), up.getY(), true);
749                mViewport.setFrameShown(true, true);
750            } else {
751                mMagnificationController.reset(true);
752                mViewport.setFrameShown(false, true);
753            }
754        }
755
756        private void onActionTapAndHold(MotionEvent down, int policyFlags) {
757            if (DEBUG_DETECTING) {
758                Slog.i(LOG_TAG, "onActionTapAndHold()");
759            }
760            clear();
761            mTranslationEnabledBeforePan = mMagnificationController.isMagnifying();
762            mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
763                    down.getX(), down.getY(), true);
764            mViewport.setFrameShown(true, true);
765            transitionToState(STATE_VIEWPORT_DRAGGING);
766        }
767    }
768
769    private void persistScale(final float scale) {
770        new AsyncTask<Void, Void, Void>() {
771            @Override
772            protected Void doInBackground(Void... params) {
773                Settings.Secure.putFloat(mContext.getContentResolver(),
774                        Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale);
775                return null;
776            }
777        }.execute();
778    }
779
780    private float getPersistedScale() {
781        return Settings.Secure.getFloat(mContext.getContentResolver(),
782                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
783                DEFAULT_MAGNIFICATION_SCALE);
784    }
785
786    private static final class MotionEventInfo {
787
788        private static final int MAX_POOL_SIZE = 10;
789
790        private static final Object sLock = new Object();
791        private static MotionEventInfo sPool;
792        private static int sPoolSize;
793
794        private MotionEventInfo mNext;
795        private boolean mInPool;
796
797        public MotionEvent mEvent;
798        public int mPolicyFlags;
799
800        public static MotionEventInfo obtain(MotionEvent event, int policyFlags) {
801            synchronized (sLock) {
802                MotionEventInfo info;
803                if (sPoolSize > 0) {
804                    sPoolSize--;
805                    info = sPool;
806                    sPool = info.mNext;
807                    info.mNext = null;
808                    info.mInPool = false;
809                } else {
810                    info = new MotionEventInfo();
811                }
812                info.initialize(event, policyFlags);
813                return info;
814            }
815        }
816
817        private void initialize(MotionEvent event, int policyFlags) {
818            mEvent = MotionEvent.obtain(event);
819            mPolicyFlags = policyFlags;
820        }
821
822        public void recycle() {
823            synchronized (sLock) {
824                if (mInPool) {
825                    throw new IllegalStateException("Already recycled.");
826                }
827                clear();
828                if (sPoolSize < MAX_POOL_SIZE) {
829                    sPoolSize++;
830                    mNext = sPool;
831                    sPool = this;
832                    mInPool = true;
833                }
834            }
835        }
836
837        private void clear() {
838            mEvent.recycle();
839            mEvent = null;
840            mPolicyFlags = 0;
841        }
842    }
843
844    private static final class DisplayContentObserver {
845
846        private static final int MESSAGE_SHOW_VIEWPORT_FRAME = 1;
847        private static final int MESSAGE_RECOMPUTE_VIEWPORT_BOUNDS = 2;
848        private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 3;
849        private static final int MESSAGE_ON_WINDOW_TRANSITION = 4;
850        private static final int MESSAGE_ON_ROTATION_CHANGED = 5;
851
852        private final Handler mHandler = new MyHandler();
853
854        private final Rect mTempRect = new Rect();
855
856        private final IDisplayContentChangeListener mDisplayContentChangeListener;
857
858        private final Context mContext;
859        private final Viewport mViewport;
860        private final MagnificationController mMagnificationController;
861        private final IWindowManager mWindowManagerService;
862        private final DisplayProvider mDisplayProvider;
863        private final long mLongAnimationDuration;
864        private final float mWindowAnimationScale;
865
866        public DisplayContentObserver(Context context, Viewport viewport,
867                MagnificationController magnificationController,
868                IWindowManager windowManagerService, DisplayProvider displayProvider,
869                long longAnimationDuration, float windowAnimationScale) {
870            mContext = context;
871            mViewport = viewport;
872            mMagnificationController = magnificationController;
873            mWindowManagerService = windowManagerService;
874            mDisplayProvider = displayProvider;
875            mLongAnimationDuration = longAnimationDuration;
876            mWindowAnimationScale = windowAnimationScale;
877
878            mDisplayContentChangeListener = new IDisplayContentChangeListener.Stub() {
879                @Override
880                public void onWindowTransition(int displayId, int transition, WindowInfo info) {
881                    mHandler.obtainMessage(MESSAGE_ON_WINDOW_TRANSITION, transition, 0,
882                            WindowInfo.obtain(info)).sendToTarget();
883                }
884
885                @Override
886                public void onRectangleOnScreenRequested(int dsiplayId, Rect rectangle,
887                        boolean immediate) {
888                    SomeArgs args = SomeArgs.obtain();
889                    args.argi1 = rectangle.left;
890                    args.argi2 = rectangle.top;
891                    args.argi3 = rectangle.right;
892                    args.argi4 = rectangle.bottom;
893                    mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, 0,
894                            immediate ? 1 : 0, args).sendToTarget();
895                }
896
897                @Override
898                public void onRotationChanged(int rotation) throws RemoteException {
899                    mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0)
900                            .sendToTarget();
901                }
902            };
903
904            try {
905                mWindowManagerService.addDisplayContentChangeListener(
906                        mDisplayProvider.getDisplay().getDisplayId(),
907                        mDisplayContentChangeListener);
908            } catch (RemoteException re) {
909                /* ignore */
910            }
911        }
912
913        public void destroy() {
914            try {
915                mWindowManagerService.removeDisplayContentChangeListener(
916                        mDisplayProvider.getDisplay().getDisplayId(),
917                        mDisplayContentChangeListener);
918            } catch (RemoteException re) {
919                /* ignore*/
920            }
921        }
922
923        private void handleOnRotationChanged(int rotation) {
924            if (DEBUG_ROTATION) {
925                Slog.i(LOG_TAG, "Rotation: " + rotationToString(rotation));
926            }
927            resetMagnificationIfNeeded();
928            mViewport.setFrameShown(false, false);
929            mViewport.rotationChanged();
930            mViewport.recomputeBounds(false);
931            if (mMagnificationController.isMagnifying()) {
932                final long delay = (long) (2 * mLongAnimationDuration * mWindowAnimationScale);
933                Message message = mHandler.obtainMessage(MESSAGE_SHOW_VIEWPORT_FRAME);
934                mHandler.sendMessageDelayed(message, delay);
935            }
936        }
937
938        private void handleOnWindowTransition(int transition, WindowInfo info) {
939            if (DEBUG_WINDOW_TRANSITIONS) {
940                Slog.i(LOG_TAG, "Window transitioning: "
941                        + windowTransitionToString(transition));
942            }
943            try {
944                final boolean magnifying = mMagnificationController.isMagnifying();
945                if (magnifying) {
946                    switch (transition) {
947                        case WindowManagerPolicy.TRANSIT_ACTIVITY_OPEN:
948                        case WindowManagerPolicy.TRANSIT_TASK_OPEN:
949                        case WindowManagerPolicy.TRANSIT_TASK_TO_FRONT:
950                        case WindowManagerPolicy.TRANSIT_WALLPAPER_OPEN:
951                        case WindowManagerPolicy.TRANSIT_WALLPAPER_CLOSE:
952                        case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_OPEN: {
953                            resetMagnificationIfNeeded();
954                        }
955                    }
956                }
957                if (info.type == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
958                        || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD
959                        || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG) {
960                    switch (transition) {
961                        case WindowManagerPolicy.TRANSIT_ENTER:
962                        case WindowManagerPolicy.TRANSIT_SHOW:
963                        case WindowManagerPolicy.TRANSIT_EXIT:
964                        case WindowManagerPolicy.TRANSIT_HIDE: {
965                            mViewport.recomputeBounds(mMagnificationController.isMagnifying());
966                        } break;
967                    }
968                } else {
969                    switch (transition) {
970                        case WindowManagerPolicy.TRANSIT_ENTER:
971                        case WindowManagerPolicy.TRANSIT_SHOW: {
972                            if (!magnifying || !screenMagnificationAutoUpdateEnabled(mContext)) {
973                                break;
974                            }
975                            final int type = info.type;
976                            switch (type) {
977                                // TODO: Are these all the windows we want to make
978                                //       visible when they appear on the screen?
979                                //       Do we need to take some of them out?
980                                case WindowManager.LayoutParams.TYPE_APPLICATION_PANEL:
981                                case WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA:
982                                case WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL:
983                                case WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG:
984                                case WindowManager.LayoutParams.TYPE_SEARCH_BAR:
985                                case WindowManager.LayoutParams.TYPE_PHONE:
986                                case WindowManager.LayoutParams.TYPE_SYSTEM_ALERT:
987                                case WindowManager.LayoutParams.TYPE_TOAST:
988                                case WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY:
989                                case WindowManager.LayoutParams.TYPE_PRIORITY_PHONE:
990                                case WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG:
991                                case WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG:
992                                case WindowManager.LayoutParams.TYPE_SYSTEM_ERROR:
993                                case WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY:
994                                case WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL: {
995                                    Rect magnifiedRegionBounds = mMagnificationController
996                                            .getMagnifiedRegionBounds();
997                                    Rect touchableRegion = info.touchableRegion;
998                                    if (!magnifiedRegionBounds.intersect(touchableRegion)) {
999                                        ensureRectangleInMagnifiedRegionBounds(
1000                                                magnifiedRegionBounds, touchableRegion);
1001                                    }
1002                                } break;
1003                            } break;
1004                        }
1005                    }
1006                }
1007            } finally {
1008                if (info != null) {
1009                    info.recycle();
1010                }
1011            }
1012        }
1013
1014        private void handleOnRectangleOnScreenRequested(Rect rectangle, boolean immediate) {
1015            if (!mMagnificationController.isMagnifying()) {
1016                return;
1017            }
1018            Rect magnifiedRegionBounds = mMagnificationController.getMagnifiedRegionBounds();
1019            if (magnifiedRegionBounds.contains(rectangle)) {
1020                return;
1021            }
1022            ensureRectangleInMagnifiedRegionBounds(magnifiedRegionBounds, rectangle);
1023        }
1024
1025        private void ensureRectangleInMagnifiedRegionBounds(Rect magnifiedRegionBounds,
1026                Rect rectangle) {
1027            if (!Rect.intersects(rectangle, mViewport.getBounds())) {
1028                return;
1029            }
1030            final float scrollX;
1031            final float scrollY;
1032            if (rectangle.width() > magnifiedRegionBounds.width()) {
1033                scrollX = rectangle.left - magnifiedRegionBounds.left;
1034            } else if (rectangle.left < magnifiedRegionBounds.left) {
1035                scrollX = rectangle.left - magnifiedRegionBounds.left;
1036            } else if (rectangle.right > magnifiedRegionBounds.right) {
1037                scrollX = rectangle.right - magnifiedRegionBounds.right;
1038            } else {
1039                scrollX = 0;
1040            }
1041            if (rectangle.height() > magnifiedRegionBounds.height()) {
1042                scrollY = rectangle.top - magnifiedRegionBounds.top;
1043            } else if (rectangle.top < magnifiedRegionBounds.top) {
1044                scrollY = rectangle.top - magnifiedRegionBounds.top;
1045            } else if (rectangle.bottom > magnifiedRegionBounds.bottom) {
1046                scrollY = rectangle.bottom - magnifiedRegionBounds.bottom;
1047            } else {
1048                scrollY = 0;
1049            }
1050            final float viewportCenterX = mMagnificationController.getMagnifiedRegionCenterX()
1051                    + scrollX;
1052            final float viewportCenterY = mMagnificationController.getMagnifiedRegionCenterY()
1053                    + scrollY;
1054            mMagnificationController.setMagnifiedRegionCenter(viewportCenterX, viewportCenterY,
1055                    true);
1056        }
1057
1058        private void resetMagnificationIfNeeded() {
1059            if (mMagnificationController.isMagnifying()
1060                    && screenMagnificationAutoUpdateEnabled(mContext)) {
1061                mMagnificationController.reset(true);
1062                mViewport.setFrameShown(false, true);
1063            }
1064        }
1065
1066        private boolean screenMagnificationAutoUpdateEnabled(Context context) {
1067            return (Settings.Secure.getInt(context.getContentResolver(),
1068                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
1069                    DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
1070        }
1071
1072        private String windowTransitionToString(int transition) {
1073            switch (transition) {
1074                case WindowManagerPolicy.TRANSIT_UNSET: {
1075                    return "TRANSIT_UNSET";
1076                }
1077                case WindowManagerPolicy.TRANSIT_NONE: {
1078                    return "TRANSIT_NONE";
1079                }
1080                case WindowManagerPolicy.TRANSIT_ENTER: {
1081                    return "TRANSIT_ENTER";
1082                }
1083                case WindowManagerPolicy.TRANSIT_EXIT: {
1084                    return "TRANSIT_EXIT";
1085                }
1086                case WindowManagerPolicy.TRANSIT_SHOW: {
1087                    return "TRANSIT_SHOW";
1088                }
1089                case WindowManagerPolicy.TRANSIT_EXIT_MASK: {
1090                    return "TRANSIT_EXIT_MASK";
1091                }
1092                case WindowManagerPolicy.TRANSIT_PREVIEW_DONE: {
1093                    return "TRANSIT_PREVIEW_DONE";
1094                }
1095                case WindowManagerPolicy.TRANSIT_ACTIVITY_OPEN: {
1096                    return "TRANSIT_ACTIVITY_OPEN";
1097                }
1098                case WindowManagerPolicy.TRANSIT_ACTIVITY_CLOSE: {
1099                    return "TRANSIT_ACTIVITY_CLOSE";
1100                }
1101                case WindowManagerPolicy.TRANSIT_TASK_OPEN: {
1102                    return "TRANSIT_TASK_OPEN";
1103                }
1104                case WindowManagerPolicy.TRANSIT_TASK_CLOSE: {
1105                    return "TRANSIT_TASK_CLOSE";
1106                }
1107                case WindowManagerPolicy.TRANSIT_TASK_TO_FRONT: {
1108                    return "TRANSIT_TASK_TO_FRONT";
1109                }
1110                case WindowManagerPolicy.TRANSIT_TASK_TO_BACK: {
1111                    return "TRANSIT_TASK_TO_BACK";
1112                }
1113                case WindowManagerPolicy.TRANSIT_WALLPAPER_CLOSE: {
1114                    return "TRANSIT_WALLPAPER_CLOSE";
1115                }
1116                case WindowManagerPolicy.TRANSIT_WALLPAPER_OPEN: {
1117                    return "TRANSIT_WALLPAPER_OPEN";
1118                }
1119                case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_OPEN: {
1120                    return "TRANSIT_WALLPAPER_INTRA_OPEN";
1121                }
1122                case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_CLOSE: {
1123                    return "TRANSIT_WALLPAPER_INTRA_CLOSE";
1124                }
1125                default: {
1126                    return "<UNKNOWN>";
1127                }
1128            }
1129        }
1130
1131        private String rotationToString(int rotation) {
1132            switch (rotation) {
1133                case Surface.ROTATION_0: {
1134                    return "ROTATION_0";
1135                }
1136                case Surface.ROTATION_90: {
1137                    return "ROATATION_90";
1138                }
1139                case Surface.ROTATION_180: {
1140                    return "ROATATION_180";
1141                }
1142                case Surface.ROTATION_270: {
1143                    return "ROATATION_270";
1144                }
1145                default: {
1146                    throw new IllegalArgumentException("Invalid rotation: "
1147                        + rotation);
1148                }
1149            }
1150        }
1151
1152        private final class MyHandler extends Handler {
1153            @Override
1154            public void handleMessage(Message message) {
1155                final int action = message.what;
1156                switch (action) {
1157                    case MESSAGE_SHOW_VIEWPORT_FRAME: {
1158                        mViewport.setFrameShown(true, true);
1159                    } break;
1160                    case MESSAGE_RECOMPUTE_VIEWPORT_BOUNDS: {
1161                        final boolean animate = message.arg1 == 1;
1162                        mViewport.recomputeBounds(animate);
1163                    } break;
1164                    case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
1165                        SomeArgs args = (SomeArgs) message.obj;
1166                        try {
1167                            mTempRect.set(args.argi1, args.argi2, args.argi3, args.argi4);
1168                            final boolean immediate = (message.arg1 == 1);
1169                            handleOnRectangleOnScreenRequested(mTempRect, immediate);
1170                        } finally {
1171                            args.recycle();
1172                        }
1173                    } break;
1174                    case MESSAGE_ON_WINDOW_TRANSITION: {
1175                        final int transition = message.arg1;
1176                        WindowInfo info = (WindowInfo) message.obj;
1177                        handleOnWindowTransition(transition, info);
1178                    } break;
1179                    case MESSAGE_ON_ROTATION_CHANGED: {
1180                        final int rotation = message.arg1;
1181                        handleOnRotationChanged(rotation);
1182                    } break;
1183                    default: {
1184                        throw new IllegalArgumentException("Unknown message: " + action);
1185                    }
1186                }
1187            }
1188        }
1189    }
1190
1191    private final class MagnificationController {
1192
1193        private static final String PROPERTY_NAME_ACCESSIBILITY_TRANSFORMATION =
1194                "accessibilityTransformation";
1195
1196        private final MagnificationSpec mSentMagnificationSpec = new MagnificationSpec();
1197
1198        private final MagnificationSpec mCurrentMagnificationSpec = new MagnificationSpec();
1199
1200        private final Rect mTempRect = new Rect();
1201
1202        private final ValueAnimator mTransformationAnimator;
1203
1204        public MagnificationController(int animationDuration) {
1205            Property<MagnificationController, MagnificationSpec> property =
1206                    Property.of(MagnificationController.class, MagnificationSpec.class,
1207                    PROPERTY_NAME_ACCESSIBILITY_TRANSFORMATION);
1208            TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() {
1209                private final MagnificationSpec mTempTransformationSpec = new MagnificationSpec();
1210                @Override
1211                public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec,
1212                        MagnificationSpec toSpec) {
1213                    MagnificationSpec result = mTempTransformationSpec;
1214                    result.mScale = fromSpec.mScale
1215                            + (toSpec.mScale - fromSpec.mScale) * fraction;
1216                    result.mMagnifiedRegionCenterX = fromSpec.mMagnifiedRegionCenterX
1217                            + (toSpec.mMagnifiedRegionCenterX - fromSpec.mMagnifiedRegionCenterX)
1218                            * fraction;
1219                    result.mMagnifiedRegionCenterY = fromSpec.mMagnifiedRegionCenterY
1220                            + (toSpec.mMagnifiedRegionCenterY - fromSpec.mMagnifiedRegionCenterY)
1221                            * fraction;
1222                    result.mScaledOffsetX = fromSpec.mScaledOffsetX
1223                            + (toSpec.mScaledOffsetX - fromSpec.mScaledOffsetX)
1224                            * fraction;
1225                    result.mScaledOffsetY = fromSpec.mScaledOffsetY
1226                            + (toSpec.mScaledOffsetY - fromSpec.mScaledOffsetY)
1227                            * fraction;
1228                    return result;
1229                }
1230            };
1231            mTransformationAnimator = ObjectAnimator.ofObject(this, property,
1232                    evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec);
1233            mTransformationAnimator.setDuration((long) (animationDuration));
1234            mTransformationAnimator.setInterpolator(mInterpolator);
1235        }
1236
1237        public boolean isMagnifying() {
1238            return mCurrentMagnificationSpec.mScale > 1.0f;
1239        }
1240
1241        public void reset(boolean animate) {
1242            if (mTransformationAnimator.isRunning()) {
1243                mTransformationAnimator.cancel();
1244            }
1245            mCurrentMagnificationSpec.reset();
1246            if (animate) {
1247                animateAccessibilityTranformation(mSentMagnificationSpec,
1248                        mCurrentMagnificationSpec);
1249            } else {
1250                setAccessibilityTransformation(mCurrentMagnificationSpec);
1251            }
1252        }
1253
1254        public Rect getMagnifiedRegionBounds() {
1255            mTempRect.set(mViewport.getBounds());
1256            mTempRect.offset((int) -mCurrentMagnificationSpec.mScaledOffsetX,
1257                    (int) -mCurrentMagnificationSpec.mScaledOffsetY);
1258            mTempRect.scale(1.0f / mCurrentMagnificationSpec.mScale);
1259            return mTempRect;
1260        }
1261
1262        public float getScale() {
1263            return mCurrentMagnificationSpec.mScale;
1264        }
1265
1266        public float getMagnifiedRegionCenterX() {
1267            return mCurrentMagnificationSpec.mMagnifiedRegionCenterX;
1268        }
1269
1270        public float getMagnifiedRegionCenterY() {
1271            return mCurrentMagnificationSpec.mMagnifiedRegionCenterY;
1272        }
1273
1274        public float getScaledOffsetX() {
1275            return mCurrentMagnificationSpec.mScaledOffsetX;
1276        }
1277
1278        public float getScaledOffsetY() {
1279            return mCurrentMagnificationSpec.mScaledOffsetY;
1280        }
1281
1282        public void setScale(float scale, float pivotX, float pivotY, boolean animate) {
1283            MagnificationSpec spec = mCurrentMagnificationSpec;
1284            final float oldScale = spec.mScale;
1285            final float oldCenterX = spec.mMagnifiedRegionCenterX;
1286            final float oldCenterY = spec.mMagnifiedRegionCenterY;
1287            final float normPivotX = (-spec.mScaledOffsetX + pivotX) / oldScale;
1288            final float normPivotY = (-spec.mScaledOffsetY + pivotY) / oldScale;
1289            final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale);
1290            final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale);
1291            final float centerX = normPivotX + offsetX;
1292            final float centerY = normPivotY + offsetY;
1293            setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate);
1294        }
1295
1296        public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) {
1297            setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.mScale, centerX, centerY,
1298                    animate);
1299        }
1300
1301        public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY,
1302                boolean animate) {
1303            if (Float.compare(mCurrentMagnificationSpec.mScale, scale) == 0
1304                    && Float.compare(mCurrentMagnificationSpec.mMagnifiedRegionCenterX,
1305                            centerX) == 0
1306                    && Float.compare(mCurrentMagnificationSpec.mMagnifiedRegionCenterY,
1307                            centerY) == 0) {
1308                return;
1309            }
1310            if (mTransformationAnimator.isRunning()) {
1311                mTransformationAnimator.cancel();
1312            }
1313            if (DEBUG_MAGNIFICATION_CONTROLLER) {
1314                Slog.i(LOG_TAG, "scale: " + scale + " centerX: " + centerX
1315                        + " centerY: " + centerY);
1316            }
1317            mCurrentMagnificationSpec.initialize(scale, centerX, centerY);
1318            if (animate) {
1319                animateAccessibilityTranformation(mSentMagnificationSpec,
1320                        mCurrentMagnificationSpec);
1321            } else {
1322                setAccessibilityTransformation(mCurrentMagnificationSpec);
1323            }
1324        }
1325
1326        private void animateAccessibilityTranformation(MagnificationSpec fromSpec,
1327                MagnificationSpec toSpec) {
1328            mTransformationAnimator.setObjectValues(fromSpec, toSpec);
1329            mTransformationAnimator.start();
1330        }
1331
1332        @SuppressWarnings("unused")
1333        // Called from an animator.
1334        public MagnificationSpec getAccessibilityTransformation() {
1335            return mSentMagnificationSpec;
1336        }
1337
1338        public void setAccessibilityTransformation(MagnificationSpec transformation) {
1339            if (DEBUG_TRANSFORMATION) {
1340                Slog.i(LOG_TAG, "Transformation scale: " + transformation.mScale
1341                        + " offsetX: " + transformation.mScaledOffsetX
1342                        + " offsetY: " + transformation.mScaledOffsetY);
1343            }
1344            try {
1345                mSentMagnificationSpec.updateFrom(transformation);
1346                mWindowManagerService.magnifyDisplay(mDisplayProvider.getDisplay().getDisplayId(),
1347                        transformation.mScale, transformation.mScaledOffsetX,
1348                        transformation.mScaledOffsetY);
1349            } catch (RemoteException re) {
1350                /* ignore */
1351            }
1352        }
1353
1354        private class MagnificationSpec {
1355
1356            private static final float DEFAULT_SCALE = 1.0f;
1357
1358            public float mScale = DEFAULT_SCALE;
1359
1360            public float mMagnifiedRegionCenterX;
1361
1362            public float mMagnifiedRegionCenterY;
1363
1364            public float mScaledOffsetX;
1365
1366            public float mScaledOffsetY;
1367
1368            public void initialize(float scale, float magnifiedRegionCenterX,
1369                    float magnifiedRegionCenterY) {
1370                mScale = scale;
1371
1372                final int viewportWidth = mViewport.getBounds().width();
1373                final int viewportHeight = mViewport.getBounds().height();
1374                final float minMagnifiedRegionCenterX = (viewportWidth / 2) / scale;
1375                final float minMagnifiedRegionCenterY = (viewportHeight / 2) / scale;
1376                final float maxMagnifiedRegionCenterX = viewportWidth - minMagnifiedRegionCenterX;
1377                final float maxMagnifiedRegionCenterY = viewportHeight - minMagnifiedRegionCenterY;
1378
1379                mMagnifiedRegionCenterX = Math.min(Math.max(magnifiedRegionCenterX,
1380                        minMagnifiedRegionCenterX), maxMagnifiedRegionCenterX);
1381                mMagnifiedRegionCenterY = Math.min(Math.max(magnifiedRegionCenterY,
1382                        minMagnifiedRegionCenterY), maxMagnifiedRegionCenterY);
1383
1384                mScaledOffsetX = -(mMagnifiedRegionCenterX * scale - viewportWidth / 2);
1385                mScaledOffsetY = -(mMagnifiedRegionCenterY * scale - viewportHeight / 2);
1386            }
1387
1388            public void updateFrom(MagnificationSpec other) {
1389                mScale = other.mScale;
1390                mMagnifiedRegionCenterX = other.mMagnifiedRegionCenterX;
1391                mMagnifiedRegionCenterY = other.mMagnifiedRegionCenterY;
1392                mScaledOffsetX = other.mScaledOffsetX;
1393                mScaledOffsetY = other.mScaledOffsetY;
1394            }
1395
1396            public void reset() {
1397                mScale = DEFAULT_SCALE;
1398                mMagnifiedRegionCenterX = 0;
1399                mMagnifiedRegionCenterY = 0;
1400                mScaledOffsetX = 0;
1401                mScaledOffsetY = 0;
1402            }
1403        }
1404    }
1405
1406    private static final class Viewport {
1407
1408        private static final String PROPERTY_NAME_ALPHA = "alpha";
1409
1410        private static final String PROPERTY_NAME_BOUNDS = "bounds";
1411
1412        private static final int MIN_ALPHA = 0;
1413
1414        private static final int MAX_ALPHA = 255;
1415
1416        private final ArrayList<WindowInfo> mTempWindowInfoList = new ArrayList<WindowInfo>();
1417
1418        private final Rect mTempRect = new Rect();
1419
1420        private final IWindowManager mWindowManagerService;
1421        private final DisplayProvider mDisplayProvider;
1422
1423        private final ViewportWindow mViewportFrame;
1424
1425        private final ValueAnimator mResizeFrameAnimator;
1426
1427        private final ValueAnimator mShowHideFrameAnimator;
1428
1429        public Viewport(Context context, WindowManager windowManager,
1430                IWindowManager windowManagerService, DisplayProvider displayInfoProvider,
1431                Interpolator animationInterpolator, long animationDuration) {
1432            mWindowManagerService = windowManagerService;
1433            mDisplayProvider = displayInfoProvider;
1434            mViewportFrame = new ViewportWindow(context, windowManager, displayInfoProvider);
1435
1436            mShowHideFrameAnimator = ObjectAnimator.ofInt(mViewportFrame, PROPERTY_NAME_ALPHA,
1437                  MIN_ALPHA, MAX_ALPHA);
1438            mShowHideFrameAnimator.setInterpolator(animationInterpolator);
1439            mShowHideFrameAnimator.setDuration(animationDuration);
1440            mShowHideFrameAnimator.addListener(new AnimatorListener() {
1441                @Override
1442                public void onAnimationEnd(Animator animation) {
1443                    if (mShowHideFrameAnimator.getAnimatedValue().equals(MIN_ALPHA)) {
1444                        mViewportFrame.hide();
1445                    }
1446                }
1447                @Override
1448                public void onAnimationStart(Animator animation) {
1449                    /* do nothing - stub */
1450                }
1451                @Override
1452                public void onAnimationCancel(Animator animation) {
1453                    /* do nothing - stub */
1454                }
1455                @Override
1456                public void onAnimationRepeat(Animator animation) {
1457                    /* do nothing - stub */
1458                }
1459            });
1460
1461            Property<ViewportWindow, Rect> property = Property.of(ViewportWindow.class,
1462                    Rect.class, PROPERTY_NAME_BOUNDS);
1463            TypeEvaluator<Rect> evaluator = new TypeEvaluator<Rect>() {
1464                private final Rect mReusableResultRect = new Rect();
1465                @Override
1466                public Rect evaluate(float fraction, Rect fromFrame, Rect toFrame) {
1467                    Rect result = mReusableResultRect;
1468                    result.left = (int) (fromFrame.left
1469                            + (toFrame.left - fromFrame.left) * fraction);
1470                    result.top = (int) (fromFrame.top
1471                            + (toFrame.top - fromFrame.top) * fraction);
1472                    result.right = (int) (fromFrame.right
1473                            + (toFrame.right - fromFrame.right) * fraction);
1474                    result.bottom = (int) (fromFrame.bottom
1475                            + (toFrame.bottom - fromFrame.bottom) * fraction);
1476                    return result;
1477                }
1478            };
1479            mResizeFrameAnimator = ObjectAnimator.ofObject(mViewportFrame, property,
1480                    evaluator, mViewportFrame.mBounds, mViewportFrame.mBounds);
1481            mResizeFrameAnimator.setDuration((long) (animationDuration));
1482            mResizeFrameAnimator.setInterpolator(animationInterpolator);
1483
1484            recomputeBounds(false);
1485        }
1486
1487        public void recomputeBounds(boolean animate) {
1488            Rect frame = mTempRect;
1489            frame.set(0, 0, mDisplayProvider.getDisplayInfo().logicalWidth,
1490                    mDisplayProvider.getDisplayInfo().logicalHeight);
1491            ArrayList<WindowInfo> infos = mTempWindowInfoList;
1492            infos.clear();
1493            try {
1494                mWindowManagerService.getVisibleWindowsForDisplay(
1495                        mDisplayProvider.getDisplay().getDisplayId(), infos);
1496                final int windowCount = infos.size();
1497                for (int i = 0; i < windowCount; i++) {
1498                    WindowInfo info = infos.get(i);
1499                    if (info.type == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
1500                            || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD
1501                            || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG) {
1502                        subtract(frame, info.touchableRegion);
1503                    }
1504                    info.recycle();
1505                }
1506            } catch (RemoteException re) {
1507                /* ignore */
1508            } finally {
1509                infos.clear();
1510            }
1511            resize(frame, animate);
1512        }
1513
1514        public void rotationChanged() {
1515            mViewportFrame.rotationChanged();
1516        }
1517
1518        public Rect getBounds() {
1519            return mViewportFrame.getBounds();
1520        }
1521
1522        public void setFrameShown(boolean shown, boolean animate) {
1523            if (mViewportFrame.isShown() == shown) {
1524                return;
1525            }
1526            if (animate) {
1527                if (mShowHideFrameAnimator.isRunning()) {
1528                    mShowHideFrameAnimator.reverse();
1529                } else {
1530                    if (shown) {
1531                        mViewportFrame.show();
1532                        mShowHideFrameAnimator.start();
1533                    } else {
1534                        mShowHideFrameAnimator.reverse();
1535                    }
1536                }
1537            } else {
1538                mShowHideFrameAnimator.cancel();
1539                if (shown) {
1540                    mViewportFrame.show();
1541                } else {
1542                    mViewportFrame.hide();
1543                }
1544            }
1545        }
1546
1547        private void resize(Rect bounds, boolean animate) {
1548            if (mViewportFrame.getBounds().equals(bounds)) {
1549                return;
1550            }
1551            if (animate) {
1552                if (mResizeFrameAnimator.isRunning()) {
1553                    mResizeFrameAnimator.cancel();
1554                }
1555                mResizeFrameAnimator.setObjectValues(mViewportFrame.mBounds, bounds);
1556                mResizeFrameAnimator.start();
1557            } else {
1558                mViewportFrame.setBounds(bounds);
1559            }
1560        }
1561
1562        private boolean subtract(Rect lhs, Rect rhs) {
1563            if (lhs.right < rhs.left || lhs.left  > rhs.right
1564                    || lhs.bottom < rhs.top || lhs.top > rhs.bottom) {
1565                return false;
1566            }
1567            if (lhs.left < rhs.left) {
1568                lhs.right = rhs.left;
1569            }
1570            if (lhs.top < rhs.top) {
1571                lhs.bottom = rhs.top;
1572            }
1573            if (lhs.right > rhs.right) {
1574                lhs.left = rhs.right;
1575            }
1576            if (lhs.bottom > rhs.bottom) {
1577                lhs.top = rhs.bottom;
1578            }
1579            return true;
1580        }
1581
1582        private static final class ViewportWindow {
1583            private static final String WINDOW_TITLE = "Magnification Overlay";
1584
1585            private final WindowManager mWindowManager;
1586            private final DisplayProvider mDisplayProvider;
1587
1588            private final ContentView mWindowContent;
1589            private final WindowManager.LayoutParams mWindowParams;
1590
1591            private final Rect mBounds = new Rect();
1592            private boolean mShown;
1593            private int mAlpha;
1594
1595            public ViewportWindow(Context context, WindowManager windowManager,
1596                    DisplayProvider displayProvider) {
1597                mWindowManager = windowManager;
1598                mDisplayProvider = displayProvider;
1599
1600                ViewGroup.LayoutParams contentParams = new ViewGroup.LayoutParams(
1601                        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
1602                mWindowContent = new ContentView(context);
1603                mWindowContent.setLayoutParams(contentParams);
1604                mWindowContent.setBackgroundColor(R.color.transparent);
1605
1606                mWindowParams = new WindowManager.LayoutParams(
1607                        WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY);
1608                mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
1609                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
1610                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
1611                mWindowParams.setTitle(WINDOW_TITLE);
1612                mWindowParams.gravity = Gravity.CENTER;
1613                mWindowParams.width = displayProvider.getDisplayInfo().logicalWidth;
1614                mWindowParams.height = displayProvider.getDisplayInfo().logicalHeight;
1615                mWindowParams.format = PixelFormat.TRANSLUCENT;
1616            }
1617
1618            public boolean isShown() {
1619                return mShown;
1620            }
1621
1622            public void show() {
1623                if (mShown) {
1624                    return;
1625                }
1626                mShown = true;
1627                mWindowManager.addView(mWindowContent, mWindowParams);
1628                if (DEBUG_VIEWPORT_WINDOW) {
1629                    Slog.i(LOG_TAG, "ViewportWindow shown.");
1630                }
1631            }
1632
1633            public void hide() {
1634                if (!mShown) {
1635                    return;
1636                }
1637                mShown = false;
1638                mWindowManager.removeView(mWindowContent);
1639                if (DEBUG_VIEWPORT_WINDOW) {
1640                    Slog.i(LOG_TAG, "ViewportWindow hidden.");
1641                }
1642            }
1643
1644            @SuppressWarnings("unused")
1645            // Called reflectively from an animator.
1646            public int getAlpha() {
1647                return mAlpha;
1648            }
1649
1650            @SuppressWarnings("unused")
1651            // Called reflectively from an animator.
1652            public void setAlpha(int alpha) {
1653                if (mAlpha == alpha) {
1654                    return;
1655                }
1656                mAlpha = alpha;
1657                if (mShown) {
1658                    mWindowContent.invalidate();
1659                }
1660                if (DEBUG_VIEWPORT_WINDOW) {
1661                    Slog.i(LOG_TAG, "ViewportFrame set alpha: " + alpha);
1662                }
1663            }
1664
1665            public Rect getBounds() {
1666                return mBounds;
1667            }
1668
1669            public void rotationChanged() {
1670                mWindowParams.width = mDisplayProvider.getDisplayInfo().logicalWidth;
1671                mWindowParams.height = mDisplayProvider.getDisplayInfo().logicalHeight;
1672                if (mShown) {
1673                    mWindowManager.updateViewLayout(mWindowContent, mWindowParams);
1674                }
1675            }
1676
1677            public void setBounds(Rect bounds) {
1678                if (mBounds.equals(bounds)) {
1679                    return;
1680                }
1681                mBounds.set(bounds);
1682                if (mShown) {
1683                    mWindowContent.invalidate();
1684                }
1685                if (DEBUG_VIEWPORT_WINDOW) {
1686                    Slog.i(LOG_TAG, "ViewportFrame set bounds: " + bounds);
1687                }
1688            }
1689
1690            private final class ContentView extends View {
1691                private final Drawable mHighlightFrame;
1692
1693                public ContentView(Context context) {
1694                    super(context);
1695                    mHighlightFrame = context.getResources().getDrawable(
1696                            R.drawable.magnified_region_frame);
1697                }
1698
1699                @Override
1700                public void onDraw(Canvas canvas) {
1701                    canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
1702                    mHighlightFrame.setBounds(mBounds);
1703                    mHighlightFrame.setAlpha(mAlpha);
1704                    mHighlightFrame.draw(canvas);
1705                }
1706            }
1707        }
1708    }
1709
1710    private static class DisplayProvider implements DisplayListener {
1711        private final WindowManager mWindowManager;
1712        private final DisplayManager mDisplayManager;
1713        private final Display mDefaultDisplay;
1714        private final DisplayInfo mDefaultDisplayInfo = new DisplayInfo();
1715
1716        public DisplayProvider(Context context, WindowManager windowManager) {
1717            mWindowManager = windowManager;
1718            mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
1719            mDefaultDisplay = mWindowManager.getDefaultDisplay();
1720            mDisplayManager.registerDisplayListener(this, null);
1721            updateDisplayInfo();
1722        }
1723
1724        public DisplayInfo getDisplayInfo() {
1725            return mDefaultDisplayInfo;
1726        }
1727
1728        public Display getDisplay() {
1729            return mDefaultDisplay;
1730        }
1731
1732        private void updateDisplayInfo() {
1733            if (!mDefaultDisplay.getDisplayInfo(mDefaultDisplayInfo)) {
1734                Slog.e(LOG_TAG, "Default display is not valid.");
1735            }
1736        }
1737
1738        public void destroy() {
1739            mDisplayManager.unregisterDisplayListener(this);
1740        }
1741
1742        @Override
1743        public void onDisplayAdded(int displayId) {
1744            /* do noting */
1745        }
1746
1747        @Override
1748        public void onDisplayRemoved(int displayId) {
1749            // Having no default display
1750        }
1751
1752        @Override
1753        public void onDisplayChanged(int displayId) {
1754            updateDisplayInfo();
1755        }
1756    }
1757
1758    /**
1759     * The listener for receiving notifications when gestures occur.
1760     * If you want to listen for all the different gestures then implement
1761     * this interface. If you only want to listen for a subset it might
1762     * be easier to extend {@link SimpleOnScaleGestureListener}.
1763     *
1764     * An application will receive events in the following order:
1765     * <ul>
1766     *  <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
1767     *  <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
1768     *  <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
1769     * </ul>
1770     */
1771    interface OnScaleGestureListener {
1772        /**
1773         * Responds to scaling events for a gesture in progress.
1774         * Reported by pointer motion.
1775         *
1776         * @param detector The detector reporting the event - use this to
1777         *          retrieve extended info about event state.
1778         * @return Whether or not the detector should consider this event
1779         *          as handled. If an event was not handled, the detector
1780         *          will continue to accumulate movement until an event is
1781         *          handled. This can be useful if an application, for example,
1782         *          only wants to update scaling factors if the change is
1783         *          greater than 0.01.
1784         */
1785        public boolean onScale(ScaleGestureDetector detector);
1786
1787        /**
1788         * Responds to the beginning of a scaling gesture. Reported by
1789         * new pointers going down.
1790         *
1791         * @param detector The detector reporting the event - use this to
1792         *          retrieve extended info about event state.
1793         * @return Whether or not the detector should continue recognizing
1794         *          this gesture. For example, if a gesture is beginning
1795         *          with a focal point outside of a region where it makes
1796         *          sense, onScaleBegin() may return false to ignore the
1797         *          rest of the gesture.
1798         */
1799        public boolean onScaleBegin(ScaleGestureDetector detector);
1800
1801        /**
1802         * Responds to the end of a scale gesture. Reported by existing
1803         * pointers going up.
1804         *
1805         * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
1806         * and {@link ScaleGestureDetector#getFocusY()} will return the location
1807         * of the pointer remaining on the screen.
1808         *
1809         * @param detector The detector reporting the event - use this to
1810         *          retrieve extended info about event state.
1811         */
1812        public void onScaleEnd(ScaleGestureDetector detector);
1813    }
1814
1815    class ScaleGestureDetector {
1816
1817        private final MinCircleFinder mMinCircleFinder = new MinCircleFinder();
1818
1819        private final OnScaleGestureListener mListener;
1820
1821        private float mFocusX;
1822        private float mFocusY;
1823
1824        private float mCurrSpan;
1825        private float mPrevSpan;
1826        private float mCurrSpanX;
1827        private float mCurrSpanY;
1828        private float mPrevSpanX;
1829        private float mPrevSpanY;
1830        private long mCurrTime;
1831        private long mPrevTime;
1832        private boolean mInProgress;
1833
1834        public ScaleGestureDetector(OnScaleGestureListener listener) {
1835            mListener = listener;
1836        }
1837
1838        /**
1839         * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
1840         * when appropriate.
1841         *
1842         * <p>Applications should pass a complete and consistent event stream to this method.
1843         * A complete and consistent event stream involves all MotionEvents from the initial
1844         * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
1845         *
1846         * @param event The event to process
1847         * @return true if the event was processed and the detector wants to receive the
1848         *         rest of the MotionEvents in this event stream.
1849         */
1850        public boolean onTouchEvent(MotionEvent event) {
1851            boolean streamEnded = false;
1852            boolean contextChanged = false;
1853            int excludedPtrIdx = -1;
1854            final int action = event.getActionMasked();
1855            switch (action) {
1856                case MotionEvent.ACTION_DOWN:
1857                case MotionEvent.ACTION_POINTER_DOWN: {
1858                    contextChanged = true;
1859                } break;
1860                case MotionEvent.ACTION_POINTER_UP: {
1861                    contextChanged = true;
1862                    excludedPtrIdx = event.getActionIndex();
1863                } break;
1864                case MotionEvent.ACTION_UP:
1865                case MotionEvent.ACTION_CANCEL: {
1866                    streamEnded = true;
1867                } break;
1868            }
1869
1870            if (mInProgress && (contextChanged || streamEnded)) {
1871                mListener.onScaleEnd(this);
1872                mInProgress = false;
1873                mPrevSpan = 0;
1874                mPrevSpanX = 0;
1875                mPrevSpanY = 0;
1876                return true;
1877            }
1878
1879            final long currTime = mCurrTime;
1880
1881            mFocusX = 0;
1882            mFocusY = 0;
1883            mCurrSpan = 0;
1884            mCurrSpanX = 0;
1885            mCurrSpanY = 0;
1886            mCurrTime = 0;
1887            mPrevTime = 0;
1888
1889            if (!streamEnded) {
1890                MinCircleFinder.Circle circle =
1891                        mMinCircleFinder.computeMinCircleAroundPointers(event);
1892                mFocusX = circle.centerX;
1893                mFocusY = circle.centerY;
1894
1895                double sumSlope = 0;
1896                final int pointerCount = event.getPointerCount();
1897                for (int i = 0; i < pointerCount; i++) {
1898                    if (i == excludedPtrIdx) {
1899                        continue;
1900                    }
1901                    float x = event.getX(i) - mFocusX;
1902                    float y = event.getY(i) - mFocusY;
1903                    if (x == 0) {
1904                        x += 0.1f;
1905                    }
1906                    sumSlope += y / x;
1907                }
1908                final double avgSlope = sumSlope
1909                        / ((excludedPtrIdx < 0) ? pointerCount : pointerCount - 1);
1910
1911                double angle = Math.atan(avgSlope);
1912                mCurrSpan = 2 * circle.radius;
1913                mCurrSpanX = (float) Math.abs((Math.cos(angle) * mCurrSpan));
1914                mCurrSpanY = (float) Math.abs((Math.sin(angle) * mCurrSpan));
1915            }
1916
1917            if (contextChanged || mPrevSpan == 0 || mPrevSpanX == 0 || mPrevSpanY == 0) {
1918                mPrevSpan = mCurrSpan;
1919                mPrevSpanX = mCurrSpanX;
1920                mPrevSpanY = mCurrSpanY;
1921            }
1922
1923            if (!mInProgress && mCurrSpan != 0 && !streamEnded) {
1924                mInProgress = mListener.onScaleBegin(this);
1925            }
1926
1927            if (mInProgress) {
1928                mPrevTime = (currTime != 0) ? currTime : event.getEventTime();
1929                mCurrTime = event.getEventTime();
1930                if (mCurrSpan == 0) {
1931                    mListener.onScaleEnd(this);
1932                    mInProgress = false;
1933                } else {
1934                    if (mListener.onScale(this)) {
1935                        mPrevSpanX = mCurrSpanX;
1936                        mPrevSpanY = mCurrSpanY;
1937                        mPrevSpan = mCurrSpan;
1938                    }
1939                }
1940            }
1941
1942            return true;
1943        }
1944
1945        /**
1946         * Returns {@code true} if a scale gesture is in progress.
1947         */
1948        public boolean isInProgress() {
1949            return mInProgress;
1950        }
1951
1952        /**
1953         * Get the X coordinate of the current gesture's focal point.
1954         * If a gesture is in progress, the focal point is between
1955         * each of the pointers forming the gesture.
1956         *
1957         * If {@link #isInProgress()} would return false, the result of this
1958         * function is undefined.
1959         *
1960         * @return X coordinate of the focal point in pixels.
1961         */
1962        public float getFocusX() {
1963            return mFocusX;
1964        }
1965
1966        /**
1967         * Get the Y coordinate of the current gesture's focal point.
1968         * If a gesture is in progress, the focal point is between
1969         * each of the pointers forming the gesture.
1970         *
1971         * If {@link #isInProgress()} would return false, the result of this
1972         * function is undefined.
1973         *
1974         * @return Y coordinate of the focal point in pixels.
1975         */
1976        public float getFocusY() {
1977            return mFocusY;
1978        }
1979
1980        /**
1981         * Return the average distance between each of the pointers forming the
1982         * gesture in progress through the focal point.
1983         *
1984         * @return Distance between pointers in pixels.
1985         */
1986        public float getCurrentSpan() {
1987            return mCurrSpan;
1988        }
1989
1990        /**
1991         * Return the average X distance between each of the pointers forming the
1992         * gesture in progress through the focal point.
1993         *
1994         * @return Distance between pointers in pixels.
1995         */
1996        public float getCurrentSpanX() {
1997            return mCurrSpanX;
1998        }
1999
2000        /**
2001         * Return the average Y distance between each of the pointers forming the
2002         * gesture in progress through the focal point.
2003         *
2004         * @return Distance between pointers in pixels.
2005         */
2006        public float getCurrentSpanY() {
2007            return mCurrSpanY;
2008        }
2009
2010        /**
2011         * Return the previous average distance between each of the pointers forming the
2012         * gesture in progress through the focal point.
2013         *
2014         * @return Previous distance between pointers in pixels.
2015         */
2016        public float getPreviousSpan() {
2017            return mPrevSpan;
2018        }
2019
2020        /**
2021         * Return the previous average X distance between each of the pointers forming the
2022         * gesture in progress through the focal point.
2023         *
2024         * @return Previous distance between pointers in pixels.
2025         */
2026        public float getPreviousSpanX() {
2027            return mPrevSpanX;
2028        }
2029
2030        /**
2031         * Return the previous average Y distance between each of the pointers forming the
2032         * gesture in progress through the focal point.
2033         *
2034         * @return Previous distance between pointers in pixels.
2035         */
2036        public float getPreviousSpanY() {
2037            return mPrevSpanY;
2038        }
2039
2040        /**
2041         * Return the scaling factor from the previous scale event to the current
2042         * event. This value is defined as
2043         * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
2044         *
2045         * @return The current scaling factor.
2046         */
2047        public float getScaleFactor() {
2048            return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
2049        }
2050
2051        /**
2052         * Return the time difference in milliseconds between the previous
2053         * accepted scaling event and the current scaling event.
2054         *
2055         * @return Time difference since the last scaling event in milliseconds.
2056         */
2057        public long getTimeDelta() {
2058            return mCurrTime - mPrevTime;
2059        }
2060
2061        /**
2062         * Return the event time of the current event being processed.
2063         *
2064         * @return Current event time in milliseconds.
2065         */
2066        public long getEventTime() {
2067            return mCurrTime;
2068        }
2069    }
2070
2071    private static final class MinCircleFinder {
2072        private final ArrayList<PointHolder> mPoints = new ArrayList<PointHolder>();
2073        private final ArrayList<PointHolder> sBoundary = new ArrayList<PointHolder>();
2074        private final Circle mMinCircle = new Circle();
2075
2076        /**
2077         * Finds the minimal circle that contains all pointers of a motion event.
2078         *
2079         * @param event A motion event.
2080         * @return The minimal circle.
2081         */
2082        public Circle computeMinCircleAroundPointers(MotionEvent event) {
2083            ArrayList<PointHolder> points = mPoints;
2084            points.clear();
2085            final int pointerCount = event.getPointerCount();
2086            for (int i = 0; i < pointerCount; i++) {
2087                PointHolder point = PointHolder.obtain(event.getX(i), event.getY(i));
2088                points.add(point);
2089            }
2090            ArrayList<PointHolder> boundary = sBoundary;
2091            boundary.clear();
2092            computeMinCircleAroundPointsRecursive(points, boundary, mMinCircle);
2093            for (int i = points.size() - 1; i >= 0; i--) {
2094                points.remove(i).recycle();
2095            }
2096            boundary.clear();
2097            return mMinCircle;
2098        }
2099
2100        private static void computeMinCircleAroundPointsRecursive(ArrayList<PointHolder> points,
2101                ArrayList<PointHolder> boundary, Circle outCircle) {
2102            if (points.isEmpty()) {
2103                if (boundary.size() == 0) {
2104                    outCircle.initialize();
2105                } else if (boundary.size() == 1) {
2106                    outCircle.initialize(boundary.get(0).mData, boundary.get(0).mData);
2107                } else if (boundary.size() == 2) {
2108                    outCircle.initialize(boundary.get(0).mData, boundary.get(1).mData);
2109                } else if (boundary.size() == 3) {
2110                    outCircle.initialize(boundary.get(0).mData, boundary.get(1).mData,
2111                            boundary.get(2).mData);
2112                }
2113                return;
2114            }
2115            PointHolder point = points.remove(points.size() - 1);
2116            computeMinCircleAroundPointsRecursive(points, boundary, outCircle);
2117            if (!outCircle.contains(point.mData)) {
2118                boundary.add(point);
2119                computeMinCircleAroundPointsRecursive(points, boundary, outCircle);
2120                boundary.remove(point);
2121            }
2122            points.add(point);
2123        }
2124
2125        private static final class PointHolder {
2126            private static final int MAX_POOL_SIZE = 20;
2127            private static PointHolder sPool;
2128            private static int sPoolSize;
2129
2130            private PointHolder mNext;
2131            private boolean mIsInPool;
2132
2133            private final PointF mData = new PointF();
2134
2135            public static PointHolder obtain(float x, float y) {
2136                PointHolder holder;
2137                if (sPoolSize > 0) {
2138                    sPoolSize--;
2139                    holder = sPool;
2140                    sPool = sPool.mNext;
2141                    holder.mNext = null;
2142                    holder.mIsInPool = false;
2143                } else {
2144                    holder = new PointHolder();
2145                }
2146                holder.mData.set(x, y);
2147                return holder;
2148            }
2149
2150            public void recycle() {
2151                if (mIsInPool) {
2152                    throw new IllegalStateException("Already recycled.");
2153                }
2154                clear();
2155                if (sPoolSize < MAX_POOL_SIZE) {
2156                    sPoolSize++;
2157                    mNext = sPool;
2158                    sPool = this;
2159                    mIsInPool = true;
2160                }
2161            }
2162
2163            private void clear() {
2164                mData.set(0, 0);
2165            }
2166        }
2167
2168        public static final class Circle {
2169            public float centerX;
2170            public float centerY;
2171            public float radius;
2172
2173            private void initialize() {
2174                centerX = 0;
2175                centerY = 0;
2176                radius = 0;
2177            }
2178
2179            private void initialize(PointF first, PointF second, PointF third) {
2180                if (!hasLineWithInfiniteSlope(first, second, third)) {
2181                    initializeInternal(first, second, third);
2182                } else if (!hasLineWithInfiniteSlope(first, third, second)) {
2183                    initializeInternal(first, third, second);
2184                } else if (!hasLineWithInfiniteSlope(second, first, third)) {
2185                    initializeInternal(second, first, third);
2186                } else if (!hasLineWithInfiniteSlope(second, third, first)) {
2187                    initializeInternal(second, third, first);
2188                } else if (!hasLineWithInfiniteSlope(third, first, second)) {
2189                    initializeInternal(third, first, second);
2190                } else if (!hasLineWithInfiniteSlope(third, second, first)) {
2191                    initializeInternal(third, second, first);
2192                } else {
2193                    initialize();
2194                }
2195            }
2196
2197            private void initialize(PointF first, PointF second) {
2198                radius = (float) (Math.hypot(second.x - first.x, second.y - first.y) / 2);
2199                centerX = (float) (second.x + first.x) / 2;
2200                centerY = (float) (second.y + first.y) / 2;
2201            }
2202
2203            public boolean contains(PointF point) {
2204                return (int) (Math.hypot(point.x - centerX, point.y - centerY)) <= radius;
2205            }
2206
2207            private void initializeInternal(PointF first, PointF second, PointF third) {
2208                final float x1 = first.x;
2209                final float y1 = first.y;
2210                final float x2 = second.x;
2211                final float y2 = second.y;
2212                final float x3 = third.x;
2213                final float y3 = third.y;
2214
2215                final float sl1 = (y2 - y1) / (x2 - x1);
2216                final float sl2 = (y3 - y2) / (x3 - x2);
2217
2218                centerX = (int) ((sl1 * sl2 * (y1 - y3) + sl2 * (x1 + x2) - sl1 * (x2 + x3))
2219                        / (2 * (sl2 - sl1)));
2220                centerY = (int) (-1 / sl1 * (centerX - (x1 + x2) / 2) + (y1 + y2) / 2);
2221                radius = (int) Math.hypot(x1 - centerX, y1 - centerY);
2222            }
2223
2224            private boolean hasLineWithInfiniteSlope(PointF first, PointF second, PointF third) {
2225                return (second.x - first.x == 0 || third.x - second.x == 0
2226                        || second.y - first.y == 0 || third.y - second.y == 0);
2227            }
2228
2229            @Override
2230            public String toString() {
2231                return "cetner: [" + centerX + ", " + centerY + "] radius: " + radius;
2232            }
2233        }
2234    }
2235}
2236