Doze: Aggressively dial down notification-related pulses.

When dozing and buzz-worthy notifications arrive, don't follow
the LED logic with periodic pulses.

Instead, follow a simple decay schedule after the initial arrival,
 pulsing only at 10s, 30s, 60s, and 120s.

The schedule is reset when a new notification arrives, but only
for the first three times, until either the pickup sensor is
triggered or the device exits doze.

Also:
 - Make the notification trigger configurable.
 - Centralize existing sysprop configuration into DozeParameters.
 - Decouple vibration from debugging, make separately configurable.
 - Remove "delayed" pulse concept, fold into new schedule.

Bug:17496795
Change-Id: I64fc1c862bcfa1c288a4fd91c9d17e3bff245add
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index 0aa7f35..dae15e6 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -22,7 +22,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.res.Resources;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
 import android.hardware.TriggerEvent;
@@ -30,15 +29,14 @@
 import android.media.AudioAttributes;
 import android.os.Handler;
 import android.os.PowerManager;
-import android.os.SystemProperties;
 import android.os.Vibrator;
 import android.service.dreams.DreamService;
 import android.util.Log;
 import android.view.Display;
 
-import com.android.systemui.R;
 import com.android.systemui.SystemUIApplication;
 import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.phone.DozeParameters.PulseSchedule;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -72,6 +70,7 @@
     private PendingIntent mNotificationPulseIntent;
     private boolean mPowerSaveActive;
     private long mNotificationPulseTime;
+    private int mScheduleResetsRemaining;
 
     public DozeService() {
         if (DEBUG) Log.d(mTag, "new DozeService()");
@@ -90,6 +89,7 @@
         pw.print("  mNotificationLightOn: "); pw.println(mNotificationLightOn);
         pw.print("  mPowerSaveActive: "); pw.println(mPowerSaveActive);
         pw.print("  mNotificationPulseTime: "); pw.println(mNotificationPulseTime);
+        pw.print("  mScheduleResetsRemaining: "); pw.println(mScheduleResetsRemaining);
         mDozeParameters.dump(pw);
     }
 
@@ -107,16 +107,14 @@
         setWindowless(true);
 
         mSensors = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
-        mSigMotionSensor = new TriggerSensor(Sensor.TYPE_SIGNIFICANT_MOTION, "doze.pulse.sigmotion",
-                R.bool.doze_pulse_on_significant_motion);
-        mPickupSensor = new TriggerSensor(Sensor.TYPE_PICK_UP_GESTURE, "doze.pulse.pickup",
-                R.bool.doze_pulse_on_pick_up);
+        mSigMotionSensor = new TriggerSensor(Sensor.TYPE_SIGNIFICANT_MOTION,
+                mDozeParameters.getPulseOnSigMotion(), mDozeParameters.getVibrateOnSigMotion());
+        mPickupSensor = new TriggerSensor(Sensor.TYPE_PICK_UP_GESTURE,
+                mDozeParameters.getPulseOnPickup(), mDozeParameters.getVibrateOnPickup());
         mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
         mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
         mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
-        final Resources res = mContext.getResources();
-        mDisplayStateSupported = SystemProperties.getBoolean("doze.display.supported",
-                res.getBoolean(R.bool.doze_display_state_supported));
+        mDisplayStateSupported = mDozeParameters.getDisplayStateSupported();
         mNotificationPulseIntent = PendingIntent.getBroadcast(mContext, 0,
                 new Intent(NOTIFICATION_PULSE_ACTION).setPackage(getPackageName()),
                 PendingIntent.FLAG_UPDATE_CURRENT);
@@ -142,6 +140,7 @@
         }
         mDreaming = true;
         listenForPulseSignals(true);
+        rescheduleNotificationPulse(false /*predicate*/);  // cancel any pending pulse alarms
         requestDoze();
     }
 
@@ -174,15 +173,21 @@
         mHandler.removeCallbacks(mDisplayOff);
     }
 
+    @Override
+    public void startDozing() {
+        if (DEBUG) Log.d(mTag, "startDozing");
+        super.startDozing();
+    }
+
     private void requestDoze() {
         if (mHost != null) {
             mHost.requestDoze(this);
         }
     }
 
