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 17package com.android.systemui.classifier; 18 19import android.content.Context; 20import android.database.ContentObserver; 21import android.hardware.SensorEvent; 22import android.os.Handler; 23import android.os.Looper; 24import android.os.UserHandle; 25import android.provider.Settings; 26import android.util.DisplayMetrics; 27import android.util.Log; 28import android.view.MotionEvent; 29 30import java.util.ArrayDeque; 31import java.util.ArrayList; 32 33/** 34 * An classifier trying to determine whether it is a human interacting with the phone or not. 35 */ 36public 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 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 101 public static HumanInteractionClassifier getInstance(Context context) { 102 if (sInstance == null) { 103 sInstance = new HumanInteractionClassifier(context); 104 } 105 return sInstance; 106 } 107 108 private void updateConfiguration() { 109 mEnableClassifier = 0 != Settings.Global.getInt( 110 mContext.getContentResolver(), 111 HIC_ENABLE, HIC_ENABLE_DEFAULT); 112 } 113 114 public void setType(int type) { 115 mCurrentType = type; 116 } 117 118 @Override 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 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 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 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 230 public boolean isEnabled() { 231 return mEnableClassifier; 232 } 233 234 @Override 235 public String getTag() { 236 return "HIC"; 237 } 238} 239