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