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