1 /*
2  * Copyright (C) 2022 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.wm.shell.back;
18 
19 import android.annotation.FloatRange;
20 import android.os.SystemProperties;
21 import android.util.MathUtils;
22 import android.view.MotionEvent;
23 import android.view.RemoteAnimationTarget;
24 import android.window.BackEvent;
25 import android.window.BackMotionEvent;
26 
27 /**
28  * Helper class to record the touch location for gesture and generate back events.
29  */
30 class TouchTracker {
31     private static final String PREDICTIVE_BACK_LINEAR_DISTANCE_PROP =
32             "persist.wm.debug.predictive_back_linear_distance";
33     private static final int LINEAR_DISTANCE = SystemProperties
34             .getInt(PREDICTIVE_BACK_LINEAR_DISTANCE_PROP, -1);
35     private float mLinearDistance = LINEAR_DISTANCE;
36     private float mMaxDistance;
37     private float mNonLinearFactor;
38     /**
39      * Location of the latest touch event
40      */
41     private float mLatestTouchX;
42     private float mLatestTouchY;
43     private boolean mTriggerBack;
44 
45     /**
46      * Location of the initial touch event of the back gesture.
47      */
48     private float mInitTouchX;
49     private float mInitTouchY;
50     private float mLatestVelocityX;
51     private float mLatestVelocityY;
52     private float mStartThresholdX;
53     private int mSwipeEdge;
54     private boolean mCancelled;
55 
update(float touchX, float touchY, float velocityX, float velocityY)56     void update(float touchX, float touchY, float velocityX, float velocityY) {
57         /**
58          * If back was previously cancelled but the user has started swiping in the forward
59          * direction again, restart back.
60          */
61         if (mCancelled && ((touchX > mLatestTouchX && mSwipeEdge == BackEvent.EDGE_LEFT)
62                 || touchX < mLatestTouchX && mSwipeEdge == BackEvent.EDGE_RIGHT)) {
63             mCancelled = false;
64             mStartThresholdX = touchX;
65         }
66         mLatestTouchX = touchX;
67         mLatestTouchY = touchY;
68         mLatestVelocityX = velocityX;
69         mLatestVelocityY = velocityY;
70     }
71 
setTriggerBack(boolean triggerBack)72     void setTriggerBack(boolean triggerBack) {
73         if (mTriggerBack != triggerBack && !triggerBack) {
74             mCancelled = true;
75         }
76         mTriggerBack = triggerBack;
77     }
78 
setGestureStartLocation(float touchX, float touchY, int swipeEdge)79     void setGestureStartLocation(float touchX, float touchY, int swipeEdge) {
80         mInitTouchX = touchX;
81         mInitTouchY = touchY;
82         mSwipeEdge = swipeEdge;
83         mStartThresholdX = mInitTouchX;
84     }
85 
reset()86     void reset() {
87         mInitTouchX = 0;
88         mInitTouchY = 0;
89         mStartThresholdX = 0;
90         mCancelled = false;
91         mTriggerBack = false;
92         mSwipeEdge = BackEvent.EDGE_LEFT;
93     }
94 
createStartEvent(RemoteAnimationTarget target)95     BackMotionEvent createStartEvent(RemoteAnimationTarget target) {
96         return new BackMotionEvent(
97                 /* touchX = */ mInitTouchX,
98                 /* touchY = */ mInitTouchY,
99                 /* progress = */ 0,
100                 /* velocityX = */ 0,
101                 /* velocityY = */ 0,
102                 /* swipeEdge = */ mSwipeEdge,
103                 /* departingAnimationTarget = */ target);
104     }
105 
createProgressEvent()106     BackMotionEvent createProgressEvent() {
107         float progress = 0;
108         // Progress is always 0 when back is cancelled and not restarted.
109         if (!mCancelled) {
110             progress = getProgress(mLatestTouchX);
111         }
112         return createProgressEvent(progress);
113     }
114 
115     /**
116      * Progress value computed from the touch position.
117      *
118      * @param touchX the X touch position of the {@link MotionEvent}.
119      * @return progress value
120      */
121     @FloatRange(from = 0.0, to = 1.0)
getProgress(float touchX)122     float getProgress(float touchX) {
123         // If back is committed, progress is the distance between the last and first touch
124         // point, divided by the max drag distance. Otherwise, it's the distance between
125         // the last touch point and the starting threshold, divided by max drag distance.
126         // The starting threshold is initially the first touch location, and updated to
127         // the location everytime back is restarted after being cancelled.
128         float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
129         float deltaX = Math.abs(startX - touchX);
130         float linearDistance = mLinearDistance;
131         float maxDistance = getMaxDistance();
132         maxDistance = maxDistance == 0 ? 1 : maxDistance;
133         float progress;
134         if (linearDistance < maxDistance) {
135             // Up to linearDistance it behaves linearly, then slowly reaches 1f.
136 
137             // maxDistance is composed of linearDistance + nonLinearDistance
138             float nonLinearDistance = maxDistance - linearDistance;
139             float initialTarget = linearDistance + nonLinearDistance * mNonLinearFactor;
140 
141             boolean isLinear = deltaX <= linearDistance;
142             if (isLinear) {
143                 progress = deltaX / initialTarget;
144             } else {
145                 float nonLinearDeltaX = deltaX - linearDistance;
146                 float nonLinearProgress = nonLinearDeltaX / nonLinearDistance;
147                 float currentTarget = MathUtils.lerp(
148                         /* start = */ initialTarget,
149                         /* stop = */ maxDistance,
150                         /* amount = */ nonLinearProgress);
151                 progress = deltaX / currentTarget;
152             }
153         } else {
154             // Always linear behavior.
155             progress = deltaX / maxDistance;
156         }
157         return MathUtils.constrain(progress, 0, 1);
158     }
159 
160     /**
161      * Maximum distance in pixels.
162      * Progress is considered to be completed (1f) when this limit is exceeded.
163      */
getMaxDistance()164     float getMaxDistance() {
165         return mMaxDistance;
166     }
167 
createProgressEvent(float progress)168     BackMotionEvent createProgressEvent(float progress) {
169         return new BackMotionEvent(
170                 /* touchX = */ mLatestTouchX,
171                 /* touchY = */ mLatestTouchY,
172                 /* progress = */ progress,
173                 /* velocityX = */ mLatestVelocityX,
174                 /* velocityY = */ mLatestVelocityY,
175                 /* swipeEdge = */ mSwipeEdge,
176                 /* departingAnimationTarget = */ null);
177     }
178 
setProgressThresholds(float linearDistance, float maxDistance, float nonLinearFactor)179     public void setProgressThresholds(float linearDistance, float maxDistance,
180             float nonLinearFactor) {
181         if (LINEAR_DISTANCE >= 0) {
182             mLinearDistance = LINEAR_DISTANCE;
183         } else {
184             mLinearDistance = linearDistance;
185         }
186         mMaxDistance = maxDistance;
187         mNonLinearFactor = nonLinearFactor;
188     }
189 }
190