1 /*
2  * Copyright (C) 2020 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.magnification;
18 
19 import static java.lang.Math.abs;
20 
21 import android.annotation.NonNull;
22 import android.annotation.UiContext;
23 import android.content.Context;
24 import android.os.Handler;
25 import android.util.Log;
26 import android.util.Slog;
27 import android.util.TypedValue;
28 import android.view.GestureDetector;
29 import android.view.MotionEvent;
30 import android.view.ScaleGestureDetector;
31 import android.view.ViewConfiguration;
32 
33 import com.android.internal.R;
34 import com.android.server.accessibility.Flags;
35 
36 /**
37  * Handles the behavior while receiving scaling and panning gestures if it's enabled.
38  * Note it needs to receives all touch events even it's not enabled.
39  */
40 
41 class PanningScalingHandler extends
42         GestureDetector.SimpleOnGestureListener
43         implements ScaleGestureDetector.OnScaleGestureListener {
44 
45     private static final String TAG = "PanningScalingHandler";
46     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
47 
48     // TODO(b/312372035): Revisit the scope of usage of the interface
49     interface MagnificationDelegate {
processScroll(int displayId, float distanceX, float distanceY)50         boolean processScroll(int displayId, float distanceX, float distanceY);
setScale(int displayId, float scale)51         void setScale(int displayId, float scale);
getScale(int displayId)52         float getScale(int displayId);
53     }
54 
55     private final ScaleGestureDetector mScaleGestureDetector;
56     private final GestureDetector mScrollGestureDetector;
57     private final MagnificationDelegate mMagnificationDelegate;
58     private final float mScalingThreshold;
59     private final float mMinScale;
60     private final float mMaxScale;
61     private final int mDisplayId;
62     private float mInitialScaleFactor = -1;
63     // Used to identify if need to disable onScroll once scaling operation is ongoing.
64     // We can remove it if we can fully distinguish these two gestures.
65     private final boolean mBlockScroll;
66 
67     private boolean mScaling;
68     private boolean mEnable;
69 
PanningScalingHandler(@iContext Context context, float maxScale, float minScale, boolean blockScroll, @NonNull MagnificationDelegate magnificationDelegate)70     PanningScalingHandler(@UiContext Context context, float maxScale, float minScale,
71             boolean blockScroll, @NonNull MagnificationDelegate magnificationDelegate) {
72         mDisplayId = context.getDisplayId();
73         mMaxScale = maxScale;
74         mMinScale = minScale;
75         mBlockScroll = blockScroll;
76         if (Flags.pinchZoomZeroMinSpan()) {
77             mScaleGestureDetector = new ScaleGestureDetector(context,
78                     ViewConfiguration.get(context).getScaledTouchSlop() * 2,
79                     /* minSpan= */ 0, Handler.getMain(), this);
80         } else {
81             mScaleGestureDetector = new ScaleGestureDetector(context, this, Handler.getMain());
82         }
83         mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
84         mScaleGestureDetector.setQuickScaleEnabled(false);
85         mMagnificationDelegate = magnificationDelegate;
86         final TypedValue scaleValue = new TypedValue();
87         context.getResources().getValue(
88                 R.dimen.config_screen_magnification_scaling_threshold,
89                 scaleValue, false);
90         mScalingThreshold = scaleValue.getFloat();
91     }
92 
setEnabled(boolean enable)93     void setEnabled(boolean enable) {
94         clear();
95         mEnable = enable;
96     }
97 
onTouchEvent(MotionEvent motionEvent)98     void onTouchEvent(MotionEvent motionEvent) {
99         mScaleGestureDetector.onTouchEvent(motionEvent);
100         mScrollGestureDetector.onTouchEvent(motionEvent);
101     }
102 
103     @Override
104     // TODO: Try to distinguish onScroll with onScale correctly.
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)105     public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
106         if (!mEnable || (mBlockScroll && mScaling)) {
107             return true;
108         }
109         return mMagnificationDelegate.processScroll(mDisplayId, distanceX, distanceY);
110     }
111 
112     @Override
onScale(ScaleGestureDetector detector)113     public boolean onScale(ScaleGestureDetector detector) {
114         if (DEBUG) {
115             Slog.i(TAG, "onScale: triggered ");
116         }
117         if (!mScaling) {
118             if (mInitialScaleFactor < 0) {
119                 mInitialScaleFactor = detector.getScaleFactor();
120                 return false;
121             }
122             final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor;
123             mScaling = abs(deltaScale) > mScalingThreshold;
124             return mScaling;
125         }
126 
127         // Don't allow a gesture to move the user further outside the
128         // desired bounds for gesture-controlled scaling.
129         final float scale;
130         final float initialScale = mMagnificationDelegate.getScale(mDisplayId);
131         final float targetScale = initialScale * detector.getScaleFactor();
132 
133         if (targetScale > mMaxScale && targetScale > initialScale) {
134             // The target scale is too big and getting bigger.
135             scale = mMaxScale;
136         } else if (targetScale < mMinScale && targetScale < initialScale) {
137             // The target scale is too small and getting smaller.
138             scale = mMinScale;
139         } else {
140             // The target scale may be outside our bounds, but at least
141             // it's moving in the right direction. This avoids a "jump" if
142             // we're at odds with some other service's desired bounds.
143             scale = targetScale;
144         }
145 
146         if (DEBUG) Slog.i(TAG, "Scaled content to: " + scale + "x");
147         mMagnificationDelegate.setScale(mDisplayId, scale);
148         return /* handled: */ true;
149     }
150 
151     @Override
onScaleBegin(ScaleGestureDetector detector)152     public boolean onScaleBegin(ScaleGestureDetector detector) {
153         return mEnable;
154     }
155 
156     @Override
onScaleEnd(ScaleGestureDetector detector)157     public void onScaleEnd(ScaleGestureDetector detector) {
158         clear();
159     }
160 
clear()161     void clear() {
162         mInitialScaleFactor = -1;
163         mScaling = false;
164     }
165 
166     @Override
toString()167     public String toString() {
168         return "PanningScalingHandler{"
169                 + "mInitialScaleFactor=" + mInitialScaleFactor
170                 + ", mScaling=" + mScaling
171                 + '}';
172     }
173 }
174 
175