FileDocCategorySizeDatePackage
ScreenMagnifier.javaAPI DocAndroid 5.1 API49374Thu Mar 12 22:22:42 GMT 2015com.android.server.accessibility

ScreenMagnifier

public final class ScreenMagnifier extends Object implements WindowManagerInternal.MagnificationCallbacks, EventStreamTransformation
This class handles the screen magnification when accessibility is enabled. The behavior is as follows: 1. Triple tap toggles permanent screen magnification which is magnifying the area around the location of the triple tap. One can think of the location of the triple tap as the center of the magnified viewport. For example, a triple tap when not magnified would magnify the screen and leave it in a magnified state. A triple tapping when magnified would clear magnification and leave the screen in a not magnified state. 2. Triple tap and hold would magnify the screen if not magnified and enable viewport dragging mode until the finger goes up. One can think of this mode as a way to move the magnified viewport since the area around the moving finger will be magnified to fit the screen. For example, if the screen was not magnified and the user triple taps and holds the screen would magnify and the viewport will follow the user's finger. When the finger goes up the screen will zoom out. If the same user interaction is performed when the screen is magnified, the viewport movement will be the same but when the finger goes up the screen will stay magnified. In other words, the initial magnified state is sticky. 3. Pinching with any number of additional fingers when viewport dragging is enabled, i.e. the user triple tapped and holds, would adjust the magnification scale which will become the current default magnification scale. The next time the user magnifies the same magnification scale would be used. 4. When in a permanent magnified state the user can use two or more fingers to pan the viewport. Note that in this mode the content is panned as opposed to the viewport dragging mode in which the viewport is moved. 5. When in a permanent magnified state the user can use two or more fingers to change the magnification scale which will become the current default magnification scale. The next time the user magnifies the same magnification scale would be used. 6. The magnification scale will be persisted in settings and in the cloud.

Fields Summary
private static final String
LOG_TAG
private static final boolean
DEBUG_STATE_TRANSITIONS
private static final boolean
DEBUG_DETECTING
private static final boolean
DEBUG_SET_MAGNIFICATION_SPEC
private static final boolean
DEBUG_PANNING
private static final boolean
DEBUG_SCALING
private static final boolean
DEBUG_MAGNIFICATION_CONTROLLER
private static final int
STATE_DELEGATING
private static final int
STATE_DETECTING
private static final int
STATE_VIEWPORT_DRAGGING
private static final int
STATE_MAGNIFIED_INTERACTION
private static final float
DEFAULT_MAGNIFICATION_SCALE
private static final int
MULTI_TAP_TIME_SLOP_ADJUSTMENT
private static final int
MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED
private static final int
MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED
private static final int
MESSAGE_ON_USER_CONTEXT_CHANGED
private static final int
MESSAGE_ON_ROTATION_CHANGED
private static final int
DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE
private static final int
MY_PID
private final android.graphics.Rect
mTempRect
private final android.graphics.Rect
mTempRect1
private final android.content.Context
mContext
private final android.view.WindowManagerInternal
mWindowManager
private final MagnificationController
mMagnificationController
private final ScreenStateObserver
mScreenStateObserver
private final DetectingStateHandler
mDetectingStateHandler
private final MagnifiedContentInteractonStateHandler
mMagnifiedContentInteractonStateHandler
private final StateViewportDraggingHandler
mStateViewportDraggingHandler
private final AccessibilityManagerService
mAms
private final int
mTapTimeSlop
private final int
mMultiTapTimeSlop
private final int
mTapDistanceSlop
private final int
mMultiTapDistanceSlop
private final long
mLongAnimationDuration
private final android.graphics.Region
mMagnifiedBounds
private EventStreamTransformation
mNext
private int
mCurrentState
private int
mPreviousState
private boolean
mTranslationEnabledBeforePan
private android.view.MotionEvent.PointerCoords[]
mTempPointerCoords
private android.view.MotionEvent.PointerProperties[]
mTempPointerProperties
private long
mDelegatingStateDownTime
private boolean
mUpdateMagnificationSpecOnNextBoundsChange
private final android.os.Handler
mHandler
Constructors Summary
public ScreenMagnifier(android.content.Context context, int displayId, AccessibilityManagerService service)


           
        mContext = context;
        mWindowManager = LocalServices.getService(WindowManagerInternal.class);
        mAms = service;

        mLongAnimationDuration = context.getResources().getInteger(
                com.android.internal.R.integer.config_longAnimTime);
        mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();

        mDetectingStateHandler = new DetectingStateHandler();
        mStateViewportDraggingHandler = new StateViewportDraggingHandler();
        mMagnifiedContentInteractonStateHandler = new MagnifiedContentInteractonStateHandler(
                context);

        mMagnificationController = new MagnificationController(mLongAnimationDuration);
        mScreenStateObserver = new ScreenStateObserver(context, mMagnificationController);

        mWindowManager.setMagnificationCallbacks(this);

        transitionToState(STATE_DETECTING);
    
