Doze: Add config for fine grained proximity checks

Bug: 29619338
Change-Id: Ic9ff7fc78f47873858881c1a4ac14bc2b834ab84
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 134388f..fa30f49 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -224,9 +224,25 @@
     <!-- Doze: duration to avoid false pickup gestures triggered by notification vibrations -->
     <integer name="doze_pickup_vibration_threshold">2000</integer>
 
-    <!-- Doze: can we assume the pickup sensor includes a proximity check? -->
+    <!-- Doze: can we assume the pickup sensor includes a proximity check?
+         This is ignored if doze_pickup_subtype_performs_proximity_check is not empty.
+         @deprecated: use doze_pickup_subtype_performs_proximity_check instead.-->
     <bool name="doze_pickup_performs_proximity_check">false</bool>
 
+    <!-- Doze: a list of pickup sensor subtypes that perform a proximity check before they trigger.
+               If not empty, either * or !* must appear to specify the default.
+               If empty, falls back to doze_pickup_performs_proximity_check.
+
+               Examples: 1,2,3,!* -> subtypes 1,2 and 3 perform the check, all others don't.
+                         !1,!2,*  -> subtypes 1 and 2 don't perform the check, all others do.
+                         !8,*     -> subtype 8 does not perform the check, all others do
+                         1,1,*    -> illegal, every item may only appear once
+                         1,!1,*   -> illegal, no contradictions allowed
+                         1,2      -> illegal, need either * or !*
+                         1,,4a3   -> illegal, no empty or non-numeric terms allowed
+    -->
+    <string name="doze_pickup_subtype_performs_proximity_check"></string>
+
     <!-- Doze: pulse parameter - how long does it take to fade in? -->
     <integer name="doze_pulse_duration_in">900</integer>
 
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index 5f1b042..661b347 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -214,6 +214,10 @@
     }
 
     private void requestPulse(final int reason) {
+        requestPulse(reason, false /* performedProxCheck */);
+    }
+
+    private void requestPulse(final int reason, boolean performedProxCheck) {
         if (mHost != null && mDreaming && !mPulsing) {
             // Let the host know we want to pulse.  Wait for it to be ready, then
             // turn the screen on.  When finished, turn the screen off again.
@@ -226,10 +230,9 @@
                 return;
             }
             final long start = SystemClock.uptimeMillis();
-            final boolean nonBlocking = reason == DozeLog.PULSE_REASON_SENSOR_PICKUP
-                    && mDozeParameters.getPickupPerformsProxCheck();
-            if (nonBlocking) {
-                // proximity check is only done to capture statistics, continue pulsing
+            if (performedProxCheck) {
+                // the caller already performed a successful proximity check; we'll only do one to
+                // capture statistics, continue pulsing immediately.
                 continuePulsing(reason);
             }
             // perform a proximity check
@@ -239,7 +242,7 @@
                     final boolean isNear = result == RESULT_NEAR;
                     final long end = SystemClock.uptimeMillis();
                     DozeLog.traceProximityResult(mContext, isNear, end - start, reason);
-                    if (nonBlocking) {
+                    if (performedProxCheck) {
                         // we already continued
                         return;
                     }
@@ -540,9 +543,12 @@
             mWakeLock.acquire();
             try {
                 if (DEBUG) Log.d(mTag, "onTrigger: " + triggerEventToString(event));
+                boolean sensorPerformsProxCheck = false;
                 if (mSensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
                     int subType = (int) event.values[0];
                     MetricsLogger.action(mContext, MetricsEvent.ACTION_AMBIENT_GESTURE, subType);
+                    sensorPerformsProxCheck = mDozeParameters.getPickupSubtypePerformsProxCheck(
+                            subType);
                 }
                 if (mDebugVibrate) {
                     final Vibrator v = (Vibrator) mContext.getSystemService(
@@ -555,7 +561,7 @@
                 }
 
                 mRegistered = false;
-                requestPulse(mPulseReason);
+                requestPulse(mPulseReason, sensorPerformsProxCheck);
                 updateListener();  // reregister, this sensor only fires once
 
                 // reset the notification pulse schedule, but only if we think we were not triggered
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index 1d890d0..9b3ed33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -21,6 +21,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.MathUtils;
+import android.util.SparseBooleanArray;
 
 import com.android.systemui.R;
 
@@ -39,6 +40,8 @@
 
     private static PulseSchedule sPulseSchedule;
 
+    private static IntInOutMatcher sPickupSubtypePerformsProxMatcher;
+
     public DozeParameters(Context context) {
         mContext = context;
     }
@@ -61,7 +64,20 @@
         pw.print("    getPulseSchedule(): "); pw.println(getPulseSchedule());
         pw.print("    getPulseScheduleResets(): "); pw.println(getPulseScheduleResets());
         pw.print("    getPickupVibrationThreshold(): "); pw.println(getPickupVibrationThreshold());
-        pw.print("    getPickupPerformsProxCheck(): "); pw.println(getPickupPerformsProxCheck());
+        pw.print("    getPickupSubtypePerformsProxCheck(): ");pw.println(
+                dumpPickupSubtypePerformsProxCheck());
+    }
+
+    private String dumpPickupSubtypePerformsProxCheck() {
+        // Refresh sPickupSubtypePerformsProxMatcher
+        getPickupSubtypePerformsProxCheck(0);
+
+        if (sPickupSubtypePerformsProxMatcher == null) {
+            return "fallback: " + mContext.getResources().getBoolean(
+                    R.bool.doze_pickup_performs_proximity_check);
+        } else {
+            return "spec: " + sPickupSubtypePerformsProxMatcher.mSpec;
+        }
     }
 
     public boolean getDisplayStateSupported() {
@@ -106,10 +122,6 @@
         return getBoolean("doze.pulse.proxcheck", R.bool.doze_proximity_check_before_pulse);
     }
 
-    public boolean getPickupPerformsProxCheck() {
-        return getBoolean("doze.pickup.proxcheck", R.bool.doze_pickup_performs_proximity_check);
-    }
-
     public boolean getPulseOnNotifications() {
         return getBoolean("doze.pulse.notifications", R.bool.doze_pulse_on_notifications);
     }
@@ -143,6 +155,101 @@
         return SystemProperties.get(propName, mContext.getString(resId));
     }
 
+    public boolean getPickupSubtypePerformsProxCheck(int subType) {
+        String spec = getString("doze.pickup.proxcheck",
+                R.string.doze_pickup_subtype_performs_proximity_check);
+
+        if (TextUtils.isEmpty(spec)) {
+            // Fall back to non-subtype based property.
+            return mContext.getResources().getBoolean(R.bool.doze_pickup_performs_proximity_check);
+        }
+
+        if (sPickupSubtypePerformsProxMatcher == null
+                || !TextUtils.equals(spec, sPickupSubtypePerformsProxMatcher.mSpec)) {
+            sPickupSubtypePerformsProxMatcher = new IntInOutMatcher(spec);
+        }
+
+        return sPickupSubtypePerformsProxMatcher.isIn(subType);
+    }
+
+
+    /**
+     * Parses a spec of the form `1,2,3,!5,*`. The resulting object will match numbers that are
+     * listed, will not match numbers that are listed with a ! prefix, and will match / not match
+     * unlisted numbers depending on whether * or !* is present.
+     *
+     * *  -> match any numbers that are not explicitly listed
+     * !* -> don't match any numbers that are not explicitly listed
+     * 2  -> match 2
+     * !3 -> don't match 3
+     *
+     * It is illegal to specify:
+     * - an empty spec
+     * - a spec containing that are empty, or a lone !
+     * - a spec for anything other than numbers or *
+     * - multiple terms for the same number / multiple *s
+     */
+    public static class IntInOutMatcher {
+        private static final String WILDCARD = "*";
+        private static final char OUT_PREFIX = '!';
+
+        private final SparseBooleanArray mIsIn;
+        private final boolean mDefaultIsIn;
+        final String mSpec;
+
+        public IntInOutMatcher(String spec) {
+            if (TextUtils.isEmpty(spec)) {
+                throw new IllegalArgumentException("Spec must not be empty");
+            }
+
+            boolean defaultIsIn = false;
+            boolean foundWildcard = false;
+
+            mSpec = spec;
+            mIsIn = new SparseBooleanArray();
+
+            for (String itemPrefixed : spec.split(",", -1)) {
+                if (itemPrefixed.length() == 0) {
+                    throw new IllegalArgumentException(
+                            "Illegal spec, must not have zero-length items: `" + spec + "`");
+                }
+                boolean isIn = itemPrefixed.charAt(0) != OUT_PREFIX;
+                String item = isIn ? itemPrefixed : itemPrefixed.substring(1);
+
+                if (itemPrefixed.length() == 0) {
+                    throw new IllegalArgumentException(
+                            "Illegal spec, must not have zero-length items: `" + spec + "`");
+                }
+
+                if (WILDCARD.equals(item)) {
+                    if (foundWildcard) {
+                        throw new IllegalArgumentException("Illegal spec, `" + WILDCARD +
+                                "` must not appear multiple times in `" + spec + "`");
+                    }
+                    defaultIsIn = isIn;
+                    foundWildcard = true;
+                } else {
+                    int key = Integer.parseInt(item);
+                    if (mIsIn.indexOfKey(key) >= 0) {
+                        throw new IllegalArgumentException("Illegal spec, `" + key +
+                                "` must not appear multiple times in `" + spec + "`");
+                    }
+                    mIsIn.put(key, isIn);
+                }
+            }
+
+            if (!foundWildcard) {
+                throw new IllegalArgumentException("Illegal spec, must specify either * or !*");
+            }
+
+            mDefaultIsIn = defaultIsIn;
+        }
+
+        public boolean isIn(int value) {
+            return (mIsIn.get(value, mDefaultIsIn));
+        }
+    }
+
     public static class PulseSchedule {
         private static final Pattern PATTERN = Pattern.compile("(\\d+?)s", 0);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/phone/DozeParametersTests.java b/packages/SystemUI/tests/src/com/android/systemui/phone/DozeParametersTests.java
new file mode 100644
index 0000000..07334f3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/phone/DozeParametersTests.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2016 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.phone;
+
+import com.android.systemui.statusbar.phone.DozeParameters.IntInOutMatcher;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
+public class DozeParametersTests extends AndroidTestCase {
+
+    public void test_inOutMatcher_defaultIn() {
+        IntInOutMatcher intInOutMatcher = new IntInOutMatcher("*");
+
+        assertTrue(intInOutMatcher.isIn(1));
+        assertTrue(intInOutMatcher.isIn(-1));
+        assertTrue(intInOutMatcher.isIn(0));
+    }
+
+    public void test_inOutMatcher_defaultOut() {
+        IntInOutMatcher intInOutMatcher = new IntInOutMatcher("!*");
+
+        assertFalse(intInOutMatcher.isIn(1));
+        assertFalse(intInOutMatcher.isIn(-1));
+        assertFalse(intInOutMatcher.isIn(0));
+    }
+
+    public void test_inOutMatcher_someIn() {
+        IntInOutMatcher intInOutMatcher = new IntInOutMatcher("1,2,3,!*");
+
+        assertTrue(intInOutMatcher.isIn(1));
+        assertTrue(intInOutMatcher.isIn(2));
+        assertTrue(intInOutMatcher.isIn(3));
+
+        assertFalse(intInOutMatcher.isIn(0));
+        assertFalse(intInOutMatcher.isIn(4));
+    }
+
+    public void test_inOutMatcher_someOut() {
+        IntInOutMatcher intInOutMatcher = new IntInOutMatcher("!1,!2,!3,*");
+
+        assertFalse(intInOutMatcher.isIn(1));
+        assertFalse(intInOutMatcher.isIn(2));
+        assertFalse(intInOutMatcher.isIn(3));
+
+        assertTrue(intInOutMatcher.isIn(0));
+        assertTrue(intInOutMatcher.isIn(4));
+    }
+
+    public void test_inOutMatcher_mixed() {
+        IntInOutMatcher intInOutMatcher = new IntInOutMatcher("!1,2,!3,*");
+
+        assertFalse(intInOutMatcher.isIn(1));
+        assertTrue(intInOutMatcher.isIn(2));
+        assertFalse(intInOutMatcher.isIn(3));
+
+        assertTrue(intInOutMatcher.isIn(0));
+        assertTrue(intInOutMatcher.isIn(4));
+    }
+
+    public void test_inOutMatcher_failEmpty() {
+        try {
+            new IntInOutMatcher("");
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void test_inOutMatcher_failNull() {
+        try {
+            new IntInOutMatcher(null);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void test_inOutMatcher_failEmptyClause() {
+        try {
+            new IntInOutMatcher("!1,*,");
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void test_inOutMatcher_failDuplicate() {
+        try {
+            new IntInOutMatcher("!1,*,!1");
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void test_inOutMatcher_failDuplicateDefault() {
+        try {
+            new IntInOutMatcher("!1,*,*");
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void test_inOutMatcher_failMalformedNot() {
+        try {
+            new IntInOutMatcher("!,*");
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void test_inOutMatcher_failText() {
+        try {
+            new IntInOutMatcher("!abc,*");
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void test_inOutMatcher_failContradiction() {
+        try {
+            new IntInOutMatcher("1,!1,*");
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void test_inOutMatcher_failContradictionDefault() {
+        try {
+            new IntInOutMatcher("1,*,!*");
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void test_inOutMatcher_failMissingDefault() {
+        try {
+            new IntInOutMatcher("1");
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+}
\ No newline at end of file