ScreenMagnifier.java revision 2cee686498c6141f5a68532ecb79f598e043ec7e
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.ScaleGestureDetector;
50import android.view.MotionEvent.PointerCoords;
51import android.view.MotionEvent.PointerProperties;
52import android.view.ScaleGestureDetector.OnScaleGestureListener;
53import android.view.Surface;
54import android.view.View;
55import android.view.ViewConfiguration;
56import android.view.ViewGroup;
57import android.view.WindowInfo;
58import android.view.WindowManager;
59import android.view.WindowManagerPolicy;
60import android.view.accessibility.AccessibilityEvent;
61import android.view.animation.DecelerateInterpolator;
62import android.view.animation.Interpolator;
63
64import com.android.internal.R;
65import com.android.internal.os.SomeArgs;
66
67import java.util.ArrayList;
68
69/**
70 * This class handles the screen magnification when accessibility is enabled.
71 * The behavior is as follows:
72 *
73 * 1. Triple tap toggles permanent screen magnification which is magnifying
74 *    the area around the location of the triple tap. One can think of the
75 *    location of the triple tap as the center of the magnified viewport.
76 *    For example, a triple tap when not magnified would magnify the screen
77 *    and leave it in a magnified state. A triple tapping when magnified would
78 *    clear magnification and leave the screen in a not magnified state.
79 *
80 * 2. Triple tap and hold would magnify the screen if not magnified and enable
81 *    viewport dragging mode until the finger goes up. One can think of this
82 *    mode as a way to move the magnified viewport since the area around the
83 *    moving finger will be magnified to fit the screen. For example, if the
84 *    screen was not magnified and the user triple taps and holds the screen
85 *    would magnify and the viewport will follow the user's finger. When the
86 *    finger goes up the screen will clear zoom out. If the same user interaction
87 *    is performed when the screen is magnified, the viewport movement will
88 *    be the same but when the finger goes up the screen will stay magnified.
89 *    In other words, the initial magnified state is sticky.
90 *
91 * 3. Pinching with any number of additional fingers when viewport dragging
92 *    is enabled, i.e. the user triple tapped and holds, would adjust the
93 *    magnification scale which will become the current default magnification
94 *    scale. The next time the user magnifies the same magnification scale
95 *    would be used.
96 *
97 * 4. When in a permanent magnified state the user can use two or more fingers
98 *    to pan the viewport. Note that in this mode the content is panned as
99 *    opposed to the viewport dragging mode in which the viewport is moved.
100 *
101 * 5. When in a permanent magnified state the user can use three or more
102 *    fingers to change the magnification scale which will become the current
103 *    default magnification scale. The next time the user magnifies the same
104 *    magnification scale would be used.
105 *
106 * 6. The magnification scale will be persisted in settings and in the cloud.
107 */
108public final class ScreenMagnifier implements EventStreamTransformation {
109
110    private static final boolean DEBUG_STATE_TRANSITIONS = false;
111    private static final boolean DEBUG_DETECTING = false;
112    private static final boolean DEBUG_TRANSFORMATION = false;
113    private static final boolean DEBUG_PANNING = false;
114    private static final boolean DEBUG_SCALING = false;
115    private static final boolean DEBUG_VIEWPORT_WINDOW = false;
116    private static final boolean DEBUG_WINDOW_TRANSITIONS = false;
117    private static final boolean DEBUG_ROTATION = false;
118    private static final boolean DEBUG_GESTURE_DETECTOR = false;
119    private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false;
120
121    private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName();
122
123    private static final int STATE_DELEGATING = 1;
124    private static final int STATE_DETECTING = 2;
125    private static final int STATE_SCALING = 3;
126    private static final int STATE_VIEWPORT_DRAGGING = 4;
127    private static final int STATE_PANNING = 5;
128    private static final int STATE_DECIDE_PAN_OR_SCALE = 6;
129
130    private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;
131    private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1;
132    private static final float DEFAULT_WINDOW_ANIMATION_SCALE = 1.0f;
133
134    private final IWindowManager mWindowManagerService = IWindowManager.Stub.asInterface(
135            ServiceManager.getService("window"));
136    private final WindowManager mWindowManager;
137    private final DisplayProvider mDisplayProvider;
138
139    private final DetectingStateHandler mDetectingStateHandler = new DetectingStateHandler();
140    private final GestureDetector mGestureDetector;
141    private final StateViewportDraggingHandler mStateViewportDraggingHandler =
142            new StateViewportDraggingHandler();
143
144    private final Interpolator mInterpolator = new DecelerateInterpolator(2.5f);
145
146    private final MagnificationController mMagnificationController;
147    private final DisplayContentObserver mDisplayContentObserver;
148    private final Viewport mViewport;
149
150    private final int mTapTimeSlop = ViewConfiguration.getTapTimeout();
151    private final int mMultiTapTimeSlop = ViewConfiguration.getDoubleTapTimeout();
152    private final int mTapDistanceSlop;
153    private final int mMultiTapDistanceSlop;
154
155    private final int mShortAnimationDuration;
156    private final int mLongAnimationDuration;
157    private final float mWindowAnimationScale;
158
159    private final Context mContext;
160
161    private EventStreamTransformation mNext;
162
163    private int mCurrentState;
164    private boolean mTranslationEnabledBeforePan;
165
166    private PointerCoords[] mTempPointerCoords;
167    private PointerProperties[] mTempPointerProperties;
168
169    public ScreenMagnifier(Context context) {
170        mContext = context;
171        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
172
173        mShortAnimationDuration = context.getResources().getInteger(
174                com.android.internal.R.integer.config_shortAnimTime);
175        mLongAnimationDuration = context.getResources().getInteger(
176                com.android.internal.R.integer.config_longAnimTime);
177        mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop();
178        mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
179        mWindowAnimationScale = Settings.System.getFloat(context.getContentResolver(),
180                Settings.System.WINDOW_ANIMATION_SCALE, DEFAULT_WINDOW_ANIMATION_SCALE);
181
182        mMagnificationController = new MagnificationController(mShortAnimationDuration);
183        mDisplayProvider = new DisplayProvider(context, mWindowManager);
184        mViewport = new Viewport(mContext, mWindowManager, mWindowManagerService,
185                mDisplayProvider, mInterpolator, mShortAnimationDuration);
186        mDisplayContentObserver = new DisplayContentObserver(mContext, mViewport,
187                mMagnificationController, mWindowManagerService, mDisplayProvider,
188                mLongAnimationDuration, mWindowAnimationScale);
189
190        mGestureDetector = new GestureDetector(context);
191
192        transitionToState(STATE_DETECTING);
193    }
194
195    @Override
196    public void onMotionEvent(MotionEvent event, int policyFlags) {
197        switch (mCurrentState) {
198            case STATE_DELEGATING: {
199                handleMotionEventStateDelegating(event, policyFlags);
200            } break;
201            case STATE_DETECTING: {
202                mDetectingStateHandler.onMotionEvent(event, policyFlags);
203            } break;
204            case STATE_VIEWPORT_DRAGGING: {
205                mStateViewportDraggingHandler.onMotionEvent(event, policyFlags);
206            } break;
207            case STATE_SCALING:
208            case STATE_PANNING:
209            case STATE_DECIDE_PAN_OR_SCALE: {
210                // Handled by the gesture detector. Since the detector
211                // needs all touch events to work properly we cannot
212                // call it only for these states.
213            } break;
214            default: {
215                throw new IllegalStateException("Unknown state: " + mCurrentState);
216            }
217        }
218        mGestureDetector.onMotionEvent(event);
219    }
220
221    @Override
222    public void onAccessibilityEvent(AccessibilityEvent event) {
223        if (mNext != null) {
224            mNext.onAccessibilityEvent(event);
225        }
226    }
227
228    @Override
229    public void setNext(EventStreamTransformation next) {
230        mNext = next;
231    }
232
233    @Override
234    public void clear() {
235        mCurrentState = STATE_DETECTING;
236        mDetectingStateHandler.clear();
237        mStateViewportDraggingHandler.clear();
238        mGestureDetector.clear();
239
240        if (mNext != null) {
241            mNext.clear();
242        }
243    }
244
245    @Override
246    public void onDestroy() {
247        mDisplayProvider.destroy();
248        mDisplayContentObserver.destroy();
249    }
250
251    private void handleMotionEventStateDelegating(MotionEvent event, int policyFlags) {
252        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
253            if (mDetectingStateHandler.mDelayedEventQueue == null) {
254                transitionToState(STATE_DETECTING);
255            }
256        }
257        if (mNext != null) {
258            // If the event is within the magnified portion of the screen we have
259            // to change its location to be where the user thinks he is poking the
260            // UI which may have been magnified and panned.
261            final float eventX = event.getX();
262            final float eventY = event.getY();
263            if (mMagnificationController.isMagnifying()
264                    && mViewport.getBounds().contains((int) eventX, (int) eventY)) {
265                final float scale = mMagnificationController.getScale();
266                final float scaledOffsetX = mMagnificationController.getScaledOffsetX();
267                final float scaledOffsetY = mMagnificationController.getScaledOffsetY();
268                final int pointerCount = event.getPointerCount();
269                PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
270                PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
271                for (int i = 0; i < pointerCount; i++) {
272                    event.getPointerCoords(i, coords[i]);
273                    coords[i].x = (coords[i].x - scaledOffsetX) / scale;
274                    coords[i].y = (coords[i].y - scaledOffsetY) / scale;
275                    event.getPointerProperties(i, properties[i]);
276                }
277                event = MotionEvent.obtain(event.getDownTime(),
278                        event.getEventTime(), event.getAction(), pointerCount, properties,
279                        coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(),
280                        event.getFlags());
281            }
282            mNext.onMotionEvent(event, policyFlags);
283        }
284    }
285
286    private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
287        final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
288        if (oldSize < size) {
289            PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
290            mTempPointerCoords = new PointerCoords[size];
291            if (oldTempPointerCoords != null) {
292                System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
293            }
294        }
295        for (int i = oldSize; i < size; i++) {
296            mTempPointerCoords[i] = new PointerCoords();
297        }
298        return mTempPointerCoords;
299    }
300
301    private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
302        final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0;
303        if (oldSize < size) {
304            PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
305            mTempPointerProperties = new PointerProperties[size];
306            if (oldTempPointerProperties != null) {
307                System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize);
308            }
309        }
310        for (int i = oldSize; i < size; i++) {
311            mTempPointerProperties[i] = new PointerProperties();
312        }
313        return mTempPointerProperties;
314    }
315
316    private void transitionToState(int state) {
317        if (DEBUG_STATE_TRANSITIONS) {
318            switch (state) {
319                case STATE_DELEGATING: {
320                    Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING");
321                } break;
322                case STATE_DETECTING: {
323                    Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING");
324                } break;
325                case STATE_VIEWPORT_DRAGGING: {
326                    Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING");
327                } break;
328                case STATE_SCALING: {
329                    Slog.i(LOG_TAG, "mCurrentState: STATE_SCALING");
330                } break;
331                case STATE_PANNING: {
332                    Slog.i(LOG_TAG, "mCurrentState: STATE_PANNING");
333                } break;
334                case STATE_DECIDE_PAN_OR_SCALE: {
335                    Slog.i(LOG_TAG, "mCurrentState: STATE_DECIDE_PAN_OR_SCALE");
336                } break;
337                default: {
338                    throw new IllegalArgumentException("Unknown state: " + state);
339                }
340            }
341        }
342        mCurrentState = state;
343    }
344
345    private final class GestureDetector implements OnScaleGestureListener {
346        private static final float MIN_SCALE = 1.3f;
347        private static final float MAX_SCALE = 5.0f;
348
349        private static final float DETECT_SCALING_THRESHOLD = 0.25f;
350        private static final int DETECT_PANNING_THRESHOLD_DIP = 30;
351
352        private final float mScaledDetectPanningThreshold;
353
354        private final ScaleGestureDetector mScaleGestureDetector;
355
356        private final PointF mPrevFocus = new PointF(Float.NaN, Float.NaN);
357        private final PointF mInitialFocus = new PointF(Float.NaN, Float.NaN);
358
359        private float mCurrScale = Float.NaN;
360        private float mCurrScaleFactor = 1.0f;
361        private float mPrevScaleFactor = 1.0f;
362        private float mCurrPan;
363        private float mPrevPan;
364
365        private float mScaleFocusX = Float.NaN;
366        private float mScaleFocusY = Float.NaN;
367
368        public GestureDetector(Context context) {
369            final float density = context.getResources().getDisplayMetrics().density;
370            mScaledDetectPanningThreshold = DETECT_PANNING_THRESHOLD_DIP * density;
371            mScaleGestureDetector = new ScaleGestureDetector(context, this);
372        }
373
374        public void onMotionEvent(MotionEvent event) {
375            mScaleGestureDetector.onTouchEvent(event);
376            switch (mCurrentState) {
377                case STATE_DETECTING:
378                case STATE_DELEGATING:
379                case STATE_VIEWPORT_DRAGGING: {
380                    return;
381                }
382            }
383            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
384                clear();
385                if (mCurrentState == STATE_SCALING) {
386                    persistScale(mMagnificationController.getScale());
387                }
388                transitionToState(STATE_DETECTING);
389            }
390        }
391
392        @Override
393        public boolean onScale(ScaleGestureDetector detector) {
394            switch (mCurrentState) {
395                case STATE_DETECTING:
396                case STATE_DELEGATING:
397                case STATE_VIEWPORT_DRAGGING: {
398                    return true;
399                }
400                case STATE_DECIDE_PAN_OR_SCALE: {
401                    mCurrScaleFactor = mScaleGestureDetector.getScaleFactor();
402                    final float scaleDelta = Math.abs(1.0f - mCurrScaleFactor * mPrevScaleFactor);
403                    if (DEBUG_GESTURE_DETECTOR) {
404                        Slog.i(LOG_TAG, "scaleDelta: " + scaleDelta);
405                    }
406                    if (scaleDelta > DETECT_SCALING_THRESHOLD) {
407                        performScale(detector, true);
408                        clear();
409                        transitionToState(STATE_SCALING);
410                        return false;
411                    }
412                    mCurrPan = (float) MathUtils.dist(
413                            mScaleGestureDetector.getFocusX(),
414                            mScaleGestureDetector.getFocusY(),
415                            mInitialFocus.x, mInitialFocus.y);
416                    final float panDelta = mCurrPan + mPrevPan;
417                    if (DEBUG_GESTURE_DETECTOR) {
418                        Slog.i(LOG_TAG, "panDelta: " + panDelta);
419                    }
420                    if (panDelta > mScaledDetectPanningThreshold) {
421                        performPan(detector, true);
422                        clear();
423                        transitionToState(STATE_PANNING);
424                        return false;
425                    }
426                } break;
427                case STATE_SCALING: {
428                    performScale(detector, false);
429                } break;
430                case STATE_PANNING: {
431                    performPan(detector, false);
432                } break;
433            }
434            return false;
435        }
436
437        @Override
438        public boolean onScaleBegin(ScaleGestureDetector detector) {
439            switch (mCurrentState) {
440                case STATE_DECIDE_PAN_OR_SCALE: {
441                    mPrevScaleFactor *= mCurrScaleFactor;
442                    mPrevPan += mCurrPan;
443                    mPrevFocus.x = mInitialFocus.x = detector.getFocusX();
444                    mPrevFocus.y = mInitialFocus.y = detector.getFocusY();
445                } break;
446                case STATE_SCALING: {
447                    mPrevScaleFactor = 1.0f;
448                    mCurrScale = Float.NaN;
449                } break;
450                case STATE_PANNING: {
451                    mPrevPan += mCurrPan;
452                    mPrevFocus.x = mInitialFocus.x = detector.getFocusX();
453                    mPrevFocus.y = mInitialFocus.y = detector.getFocusY();
454                } break;
455            }
456            return true;
457        }
458
459        @Override
460        public void onScaleEnd(ScaleGestureDetector detector) {
461            /* do nothing */
462        }
463
464        public void clear() {
465            mCurrScaleFactor = 1.0f;
466            mPrevScaleFactor = 1.0f;
467            mPrevPan = 0;
468            mCurrPan = 0;
469            mInitialFocus.set(Float.NaN, Float.NaN);
470            mPrevFocus.set(Float.NaN, Float.NaN);
471            mCurrScale = Float.NaN;
472            mScaleFocusX = Float.NaN;
473            mScaleFocusY = Float.NaN;
474        }
475
476        private void performPan(ScaleGestureDetector detector, boolean animate) {
477            if (Float.compare(mPrevFocus.x, Float.NaN) == 0
478                    && Float.compare(mPrevFocus.y, Float.NaN) == 0) {
479                mPrevFocus.set(detector.getFocusX(), detector.getFocusY());
480                return;
481            }
482            final float scale = mMagnificationController.getScale();
483            final float scrollX = (detector.getFocusX() - mPrevFocus.x) / scale;
484            final float scrollY = (detector.getFocusY() - mPrevFocus.y) / scale;
485            final float centerX = mMagnificationController.getMagnifiedRegionCenterX()
486                    - scrollX;
487            final float centerY = mMagnificationController.getMagnifiedRegionCenterY()
488                    - scrollY;
489            if (DEBUG_PANNING) {
490                Slog.i(LOG_TAG, "Panned content by scrollX: " + scrollX
491                        + " scrollY: " + scrollY);
492            }
493            mMagnificationController.setMagnifiedRegionCenter(centerX, centerY, animate);
494            mPrevFocus.set(detector.getFocusX(), detector.getFocusY());
495        }
496
497        private void performScale(ScaleGestureDetector detector, boolean animate) {
498            if (Float.compare(mCurrScale, Float.NaN) == 0) {
499                mCurrScale = mMagnificationController.getScale();
500                return;
501            }
502            final float totalScaleFactor = mPrevScaleFactor * detector.getScaleFactor();
503            final float newScale = mCurrScale * totalScaleFactor;
504            final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE),
505                    MAX_SCALE);
506            if (DEBUG_SCALING) {
507                Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale);
508            }
509            if (Float.compare(mScaleFocusX, Float.NaN) == 0
510                    && Float.compare(mScaleFocusY, Float.NaN) == 0) {
511                mScaleFocusX = detector.getFocusX();
512                mScaleFocusY = detector.getFocusY();
513            }
514            mMagnificationController.setScale(normalizedNewScale, mScaleFocusX,
515                    mScaleFocusY, animate);
516        }
517    }
518
519    private final class StateViewportDraggingHandler {
520        private boolean mLastMoveOutsideMagnifiedRegion;
521
522        private void onMotionEvent(MotionEvent event, int policyFlags) {
523            final int action = event.getActionMasked();
524            switch (action) {
525                case MotionEvent.ACTION_DOWN: {
526                    throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN");
527                }
528                case MotionEvent.ACTION_POINTER_DOWN: {
529                    clear();
530                    transitionToState(STATE_SCALING);
531                } break;
532                case MotionEvent.ACTION_MOVE: {
533                    if (event.getPointerCount() != 1) {
534                        throw new IllegalStateException("Should have one pointer down.");
535                    }
536                    final float eventX = event.getX();
537                    final float eventY = event.getY();
538                    if (mViewport.getBounds().contains((int) eventX, (int) eventY)) {
539                        if (mLastMoveOutsideMagnifiedRegion) {
540                            mLastMoveOutsideMagnifiedRegion = false;
541                            mMagnificationController.setMagnifiedRegionCenter(eventX,
542                                    eventY, true);
543                        } else {
544                            mMagnificationController.setMagnifiedRegionCenter(eventX,
545                                    eventY, false);
546                        }
547                    } else {
548                        mLastMoveOutsideMagnifiedRegion = true;
549                    }
550                } break;
551                case MotionEvent.ACTION_UP: {
552                    if (!mTranslationEnabledBeforePan) {
553                        mMagnificationController.reset(true);
554                        mViewport.setFrameShown(false, true);
555                    }
556                    clear();
557                    transitionToState(STATE_DETECTING);
558                } break;
559                case MotionEvent.ACTION_POINTER_UP: {
560                    throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP");
561                }
562            }
563        }
564
565        public void clear() {
566            mLastMoveOutsideMagnifiedRegion = false;
567        }
568    }
569
570    private final class DetectingStateHandler {
571
572        private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1;
573
574        private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
575
576        private static final int ACTION_TAP_COUNT = 3;
577
578        private MotionEventInfo mDelayedEventQueue;
579
580        private MotionEvent mLastDownEvent;
581        private MotionEvent mLastTapUpEvent;
582        private int mTapCount;
583
584        private final Handler mHandler = new Handler() {
585            @Override
586            public void handleMessage(Message message) {
587                final int type = message.what;
588                switch (type) {
589                    case MESSAGE_ON_ACTION_TAP_AND_HOLD: {
590                        MotionEvent event = (MotionEvent) message.obj;
591                        final int policyFlags = message.arg1;
592                        onActionTapAndHold(event, policyFlags);
593                    } break;
594                    case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
595                        transitionToState(STATE_DELEGATING);
596                        sendDelayedMotionEvents();
597                        clear();
598                    } break;
599                    default: {
600                        throw new IllegalArgumentException("Unknown message type: " + type);
601                    }
602                }
603            }
604        };
605
606        public void onMotionEvent(MotionEvent event, int policyFlags) {
607            cacheDelayedMotionEvent(event, policyFlags);
608            final int action = event.getActionMasked();
609            switch (action) {
610                case MotionEvent.ACTION_DOWN: {
611                    mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
612                    if (!mViewport.getBounds().contains((int) event.getX(),
613                            (int) event.getY())) {
614                        transitionToDelegatingStateAndClear();
615                        return;
616                    }
617                    if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null
618                            && GestureUtils.isMultiTap(mLastDownEvent, event,
619                                    mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
620                        Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD,
621                                policyFlags, 0, event);
622                        mHandler.sendMessageDelayed(message,
623                                ViewConfiguration.getLongPressTimeout());
624                    } else if (mTapCount < ACTION_TAP_COUNT) {
625                        Message message = mHandler.obtainMessage(
626                                MESSAGE_TRANSITION_TO_DELEGATING_STATE);
627                        mHandler.sendMessageDelayed(message, mTapTimeSlop + mMultiTapDistanceSlop);
628                    }
629                    clearLastDownEvent();
630                    mLastDownEvent = MotionEvent.obtain(event);
631                } break;
632                case MotionEvent.ACTION_POINTER_DOWN: {
633                    if (mMagnificationController.isMagnifying()) {
634                        transitionToState(STATE_DECIDE_PAN_OR_SCALE);
635                        clear();
636                    } else {
637                        transitionToDelegatingStateAndClear();
638                    }
639                } break;
640                case MotionEvent.ACTION_MOVE: {
641                    if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) {
642                        final double distance = GestureUtils.computeDistance(mLastDownEvent,
643                                event, 0);
644                        if (Math.abs(distance) > mTapDistanceSlop) {
645                            transitionToDelegatingStateAndClear();
646                        }
647                    }
648                } break;
649                case MotionEvent.ACTION_UP: {
650                    if (mLastDownEvent == null) {
651                        return;
652                    }
653                    mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
654                    if (!mViewport.getBounds().contains((int) event.getX(), (int) event.getY())) {
655                         transitionToDelegatingStateAndClear();
656                         return;
657                    }
658                    if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop,
659                            mTapDistanceSlop, 0)) {
660                        transitionToDelegatingStateAndClear();
661                        return;
662                    }
663                    if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent,
664                            event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
665                        transitionToDelegatingStateAndClear();
666                        return;
667                    }
668                    mTapCount++;
669                    if (DEBUG_DETECTING) {
670                        Slog.i(LOG_TAG, "Tap count:" + mTapCount);
671                    }
672                    if (mTapCount == ACTION_TAP_COUNT) {
673                        clear();
674                        onActionTap(event, policyFlags);
675                        return;
676                    }
677                    clearLastTapUpEvent();
678                    mLastTapUpEvent = MotionEvent.obtain(event);
679                } break;
680                case MotionEvent.ACTION_POINTER_UP: {
681                    /* do nothing */
682                } break;
683            }
684        }
685
686        public void clear() {
687            mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
688            mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
689            clearTapDetectionState();
690            clearDelayedMotionEvents();
691        }
692
693        private void clearTapDetectionState() {
694            mTapCount = 0;
695            clearLastTapUpEvent();
696            clearLastDownEvent();
697        }
698
699        private void clearLastTapUpEvent() {
700            if (mLastTapUpEvent != null) {
701                mLastTapUpEvent.recycle();
702                mLastTapUpEvent = null;
703            }
704        }
705
706        private void clearLastDownEvent() {
707            if (mLastDownEvent != null) {
708                mLastDownEvent.recycle();
709                mLastDownEvent = null;
710            }
711        }
712
713        private void cacheDelayedMotionEvent(MotionEvent event, int policyFlags) {
714            MotionEventInfo info = MotionEventInfo.obtain(event, policyFlags);
715            if (mDelayedEventQueue == null) {
716                mDelayedEventQueue = info;
717            } else {
718                MotionEventInfo tail = mDelayedEventQueue;
719                while (tail.mNext != null) {
720                    tail = tail.mNext;
721                }
722                tail.mNext = info;
723            }
724        }
725
726        private void sendDelayedMotionEvents() {
727            while (mDelayedEventQueue != null) {
728                MotionEventInfo info = mDelayedEventQueue;
729                mDelayedEventQueue = info.mNext;
730                ScreenMagnifier.this.onMotionEvent(info.mEvent, info.mPolicyFlags);
731                info.recycle();
732            }
733        }
734
735        private void clearDelayedMotionEvents() {
736            while (mDelayedEventQueue != null) {
737                MotionEventInfo info = mDelayedEventQueue;
738                mDelayedEventQueue = info.mNext;
739                info.recycle();
740            }
741        }
742
743        private void transitionToDelegatingStateAndClear() {
744            transitionToState(STATE_DELEGATING);
745            sendDelayedMotionEvents();
746            clear();
747        }
748
749        private void onActionTap(MotionEvent up, int policyFlags) {
750            if (DEBUG_DETECTING) {
751                Slog.i(LOG_TAG, "onActionTap()");
752            }
753            if (!mMagnificationController.isMagnifying()) {
754                mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
755                        up.getX(), up.getY(), true);
756                mViewport.setFrameShown(true, true);
757            } else {
758                mMagnificationController.reset(true);
759                mViewport.setFrameShown(false, true);
760            }
761        }
762
763        private void onActionTapAndHold(MotionEvent down, int policyFlags) {
764            if (DEBUG_DETECTING) {
765                Slog.i(LOG_TAG, "onActionTapAndHold()");
766            }
767            clear();
768            mTranslationEnabledBeforePan = mMagnificationController.isMagnifying();
769            mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
770                    down.getX(), down.getY(), true);
771            mViewport.setFrameShown(true, true);
772            transitionToState(STATE_VIEWPORT_DRAGGING);
773        }
774    }
775
776    private void persistScale(final float scale) {
777        new AsyncTask<Void, Void, Void>() {
778            @Override
779            protected Void doInBackground(Void... params) {
780                Settings.Secure.putFloat(mContext.getContentResolver(),
781                        Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale);
782                return null;
783            }
784        }.execute();
785    }
786
787    private float getPersistedScale() {
788        return Settings.Secure.getFloat(mContext.getContentResolver(),
789                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
790                DEFAULT_MAGNIFICATION_SCALE);
791    }
792
793    private static final class MotionEventInfo {
794
795        private static final int MAX_POOL_SIZE = 10;
796
797        private static final Object sLock = new Object();
798        private static MotionEventInfo sPool;
799        private static int sPoolSize;
800
801        private MotionEventInfo mNext;
802        private boolean mInPool;
803
804        public MotionEvent mEvent;
805        public int mPolicyFlags;
806
807        public static MotionEventInfo obtain(MotionEvent event, int policyFlags) {
808            synchronized (sLock) {
809                MotionEventInfo info;
810                if (sPoolSize > 0) {
811                    sPoolSize--;
812                    info = sPool;
813                    sPool = info.mNext;
814                    info.mNext = null;
815                    info.mInPool = false;
816                } else {
817                    info = new MotionEventInfo();
818                }
819                info.initialize(event, policyFlags);
820                return info;
821            }
822        }
823
824        private void initialize(MotionEvent event, int policyFlags) {
825            mEvent = MotionEvent.obtain(event);
826            mPolicyFlags = policyFlags;
827        }
828
829        public void recycle() {
830            synchronized (sLock) {
831                if (mInPool) {
832                    throw new IllegalStateException("Already recycled.");
833                }
834                clear();
835                if (sPoolSize < MAX_POOL_SIZE) {
836                    sPoolSize++;
837                    mNext = sPool;
838                    sPool = this;
839                    mInPool = true;
840                }
841            }
842        }
843
844        private void clear() {
845            mEvent.recycle();
846            mEvent = null;
847            mPolicyFlags = 0;
848        }
849    }
850
851    private static final class DisplayContentObserver {
852
853        private static final int MESSAGE_SHOW_VIEWPORT_FRAME = 1;
854        private static final int MESSAGE_RECOMPUTE_VIEWPORT_BOUNDS = 2;
855        private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 3;
856        private static final int MESSAGE_ON_WINDOW_TRANSITION = 4;
857        private static final int MESSAGE_ON_ROTATION_CHANGED = 5;
858
859        private final Handler mHandler = new MyHandler();
860
861        private final Rect mTempRect = new Rect();
862
863        private final IDisplayContentChangeListener mDisplayContentChangeListener;
864
865        private final Context mContext;
866        private final Viewport mViewport;
867        private final MagnificationController mMagnificationController;
868        private final IWindowManager mWindowManagerService;
869        private final DisplayProvider mDisplayProvider;
870        private final long mLongAnimationDuration;
871        private final float mWindowAnimationScale;
872
873        public DisplayContentObserver(Context context, Viewport viewport,
874                MagnificationController magnificationController,
875                IWindowManager windowManagerService, DisplayProvider displayProvider,
876                long longAnimationDuration, float windowAnimationScale) {
877            mContext = context;
878            mViewport = viewport;
879            mMagnificationController = magnificationController;
880            mWindowManagerService = windowManagerService;
881            mDisplayProvider = displayProvider;
882            mLongAnimationDuration = longAnimationDuration;
883            mWindowAnimationScale = windowAnimationScale;
884
885            mDisplayContentChangeListener = new IDisplayContentChangeListener.Stub() {
886                @Override
887                public void onWindowTransition(int displayId, int transition, WindowInfo info) {
888                    mHandler.obtainMessage(MESSAGE_ON_WINDOW_TRANSITION, transition, 0,
889                            WindowInfo.obtain(info)).sendToTarget();
890                }
891
892                @Override
893                public void onRectangleOnScreenRequested(int dsiplayId, Rect rectangle,
894                        boolean immediate) {
895                    SomeArgs args = SomeArgs.obtain();
896                    args.argi1 = rectangle.left;
897                    args.argi2 = rectangle.top;
898                    args.argi3 = rectangle.right;
899                    args.argi4 = rectangle.bottom;
900                    mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, 0,
901                            immediate ? 1 : 0, args).sendToTarget();
902                }
903
904                @Override
905                public void onRotationChanged(int rotation) throws RemoteException {
906                    mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0)
907                            .sendToTarget();
908                }
909            };
910
911            try {
912                mWindowManagerService.addDisplayContentChangeListener(
913                        mDisplayProvider.getDisplay().getDisplayId(),
914                        mDisplayContentChangeListener);
915            } catch (RemoteException re) {
916                /* ignore */
917            }
918        }
919
920        public void destroy() {
921            try {
922                mWindowManagerService.removeDisplayContentChangeListener(
923                        mDisplayProvider.getDisplay().getDisplayId(),
924                        mDisplayContentChangeListener);
925            } catch (RemoteException re) {
926                /* ignore*/
927            }
928        }
929
930        private void handleOnRotationChanged(int rotation) {
931            if (DEBUG_ROTATION) {
932                Slog.i(LOG_TAG, "Rotation: " + rotationToString(rotation));
933            }
934            resetMagnificationIfNeeded();
935            mViewport.setFrameShown(false, false);
936            mViewport.rotationChanged();
937            mViewport.recomputeBounds(false);
938            if (mMagnificationController.isMagnifying()) {
939                final long delay = (long) (2 * mLongAnimationDuration * mWindowAnimationScale);
940                Message message = mHandler.obtainMessage(MESSAGE_SHOW_VIEWPORT_FRAME);
941                mHandler.sendMessageDelayed(message, delay);
942            }
943        }
944
945        private void handleOnWindowTransition(int transition, WindowInfo info) {
946            if (DEBUG_WINDOW_TRANSITIONS) {
947                Slog.i(LOG_TAG, "Window transitioning: "
948                        + windowTransitionToString(transition));
949            }
950            try {
951                final boolean magnifying = mMagnificationController.isMagnifying();
952                if (magnifying) {
953                    switch (transition) {
954                        case WindowManagerPolicy.TRANSIT_ACTIVITY_OPEN:
955                        case WindowManagerPolicy.TRANSIT_TASK_OPEN:
956                        case WindowManagerPolicy.TRANSIT_TASK_TO_FRONT:
957                        case WindowManagerPolicy.TRANSIT_WALLPAPER_OPEN:
958                        case WindowManagerPolicy.TRANSIT_WALLPAPER_CLOSE:
959                        case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_OPEN: {
960                            resetMagnificationIfNeeded();
961                        }
962                    }
963                }
964                if (info.type == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
965                        || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD
966                        || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG) {
967                    switch (transition) {
968                        case WindowManagerPolicy.TRANSIT_ENTER:
969                        case WindowManagerPolicy.TRANSIT_SHOW:
970                        case WindowManagerPolicy.TRANSIT_EXIT:
971                        case WindowManagerPolicy.TRANSIT_HIDE: {
972                            mViewport.recomputeBounds(mMagnificationController.isMagnifying());
973                        } break;
974                    }
975                } else {
976                    switch (transition) {
977                        case WindowManagerPolicy.TRANSIT_ENTER:
978                        case WindowManagerPolicy.TRANSIT_SHOW: {
979                            if (!magnifying || !screenMagnificationAutoUpdateEnabled(mContext)) {
980                                break;
981                            }
982                            final int type = info.type;
983                            switch (type) {
984                                // TODO: Are these all the windows we want to make
985                                //       visible when they appear on the screen?
986                                //       Do we need to take some of them out?
987                                case WindowManager.LayoutParams.TYPE_APPLICATION_PANEL:
988                                case WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA:
989                                case WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL:
990                                case WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG:
991                                case WindowManager.LayoutParams.TYPE_SEARCH_BAR:
992                                case WindowManager.LayoutParams.TYPE_PHONE:
993                                case WindowManager.LayoutParams.TYPE_SYSTEM_ALERT:
994                                case WindowManager.LayoutParams.TYPE_TOAST:
995                                case WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY:
996                                case WindowManager.LayoutParams.TYPE_PRIORITY_PHONE:
997                                case WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG:
998                                case WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG:
999                                case WindowManager.LayoutParams.TYPE_SYSTEM_ERROR:
1000                                case WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY:
1001                                case WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL: {
1002                                    Rect magnifiedRegionBounds = mMagnificationController
1003                                            .getMagnifiedRegionBounds();
1004                                    Rect touchableRegion = info.touchableRegion;
1005                                    if (!magnifiedRegionBounds.intersect(touchableRegion)) {
1006                                        ensureRectangleInMagnifiedRegionBounds(
1007                                                magnifiedRegionBounds, touchableRegion);
1008                                    }
1009                                } break;
1010                            } break;
1011                        }
1012                    }
1013                }
1014            } finally {
1015                if (info != null) {
1016                    info.recycle();
1017                }
1018            }
1019        }
1020
1021        private void handleOnRectangleOnScreenRequested(Rect rectangle, boolean immediate) {
1022            if (!mMagnificationController.isMagnifying()) {
1023                return;
1024            }
1025            Rect magnifiedRegionBounds = mMagnificationController.getMagnifiedRegionBounds();
1026            if (magnifiedRegionBounds.contains(rectangle)) {
1027                return;
1028            }
1029            ensureRectangleInMagnifiedRegionBounds(magnifiedRegionBounds, rectangle);
1030        }
1031
1032        private void ensureRectangleInMagnifiedRegionBounds(Rect magnifiedRegionBounds,
1033                Rect rectangle) {
1034            if (!Rect.intersects(rectangle, mViewport.getBounds())) {
1035                return;
1036            }
1037            final float scrollX;
1038            final float scrollY;
1039            if (rectangle.width() > magnifiedRegionBounds.width()) {
1040                scrollX = rectangle.left - magnifiedRegionBounds.left;
1041            } else if (rectangle.left < magnifiedRegionBounds.left) {
1042                scrollX = rectangle.left - magnifiedRegionBounds.left;
1043            } else if (rectangle.right > magnifiedRegionBounds.right) {
1044                scrollX = rectangle.right - magnifiedRegionBounds.right;
1045            } else {
1046                scrollX = 0;
1047            }
1048            if (rectangle.height() > magnifiedRegionBounds.height()) {
1049                scrollY = rectangle.top - magnifiedRegionBounds.top;
1050            } else if (rectangle.top < magnifiedRegionBounds.top) {
1051                scrollY = rectangle.top - magnifiedRegionBounds.top;
1052            } else if (rectangle.bottom > magnifiedRegionBounds.bottom) {
1053                scrollY = rectangle.bottom - magnifiedRegionBounds.bottom;
1054            } else {
1055                scrollY = 0;
1056            }
1057            final float viewportCenterX = mMagnificationController.getMagnifiedRegionCenterX()
1058                    + scrollX;
1059            final float viewportCenterY = mMagnificationController.getMagnifiedRegionCenterY()
1060                    + scrollY;
1061            mMagnificationController.setMagnifiedRegionCenter(viewportCenterX, viewportCenterY,
1062                    true);
1063        }
1064
1065        private void resetMagnificationIfNeeded() {
1066            if (mMagnificationController.isMagnifying()
1067                    && screenMagnificationAutoUpdateEnabled(mContext)) {
1068                mMagnificationController.reset(true);
1069                mViewport.setFrameShown(false, true);
1070            }
1071        }
1072
1073        private boolean screenMagnificationAutoUpdateEnabled(Context context) {
1074            return (Settings.Secure.getInt(context.getContentResolver(),
1075                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
1076                    DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
1077        }
1078
1079        private String windowTransitionToString(int transition) {
1080            switch (transition) {
1081                case WindowManagerPolicy.TRANSIT_UNSET: {
1082                    return "TRANSIT_UNSET";
1083                }
1084                case WindowManagerPolicy.TRANSIT_NONE: {
1085                    return "TRANSIT_NONE";
1086                }
1087                case WindowManagerPolicy.TRANSIT_ENTER: {
1088                    return "TRANSIT_ENTER";
1089                }
1090                case WindowManagerPolicy.TRANSIT_EXIT: {
1091                    return "TRANSIT_EXIT";
1092                }
1093                case WindowManagerPolicy.TRANSIT_SHOW: {
1094                    return "TRANSIT_SHOW";
1095                }
1096                case WindowManagerPolicy.TRANSIT_EXIT_MASK: {
1097                    return "TRANSIT_EXIT_MASK";
1098                }
1099                case WindowManagerPolicy.TRANSIT_PREVIEW_DONE: {
1100                    return "TRANSIT_PREVIEW_DONE";
1101                }
1102                case WindowManagerPolicy.TRANSIT_ACTIVITY_OPEN: {
1103                    return "TRANSIT_ACTIVITY_OPEN";
1104                }
1105                case WindowManagerPolicy.TRANSIT_ACTIVITY_CLOSE: {
1106                    return "TRANSIT_ACTIVITY_CLOSE";
1107                }
1108                case WindowManagerPolicy.TRANSIT_TASK_OPEN: {
1109                    return "TRANSIT_TASK_OPEN";
1110                }
1111                case WindowManagerPolicy.TRANSIT_TASK_CLOSE: {
1112                    return "TRANSIT_TASK_CLOSE";
1113                }
1114                case WindowManagerPolicy.TRANSIT_TASK_TO_FRONT: {
1115                    return "TRANSIT_TASK_TO_FRONT";
1116                }
1117                case WindowManagerPolicy.TRANSIT_TASK_TO_BACK: {
1118                    return "TRANSIT_TASK_TO_BACK";
1119                }
1120                case WindowManagerPolicy.TRANSIT_WALLPAPER_CLOSE: {
1121                    return "TRANSIT_WALLPAPER_CLOSE";
1122                }
1123                case WindowManagerPolicy.TRANSIT_WALLPAPER_OPEN: {
1124                    return "TRANSIT_WALLPAPER_OPEN";
1125                }
1126                case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_OPEN: {
1127                    return "TRANSIT_WALLPAPER_INTRA_OPEN";
1128                }
1129                case WindowManagerPolicy.TRANSIT_WALLPAPER_INTRA_CLOSE: {
1130                    return "TRANSIT_WALLPAPER_INTRA_CLOSE";
1131                }
1132                default: {
1133                    return "<UNKNOWN>";
1134                }
1135            }
1136        }
1137
1138        private String rotationToString(int rotation) {
1139            switch (rotation) {
1140                case Surface.ROTATION_0: {
1141                    return "ROTATION_0";
1142                }
1143                case Surface.ROTATION_90: {
1144                    return "ROATATION_90";
1145                }
1146                case Surface.ROTATION_180: {
1147                    return "ROATATION_180";
1148                }
1149                case Surface.ROTATION_270: {
1150                    return "ROATATION_270";
1151                }
1152                default: {
1153                    throw new IllegalArgumentException("Invalid rotation: "
1154                        + rotation);
1155                }
1156            }
1157        }
1158
1159        private final class MyHandler extends Handler {
1160            @Override
1161            public void handleMessage(Message message) {
1162                final int action = message.what;
1163                switch (action) {
1164                    case MESSAGE_SHOW_VIEWPORT_FRAME: {
1165                        mViewport.setFrameShown(true, true);
1166                    } break;
1167                    case MESSAGE_RECOMPUTE_VIEWPORT_BOUNDS: {
1168                        final boolean animate = message.arg1 == 1;
1169                        mViewport.recomputeBounds(animate);
1170                    } break;
1171                    case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
1172                        SomeArgs args = (SomeArgs) message.obj;
1173                        try {
1174                            mTempRect.set(args.argi1, args.argi2, args.argi3, args.argi4);
1175                            final boolean immediate = (message.arg1 == 1);
1176                            handleOnRectangleOnScreenRequested(mTempRect, immediate);
1177                        } finally {
1178                            args.recycle();
1179                        }
1180                    } break;
1181                    case MESSAGE_ON_WINDOW_TRANSITION: {
1182                        final int transition = message.arg1;
1183                        WindowInfo info = (WindowInfo) message.obj;
1184                        handleOnWindowTransition(transition, info);
1185                    } break;
1186                    case MESSAGE_ON_ROTATION_CHANGED: {
1187                        final int rotation = message.arg1;
1188                        handleOnRotationChanged(rotation);
1189                    } break;
1190                    default: {
1191                        throw new IllegalArgumentException("Unknown message: " + action);
1192                    }
1193                }
1194            }
1195        }
1196    }
1197
1198    private final class MagnificationController {
1199
1200        private static final String PROPERTY_NAME_ACCESSIBILITY_TRANSFORMATION =
1201                "accessibilityTransformation";
1202
1203        private final MagnificationSpec mSentMagnificationSpec = new MagnificationSpec();
1204
1205        private final MagnificationSpec mCurrentMagnificationSpec = new MagnificationSpec();
1206
1207        private final Rect mTempRect = new Rect();
1208
1209        private final ValueAnimator mTransformationAnimator;
1210
1211        public MagnificationController(int animationDuration) {
1212            Property<MagnificationController, MagnificationSpec> property =
1213                    Property.of(MagnificationController.class, MagnificationSpec.class,
1214                    PROPERTY_NAME_ACCESSIBILITY_TRANSFORMATION);
1215            TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() {
1216                private final MagnificationSpec mTempTransformationSpec = new MagnificationSpec();
1217                @Override
1218                public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec,
1219                        MagnificationSpec toSpec) {
1220                    MagnificationSpec result = mTempTransformationSpec;
1221                    result.mScale = fromSpec.mScale
1222                            + (toSpec.mScale - fromSpec.mScale) * fraction;
1223                    result.mMagnifiedRegionCenterX = fromSpec.mMagnifiedRegionCenterX
1224                            + (toSpec.mMagnifiedRegionCenterX - fromSpec.mMagnifiedRegionCenterX)
1225                            * fraction;
1226                    result.mMagnifiedRegionCenterY = fromSpec.mMagnifiedRegionCenterY
1227                            + (toSpec.mMagnifiedRegionCenterY - fromSpec.mMagnifiedRegionCenterY)
1228                            * fraction;
1229                    result.mScaledOffsetX = fromSpec.mScaledOffsetX
1230                            + (toSpec.mScaledOffsetX - fromSpec.mScaledOffsetX)
1231                            * fraction;
1232                    result.mScaledOffsetY = fromSpec.mScaledOffsetY
1233                            + (toSpec.mScaledOffsetY - fromSpec.mScaledOffsetY)
1234                            * fraction;
1235                    return result;
1236                }
1237            };
1238            mTransformationAnimator = ObjectAnimator.ofObject(this, property,
1239                    evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec);
1240            mTransformationAnimator.setDuration((long) (animationDuration));
1241            mTransformationAnimator.setInterpolator(mInterpolator);
1242        }
1243
1244        public boolean isMagnifying() {
1245            return mCurrentMagnificationSpec.mScale > 1.0f;
1246        }
1247
1248        public void reset(boolean animate) {
1249            if (mTransformationAnimator.isRunning()) {
1250                mTransformationAnimator.cancel();
1251            }
1252            mCurrentMagnificationSpec.reset();
1253            if (animate) {
1254                animateAccessibilityTranformation(mSentMagnificationSpec,
1255                        mCurrentMagnificationSpec);
1256            } else {
1257                setAccessibilityTransformation(mCurrentMagnificationSpec);
1258            }
1259        }
1260
1261        public Rect getMagnifiedRegionBounds() {
1262            mTempRect.set(mViewport.getBounds());
1263            mTempRect.offset((int) -mCurrentMagnificationSpec.mScaledOffsetX,
1264                    (int) -mCurrentMagnificationSpec.mScaledOffsetY);
1265            mTempRect.scale(1.0f / mCurrentMagnificationSpec.mScale);
1266            return mTempRect;
1267        }
1268
1269        public float getScale() {
1270            return mCurrentMagnificationSpec.mScale;
1271        }
1272
1273        public float getMagnifiedRegionCenterX() {
1274            return mCurrentMagnificationSpec.mMagnifiedRegionCenterX;
1275        }
1276
1277        public float getMagnifiedRegionCenterY() {
1278            return mCurrentMagnificationSpec.mMagnifiedRegionCenterY;
1279        }
1280
1281        public float getScaledOffsetX() {
1282            return mCurrentMagnificationSpec.mScaledOffsetX;
1283        }
1284
1285        public float getScaledOffsetY() {
1286            return mCurrentMagnificationSpec.mScaledOffsetY;
1287        }
1288
1289        public void setScale(float scale, float pivotX, float pivotY, boolean animate) {
1290            MagnificationSpec spec = mCurrentMagnificationSpec;
1291            final float oldScale = spec.mScale;
1292            final float oldCenterX = spec.mMagnifiedRegionCenterX;
1293            final float oldCenterY = spec.mMagnifiedRegionCenterY;
1294            final float normPivotX = (-spec.mScaledOffsetX + pivotX) / oldScale;
1295            final float normPivotY = (-spec.mScaledOffsetY + pivotY) / oldScale;
1296            final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale);
1297            final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale);
1298            final float centerX = normPivotX + offsetX;
1299            final float centerY = normPivotY + offsetY;
1300            setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate);
1301        }
1302
1303        public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) {
1304            setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.mScale, centerX, centerY,
1305                    animate);
1306        }
1307
1308        public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY,
1309                boolean animate) {
1310            if (Float.compare(mCurrentMagnificationSpec.mScale, scale) == 0
1311                    && Float.compare(mCurrentMagnificationSpec.mMagnifiedRegionCenterX,
1312                            centerX) == 0
1313                    && Float.compare(mCurrentMagnificationSpec.mMagnifiedRegionCenterY,
1314                            centerY) == 0) {
1315                return;
1316            }
1317            if (mTransformationAnimator.isRunning()) {
1318                mTransformationAnimator.cancel();
1319            }
1320            if (DEBUG_MAGNIFICATION_CONTROLLER) {
1321                Slog.i(LOG_TAG, "scale: " + scale + " centerX: " + centerX
1322                        + " centerY: " + centerY);
1323            }
1324            mCurrentMagnificationSpec.initialize(scale, centerX, centerY);
1325            if (animate) {
1326                animateAccessibilityTranformation(mSentMagnificationSpec,
1327                        mCurrentMagnificationSpec);
1328            } else {
1329                setAccessibilityTransformation(mCurrentMagnificationSpec);
1330            }
1331        }
1332
1333        private void animateAccessibilityTranformation(MagnificationSpec fromSpec,
1334                MagnificationSpec toSpec) {
1335            mTransformationAnimator.setObjectValues(fromSpec, toSpec);
1336            mTransformationAnimator.start();
1337        }
1338
1339        @SuppressWarnings("unused")
1340        // Called from an animator.
1341        public MagnificationSpec getAccessibilityTransformation() {
1342            return mSentMagnificationSpec;
1343        }
1344
1345        public void setAccessibilityTransformation(MagnificationSpec transformation) {
1346            if (DEBUG_TRANSFORMATION) {
1347                Slog.i(LOG_TAG, "Transformation scale: " + transformation.mScale
1348                        + " offsetX: " + transformation.mScaledOffsetX
1349                        + " offsetY: " + transformation.mScaledOffsetY);
1350            }
1351            try {
1352                mSentMagnificationSpec.updateFrom(transformation);
1353                mWindowManagerService.magnifyDisplay(mDisplayProvider.getDisplay().getDisplayId(),
1354                        transformation.mScale, transformation.mScaledOffsetX,
1355                        transformation.mScaledOffsetY);
1356            } catch (RemoteException re) {
1357                /* ignore */
1358            }
1359        }
1360
1361        private class MagnificationSpec {
1362
1363            private static final float DEFAULT_SCALE = 1.0f;
1364
1365            public float mScale = DEFAULT_SCALE;
1366
1367            public float mMagnifiedRegionCenterX;
1368
1369            public float mMagnifiedRegionCenterY;
1370
1371            public float mScaledOffsetX;
1372
1373            public float mScaledOffsetY;
1374
1375            public void initialize(float scale, float magnifiedRegionCenterX,
1376                    float magnifiedRegionCenterY) {
1377                mScale = scale;
1378
1379                final int viewportWidth = mViewport.getBounds().width();
1380                final int viewportHeight = mViewport.getBounds().height();
1381                final float minMagnifiedRegionCenterX = (viewportWidth / 2) / scale;
1382                final float minMagnifiedRegionCenterY = (viewportHeight / 2) / scale;
1383                final float maxMagnifiedRegionCenterX = viewportWidth - minMagnifiedRegionCenterX;
1384                final float maxMagnifiedRegionCenterY = viewportHeight - minMagnifiedRegionCenterY;
1385
1386                mMagnifiedRegionCenterX = Math.min(Math.max(magnifiedRegionCenterX,
1387                        minMagnifiedRegionCenterX), maxMagnifiedRegionCenterX);
1388                mMagnifiedRegionCenterY = Math.min(Math.max(magnifiedRegionCenterY,
1389                        minMagnifiedRegionCenterY), maxMagnifiedRegionCenterY);
1390
1391                mScaledOffsetX = -(mMagnifiedRegionCenterX * scale - viewportWidth / 2);
1392                mScaledOffsetY = -(mMagnifiedRegionCenterY * scale - viewportHeight / 2);
1393            }
1394
1395            public void updateFrom(MagnificationSpec other) {
1396                mScale = other.mScale;
1397                mMagnifiedRegionCenterX = other.mMagnifiedRegionCenterX;
1398                mMagnifiedRegionCenterY = other.mMagnifiedRegionCenterY;
1399                mScaledOffsetX = other.mScaledOffsetX;
1400                mScaledOffsetY = other.mScaledOffsetY;
1401            }
1402
1403            public void reset() {
1404                mScale = DEFAULT_SCALE;
1405                mMagnifiedRegionCenterX = 0;
1406                mMagnifiedRegionCenterY = 0;
1407                mScaledOffsetX = 0;
1408                mScaledOffsetY = 0;
1409            }
1410        }
1411    }
1412
1413    private static final class Viewport {
1414
1415        private static final String PROPERTY_NAME_ALPHA = "alpha";
1416
1417        private static final String PROPERTY_NAME_BOUNDS = "bounds";
1418
1419        private static final int MIN_ALPHA = 0;
1420
1421        private static final int MAX_ALPHA = 255;
1422
1423        private final ArrayList<WindowInfo> mTempWindowInfoList = new ArrayList<WindowInfo>();
1424
1425        private final Rect mTempRect = new Rect();
1426
1427        private final IWindowManager mWindowManagerService;
1428        private final DisplayProvider mDisplayProvider;
1429
1430        private final ViewportWindow mViewportFrame;
1431
1432        private final ValueAnimator mResizeFrameAnimator;
1433
1434        private final ValueAnimator mShowHideFrameAnimator;
1435
1436        public Viewport(Context context, WindowManager windowManager,
1437                IWindowManager windowManagerService, DisplayProvider displayInfoProvider,
1438                Interpolator animationInterpolator, long animationDuration) {
1439            mWindowManagerService = windowManagerService;
1440            mDisplayProvider = displayInfoProvider;
1441            mViewportFrame = new ViewportWindow(context, windowManager, displayInfoProvider);
1442
1443            mShowHideFrameAnimator = ObjectAnimator.ofInt(mViewportFrame, PROPERTY_NAME_ALPHA,
1444                  MIN_ALPHA, MAX_ALPHA);
1445            mShowHideFrameAnimator.setInterpolator(animationInterpolator);
1446            mShowHideFrameAnimator.setDuration(animationDuration);
1447            mShowHideFrameAnimator.addListener(new AnimatorListener() {
1448                @Override
1449                public void onAnimationEnd(Animator animation) {
1450                    if (mShowHideFrameAnimator.getAnimatedValue().equals(MIN_ALPHA)) {
1451                        mViewportFrame.hide();
1452                    }
1453                }
1454                @Override
1455                public void onAnimationStart(Animator animation) {
1456                    /* do nothing - stub */
1457                }
1458                @Override
1459                public void onAnimationCancel(Animator animation) {
1460                    /* do nothing - stub */
1461                }
1462                @Override
1463                public void onAnimationRepeat(Animator animation) {
1464                    /* do nothing - stub */
1465                }
1466            });
1467
1468            Property<ViewportWindow, Rect> property = Property.of(ViewportWindow.class,
1469                    Rect.class, PROPERTY_NAME_BOUNDS);
1470            TypeEvaluator<Rect> evaluator = new TypeEvaluator<Rect>() {
1471                private final Rect mReusableResultRect = new Rect();
1472                @Override
1473                public Rect evaluate(float fraction, Rect fromFrame, Rect toFrame) {
1474                    Rect result = mReusableResultRect;
1475                    result.left = (int) (fromFrame.left
1476                            + (toFrame.left - fromFrame.left) * fraction);
1477                    result.top = (int) (fromFrame.top
1478                            + (toFrame.top - fromFrame.top) * fraction);
1479                    result.right = (int) (fromFrame.right
1480                            + (toFrame.right - fromFrame.right) * fraction);
1481                    result.bottom = (int) (fromFrame.bottom
1482                            + (toFrame.bottom - fromFrame.bottom) * fraction);
1483                    return result;
1484                }
1485            };
1486            mResizeFrameAnimator = ObjectAnimator.ofObject(mViewportFrame, property,
1487                    evaluator, mViewportFrame.mBounds, mViewportFrame.mBounds);
1488            mResizeFrameAnimator.setDuration((long) (animationDuration));
1489            mResizeFrameAnimator.setInterpolator(animationInterpolator);
1490
1491            recomputeBounds(false);
1492        }
1493
1494        public void recomputeBounds(boolean animate) {
1495            Rect frame = mTempRect;
1496            frame.set(0, 0, mDisplayProvider.getDisplayInfo().logicalWidth,
1497                    mDisplayProvider.getDisplayInfo().logicalHeight);
1498            ArrayList<WindowInfo> infos = mTempWindowInfoList;
1499            infos.clear();
1500            try {
1501                mWindowManagerService.getVisibleWindowsForDisplay(
1502                        mDisplayProvider.getDisplay().getDisplayId(), infos);
1503                final int windowCount = infos.size();
1504                for (int i = 0; i < windowCount; i++) {
1505                    WindowInfo info = infos.get(i);
1506                    if (info.type == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
1507                            || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD
1508                            || info.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG) {
1509                        subtract(frame, info.touchableRegion);
1510                    }
1511                    info.recycle();
1512                }
1513            } catch (RemoteException re) {
1514                /* ignore */
1515            } finally {
1516                infos.clear();
1517            }
1518            resize(frame, animate);
1519        }
1520
1521        public void rotationChanged() {
1522            mViewportFrame.rotationChanged();
1523        }
1524
1525        public Rect getBounds() {
1526            return mViewportFrame.getBounds();
1527        }
1528
1529        public void setFrameShown(boolean shown, boolean animate) {
1530            if (mViewportFrame.isShown() == shown) {
1531                return;
1532            }
1533            if (animate) {
1534                if (mShowHideFrameAnimator.isRunning()) {
1535                    mShowHideFrameAnimator.reverse();
1536                } else {
1537                    if (shown) {
1538                        mViewportFrame.show();
1539                        mShowHideFrameAnimator.start();
1540                    } else {
1541                        mShowHideFrameAnimator.reverse();
1542                    }
1543                }
1544            } else {
1545                mShowHideFrameAnimator.cancel();
1546                if (shown) {
1547                    mViewportFrame.show();
1548                } else {
1549                    mViewportFrame.hide();
1550                }
1551            }
1552        }
1553
1554        private void resize(Rect bounds, boolean animate) {
1555            if (mViewportFrame.getBounds().equals(bounds)) {
1556                return;
1557            }
1558            if (animate) {
1559                if (mResizeFrameAnimator.isRunning()) {
1560                    mResizeFrameAnimator.cancel();
1561                }
1562                mResizeFrameAnimator.setObjectValues(mViewportFrame.mBounds, bounds);
1563                mResizeFrameAnimator.start();
1564            } else {
1565                mViewportFrame.setBounds(bounds);
1566            }
1567        }
1568
1569        private boolean subtract(Rect lhs, Rect rhs) {
1570            if (lhs.right < rhs.left || lhs.left  > rhs.right
1571                    || lhs.bottom < rhs.top || lhs.top > rhs.bottom) {
1572                return false;
1573            }
1574            if (lhs.left < rhs.left) {
1575                lhs.right = rhs.left;
1576            }
1577            if (lhs.top < rhs.top) {
1578                lhs.bottom = rhs.top;
1579            }
1580            if (lhs.right > rhs.right) {
1581                lhs.left = rhs.right;
1582            }
1583            if (lhs.bottom > rhs.bottom) {
1584                lhs.top = rhs.bottom;
1585            }
1586            return true;
1587        }
1588
1589        private static final class ViewportWindow {
1590            private static final String WINDOW_TITLE = "Magnification Overlay";
1591
1592            private final WindowManager mWindowManager;
1593            private final DisplayProvider mDisplayProvider;
1594
1595            private final ContentView mWindowContent;
1596            private final WindowManager.LayoutParams mWindowParams;
1597
1598            private final Rect mBounds = new Rect();
1599            private boolean mShown;
1600            private int mAlpha;
1601
1602            public ViewportWindow(Context context, WindowManager windowManager,
1603                    DisplayProvider displayProvider) {
1604                mWindowManager = windowManager;
1605                mDisplayProvider = displayProvider;
1606
1607                ViewGroup.LayoutParams contentParams = new ViewGroup.LayoutParams(
1608                        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
1609                mWindowContent = new ContentView(context);
1610                mWindowContent.setLayoutParams(contentParams);
1611                mWindowContent.setBackgroundColor(R.color.transparent);
1612
1613                mWindowParams = new WindowManager.LayoutParams(
1614                        WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY);
1615                mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
1616                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
1617                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
1618                mWindowParams.setTitle(WINDOW_TITLE);
1619                mWindowParams.gravity = Gravity.CENTER;
1620                mWindowParams.width = displayProvider.getDisplayInfo().logicalWidth;
1621                mWindowParams.height = displayProvider.getDisplayInfo().logicalHeight;
1622                mWindowParams.format = PixelFormat.TRANSLUCENT;
1623            }
1624
1625            public boolean isShown() {
1626                return mShown;
1627            }
1628
1629            public void show() {
1630                if (mShown) {
1631                    return;
1632                }
1633                mShown = true;
1634                mWindowManager.addView(mWindowContent, mWindowParams);
1635                if (DEBUG_VIEWPORT_WINDOW) {
1636                    Slog.i(LOG_TAG, "ViewportWindow shown.");
1637                }
1638            }
1639
1640            public void hide() {
1641                if (!mShown) {
1642                    return;
1643                }
1644                mShown = false;
1645                mWindowManager.removeView(mWindowContent);
1646                if (DEBUG_VIEWPORT_WINDOW) {
1647                    Slog.i(LOG_TAG, "ViewportWindow hidden.");
1648                }
1649            }
1650
1651            @SuppressWarnings("unused")
1652            // Called reflectively from an animator.
1653            public int getAlpha() {
1654                return mAlpha;
1655            }
1656
1657            @SuppressWarnings("unused")
1658            // Called reflectively from an animator.
1659            public void setAlpha(int alpha) {
1660                if (mAlpha == alpha) {
1661                    return;
1662                }
1663                mAlpha = alpha;
1664                if (mShown) {
1665                    mWindowContent.invalidate();
1666                }
1667                if (DEBUG_VIEWPORT_WINDOW) {
1668                    Slog.i(LOG_TAG, "ViewportFrame set alpha: " + alpha);
1669                }
1670            }
1671
1672            public Rect getBounds() {
1673                return mBounds;
1674            }
1675
1676            public void rotationChanged() {
1677                mWindowParams.width = mDisplayProvider.getDisplayInfo().logicalWidth;
1678                mWindowParams.height = mDisplayProvider.getDisplayInfo().logicalHeight;
1679                if (mShown) {
1680                    mWindowManager.updateViewLayout(mWindowContent, mWindowParams);
1681                }
1682            }
1683
1684            public void setBounds(Rect bounds) {
1685                if (mBounds.equals(bounds)) {
1686                    return;
1687                }
1688                mBounds.set(bounds);
1689                if (mShown) {
1690                    mWindowContent.invalidate();
1691                }
1692                if (DEBUG_VIEWPORT_WINDOW) {
1693                    Slog.i(LOG_TAG, "ViewportFrame set bounds: " + bounds);
1694                }
1695            }
1696
1697            private final class ContentView extends View {
1698                private final Drawable mHighlightFrame;
1699
1700                public ContentView(Context context) {
1701                    super(context);
1702                    mHighlightFrame = context.getResources().getDrawable(
1703                            R.drawable.magnified_region_frame);
1704                }
1705
1706                @Override
1707                public void onDraw(Canvas canvas) {
1708                    canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
1709                    mHighlightFrame.setBounds(mBounds);
1710                    mHighlightFrame.setAlpha(mAlpha);
1711                    mHighlightFrame.draw(canvas);
1712                }
1713            }
1714        }
1715    }
1716
1717    private static class DisplayProvider implements DisplayListener {
1718        private final WindowManager mWindowManager;
1719        private final DisplayManager mDisplayManager;
1720        private final Display mDefaultDisplay;
1721        private final DisplayInfo mDefaultDisplayInfo = new DisplayInfo();
1722
1723        public DisplayProvider(Context context, WindowManager windowManager) {
1724            mWindowManager = windowManager;
1725            mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
1726            mDefaultDisplay = mWindowManager.getDefaultDisplay();
1727            mDisplayManager.registerDisplayListener(this, null);
1728            updateDisplayInfo();
1729        }
1730
1731        public DisplayInfo getDisplayInfo() {
1732            return mDefaultDisplayInfo;
1733        }
1734
1735        public Display getDisplay() {
1736            return mDefaultDisplay;
1737        }
1738
1739        private void updateDisplayInfo() {
1740            if (!mDefaultDisplay.getDisplayInfo(mDefaultDisplayInfo)) {
1741                Slog.e(LOG_TAG, "Default display is not valid.");
1742            }
1743        }
1744
1745        public void destroy() {
1746            mDisplayManager.unregisterDisplayListener(this);
1747        }
1748
1749        @Override
1750        public void onDisplayAdded(int displayId) {
1751            /* do noting */
1752        }
1753
1754        @Override
1755        public void onDisplayRemoved(int displayId) {
1756            // Having no default display
1757        }
1758
1759        @Override
1760        public void onDisplayChanged(int displayId) {
1761            updateDisplayInfo();
1762        }
1763    }
1764}
1765