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