Zen: Implement calendar event system condition provider.

 - Wire up basic implementation for the system provider that
   handles event-based DND subscriptions.
 - Backed by the standard system calendar content provider.
 - Move shared time utils to base class, clean up logging.

Bug: 20064962
Change-Id: I070b6baa580c592c2ab4101c6b44a254787f9dd7
diff --git a/services/core/java/com/android/server/notification/EventConditionProvider.java b/services/core/java/com/android/server/notification/EventConditionProvider.java
index 425e222..dea6325 100644
--- a/services/core/java/com/android/server/notification/EventConditionProvider.java
+++ b/services/core/java/com/android/server/notification/EventConditionProvider.java
@@ -16,16 +16,23 @@
 
 package com.android.server.notification;
 
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.net.Uri;
 import android.service.notification.Condition;
 import android.service.notification.IConditionProvider;
 import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.EventInfo;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.server.notification.CalendarTracker.CheckEventResult;
 import com.android.server.notification.NotificationManagerService.DumpFilter;
 
 import java.io.PrintWriter;
@@ -34,20 +41,27 @@
  * Built-in zen condition provider for calendar event-based conditions.
  */
 public class EventConditionProvider extends SystemConditionProviderService {
-    private static final String TAG = "ConditionProviders";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final String TAG = "ConditionProviders.ECP";
+    private static final boolean DEBUG = Log.isLoggable("ConditionProviders", Log.DEBUG);
 
     public static final ComponentName COMPONENT =
             new ComponentName("android", EventConditionProvider.class.getName());
     private static final String NOT_SHOWN = "...";
+    private static final String SIMPLE_NAME = EventConditionProvider.class.getSimpleName();
+    private static final String ACTION_EVALUATE = SIMPLE_NAME + ".EVALUATE";
+    private static final int REQUEST_CODE_EVALUATE = 1;
+    private static final String EXTRA_TIME = "time";
 
+    private final Context mContext = this;
     private final ArraySet<Uri> mSubscriptions = new ArraySet<Uri>();
+    private final CalendarTracker mTracker = new CalendarTracker(mContext);
 
     private boolean mConnected;
     private boolean mRegistered;
+    private boolean mBootComplete;  // don't hammer the calendar provider until boot completes.
 
     public EventConditionProvider() {
-        if (DEBUG) Slog.d(TAG, "new EventConditionProvider()");
+        if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
     }
 
     @Override
@@ -62,14 +76,25 @@
 
     @Override
     public void dump(PrintWriter pw, DumpFilter filter) {
-        pw.println("    EventConditionProvider:");
+        pw.print("    "); pw.print(SIMPLE_NAME); pw.println(":");
         pw.print("      mConnected="); pw.println(mConnected);
         pw.print("      mRegistered="); pw.println(mRegistered);
+        pw.print("      mBootComplete="); pw.println(mBootComplete);
         pw.println("      mSubscriptions=");
         for (Uri conditionId : mSubscriptions) {
             pw.print("        ");
             pw.println(conditionId);
         }
+        pw.println("      mTracker=");
+        mTracker.dump("        ", pw);
+    }
+
+    @Override
+    public void onBootComplete() {
+        if (DEBUG) Slog.d(TAG, "onBootComplete");
+        if (mBootComplete) return;
+        mBootComplete = true;
+        evaluateSubscriptions();
     }
 
     @Override
@@ -98,8 +123,9 @@
             notifyCondition(conditionId, Condition.STATE_FALSE, "badCondition");
             return;
         }
-        mSubscriptions.add(conditionId);
-        evaluateSubscriptions();
+        if (mSubscriptions.add(conditionId)) {
+            evaluateSubscriptions();
+        }
     }
 
     @Override
@@ -121,9 +147,52 @@
     }
 
     private void evaluateSubscriptions() {
-        for (Uri conditionId : mSubscriptions) {
-            notifyCondition(conditionId, Condition.STATE_FALSE, "notImplemented");
+        if (DEBUG) Log.d(TAG, "evaluateSubscriptions");
+        if (!mBootComplete) {
+            if (DEBUG) Log.d(TAG, "Skipping evaluate before boot complete");
+            return;
         }
+        final long now = System.currentTimeMillis();
+        mTracker.setCallback(mSubscriptions.isEmpty() ? null : mTrackerCallback);
+        setRegistered(!mSubscriptions.isEmpty());
+        long reevaluateAt = 0;
+        for (Uri conditionId : mSubscriptions) {
+            final EventInfo event = ZenModeConfig.tryParseEventConditionId(conditionId);
+            if (event == null) {
+                notifyCondition(conditionId, Condition.STATE_FALSE, "badConditionId");
+                continue;
+            }
+            final CheckEventResult result = mTracker.checkEvent(event, now);
+            if (result.recheckAt != 0 && (reevaluateAt == 0 || result.recheckAt < reevaluateAt)) {
+                reevaluateAt = result.recheckAt;
+            }
+            if (!result.inEvent) {
+                notifyCondition(conditionId, Condition.STATE_FALSE, "!inEventNow");
+                continue;
+            }
+            notifyCondition(conditionId, Condition.STATE_TRUE, "inEventNow");
+        }
+        updateAlarm(now, reevaluateAt);
+        if (DEBUG) Log.d(TAG, "evaluateSubscriptions took " + (System.currentTimeMillis() - now));
+    }
+
+    private void updateAlarm(long now, long time) {
+        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
+                REQUEST_CODE_EVALUATE,
+                new Intent(ACTION_EVALUATE)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+                        .putExtra(EXTRA_TIME, time),
+                PendingIntent.FLAG_UPDATE_CURRENT);
+        alarms.cancel(pendingIntent);
+        if (time == 0 || time < now) {
+            if (DEBUG) Slog.d(TAG, "Not scheduling evaluate: " + (time == 0 ? "no time specified"
+                    : "specified time in the past"));
+            return;
+        }
+        if (DEBUG) Slog.d(TAG, String.format("Scheduling evaluate for %s, in %s, now=%s",
+                ts(time), formatDuration(time - now), ts(now)));
+        alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
     }
 
     private void notifyCondition(Uri conditionId, int state, String reason) {
@@ -139,4 +208,34 @@
         return new Condition(id, summary, line1, line2, 0, state, Condition.FLAG_RELEVANT_ALWAYS);
     }
 
+    private void setRegistered(boolean registered) {
+        if (mRegistered == registered) return;
+        if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
+        mRegistered = registered;
+        if (mRegistered) {
+            final IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_TIME_CHANGED);
+            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+            filter.addAction(ACTION_EVALUATE);
+            registerReceiver(mReceiver, filter);
+        } else {
+            unregisterReceiver(mReceiver);
+        }
+    }
+
+    private final CalendarTracker.Callback mTrackerCallback = new CalendarTracker.Callback() {
+        @Override
+        public void onChanged() {
+            if (DEBUG) Log.d(TAG, "mTrackerCallback.onChanged");
+            evaluateSubscriptions();
+        }
+    };
+
+    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
+            evaluateSubscriptions();
+        }
+    };
 }