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