Methods Summary
public voidclear()

        mCurrentState = STATE_DETECTING;
        mDetectingStateHandler.clear();
        mStateViewportDraggingHandler.clear();
        mMagnifiedContentInteractonStateHandler.clear();
        if (mNext != null) {
            mNext.clear();
        }
    
private voidgetMagnifiedFrameInContentCoords(android.graphics.Rect rect)

        MagnificationSpec spec = mMagnificationController.getMagnificationSpec();
        mMagnifiedBounds.getBounds(rect);
        rect.offset((int) -spec.offsetX, (int) -spec.offsetY);
        rect.scale(1.0f / spec.scale);
    
private floatgetPersistedScale()

        return Settings.Secure.getFloat(mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
                DEFAULT_MAGNIFICATION_SCALE);
    
private android.view.MotionEvent.PointerCoords[]getTempPointerCoordsWithMinSize(int size)

        final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0;
        if (oldSize < size) {
            PointerCoords[] oldTempPointerCoords = mTempPointerCoords;
            mTempPointerCoords = new PointerCoords[size];
            if (oldTempPointerCoords != null) {
                System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize);
            }
        }
        for (int i = oldSize; i < size; i++) {
            mTempPointerCoords[i] = new PointerCoords();
        }
        return mTempPointerCoords;
    
private android.view.MotionEvent.PointerProperties[]getTempPointerPropertiesWithMinSize(int size)

        final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length : 0;
        if (oldSize < size) {
            PointerProperties[] oldTempPointerProperties = mTempPointerProperties;
            mTempPointerProperties = new PointerProperties[size];
            if (oldTempPointerProperties != null) {
                System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, oldSize);
            }
        }
        for (int i = oldSize; i < size; i++) {
            mTempPointerProperties[i] = new PointerProperties();
        }
        return mTempPointerProperties;
    
