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