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