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 
17 package com.android.systemui.classifier;
18 
19 import android.content.Context;
20 import android.database.ContentObserver;
21 import android.hardware.SensorEvent;
22 import android.os.Handler;
23 import android.os.UserHandle;
24 import android.provider.Settings;
25 import android.util.DisplayMetrics;
26 import android.util.Log;
27 import android.view.MotionEvent;
28 
29 import java.util.ArrayDeque;
30 import java.util.ArrayList;
31 
32 /**
33  * An classifier trying to determine whether it is a human interacting with the phone or not.
34  */
35 public class HumanInteractionClassifier extends Classifier {
36     private static final String HIC_ENABLE = "HIC_enable";
37     private static final float FINGER_DISTANCE = 0.1f;
38 
39     /** Default value for the HIC_ENABLE setting: 1 - enabled, 0 - disabled */
40     private static final int HIC_ENABLE_DEFAULT = 1;
41 
42     private static HumanInteractionClassifier sInstance = null;
43 
44     private final Handler mHandler = new Handler();
45     private final Context mContext;
46 
47     private final StrokeClassifier[] mStrokeClassifiers;
48     private final GestureClassifier[] mGestureClassifiers;
49     private final ArrayDeque<MotionEvent> mBufferedEvents = new ArrayDeque<>();
50     private final HistoryEvaluator mHistoryEvaluator;
51     private final float mDpi;
52 
53     private boolean mEnableClassifier = false;
54     private int mCurrentType = Classifier.GENERIC;
55 
56     protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
57         @Override
58         public void onChange(boolean selfChange) {
59             updateConfiguration();
60         }
61     };
62 
HumanInteractionClassifier(Context context)63     private HumanInteractionClassifier(Context context) {
64         mContext = context;
65         DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
66 
67         // If the phone is rotated to landscape, the calculations would be wrong if xdpi and ydpi
68         // were to be used separately. Due negligible differences in xdpi and ydpi we can just
69         // take the average.
70         // Note that xdpi and ydpi are the physical pixels per inch and are not affected by scaling.
71         mDpi = (displayMetrics.xdpi + displayMetrics.ydpi) / 2.0f;
72         mClassifierData = new ClassifierData(mDpi);
73         mHistoryEvaluator = new HistoryEvaluator();
74 
75         mStrokeClassifiers = new StrokeClassifier[]{
76                 new AnglesClassifier(mClassifierData),
77                 new SpeedClassifier(mClassifierData),
78                 new DurationCountClassifier(mClassifierData),
79                 new EndPointRatioClassifier(mClassifierData),
80                 new EndPointLengthClassifier(mClassifierData),
81                 new AccelerationClassifier(mClassifierData),
82                 new SpeedAnglesClassifier(mClassifierData),
83                 new LengthCountClassifier(mClassifierData),
84                 new DirectionClassifier(mClassifierData),
85         };
86 
87         mGestureClassifiers = new GestureClassifier[] {
88                 new PointerCountClassifier(mClassifierData),
89                 new ProximityClassifier(mClassifierData)
90         };
91 
92         mContext.getContentResolver().registerContentObserver(
93                 Settings.Global.getUriFor(HIC_ENABLE), false,
94                 mSettingsObserver,
95                 UserHandle.USER_ALL);
96 
97         updateConfiguration();
98     }
99 
getInstance(Context context)100     public static HumanInteractionClassifier getInstance(Context context) {
101         if (sInstance == null) {
102             sInstance = new HumanInteractionClassifier(context);
103         }
104         return sInstance;
105     }
106 
updateConfiguration()107     private void updateConfiguration() {
108         mEnableClassifier = 0 != Settings.Global.getInt(
109                 mContext.getContentResolver(),
110                 HIC_ENABLE, HIC_ENABLE_DEFAULT);
111     }
112 
setType(int type)113     public void setType(int type) {
114         mCurrentType = type;
115     }
116 
117     @Override
onTouchEvent(MotionEvent event)118     public void onTouchEvent(MotionEvent event) {
119         if (!mEnableClassifier) {
120             return;
121         }
122 
123         // If the user is dragging down the notification, they might want to drag it down
124         // enough to see the content, read it for a while and then lift the finger to open
125         // the notification. This kind of motion scores very bad in the Classifier so the
126         // MotionEvents which are close to the current position of the finger are not
127         // sent to the classifiers until the finger moves far enough. When the finger if lifted
128         // up, the last MotionEvent which was far enough from the finger is set as the final
129         // MotionEvent and sent to the Classifiers.
130         if (mCurrentType == Classifier.NOTIFICATION_DRAG_DOWN) {
131             mBufferedEvents.add(MotionEvent.obtain(event));
132             Point pointEnd = new Point(event.getX() / mDpi, event.getY() / mDpi);
133 
134             while (pointEnd.dist(new Point(mBufferedEvents.getFirst().getX() / mDpi,
135                     mBufferedEvents.getFirst().getY() / mDpi)) > FINGER_DISTANCE) {
136                 addTouchEvent(mBufferedEvents.getFirst());
137                 mBufferedEvents.remove();
138             }
139 
140             int action = event.getActionMasked();
141             if (action == MotionEvent.ACTION_UP) {
142                 mBufferedEvents.getFirst().setAction(MotionEvent.ACTION_UP);
143                 addTouchEvent(mBufferedEvents.getFirst());
144                 mBufferedEvents.clear();
145             }
146         } else {
147             addTouchEvent(event);
148         }
149     }
150 
addTouchEvent(MotionEvent event)151     private void addTouchEvent(MotionEvent event) {
152         mClassifierData.update(event);
153 
154         for (StrokeClassifier c : mStrokeClassifiers) {
155             c.onTouchEvent(event);
156         }
157 
158         for (GestureClassifier c : mGestureClassifiers) {
159             c.onTouchEvent(event);
160         }
161 
162         int size = mClassifierData.getEndingStrokes().size();
163         for (int i = 0; i < size; i++) {
164             Stroke stroke = mClassifierData.getEndingStrokes().get(i);
165             float evaluation = 0.0f;
166             StringBuilder sb = FalsingLog.ENABLED ? new StringBuilder("stroke") : null;
167             for (StrokeClassifier c : mStrokeClassifiers) {
168                 float e = c.getFalseTouchEvaluation(mCurrentType, stroke);
169                 if (FalsingLog.ENABLED) {
170                     String tag = c.getTag();
171                     sb.append(" ").append(e >= 1f ? tag : tag.toLowerCase()).append("=").append(e);
172                 }
173                 evaluation += e;
174             }
175 
176             if (FalsingLog.ENABLED) {
177                 FalsingLog.i(" addTouchEvent", sb.toString());
178             }
179             mHistoryEvaluator.addStroke(evaluation);
180         }
181 
182         int action = event.getActionMasked();
183         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
184             float evaluation = 0.0f;
185             StringBuilder sb = FalsingLog.ENABLED ? new StringBuilder("gesture") : null;
186             for (GestureClassifier c : mGestureClassifiers) {
187                 float e = c.getFalseTouchEvaluation(mCurrentType);
188                 if (FalsingLog.ENABLED) {
189                     String tag = c.getTag();
190                     sb.append(" ").append(e >= 1f ? tag : tag.toLowerCase()).append("=").append(e);
191                 }
192                 evaluation += e;
193             }
194             if (FalsingLog.ENABLED) {
195                 FalsingLog.i(" addTouchEvent", sb.toString());
196             }
197             mHistoryEvaluator.addGesture(evaluation);
198             setType(Classifier.GENERIC);
199         }
200 
201         mClassifierData.cleanUp(event);
202     }
203 
204     @Override
onSensorChanged(SensorEvent event)205     public void onSensorChanged(SensorEvent event) {
206         for (Classifier c : mStrokeClassifiers) {
207             c.onSensorChanged(event);
208         }
209 
210         for (Classifier c : mGestureClassifiers) {
211             c.onSensorChanged(event);
212         }
213     }
214 
isFalseTouch()215     public boolean isFalseTouch() {
216         if (mEnableClassifier) {
217             float evaluation = mHistoryEvaluator.getEvaluation();
218             boolean result = evaluation >= 5.0f;
219             if (FalsingLog.ENABLED) {
220                 FalsingLog.i("isFalseTouch", new StringBuilder()
221                         .append("eval=").append(evaluation).append(" result=")
222                         .append(result ? 1 : 0).toString());
223             }
224             return result;
225         }
226         return false;
227     }
228 
isEnabled()229     public boolean isEnabled() {
230         return mEnableClassifier;
231     }
232 
233     @Override
getTag()234     public String getTag() {
235         return "HIC";
236     }
237 }
238