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