| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.systemui.classifier; |
| |
| import android.content.Context; |
| import android.database.ContentObserver; |
| import android.hardware.SensorEvent; |
| import android.os.Handler; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| |
| /** |
| * An classifier trying to determine whether it is a human interacting with the phone or not. |
| */ |
| public class HumanInteractionClassifier extends Classifier { |
| private static final String HIC_ENABLE = "HIC_enable"; |
| private static final float FINGER_DISTANCE = 0.1f; |
| |
| /** Default value for the HIC_ENABLE setting: 1 - enabled, 0 - disabled */ |
| private static final int HIC_ENABLE_DEFAULT = 1; |
| |
| private static HumanInteractionClassifier sInstance = null; |
| |
| private final Handler mHandler = new Handler(); |
| private final Context mContext; |
| |
| private final StrokeClassifier[] mStrokeClassifiers; |
| private final GestureClassifier[] mGestureClassifiers; |
| private final ArrayDeque<MotionEvent> mBufferedEvents = new ArrayDeque<>(); |
| private final HistoryEvaluator mHistoryEvaluator; |
| private final float mDpi; |
| |
| private boolean mEnableClassifier = false; |
| private int mCurrentType = Classifier.GENERIC; |
| |
| protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) { |
| @Override |
| public void onChange(boolean selfChange) { |
| updateConfiguration(); |
| } |
| }; |
| |
| private HumanInteractionClassifier(Context context) { |
| mContext = context; |
| DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); |
| |
| // If the phone is rotated to landscape, the calculations would be wrong if xdpi and ydpi |
| // were to be used separately. Due negligible differences in xdpi and ydpi we can just |
| // take the average. |
| // Note that xdpi and ydpi are the physical pixels per inch and are not affected by scaling. |
| mDpi = (displayMetrics.xdpi + displayMetrics.ydpi) / 2.0f; |
| mClassifierData = new ClassifierData(mDpi); |
| mHistoryEvaluator = new HistoryEvaluator(); |
| |
| mStrokeClassifiers = new StrokeClassifier[]{ |
| new AnglesClassifier(mClassifierData), |
| new SpeedClassifier(mClassifierData), |
| new DurationCountClassifier(mClassifierData), |
| new EndPointRatioClassifier(mClassifierData), |
| new EndPointLengthClassifier(mClassifierData), |
| new AccelerationClassifier(mClassifierData), |
| new SpeedAnglesClassifier(mClassifierData), |
| new LengthCountClassifier(mClassifierData), |
| new DirectionClassifier(mClassifierData), |
| }; |
| |
| mGestureClassifiers = new GestureClassifier[] { |
| new PointerCountClassifier(mClassifierData), |
| new ProximityClassifier(mClassifierData) |
| }; |
| |
| mContext.getContentResolver().registerContentObserver( |
| Settings.Global.getUriFor(HIC_ENABLE), false, |
| mSettingsObserver, |
| UserHandle.USER_ALL); |
| |
| updateConfiguration(); |
| } |
| |
| public static HumanInteractionClassifier getInstance(Context context) { |
| if (sInstance == null) { |
| sInstance = new HumanInteractionClassifier(context); |
| } |
| return sInstance; |
| } |
| |
| private void updateConfiguration() { |
| mEnableClassifier = 0 != Settings.Global.getInt( |
| mContext.getContentResolver(), |
| HIC_ENABLE, HIC_ENABLE_DEFAULT); |
| } |
| |
| public void setType(int type) { |
| mCurrentType = type; |
| } |
| |
| @Override |
| public void onTouchEvent(MotionEvent event) { |
| if (!mEnableClassifier) { |
| return; |
| } |
| |
| // If the user is dragging down the notification, they might want to drag it down |
| // enough to see the content, read it for a while and then lift the finger to open |
| // the notification. This kind of motion scores very bad in the Classifier so the |
| // MotionEvents which are close to the current position of the finger are not |
| // sent to the classifiers until the finger moves far enough. When the finger if lifted |
| // up, the last MotionEvent which was far enough from the finger is set as the final |
| // MotionEvent and sent to the Classifiers. |
| if (mCurrentType == Classifier.NOTIFICATION_DRAG_DOWN) { |
| mBufferedEvents.add(MotionEvent.obtain(event)); |
| Point pointEnd = new Point(event.getX() / mDpi, event.getY() / mDpi); |
| |
| while (pointEnd.dist(new Point(mBufferedEvents.getFirst().getX() / mDpi, |
| mBufferedEvents.getFirst().getY() / mDpi)) > FINGER_DISTANCE) { |
| addTouchEvent(mBufferedEvents.getFirst()); |
| mBufferedEvents.remove(); |
| } |
| |
| int action = event.getActionMasked(); |
| if (action == MotionEvent.ACTION_UP) { |
| mBufferedEvents.getFirst().setAction(MotionEvent.ACTION_UP); |
| addTouchEvent(mBufferedEvents.getFirst()); |
| mBufferedEvents.clear(); |
| } |
| } else { |
| addTouchEvent(event); |
| } |
| } |
| |
| private void addTouchEvent(MotionEvent event) { |
| mClassifierData.update(event); |
| |
| for (StrokeClassifier c : mStrokeClassifiers) { |
| c.onTouchEvent(event); |
| } |
| |
| for (GestureClassifier c : mGestureClassifiers) { |
| c.onTouchEvent(event); |
| } |
| |
| int size = mClassifierData.getEndingStrokes().size(); |
| for (int i = 0; i < size; i++) { |
| Stroke stroke = mClassifierData.getEndingStrokes().get(i); |
| float evaluation = 0.0f; |
| StringBuilder sb = FalsingLog.ENABLED ? new StringBuilder("stroke") : null; |
| for (StrokeClassifier c : mStrokeClassifiers) { |
| float e = c.getFalseTouchEvaluation(mCurrentType, stroke); |
| if (FalsingLog.ENABLED) { |
| String tag = c.getTag(); |
| sb.append(" ").append(e >= 1f ? tag : tag.toLowerCase()).append("=").append(e); |
| } |
| evaluation += e; |
| } |
| |
| if (FalsingLog.ENABLED) { |
| FalsingLog.i(" addTouchEvent", sb.toString()); |
| } |
| mHistoryEvaluator.addStroke(evaluation); |
| } |
| |
| int action = event.getActionMasked(); |
| if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { |
| float evaluation = 0.0f; |
| StringBuilder sb = FalsingLog.ENABLED ? new StringBuilder("gesture") : null; |
| for (GestureClassifier c : mGestureClassifiers) { |
| float e = c.getFalseTouchEvaluation(mCurrentType); |
| if (FalsingLog.ENABLED) { |
| String tag = c.getTag(); |
| sb.append(" ").append(e >= 1f ? tag : tag.toLowerCase()).append("=").append(e); |
| } |
| evaluation += e; |
| } |
| if (FalsingLog.ENABLED) { |
| FalsingLog.i(" addTouchEvent", sb.toString()); |
| } |
| mHistoryEvaluator.addGesture(evaluation); |
| setType(Classifier.GENERIC); |
| } |
| |
| mClassifierData.cleanUp(event); |
| } |
| |
| @Override |
| public void onSensorChanged(SensorEvent event) { |
| for (Classifier c : mStrokeClassifiers) { |
| c.onSensorChanged(event); |
| } |
| |
| for (Classifier c : mGestureClassifiers) { |
| c.onSensorChanged(event); |
| } |
| } |
| |
| public boolean isFalseTouch() { |
| if (mEnableClassifier) { |
| float evaluation = mHistoryEvaluator.getEvaluation(); |
| boolean result = evaluation >= 5.0f; |
| if (FalsingLog.ENABLED) { |
| FalsingLog.i("isFalseTouch", new StringBuilder() |
| .append("eval=").append(evaluation).append(" result=") |
| .append(result ? 1 : 0).toString()); |
| } |
| return result; |
| } |
| return false; |
| } |
| |
| public boolean isEnabled() { |
| return mEnableClassifier; |
| } |
| |
| @Override |
| public String getTag() { |
| return "HIC"; |
| } |
| } |