Merge "AOD: Fix broken triggers after failed prox check" into oc-dev
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 90e1c07..7139d59 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -43,6 +43,8 @@
     public static final int PULSE_REASON_SENSOR_PICKUP = 3;
     public static final int PULSE_REASON_SENSOR_DOUBLE_TAP = 4;
 
+    private static boolean sRegisterKeyguardCallback = true;
+
     private static long[] sTimes;
     private static String[] sMessages;
     private static int sPosition;
@@ -103,7 +105,9 @@
                     sProxStats[i][1] = new SummaryStats();
                 }
                 log("init");
-                KeyguardUpdateMonitor.getInstance(context).registerCallback(sKeyguardCallback);
+                if (sRegisterKeyguardCallback) {
+                    KeyguardUpdateMonitor.getInstance(context).registerCallback(sKeyguardCallback);
+                }
             }
         }
     }
@@ -218,6 +222,31 @@
         if (DEBUG) Log.d(TAG, msg);
     }
 
+    public static void tracePulseDropped(Context context, boolean pulsePending,
+            DozeMachine.State state, boolean blocked) {
+        if (!ENABLED) return;
+        init(context);
+        log("pulseDropped pulsePending=" + pulsePending + " state="
+                + state + " blocked=" + blocked);
+    }
+
+    public static void tracePulseCanceledByProx(Context context) {
+        if (!ENABLED) return;
+        init(context);
+        log("pulseCanceledByProx");
+    }
+
+    public static void setRegisterKeyguardCallback(boolean registerKeyguardCallback) {
+        if (!ENABLED) return;
+        synchronized (DozeLog.class) {
+            if (sRegisterKeyguardCallback != registerKeyguardCallback && sMessages != null) {
+                throw new IllegalStateException("Cannot change setRegisterKeyguardCallback "
+                        + "after init()");
+            }
+            sRegisterKeyguardCallback = registerKeyguardCallback;
+        }
+    }
+
     private static class SummaryStats {
         private int mCount;
 
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
index 1cc5fb9..38b32e9 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -42,7 +42,7 @@
     static final String TAG = "DozeMachine";
     static final boolean DEBUG = DozeService.DEBUG;
 
-    enum State {
+    public enum State {
         /** Default state. Transition to INITIALIZED to get Doze going. */
         UNINITIALIZED,
         /** Doze components are set up. Followed by transition to DOZE or DOZE_AOD. */
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 9b3593b..ea55c5f 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -119,6 +119,7 @@
         DozeMachine.State state = mMachine.getState();
         if (near && state == DozeMachine.State.DOZE_PULSING) {
             if (DEBUG) Log.i(TAG, "Prox NEAR, ending pulse");
+            DozeLog.tracePulseCanceledByProx(mContext);
             mMachine.requestState(DozeMachine.State.DOZE_PULSE_DONE);
         }
         if (far && state == DozeMachine.State.DOZE_AOD_PAUSED) {
@@ -181,6 +182,10 @@
         Assert.isMainThread();
         mDozeHost.extendPulse();
         if (mPulsePending || !mAllowPulseTriggers || !canPulse()) {
+            if (mAllowPulseTriggers) {
+                DozeLog.tracePulseDropped(mContext, mPulsePending, mMachine.getState(),
+                        mDozeHost.isPulsingBlocked());
+            }
             return;
         }
 
@@ -204,6 +209,7 @@
                 }
                 // avoid pulsing in pockets
                 if (result == RESULT_NEAR) {
+                    mPulsePending = false;
                     return;
                 }
 
@@ -221,6 +227,8 @@
     private void continuePulseRequest(int reason) {
         mPulsePending = false;
         if (mDozeHost.isPulsingBlocked() || !canPulse()) {
+            DozeLog.tracePulseDropped(mContext, mPulsePending, mMachine.getState(),
+                    mDozeHost.isPulsingBlocked());
             return;
         }
         mMachine.requestState(DozeMachine.State.DOZE_REQUEST_PULSE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java
new file mode 100644
index 0000000..3aef247
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 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.doze;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import com.android.internal.hardware.AmbientDisplayConfiguration;
+import com.android.systemui.statusbar.phone.DozeParameters;
+
+import org.mockito.Answers;
+import org.mockito.MockSettings;
+
+public class DozeConfigurationUtil {
+    public static DozeParameters createMockParameters() {
+        boolean[] doneHolder = new boolean[1];
+        DozeParameters params = mock(DozeParameters.class, noDefaultAnswer(doneHolder));
+
+        when(params.getPulseOnSigMotion()).thenReturn(false);
+        when(params.getSensorsWakeUpFully()).thenReturn(false);
+        when(params.getPickupVibrationThreshold()).thenReturn(0);
+        when(params.getProxCheckBeforePulse()).thenReturn(true);
+        when(params.getPickupSubtypePerformsProxCheck(anyInt())).thenReturn(true);
+
+        doneHolder[0] = true;
+        return params;
+    }
+
+    public static AmbientDisplayConfiguration createMockConfig() {
+        boolean[] doneHolder = new boolean[1];
+        AmbientDisplayConfiguration config = mock(AmbientDisplayConfiguration.class,
+                noDefaultAnswer(doneHolder));
+        when(config.pulseOnDoubleTapEnabled(anyInt())).thenReturn(false);
+        when(config.pulseOnPickupEnabled(anyInt())).thenReturn(false);
+        when(config.pulseOnNotificationEnabled(anyInt())).thenReturn(true);
+
+        when(config.doubleTapSensorType()).thenReturn(null);
+        when(config.pulseOnPickupAvailable()).thenReturn(false);
+
+        doneHolder[0] = true;
+        return config;
+    }
+
+    private static MockSettings noDefaultAnswer(boolean[] setupDoneHolder) {
+        return withSettings().defaultAnswer((i) -> {
+            if (setupDoneHolder[0]) {
+                throw new IllegalArgumentException("not defined");
+            } else {
+                return Answers.RETURNS_DEFAULTS.answer(i);
+            }
+        });
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeHostFake.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeHostFake.java
new file mode 100644
index 0000000..d2afa2a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeHostFake.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 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.doze;
+
+import android.annotation.NonNull;
+import android.app.PendingIntent;
+
+/**
+ * A rudimentary fake for DozeHost.
+ */
+class DozeHostFake implements DozeHost {
+    Callback callback;
+    private boolean pulseAborted;
+    private boolean pulseExtended;
+
+    @Override
+    public void addCallback(@NonNull Callback callback) {
+        this.callback = callback;
+    }
+
+    @Override
+    public void removeCallback(@NonNull Callback callback) {
+        this.callback = null;
+    }
+
+    @Override
+    public void startDozing() {
+        throw new RuntimeException("not implemented");
+    }
+
+    @Override
+    public void pulseWhileDozing(@NonNull PulseCallback callback, int reason) {
+        throw new RuntimeException("not implemented");
+    }
+
+    @Override
+    public void stopDozing() {
+        throw new RuntimeException("not implemented");
+    }
+
+    @Override
+    public void dozeTimeTick() {
+        throw new RuntimeException("not implemented");
+    }
+
+    @Override
+    public boolean isPowerSaveActive() {
+        return false;
+    }
+
+    @Override
+    public boolean isPulsingBlocked() {
+        return false;
+    }
+
+    @Override
+    public void startPendingIntentDismissingKeyguard(PendingIntent intent) {
+        throw new RuntimeException("not implemented");
+    }
+
+    @Override
+    public void abortPulsing() {
+        pulseAborted = true;
+    }
+
+    @Override
+    public void extendPulse() {
+        pulseExtended = true;
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
new file mode 100644
index 0000000..12e75a1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 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.doze;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+
+import com.android.internal.hardware.AmbientDisplayConfiguration;
+import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.util.wakelock.WakeLock;
+import com.android.systemui.util.wakelock.WakeLockFake;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.Answers;
+import org.mockito.MockSettings;
+
+public class DozeTriggersTest {
+    private Context mContext;
+    private DozeTriggers mTriggers;
+    private DozeMachine mMachine;
+    private DozeHostFake mHost;
+    private AmbientDisplayConfiguration mConfig;
+    private DozeParameters mParameters;
+    private SensorManagerFake mSensors;
+    private Handler mHandler;
+    private WakeLock mWakeLock;
+    private Instrumentation mInstrumentation;
+
+    @BeforeClass
+    public static void setupSuite() {
+        // We can't use KeyguardUpdateMonitor from tests.
+        DozeLog.setRegisterKeyguardCallback(false);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = InstrumentationRegistry.getContext();
+        mMachine = mock(DozeMachine.class);
+        mHost = new DozeHostFake();
+        mConfig = DozeConfigurationUtil.createMockConfig();
+        mParameters = DozeConfigurationUtil.createMockParameters();
+        mSensors = new SensorManagerFake(mContext);
+        mHandler = new Handler(Looper.getMainLooper());
+        mWakeLock = new WakeLockFake();
+
+        mInstrumentation.runOnMainSync(() -> {
+            mTriggers = new DozeTriggers(mContext, mMachine, mHost,
+                    mConfig, mParameters, mSensors, mHandler, mWakeLock, true);
+        });
+    }
+
+    @Test
+    @Ignore("setup crashes on virtual devices")
+    public void testOnNotification_stillWorksAfterOneFailedProxCheck() throws Exception {
+        when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE);
+
+        mInstrumentation.runOnMainSync(()->{
+            mTriggers.transitionTo(DozeMachine.State.UNINITIALIZED, DozeMachine.State.INITIALIZED);
+            mTriggers.transitionTo(DozeMachine.State.UNINITIALIZED, DozeMachine.State.DOZE);
+
+            mHost.callback.onNotificationHeadsUp();
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mSensors.PROXIMITY.sendProximityResult(false); /* Near */
+        });
+
+        verify(mMachine, never()).requestState(any());
+
+        mInstrumentation.runOnMainSync(()->{
+            mHost.callback.onNotificationHeadsUp();
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mSensors.PROXIMITY.sendProximityResult(true); /* Far */
+        });
+
+        verify(mMachine).requestState(DozeMachine.State.DOZE_REQUEST_PULSE);
+    }
+
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/SensorManagerFake.java b/packages/SystemUI/tests/src/com/android/systemui/doze/SensorManagerFake.java
new file mode 100644
index 0000000..5b4b891
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/SensorManagerFake.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2017 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.doze;
+
+import android.content.Context;
+import android.hardware.HardwareBuffer;
+import android.hardware.Sensor;
+import android.hardware.SensorAdditionalInfo;
+import android.hardware.SensorDirectChannel;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.TriggerEventListener;
+import android.os.Handler;
+import android.os.MemoryFile;
+import android.os.SystemClock;
+import android.util.ArraySet;
+
+import com.google.android.collect.Lists;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Rudimentary fake for SensorManager
+ *
+ * Currently only supports the proximity sensor.
+ *
+ * Note that this class ignores the "Handler" argument, so the test is responsible for calling the
+ * listener on the right thread.
+ */
+public class SensorManagerFake extends SensorManager {
+
+    public MockSensor PROXIMITY;
+
+    public SensorManagerFake(Context context) {
+        PROXIMITY = new MockSensor(context.getSystemService(SensorManager.class)
+                .getDefaultSensor(Sensor.TYPE_PROXIMITY));
+    }
+
+    @Override
+    protected List<Sensor> getFullSensorList() {
+        return Lists.newArrayList(PROXIMITY.sensor);
+    }
+
+    @Override
+    protected List<Sensor> getFullDynamicSensorList() {
+        return new ArrayList<>();
+    }
+
+    @Override
+    protected void unregisterListenerImpl(SensorEventListener listener, Sensor sensor) {
+        if (sensor == PROXIMITY.sensor || sensor == null) {
+            PROXIMITY.listeners.remove(listener);
+        }
+    }
+
+    @Override
+    protected boolean registerListenerImpl(SensorEventListener listener, Sensor sensor,
+            int delayUs,
+            Handler handler, int maxReportLatencyUs, int reservedFlags) {
+        if (sensor == PROXIMITY.sensor) {
+            PROXIMITY.listeners.add(listener);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected boolean flushImpl(SensorEventListener listener) {
+        return false;
+    }
+
+    @Override
+    protected SensorDirectChannel createDirectChannelImpl(MemoryFile memoryFile,
+            HardwareBuffer hardwareBuffer) {
+        return null;
+    }
+
+    @Override
+    protected void destroyDirectChannelImpl(SensorDirectChannel channel) {
+
+    }
+
+    @Override
+    protected int configureDirectChannelImpl(SensorDirectChannel channel, Sensor s, int rate) {
+        return 0;
+    }
+
+    @Override
+    protected void registerDynamicSensorCallbackImpl(DynamicSensorCallback callback,
+            Handler handler) {
+
+    }
+
+    @Override
+    protected void unregisterDynamicSensorCallbackImpl(
+            DynamicSensorCallback callback) {
+
+    }
+
+    @Override
+    protected boolean requestTriggerSensorImpl(TriggerEventListener listener, Sensor sensor) {
+        return false;
+    }
+
+    @Override
+    protected boolean cancelTriggerSensorImpl(TriggerEventListener listener, Sensor sensor,
+            boolean disable) {
+        return false;
+    }
+
+    @Override
+    protected boolean initDataInjectionImpl(boolean enable) {
+        return false;
+    }
+
+    @Override
+    protected boolean injectSensorDataImpl(Sensor sensor, float[] values, int accuracy,
+            long timestamp) {
+        return false;
+    }
+
+    @Override
+    protected boolean setOperationParameterImpl(SensorAdditionalInfo parameter) {
+        return false;
+    }
+
+    public class MockSensor {
+        final Sensor sensor;
+        final ArraySet<SensorEventListener> listeners = new ArraySet<>();
+
+        private MockSensor(Sensor sensor) {
+            this.sensor = sensor;
+        }
+
+        public void sendProximityResult(boolean far) {
+            SensorEvent event = createSensorEvent(1);
+            event.values[0] = far ? sensor.getMaximumRange() : 0;
+            for (SensorEventListener listener : listeners) {
+                listener.onSensorChanged(event);
+            }
+        }
+
+        private SensorEvent createSensorEvent(int valuesSize) {
+            SensorEvent event;
+            try {
+                Constructor<SensorEvent> constr =
+                        SensorEvent.class.getDeclaredConstructor(Integer.TYPE);
+                constr.setAccessible(true);
+                event = constr.newInstance(valuesSize);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+            event.sensor = sensor;
+            event.timestamp = SystemClock.elapsedRealtimeNanos();
+
+            return event;
+        }
+    }
+}