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.systemui.navigationbar.buttons;
18 
19 import android.animation.ObjectAnimator;
20 import android.content.res.Resources;
21 import android.graphics.Canvas;
22 import android.os.SystemClock;
23 import android.util.FloatProperty;
24 import android.util.Slog;
25 import android.view.MotionEvent;
26 import android.view.Surface;
27 
28 import com.android.systemui.Dependency;
29 import com.android.systemui.R;
30 import com.android.systemui.navigationbar.NavigationBarController;
31 import com.android.systemui.navigationbar.NavigationBarView;
32 
33 import javax.inject.Inject;
34 
35 /**
36  * The "dead zone" consumes unintentional taps along the top edge of the navigation bar.
37  * When users are typing quickly on an IME they may attempt to hit the space bar, overshoot, and
38  * accidentally hit the home button. The DeadZone expands temporarily after each tap in the UI
39  * outside the navigation bar (since this is when accidental taps are more likely), then contracts
40  * back over time (since a later tap might be intended for the top of the bar).
41  */
42 public class DeadZone {
43     public static final String TAG = "DeadZone";
44 
45     public static final boolean DEBUG = false;
46     public static final int HORIZONTAL = 0;  // Consume taps along the top edge.
47     public static final int VERTICAL = 1;  // Consume taps along the left edge.
48 
49     private static final boolean CHATTY = true; // print to logcat when we eat a click
50 
51     private static final FloatProperty<DeadZone> FLASH_PROPERTY =
52             new FloatProperty<DeadZone>("DeadZoneFlash") {
53         @Override
54         public void setValue(DeadZone object, float value) {
55             object.setFlash(value);
56         }
57 
58         @Override
59         public Float get(DeadZone object) {
60             return object.getFlash();
61         }
62     };
63 
64     private final NavigationBarController mNavBarController;
65     private final NavigationBarView mNavigationBarView;
66 
67     private boolean mShouldFlash;
68     private float mFlashFrac = 0f;
69 
70     private int mSizeMax;
71     private int mSizeMin;
72     // Upon activity elsewhere in the UI, the dead zone will hold steady for
73     // mHold ms, then move back over the course of mDecay ms
74     private int mHold, mDecay;
75     private boolean mVertical;
76     private long mLastPokeTime;
77     private int mDisplayRotation;
78     private final int mDisplayId;
79 
80     private final Runnable mDebugFlash = new Runnable() {
81         @Override
82         public void run() {
83             ObjectAnimator.ofFloat(DeadZone.this, FLASH_PROPERTY, 1f, 0f).setDuration(150).start();
84         }
85     };
86 
87     @Inject
DeadZone(NavigationBarView view)88     public DeadZone(NavigationBarView view) {
89         mNavigationBarView = view;
90         mNavBarController = Dependency.get(NavigationBarController.class);
91         mDisplayId = view.getContext().getDisplayId();
92         onConfigurationChanged(HORIZONTAL);
93     }
94 
lerp(float a, float b, float f)95     static float lerp(float a, float b, float f) {
96         return (b - a) * f + a;
97     }
98 
getSize(long now)99     private float getSize(long now) {
100         if (mSizeMax == 0)
101             return 0;
102         long dt = (now - mLastPokeTime);
103         if (dt > mHold + mDecay)
104             return mSizeMin;
105         if (dt < mHold)
106             return mSizeMax;
107         return (int) lerp(mSizeMax, mSizeMin, (float) (dt - mHold) / mDecay);
108     }
109 
setFlashOnTouchCapture(boolean dbg)110     public void setFlashOnTouchCapture(boolean dbg) {
111         mShouldFlash = dbg;
112         mFlashFrac = 0f;
113         mNavigationBarView.postInvalidate();
114     }
115 
onConfigurationChanged(int rotation)116     public void onConfigurationChanged(int rotation) {
117         mDisplayRotation = rotation;
118 
119         final Resources res = mNavigationBarView.getResources();
120         mHold = res.getInteger(R.integer.navigation_bar_deadzone_hold);
121         mDecay = res.getInteger(R.integer.navigation_bar_deadzone_decay);
122 
123         mSizeMin = res.getDimensionPixelSize(R.dimen.navigation_bar_deadzone_size);
124         mSizeMax = res.getDimensionPixelSize(R.dimen.navigation_bar_deadzone_size_max);
125         int index = res.getInteger(R.integer.navigation_bar_deadzone_orientation);
126         mVertical = (index == VERTICAL);
127 
128         if (DEBUG) {
129             Slog.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold
130                     + (mVertical ? " vertical" : " horizontal"));
131         }
132         setFlashOnTouchCapture(res.getBoolean(R.bool.config_dead_zone_flash));
133     }
134 
135     // I made you a touch event...
onTouchEvent(MotionEvent event)136     public boolean onTouchEvent(MotionEvent event) {
137         if (DEBUG) {
138             Slog.v(TAG, this + " onTouch: " + MotionEvent.actionToString(event.getAction()));
139         }
140 
141         // Don't consume events for high precision pointing devices. For this purpose a stylus is
142         // considered low precision (like a finger), so its events may be consumed.
143         if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
144             return false;
145         }
146 
147         final int action = event.getAction();
148         if (action == MotionEvent.ACTION_OUTSIDE) {
149             poke(event);
150             return true;
151         } else if (action == MotionEvent.ACTION_DOWN) {
152             if (DEBUG) {
153                 Slog.v(TAG, this + " ACTION_DOWN: " + event.getX() + "," + event.getY());
154             }
155             mNavBarController.touchAutoDim(mDisplayId);
156             int size = (int) getSize(event.getEventTime());
157             // In the vertical orientation consume taps along the left edge.
158             // In horizontal orientation consume taps along the top edge.
159             final boolean consumeEvent;
160             if (mVertical) {
161                 if (mDisplayRotation == Surface.ROTATION_270) {
162                     consumeEvent = event.getX() > mNavigationBarView.getWidth() - size;
163                 } else {
164                     consumeEvent = event.getX() < size;
165                 }
166             } else {
167                 consumeEvent = event.getY() < size;
168             }
169             if (consumeEvent) {
170                 if (CHATTY) {
171                     Slog.v(TAG, "consuming errant click: (" + event.getX() + "," + event.getY() + ")");
172                 }
173                 if (mShouldFlash) {
174                     mNavigationBarView.post(mDebugFlash);
175                     mNavigationBarView.postInvalidate();
176                 }
177                 return true; // ...but I eated it
178             }
179         }
180         return false;
181     }
182 
183     private void poke(MotionEvent event) {
184         mLastPokeTime = event.getEventTime();
185         if (DEBUG)
186             Slog.v(TAG, "poked! size=" + getSize(mLastPokeTime));
187         if (mShouldFlash) mNavigationBarView.postInvalidate();
188     }
189 
190     public void setFlash(float f) {
191         mFlashFrac = f;
192         mNavigationBarView.postInvalidate();
193     }
194 
195     public float getFlash() {
196         return mFlashFrac;
197     }
198 
199     public void onDraw(Canvas can) {
200         if (!mShouldFlash || mFlashFrac <= 0f) {
201             return;
202         }
203 
204         final int size = (int) getSize(SystemClock.uptimeMillis());
205         if (mVertical) {
206             if (mDisplayRotation == Surface.ROTATION_270) {
207                 can.clipRect(can.getWidth() - size, 0, can.getWidth(), can.getHeight());
208             } else {
209                 can.clipRect(0, 0, size, can.getHeight());
210             }
211         } else {
212             can.clipRect(0, 0, can.getWidth(), size);
213         }
214 
215         final float frac = DEBUG ? (mFlashFrac - 0.5f) + 0.5f : mFlashFrac;
216         can.drawARGB((int) (frac * 0xFF), 0xDD, 0xEE, 0xAA);
217 
218         if (DEBUG && size > mSizeMin)
219             // Very aggressive redrawing here, for debugging only
220             mNavigationBarView.postInvalidateDelayed(100);
221     }
222 }
223