private voidhandleMotionEventStateDelegating(android.view.MotionEvent event, android.view.MotionEvent rawEvent, int policyFlags)

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                mDelegatingStateDownTime = event.getDownTime();
            } break;
            case MotionEvent.ACTION_UP: {
                if (mDetectingStateHandler.mDelayedEventQueue == null) {
                    transitionToState(STATE_DETECTING);
                }
            } break;
        }
        if (mNext != null) {
            // If the event is within the magnified portion of the screen we have
            // to change its location to be where the user thinks he is poking the
            // UI which may have been magnified and panned.
            final float eventX = event.getX();
            final float eventY = event.getY();
            if (mMagnificationController.isMagnifying()
                    && mMagnifiedBounds.contains((int) eventX, (int) eventY)) {
                final float scale = mMagnificationController.getScale();
                final float scaledOffsetX = mMagnificationController.getOffsetX();
                final float scaledOffsetY = mMagnificationController.getOffsetY();
                final int pointerCount = event.getPointerCount();
                PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount);
                PointerProperties[] properties = getTempPointerPropertiesWithMinSize(pointerCount);
                for (int i = 0; i < pointerCount; i++) {
                    event.getPointerCoords(i, coords[i]);
                    coords[i].x = (coords[i].x - scaledOffsetX) / scale;
                    coords[i].y = (coords[i].y - scaledOffsetY) / scale;
                    event.getPointerProperties(i, properties[i]);
                }
                event = MotionEvent.obtain(event.getDownTime(),
                        event.getEventTime(), event.getAction(), pointerCount, properties,
                        coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(),
                        event.getFlags());
            }
            // We cache some events to see if the user wants to trigger magnification.
            // If no magnification is triggered we inject these events with adjusted
            // time and down time to prevent subsequent transformations being confused
            // by stale events. After the cached events, which always have a down, are
            // injected we need to also update the down time of all subsequent non cached
            // events. All delegated events cached and non-cached are delivered here.
            event.setDownTime(mDelegatingStateDownTime);
            mNext.onMotionEvent(event, rawEvent, policyFlags);
        }
    
private voidhandleOnMagnifiedBoundsChanged(android.graphics.Region bounds)

        // If there was a rotation we have to update the center of the magnified
        // region since the old offset X/Y may be out of its acceptable range for
        // the new display width and height.
        if (mUpdateMagnificationSpecOnNextBoundsChange) {
            mUpdateMagnificationSpecOnNextBoundsChange = false;
            MagnificationSpec spec = mMagnificationController.getMagnificationSpec();
            Rect magnifiedFrame = mTempRect;
            mMagnifiedBounds.getBounds(magnifiedFrame);
            final float scale = spec.scale;
            final float centerX = (-spec.offsetX + magnifiedFrame.width() / 2) / scale;
            final float centerY = (-spec.offsetY + magnifiedFrame.height() / 2) / scale;
            mMagnificationController.setScaleAndMagnifiedRegionCenter(scale, centerX,
                    centerY, false);
        }
        mMagnifiedBounds.set(bounds);
        mAms.onMagnificationStateChanged();
    
private voidhandleOnRectangleOnScreenRequested(int left, int top, int right, int bottom)

        Rect magnifiedFrame = mTempRect;
        mMagnifiedBounds.getBounds(magnifiedFrame);
        if (!magnifiedFrame.intersects(left, top, right, bottom)) {
            return;
        }
        Rect magnifFrameInScreenCoords = mTempRect1;
        getMagnifiedFrameInContentCoords(magnifFrameInScreenCoords);
        final float scrollX;
        final float scrollY;
        if (right - left > magnifFrameInScreenCoords.width()) {
            final int direction = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault());
            if (direction == View.LAYOUT_DIRECTION_LTR) {
                scrollX = left - magnifFrameInScreenCoords.left;
            } else {
                scrollX = right - magnifFrameInScreenCoords.right;
            }
        } else if (left < magnifFrameInScreenCoords.left) {
            scrollX = left - magnifFrameInScreenCoords.left;
        } else if (right > magnifFrameInScreenCoords.right) {
            scrollX = right - magnifFrameInScreenCoords.right;
        } else {
            scrollX = 0;
        }
        if (bottom - top > magnifFrameInScreenCoords.height()) {
            scrollY = top - magnifFrameInScreenCoords.top;
        } else if (top < magnifFrameInScreenCoords.top) {
            scrollY = top - magnifFrameInScreenCoords.top;
        } else if (bottom > magnifFrameInScreenCoords.bottom) {
            scrollY = bottom - magnifFrameInScreenCoords.bottom;
        } else {
            scrollY = 0;
        }
        final float scale = mMagnificationController.getScale();
        mMagnificationController.offsetMagnifiedRegionCenter(scrollX * scale, scrollY * scale);
    
