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             mGestureDetector = new GestureDetector(context, this);
    506         }
    507 
    508         public void onMotionEvent(MotionEvent event) {
    509             mScaleGestureDetector.onTouchEvent(event);
    510             mGestureDetector.onTouchEvent(event);
    511             if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
    512                 return;
    513             }
    514             if (event.getActionMasked() == MotionEvent.ACTION_UP) {
    515                 clear();
    516                 final float scale = Math.min(Math.max(mMagnificationController.getScale(),
    517                         MIN_SCALE), MAX_SCALE);
    518                 if (scale != getPersistedScale()) {
    519                     persistScale(scale);
    520                 }
    521                 if (mPreviousState == STATE_VIEWPORT_DRAGGING) {
    522                     transitionToState(STATE_VIEWPORT_DRAGGING);
    523                 } else {
    524                     transitionToState(STATE_DETECTING);
    525                 }
    526             }
    527         }
    528 
    529         @Override
    530         public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX,
    531                 float distanceY) {
    532             if (mCurrentState != STATE_MAGNIFIED_INTERACTION) {
    533                 return true;
    534             }
    535             if (DEBUG_PANNING) {
    536                 Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX
    537                         + " scrollY: " + distanceY);
    538             }
    539             mMagnificationController.offsetMagnifiedRegionCenter(distanceX, distanceY);
    540             return true;
    541         }
    542 
    543         @Override
    544         public boolean onScale(ScaleGestureDetector detector) {
    545             if (!mScaling) {
    546                 if (mInitialScaleFactor < 0) {
    547                     mInitialScaleFactor = detector.getScaleFactor();
    548                 } else {
    549                     final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor;
    550                     if (Math.abs(deltaScale) > SCALING_THRESHOLD) {
    551                         mScaling = true;
    552                         return true;
    553                     }
    554                 }
    555                 return false;
    556             }
    557             final float newScale = mMagnificationController.getScale()
    558                     * detector.getScaleFactor();
    559             final float normalizedNewScale = Math.min(Math.max(newScale, MIN_SCALE), MAX_SCALE);
    560             if (DEBUG_SCALING) {
    561                 Slog.i(LOG_TAG, "normalizedNewScale: " + normalizedNewScale);
    562             }
    563             mMagnificationController.setScale(normalizedNewScale, detector.getFocusX(),
    564                     detector.getFocusY(), false);
    565             return true;
    566         }
    567 
    568         @Override
    569         public boolean onScaleBegin(ScaleGestureDetector detector) {
    570             return (mCurrentState == STATE_MAGNIFIED_INTERACTION);
    571         }
    572 
    573         @Override
    574         public void onScaleEnd(ScaleGestureDetector detector) {
    575             clear();
    576         }
    577 
    578         private void clear() {
    579             mInitialScaleFactor = -1;
    580             mScaling = false;
    581         }
    582     }
    583 
    584     private final class StateViewportDraggingHandler {
    585         private boolean mLastMoveOutsideMagnifiedRegion;
    586 
    587         private void onMotionEvent(MotionEvent event, int policyFlags) {
    588             final int action = event.getActionMasked();
    589             switch (action) {
    590                 case MotionEvent.ACTION_DOWN: {
    591                     throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN");
    592                 }
    593                 case MotionEvent.ACTION_POINTER_DOWN: {
    594                     clear();
    595                     transitionToState(STATE_MAGNIFIED_INTERACTION);
    596                 } break;
    597                 case MotionEvent.ACTION_MOVE: {
    598                     if (event.getPointerCount() != 1) {
    599                         throw new IllegalStateException("Should have one pointer down.");
    600                     }
    601                     final float eventX = event.getX();
    602                     final float eventY = event.getY();
    603                     if (mMagnifiedBounds.contains((int) eventX, (int) eventY)) {
    604                         if (mLastMoveOutsideMagnifiedRegion) {
    605                             mLastMoveOutsideMagnifiedRegion = false;
    606                             mMagnificationController.setMagnifiedRegionCenter(eventX,
    607                                     eventY, true);
    608                         } else {
    609                             mMagnificationController.setMagnifiedRegionCenter(eventX,
    610                                     eventY, false);
    611                         }
    612                     } else {
    613                         mLastMoveOutsideMagnifiedRegion = true;
    614                     }
    615                 } break;
    616                 case MotionEvent.ACTION_UP: {
    617                     if (!mTranslationEnabledBeforePan) {
    618                         mMagnificationController.reset(true);
    619                     }
    620                     clear();
    621                     transitionToState(STATE_DETECTING);
    622                 } break;
    623                 case MotionEvent.ACTION_POINTER_UP: {
    624                     throw new IllegalArgumentException("Unexpected event type: ACTION_POINTER_UP");
    625                 }
    626             }
    627         }
    628 
    629         public void clear() {
    630             mLastMoveOutsideMagnifiedRegion = false;
    631         }
    632     }
    633 
    634     private final class DetectingStateHandler {
    635 
    636         private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1;
    637 
    638         private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
    639 
    640         private static final int ACTION_TAP_COUNT = 3;
    641 
    642         private MotionEventInfo mDelayedEventQueue;
    643 
    644         private MotionEvent mLastDownEvent;
    645         private MotionEvent mLastTapUpEvent;
    646         private int mTapCount;
    647 
    648         private final Handler mHandler = new Handler() {
    649             @Override
    650             public void handleMessage(Message message) {
    651                 final int type = message.what;
    652                 switch (type) {
    653                     case MESSAGE_ON_ACTION_TAP_AND_HOLD: {
    654                         MotionEvent event = (MotionEvent) message.obj;
    655                         final int policyFlags = message.arg1;
    656                         onActionTapAndHold(event, policyFlags);
    657                     } break;
    658                     case MESSAGE_TRANSITION_TO_DELEGATING_STATE: {
    659                         transitionToState(STATE_DELEGATING);
    660                         sendDelayedMotionEvents();
    661                         clear();
    662                     } break;
    663                     default: {
    664                         throw new IllegalArgumentException("Unknown message type: " + type);
    665                     }
    666                 }
    667             }
    668         };
    669 
    670         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
    671             cacheDelayedMotionEvent(event, rawEvent, policyFlags);
    672             final int action = event.getActionMasked();
    673             switch (action) {
    674                 case MotionEvent.ACTION_DOWN: {
    675                     mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
    676                     if (!mMagnifiedBounds.contains((int) event.getX(),
    677                             (int) event.getY())) {
    678                         transitionToDelegatingStateAndClear();
    679                         return;
    680                     }
    681                     if (mTapCount == ACTION_TAP_COUNT - 1 && mLastDownEvent != null
    682                             && GestureUtils.isMultiTap(mLastDownEvent, event,
    683                                     mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
    684                         Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD,
    685                                 policyFlags, 0, event);
    686                         mHandler.sendMessageDelayed(message,
    687                                 ViewConfiguration.getLongPressTimeout());
    688                     } else if (mTapCount < ACTION_TAP_COUNT) {
    689                         Message message = mHandler.obtainMessage(
    690                                 MESSAGE_TRANSITION_TO_DELEGATING_STATE);
    691                         mHandler.sendMessageDelayed(message, mMultiTapTimeSlop);
    692                     }
    693                     clearLastDownEvent();
    694                     mLastDownEvent = MotionEvent.obtain(event);
    695                 } break;
    696                 case MotionEvent.ACTION_POINTER_DOWN: {
    697                     if (mMagnificationController.isMagnifying()) {
    698                         transitionToState(STATE_MAGNIFIED_INTERACTION);
    699                         clear();
    700                     } else {
    701                         transitionToDelegatingStateAndClear();
    702                     }
    703                 } break;
    704                 case MotionEvent.ACTION_MOVE: {
    705                     if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) {
    706                         final double distance = GestureUtils.computeDistance(mLastDownEvent,
    707                                 event, 0);
    708                         if (Math.abs(distance) > mTapDistanceSlop) {
    709                             transitionToDelegatingStateAndClear();
    710                         }
    711                     }
    712                 } break;
    713                 case MotionEvent.ACTION_UP: {
    714                     if (mLastDownEvent == null) {
    715                         return;
    716                     }
    717                     mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
    718                     if (!mMagnifiedBounds.contains((int) event.getX(), (int) event.getY())) {
    719                          transitionToDelegatingStateAndClear();
    720                          return;
    721                     }
    722                     if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop,
    723                             mTapDistanceSlop, 0)) {
    724                         transitionToDelegatingStateAndClear();
    725                         return;
    726                     }
    727                     if (mLastTapUpEvent != null && !GestureUtils.isMultiTap(mLastTapUpEvent,
    728                             event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) {
    729                         transitionToDelegatingStateAndClear();
    730                         return;
    731                     }
    732                     mTapCount++;
    733                     if (DEBUG_DETECTING) {
    734                         Slog.i(LOG_TAG, "Tap count:" + mTapCount);
    735                     }
    736                     if (mTapCount == ACTION_TAP_COUNT) {
    737                         clear();
    738                         onActionTap(event, policyFlags);
    739                         return;
    740                     }
    741                     clearLastTapUpEvent();
    742                     mLastTapUpEvent = MotionEvent.obtain(event);
    743                 } break;
    744                 case MotionEvent.ACTION_POINTER_UP: {
    745                     /* do nothing */
    746                 } break;
    747             }
    748         }
    749 
    750         public void clear() {
    751             mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD);
    752             mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
    753             clearTapDetectionState();
    754             clearDelayedMotionEvents();
    755         }
    756 
    757         private void clearTapDetectionState() {
    758             mTapCount = 0;
    759             clearLastTapUpEvent();
    760             clearLastDownEvent();
    761         }
    762 
    763         private void clearLastTapUpEvent() {
    764             if (mLastTapUpEvent != null) {
    765                 mLastTapUpEvent.recycle();
    766                 mLastTapUpEvent = null;
    767             }
    768         }
    769 
    770         private void clearLastDownEvent() {
    771             if (mLastDownEvent != null) {
    772                 mLastDownEvent.recycle();
    773                 mLastDownEvent = null;
    774             }
    775         }
    776 
    777         private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
    778                 int policyFlags) {
    779             MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent,
    780                     policyFlags);
    781             if (mDelayedEventQueue == null) {
    782                 mDelayedEventQueue = info;
    783             } else {
    784                 MotionEventInfo tail = mDelayedEventQueue;
    785                 while (tail.mNext != null) {
    786                     tail = tail.mNext;
    787                 }
    788                 tail.mNext = info;
    789             }
    790         }
    791 
    792         private void sendDelayedMotionEvents() {
    793             while (mDelayedEventQueue != null) {
    794                 MotionEventInfo info = mDelayedEventQueue;
    795                 mDelayedEventQueue = info.mNext;
    796                 final long offset = SystemClock.uptimeMillis() - info.mCachedTimeMillis;
    797                 MotionEvent event = obtainEventWithOffsetTimeAndDownTime(info.mEvent, offset);
    798                 MotionEvent rawEvent = obtainEventWithOffsetTimeAndDownTime(info.mRawEvent, offset);
    799                 ScreenMagnifier.this.onMotionEvent(event, rawEvent, info.mPolicyFlags);
    800                 event.recycle();
    801                 rawEvent.recycle();
    802                 info.recycle();
    803             }
    804         }
    805 
    806         private MotionEvent obtainEventWithOffsetTimeAndDownTime(MotionEvent event, long offset) {
    807             final int pointerCount = event.getPointerCount();
    808             PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
    809             PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
    810             for (int i = 0; i < pointerCount; i++) {
    811                 event.getPointerCoords(i, coords[i]);
    812                 event.getPointerProperties(i, properties[i]);
    813             }
    814             final long downTime = event.getDownTime() + offset;
    815             final long eventTime = event.getEventTime() + offset;
    816             return MotionEvent.obtain(downTime, eventTime,
    817                     event.getAction(), pointerCount, properties, coords,
    818                     event.getMetaState(), event.getButtonState(),
    819                     1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(),
    820                     event.getSource(), event.getFlags());
    821         }
    822 
    823         private void clearDelayedMotionEvents() {
    824             while (mDelayedEventQueue != null) {
    825                 MotionEventInfo info = mDelayedEventQueue;
    826                 mDelayedEventQueue = info.mNext;
    827                 info.recycle();
    828             }
    829         }
    830 
    831         private void transitionToDelegatingStateAndClear() {
    832             transitionToState(STATE_DELEGATING);
    833             sendDelayedMotionEvents();
    834             clear();
    835         }
    836 
    837         private void onActionTap(MotionEvent up, int policyFlags) {
    838             if (DEBUG_DETECTING) {
    839                 Slog.i(LOG_TAG, "onActionTap()");
    840             }
    841             if (!mMagnificationController.isMagnifying()) {
    842                 mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
    843                         up.getX(), up.getY(), true);
    844             } else {
    845                 mMagnificationController.reset(true);
    846             }
    847         }
    848 
    849         private void onActionTapAndHold(MotionEvent down, int policyFlags) {
    850             if (DEBUG_DETECTING) {
    851                 Slog.i(LOG_TAG, "onActionTapAndHold()");
    852             }
    853             clear();
    854             mTranslationEnabledBeforePan = mMagnificationController.isMagnifying();
    855             mMagnificationController.setScaleAndMagnifiedRegionCenter(getPersistedScale(),
    856                     down.getX(), down.getY(), true);
    857             transitionToState(STATE_VIEWPORT_DRAGGING);
    858         }
    859     }
    860 
    861     private void persistScale(final float scale) {
    862         new AsyncTask<Void, Void, Void>() {
    863             @Override
    864             protected Void doInBackground(Void... params) {
    865                 Settings.Secure.putFloat(mContext.getContentResolver(),
    866                         Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale);
    867                 return null;
    868             }
    869         }.execute();
    870     }
    871 
    872     private float getPersistedScale() {
    873         return Settings.Secure.getFloat(mContext.getContentResolver(),
    874                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
    875                 DEFAULT_MAGNIFICATION_SCALE);
    876     }
    877 
    878     private static boolean isScreenMagnificationAutoUpdateEnabled(Context context) {
    879         return (Settings.Secure.getInt(context.getContentResolver(),
    880                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
    881                 DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
    882     }
    883 
    884     private static final class MotionEventInfo {
    885 
    886         private static final int MAX_POOL_SIZE = 10;
    887 
    888         private static final Object sLock = new Object();
    889         private static MotionEventInfo sPool;
    890         private static int sPoolSize;
    891 
    892         private MotionEventInfo mNext;
    893         private boolean mInPool;
    894 
    895         public MotionEvent mEvent;
    896         public MotionEvent mRawEvent;
    897         public int mPolicyFlags;
    898         public long mCachedTimeMillis;
    899 
    900         public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent,
    901                 int policyFlags) {
    902             synchronized (sLock) {
    903                 MotionEventInfo info;
    904                 if (sPoolSize > 0) {
    905                     sPoolSize--;
    906                     info = sPool;
    907                     sPool = info.mNext;
    908                     info.mNext = null;
    909                     info.mInPool = false;
    910                 } else {
    911                     info = new MotionEventInfo();
    912                 }
    913                 info.initialize(event, rawEvent, policyFlags);
    914                 return info;
    915             }
    916         }
    917 
    918         private void initialize(MotionEvent event, MotionEvent rawEvent,
    919                 int policyFlags) {
    920             mEvent = MotionEvent.obtain(event);
    921             mRawEvent = MotionEvent.obtain(rawEvent);
    922             mPolicyFlags = policyFlags;
    923             mCachedTimeMillis = SystemClock.uptimeMillis();
    924         }
    925 
    926         public void recycle() {
    927             synchronized (sLock) {
    928                 if (mInPool) {
    929                     throw new IllegalStateException("Already recycled.");
    930                 }
    931                 clear();
    932                 if (sPoolSize < MAX_POOL_SIZE) {
    933                     sPoolSize++;
    934                     mNext = sPool;
    935                     sPool = this;
    936                     mInPool = true;
    937                 }
    938             }
    939         }
    940 
    941         private void clear() {
    942             mEvent.recycle();
    943             mEvent = null;
    944             mRawEvent.recycle();
    945             mRawEvent = null;
    946             mPolicyFlags = 0;
    947             mCachedTimeMillis = 0;
    948         }
    949     }
    950 
    951     private final class MagnificationController {
    952 
    953         private static final String PROPERTY_NAME_MAGNIFICATION_SPEC =
    954                 "magnificationSpec";
    955 
    956         private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain();
    957 
    958         private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain();
    959 
    960         private final Rect mTempRect = new Rect();
    961 
    962         private final ValueAnimator mTransformationAnimator;
    963 
    964         public MagnificationController(long animationDuration) {
    965             Property<MagnificationController, MagnificationSpec> property =
    966                     Property.of(MagnificationController.class, MagnificationSpec.class,
    967                     PROPERTY_NAME_MAGNIFICATION_SPEC);
    968             TypeEvaluator<MagnificationSpec> evaluator = new TypeEvaluator<MagnificationSpec>() {
    969                 private final MagnificationSpec mTempTransformationSpec =
    970                         MagnificationSpec.obtain();
    971                 @Override
    972                 public MagnificationSpec evaluate(float fraction, MagnificationSpec fromSpec,
    973                         MagnificationSpec toSpec) {
    974                     MagnificationSpec result = mTempTransformationSpec;
    975                     result.scale = fromSpec.scale
    976                             + (toSpec.scale - fromSpec.scale) * fraction;
    977                     result.offsetX = fromSpec.offsetX + (toSpec.offsetX - fromSpec.offsetX)
    978                             * fraction;
    979                     result.offsetY = fromSpec.offsetY + (toSpec.offsetY - fromSpec.offsetY)
    980                             * fraction;
    981                     return result;
    982                 }
    983             };
    984             mTransformationAnimator = ObjectAnimator.ofObject(this, property,
    985                     evaluator, mSentMagnificationSpec, mCurrentMagnificationSpec);
    986             mTransformationAnimator.setDuration((long) (animationDuration));
    987             mTransformationAnimator.setInterpolator(new DecelerateInterpolator(2.5f));
    988         }
    989 
    990         public boolean isMagnifying() {
    991             return mCurrentMagnificationSpec.scale > 1.0f;
    992         }
    993 
    994         public void reset(boolean animate) {
    995             if (mTransformationAnimator.isRunning()) {
    996                 mTransformationAnimator.cancel();
    997             }
    998             mCurrentMagnificationSpec.clear();
    999             if (animate) {
   1000                 animateMangificationSpec(mSentMagnificationSpec,
   1001                         mCurrentMagnificationSpec);
   1002             } else {
   1003                 setMagnificationSpec(mCurrentMagnificationSpec);
   1004             }
   1005             Rect bounds = mTempRect;
   1006             bounds.setEmpty();
   1007             mAms.onMagnificationStateChanged();
   1008         }
   1009 
   1010         public float getScale() {
   1011             return mCurrentMagnificationSpec.scale;
   1012         }
   1013 
   1014         public float getOffsetX() {
   1015             return mCurrentMagnificationSpec.offsetX;
   1016         }
   1017 
   1018         public float getOffsetY() {
   1019             return mCurrentMagnificationSpec.offsetY;
   1020         }
   1021 
   1022         public void setScale(float scale, float pivotX, float pivotY, boolean animate) {
   1023             Rect magnifiedFrame = mTempRect;
   1024             mMagnifiedBounds.getBounds(magnifiedFrame);
   1025             MagnificationSpec spec = mCurrentMagnificationSpec;
   1026             final float oldScale = spec.scale;
   1027             final float oldCenterX = (-spec.offsetX + magnifiedFrame.width() / 2) / oldScale;
   1028             final float oldCenterY = (-spec.offsetY + magnifiedFrame.height() / 2) / oldScale;
   1029             final float normPivotX = (-spec.offsetX + pivotX) / oldScale;
   1030             final float normPivotY = (-spec.offsetY + pivotY) / oldScale;
   1031             final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale);
   1032             final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale);
   1033             final float centerX = normPivotX + offsetX;
   1034             final float centerY = normPivotY + offsetY;
   1035             setScaleAndMagnifiedRegionCenter(scale, centerX, centerY, animate);
   1036         }
   1037 
   1038         public void setMagnifiedRegionCenter(float centerX, float centerY, boolean animate) {
   1039             setScaleAndMagnifiedRegionCenter(mCurrentMagnificationSpec.scale, centerX, centerY,
   1040                     animate);
   1041         }
   1042 
   1043         public void offsetMagnifiedRegionCenter(float offsetX, float offsetY) {
   1044             final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX;
   1045             mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX,
   1046                     getMinOffsetX()), 0);
   1047             final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY;
   1048             mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY,
   1049                     getMinOffsetY()), 0);
   1050             setMagnificationSpec(mCurrentMagnificationSpec);
   1051         }
   1052 
   1053         public void setScaleAndMagnifiedRegionCenter(float scale, float centerX, float centerY,
   1054                 boolean animate) {
   1055             if (Float.compare(mCurrentMagnificationSpec.scale, scale) == 0
   1056                     && Float.compare(mCurrentMagnificationSpec.offsetX,
   1057                             centerX) == 0
   1058                     && Float.compare(mCurrentMagnificationSpec.offsetY,
   1059                             centerY) == 0) {
   1060                 return;
   1061             }
   1062             if (mTransformationAnimator.isRunning()) {
   1063                 mTransformationAnimator.cancel();
   1064             }
   1065             if (DEBUG_MAGNIFICATION_CONTROLLER) {
   1066                 Slog.i(LOG_TAG, "scale: " + scale + " offsetX: " + centerX
   1067                         + " offsetY: " + centerY);
   1068             }
   1069             updateMagnificationSpec(scale, centerX, centerY);
   1070             if (animate) {
   1071                 animateMangificationSpec(mSentMagnificationSpec,
   1072                         mCurrentMagnificationSpec);
   1073             } else {
   1074                 setMagnificationSpec(mCurrentMagnificationSpec);
   1075             }
   1076             mAms.onMagnificationStateChanged();
   1077         }
   1078 
   1079         public void updateMagnificationSpec(float scale, float magnifiedCenterX,
   1080                 float magnifiedCenterY) {
   1081             Rect magnifiedFrame = mTempRect;
   1082             mMagnifiedBounds.getBounds(magnifiedFrame);
   1083             mCurrentMagnificationSpec.scale = scale;
   1084             final int viewportWidth = magnifiedFrame.width();
   1085             final float nonNormOffsetX = viewportWidth / 2 - magnifiedCenterX * scale;
   1086             mCurrentMagnificationSpec.offsetX = Math.min(Math.max(nonNormOffsetX,
   1087                     getMinOffsetX()), 0);
   1088             final int viewportHeight = magnifiedFrame.height();
   1089             final float nonNormOffsetY = viewportHeight / 2 - magnifiedCenterY * scale;
   1090             mCurrentMagnificationSpec.offsetY = Math.min(Math.max(nonNormOffsetY,
   1091                     getMinOffsetY()), 0);
   1092         }
   1093 
   1094         private float getMinOffsetX() {
   1095             Rect magnifiedFrame = mTempRect;
   1096             mMagnifiedBounds.getBounds(magnifiedFrame);
   1097             final float viewportWidth = magnifiedFrame.width();
   1098             return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale;
   1099         }
   1100 
   1101         private float getMinOffsetY() {
   1102             Rect magnifiedFrame = mTempRect;
   1103             mMagnifiedBounds.getBounds(magnifiedFrame);
   1104             final float viewportHeight = magnifiedFrame.height();
   1105             return viewportHeight - viewportHeight * mCurrentMagnificationSpec.scale;
   1106         }
   1107 
   1108         private void animateMangificationSpec(MagnificationSpec fromSpec,
   1109                 MagnificationSpec toSpec) {
   1110             mTransformationAnimator.setObjectValues(fromSpec, toSpec);
   1111             mTransformationAnimator.start();
   1112         }
   1113 
   1114         public MagnificationSpec getMagnificationSpec() {
   1115             return mSentMagnificationSpec;
   1116         }
   1117 
   1118         public void setMagnificationSpec(MagnificationSpec spec) {
   1119             if (DEBUG_SET_MAGNIFICATION_SPEC) {
   1120                 Slog.i(LOG_TAG, "Sending: " + spec);
   1121             }
   1122             try {
   1123                 mSentMagnificationSpec.scale = spec.scale;
   1124                 mSentMagnificationSpec.offsetX = spec.offsetX;
   1125                 mSentMagnificationSpec.offsetY = spec.offsetY;
   1126                 mWindowManager.setMagnificationSpec(
   1127                         MagnificationSpec.obtain(spec));
   1128             } catch (RemoteException re) {
   1129                 /* ignore */
   1130             }
   1131         }
   1132     }
   1133 
   1134     private final class ScreenStateObserver extends BroadcastReceiver {
   1135         private static final int MESSAGE_ON_SCREEN_STATE_CHANGE = 1;
   1136 
   1137         private final Context mContext;
   1138         private final MagnificationController mMagnificationController;
   1139 
   1140         private final Handler mHandler = new Handler() {
   1141             @Override
   1142             public void handleMessage(Message message) {
   1143                  switch (message.what) {
   1144                     case MESSAGE_ON_SCREEN_STATE_CHANGE: {
   1145                         String action = (String) message.obj;
   1146                         handleOnScreenStateChange(action);
   1147                     } break;
   1148                 }
   1149             }
   1150         };
   1151 
   1152         public ScreenStateObserver(Context context,
   1153                 MagnificationController magnificationController) {
   1154             mContext = context;
   1155             mMagnificationController = magnificationController;
   1156             mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
   1157         }
   1158 
   1159         public void destroy() {
   1160             mContext.unregisterReceiver(this);
   1161         }
   1162 
   1163         @Override
   1164         public void onReceive(Context context, Intent intent) {
   1165             mHandler.obtainMessage(MESSAGE_ON_SCREEN_STATE_CHANGE,
   1166                     intent.getAction()).sendToTarget();
   1167         }
   1168 
   1169         private void handleOnScreenStateChange(String action) {
   1170             if (mMagnificationController.isMagnifying()
   1171                     && isScreenMagnificationAutoUpdateEnabled(mContext)) {
   1172                 mMagnificationController.reset(false);
   1173             }
   1174         }
   1175     }
   1176 }
   1177