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