Make DozeSensors, DozeTriggers use ProximitySensor

This CL takes the features used in DozeSensors and applies
them to the publicly available util.sensors.ProximitySensor class.

It introduces a RateLimiter class for temprorarily suspending the
ProximitySensor as requested.

It introduces a ProximityCheck class for a quick, one time test
of the proximity sensor.

There is some reshuffling of Sensor related code to its own package
as well.

Bug: 141122220
Test: atest SystemUITests
Change-Id: If126be9a56c6966c1678ee70fa6f4623e51361a7
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index bd5b9c7..ca6803d 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -15,6 +15,7 @@
 package com.android.systemui;
 
 import android.annotation.Nullable;
+import android.app.AlarmManager;
 import android.app.INotificationManager;
 import android.content.res.Configuration;
 import android.hardware.SensorPrivacyManager;
@@ -110,10 +111,10 @@
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.tuner.TunablePadding.TunablePaddingService;
 import com.android.systemui.tuner.TunerService;
-import com.android.systemui.util.AsyncSensorManager;
 import com.android.systemui.util.leak.GarbageMonitor;
 import com.android.systemui.util.leak.LeakDetector;
 import com.android.systemui.util.leak.LeakReporter;
+import com.android.systemui.util.sensors.AsyncSensorManager;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -314,6 +315,7 @@
     @Inject Lazy<INotificationManager> mINotificationManager;
     @Inject Lazy<FalsingManager> mFalsingManager;
     @Inject Lazy<SysUiState> mSysUiStateFlagsContainer;
+    @Inject Lazy<AlarmManager> mAlarmManager;
 
     @Inject
     public Dependency() {
@@ -508,6 +510,7 @@
         mProviders.put(INotificationManager.class, mINotificationManager::get);
         mProviders.put(FalsingManager.class, mFalsingManager::get);
         mProviders.put(SysUiState.class, mSysUiStateFlagsContainer::get);
+        mProviders.put(AlarmManager.class, mAlarmManager::get);
 
         // TODO(b/118592525): to support multi-display , we start to add something which is
         //                    per-display, while others may be global. I think it's time to add
diff --git a/packages/SystemUI/src/com/android/systemui/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/DependencyProvider.java
index d46a86c..239cbfe 100644
--- a/packages/SystemUI/src/com/android/systemui/DependencyProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/DependencyProvider.java
@@ -23,6 +23,7 @@
 import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
 
 import android.annotation.Nullable;
+import android.app.AlarmManager;
 import android.app.INotificationManager;
 import android.content.Context;
 import android.hardware.SensorPrivacyManager;
@@ -230,4 +231,11 @@
             @Named(MAIN_HANDLER_NAME) Handler mainHandler) {
         return new DeviceProvisionedControllerImpl(context, mainHandler);
     }
+
+    /** */
+    @Singleton
+    @Provides
+    public AlarmManager provideAlarmManager(Context context) {
+        return context.getSystemService(AlarmManager.class);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/SystemUIModule.java
index ff4eb83..38f2f5e8 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIModule.java
@@ -24,7 +24,7 @@
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.phone.KeyguardLiftController;
-import com.android.systemui.util.AsyncSensorManager;
+import com.android.systemui.util.sensors.AsyncSensorManager;
 
 import javax.inject.Singleton;
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java
index c2b0fe4..20742d6 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java
@@ -43,7 +43,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.util.AsyncSensorManager;
+import com.android.systemui.util.sensors.AsyncSensorManager;
 
 import java.io.PrintWriter;
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
index 85bc22b..914258f 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
@@ -20,6 +20,7 @@
 import static com.android.systemui.Dependency.MAIN_HANDLER_NAME;
 
 import android.content.Context;
+import android.hardware.SensorManager;
 import android.net.Uri;
 import android.os.Handler;
 import android.provider.DeviceConfig;
@@ -35,7 +36,7 @@
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.util.DeviceConfigProxy;
-import com.android.systemui.util.ProximitySensor;
+import com.android.systemui.util.sensors.ProximitySensor;
 
 import java.io.PrintWriter;
 
@@ -66,6 +67,7 @@
             DeviceConfigProxy deviceConfig) {
         mProximitySensor = proximitySensor;
         mProximitySensor.setTag(PROXIMITY_SENSOR_TAG);
+        mProximitySensor.setSensorDelay(SensorManager.SENSOR_DELAY_GAME);
         mDeviceConfig = deviceConfig;
         mDeviceConfigListener =
                 properties -> onDeviceConfigPropertiesChanged(context, properties.getNamespace());
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 3f5cae6..0aa66af 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java
@@ -30,7 +30,7 @@
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.util.DeviceConfigProxy;
-import com.android.systemui.util.ProximitySensor;
+import com.android.systemui.util.sensors.ProximitySensor;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java
index bf39751..53ca783 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java
@@ -19,7 +19,7 @@
 import android.view.MotionEvent;
 
 import com.android.systemui.classifier.Classifier;
-import com.android.systemui.util.ProximitySensor;
+import com.android.systemui.util.sensors.ProximitySensor;
 
 import java.util.List;
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java
index eeca409..f0feb4e 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/ProximityClassifier.java
@@ -23,7 +23,7 @@
 import android.view.MotionEvent;
 
 import com.android.systemui.util.DeviceConfigProxy;
-import com.android.systemui.util.ProximitySensor;
+import com.android.systemui.util.sensors.ProximitySensor;
 
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java b/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java
index c367286..bb8c7f1 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java
@@ -33,7 +33,7 @@
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 import com.android.systemui.statusbar.phone.DozeParameters;
-import com.android.systemui.util.AsyncSensorManager;
+import com.android.systemui.util.sensors.AsyncSensorManager;
 import com.android.systemui.util.wakelock.DelayedWakeLock;
 import com.android.systemui.util.wakelock.WakeLock;
 
@@ -45,7 +45,7 @@
     /** Creates a DozeMachine with its parts for {@code dozeService}. */
     public DozeMachine assembleMachine(DozeService dozeService, FalsingManager falsingManager) {
         Context context = dozeService;
-        SensorManager sensorManager = Dependency.get(AsyncSensorManager.class);
+        AsyncSensorManager sensorManager = Dependency.get(AsyncSensorManager.class);
         AlarmManager alarmManager = context.getSystemService(AlarmManager.class);
         DockManager dockManager = Dependency.get(DockManager.class);
         WakefulnessLifecycle wakefulnessLifecycle = Dependency.get(WakefulnessLifecycle.class);
@@ -91,7 +91,7 @@
                 params.getPolicy());
     }
 