private voidhandleOnRotationChanged(int rotation)

        resetMagnificationIfNeeded();
        if (mMagnificationController.isMagnifying()) {
            mUpdateMagnificationSpecOnNextBoundsChange = true;
        }
    
private voidhandleOnUserContextChanged()

        resetMagnificationIfNeeded();
    
private static booleanisScreenMagnificationAutoUpdateEnabled(android.content.Context context)

        return (Settings.Secure.getInt(context.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_AUTO_UPDATE,
                DEFAULT_SCREEN_MAGNIFICATION_AUTO_UPDATE) == 1);
    
public voidonAccessibilityEvent(android.view.accessibility.AccessibilityEvent event)

        if (mNext != null) {
            mNext.onAccessibilityEvent(event);
        }
    
public voidonDestroy()

        mScreenStateObserver.destroy();
        mWindowManager.setMagnificationCallbacks(null);
    
public voidonMagnifedBoundsChanged(android.graphics.Region bounds)

        Region newBounds = Region.obtain(bounds);
        mHandler.obtainMessage(MESSAGE_ON_MAGNIFIED_BOUNDS_CHANGED, newBounds).sendToTarget();
        if (MY_PID != Binder.getCallingPid()) {
            bounds.recycle();
        }
    
public voidonMotionEvent(android.view.MotionEvent event, android.view.MotionEvent rawEvent, int policyFlags)

        mMagnifiedContentInteractonStateHandler.onMotionEvent(event);
        switch (mCurrentState) {
            case STATE_DELEGATING: {
                handleMotionEventStateDelegating(event, rawEvent, policyFlags);
            } break;
            case STATE_DETECTING: {
                mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags);
            } break;
            case STATE_VIEWPORT_DRAGGING: {
                mStateViewportDraggingHandler.onMotionEvent(event, policyFlags);
            } break;
            case STATE_MAGNIFIED_INTERACTION: {
                // mMagnifiedContentInteractonStateHandler handles events only
                // if this is the current state since it uses ScaleGestureDetecotr
                // and a GestureDetector which need well formed event stream.
            } break;
            default: {
                throw new IllegalStateException("Unknown state: " + mCurrentState);
            }
        }
    
public voidonRectangleOnScreenRequested(int left, int top, int right, int bottom)

        SomeArgs args = SomeArgs.obtain();
        args.argi1 = left;
        args.argi2 = top;
        args.argi3 = right;
        args.argi4 = bottom;
        mHandler.obtainMessage(MESSAGE_ON_RECTANGLE_ON_SCREEN_REQUESTED, args).sendToTarget();
    
public voidonRotationChanged(int rotation)

        mHandler.obtainMessage(MESSAGE_ON_ROTATION_CHANGED, rotation, 0).sendToTarget();
    
public voidonUserContextChanged()

        mHandler.sendEmptyMessage(MESSAGE_ON_USER_CONTEXT_CHANGED);
    
private voidpersistScale(float scale)

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                Settings.Secure.putFloat(mContext.getContentResolver(),
                        Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale);
                return null;
            }
        }.execute();
    
private voidresetMagnificationIfNeeded()

        if (mMagnificationController.isMagnifying()
                && isScreenMagnificationAutoUpdateEnabled(mContext)) {
            mMagnificationController.reset(true);
        }
    
public voidsetNext(EventStreamTransformation next)

        mNext = next;
    
private voidtransitionToState(int state)

        if (DEBUG_STATE_TRANSITIONS) {
            switch (state) {
                case STATE_DELEGATING: {
                    Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING");
                } break;
                case STATE_DETECTING: {
                    Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING");
                } break;
                case STATE_VIEWPORT_DRAGGING: {
                    Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING");
                } break;
                case STATE_MAGNIFIED_INTERACTION: {
                    Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION");
                } break;
                default: {
                    throw new IllegalArgumentException("Unknown state: " + state);
                }
            }
        }
        mPreviousState = mCurrentState;
        mCurrentState = state;