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