Factor out AlarmTimeout

Factors out a helper class that allows scheduling timeouts that are
delivered through alarm manager and cause a wakeup.

Test: check that AOD time still updates every minute
Bug: 62292293
Change-Id: Iff84c01982f0757095bc8719d41bbd22dab4e329
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
index cf87fca..97731cf 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeUi.java
@@ -23,6 +23,7 @@
 import android.text.format.Formatter;
 import android.util.Log;
 
+import com.android.systemui.util.AlarmTimeout;
 import com.android.systemui.util.wakelock.WakeLock;
 
 import java.util.Calendar;
@@ -35,26 +36,23 @@
 
     private static final long TIME_TICK_DEADLINE_MILLIS = 90 * 1000; // 1.5min
     private final Context mContext;
-    private final AlarmManager mAlarmManager;
     private final DozeHost mHost;
     private final Handler mHandler;
     private final WakeLock mWakeLock;
     private final DozeMachine mMachine;
-    private final AlarmManager.OnAlarmListener mTimeTick;
+    private final AlarmTimeout mTimeTicker;
 
-    private boolean mTimeTickScheduled = false;
     private long mLastTimeTickElapsed = 0;
 
     public DozeUi(Context context, AlarmManager alarmManager, DozeMachine machine,
             WakeLock wakeLock, DozeHost host, Handler handler) {
         mContext = context;
-        mAlarmManager = alarmManager;
         mMachine = machine;
         mWakeLock = wakeLock;
         mHost = host;
         mHandler = handler;
 
-        mTimeTick = this::onTimeTick;
+        mTimeTicker = new AlarmTimeout(alarmManager, this::onTimeTick, "doze_time_tick", handler);
     }
 
     private void pulseWhileDozing(int reason) {
@@ -112,25 +110,21 @@
     }
 
     private void scheduleTimeTick() {
-        if (mTimeTickScheduled) {
+        if (mTimeTicker.isScheduled()) {
             return;
         }
 
         long delta = roundToNextMinute(System.currentTimeMillis()) - System.currentTimeMillis();
-        mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
-                SystemClock.elapsedRealtime() + delta, "doze_time_tick", mTimeTick, mHandler);
-
-        mTimeTickScheduled = true;
+        mTimeTicker.schedule(delta, AlarmTimeout.MODE_IGNORE_IF_SCHEDULED);
         mLastTimeTickElapsed = SystemClock.elapsedRealtime();
     }
 
     private void unscheduleTimeTick() {
-        if (!mTimeTickScheduled) {
+        if (!mTimeTicker.isScheduled()) {
             return;
         }
         verifyLastTimeTick();
-        mAlarmManager.cancel(mTimeTick);
-        mTimeTickScheduled = false;
+        mTimeTicker.cancel();
     }
 
     private void verifyLastTimeTick() {
@@ -153,10 +147,6 @@
     }
 
     private void onTimeTick() {
-        if (!mTimeTickScheduled) {
-            // Alarm was canceled, but we still got the callback. Ignore.
-            return;
-        }
         verifyLastTimeTick();
 
         mHost.dozeTimeTick();
@@ -164,7 +154,6 @@
         // Keep wakelock until a frame has been pushed.
         mHandler.post(mWakeLock.wrap(() -> {}));
 
-        mTimeTickScheduled = false;
         scheduleTimeTick();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/AlarmTimeout.java b/packages/SystemUI/src/com/android/systemui/util/AlarmTimeout.java
new file mode 100644
index 0000000..f7f61af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/AlarmTimeout.java
@@ -0,0 +1,93 @@
+/*
+ * 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.util;
+
+import android.app.AlarmManager;
+import android.os.Handler;
+import android.os.SystemClock;
+
+/**
+ * Schedules a timeout through AlarmManager. Ensures that the timeout is called even when
+ * the device is asleep.
+ */
+public class AlarmTimeout implements AlarmManager.OnAlarmListener {
+
+    public static final int MODE_CRASH_IF_SCHEDULED = 0;
+    public static final int MODE_IGNORE_IF_SCHEDULED = 1;
+    public static final int MODE_RESCHEDULE_IF_SCHEDULED = 2;
+
+    private final AlarmManager mAlarmManager;
+    private final AlarmManager.OnAlarmListener mListener;
+    private final String mTag;
+    private final Handler mHandler;
+    private boolean mScheduled;
+
+    public AlarmTimeout(AlarmManager alarmManager, AlarmManager.OnAlarmListener listener,
+            String tag, Handler handler) {
+        mAlarmManager = alarmManager;
+        mListener = listener;
+        mTag = tag;
+        mHandler = handler;
+    }
+
+    public void schedule(long timeout, int mode) {
+        switch (mode) {
+            case MODE_CRASH_IF_SCHEDULED:
+                if (mScheduled) {
+                    throw new IllegalStateException(mTag + " timeout is already scheduled");
+                }
+                break;
+            case MODE_IGNORE_IF_SCHEDULED:
+                if (mScheduled) {
+                    return;
+                }
+                break;
+            case MODE_RESCHEDULE_IF_SCHEDULED:
+                if (mScheduled) {
+                    cancel();
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("Illegal mode: " + mode);
+        }
+
+        mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                SystemClock.elapsedRealtime() + timeout, mTag, this, mHandler);
+        mScheduled = true;
+    }
+
+    public boolean isScheduled() {
+        return mScheduled;
+    }
+
+    public void cancel() {
+        if (mScheduled) {
+            mAlarmManager.cancel(this);
+            mScheduled = false;
+        }
+    }
+
+    @Override
+    public void onAlarm() {
+        if (!mScheduled) {
+            // We canceled the alarm, but it still fired. Ignore.
+            return;
+        }
+        mScheduled = false;
+        mListener.onAlarm();
+    }
+}