1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.accessibility;
18
19import com.android.internal.R;
20import com.android.internal.annotations.GuardedBy;
21import com.android.internal.os.SomeArgs;
22import com.android.server.LocalServices;
23
24import android.animation.ValueAnimator;
25import android.annotation.NonNull;
26import android.content.BroadcastReceiver;
27import android.content.ContentResolver;
28import android.content.Context;
29import android.content.Intent;
30import android.content.IntentFilter;
31import android.graphics.Rect;
32import android.graphics.Region;
33import android.os.AsyncTask;
34import android.os.Handler;
35import android.os.Message;
36import android.provider.Settings;
37import android.text.TextUtils;
38import android.util.MathUtils;
39import android.util.Slog;
40import android.view.MagnificationSpec;
41import android.view.View;
42import android.view.WindowManagerInternal;
43import android.view.animation.DecelerateInterpolator;
44
45import java.util.Locale;
46
47/**
48 * This class is used to control and query the state of display magnification
49 * from the accessibility manager and related classes. It is responsible for
50 * holding the current state of magnification and animation, and it handles
51 * communication between the accessibility manager and window manager.
52 *
53 * Magnification is limited to the range [MIN_SCALE, MAX_SCALE], and can only occur inside the
54 * magnification region. If a value is out of bounds, it will be adjusted to guarantee these
55 * constraints.
56 */
57class MagnificationController implements Handler.Callback {
58    private static final String LOG_TAG = "MagnificationController";
59
60    public static final float MIN_SCALE = 1.0f;
61    public static final float MAX_SCALE = 5.0f;
62
63    private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;
64
65    private static final int INVALID_ID = -1;
66
67    private static final float DEFAULT_MAGNIFICATION_SCALE = 2.0f;
68
69    // Messages
70    private static final int MSG_SEND_SPEC_TO_ANIMATION = 1;
71    private static final int MSG_SCREEN_TURNED_OFF = 2;
72    private static final int MSG_ON_MAGNIFIED_BOUNDS_CHANGED = 3;
73    private static final int MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED = 4;
74    private static final int MSG_ON_USER_CONTEXT_CHANGED = 5;
75
76    private final Object mLock;
77
78    /**
79     * The current magnification spec. If an animation is running, this
80     * reflects the end state.
81     */
82    private final MagnificationSpec mCurrentMagnificationSpec = MagnificationSpec.obtain();
83
84    private final Region mMagnificationRegion = Region.obtain();
85    private final Rect mMagnificationBounds = new Rect();
86
87    private final Rect mTempRect = new Rect();
88    private final Rect mTempRect1 = new Rect();
89
90    private final AccessibilityManagerService mAms;
91
92    private final SettingsBridge mSettingsBridge;
93
94    private final ScreenStateObserver mScreenStateObserver;
95
96    private final SpecAnimationBridge mSpecAnimationBridge;
97
98    private final WindowManagerInternal.MagnificationCallbacks mWMCallbacks =
99            new WindowManagerInternal.MagnificationCallbacks () {
100                @Override
101                public void onMagnificationRegionChanged(Region region) {
102                    final SomeArgs args = SomeArgs.obtain();
103                    args.arg1 = Region.obtain(region);
104                    mHandler.obtainMessage(MSG_ON_MAGNIFIED_BOUNDS_CHANGED, args).sendToTarget();
105                }
106
107                @Override
108                public void onRectangleOnScreenRequested(int left, int top, int right, int bottom) {
109                    final SomeArgs args = SomeArgs.obtain();
110                    args.argi1 = left;
111                    args.argi2 = top;
112                    args.argi3 = right;
113                    args.argi4 = bottom;
114                    mHandler.obtainMessage(MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED, args)
115                            .sendToTarget();
116                }
117
118                @Override
119                public void onRotationChanged(int rotation) {
120                    // Treat as context change and reset
121                    mHandler.sendEmptyMessage(MSG_ON_USER_CONTEXT_CHANGED);
122                }
123
124                @Override
125                public void onUserContextChanged() {
126                    mHandler.sendEmptyMessage(MSG_ON_USER_CONTEXT_CHANGED);
127                }
128            };
129
130    private int mUserId;
131
132    private final long mMainThreadId;
133
134    private Handler mHandler;
135
136    private int mIdOfLastServiceToMagnify = INVALID_ID;
137
138    private final WindowManagerInternal mWindowManager;
139
140    // Flag indicating that we are registered with window manager.
141    private boolean mRegistered;
142
143    private boolean mUnregisterPending;
144
145    public MagnificationController(Context context, AccessibilityManagerService ams, Object lock) {
146        this(context, ams, lock, null, LocalServices.getService(WindowManagerInternal.class),
147                new ValueAnimator(), new SettingsBridge(context.getContentResolver()));
148        mHandler = new Handler(context.getMainLooper(), this);
149    }
150
151    public MagnificationController(Context context, AccessibilityManagerService ams, Object lock,
152            Handler handler, WindowManagerInternal windowManagerInternal,
153            ValueAnimator valueAnimator, SettingsBridge settingsBridge) {
154        mHandler = handler;
155        mWindowManager = windowManagerInternal;
156        mMainThreadId = context.getMainLooper().getThread().getId();
157        mAms = ams;
158        mScreenStateObserver = new ScreenStateObserver(context, this);
159        mLock = lock;
160        mSpecAnimationBridge = new SpecAnimationBridge(
161                context, mLock, mWindowManager, valueAnimator);
162        mSettingsBridge = settingsBridge;
163    }
164
165    /**
166     * Start tracking the magnification region for services that control magnification and the
167     * magnification gesture handler.
168     *
169     * This tracking imposes a cost on the system, so we avoid tracking this data unless it's
170     * required.
171     */
172    public void register() {
173        synchronized (mLock) {
174            if (!mRegistered) {
175                mScreenStateObserver.register();
176                mWindowManager.setMagnificationCallbacks(mWMCallbacks);
177                mSpecAnimationBridge.setEnabled(true);
178                // Obtain initial state.
179                mWindowManager.getMagnificationRegion(mMagnificationRegion);
180                mMagnificationRegion.getBounds(mMagnificationBounds);
181                mRegistered = true;
182            }
183        }
184    }
185
186    /**
187     * Stop requiring tracking the magnification region. We may remain registered while we
188     * reset magnification.
189     */
190    public void unregister() {
191        synchronized (mLock) {
192            if (!isMagnifying()) {
193                unregisterInternalLocked();
194            } else {
195                mUnregisterPending = true;
196                resetLocked(true);
197            }
198        }
199    }
200
201    /**
202     * Check if we are registered. Note that we may be planning to unregister at any moment.
203     *
204     * @return {@code true} if the controller is registered. {@code false} otherwise.
205     */
206    public boolean isRegisteredLocked() {
207        return mRegistered;
208    }
209
210    private void unregisterInternalLocked() {
211        if (mRegistered) {
212            mSpecAnimationBridge.setEnabled(false);
213            mScreenStateObserver.unregister();
214            mWindowManager.setMagnificationCallbacks(null);
215            mMagnificationRegion.setEmpty();
216            mRegistered = false;
217        }
218        mUnregisterPending = false;
219    }
220
221    /**
222     * @return {@code true} if magnification is active, e.g. the scale
223     *         is > 1, {@code false} otherwise
224     */
225    public boolean isMagnifying() {
226        return mCurrentMagnificationSpec.scale > 1.0f;
227    }
228
229    /**
230     * Update our copy of the current magnification region
231     *
232     * @param magnified the magnified region
233     */
234    private void onMagnificationRegionChanged(Region magnified) {
235        synchronized (mLock) {
236            if (!mRegistered) {
237                // Don't update if we've unregistered
238                return;
239            }
240            if (!mMagnificationRegion.equals(magnified)) {
241                mMagnificationRegion.set(magnified);
242                mMagnificationRegion.getBounds(mMagnificationBounds);
243                // It's possible that our magnification spec is invalid with the new bounds.
244                // Adjust the current spec's offsets if necessary.
245                if (updateCurrentSpecWithOffsetsLocked(
246                        mCurrentMagnificationSpec.offsetX, mCurrentMagnificationSpec.offsetY)) {
247                    sendSpecToAnimation(mCurrentMagnificationSpec, false);
248                }
249                onMagnificationChangedLocked();
250            }
251        }
252    }
253
254    /**
255     * Returns whether the magnification region contains the specified
256     * screen-relative coordinates.
257     *
258     * @param x the screen-relative X coordinate to check
259     * @param y the screen-relative Y coordinate to check
260     * @return {@code true} if the coordinate is contained within the
261     *         magnified region, or {@code false} otherwise
262     */
263    public boolean magnificationRegionContains(float x, float y) {
264        synchronized (mLock) {
265            return mMagnificationRegion.contains((int) x, (int) y);
266        }
267    }
268
269    /**
270     * Populates the specified rect with the screen-relative bounds of the
271     * magnification region. If magnification is not enabled, the returned
272     * bounds will be empty.
273     *
274     * @param outBounds rect to populate with the bounds of the magnified
275     *                  region
276     */
277    public void getMagnificationBounds(@NonNull Rect outBounds) {
278        synchronized (mLock) {
279            outBounds.set(mMagnificationBounds);
280        }
281    }
282
283    /**
284     * Populates the specified region with the screen-relative magnification
285     * region. If magnification is not enabled, then the returned region
286     * will be empty.
287     *
288     * @param outRegion the region to populate
289     */
290    public void getMagnificationRegion(@NonNull Region outRegion) {
291        synchronized (mLock) {
292            outRegion.set(mMagnificationRegion);
293        }
294    }
295
296    /**
297     * Returns the magnification scale. If an animation is in progress,
298     * this reflects the end state of the animation.
299     *
300     * @return the scale
301     */
302    public float getScale() {
303        return mCurrentMagnificationSpec.scale;
304    }
305
306    /**
307     * Returns the X offset of the magnification viewport. If an animation
308     * is in progress, this reflects the end state of the animation.
309     *
310     * @return the X offset
311     */
312    public float getOffsetX() {
313        return mCurrentMagnificationSpec.offsetX;
314    }
315
316
317    /**
318     * Returns the screen-relative X coordinate of the center of the
319     * magnification viewport.
320     *
321     * @return the X coordinate
322     */
323    public float getCenterX() {
324        synchronized (mLock) {
325            return (mMagnificationBounds.width() / 2.0f
326                    + mMagnificationBounds.left - getOffsetX()) / getScale();
327        }
328    }
329
330    /**
331     * Returns the Y offset of the magnification viewport. If an animation
332     * is in progress, this reflects the end state of the animation.
333     *
334     * @return the Y offset
335     */
336    public float getOffsetY() {
337        return mCurrentMagnificationSpec.offsetY;
338    }
339
340    /**
341     * Returns the screen-relative Y coordinate of the center of the
342     * magnification viewport.
343     *
344     * @return the Y coordinate
345     */
346    public float getCenterY() {
347        synchronized (mLock) {
348            return (mMagnificationBounds.height() / 2.0f
349                    + mMagnificationBounds.top - getOffsetY()) / getScale();
350        }
351    }
352
353    /**
354     * Returns the scale currently used by the window manager. If an
355     * animation is in progress, this reflects the current state of the
356     * animation.
357     *
358     * @return the scale currently used by the window manager
359     */
360    private float getSentScale() {
361        return mSpecAnimationBridge.mSentMagnificationSpec.scale;
362    }
363
364    /**
365     * Returns the X offset currently used by the window manager. If an
366     * animation is in progress, this reflects the current state of the
367     * animation.
368     *
369     * @return the X offset currently used by the window manager
370     */
371    private float getSentOffsetX() {
372        return mSpecAnimationBridge.mSentMagnificationSpec.offsetX;
373    }
374
375    /**
376     * Returns the Y offset currently used by the window manager. If an
377     * animation is in progress, this reflects the current state of the
378     * animation.
379     *
380     * @return the Y offset currently used by the window manager
381     */
382    private float getSentOffsetY() {
383        return mSpecAnimationBridge.mSentMagnificationSpec.offsetY;
384    }
385
386    /**
387     * Resets the magnification scale and center, optionally animating the
388     * transition.
389     *
390     * @param animate {@code true} to animate the transition, {@code false}
391     *                to transition immediately
392     * @return {@code true} if the magnification spec changed, {@code false} if
393     *         the spec did not change
394     */
395    public boolean reset(boolean animate) {
396        synchronized (mLock) {
397            return resetLocked(animate);
398        }
399    }
400
401    private boolean resetLocked(boolean animate) {
402        if (!mRegistered) {
403            return false;
404        }
405        final MagnificationSpec spec = mCurrentMagnificationSpec;
406        final boolean changed = !spec.isNop();
407        if (changed) {
408            spec.clear();
409            onMagnificationChangedLocked();
410        }
411        mIdOfLastServiceToMagnify = INVALID_ID;
412        sendSpecToAnimation(spec, animate);
413        return changed;
414    }
415
416    /**
417     * Scales the magnified region around the specified pivot point,
418     * optionally animating the transition. If animation is disabled, the
419     * transition is immediate.
420     *
421     * @param scale the target scale, must be >= 1
422     * @param pivotX the screen-relative X coordinate around which to scale
423     * @param pivotY the screen-relative Y coordinate around which to scale
424     * @param animate {@code true} to animate the transition, {@code false}
425     *                to transition immediately
426     * @param id the ID of the service requesting the change
427     * @return {@code true} if the magnification spec changed, {@code false} if
428     *         the spec did not change
429     */
430    public boolean setScale(float scale, float pivotX, float pivotY, boolean animate, int id) {
431        synchronized (mLock) {
432            if (!mRegistered) {
433                return false;
434            }
435            // Constrain scale immediately for use in the pivot calculations.
436            scale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
437
438            final Rect viewport = mTempRect;
439            mMagnificationRegion.getBounds(viewport);
440            final MagnificationSpec spec = mCurrentMagnificationSpec;
441            final float oldScale = spec.scale;
442            final float oldCenterX = (viewport.width() / 2.0f - spec.offsetX) / oldScale;
443            final float oldCenterY = (viewport.height() / 2.0f - spec.offsetY) / oldScale;
444            final float normPivotX = (pivotX - spec.offsetX) / oldScale;
445            final float normPivotY = (pivotY - spec.offsetY) / oldScale;
446            final float offsetX = (oldCenterX - normPivotX) * (oldScale / scale);
447            final float offsetY = (oldCenterY - normPivotY) * (oldScale / scale);
448            final float centerX = normPivotX + offsetX;
449            final float centerY = normPivotY + offsetY;
450            mIdOfLastServiceToMagnify = id;
451            return setScaleAndCenterLocked(scale, centerX, centerY, animate, id);
452        }
453    }
454
455    /**
456     * Sets the center of the magnified region, optionally animating the
457     * transition. If animation is disabled, the transition is immediate.
458     *
459     * @param centerX the screen-relative X coordinate around which to
460     *                center
461     * @param centerY the screen-relative Y coordinate around which to
462     *                center
463     * @param animate {@code true} to animate the transition, {@code false}
464     *                to transition immediately
465     * @param id the ID of the service requesting the change
466     * @return {@code true} if the magnification spec changed, {@code false} if
467     *         the spec did not change
468     */
469    public boolean setCenter(float centerX, float centerY, boolean animate, int id) {
470        synchronized (mLock) {
471            if (!mRegistered) {
472                return false;
473            }
474            return setScaleAndCenterLocked(Float.NaN, centerX, centerY, animate, id);
475        }
476    }
477
478    /**
479     * Sets the scale and center of the magnified region, optionally
480     * animating the transition. If animation is disabled, the transition
481     * is immediate.
482     *
483     * @param scale the target scale, or {@link Float#NaN} to leave unchanged
484     * @param centerX the screen-relative X coordinate around which to
485     *                center and scale, or {@link Float#NaN} to leave unchanged
486     * @param centerY the screen-relative Y coordinate around which to
487     *                center and scale, or {@link Float#NaN} to leave unchanged
488     * @param animate {@code true} to animate the transition, {@code false}
489     *                to transition immediately
490     * @param id the ID of the service requesting the change
491     * @return {@code true} if the magnification spec changed, {@code false} if
492     *         the spec did not change
493     */
494    public boolean setScaleAndCenter(
495            float scale, float centerX, float centerY, boolean animate, int id) {
496        synchronized (mLock) {
497            if (!mRegistered) {
498                return false;
499            }
500            return setScaleAndCenterLocked(scale, centerX, centerY, animate, id);
501        }
502    }
503
504    private boolean setScaleAndCenterLocked(float scale, float centerX, float centerY,
505            boolean animate, int id) {
506        final boolean changed = updateMagnificationSpecLocked(scale, centerX, centerY);
507        sendSpecToAnimation(mCurrentMagnificationSpec, animate);
508        if (isMagnifying() && (id != INVALID_ID)) {
509            mIdOfLastServiceToMagnify = id;
510        }
511        return changed;
512    }
513
514    /**
515     * Offsets the magnified region. Note that the offsetX and offsetY values actually move in the
516     * opposite direction as the offsets passed in here.
517     *
518     * @param offsetX the amount in pixels to offset the region in the X direction, in current
519     * screen pixels.
520     * @param offsetY the amount in pixels to offset the region in the Y direction, in current
521     * screen pixels.
522     * @param id the ID of the service requesting the change
523     */
524    public void offsetMagnifiedRegion(float offsetX, float offsetY, int id) {
525        synchronized (mLock) {
526            if (!mRegistered) {
527                return;
528            }
529
530            final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX;
531            final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY;
532            updateCurrentSpecWithOffsetsLocked(nonNormOffsetX, nonNormOffsetY);
533            if (id != INVALID_ID) {
534                mIdOfLastServiceToMagnify = id;
535            }
536            sendSpecToAnimation(mCurrentMagnificationSpec, false);
537        }
538    }
539
540    /**
541     * Get the ID of the last service that changed the magnification spec.
542     *
543     * @return The id
544     */
545    public int getIdOfLastServiceToMagnify() {
546        return mIdOfLastServiceToMagnify;
547    }
548
549    private void onMagnificationChangedLocked() {
550        mAms.notifyMagnificationChanged(mMagnificationRegion,
551                getScale(), getCenterX(), getCenterY());
552        if (mUnregisterPending && !isMagnifying()) {
553            unregisterInternalLocked();
554        }
555    }
556
557    /**
558     * Persists the current magnification scale to the current user's settings.
559     */
560    public void persistScale() {
561        final float scale = mCurrentMagnificationSpec.scale;
562        final int userId = mUserId;
563
564        new AsyncTask<Void, Void, Void>() {
565            @Override
566            protected Void doInBackground(Void... params) {
567                mSettingsBridge.putMagnificationScale(scale, userId);
568                return null;
569            }
570        }.execute();
571    }
572
573    /**
574     * Retrieves a previously persisted magnification scale from the current
575     * user's settings.
576     *
577     * @return the previously persisted magnification scale, or the default
578     *         scale if none is available
579     */
580    public float getPersistedScale() {
581        return mSettingsBridge.getMagnificationScale(mUserId);
582    }
583
584    /**
585     * Updates the current magnification spec.
586     *
587     * @param scale the magnification scale
588     * @param centerX the unscaled, screen-relative X coordinate of the center
589     *                of the viewport, or {@link Float#NaN} to leave unchanged
590     * @param centerY the unscaled, screen-relative Y coordinate of the center
591     *                of the viewport, or {@link Float#NaN} to leave unchanged
592     * @return {@code true} if the magnification spec changed or {@code false}
593     *         otherwise
594     */
595    private boolean updateMagnificationSpecLocked(float scale, float centerX, float centerY) {
596        // Handle defaults.
597        if (Float.isNaN(centerX)) {
598            centerX = getCenterX();
599        }
600        if (Float.isNaN(centerY)) {
601            centerY = getCenterY();
602        }
603        if (Float.isNaN(scale)) {
604            scale = getScale();
605        }
606
607        // Compute changes.
608        boolean changed = false;
609
610        final float normScale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE);
611        if (Float.compare(mCurrentMagnificationSpec.scale, normScale) != 0) {
612            mCurrentMagnificationSpec.scale = normScale;
613            changed = true;
614        }
615
616        final float nonNormOffsetX = mMagnificationBounds.width() / 2.0f
617                + mMagnificationBounds.left - centerX * normScale;
618        final float nonNormOffsetY = mMagnificationBounds.height() / 2.0f
619                + mMagnificationBounds.top - centerY * normScale;
620        changed |= updateCurrentSpecWithOffsetsLocked(nonNormOffsetX, nonNormOffsetY);
621
622        if (changed) {
623            onMagnificationChangedLocked();
624        }
625
626        return changed;
627    }
628
629    private boolean updateCurrentSpecWithOffsetsLocked(float nonNormOffsetX, float nonNormOffsetY) {
630        boolean changed = false;
631        final float offsetX = MathUtils.constrain(nonNormOffsetX, getMinOffsetXLocked(), 0);
632        if (Float.compare(mCurrentMagnificationSpec.offsetX, offsetX) != 0) {
633            mCurrentMagnificationSpec.offsetX = offsetX;
634            changed = true;
635        }
636        final float offsetY = MathUtils.constrain(nonNormOffsetY, getMinOffsetYLocked(), 0);
637        if (Float.compare(mCurrentMagnificationSpec.offsetY, offsetY) != 0) {
638            mCurrentMagnificationSpec.offsetY = offsetY;
639            changed = true;
640        }
641        return changed;
642    }
643
644    private float getMinOffsetXLocked() {
645        final float viewportWidth = mMagnificationBounds.width();
646        return viewportWidth - viewportWidth * mCurrentMagnificationSpec.scale;
647    }
648
649    private float getMinOffsetYLocked() {
650        final float viewportHeight = mMagnificationBounds.height();
651        return viewportHeight - viewportHeight * mCurrentMagnificationSpec.scale;
652    }
653
654    /**
655     * Sets the currently active user ID.
656     *
657     * @param userId the currently active user ID
658     */
659    public void setUserId(int userId) {
660        if (mUserId != userId) {
661            mUserId = userId;
662
663            synchronized (mLock) {
664                if (isMagnifying()) {
665                    reset(false);
666                }
667            }
668        }
669    }
670
671    /**
672     * Resets magnification if magnification and auto-update are both enabled.
673     *
674     * @param animate whether the animate the transition
675     * @return {@code true} if magnification was reset to the disabled state,
676     *         {@code false} if magnification is still active
677     */
678    boolean resetIfNeeded(boolean animate) {
679        synchronized (mLock) {
680            if (isMagnifying()) {
681                reset(animate);
682                return true;
683            }
684            return false;
685        }
686    }
687
688    void setForceShowMagnifiableBounds(boolean show) {
689        if (mRegistered) {
690            mWindowManager.setForceShowMagnifiableBounds(show);
691        }
692    }
693
694    private void getMagnifiedFrameInContentCoordsLocked(Rect outFrame) {
695        final float scale = getSentScale();
696        final float offsetX = getSentOffsetX();
697        final float offsetY = getSentOffsetY();
698        getMagnificationBounds(outFrame);
699        outFrame.offset((int) -offsetX, (int) -offsetY);
700        outFrame.scale(1.0f / scale);
701    }
702
703    private void requestRectangleOnScreen(int left, int top, int right, int bottom) {
704        synchronized (mLock) {
705            final Rect magnifiedFrame = mTempRect;
706            getMagnificationBounds(magnifiedFrame);
707            if (!magnifiedFrame.intersects(left, top, right, bottom)) {
708                return;
709            }
710
711            final Rect magnifFrameInScreenCoords = mTempRect1;
712            getMagnifiedFrameInContentCoordsLocked(magnifFrameInScreenCoords);
713
714            final float scrollX;
715            final float scrollY;
716            if (right - left > magnifFrameInScreenCoords.width()) {
717                final int direction = TextUtils
718                        .getLayoutDirectionFromLocale(Locale.getDefault());
719                if (direction == View.LAYOUT_DIRECTION_LTR) {
720                    scrollX = left - magnifFrameInScreenCoords.left;
721                } else {
722                    scrollX = right - magnifFrameInScreenCoords.right;
723                }
724            } else if (left < magnifFrameInScreenCoords.left) {
725                scrollX = left - magnifFrameInScreenCoords.left;
726            } else if (right > magnifFrameInScreenCoords.right) {
727                scrollX = right - magnifFrameInScreenCoords.right;
728            } else {
729                scrollX = 0;
730            }
731
732            if (bottom - top > magnifFrameInScreenCoords.height()) {
733                scrollY = top - magnifFrameInScreenCoords.top;
734            } else if (top < magnifFrameInScreenCoords.top) {
735                scrollY = top - magnifFrameInScreenCoords.top;
736            } else if (bottom > magnifFrameInScreenCoords.bottom) {
737                scrollY = bottom - magnifFrameInScreenCoords.bottom;
738            } else {
739                scrollY = 0;
740            }
741
742            final float scale = getScale();
743            offsetMagnifiedRegion(scrollX * scale, scrollY * scale, INVALID_ID);
744        }
745    }
746
747    private void sendSpecToAnimation(MagnificationSpec spec, boolean animate) {
748        if (Thread.currentThread().getId() == mMainThreadId) {
749            mSpecAnimationBridge.updateSentSpecMainThread(spec, animate);
750        } else {
751            mHandler.obtainMessage(MSG_SEND_SPEC_TO_ANIMATION,
752                    animate ? 1 : 0, 0, spec).sendToTarget();
753        }
754    }
755
756    private void onScreenTurnedOff() {
757        mHandler.sendEmptyMessage(MSG_SCREEN_TURNED_OFF);
758    }
759
760    public boolean handleMessage(Message msg) {
761        switch (msg.what) {
762            case MSG_SEND_SPEC_TO_ANIMATION:
763                final boolean animate = msg.arg1 == 1;
764                final MagnificationSpec spec = (MagnificationSpec) msg.obj;
765                mSpecAnimationBridge.updateSentSpecMainThread(spec, animate);
766                break;
767            case MSG_SCREEN_TURNED_OFF:
768                resetIfNeeded(false);
769                break;
770            case MSG_ON_MAGNIFIED_BOUNDS_CHANGED: {
771                final SomeArgs args = (SomeArgs) msg.obj;
772                final Region magnifiedBounds = (Region) args.arg1;
773                onMagnificationRegionChanged(magnifiedBounds);
774                magnifiedBounds.recycle();
775                args.recycle();
776            } break;
777            case MSG_ON_RECTANGLE_ON_SCREEN_REQUESTED: {
778                final SomeArgs args = (SomeArgs) msg.obj;
779                final int left = args.argi1;
780                final int top = args.argi2;
781                final int right = args.argi3;
782                final int bottom = args.argi4;
783                requestRectangleOnScreen(left, top, right, bottom);
784                args.recycle();
785            } break;
786            case MSG_ON_USER_CONTEXT_CHANGED:
787                resetIfNeeded(true);
788                break;
789        }
790        return true;
791    }
792
793    /**
794     * Class responsible for animating spec on the main thread and sending spec
795     * updates to the window manager.
796     */
797    private static class SpecAnimationBridge implements ValueAnimator.AnimatorUpdateListener {
798        private final WindowManagerInternal mWindowManager;
799
800        /**
801         * The magnification spec that was sent to the window manager. This should
802         * only be accessed with the lock held.
803         */
804        private final MagnificationSpec mSentMagnificationSpec = MagnificationSpec.obtain();
805
806        private final MagnificationSpec mStartMagnificationSpec = MagnificationSpec.obtain();
807
808        private final MagnificationSpec mEndMagnificationSpec = MagnificationSpec.obtain();
809
810        private final MagnificationSpec mTmpMagnificationSpec = MagnificationSpec.obtain();
811
812        /**
813         * The animator should only be accessed and modified on the main (e.g. animation) thread.
814         */
815        private final ValueAnimator mValueAnimator;
816
817        private final Object mLock;
818
819        @GuardedBy("mLock")
820        private boolean mEnabled = false;
821
822        private SpecAnimationBridge(Context context, Object lock, WindowManagerInternal wm,
823                ValueAnimator animator) {
824            mLock = lock;
825            mWindowManager = wm;
826            final long animationDuration = context.getResources().getInteger(
827                    R.integer.config_longAnimTime);
828            mValueAnimator = animator;
829            mValueAnimator.setDuration(animationDuration);
830            mValueAnimator.setInterpolator(new DecelerateInterpolator(2.5f));
831            mValueAnimator.setFloatValues(0.0f, 1.0f);
832            mValueAnimator.addUpdateListener(this);
833        }
834
835        /**
836         * Enabled means the bridge will accept input. When not enabled, the output of the animator
837         * will be ignored
838         */
839        public void setEnabled(boolean enabled) {
840            synchronized (mLock) {
841                if (enabled != mEnabled) {
842                    mEnabled = enabled;
843                    if (!mEnabled) {
844                        mSentMagnificationSpec.clear();
845                        mWindowManager.setMagnificationSpec(mSentMagnificationSpec);
846                    }
847                }
848            }
849        }
850
851        public void updateSentSpecMainThread(MagnificationSpec spec, boolean animate) {
852            if (mValueAnimator.isRunning()) {
853                mValueAnimator.cancel();
854            }
855
856            // If the current and sent specs don't match, update the sent spec.
857            synchronized (mLock) {
858                final boolean changed = !mSentMagnificationSpec.equals(spec);
859                if (changed) {
860                    if (animate) {
861                        animateMagnificationSpecLocked(spec);
862                    } else {
863                        setMagnificationSpecLocked(spec);
864                    }
865                }
866            }
867        }
868
869        private void setMagnificationSpecLocked(MagnificationSpec spec) {
870            if (mEnabled) {
871                if (DEBUG_SET_MAGNIFICATION_SPEC) {
872                    Slog.i(LOG_TAG, "Sending: " + spec);
873                }
874
875                mSentMagnificationSpec.setTo(spec);
876                mWindowManager.setMagnificationSpec(spec);
877            }
878        }
879
880        private void animateMagnificationSpecLocked(MagnificationSpec toSpec) {
881            mEndMagnificationSpec.setTo(toSpec);
882            mStartMagnificationSpec.setTo(mSentMagnificationSpec);
883            mValueAnimator.start();
884        }
885
886        @Override
887        public void onAnimationUpdate(ValueAnimator animation) {
888            synchronized (mLock) {
889                if (mEnabled) {
890                    float fract = animation.getAnimatedFraction();
891                    mTmpMagnificationSpec.scale = mStartMagnificationSpec.scale +
892                            (mEndMagnificationSpec.scale - mStartMagnificationSpec.scale) * fract;
893                    mTmpMagnificationSpec.offsetX = mStartMagnificationSpec.offsetX +
894                            (mEndMagnificationSpec.offsetX - mStartMagnificationSpec.offsetX)
895                                    * fract;
896                    mTmpMagnificationSpec.offsetY = mStartMagnificationSpec.offsetY +
897                            (mEndMagnificationSpec.offsetY - mStartMagnificationSpec.offsetY)
898                                    * fract;
899                    synchronized (mLock) {
900                        setMagnificationSpecLocked(mTmpMagnificationSpec);
901                    }
902                }
903            }
904        }
905    }
906
907    private static class ScreenStateObserver extends BroadcastReceiver {
908        private final Context mContext;
909        private final MagnificationController mController;
910
911        public ScreenStateObserver(Context context, MagnificationController controller) {
912            mContext = context;
913            mController = controller;
914        }
915
916        public void register() {
917            mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF));
918        }
919
920        public void unregister() {
921            mContext.unregisterReceiver(this);
922        }
923
924        @Override
925        public void onReceive(Context context, Intent intent) {
926            mController.onScreenTurnedOff();
927        }
928    }
929
930    // Extra class to get settings so tests can mock it
931    public static class SettingsBridge {
932        private final ContentResolver mContentResolver;
933
934        public SettingsBridge(ContentResolver contentResolver) {
935            mContentResolver = contentResolver;
936        }
937
938        public void putMagnificationScale(float value, int userId) {
939            Settings.Secure.putFloatForUser(mContentResolver,
940                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, value, userId);
941        }
942
943        public float getMagnificationScale(int userId) {
944            return Settings.Secure.getFloatForUser(mContentResolver,
945                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
946                    DEFAULT_MAGNIFICATION_SCALE, userId);
947        }
948    }
949}
950