Home | History | Annotate | Download | only in accessibility
      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 
     17 package com.android.server.accessibility;
     18 
     19 import android.animation.ObjectAnimator;
     20 import android.animation.TypeEvaluator;
     21 import android.animation.ValueAnimator;
     22 import android.content.BroadcastReceiver;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.IntentFilter;
     26 import android.graphics.Rect;
     27 import android.graphics.Region;
     28 import android.os.AsyncTask;
     29 import android.os.Binder;
     30 import android.os.Handler;
     31 import android.os.Message;
     32 import android.os.RemoteException;
     33 import android.os.ServiceManager;
     34 import android.os.SystemClock;
     35 import android.provider.Settings;
     36 import android.text.TextUtils;
     37 import android.util.Property;
     38 import android.util.Slog;
     39 import android.view.GestureDetector;
     40 import android.view.GestureDetector.SimpleOnGestureListener;
     41 import android.view.IMagnificationCallbacks;
     42 import android.view.IWindowManager;
     43 import android.view.MagnificationSpec;
     44 import android.view.MotionEvent;
     45 import android.view.MotionEvent.PointerCoords;
     46 import android.view.MotionEvent.PointerProperties;
     47 import android.view.ScaleGestureDetector;
     48 import android.view.ScaleGestureDetector.OnScaleGestureListener;
     49 import android.view.View;
     50 import android.view.ViewConfiguration;
     51 import android.view.accessibility.AccessibilityEvent;
     52 import android.view.animation.DecelerateInterpolator;
     53 
     54 import com.android.internal.os.SomeArgs;
     55 
     56 import java.util.Locale;
     57 
     58 /**
     59  * This class handles the screen magnification when accessibility is enabled.
     60  * The behavior is as follows:
     61  *
     62  * 1. Triple tap toggles permanent screen magnification which is magnifying
     63  *    the area around the location of the triple tap. One can think of the
     64  *    location of the triple tap as the center of the magnified viewport.
     65  *    For example, a triple tap when not magnified would magnify the screen
     66  *    and leave it in a magnified state. A triple tapping when magnified would
     67  *    clear magnification and leave the screen in a not magnified state.
     68  *
     69  * 2. Triple tap and hold would magnify the screen if not magnified and enable
     70  *    viewport dragging mode until the finger goes up. One can think of this
     71  *    mode as a way to move the magnified viewport since the area around the
     72  *    moving finger will be magnified to fit the screen. For example, if the
     73  *    screen was not magnified and the user triple taps and holds the screen
     74  *    would magnify and the viewport will follow the user's finger. When the
     75  *    finger goes up the screen will zoom out. If the same user interaction
     76  *    is performed when the screen is magnified, the viewport movement will
     77  *    be the same but when the finger goes up the screen will stay magnified.
     78  *    In other words, the initial magnified state is sticky.
     79  *
     80  * 3. Pinching with any number of additional fingers when viewport dragging
     81  *    is enabled, i.e. the user triple tapped and holds, would adjust the
     82  *    magnification scale which will become the current default magnification
     83  *    scale. The next time the user magnifies the same magnification scale
     84  *    would be used.
     85  *
     86  * 4. When in a permanent magnified state the user can use two or more fingers
     87  *    to pan the viewport. Note that in this mode the content is panned as
     88  *    opposed to the viewport dragging mode in which the viewport is moved.
     89  *
     90  * 5. When in a permanent magnified state the user can use two or more
     91  *    fingers to change the magnification scale which will become the current
     92  *    default magnification scale. The next time the user magnifies the same
     93  *    magnification scale would be used.
     94  *
     95  * 6. The magnification scale will be persisted in settings and in the cloud.
     96  */
     97 public final class ScreenMagnifier extends IMagnificationCallbacks.Stub
     98         implements EventStreamTransformation {
     99 
    100     private static final String LOG_TAG = ScreenMagnifier.class.getSimpleName();
    101 
    102     private static final boolean DEBUG_STATE_TRANSITIONS = false;
    103     private static final boolean DEBUG_DETECTING = false;
    104     private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;
    105     private static final boolean DEBUG_PANNING = false;
    106     private static final boolean DEBUG_SCALING = false;
    107     private static final boolean DEBUG_MAGNIFICATION_CONTROLLER = false;
    108 
    109     private static final int STATE_DELEGATING = 1;
    110     private static final int STATE_DETECTING = 2;
    111     private static final int STATE_VIEWPORT_DRAGGING = 3;
    112     private static final int STATE_MAGNIFIED_INTERACTION = 4;
    113 
    114     private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;
    115     private static final int MULTI_TAP_TIME_SLOP_ADJUSTMENT = 50;
    116 
    117     private static final int MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED = 1;
    118     private static final int MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED = 2;
    119     private static final int MESSAGE_ON_USER_CONTEXT_CHANGED = 3;
    120     private static final int MESSAGE_ON_ROTATION_CHANGED = 4;
    121 
    122     private static final int DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE = 1;
    123 
    124     private static final int MY_PID = android.os.Process.myPid();
    125 
    126     private final Rect mTempRect = new Rect();
    127     private final Rect mTempRect1 = new Rect();
    128 
    129     private final Context mContext;
    130     private final IWindowManager mWindowManager;
    131     private final MagnificationController mMagnificationController;
    132     private final ScreenStateObserver mScreenStateObserver;
    133 
    134     private final DetectingStateHandler mDetectingStateHandler;
    135     private final MagnifiedContentInteractonStateHandler mMagnifiedContentInteractonStateHandler;
    136     private final StateViewportDraggingHandler mStateViewportDraggingHandler;
    137 
    138     private final AccessibilityManagerService mAms;
    139 
    140     private final int mTapTimeSlop = ViewConfiguration.getTapTimeout();
    141     private final int mMultiTapTimeSlop =
    142             ViewConfiguration.getDoubleTapTimeout() - MULTI_TAP_TIME_SLOP_ADJUSTMENT;
    143     private final int mTapDistanceSlop;
    144     private final int mMultiTapDistanceSlop;
    145 
    146     private final long mLongAnimationDuration;
    147 
    148     private final Region mMagnifiedBounds = new Region();
    149 
    150     private EventStreamTransformation mNext;
    151 
    152     private int mCurrentState;
    153     private int mPreviousState;
    154     private boolean mTranslationEnabledBeforePan;
    155 
    156     private PointerCoords[] mTempPointerCoords;
    157     private PointerProperties[] mTempPointerProperties;
    158 
    159     private long mDelegatingStateDownTime;
    160 
    161     private boolean mUpdateMagnificationSpecOnNextBoundsChange;
    162 
    163     private final Handler mHandler = new Handler() {
    164         @Override
    165         public void handleMessage(Message message) {
    166             switch (message.what) {
    167                 case MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED: {
    168                     Region bounds = (Region) message.obj;
    169                     handleOnMagnifiedBoundsChanged(bounds);
    170                     bounds.recycle();
    171                 } break;
    172                 case MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
    173                     SomeArgs args = (SomeArgs) message.obj;
    174                     final int left = args.argi1;
    175                     final int top = args.argi2;
    176                     final int right = args.argi3;
    177                     final int bottom = args.argi4;
    178                     handleOnRectangleOnScreenRequested(left, top, right, bottom);
    179                     args.recycle();
    180                 } break;
    181                 case MESSAGE_ON_USER_CONTEXT_CHANGED: {
    182                     handleOnUserContextChanged();
    183                 } break;
    184                 case MESSAGE_ON_ROTATION_CHANGED: {
    185                     final int rotation = message.arg1;
    186                     handleOnRotationChanged(rotation);
    187                 } break;
    188             }
    189         }
    190     };
    191 
    192     public ScreenMagnifier(Context context, int displayId, AccessibilityManagerService service) {
    193         mContext = context;
    194         mWindowManager = IWindowManager.Stub.asInterface(
    195                 ServiceManager.getService("window"));
    196         mAms = service;
    197 
    198         mLongAnimationDuration = context.getResources().getInteger(
    199                 com.android.internal.R.integer.config_longAnimTime);
    200         mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    201         mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
    202 
    203         mDetectingStateHandler = new DetectingStateHandler();
    204         mStateViewportDraggingHandler = new StateViewportDraggingHandler();
    205         mMagnifiedContentInteractonStateHandler = new MagnifiedContentInteractonStateHandler(
    206                 context);
    207 
    208         mMagnificationController = new MagnificationController(mLongAnimationDuration);
    209         mScreenStateObserver = new ScreenStateObserver(context, mMagnificationController);
    210 
    211         try {
    212             mWindowManager.setMagnificationCallbacks(this);
    213         } catch (RemoteException re) {
    214             /* ignore */
    215         }
    216 
    217         transitionToState(STATE_DETECTING);
    218     }
    219 
    220     @Override
    221     public void onMagnifedBoundsChanged(Region bounds) {
    222         Region newBounds = Region.obtain(bounds);
    223         mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, newBounds).sendToTarget();
    224         if (MY_PID != Binder.getCallingPid()) {
    225             bounds.recycle();
    226         }
    227     }
    228 
    229     private void handleOnMagnifiedBoundsChanged(Region bounds) {
    230         // If there was a rotation we have to update the center of the magnified
    231         // region since the old offset X/Y may be out of its acceptable range for
    232         // the new display width and height.
    233         if (mUpdateMagnificationSpecOnNextBoundsChange) {
    234             mUpdateMagnificationSpecOnNextBoundsChange = false;
    235             MagnificationSpec spec = mMagnificationController.getMagnificationSpec();
    236             Rect magnifiedFrame = mTempRect;
    237             mMagnifiedBounds.getBounds(magnifiedFrame);
    238             final float scale = spec.scale;
    239             final float centerX = (-spec.offsetX + magnifiedFrame.width() / 2) / scale;
    240             final float centerY = (-spec.offsetY + magnifiedFrame.height() / 2) / scale;
    241             mMagnificationController.setScaleAndMagnifiedRegionCenter(scale, centerX,
    242                     centerY, false);
    243         }
    244         mMagnifiedBounds.set(bounds);
    245         mAms.onMagnificationStateChanged();
    246     }
    247 
    248     @Override
    249     public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) {
    250         SomeArgs args = SomeArgs.obtain();
    251         args.argi1 = left;
    252         args.argi2 = top;
    253         args.argi3 = right;
    254         args.argi4 = bottom;
    255         mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, args).sendToTarget();
    256     }
    257 
    258     private void handleOnRectangleOnScreenRequested(int left, int top, int right, int bottom) {
    259         Rect magnifiedFrame = mTempRect;
    260         mMagnifiedBounds.getBounds(magnifiedFrame);
    261         if (!magnifiedFrame.intersects(left, top, right, bottom)) {
    262             return;
    263         }
    264         Rect magnifFrameInScreenCoords = mTempRect1;
    265         getMagnifiedFrameInContentCoords(magnifFrameInScreenCoords);
    266         final float scrollX;
    267         final float scrollY;
    268         if (right - left > magnifFrameInScreenCoords.width()) {
    269             final int direction = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault());
    270             if (direction == View.LAYOUT_DIRECTION_LTR) {
    271                 scrollX = left - magnifFrameInScreenCoords.left;
    272             } else {
    273                 scrollX = right - magnifFrameInScreenCoords.right;
    274             }
    275         } else if (left < magnifFrameInScreenCoords.left) {
    276             scrollX = left - magnifFrameInScreenCoords.left;
    277         } else if (right > magnifFrameInScreenCoords.right) {
    278             scrollX = right - magnifFrameInScreenCoords.right;
    279         } else {
    280             scrollX = 0;
    281         }
    282         if (bottom - top > magnifFrameInScreenCoords.height()) {
    283             scrollY = top - magnifFrameInScreenCoords.top;
    284         } else if (top < magnifFrameInScreenCoords.top) {
    285             scrollY = top - magnifFrameInScreenCoords.top;
    286         } else if (bottom > magnifFrameInScreenCoords.bottom) {
    287             scrollY = bottom - magnifFrameInScreenCoords.bottom;
    288         } else {
    289             scrollY = 0;
    290         }
    291         final float scale = mMagnificationController.getScale();
    292         mMagnificationController.offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale);
    293     }
    294 
    295     @Override
    296     public void onRotationChanged(int rotation) {
    297         mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0).sendToTarget();
    298     }
    299 
    300     private void handleOnRotationChanged(int rotation) {
    301         resetMagnificationIfNeeded();
    302         if (mMagnificationController.isMagnifying()) {
    303             mUpdateMagnificationSpecOnNextBoundsChange = true;
    304         }
    305     }
    306 
    307     @Override
    308     public void onUserContextChanged() {
    309         mHandler.sendEmptyMessage(MESSAGE_ON_USER_CONTEXT_CHANGED);
    310     }
    311 
    312     private void handleOnUserContextChanged() {
    313         resetMagnificationIfNeeded();
    314     }
    315 
    316     private void getMagnifiedFrameInContentCoords(Rect rect) {
    317         MagnificationSpec spec = mMagnificationController.getMagnificationSpec();
    318         mMagnifiedBounds.getBounds(rect);
    319         rect.offset((int) -spec.offsetX, (int) -spec.offsetY);
    320         rect.scale(1.0f / spec.scale);
    321     }
    322 
    323     private void resetMagnificationIfNeeded() {
    324         if (mMagnificationController.isMagnifying()
    325                 && isScreenMagnificationAutoUpdateEnabled(mContext)) {
    326             mMagnificationController.reset(true);
    327         }
    328     }
    329 
    330     @Override
    331     public void onMotionEvent(MotionEvent event, MotionEvent rawEvent,
    332             int policyFlags) {
    333         mMagnifiedContentInteractonStateHandler.onMotionEvent(event);
    334         switch (mCurrentState) {
    335             case STATE_DELEGATING: {
    336                 handleMotionEventStateDelegating(event, rawEvent, policyFlags);
    337             } break;
    338             case STATE_DETECTING: {
    339                 mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags);
    340             } break;
    341             case STATE_VIEWPORT_DRAGGING: {
    342                 mStateViewportDraggingHandler.onMotionEvent(event, policyFlags);
    343             } break;
    344             case STATE_MAGNIFIED_INTERACTION: {
    345                 // mMagnifiedContentInteractonStateHandler handles events only
    346                 // if this is the current state since it uses ScaleGestureDetecotr
    347                 // and a GestureDetector which need well formed event stream.
    348             } break;
    349             default: {
    350                 throw new IllegalStateException("Unknown state: " + mCurrentState);
    351             }
    352         }
    353     }
    354 
    355     @Override
    356     public void onAccessibilityEvent(AccessibilityEvent event) {
    357         if (mNext != null) {
    358             mNext.onAccessibilityEvent(event);
    359         }
    360     }
    361 
    362     @Override
    363     public void setNext(EventStreamTransformation next) {
    364         mNext = next;
    365     }
    366 
    367     @Override
    368     public void clear() {
    369         mCurrentState = STATE_DETECTING;
    370         mDetectingStateHandler.clear();
    371         mStateViewportDraggingHandler.clear();
    372         mMagnifiedContentInteractonStateHandler.clear();
    373         if (mNext != null) {
    374             mNext.clear();
    375         }
    376     }
    377 
    378     @Override
    379     public void onDestroy() {
    380         mScreenStateObserver.destroy();
    381         try {
    382             mWindowManager.setMagnificationCallbacks(null);
    383         } catch (RemoteException re) {
    384             /* ignore */
    385         }
    386     }
    387 
    388     private void handleMotionEventStateDelegating(MotionEvent event,
    389             MotionEvent rawEvent, int policyFlags) {
    390         switch (event.getActionMasked()) {
    391             case MotionEvent.ACTION_DOWN: {
    392                 mDelegatingStateDownTime = event.getDownTime();
    393             } break;
    394             case MotionEvent.ACTION_UP: {
    395                 if (mDetectingStateHandler.mDelayedEventQueue == null) {
    396                     transitionToState(STATE_DETECTING);
    397                 }
    398             } break;
    399         }
    400         if (mNext != null) {
    401             // If the event is within the magnified portion of the screen we have
    402             // to change its location to be where the user thinks he is poking the
    403             // UI which may have been magnified and panned.
    404             final float eventX = event.getX();
    405             final float eventY = event.getY();
    406             if (mMagnificationController.isMagnifying()
    407                     && mMagnifiedBounds.contains((int) eventX, (int) eventY)) {
    408                 final float scale = mMagnificationController.getScale();
    409                 final float scaledOffsetX = mMagnificationController.getOffsetX();
    410                 final float scaledOffsetY = mMagnificationController.getOffsetY();
    411                 final int pointerCount = event.getPointerCount();
    412                 PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
    413                 PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
    414                 for (int i = 0; i < pointerCount; i++) {
    415                     event.getPointerCoords(i, coords[i]);
    416                     coords[i].x = (coords[i].x - scaledOffsetX) / scale;
    417                     coords[i].y = (coords[i].y - scaledOffsetY) / scale;
    418                     event.getPointerProperties(i, properties[i]);
    419                 }
    420                 event = MotionEvent.obtain(event.getDownTime(),
    421                         event.getEventTime(), event.getAction(), pointerCount, properties,
    422                         coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(),
    423                         event.getFlags());
    424             }
    425             // We cache some events to see if the user wants to trigger magnification.
    426             // If no magnification is triggered we inject these events with adjusted
    427             // time and down time to prevent subsequent transformations being confused
    428             // by stale events. After the cached events, which always have a down, are
    429             // injected we need to also update the down time of all subsequent non cached
    430             // events. All delegated events cached and non-cached are delivered here.
    431             event.setDownTime(mDelegatingStateDownTime);
    432             mNext.onMotionEvent(event, rawEvent, policyFlags);
    433         }
    434     }
    435 
    436     private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
    437         final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
    438         if (oldSize < size) {
    439             PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
    440             mTempPointerCoords = new PointerCoords[size];
    441             if (oldTempPointerCoords != null) {
    442                 System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
    443             }
    444         }
    445         for (int i = oldSize; i < size; i++) {
    446             mTempPointerCoords[i] = new PointerCoords();
    447         }
    448         return mTempPointerCoords;
    449     }
    450 
    451     private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) {
    452         final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0;
    453         if (oldSize < size) {
    454             PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
    455             mTempPointerProperties = new PointerProperties[size];
    456             if (oldTempPointerProperties != null) {
    457                 System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize);
    458             }
    459         }
    460         for (int i = oldSize; i < size; i++) {
    461             mTempPointerProperties[i] = new PointerProperties();
    462         }
    463         return mTempPointerProperties;
    464     }
    465 
    466     private void transitionToState(int state) {
    467         if (DEBUG_STATE_TRANSITIONS) {
    468             switch (state) {
    469                 case STATE_DELEGATING: {
    470                     Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING");
    471                 } break;
    472                 case STATE_DETECTING: {
    473                     Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING");
    474                 } break;
    475                 case STATE_VIEWPORT_DRAGGING: {
    476                     Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING");
    477                 } break;
    478                 case STATE_MAGNIFIED_INTERACTION: {
    479                     Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION");
    480                 } break;
    481                 default: {
    482                     throw new IllegalArgumentException("Unknown state: " + state);
    483                 }
    484             }
    485         }
    486         mPreviousState = mCurrentState;
    487         mCurrentState = state;
    488     }
    489 
    490     private final class MagnifiedContentInteractonStateHandler
    491             extends SimpleOnGestureListener implements OnScaleGestureListener {
    492         private static final float MIN_SCALE = 1.3f;
    493         private static final float MAX_SCALE = 5.0f;
    494 
    495         private static final float SCALING_THRESHOLD = 0.3f;
    496 
    497         private final ScaleGestureDetector mScaleGestureDetector;
    498         private final GestureDetector mGestureDetector;
    499 
    500         private float mInitialScaleFactor = -1;
    501         private boolean mScaling;
    502 
    503         public MagnifiedContentInteractonStateHandler(Context context) {
    504             mScaleGestureDetector = new ScaleGestureDetector(context, this);
    505             mScaleGestureDetector.setQuickScaleEnabled(false);
    506             mGestureDetector = new GestureDetector(context, this);
    507         }
    508 
    509         public void onMotionEvent(MotionEvent event) {
    510             mScaleGestureDetector.onTouchEvent(event);
    511             mGestureDetector.onTouchEvent(event);
    512             if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
    513                 return;
    514             }
    515             if (event.getActionMasked() == MotionEvent.ACTION_UP) {
    516                 clear();
    517                 final float scale = Math.min(Math.max(mMagnificationController.getScale(),
    518                         MIN_SCALE), MAX_SCALE);
    519                 if (scale != getPersistedScale()) {
    520                     persistScale(scale);
    521                 }
    522                 if (mPreviousState == STATE_VIEWPORT_DRAGGING) {
    523                     transitionToState(STATE_VIEWPORT_DRAGGING);
    524                 } else {
    525                     transitionToState(STATE_DETECTING);
    526                 }
    527             }
    528         }
    529 
    530         @Override
    531         public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX,
    532                 float distanceY) {
    533             if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
    534                 return true;
    535             }
    536             if (DEBUG_PANNING) {
    537                 Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX
    538                         + " scrollY: " + distanceY);
    539             }
    540             mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY);
    541             return true;
    542         }
    543 
    544         @Override
    545         public boolean onScale(ScaleGestureDetector detector) {
    546             if (!mScaling) {
    547                 if (mInitialScaleFactor < 0) {
    548                     mInitialScaleFactor = detector.getScaleFactor();
    549                 } else {
    550                     final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor;
    551                     if (Math.abs(deltaScale) > SCALING_THRESHOLD) {
    552                         mScaling = true;
    553                         return true;
    554                     }
    555                 }
    556                 return false;
    557             }
    558             final float newScale = mMagnificationController.getScale()
    559                     * detector.getScaleFactor();
    560             final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), MAX_SCALE);
    561             if (DEBUG_SCALING) {
    562                 Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale);
    563             }
    564             mMagnificationController.setScale(normalizedNewScale, detector.getFocusX(),
    565                     detector.getFocusY(), false);
    566             return true;
    567         }
    568 
    569         @Override
    570         public boolean onScaleBegin(ScaleGestureDetector detector) {
    571             return (mCurrentState == STATE_MAGNIFIED_INTERACTION);
    572         }
    573 
    574         @Override
    575         public void onScaleEnd(ScaleGestureDetector detector) {
    576             clear();
    577         }
    578 
    579         private void clear() {
    580             mInitialScaleFactor = -1;
    581             mScaling = false;
    582         }
    583     }
    584 
    585     private final class StateViewportDraggingHandler {
    586         private boolean mLastMoveOutsideMagnifiedRegion;
    587 
    588         private void onMotionEvent(MotionEvent event, int policyFlags) {
    589             final int action = event.getActionMasked();
    590             switch (action) {
    591                 case MotionEvent.ACTION_DOWN: {
    592                     throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN");
    593                 }
    594                 case MotionEvent.ACTION_POINTER_DOWN: {
    595                     clear();
    596                     transitionToState(STATE_MAGNIFIED_INTERACTION);
    597                 } break;
    598                 case MotionEvent.ACTION_MOVE: {
    599                     if (event.getPointerCount() != 1) {
    600                         throw new IllegalStateException("Should have one pointer down.");
    601                     }
    602                     final float eventX = event.getX();
    603                     final float eventY = event.getY();
    604                     if (mMagnifiedBounds.contains((int) eventX, (int) eventY)) {
    605                         if (mLastMoveOutsideMagnifiedRegion) {
    606                             mLastMoveOutsideMagnifiedRegion = false;
    607                             mMagnificationController.setMagnifiedRegionCenter(eventX,
    608                                     eventY, true);
    609                         } else {
    610                             mMagnificationController.setMagnifiedRegionCenter(eventX,
    611                                     eventY, false);
    612                         }
    613                     } else {
    614                         mLastMoveOutsideMagnifiedRegion = true;
    615                     }
    616                 } break;
    617                 case MotionEvent.ACTION_UP: {
    618                     if (!mTranslationEnabledBeforePan) {
    619                         mMagnificationController.reset(true);
    620                     }
    621                     clear();
    622                     transitionToState(STATE_DETECTING);
    623                 } break;
    624                 case MotionEvent.ACTION_POINTER_UP: {
    625                     throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP");
    626                 }
    627             }
    628         }
    629 
    630         public void clear() {
    631             mLastMoveOutsideMagnifiedRegion = false;
    632         }
    633     }
    634 
    635     private final class DetectingStateHandler {
    636 
    637         private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1;
    638 
    639         private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
    640 
    641         private static final int ACTION_TAP_COUNT = 3;
    642 
    643         private MotionEventInfo mDelayedEventQueue;
    644 
    645         private MotionEvent mLastDownEvent;
    646         private MotionEvent mLastTapUpEvent;
    647         private int mTapCount;
    648 
    649         private final Handler mHandler = new Handler() {
    650             @Override
    651             public void handleMessage(Message message) {
    652                 final int type = message.what;
    653                 switch (type) {
    654                     case MESSAGE_ON_ACTION_TAP_AND_HOLD: {
    655                         MotionEvent event = (MotionEvent) message.obj;
    656                         final int policyFlags = message.arg1;
    657                         onActionTapAndHold(event, policyFlags);
    658                     } break;
    659                     case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
    660                         transitionToState(STATE_DELEGATING);
    661                         sendDelayedMotionEvents();
    662                         clear();
    663                     } break;
    664                     default: {
    665                         throw new IllegalArgumentException("Unknown message type: " + type);
    666                     }
    667                 }
    668             }
    669         };
    670 
    671         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
    672             cacheDelayedMotionEvent(event, rawEvent, policyFlags);
    673             final int action = event.getActionMasked();
    674             switch (action) {
    675                 case MotionEvent.ACTION_DOWN: {
    676                     mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
    677                     if (!mMagnifiedBounds.contains((int) event.getX(),
    678                             (int) event.getY())) {
    679                         transitionToDelegatingStateAndClear();
    680                         return;
    681                     }
    682                     if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null
    683                             && GestureUtils.isMultiTap(mLastDownEvent, event,
    684                                     mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
    685                         Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD,
    686                                 policyFlags, 0, event);
    687                         mHandler.sendMessageDelayed(message,
    688                                 ViewConfiguration.getLongPressTimeout());
    689                     } else if (mTapCount < ACTION_TAP_COUNT) {
    690                         Message message = mHandler.obtainMessage(
    691                                 MESSAGE_TRANSITION_TO_DELEGATING_STATE);
    692                         mHandler.sendMessageDelayed(message, mMultiTapTimeSlop);
    693                     }
    694                     clearLastDownEvent();
    695                     mLastDownEvent = MotionEvent.obtain(event);
    696                 } break;
    697                 case MotionEvent.ACTION_POINTER_DOWN: {
    698                     if (mMagnificationController.isMagnifying()) {
    699                         transitionToState(STATE_MAGNIFIED_INTERACTION);
    700                         clear();
    701                     } else {
    702                         transitionToDelegatingStateAndClear();
    703                     }
    704                 } break;
    705                 case MotionEvent.ACTION_MOVE: {
    706                     if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) {
    707                         final double distance = GestureUtils.computeDistance(mLastDownEvent,
    708                                 event, 0);
    709                         if (Math.abs(distance) > mTapDistanceSlop) {
    710                             transitionToDelegatingStateAndClear();
    711                         }
    712                     }
    713                 } break;
    714                 case MotionEvent.ACTION_UP: {
    715                     if (mLastDownEvent == null) {
    716                         return;
    717                     }
    718                     mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
    719                     if (!mMagnifiedBounds.contains((int) event.getX(), (int) event.getY())) {
    720                          transitionToDelegatingStateAndClear();
    721                          return;
    722                     }
    723                     if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop,
    724                             mTapDistanceSlop, 0)) {
    725                         transitionToDelegatingStateAndClear();
    726                         return;
    727                     }
    728                     if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent,
    729                             event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
    730                         transitionToDelegatingStateAndClear();
    731                         return;
    732                     }
    733                     mTapCount++;
    734                     if (DEBUG_DETECTING) {
    735                         Slog.i(LOG_TAG, "Tap count:" + mTapCount);
    736                     }
    737                     if (mTapCount == ACTION_TAP_COUNT) {
    738                         clear();
    739                         onActionTap(event, policyFlags);
    740                         return;
    741                     }
    742                     clearLastTapUpEvent();
    743                     mLastTapUpEvent = MotionEvent.obtain(event);
    744                 } break;
    745                 case MotionEvent.ACTION_POINTER_UP: {
    746                     /* do nothing */
    747                 } break;
    748             }
    749         }
    750 
    751         public void clear() {
    752             mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
    753             mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
    754             clearTapDetectionState();
    755             clearDelayedMotionEvents();
    756         }
    757 
    758         private void clearTapDetectionState() {
    759             mTapCount = 0;
    760             clearLastTapUpEvent();
    761             clearLastDownEvent();
    762         }
    763 
    764         private void clearLastTapUpEvent() {
    765             if (mLastTapUpEvent != null) {
    766                 mLastTapUpEvent.recycle();
    767                 mLastTapUpEvent = null;
    768             }
    769         }
    770 
    771         private void clearLastDownEvent() {
    772             if (mLastDownEvent != null) {
    773                 mLastDownEvent.recycle();
    774                 mLastDownEvent = null;
    775             }
    776         }
    777 
    778         private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
    779                 int policyFlags) {
    780             MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent,
    781                     policyFlags);
    782             if (mDelayedEventQueue == null) {
    783                 mDelayedEventQueue = info;
    784             } else {
    785                 MotionEventInfo tail = mDelayedEventQueue;
    786                 while (tail.mNext != null) {
    787                     tail = tail.mNext;
    788                 }
    789                 tail.mNext = info;
    790             }
    791         }
    792 
    793         private void sendDelayedMotionEvents() {
    794             while (mDelayedEventQueue != null) {
    795                 MotionEventInfo info = mDelayedEventQueue;
    796                 mDelayedEventQueue = info.mNext;
    797                 final long offset = SystemClock.uptimeMillis() - info.mCachedTimeMillis;
    798                 MotionEvent event = obtainEventWithOffsetTimeAndDownTime(info.mEvent, offset);
    799                 MotionEvent rawEvent = obtainEventWithOffsetTimeAndDownTime(info.mRawEvent, offset);
    800                 ScreenMagnifier.this.onMotionEvent(event, rawEvent, info.mPolicyFlags);
    801                 event.recycle();
    802                 rawEvent.recycle();
    803                 info.recycle();
    804             }
    805         }
    806 
    807         private MotionEvent obtainEventWithOffsetTimeAndDownTime(MotionEvent event, long offset) {
    808             final int pointerCount = event.getPointerCount();
    809             PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
    810             PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
    811             for (int i = 0; i < pointerCount; i++) {
    812                 event.getPointerCoords(i, coords[i]);
    813                 event.getPointerProperties(i, properties[i]);
    814             }
    815             final long downTime = event.getDownTime() + offset;
    816             final long eventTime = event.getEventTime() + offset;
    817             return MotionEvent.obtain(downTime, eventTime,
    818                     event.getAction(), pointerCount, properties, coords,
    819                     event.getMetaState(), event.getButtonState(),
    820                     1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(),
    821                     event.getSource(), event.getFlags());
    822         }
    823 
    824         private void clearDelayedMotionEvents() {
    825             while (mDelayedEventQueue != null) {
    826                 MotionEventInfo info = mDelayedEventQueue;
    827                 mDelayedEventQueue = info.mNext;
    828                 info.recycle();
    829             }
    830         }
    831 
    832         private void transitionToDelegatingStateAndClear() {
    833             transitionToState(STATE_DELEGATING);
    834             sendDelayedMotionEvents();
    835             clear();
    836         }
    837 
    838         private void onActionTap(MotionEvent up, int policyFlags) {
    839             if (DEBUG_DETECTING) {
    840                 Slog.i(LOG_TAG, "onActionTap()");
    841             }
    842             if (!mMagnificationController.isMagnifying()) {
    843                 mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
    844                         up.getX(), up.getY(), true);
    845             } else {
    846                 mMagnificationController.reset(true);
    847             }
    848         }
    849 
    850         private void onActionTapAndHold(MotionEvent down, int policyFlags) {
    851             if (DEBUG_DETECTING) {
    852                 Slog.i(LOG_TAG, "onActionTapAndHold()");
    853             }
    854             clear();
    855             mTranslationEnabledBeforePan = mMagnificationController.isMagnifying();
    856             mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
    857                     down.getX(), down.getY(), true);
    858             transitionToState(STATE_VIEWPORT_DRAGGING);
    859         }
    860     }
    861 
    862     private void persistScale(final float scale) {
    863         new AsyncTask<Void, Void, Void>() {
    864             @Override
    865             protected Void doInBackground(Void... params) {
    866                 Settings.Secure.putFloat(mContext.getContentResolver(),
    867                         Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale);
    868                 return null;
    869             }
    870         }.execute();
    871     }
    872 
    873     private float getPersistedScale() {
    874         return Settings.Secure.getFloat(mContext.getContentResolver(),
    875                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
    876                 DEFAULT_MAGNIFICATION_SCALE);
    877     }
    878 
    879     private static boolean isScreenMagnificationAutoUpdateEnabled(Context context) {
    880         return (Settings.Secure.getInt(context.getContentResolver(),
    881                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
    882                 DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
    883     }
    884 
    885     private static final class MotionEventInfo {
    886 
    887         private static final int MAX_POOL_SIZE = 10;
    888 
    889         private static final Object sLock = new Object();
    890         private static MotionEventInfo sPool;
    891         private static int sPoolSize;
    892 
    893         private MotionEventInfo mNext;
    894         private boolean mInPool;
    895 
    896         public MotionEvent mEvent;
    897         public MotionEvent mRawEvent;
    898         public int mPolicyFlags;
    899         public long mCachedTimeMillis;
    900 
    901         public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent,
    902                 int policyFlags) {
    903             synchronized (sLock) {
    904                 MotionEventInfo info;
    905                 if (sPoolSize > 0) {
    906                     sPoolSize--;
    907                     info = sPool;
    908                     sPool = info.mNext;
    909                     info.mNext = null;
    910                     info.mInPool = false;
    911                 } else {
    912                     info = new MotionEventInfo();
    913                 }
    914                 info.initialize(event, rawEvent, policyFlags);
    915                 return info;
    916             }
    917         }
    918 
    919         private void initialize(MotionEvent event, MotionEvent rawEvent,
    920                 int policyFlags) {
    921             mEvent = MotionEvent.obtain(event);
    922             mRawEvent = MotionEvent.obtain(rawEvent);
    923             mPolicyFlags = policyFlags;
    924             mCachedTimeMillis = SystemClock.uptimeMillis();
    925         }
    926 
    927         public void recycle() {
    928             synchronized (sLock) {
    929                 if (mInPool) {
    930                     throw new IllegalStateException("Already recycled.");
    931                 }
    932                 clear();
    933                 if (sPoolSize < MAX_POOL_SIZE) {
    934                     sPoolSize++;
    935                     mNext = sPool;
    936                     sPool = this;
    937                     mInPool = true;
    938                 }
    939             }
    940         }
    941 
    942         private void clear() {
    943             mEvent.recycle();
    944             mEvent = null;
    945             mRawEvent.recycle();
    946             mRawEvent = null;
    947             mPolicyFlags = 0;
    948             mCachedTimeMillis = 0;
    949         }
    950     }
    951 
    952     private final class MagnificationController {
    953 
    954         private static final String PROPERTY_NAME_MAGNIFICATION_SPEC =
    955                 "magnificationSpec";
    956 
    957         private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain();
    958 
    959         private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain();
    960 
    961         private final Rect mTempRect = new Rect();
    962 
    963         private final ValueAnimator mTransformationAnimator;
    964 
    965         public MagnificationController(long animationDuration) {
    966             Property<MagnificationController, MagnificationSpec> property =
    967                     Property.of(MagnificationController.class, MagnificationSpec.class,
    968                     PROPERTY_NAME_MAGNIFICATION_SPEC);
    969             TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() {
    970                 private final MagnificationSpec mTempTransformationSpec =
    971                         MagnificationSpec.obtain();
    972                 @Override
    973                 public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec,
    974                         MagnificationSpec toSpec) {
    975                     MagnificationSpec result = mTempTransformationSpec;
    976                     result.scale = fromSpec.scale
    977                             + (toSpec.scale - fromSpec.scale) * fraction;
    978                     result.offsetX = fromSpec.offsetX + (toSpec.offsetX - fromSpec.offsetX)
    979                             * fraction;
    980                     result.offsetY = fromSpec.offsetY + (toSpec.offsetY - fromSpec.offsetY)
    981                             * fraction;
    982                     return result;
    983                 }
    984             };
    985             mTransformationAnimator = ObjectAnimator.ofObject(this, property,
    986                     evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec);
    987             mTransformationAnimator.setDuration((long) (animationDuration));
    988             mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f));
    989         }
    990 
    991         public boolean isMagnifying() {
    992             return mCurrentMagnificationSpec.scale > 1.0f;
    993         }
    994 
    995         public void reset(boolean animate) {
    996             if (mTransformationAnimator.isRunning()) {
    997                 mTransformationAnimator.cancel();
    998             }
    999             mCurrentMagnificationSpec.clear();
   1000             if (animate) {
   1001                 animateMangificationSpec(mSentMagnificationSpec,
   1002                         mCurrentMagnificationSpec);
   1003             } else {
   1004                 setMagnificationSpec(mCurrentMagnificationSpec);
   1005             }
   1006             Rect bounds = mTempRect;
   1007             bounds.setEmpty();
   1008             mAms.onMagnificationStateChanged();
   1009         }
   1010 
   1011         public float getScale() {
   1012             return mCurrentMagnificationSpec.scale;
   1013         }
   1014 
   1015         public float getOffsetX() {
   1016             return mCurrentMagnificationSpec.offsetX;
   1017         }
   1018 
   1019         public float getOffsetY() {
   1020             return mCurrentMagnificationSpec.offsetY;
   1021         }
   1022 
   1023         public void setScale(float scale, float pivotX, float pivotY, boolean animate) {
   1024             Rect magnifiedFrame = mTempRect;
   1025             mMagnifiedBounds.getBounds(magnifiedFrame);
   1026             MagnificationSpec spec = mCurrentMagnificationSpec;
   1027             final float oldScale = spec.scale;
   1028             final float oldCenterX = (-spec.offsetX + magnifiedFrame.width() / 2) / oldScale;
   1029             final float oldCenterY = (-spec.offsetY + magnifiedFrame.height() / 2) / oldScale;
   1030             final float normPivotX = (-spec.offsetX + pivotX) / oldScale;
   1031             final float normPivotY = (-spec.offsetY + pivotY) / oldScale;
   1032             final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale);
   1033             final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale);
   1034             final float centerX = normPivotX + offsetX;
   1035             final float centerY = normPivotY + offsetY;
   1036             setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate);
   1037         }
   1038 
   1039         public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) {
   1040             setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.scale, centerX, centerY,
   1041                     animate);
   1042         }
   1043 
   1044         public void offsetMagnifiedRegionCenter(float offsetX, float offsetY) {
   1045             final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX;
   1046             mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX,
   1047                     getMinOffsetX()), 0);
   1048             final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY;
   1049             mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY,
   1050                     getMinOffsetY()), 0);
   1051             setMagnificationSpec(mCurrentMagnificationSpec);
   1052         }
   1053 
   1054         public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY,
   1055                 boolean animate) {
   1056             if (Float.compare(mCurrentMagnificationSpec.scale, scale) == 0
   1057                     && Float.compare(mCurrentMagnificationSpec.offsetX,
   1058                             centerX) == 0
   1059                     && Float.compare(mCurrentMagnificationSpec.offsetY,
   1060                             centerY) == 0) {
   1061                 return;
   1062             }
   1063             if (mTransformationAnimator.isRunning()) {
   1064                 mTransformationAnimator.cancel();
   1065             }
   1066             if (DEBUG_MAGNIFICATION_CONTROLLER) {
   1067                 Slog.i(LOG_TAG, "scale: " + scale + " offsetX: " + centerX
   1068                         + " offsetY: " + centerY);
   1069             }
   1070             updateMagnificationSpec(scale, centerX, centerY);
   1071             if (animate) {
   1072                 animateMangificationSpec(mSentMagnificationSpec,
   1073                         mCurrentMagnificationSpec);
   1074             } else {
   1075                 setMagnificationSpec(mCurrentMagnificationSpec);
   1076             }
   1077             mAms.onMagnificationStateChanged();
   1078         }
   1079 
   1080         public void updateMagnificationSpec(float scale, float magnifiedCenterX,
   1081                 float magnifiedCenterY) {
   1082             Rect magnifiedFrame = mTempRect;
   1083             mMagnifiedBounds.getBounds(magnifiedFrame);
   1084             mCurrentMagnificationSpec.scale = scale;
   1085             final int viewportWidth = magnifiedFrame.width();
   1086             final float nonNormOffsetX = viewportWidth / 2 - magnifiedCenterX * scale;
   1087             mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX,
   1088                     getMinOffsetX()), 0);
   1089             final int viewportHeight = magnifiedFrame.height();
   1090             final float nonNormOffsetY = viewportHeight / 2 - magnifiedCenterY * scale;
   1091             mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY,
   1092                     getMinOffsetY()), 0);
   1093         }
   1094 
   1095         private float getMinOffsetX() {
   1096             Rect magnifiedFrame = mTempRect;
   1097             mMagnifiedBounds.getBounds(magnifiedFrame);
   1098             final float viewportWidth = magnifiedFrame.width();
   1099             return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale;
   1100         }
   1101 
   1102         private float getMinOffsetY() {
   1103             Rect magnifiedFrame = mTempRect;
   1104             mMagnifiedBounds.getBounds(magnifiedFrame);
   1105             final float viewportHeight = magnifiedFrame.height();
   1106             return viewportHeight - viewportHeight * mCurrentMagnificationSpec.scale;
   1107         }
   1108 
   1109         private void animateMangificationSpec(MagnificationSpec fromSpec,
   1110                 MagnificationSpec toSpec) {
   1111             mTransformationAnimator.setObjectValues(fromSpec, toSpec);
   1112             mTransformationAnimator.start();
   1113         }
   1114 
   1115         public MagnificationSpec getMagnificationSpec() {
   1116             return mSentMagnificationSpec;
   1117         }
   1118 
   1119         public void setMagnificationSpec(MagnificationSpec spec) {
   1120             if (DEBUG_SET_MAGNIFICATION_SPEC) {
   1121                 Slog.i(LOG_TAG, "Sending: " + spec);
   1122             }
   1123             try {
   1124                 mSentMagnificationSpec.scale = spec.scale;
   1125                 mSentMagnificationSpec.offsetX = spec.offsetX;
   1126                 mSentMagnificationSpec.offsetY = spec.offsetY;
   1127                 mWindowManager.setMagnificationSpec(
   1128                         MagnificationSpec.obtain(spec));
   1129             } catch (RemoteException re) {
   1130                 /* ignore */
   1131             }
   1132         }
   1133     }
   1134 
   1135     private final class ScreenStateObserver extends BroadcastReceiver {
   1136         private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1;
   1137 
   1138         private final Context mContext;
   1139         private final MagnificationController mMagnificationController;
   1140 
   1141         private final Handler mHandler = new Handler() {
   1142             @Override
   1143             public void handleMessage(Message message) {
   1144                  switch (message.what) {
   1145                     case MESSAGE_ON_SCREEN_STATE_CHANGE: {
   1146                         String action = (String) message.obj;
   1147                         handleOnScreenStateChange(action);
   1148                     } break;
   1149                 }
   1150             }
   1151         };
   1152 
   1153         public ScreenStateObserver(Context context,
   1154                 MagnificationController magnificationController) {
   1155             mContext = context;
   1156             mMagnificationController = magnificationController;
   1157             mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
   1158         }
   1159 
   1160         public void destroy() {
   1161             mContext.unregisterReceiver(this);
   1162         }
   1163 
   1164         @Override
   1165         public void onReceive(Context context, Intent intent) {
   1166             mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE,
   1167                     intent.getAction()).sendToTarget();
   1168         }
   1169 
   1170         private void handleOnScreenStateChange(String action) {
   1171             if (mMagnificationController.isMagnifying()
   1172                     && isScreenMagnificationAutoUpdateEnabled(mContext)) {
   1173                 mMagnificationController.reset(false);
   1174             }
   1175         }
   1176     }
   1177 }
   1178