diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java index 5f1fc2f2a4571..57be77e9960ca 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java @@ -64,10 +64,14 @@ public class BrightLineFalsingManager implements FalsingManager { mDataProvider = falsingDataProvider; mSensorManager = sensorManager; mClassifiers = new ArrayList<>(); + DistanceClassifier distanceClassifier = new DistanceClassifier(mDataProvider); + ProximityClassifier proximityClassifier = new ProximityClassifier(distanceClassifier, + mDataProvider); mClassifiers.add(new PointerCountClassifier(mDataProvider)); mClassifiers.add(new TypeClassifier(mDataProvider)); mClassifiers.add(new DiagonalClassifier(mDataProvider)); - mClassifiers.add(new DistanceClassifier(mDataProvider)); + mClassifiers.add(distanceClassifier); + mClassifiers.add(proximityClassifier); } private void registerSensors() { diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java new file mode 100644 index 0000000000000..94a8ac85b7244 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2019 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.brightline; + +import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.view.MotionEvent; + + +/** + * False touch if proximity sensor is covered for more than a certain percentage of the gesture. + * + * This classifer is essentially a no-op for QUICK_SETTINGS, as we assume the sensor may be + * covered when swiping from the top. + */ +class ProximityClassifier extends FalsingClassifier { + + private static final double PERCENT_COVERED_THRESHOLD = 0.1; + private final DistanceClassifier mDistanceClassifier; + + private boolean mNear; + private long mGestureStartTimeNs; + private long mPrevNearTimeNs; + private long mNearDurationNs; + private float mPercentNear; + + ProximityClassifier(DistanceClassifier distanceClassifier, + FalsingDataProvider dataProvider) { + super(dataProvider); + this.mDistanceClassifier = distanceClassifier; + } + + @Override + void onSessionStarted() { + mPrevNearTimeNs = 0; + mPercentNear = 0; + } + + @Override + void onSessionEnded() { + mPrevNearTimeNs = 0; + mPercentNear = 0; + } + + @Override + public void onTouchEvent(MotionEvent motionEvent) { + int action = motionEvent.getActionMasked(); + + if (action == MotionEvent.ACTION_DOWN) { + mGestureStartTimeNs = motionEvent.getEventTimeNano(); + if (mPrevNearTimeNs > 0) { + // We only care about if the proximity sensor is triggered while a move event is + // happening. + mPrevNearTimeNs = motionEvent.getEventTimeNano(); + } + logDebug("Gesture start time: " + mGestureStartTimeNs); + mNearDurationNs = 0; + } + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + update(mNear, motionEvent.getEventTimeNano()); + long duration = motionEvent.getEventTimeNano() - mGestureStartTimeNs; + + logDebug("Gesture duration, Proximity duration: " + duration + ", " + mNearDurationNs); + + if (duration == 0) { + mPercentNear = mNear ? 1.0f : 0.0f; + } else { + mPercentNear = (float) mNearDurationNs / (float) duration; + } + } + + } + + @Override + public void onSensorEvent(SensorEvent sensorEvent) { + if (sensorEvent.sensor.getType() == Sensor.TYPE_PROXIMITY) { + logDebug("Sensor is: " + (sensorEvent.values[0] < sensorEvent.sensor.getMaximumRange()) + + " at time " + sensorEvent.timestamp); + update( + sensorEvent.values[0] < sensorEvent.sensor.getMaximumRange(), + sensorEvent.timestamp); + } + } + + @Override + public boolean isFalseTouch() { + if (getInteractionType() == QUICK_SETTINGS) { + return false; + } + + logInfo("Percent of gesture in proximity: " + mPercentNear); + + if (mPercentNear > PERCENT_COVERED_THRESHOLD) { + return !mDistanceClassifier.isLongSwipe(); + } + + return false; + } + + /** + * @param near is the sensor showing the near state right now + * @param timeStampNs time of this event in nanoseconds + */ + private void update(boolean near, long timeStampNs) { + if (mPrevNearTimeNs != 0 && timeStampNs > mPrevNearTimeNs && mNear) { + mNearDurationNs += timeStampNs - mPrevNearTimeNs; + logDebug("Updating duration: " + mNearDurationNs); + } + + if (near) { + logDebug("Set prevNearTimeNs: " + timeStampNs); + mPrevNearTimeNs = timeStampNs; + } + + mNear = near; + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ProximityClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ProximityClassifierTest.java new file mode 100644 index 0000000000000..2ed792542efd0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ProximityClassifierTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2019 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.brightline; + +import static com.android.systemui.classifier.Classifier.GENERIC; +import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.MotionEvent; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class ProximityClassifierTest extends SysuiTestCase { + + private static final long NS_PER_MS = 1000000; + + @Mock + private FalsingDataProvider mDataProvider; + @Mock + private DistanceClassifier mDistanceClassifier; + private FalsingClassifier mClassifier; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mDataProvider.getInteractionType()).thenReturn(GENERIC); + when(mDistanceClassifier.isLongSwipe()).thenReturn(false); + mClassifier = new ProximityClassifier(mDistanceClassifier, mDataProvider); + } + + @Test + public void testPass_uncovered() { + touchDown(); + touchUp(10); + assertThat(mClassifier.isFalseTouch(), is(false)); + } + + @Test + public void testPass_mostlyUncovered() { + touchDown(); + mClassifier.onSensorEvent(createSensorEvent(true, 1)); + mClassifier.onSensorEvent(createSensorEvent(false, 2)); + touchUp(20); + assertThat(mClassifier.isFalseTouch(), is(false)); + } + + @Test + public void testPass_quickSettings() { + touchDown(); + when(mDataProvider.getInteractionType()).thenReturn(QUICK_SETTINGS); + mClassifier.onSensorEvent(createSensorEvent(true, 1)); + mClassifier.onSensorEvent(createSensorEvent(false, 11)); + touchUp(10); + assertThat(mClassifier.isFalseTouch(), is(false)); + } + + @Test + public void testFail_covered() { + touchDown(); + mClassifier.onSensorEvent(createSensorEvent(true, 1)); + mClassifier.onSensorEvent(createSensorEvent(false, 11)); + touchUp(10); + assertThat(mClassifier.isFalseTouch(), is(true)); + } + + @Test + public void testFail_mostlyCovered() { + touchDown(); + mClassifier.onSensorEvent(createSensorEvent(true, 1)); + mClassifier.onSensorEvent(createSensorEvent(true, 95)); + mClassifier.onSensorEvent(createSensorEvent(true, 96)); + mClassifier.onSensorEvent(createSensorEvent(false, 100)); + touchUp(100); + assertThat(mClassifier.isFalseTouch(), is(true)); + } + + @Test + public void testPass_coveredWithLongSwipe() { + touchDown(); + mClassifier.onSensorEvent(createSensorEvent(true, 1)); + mClassifier.onSensorEvent(createSensorEvent(false, 11)); + touchUp(10); + when(mDistanceClassifier.isLongSwipe()).thenReturn(true); + assertThat(mClassifier.isFalseTouch(), is(false)); + } + + private void touchDown() { + MotionEvent motionEvent = MotionEvent.obtain(1, 1, MotionEvent.ACTION_DOWN, 0, 0, 0); + mClassifier.onTouchEvent(motionEvent); + motionEvent.recycle(); + } + + private void touchUp(long duration) { + MotionEvent motionEvent = MotionEvent.obtain(1, 1 + duration, MotionEvent.ACTION_UP, 0, + 100, 0); + + mClassifier.onTouchEvent(motionEvent); + + motionEvent.recycle(); + } + + private SensorEvent createSensorEvent(boolean covered, long timestampMs) { + SensorEvent sensorEvent = Mockito.mock(SensorEvent.class); + Sensor sensor = Mockito.mock(Sensor.class); + when(sensor.getType()).thenReturn(Sensor.TYPE_PROXIMITY); + when(sensor.getMaximumRange()).thenReturn(1f); + sensorEvent.sensor = sensor; + sensorEvent.timestamp = timestampMs * NS_PER_MS; + try { + Field valuesField = SensorEvent.class.getField("values"); + valuesField.setAccessible(true); + float[] sensorValue = {covered ? 0 : 1}; + try { + valuesField.set(sensorEvent, sensorValue); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + + return sensorEvent; + } +}