-    private DozeTriggers createDozeTriggers(Context context, SensorManager sensorManager,
+    private DozeTriggers createDozeTriggers(Context context, AsyncSensorManager sensorManager,
             DozeHost host, AlarmManager alarmManager, AmbientDisplayConfiguration config,
             DozeParameters params, Handler handler, WakeLock wakeLock, DozeMachine machine,
             DockManager dockManager) {
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 026a625..7c1c040 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -26,7 +26,6 @@
 import android.content.Context;
 import android.database.ContentObserver;
 import android.hardware.Sensor;
-import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.hardware.TriggerEvent;
 import android.hardware.TriggerEventListener;
@@ -43,11 +42,13 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto;
-import com.android.systemui.R;
+import com.android.systemui.Dependency;
 import com.android.systemui.plugins.SensorManagerPlugin;
+import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.phone.DozeParameters;
-import com.android.systemui.util.AlarmTimeout;
-import com.android.systemui.util.AsyncSensorManager;
+import com.android.systemui.util.sensors.AsyncSensorManager;
+import com.android.systemui.util.sensors.ProximitySensor;
+import com.android.systemui.util.sensors.SensorRateLimiter;
 import com.android.systemui.util.wakelock.WakeLock;
 
 import java.io.PrintWriter;
@@ -62,7 +63,7 @@
 
     private final Context mContext;
     private final AlarmManager mAlarmManager;
-    private final SensorManager mSensorManager;
+    private final AsyncSensorManager mSensorManager;
     private final ContentResolver mResolver;
     private final TriggerSensor mPickupSensor;
     private final DozeParameters mDozeParameters;
@@ -74,13 +75,13 @@
     protected TriggerSensor[] mSensors;
 
     private final Handler mHandler = new Handler();
-    private final ProxSensor mProxSensor;
+    private final ProximitySensor mProximitySensor;
     private long mDebounceFrom;
     private boolean mSettingRegistered;
     private boolean mListening;
     private boolean mPaused;
 
-    public DozeSensors(Context context, AlarmManager alarmManager, SensorManager sensorManager,
+    public DozeSensors(Context context, AlarmManager alarmManager, AsyncSensorManager sensorManager,
             DozeParameters dozeParameters, AmbientDisplayConfiguration config, WakeLock wakeLock,
             Callback callback, Consumer<Boolean> proxCallback, AlwaysOnDisplayPolicy policy) {
         mContext = context;
@@ -91,6 +92,7 @@
         mWakeLock = wakeLock;
         mProxCallback = proxCallback;
         mResolver = mContext.getContentResolver();
+        mCallback = callback;
 
         boolean alwaysOn = mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT);
         mSensors = new TriggerSensor[] {
@@ -146,8 +148,13 @@
                         false /* touchscreen */, mConfig.getWakeLockScreenDebounce()),
         };
 
-        mProxSensor = new ProxSensor(policy);
-        mCallback = callback;
+        mProximitySensor = new ProximitySensor(
+                context, sensorManager, Dependency.get(PluginManager.class));
+        new SensorRateLimiter(mProximitySensor, mAlarmManager, policy.proxCooldownTriggerMs,
+                policy.proxCooldownPeriodMs, "prox_sensor");
+
+        mProximitySensor.register(
+                proximityEvent -> mProxCallback.accept(!proximityEvent.getNear()));
     }
 
     /**
@@ -236,7 +243,15 @@
     }
 
     public void setProxListening(boolean listen) {
-        mProxSensor.setRequested(listen);
+        if (mProximitySensor.isRegistered() && listen) {
+            mProximitySensor.alertListeners();
+        } else {
+            if (listen) {
+                mProximitySensor.resume();
+            } else {
+                mProximitySensor.pause();
+            }
+        }
     }
 
     private final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
@@ -267,115 +282,16 @@
     /** Dump current state */
     public void dump(PrintWriter pw) {
         for (TriggerSensor s : mSensors) {
-            pw.print("  Sensor: "); pw.println(s.toString());
+            pw.println("  Sensor: " + s.toString());
         }
-        pw.print("  ProxSensor: "); pw.println(mProxSensor.toString());
+        pw.println("  ProxSensor: " + mProximitySensor.toString());
     }
 
     /**
-     * @return true if prox is currently far, false if near or null if unknown.
+     * @return true if prox is currently near, false if far or null if unknown.
      */
-    public Boolean isProximityCurrentlyFar() {
-        return mProxSensor.mCurrentlyFar;
-    }
-
-    private class ProxSensor implements SensorEventListener {
-
-        boolean mRequested;
-        boolean mRegistered;
-        Boolean mCurrentlyFar;
-        long mLastNear;
-        final AlarmTimeout mCooldownTimer;
-        final AlwaysOnDisplayPolicy mPolicy;
-        final Sensor mSensor;
-        final boolean mUsingBrightnessSensor;
-
-        public ProxSensor(AlwaysOnDisplayPolicy policy) {
-            mPolicy = policy;
-            mCooldownTimer = new AlarmTimeout(mAlarmManager, this::updateRegistered,
-                    "prox_cooldown", mHandler);
-
-            // The default prox sensor can be noisy, so let's use a prox gated brightness sensor
-            // if available.
-            Sensor sensor = DozeSensors.findSensorWithType(mSensorManager,
-                    mContext.getString(R.string.doze_brightness_sensor_type));
-            mUsingBrightnessSensor = sensor != null;
-            if (sensor == null) {
-                sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
-            }
-            mSensor = sensor;
-        }
-
-        void setRequested(boolean requested) {
-            if (mRequested == requested) {
-                // Send an update even if we don't re-register.
-                mHandler.post(() -> {
-                    if (mCurrentlyFar != null) {
-                        mProxCallback.accept(mCurrentlyFar);
-                    }
-                });
-                return;
-            }
-            mRequested = requested;
-            updateRegistered();
-        }
-
-        private void updateRegistered() {
-            setRegistered(mRequested && !mCooldownTimer.isScheduled());
-        }
-
-        private void setRegistered(boolean register) {
-            if (mRegistered == register) {
-                return;
-            }
-            if (register) {
-                mRegistered = mSensorManager.registerListener(this, mSensor,
-                        SensorManager.SENSOR_DELAY_NORMAL, mHandler);
-            } else {
-                mSensorManager.unregisterListener(this);
-                mRegistered = false;
-                mCurrentlyFar = null;
-            }
-        }
-
-        @Override
-        public void onSensorChanged(android.hardware.SensorEvent event) {
-            if (DEBUG) Log.d(TAG, "onSensorChanged " + event);
-
-            if (mUsingBrightnessSensor) {
-                // The custom brightness sensor is gated by the proximity sensor and will return 0
-                // whenever prox is covered.
-                mCurrentlyFar = event.values[0] > 0;
-            } else {
-                mCurrentlyFar = event.values[0] >= event.sensor.getMaximumRange();
-            }
-            mProxCallback.accept(mCurrentlyFar);
-
-            long now = SystemClock.elapsedRealtime();
-            if (mCurrentlyFar == null) {
-                // Sensor has been unregistered by the proxCallback. Do nothing.
-            } else if (!mCurrentlyFar) {
-                mLastNear = now;
-            } else if (mCurrentlyFar && now - mLastNear < mPolicy.proxCooldownTriggerMs) {
-                // If the last near was very recent, we might be using more power for prox
-                // wakeups than we're saving from turning of the screen. Instead, turn it off
-                // for a while.
-                mCooldownTimer.schedule(mPolicy.proxCooldownPeriodMs,
-                        AlarmTimeout.MODE_IGNORE_IF_SCHEDULED);
-                updateRegistered();
-            }
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-        }
-
-        @Override
-        public String toString() {
-            return String.format("{registered=%s, requested=%s, coolingDown=%s, currentlyFar=%s,"
-                    + " sensor=%s}", mRegistered, mRequested, mCooldownTimer.isScheduled(),
-                    mCurrentlyFar, mSensor);
-        }
+    public Boolean isProximityCurrentlyNear() {
+        return mProximitySensor.isNear();
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index bab64db..80d4b63 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -24,10 +24,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Configuration;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.metrics.LogMaker;
 import android.os.Handler;
@@ -39,16 +35,16 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.util.Preconditions;
 import com.android.systemui.Dependency;
-import com.android.systemui.R;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.util.Assert;
+import com.android.systemui.util.sensors.AsyncSensorManager;
+import com.android.systemui.util.sensors.ProximitySensor;
 import com.android.systemui.util.wakelock.WakeLock;
 
 import java.io.PrintWriter;
-import java.util.function.IntConsumer;
+import java.util.function.Consumer;
 
 /**
  * Handles triggers for ambient state changes.
@@ -67,13 +63,15 @@
      */
     private static boolean sWakeDisplaySensorState = true;
 
+    private static final int PROXIMITY_TIMEOUT_DELAY_MS = 500;
+
     private final Context mContext;
     private final DozeMachine mMachine;
     private final DozeSensors mDozeSensors;
     private final DozeHost mDozeHost;
     private final AmbientDisplayConfiguration mConfig;
     private final DozeParameters mDozeParameters;
-    private final SensorManager mSensorManager;
+    private final AsyncSensorManager mSensorManager;
     private final Handler mHandler;
     private final WakeLock mWakeLock;
     private final boolean mAllowPulseTriggers;
@@ -81,6 +79,7 @@
     private final TriggerReceiver mBroadcastReceiver = new TriggerReceiver();
     private final DockEventListener mDockEventListener = new DockEventListener();
     private final DockManager mDockManager;
+    private final ProximitySensor.ProximityCheck mProxCheck;
 
     private long mNotificationPulseTime;
     private boolean mPulsePending;
@@ -89,7 +88,7 @@
 
     public DozeTriggers(Context context, DozeMachine machine, DozeHost dozeHost,
             AlarmManager alarmManager, AmbientDisplayConfiguration config,
-            DozeParameters dozeParameters, SensorManager sensorManager, Handler handler,
+            DozeParameters dozeParameters, AsyncSensorManager sensorManager, Handler handler,
             WakeLock wakeLock, boolean allowPulseTriggers, DockManager dockManager) {
         mContext = context;
         mMachine = machine;
@@ -105,6 +104,9 @@
                 dozeParameters.getPolicy());
         mUiModeManager = mContext.getSystemService(UiModeManager.class);
         mDockManager = dockManager;
+        mProxCheck = new ProximitySensor.ProximityCheck(
+                new ProximitySensor(mContext, mSensorManager, null),
+                mHandler);
     }
 
     private void onNotification(Runnable onPulseSuppressedListener) {
@@ -134,25 +136,27 @@
         }
     }
 
-    private void proximityCheckThenCall(IntConsumer callback,
+    private void proximityCheckThenCall(Consumer<Boolean> callback,
             boolean alreadyPerformedProxCheck,
             int reason) {
-        Boolean cachedProxFar = mDozeSensors.isProximityCurrentlyFar();
+        Boolean cachedProxNear = mDozeSensors.isProximityCurrentlyNear();
         if (alreadyPerformedProxCheck) {
-            callback.accept(ProximityCheck.RESULT_NOT_CHECKED);
-        } else if (cachedProxFar != null) {
-            callback.accept(cachedProxFar ? ProximityCheck.RESULT_FAR : ProximityCheck.RESULT_NEAR);
+            callback.accept(null);
+        } else if (cachedProxNear != null) {
+            callback.accept(cachedProxNear);
         } else {
             final long start = SystemClock.uptimeMillis();
-            new ProximityCheck() {
-                @Override
-                public void onProximityResult(int result) {
-                    final long end = SystemClock.uptimeMillis();
-                    DozeLog.traceProximityResult(mContext, result == RESULT_NEAR,
-                            end - start, reason);
-                    callback.accept(result);
-                }
-            }.check();
+            mProxCheck.check(PROXIMITY_TIMEOUT_DELAY_MS, near -> {
+                final long end = SystemClock.uptimeMillis();
+                DozeLog.traceProximityResult(
+                        mContext,
+                        near == null ? false : near,
+                        end - start,
+                        reason);
+                callback.accept(near);
+                mWakeLock.release(TAG);
+            });
+            mWakeLock.acquire(TAG);
         }
     }
 
@@ -178,7 +182,7 @@
             }
         } else {
             proximityCheckThenCall((result) -> {
-                if (result == ProximityCheck.RESULT_NEAR) {
+                if (result) {
                     // In pocket, drop event.
                     return;
                 }
@@ -267,7 +271,7 @@
 
         if (wake) {
             proximityCheckThenCall((result) -> {
-                if (result == ProximityCheck.RESULT_NEAR) {
+                if (result) {
                     // In pocket, drop event.
                     return;
                 }
@@ -376,7 +380,7 @@
 
         mPulsePending = true;
         proximityCheckThenCall((result) -> {
-            if (result == ProximityCheck.RESULT_NEAR) {
+            if (result) {
                 // in pocket, abort pulse
                 DozeLog.tracePulseDropped(mContext, "inPocket");
                 mPulsePending = false;
@@ -412,104 +416,11 @@
         pw.print(" notificationPulseTime=");
         pw.println(Formatter.formatShortElapsedTime(mContext, mNotificationPulseTime));
 
-        pw.print(" pulsePending="); pw.println(mPulsePending);
+        pw.println(" pulsePending=" + mPulsePending);
         pw.println("DozeSensors:");
         mDozeSensors.dump(pw);
     }
 
-    /**
-     * @see DozeSensors.ProxSensor
-     */
-    private abstract class ProximityCheck implements SensorEventListener, Runnable {
-        private static final int TIMEOUT_DELAY_MS = 500;
-
-        protected static final int RESULT_UNKNOWN = 0;
-        protected static final int RESULT_NEAR = 1;
-        protected static final int RESULT_FAR = 2;
-        protected static final int RESULT_NOT_CHECKED = 3;
-
-        private boolean mRegistered;
-        private boolean mFinished;
-        private float mMaxRange;
-        private boolean mUsingBrightnessSensor;
-
-        protected abstract void onProximityResult(int result);
-
-        public void check() {
-            Preconditions.checkState(!mFinished && !mRegistered);
-            Sensor sensor = DozeSensors.findSensorWithType(mSensorManager,
-                    mContext.getString(R.string.doze_brightness_sensor_type));
-            mUsingBrightnessSensor = sensor != null;
-            if (sensor == null) {
-                sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
-            }
-            if (sensor == null) {
-                if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: No sensor found");
-                finishWithResult(RESULT_UNKNOWN);
-                return;
-            }
-            mDozeSensors.setDisableSensorsInterferingWithProximity(true);
-
-            mMaxRange = sensor.getMaximumRange();
-            mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, 0,
-                    mHandler);
-            mHandler.postDelayed(this, TIMEOUT_DELAY_MS);
-            mWakeLock.acquire(TAG);
-            mRegistered = true;
-        }
-
-        /**
-         * @see DozeSensors.ProxSensor#onSensorChanged(SensorEvent)
-         */
-        @Override
-        public void onSensorChanged(SensorEvent event) {
-            if (event.values.length == 0) {
-                if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: Event has no values!");
-                finishWithResult(RESULT_UNKNOWN);
-            } else {
-                if (DozeMachine.DEBUG) {
-                    Log.d(TAG, "ProxCheck: Event: value=" + event.values[0] + " max=" + mMaxRange);
-                }
-                final boolean isNear;
-                if (mUsingBrightnessSensor) {
-                    // The custom brightness sensor is gated by the proximity sensor and will
-                    // return 0 whenever prox is covered.
-                    isNear = event.values[0] == 0;
-                } else {
-                    isNear = event.values[0] < mMaxRange;
-                }
-                finishWithResult(isNear ? RESULT_NEAR : RESULT_FAR);
-            }
-        }
-
-        @Override
-        public void run() {
-            if (DozeMachine.DEBUG) Log.d(TAG, "ProxCheck: No event received before timeout");
-            finishWithResult(RESULT_UNKNOWN);
-        }
-
-        private void finishWithResult(int result) {
-            if (mFinished) return;
-            boolean wasRegistered = mRegistered;
-            if (mRegistered) {
-                mHandler.removeCallbacks(this);
-                mSensorManager.unregisterListener(this);
-                mDozeSensors.setDisableSensorsInterferingWithProximity(false);
-                mRegistered = false;
-            }
-            onProximityResult(result);
-            if (wasRegistered) {
-                mWakeLock.release(TAG);
-            }
-            mFinished = true;
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            // noop
-        }
-    }
-
     private class TriggerReceiver extends BroadcastReceiver {
         private boolean mRegistered;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
index f7bb97b..00b764b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.Dependency
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.util.Assert
-import com.android.systemui.util.AsyncSensorManager
+import com.android.systemui.util.sensors.AsyncSensorManager
 
 class KeyguardLiftController constructor(
     private val statusBarStateController: StatusBarStateController,
diff --git a/packages/SystemUI/src/com/android/systemui/util/ProximitySensor.java b/packages/SystemUI/src/com/android/systemui/util/ProximitySensor.java
deleted file mode 100644
index a905eba..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/ProximitySensor.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * 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.util;
-
-import android.content.Context;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.util.Log;
-
-import com.android.systemui.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-
-/**
- * Simple wrapper around SensorManager customized for the Proximity sensor.
- */
-public class ProximitySensor {
-    private static final String TAG = "ProxSensor";
-    private static final boolean DEBUG = false;
-
-    private final Sensor mSensor;
-    private final AsyncSensorManager mSensorManager;
-    private final boolean mUsingBrightnessSensor;
-    private final float mMaxRange;
-
-    private SensorEventListener mSensorEventListener = new SensorEventListener() {
-        @Override
-        public synchronized void onSensorChanged(SensorEvent event) {
-            onSensorEvent(event);
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-        }
-    };
-    private boolean mNear;
-    private List<ProximitySensorListener> mListeners = new ArrayList<>();
-    private String mTag = null;
-
-    @Inject
-    public ProximitySensor(Context context, AsyncSensorManager sensorManager) {
-        mSensorManager = sensorManager;
-        Sensor sensor = findBrightnessSensor(context, sensorManager);
-
-        if (sensor == null) {
-            mUsingBrightnessSensor = false;
-            sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
-        } else {
-            mUsingBrightnessSensor = true;
-        }
-        mSensor = sensor;
-        if (mSensor != null) {
-            mMaxRange = mSensor.getMaximumRange();
-        } else {
-            mMaxRange = 0;
-        }
-    }
-
-    public void setTag(String tag) {
-        mTag = tag;
-    }
-
-    private Sensor findBrightnessSensor(Context context, SensorManager sensorManager) {
-        String sensorType = context.getString(R.string.doze_brightness_sensor_type);
-        List<Sensor> sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
-        Sensor sensor = null;
-        for (Sensor s : sensorList) {
-            if (sensorType.equals(s.getStringType())) {
-                sensor = s;
-                break;
-            }
-        }
-
-        return sensor;
-    }
-
-    /**
-     * Returns {@code false} if a Proximity sensor is not available.
-     */
-    public boolean getSensorAvailable() {
-        return mSensor != null;
-    }
-
-    /**
-     * Add a listener.
-     *
-     * Registers itself with the {@link SensorManager} if this is the first listener
-     * added.
-     */
-    public boolean register(ProximitySensorListener listener) {
-        if (!getSensorAvailable()) {
-            return false;
-        }
-
-        logDebug("using brightness sensor? " + mUsingBrightnessSensor);
-        mListeners.add(listener);
-        if (mListeners.size() == 1) {
-            logDebug("registering sensor listener");
-            mSensorManager.registerListener(
-                    mSensorEventListener, mSensor, SensorManager.SENSOR_DELAY_GAME);
-        }
-
-        return true;
-    }
-
-    /**
-     * Remove a listener.
-     *
-     * If all listeners are removed from an instance of this class,
-     * it will unregister itself with the SensorManager.
-     */
-    public void unregister(ProximitySensorListener listener) {
-        mListeners.remove(listener);
-        if (mListeners.size() == 0) {
-            logDebug("unregistering sensor listener");
-            mSensorManager.unregisterListener(mSensorEventListener);
-        }
-    }
-
-    public boolean isNear() {
-        return getSensorAvailable() && mNear;
-    }
-
-    private void onSensorEvent(SensorEvent event) {
-        boolean near = event.values[0] < mMaxRange;
-        if (mUsingBrightnessSensor) {
-            near = event.values[0] == 0;
-        }
-        mNear = near;
-        mListeners.forEach(proximitySensorListener ->
-                proximitySensorListener.onProximitySensorEvent(
-                        new ProximityEvent(mNear, event.timestamp)));
-    }
-
-    /** Implement to be notified of ProximityEvents. */
-    public interface ProximitySensorListener {
-        /** Called when the ProximitySensor changes. */
-        void onProximitySensorEvent(ProximityEvent proximityEvent);
-    }
-
-    /**
-     * Returned when the near/far state of a {@link ProximitySensor} changes.
-     */
-    public static class ProximityEvent {
-        private final boolean mNear;
-        private final long mTimestampNs;
-
-        public ProximityEvent(boolean near, long timestampNs) {
-            mNear = near;
-            mTimestampNs = timestampNs;
-        }
-
-        public boolean getNear() {
-            return mNear;
-        }
-
-        public long getTimestampNs() {
-            return mTimestampNs;
-        }
-    }
-
-    private void logDebug(String msg) {
-        if (DEBUG) {
-            Log.d(TAG, (mTag != null ? "[" + mTag + "] " : "") + msg);
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/AsyncSensorManager.java b/packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java
similarity index 90%
rename from packages/SystemUI/src/com/android/systemui/util/AsyncSensorManager.java
rename to packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java
index b9c5ee5..dcd0c58 100644
--- a/packages/SystemUI/src/com/android/systemui/util/AsyncSensorManager.java
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.util;
+package com.android.systemui.util.sensors;
 
 import android.content.Context;
 import android.hardware.HardwareBuffer;
@@ -56,23 +56,31 @@
 
     private final SensorManager mInner;
     private final List<Sensor> mSensorCache;
-    private final HandlerThread mHandlerThread = new HandlerThread("async_sensor");
-    @VisibleForTesting final Handler mHandler;
+    private final Handler mHandler;
     private final List<SensorManagerPlugin> mPlugins;
 
     @Inject
     public AsyncSensorManager(Context context, PluginManager pluginManager) {
-        this(context.getSystemService(SensorManager.class), pluginManager);
+        this(context.getSystemService(SensorManager.class), pluginManager, null);
     }
 
     @VisibleForTesting
-    AsyncSensorManager(SensorManager sensorManager, PluginManager pluginManager) {
+    public AsyncSensorManager(
+            SensorManager sensorManager, PluginManager pluginManager, Handler handler) {
         mInner = sensorManager;
-        mHandlerThread.start();
-        mHandler = new Handler(mHandlerThread.getLooper());
+        if (handler == null) {
+            HandlerThread handlerThread = new HandlerThread("async_sensor");
+            handlerThread.start();
+            mHandler = new Handler(handlerThread.getLooper());
+        } else {
+            mHandler = handler;
+        }
         mSensorCache = mInner.getSensorList(Sensor.TYPE_ALL);
         mPlugins = new ArrayList<>();
-        pluginManager.addPluginListener(this, SensorManagerPlugin.class, true /* allowMultiple */);
+        if (pluginManager != null) {
+            pluginManager.addPluginListener(this, SensorManagerPlugin.class,
+                    true /* allowMultiple */);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/LimitableSensor.java b/packages/SystemUI/src/com/android/systemui/util/sensors/LimitableSensor.java
new file mode 100644
index 0000000..2dfd187
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/LimitableSensor.java
@@ -0,0 +1,32 @@
+/*
+ * 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.util.sensors;
+
+/**
+ * Represents a sensor that can be throttled by a {@link SensorRateLimiter}.
+ */
+public interface LimitableSensor {
+    /**
+     * Allows a sensor to be temporarily throttled or disabled.
+     */
+    void setRateLimited(boolean limited);
+
+    /**
+     * Registers a {@link SensorRateLimiter} with the sensor.
+     */
+    void setRateLimiter(SensorRateLimiter rateLimiter);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java
new file mode 100644
index 0000000..1eed566
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java
@@ -0,0 +1,332 @@
+/*
+ * 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.util.sensors;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.util.Log;
+
+import com.android.systemui.R;
+import com.android.systemui.shared.plugins.PluginManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * Simple wrapper around SensorManager customized for the Proximity sensor.
+ */
+public class ProximitySensor implements LimitableSensor {
+    private static final String TAG = "ProxSensor";
+    private static final boolean DEBUG = false;
+
+    private final Sensor mSensor;
+    private final AsyncSensorManager mSensorManager;
+    private final boolean mUsingBrightnessSensor;
+    private final float mMaxRange;
+    private List<ProximitySensorListener> mListeners = new ArrayList<>();
+    private String mTag = null;
+    private ProximityEvent mLastEvent;
+    private int mSensorDelay = SensorManager.SENSOR_DELAY_NORMAL;
+    private boolean mPaused;
+    private boolean mRegistered;
+    private SensorRateLimiter mRateLimiter;
+    private boolean mRateLimited = false;
+
+    private SensorEventListener mSensorEventListener = new SensorEventListener() {
+        @Override
+        public synchronized void onSensorChanged(SensorEvent event) {
+            onSensorEvent(event);
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+        }
+    };
+
+    @Inject
+    public ProximitySensor(
+            Context context, AsyncSensorManager sensorManager, PluginManager pluginManager) {
+        mSensorManager = sensorManager;
+        Sensor sensor = findBrightnessSensor(context);
+
+        if (sensor == null) {
+            mUsingBrightnessSensor = false;
+            sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+        } else {
+            mUsingBrightnessSensor = true;
+        }
+        mSensor = sensor;
+        if (mSensor != null) {
+            mMaxRange = mSensor.getMaximumRange();
+        } else {
+            mMaxRange = 0;
+        }
+    }
+
+    public void setTag(String tag) {
+        mTag = tag;
+    }
+
+    public void setSensorDelay(int sensorDelay) {
+        mSensorDelay = sensorDelay;
+    }
+
+    /**
+     * Unregister with the {@link SensorManager} without unsetting listeners on this object.
+     */
+    public void pause() {
+        mPaused = true;
+        unregisterInternal();
+    }
+
+    /**
+     * Register with the {@link SensorManager}. No-op if no listeners are registered on this object.
+     */
+    public void resume() {
+        mPaused = false;
+        registerInternal();
+    }
+
+    /**
+     * Supply a {@link SensorRateLimiter} to help limit power usage.
+     */
+    @Override
+    public void setRateLimiter(SensorRateLimiter rateLimiter) {
+        mRateLimiter = rateLimiter;
+    }
+
+    private Sensor findBrightnessSensor(Context context) {
+        String sensorType = context.getString(R.string.doze_brightness_sensor_type);
+        List<Sensor> sensorList = mSensorManager.getSensorList(Sensor.TYPE_ALL);
+        Sensor sensor = null;
+        for (Sensor s : sensorList) {
+            if (sensorType.equals(s.getStringType())) {
+                sensor = s;
+                break;
+            }
+        }
+
+        return sensor;
+    }
+
+    /**
+     * Registers/unregisters the proximity sensor with the SensorManager.
+     */
+    @Override
+    public void setRateLimited(boolean limited) {
+        mRateLimited = limited;
+        if (mRateLimited) {
+            unregisterInternal();
+        } else {
+            registerInternal();
+        }
+    }
+
+    /**
+     * Returns true if we are registered with the SensorManager.
+     */
+    public boolean isRegistered() {
+        return mRegistered;
+    }
+
+    /**
+     * Returns {@code false} if a Proximity sensor is not available.
+     */
+    public boolean getSensorAvailable() {
+        return mSensor != null;
+    }
+
+    /**
+     * Add a listener.
+     *
+     * Registers itself with the {@link SensorManager} if this is the first listener
+     * added. If a cool down is currently running, the sensor will be registered when it is over.
+     */
+    public boolean register(ProximitySensorListener listener) {
+        if (!getSensorAvailable()) {
+            return false;
+        }
+
+        logDebug("Using brightness sensor? " + mUsingBrightnessSensor);
+        mListeners.add(listener);
+        registerInternal();
+
+        return true;
+    }
+
+    private void registerInternal() {
+        if (mRegistered || mRateLimited || mPaused || mListeners.isEmpty()) {
+            return;
+        }
+        logDebug("Registering sensor listener");
+        mRegistered = true;
+        mSensorManager.registerListener(mSensorEventListener, mSensor, mSensorDelay);
+    }
+
+    /**
+     * Remove a listener.
+     *
+     * If all listeners are removed from an instance of this class,
+     * it will unregister itself with the SensorManager.
+     */
+    public void unregister(ProximitySensorListener listener) {
+        mListeners.remove(listener);
+        if (mListeners.size() == 0) {
+            unregisterInternal();
+        }
+    }
+
+    private void unregisterInternal() {
+        if (!mRegistered) {
+            return;
+        }
+        logDebug("unregistering sensor listener");
+        mSensorManager.unregisterListener(mSensorEventListener);
+        mRegistered = false;
+    }
+
+    public Boolean isNear() {
+        return getSensorAvailable() && mLastEvent != null ? mLastEvent.getNear() : null;
+    }
+
+    /** Update all listeners with the last value this class received from the sensor. */
+    public void alertListeners() {
+        mListeners.forEach(proximitySensorListener ->
+                proximitySensorListener.onSensorEvent(mLastEvent));
+    }
+
+    private void onSensorEvent(SensorEvent event) {
+        boolean near = event.values[0] < mMaxRange;
+        if (mUsingBrightnessSensor) {
+            near = event.values[0] == 0;
+        }
+        mLastEvent = new ProximityEvent(near, event.timestamp);
+        alertListeners();
+        if (mRateLimiter != null) {
+            mRateLimiter.onSensorEvent(mLastEvent.getTimestampMs());
+        }
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{registered=%s, paused=%s, near=%s, rateLimiter=%s, sensor=%s}",
+                isRegistered(), mPaused, isNear(), mRateLimiter, mSensor);
+    }
+
+    /**
+     * Convenience class allowing for briefly checking the proximity sensor.
+     */
+    public static class ProximityCheck implements Runnable {
+
+        private final ProximitySensor mSensor;
+        private final Handler mHandler;
+        private List<Consumer<Boolean>> mCallbacks = new ArrayList<>();
+
+        @Inject
+        public ProximityCheck(ProximitySensor sensor, Handler handler) {
+            mSensor = sensor;
+            mSensor.setTag("prox_check");
+            mHandler = handler;
+            mSensor.pause();
+            ProximitySensorListener listener = proximityEvent -> {
+                mCallbacks.forEach(
+                        booleanConsumer ->
+                                booleanConsumer.accept(
+                                        proximityEvent == null ? null : proximityEvent.getNear()));
+                mCallbacks.clear();
+                mSensor.pause();
+            };
+            mSensor.register(listener);
+        }
+
+        /** Set a descriptive tag for the sensors registration. */
+        public void setTag(String tag) {
+            mSensor.setTag(tag);
+        }
+
+        @Override
+        public void run() {
+            mSensor.pause();
+            mSensor.alertListeners();
+        }
+
+        /**
+         * Query the proximity sensor, timing out if no result.
+         */
+        public void check(long timeoutMs, Consumer<Boolean> callback) {
+            if (!mSensor.getSensorAvailable()) {
+                callback.accept(null);
+            }
+            mCallbacks.add(callback);
+            if (!mSensor.isRegistered()) {
+                mSensor.resume();
+                mHandler.postDelayed(this, timeoutMs);
+            }
+        }
+    }
+
+    /** Implement to be notified of ProximityEvents. */
+    public interface ProximitySensorListener {
+        /** Called when the ProximitySensor changes. */
+        void onSensorEvent(ProximityEvent proximityEvent);
+    }
+
+    /**
+     * Returned when the near/far state of a {@link ProximitySensor} changes.
+     */
+    public static class ProximityEvent {
+        private final boolean mNear;
+        private final long mTimestampNs;
+
+        public ProximityEvent(boolean near, long timestampNs) {
+            mNear = near;
+            mTimestampNs = timestampNs;
+        }
+
+        public boolean getNear() {
+            return mNear;
+        }
+
+        public long getTimestampNs() {
+            return mTimestampNs;
+        }
+
+        public long getTimestampMs() {
+            return mTimestampNs / 1000000;
+        }
+
+        @Override
+        public String toString() {
+            return String.format((Locale) null, "{near=%s, timestamp_ns=%d}", mNear, mTimestampNs);
+        }
+
+    }
+
+    private void logDebug(String msg) {
+        if (DEBUG) {
+            Log.d(TAG, (mTag != null ? "[" + mTag + "] " : "") + msg);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/SensorRateLimiter.java b/packages/SystemUI/src/com/android/systemui/util/sensors/SensorRateLimiter.java
new file mode 100644
index 0000000..00f5bb0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/SensorRateLimiter.java
@@ -0,0 +1,77 @@
+/*
+ * 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.util.sensors;
+
+import android.app.AlarmManager;
+import android.os.Handler;
+
+import com.android.systemui.util.AlarmTimeout;
+
+/**
+ * Monitors a sensor for over-activity and can throttle it down as needed.
+ *
+ * Currently, this class is designed with the {@link ProximitySensor} in mind.
+ */
+public class SensorRateLimiter {
+    private final LimitableSensor mSensor;
+    private final AlarmTimeout mCooldownTimer;
+    private final long mCoolDownTriggerMs;
+    private final long mCoolDownPeriodMs;
+    private long mLastTimestampMs;
+
+    public SensorRateLimiter(
+            LimitableSensor sensor,
+            AlarmManager alarmManager,
+            long coolDownTriggerMs,
+            long coolDownPeriodMs,
+            String alarmTag) {
+
+        mSensor = sensor;
+        mCoolDownTriggerMs = coolDownTriggerMs;
+        mCoolDownPeriodMs = coolDownPeriodMs;
+        mLastTimestampMs = -1;
+
+        Handler handler = new Handler();
+        mCooldownTimer = new AlarmTimeout(alarmManager, this::coolDownComplete, alarmTag, handler);
+
+        mSensor.setRateLimiter(this);
+    }
+
+
+    void onSensorEvent(long timestampMs) {
+        if (mLastTimestampMs >= 0 && mCoolDownTriggerMs > 0 && mCoolDownPeriodMs > 0
+                && timestampMs - mLastTimestampMs < mCoolDownTriggerMs) {
+            scheduleCoolDown();
+        }
+
+        mLastTimestampMs = timestampMs;
+    }
+
+    private void scheduleCoolDown() {
+        mSensor.setRateLimited(true);
+        mCooldownTimer.schedule(mCoolDownPeriodMs, AlarmTimeout.MODE_IGNORE_IF_SCHEDULED);
+    }
+
+    private void coolDownComplete() {
+        mSensor.setRateLimited(false);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{coolingDown=%s}", mCooldownTimer.isScheduled());
+    }
+}