-    private void requestPulse(boolean delayed) {
+    private void requestPulse() {
         if (mHost != null) {
-            mHost.requestPulse(delayed, this);
+            mHost.requestPulse(this);
         }
     }
 
@@ -222,24 +227,61 @@
     private void listenForNotifications(boolean listen) {
         if (mHost == null) return;
         if (listen) {
+            resetNotificationResets();
             mHost.addCallback(mHostCallback);
         } else {
             mHost.removeCallback(mHostCallback);
         }
     }
 
-    private void rescheduleNotificationPulse() {
-        mAlarmManager.cancel(mNotificationPulseIntent);
-        if (mNotificationLightOn) {
-            final long now = System.currentTimeMillis();
-            final long age = now - mNotificationPulseTime;
-            final long period = mDozeParameters.getPulsePeriod(age);
-            final long time = now + period;
-            if (period > 0) {
-                if (DEBUG) Log.d(TAG, "Scheduling pulse in " + period + " for " + new Date(time));
-                mAlarmManager.setExact(AlarmManager.RTC_WAKEUP, time, mNotificationPulseIntent);
-            }
+    private void resetNotificationResets() {
+        if (DEBUG) Log.d(TAG, "resetNotificationResets");
+        mScheduleResetsRemaining = mDozeParameters.getPulseScheduleResets();
+    }
+
+    private void updateNotificationPulse() {
+        if (DEBUG) Log.d(TAG, "updateNotificationPulse");
+        if (!mDozeParameters.getPulseOnNotifications()) return;
+        if (mScheduleResetsRemaining <= 0) {
+            if (DEBUG) Log.d(TAG, "No more schedule resets remaining");
+            return;
         }
+        final long now = System.currentTimeMillis();
+        if ((now - mNotificationPulseTime) < mDozeParameters.getPulseDuration()) {
+            if (DEBUG) Log.d(TAG, "Recently updated, not resetting schedule");
+            return;
+        }
+        mScheduleResetsRemaining--;
+        if (DEBUG) Log.d(TAG, "mScheduleResetsRemaining = " + mScheduleResetsRemaining);
+        mNotificationPulseTime = now;
+        rescheduleNotificationPulse(true /*predicate*/);
+    }
+
+    private void rescheduleNotificationPulse(boolean predicate) {
+        if (DEBUG) Log.d(TAG, "rescheduleNotificationPulse predicate=" + predicate);
+        mAlarmManager.cancel(mNotificationPulseIntent);
+        if (!predicate) {
+            if (DEBUG) Log.d(TAG, "  don't reschedule: predicate is false");
+            return;
+        }
+        final PulseSchedule schedule = mDozeParameters.getPulseSchedule();
+        if (schedule == null) {
+            if (DEBUG) Log.d(TAG, "  don't reschedule: schedule is null");
+            return;
+        }
+        final long now = System.currentTimeMillis();
+        final long time = schedule.getNextTime(now, mNotificationPulseTime);
+        if (time <= 0) {
+            if (DEBUG) Log.d(TAG, "  don't reschedule: time is " + time);
+            return;
+        }
+        final long delta = time - now;
+        if (delta <= 0) {
+            if (DEBUG) Log.d(TAG, "  don't reschedule: delta is " + delta);
+            return;
+        }
+        if (DEBUG) Log.d(TAG, "Scheduling pulse in " + delta + "ms for " + new Date(time));
+        mAlarmManager.setExact(AlarmManager.RTC_WAKEUP, time, mNotificationPulseIntent);
     }
 
     private static String triggerEventToString(TriggerEvent event) {
@@ -268,12 +310,12 @@
         public void onReceive(Context context, Intent intent) {
             if (PULSE_ACTION.equals(intent.getAction())) {
                 if (DEBUG) Log.d(mTag, "Received pulse intent");
-                requestPulse(false /*delayed*/);
+                requestPulse();
             }
             if (NOTIFICATION_PULSE_ACTION.equals(intent.getAction())) {
                 if (DEBUG) Log.d(mTag, "Received notification pulse intent");
-                requestPulse(true /*delayed*/);
-                rescheduleNotificationPulse();
+                requestPulse();
+                rescheduleNotificationPulse(mNotificationLightOn);
             }
         }
     };
@@ -288,8 +330,7 @@
         @Override
         public void onBuzzBeepBlinked() {
             if (DEBUG) Log.d(mTag, "onBuzzBeepBlinked");
-            mNotificationPulseTime = System.currentTimeMillis();
-            requestPulse(true /*delayed*/);
+            updateNotificationPulse();
         }
 
         @Override
@@ -298,10 +339,8 @@
             if (mNotificationLightOn == on) return;
             mNotificationLightOn = on;
             if (mNotificationLightOn) {
-                mNotificationPulseTime = System.currentTimeMillis();
-                requestPulse(true /*delayed*/);
+                updateNotificationPulse();
             }
-            rescheduleNotificationPulse();
         }
 
         @Override
@@ -317,7 +356,7 @@
         void addCallback(Callback callback);
         void removeCallback(Callback callback);
         void requestDoze(DozeService dozeService);
-        void requestPulse(boolean delayed, DozeService dozeService);
+        void requestPulse(DozeService dozeService);
         void dozingStopped(DozeService dozeService);
         boolean isPowerSaveActive();
 
@@ -332,13 +371,14 @@
     private class TriggerSensor extends TriggerEventListener {
         private final Sensor mSensor;
         private final boolean mConfigured;
+        private final boolean mDebugVibrate;
 
         private boolean mEnabled;
 
-        public TriggerSensor(int type, String sysPropConfig, int resConfig) {
+        public TriggerSensor(int type, boolean configured, boolean debugVibrate) {
             mSensor = mSensors.getDefaultSensor(type);
-            mConfigured = SystemProperties.getBoolean(sysPropConfig,
-                    mContext.getResources().getBoolean(resConfig));
+            mConfigured = configured;
+            mDebugVibrate = debugVibrate;
         }
 
         public void setListening(boolean listen) {
@@ -354,13 +394,14 @@
         @Override
         public String toString() {
             return new StringBuilder("{mEnabled=").append(mEnabled).append(", mConfigured=")
-                    .append(mConfigured).append(", mSensor=").append(mSensor).append("}").toString();
+                    .append(mConfigured).append(", mDebugVibrate=").append(mDebugVibrate)
+                    .append(", mSensor=").append(mSensor).append("}").toString();
         }
 
         @Override
         public void onTrigger(TriggerEvent event) {
             if (DEBUG) Log.d(mTag, "onTrigger: " + triggerEventToString(event));
-            if (DEBUG) {
+            if (mDebugVibrate) {
                 final Vibrator v = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
                 if (v != null) {
                     v.vibrate(1000, new AudioAttributes.Builder()
@@ -368,8 +409,9 @@
                             .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build());
                 }
             }
-            requestPulse(false /*delayed*/);
+            requestPulse();
             setListening(true);  // reregister, this sensor only fires once
+            resetNotificationResets();
         }
     }
 }
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 e14ef12..bcf42de 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -25,17 +25,19 @@
 import com.android.systemui.R;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 public class DozeParameters {
     private static final String TAG = "DozeParameters";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final int MAX_DURATION = 10 * 1000;
 
     private final Context mContext;
 
-    private StepFunction mPulsePeriodFunction;
+    private static PulseSchedule sPulseSchedule;
 
     public DozeParameters(Context context) {
         mContext = context;
@@ -43,12 +45,22 @@
 
     public void dump(PrintWriter pw) {
         pw.println("  DozeParameters:");
+        pw.print("    getDisplayStateSupported(): "); pw.println(getDisplayStateSupported());
         pw.print("    getPulseDuration(): "); pw.println(getPulseDuration());
         pw.print("    getPulseInDuration(): "); pw.println(getPulseInDuration());
         pw.print("    getPulseInVisibleDuration(): "); pw.println(getPulseVisibleDuration());
         pw.print("    getPulseOutDuration(): "); pw.println(getPulseOutDuration());
-        pw.print("    getPulseStartDelay(): "); pw.println(getPulseStartDelay());
-        pw.print("    getPulsePeriodFunction(): "); pw.println(getPulsePeriodFunction());
+        pw.print("    getPulseOnSigMotion(): "); pw.println(getPulseOnSigMotion());
+        pw.print("    getVibrateOnSigMotion(): "); pw.println(getVibrateOnSigMotion());
+        pw.print("    getPulseOnPickup(): "); pw.println(getPulseOnPickup());
+        pw.print("    getVibrateOnPickup(): "); pw.println(getVibrateOnPickup());
+        pw.print("    getPulseOnNotifications(): "); pw.println(getPulseOnNotifications());
+        pw.print("    getPulseSchedule(): "); pw.println(getPulseSchedule());
+        pw.print("    getPulseScheduleResets(): "); pw.println(getPulseScheduleResets());
+    }
+
+    public boolean getDisplayStateSupported() {
+        return getBoolean("doze.display.supported", R.bool.doze_display_state_supported);
     }
 
     public int getPulseDuration() {
@@ -67,20 +79,40 @@
         return getInt("doze.pulse.duration.out", R.integer.doze_pulse_duration_out);
     }
 
-    public int getPulseStartDelay() {
-        return getInt("doze.pulse.delay", R.integer.doze_pulse_delay);
+    public boolean getPulseOnSigMotion() {
+        return getBoolean("doze.pulse.sigmotion", R.bool.doze_pulse_on_significant_motion);
     }
 
-    public long getPulsePeriod(long age) {
-        final String spec = getPulsePeriodFunction();
-        if (mPulsePeriodFunction == null || !mPulsePeriodFunction.mSpec.equals(spec)) {
-            mPulsePeriodFunction = StepFunction.parse(spec);
+    public boolean getVibrateOnSigMotion() {
+        return SystemProperties.getBoolean("doze.vibrate.sigmotion", false);
+    }
+
+    public boolean getPulseOnPickup() {
+        return getBoolean("doze.pulse.pickup", R.bool.doze_pulse_on_pick_up);
+    }
+
+    public boolean getVibrateOnPickup() {
+        return SystemProperties.getBoolean("doze.vibrate.pickup", false);
+    }
+
+    public boolean getPulseOnNotifications() {
+        return getBoolean("doze.pulse.notifications", R.bool.doze_pulse_on_notifications);
+    }
+
+    public PulseSchedule getPulseSchedule() {
+        final String spec = getString("doze.pulse.schedule", R.string.doze_pulse_schedule);
+        if (sPulseSchedule == null || !sPulseSchedule.mSpec.equals(spec)) {
+            sPulseSchedule = PulseSchedule.parse(spec);
         }
-        return mPulsePeriodFunction != null ? mPulsePeriodFunction.evaluate(age) : 0;
+        return sPulseSchedule;
     }
 
-    private String getPulsePeriodFunction() {
-        return getString("doze.pulse.period.function", R.string.doze_pulse_period_function);
+    public int getPulseScheduleResets() {
+        return getInt("doze.pulse.schedule.resets", R.integer.doze_pulse_schedule_resets);
+    }
+
+    private boolean getBoolean(String propName, int resId) {
+        return SystemProperties.getBoolean(propName, mContext.getResources().getBoolean(resId));
     }
 
     private int getInt(String propName, int resId) {
@@ -92,29 +124,25 @@
         return SystemProperties.get(propName, mContext.getString(resId));
     }
 
-    private static class StepFunction {
-        private static final Pattern PATTERN = Pattern.compile("(\\d+?)(:(\\d+?))?", 0);
+    public static class PulseSchedule {
+        private static final Pattern PATTERN = Pattern.compile("(\\d+?)s", 0);
 
         private String mSpec;
-        private long[] mSteps;
-        private long[] mValues;
-        private long mDefault;
+        private int[] mSchedule;
 
-        public static StepFunction parse(String spec) {
+        public static PulseSchedule parse(String spec) {
             if (TextUtils.isEmpty(spec)) return null;
             try {
-                final StepFunction rt = new StepFunction();
+                final PulseSchedule rt = new PulseSchedule();
                 rt.mSpec = spec;
                 final String[] tokens = spec.split(",");
-                rt.mSteps = new long[tokens.length - 1];
-                rt.mValues = new long[tokens.length - 1];
-                for (int i = 0; i < tokens.length - 1; i++) {
+                rt.mSchedule = new int[tokens.length];
+                for (int i = 0; i < tokens.length; i++) {
                     final Matcher m = PATTERN.matcher(tokens[i]);
                     if (!m.matches()) throw new IllegalArgumentException("Bad token: " + tokens[i]);
-                    rt.mSteps[i] = Long.parseLong(m.group(1));
-                    rt.mValues[i] = Long.parseLong(m.group(3));
+                    rt.mSchedule[i] = Integer.parseInt(m.group(1));
                 }
-                rt.mDefault = Long.parseLong(tokens[tokens.length - 1]);
+                if (DEBUG) Log.d(TAG, "Parsed spec [" + spec + "] as: " + rt);
                 return rt;
             } catch (RuntimeException e) {
                 Log.w(TAG, "Error parsing spec: " + spec, e);
@@ -122,11 +150,17 @@
             }
         }
 
-        public long evaluate(long x) {
-            for (int i = 0; i < mSteps.length; i++) {
-                if (x < mSteps[i]) return mValues[i];
+        @Override
+        public String toString() {
+            return Arrays.toString(mSchedule);
+        }
+
+        public long getNextTime(long now, long notificationTime) {
+            for (int i = 0; i < mSchedule.length; i++) {
+                final long time = notificationTime + mSchedule[i] * 1000;
+                if (time > now) return time;
             }
-            return mDefault;
+            return 0;
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index e6db2c8..d86f030 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -4083,11 +4083,10 @@
         }
 
         @Override
-        public void requestPulse(boolean delayed, DozeService dozeService) {
+        public void requestPulse(DozeService dozeService) {
             if (dozeService == null) return;
             dozeService.stayAwake(PROCESSING_TIME);
-            mHandler.obtainMessage(H.REQUEST_PULSE, delayed ? 1 : 0, 0, dozeService)
-                    .sendToTarget();
+            mHandler.obtainMessage(H.REQUEST_PULSE, dozeService).sendToTarget();
         }
 
         @Override
@@ -4110,9 +4109,9 @@
             mCurrentDozeService.startDozing();
         }
 
-        private void handleRequestPulse(boolean delayed, DozeService dozeService) {
+        private void handleRequestPulse(DozeService dozeService) {
             if (!dozeService.equals(mCurrentDozeService)) return;
-            final long stayAwake = mScrimController.pulse(delayed);
+            final long stayAwake = mScrimController.pulse();
             mCurrentDozeService.stayAwake(stayAwake);
         }
 
@@ -4136,7 +4135,7 @@
                 if (msg.what == REQUEST_DOZE) {
                     handleRequestDoze((DozeService) msg.obj);
                 } else if (msg.what == REQUEST_PULSE) {
-                    handleRequestPulse(msg.arg1 != 0, (DozeService) msg.obj);
+                    handleRequestPulse((DozeService) msg.obj);
                 } else if (msg.what == DOZING_STOPPED) {
                     handleDozingStopped((DozeService) msg.obj);
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 57e5755..3ff11d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -129,20 +129,20 @@
         if (!mDozing) {
             cancelPulsing();
             mAnimateChange = true;
+        } else {
+            mAnimateChange = false;
         }
         scheduleUpdate();
     }
 
     /** When dozing, fade screen contents in and out using the front scrim. */
-    public long pulse(boolean delayed) {
+    public long pulse() {
         if (!mDozing) return 0;
         final long now = System.currentTimeMillis();
-        if (DEBUG) Log.d(TAG, "pulse delayed=" + delayed + " mPulseEndTime=" + mPulseEndTime
-                + " now=" + now);
+        if (DEBUG) Log.d(TAG, "pulse mPulseEndTime=" + mPulseEndTime + " now=" + now);
         if (mPulseEndTime != 0 && mPulseEndTime > now) return mPulseEndTime - now;
-        final long delay = delayed ? mDozeParameters.getPulseStartDelay() : 0;
-        mScrimInFront.postDelayed(mPulseIn, delay);
-        mPulseEndTime = now + delay + mDozeParameters.getPulseDuration();
+        mScrimInFront.post(mPulseIn);
+        mPulseEndTime = now + mDozeParameters.getPulseDuration();
         return mPulseEndTime - now;
     }