Merge Android Pie into master

Bug: 112104996
Change-Id: Id3f1f5950e3abad06e0b3c39c6a8bbd09c78de02
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 255f39a..9816d4f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -18,8 +18,6 @@
         package="com.android.providers.calendar"
         android:sharedUserId="android.uid.calendar">
 
-    <uses-sdk android:targetSdkVersion="25" android:minSdkVersion="25"/> <!-- TODO Remove it -->
-
     <uses-permission android:name="android.permission.READ_CALENDAR" />
     <uses-permission android:name="android.permission.WRITE_CALENDAR" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -38,14 +36,12 @@
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
+    <uses-permission android:name="android.permission.USE_RESERVED_DISK" />
 
     <application android:label="@string/calendar_storage"
                  android:allowBackup="false"
                  android:icon="@drawable/app_icon"
                  android:usesCleartextTraffic="false">
-        <!-- TODO: Remove dependency of application on the test runner
-             (android.test) library. -->
-        <uses-library android:name="android.test.runner" />
 
         <provider android:name="CalendarProvider2" android:authorities="com.android.calendar"
                 android:label="@string/provider_label"
diff --git a/src/com/android/providers/calendar/CalendarAlarmManager.java b/src/com/android/providers/calendar/CalendarAlarmManager.java
index 8586e6b..7019797 100644
--- a/src/com/android/providers/calendar/CalendarAlarmManager.java
+++ b/src/com/android/providers/calendar/CalendarAlarmManager.java
@@ -106,6 +106,10 @@
             "com.android.providers.calendar.intent.CalendarProvider2";
     static final int ALARM_CHECK_DELAY_MILLIS = 5000;
 
+    /** 24 hours - 15 minutes. */
+    static final long NEXT_ALARM_CHECK_TIME_MS = DateUtils.DAY_IN_MILLIS -
+            (15 * DateUtils.MINUTE_IN_MILLIS);
+
     /**
      * Used for tracking if the next alarm is already scheduled
      */
@@ -123,9 +127,6 @@
 
     public CalendarAlarmManager(Context context) {
         initializeWithContext(context);
-
-        PowerManager powerManager = (PowerManager) mContext.getSystemService(
-                Context.POWER_SERVICE);
     }
 
     protected void initializeWithContext(Context context) {
@@ -139,10 +140,16 @@
     static Intent getCheckNextAlarmIntent(Context context, boolean removeAlarms) {
         Intent intent = new Intent(CalendarAlarmManager.ACTION_CHECK_NEXT_ALARM);
         intent.setClass(context, CalendarProviderBroadcastReceiver.class);
-        intent.putExtra(KEY_REMOVE_ALARMS, removeAlarms);
+        if (removeAlarms) {
+            intent.putExtra(KEY_REMOVE_ALARMS, true);
+        }
         return intent;
     }
 
+    public static Intent getCheckNextAlarmIntentForBroadcast(Context context) {
+        return getCheckNextAlarmIntent(context, false);
+    }
+
     /**
      * Called by CalendarProvider to check the next alarm. A small delay is added before the real
      * checking happens in order to batch the requests.
@@ -178,33 +185,11 @@
         }
     }
 
-    /**
-     * Similar to {@link #checkNextAlarm}, but schedule the checking at specific {@code
-     * triggerTime}. In general, we do not need an alarm for scheduling. Instead we set the next
-     * alarm check immediately when a reminder is shown. The only use case for this
-     * is to schedule the next alarm check when there is no reminder within 1 day.
-     *
-     * @param triggerTimeMillis Time to run the next alarm check, in milliseconds.
-     */
-    void scheduleNextAlarmCheck(long triggerTimeMillis) {
-        Intent intent = getCheckNextAlarmIntent(mContext, false /* removeAlarms*/);
-        PendingIntent pending = PendingIntent.getBroadcast(
-                mContext, 0, intent, PendingIntent.FLAG_NO_CREATE);
-        if (pending != null) {
-            // Cancel any previous alarms that do the same thing.
-            cancel(pending);
-        }
-        pending = PendingIntent.getBroadcast(
-                mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
-
-        if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
-            Time time = new Time();
-            time.set(triggerTimeMillis);
-            String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
-            Log.d(CalendarProvider2.TAG,
-                    "scheduleNextAlarmCheck at: " + triggerTimeMillis + timeStr);
-        }
-        setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTimeMillis, pending);
+    static void checkNextAlarmCheckRightNow(Context context) {
+        // We should probably call scheduleNextAlarmLocked() directly but we don't want
+        // to mix java synchronization and DB transactions that might cause deadlocks, so we
+        // just send a broadcast to serialize all the calls.
+        context.sendBroadcast(getCheckNextAlarmIntentForBroadcast(context));
     }
 
     void rescheduleMissedAlarms() {
@@ -264,6 +249,8 @@
      * @param cp2 TODO
      */
     private void scheduleNextAlarmLocked(SQLiteDatabase db, CalendarProvider2 cp2) {
+        CalendarSanityChecker.getInstance(mContext).updateLastCheckTime();
+
         Time time = new Time();
 
         final long currentMillis = System.currentTimeMillis();
@@ -467,14 +454,8 @@
         }
 
         // No event alarm is scheduled, check again in 24 hours - 15
-        // minutes. Scheduling the check 15 minutes earlier than 24
-        // hours to prevent the scheduler alarm from using up the
-        // alarms quota for reminders during dozing. If a new event is
-        // inserted before the next alarm check, then this method will
-        // be run again when the new event is inserted.
-        if (!alarmScheduled) {
-            scheduleNextAlarmCheck(end - (15 * DateUtils.MINUTE_IN_MILLIS));
-        }
+        // minutes.
+        // We have a repeated alarm to check the next even every N hours, so nothing to do here.
     }
 
     /**
diff --git a/src/com/android/providers/calendar/CalendarProvider2.java b/src/com/android/providers/calendar/CalendarProvider2.java
index 55e8c6d..c7f1f7b 100644
--- a/src/com/android/providers/calendar/CalendarProvider2.java
+++ b/src/com/android/providers/calendar/CalendarProvider2.java
@@ -24,12 +24,15 @@
 import android.app.AppOpsManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.OperationApplicationException;
 import android.content.UriMatcher;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
@@ -63,12 +66,15 @@
 import com.android.calendarcommon2.EventRecurrence;
 import com.android.calendarcommon2.RecurrenceProcessor;
 import com.android.calendarcommon2.RecurrenceSet;
+import com.android.internal.util.ProviderAccessStats;
 import com.android.providers.calendar.CalendarDatabaseHelper.Tables;
 import com.android.providers.calendar.CalendarDatabaseHelper.Views;
 import com.google.android.collect.Sets;
 import com.google.common.annotations.VisibleForTesting;
 
 import java.io.File;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.lang.reflect.Array;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -445,6 +451,9 @@
     @VisibleForTesting
     protected CalendarAlarmManager mCalendarAlarm;
 
+    private final ThreadLocal<Integer> mCallingUid = new ThreadLocal<>();
+    private final ProviderAccessStats mStats = new ProviderAccessStats();
+
     /**
      * Listens for timezone changes and disk-no-longer-full events
      */
@@ -805,11 +814,18 @@
     @Override
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
+        CalendarSanityChecker.getInstance(mContext).checkLastCheckTime();
+
+        // Note don't use mCallingUid here. That's only used by mutation functions.
+        final int callingUid = Binder.getCallingUid();
+
+        mStats.incrementQueryStats(callingUid);
         final long identity = clearCallingIdentityInternal();
         try {
             return queryInternal(uri, projection, selection, selectionArgs, sortOrder);
         } finally {
             restoreCallingIdentityInternal(identity);
+            mStats.finishOperation(callingUid);
         }
     }
 
@@ -2072,10 +2088,78 @@
     }
 
     @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        final int callingUid = Binder.getCallingUid();
+        mCallingUid.set(callingUid);
+
+        mStats.incrementBatchStats(callingUid);
+        try {
+            return super.bulkInsert(uri, values);
+        } finally {
+            mStats.finishOperation(callingUid);
+        }
+    }
+
+    @Override
+    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+            throws OperationApplicationException {
+        final int callingUid = Binder.getCallingUid();
+        mCallingUid.set(callingUid);
+
+        mStats.incrementBatchStats(callingUid);
+        try {
+            return super.applyBatch(operations);
+        } finally {
+            mStats.finishOperation(callingUid);
+        }
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        if (!applyingBatch()) {
+            mCallingUid.set(Binder.getCallingUid());
+        }
+
+        return super.insert(uri, values);
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        if (!applyingBatch()) {
+            mCallingUid.set(Binder.getCallingUid());
+        }
+
+        return super.update(uri, values, selection, selectionArgs);
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        if (!applyingBatch()) {
+            mCallingUid.set(Binder.getCallingUid());
+        }
+
+        return super.delete(uri, selection, selectionArgs);
+    }
+
+    @Override
     protected Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
+        final int callingUid = mCallingUid.get();
+
+        mStats.incrementInsertStats(callingUid, applyingBatch());
+        try {
+            return insertInTransactionInner(uri, values, callerIsSyncAdapter);
+        } finally {
+            mStats.finishOperation(callingUid);
+        }
+    }
+
+    private Uri insertInTransactionInner(
+            Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
         if (Log.isLoggable(TAG, Log.VERBOSE)) {
             Log.v(TAG, "insertInTransaction: " + uri);
         }
+        CalendarSanityChecker.getInstance(mContext).checkLastCheckTime();
+
         validateUriParameters(uri.getQueryParameterNames());
         final int match = sUriMatcher.match(uri);
         verifyTransactionAllowed(TRANSACTION_INSERT, uri, values, callerIsSyncAdapter, match,
@@ -3050,9 +3134,22 @@
     @Override
     protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
             boolean callerIsSyncAdapter) {
+        final int callingUid = mCallingUid.get();
+        mStats.incrementDeleteStats(callingUid, applyingBatch());
+        try {
+            return deleteInTransactionInner(uri, selection, selectionArgs, callerIsSyncAdapter);
+        } finally {
+            mStats.finishOperation(callingUid);
+        }
+    }
+
+    private int deleteInTransactionInner(Uri uri, String selection, String[] selectionArgs,
+            boolean callerIsSyncAdapter) {
         if (Log.isLoggable(TAG, Log.VERBOSE)) {
             Log.v(TAG, "deleteInTransaction: " + uri);
         }
+        CalendarSanityChecker.getInstance(mContext).checkLastCheckTime();
+
         validateUriParameters(uri.getQueryParameterNames());
         final int match = sUriMatcher.match(uri);
         verifyTransactionAllowed(TRANSACTION_DELETE, uri, null, callerIsSyncAdapter, match,
@@ -3915,9 +4012,23 @@
     @Override
     protected int updateInTransaction(Uri uri, ContentValues values, String selection,
             String[] selectionArgs, boolean callerIsSyncAdapter) {
+        final int callingUid = mCallingUid.get();
+        mStats.incrementUpdateStats(callingUid, applyingBatch());
+        try {
+            return updateInTransactionInner(uri, values, selection, selectionArgs,
+                    callerIsSyncAdapter);
+        } finally {
+            mStats.finishOperation(callingUid);
+        }
+    }
+
+    private int updateInTransactionInner(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs, boolean callerIsSyncAdapter) {
         if (Log.isLoggable(TAG, Log.VERBOSE)) {
             Log.v(TAG, "updateInTransaction: " + uri);
         }
+        CalendarSanityChecker.getInstance(mContext).checkLastCheckTime();
+
         validateUriParameters(uri.getQueryParameterNames());
         final int match = sUriMatcher.match(uri);
         verifyTransactionAllowed(TRANSACTION_UPDATE, uri, values, callerIsSyncAdapter, match,
@@ -5064,4 +5175,9 @@
             values.put(columnName, mutators + "," + packageName);
         }
     }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        mStats.dump(writer, "  ");
+    }
 }
diff --git a/src/com/android/providers/calendar/CalendarReceiver.java b/src/com/android/providers/calendar/CalendarReceiver.java
index d1d8a5a..55b75e0 100644
--- a/src/com/android/providers/calendar/CalendarReceiver.java
+++ b/src/com/android/providers/calendar/CalendarReceiver.java
@@ -16,15 +16,17 @@
 
 package com.android.providers.calendar;
 
+import android.app.AlarmManager;
+import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.os.PowerManager;
+import android.os.SystemProperties;
 import android.util.Log;
 
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
 /**
  * This IntentReceiver executes when the boot completes and ensures that
@@ -34,13 +36,26 @@
  * yet.
  */
 public class CalendarReceiver extends BroadcastReceiver {
-    private static final String TAG = "CalendarReceiver";
+    private static final String TAG = CalendarProvider2.TAG;
 
-    private final ExecutorService executor = Executors.newCachedThreadPool();
+    private static final long NEXT_EVENT_CHECK_INTERVAL =
+            SystemProperties.getLong("debug.calendar.check_interval", TimeUnit.HOURS.toMillis(6));
+    private static final int NEXT_EVENT_CHECK_PENDING_CODE = 100;
+
     private PowerManager.WakeLock mWakeLock;
 
     @Override
     public void onReceive(Context context, Intent intent) {
+        final String action = intent.getAction();
+
+        if (!Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+            Log.w(TAG, "Unexpected broadcast: " + action);
+            return;
+        }
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "BOOT_COMPLETED");
+        }
+
         if (mWakeLock == null) {
             PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
             mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "CalendarReceiver_Provider");
@@ -48,19 +63,15 @@
         }
         mWakeLock.acquire();
 
-        final String action = intent.getAction();
         final ContentResolver cr = context.getContentResolver();
         final PendingResult result = goAsync();
-        executor.submit(new Runnable() {
-            @Override
-            public void run() {
-                if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
-                    removeScheduledAlarms(cr);
-                }
-                result.finish();
-                mWakeLock.release();
-            }
-        });
+
+        new Thread(() -> {
+            setCalendarCheckAlarm(context);
+            removeScheduledAlarms(cr);
+            result.finish();
+            mWakeLock.release();
+        }).start();
     }
 
     /*
@@ -75,10 +86,19 @@
      * worry about serializing the use of the service.
      */
     private void removeScheduledAlarms(ContentResolver resolver) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Removing scheduled alarms");
-        }
         resolver.update(CalendarAlarmManager.SCHEDULE_ALARM_REMOVE_URI, null /* values */,
                 null /* where */, null /* selectionArgs */);
     }
+
+    private static void setCalendarCheckAlarm(Context context) {
+        final PendingIntent checkIntent = PendingIntent.getBroadcast(context,
+                NEXT_EVENT_CHECK_PENDING_CODE,
+                CalendarAlarmManager.getCheckNextAlarmIntentForBroadcast(context),
+                PendingIntent.FLAG_UPDATE_CURRENT);
+
+        final AlarmManager am = context.getSystemService(AlarmManager.class);
+
+        am.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                NEXT_EVENT_CHECK_INTERVAL, NEXT_EVENT_CHECK_INTERVAL, checkIntent);
+    }
 }
diff --git a/src/com/android/providers/calendar/CalendarSanityChecker.java b/src/com/android/providers/calendar/CalendarSanityChecker.java
new file mode 100644
index 0000000..19cb5b1
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarSanityChecker.java
@@ -0,0 +1,198 @@
+/*
+ * 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.providers.calendar;
+
+import android.annotation.Nullable;
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.SharedPreferences;
+import android.os.SystemClock;
+import android.os.UserManager;
+import android.provider.CalendarContract;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * We call {@link #checkLastCheckTime} at the provider public entry points to make sure
+ * {@link CalendarAlarmManager#scheduleNextAlarmLocked} has been called recently enough.
+ *
+ * atest tests/src/com/android/providers/calendar/CalendarSanityCheckerTest.java
+ */
+public class CalendarSanityChecker {
+    private static final String TAG = "CalendarSanityChecker";
+
+    private static final boolean DEBUG = false;
+
+    private static final long MAX_ALLOWED_CHECK_INTERVAL_MS =
+            CalendarAlarmManager.NEXT_ALARM_CHECK_TIME_MS;
+
+    /**
+     * If updateLastCheckTime isn't called after user unlock within this time,
+     * we call scheduleNextAlarmCheckRightNow.
+     */
+    private static final long MAX_ALLOWED_REAL_TIME_AFTER_UNLOCK_MS =
+            15 * DateUtils.MINUTE_IN_MILLIS;
+
+    /**
+     * Minimum interval between WTFs.
+     */
+    private static final long WTF_INTERVAL_MS = 60 * DateUtils.MINUTE_IN_MILLIS;
+
+    private static final String PREF_NAME = "sanity";
+    private static final String LAST_CHECK_REALTIME_PREF_KEY = "last_check_realtime";
+    private static final String LAST_CHECK_BOOT_COUNT_PREF_KEY = "last_check_boot_count";
+    private static final String LAST_WTF_REALTIME_PREF_KEY = "last_wtf_realtime";
+
+    private static CalendarSanityChecker sInstance;
+    private final Context mContext;
+
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    @VisibleForTesting
+    final SharedPreferences mPrefs;
+
+    protected CalendarSanityChecker(Context context) {
+        mContext = context;
+        mPrefs = mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+    }
+
+    @VisibleForTesting
+    protected long getRealtimeMillis() {
+        return SystemClock.elapsedRealtime();
+    }
+
+    @VisibleForTesting
+    protected long getBootCount() {
+        return Settings.Global.getLong(mContext.getContentResolver(), Global.BOOT_COUNT, 0);
+    }
+
+    @VisibleForTesting
+    protected long getUserUnlockTime() {
+        final UserManager um = mContext.getSystemService(UserManager.class);
+        final long startTime = um.getUserStartRealtime();
+        final long unlockTime = um.getUserUnlockRealtime();
+        if (DEBUG) {
+            Log.d(TAG, String.format("User start/unlock time=%d/%d", startTime, unlockTime));
+        }
+        return unlockTime;
+    }
+
+    public static synchronized CalendarSanityChecker getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new CalendarSanityChecker(context);
+        }
+        return sInstance;
+    }
+
+    /**
+     * Called by {@link CalendarAlarmManager#scheduleNextAlarmLocked}
+     */
+    public final void updateLastCheckTime() {
+        final long now = getRealtimeMillis();
+        if (DEBUG) {
+            Log.d(TAG, "updateLastCheckTime: now=" + now);
+        }
+        synchronized (mLock) {
+            mPrefs.edit()
+                    .putLong(LAST_CHECK_REALTIME_PREF_KEY, now)
+                    .putLong(LAST_CHECK_BOOT_COUNT_PREF_KEY, getBootCount())
+                    .apply();
+        }
+    }
+
+    /**
+     * Call this at public entry points. This will check if the last check time was recent enough,
+     * and otherwise it'll call {@link CalendarAlarmManager#checkNextAlarmCheckRightNow}.
+     */
+    public final boolean checkLastCheckTime() {
+        final long lastBootCount;
+        final long lastCheckTime;
+        final long lastWtfTime;
+
+        synchronized (mLock) {
+            lastBootCount = mPrefs.getLong(LAST_CHECK_BOOT_COUNT_PREF_KEY, -1);
+            lastCheckTime = mPrefs.getLong(LAST_CHECK_REALTIME_PREF_KEY, -1);
+            lastWtfTime = mPrefs.getLong(LAST_WTF_REALTIME_PREF_KEY, 0);
+
+            final long nowBootCount = getBootCount();
+            final long nowRealtime = getRealtimeMillis();
+
+            final long unlockTime = getUserUnlockTime();
+
+            if (DEBUG) {
+                Log.d(TAG, String.format("isStateValid: %d/%d %d/%d unlocked=%d lastWtf=%d",
+                        lastBootCount, nowBootCount, lastCheckTime, nowRealtime, unlockTime,
+                        lastWtfTime));
+            }
+
+            if (lastBootCount != nowBootCount) {
+                // This branch means updateLastCheckTime() hasn't been called since boot.
+
+                debug("checkLastCheckTime: Last check time not set.");
+
+                if (unlockTime == 0) {
+                    debug("checkLastCheckTime: unlockTime=0."); // This shouldn't happen though.
+                    return true;
+                }
+
+                if ((nowRealtime - unlockTime) <= MAX_ALLOWED_REAL_TIME_AFTER_UNLOCK_MS) {
+                    debug("checkLastCheckTime: nowRealtime okay.");
+                    return true;
+                }
+                debug("checkLastCheckTime: nowRealtime too old");
+            } else {
+                // This branch means updateLastCheckTime() has been called since boot.
+
+                if ((nowRealtime - lastWtfTime) <= WTF_INTERVAL_MS) {
+                    debug("checkLastCheckTime: Last WTF recent, skipping check.");
+                    return true;
+                }
+
+                if ((nowRealtime - lastCheckTime) <= MAX_ALLOWED_CHECK_INTERVAL_MS) {
+                    debug("checkLastCheckTime: Last check was recent, okay.");
+                    return true;
+                }
+            }
+            Slog.wtf(TAG, String.format("Last check time %d was too old. now=%d (boot count=%d/%d)",
+                    lastCheckTime, nowRealtime, lastBootCount, nowBootCount));
+
+            mPrefs.edit()
+                    .putLong(LAST_CHECK_REALTIME_PREF_KEY, 0)
+                    .putLong(LAST_WTF_REALTIME_PREF_KEY, nowRealtime)
+                    .putLong(LAST_CHECK_BOOT_COUNT_PREF_KEY, getBootCount())
+                    .apply();
+
+            // Note mCalendarProvider2 really shouldn't be null.
+            CalendarAlarmManager.checkNextAlarmCheckRightNow(mContext);
+        }
+        return false;
+    }
+
+    void debug(String message) {
+        if (DEBUG) {
+            Log.d(TAG, message);
+        }
+    }
+}
diff --git a/src/com/android/providers/calendar/SQLiteContentProvider.java b/src/com/android/providers/calendar/SQLiteContentProvider.java
index 0b18ff4..a752cc8 100644
--- a/src/com/android/providers/calendar/SQLiteContentProvider.java
+++ b/src/com/android/providers/calendar/SQLiteContentProvider.java
@@ -83,7 +83,7 @@
         return mOpenHelper;
     }
 
-    private boolean applyingBatch() {
+    protected boolean applyingBatch() {
         return mApplyingBatch.get() != null && mApplyingBatch.get();
     }
 
diff --git a/tests/Android.mk b/tests/Android.mk
index 07cf42c..88b228c 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -11,9 +11,14 @@
 LOCAL_PRIVATE_PLATFORM_APIS := true
 LOCAL_COMPATIBILITY_SUITE := device-tests
 
-LOCAL_STATIC_JAVA_LIBRARIES := calendar-common junit legacy-android-test
+LOCAL_STATIC_JAVA_LIBRARIES := calendar-common junit
 
-LOCAL_JAVA_LIBRARIES := ext android.test.runner
+LOCAL_JAVA_LIBRARIES := \
+    ext \
+    android.test.runner \
+    android.test.base \
+    android.test.mock \
+
 
 LOCAL_INSTRUMENTATION_FOR := CalendarProvider
 
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
index eddab0b..0b63420 100644
--- a/tests/AndroidTest.xml
+++ b/tests/AndroidTest.xml
@@ -23,5 +23,6 @@
     <test class="com.android.tradefed.testtype.InstrumentationTest" >
         <option name="package" value="com.android.providers.calendar.tests" />
         <option name="runner" value="android.test.InstrumentationTestRunner" />
+        <option name="hidden-api-checks" value="false"/>
     </test>
 </configuration>
diff --git a/tests/src/com/android/providers/calendar/CalendarSanityCheckerTest.java b/tests/src/com/android/providers/calendar/CalendarSanityCheckerTest.java
new file mode 100644
index 0000000..1586c61
--- /dev/null
+++ b/tests/src/com/android/providers/calendar/CalendarSanityCheckerTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.providers.calendar;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.text.format.DateUtils;
+
+public class CalendarSanityCheckerTest extends AndroidTestCase {
+    private class CalendarSanityCheckerTestable extends CalendarSanityChecker {
+        protected CalendarSanityCheckerTestable(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected long getRealtimeMillis() {
+            return mInjectedRealtimeMillis;
+        }
+
+        @Override
+        protected long getBootCount() {
+            return mInjectedBootCount;
+        }
+
+        @Override
+        protected long getUserUnlockTime() {
+            return mInjectedUnlockTime;
+        }
+    }
+
+    private long mInjectedRealtimeMillis = 1000000L;
+    private long mInjectedBootCount = 1000;
+    private long mInjectedUnlockTime = 0;
+
+    public void testWithoutLastCheckTime() {
+        CalendarSanityCheckerTestable target = new CalendarSanityCheckerTestable(getContext());
+        target.mPrefs.edit().clear().commit();
+
+        assertTrue(target.checkLastCheckTime());
+
+        // Unlock.
+        mInjectedUnlockTime = mInjectedRealtimeMillis;
+
+        mInjectedRealtimeMillis += 15 * 60 * 1000;
+        assertTrue(target.checkLastCheckTime());
+
+        mInjectedRealtimeMillis += 1;
+        assertFalse(target.checkLastCheckTime());
+    }
+
+    public void testWithLastCheckTime() {
+        CalendarSanityCheckerTestable target = new CalendarSanityCheckerTestable(getContext());
+        target.mPrefs.edit().clear().commit();
+
+        assertTrue(target.checkLastCheckTime());
+
+        mInjectedUnlockTime = mInjectedRealtimeMillis;
+
+        // Update the last check time.
+        mInjectedRealtimeMillis += 1 * 60 * 1000;
+        target.updateLastCheckTime();
+
+        // Right after, okay.
+        assertTrue(target.checkLastCheckTime());
+
+        // Still okay.
+        mInjectedRealtimeMillis += DateUtils.DAY_IN_MILLIS - (15 * DateUtils.MINUTE_IN_MILLIS);
+        assertTrue(target.checkLastCheckTime());
+
+        mInjectedRealtimeMillis += 1;
+        assertFalse(target.checkLastCheckTime());
+
+        // Repeat the same thing.
+        mInjectedRealtimeMillis += 1 * 60 * 1000;
+        target.updateLastCheckTime();
+
+        // Right after, okay.
+        assertTrue(target.checkLastCheckTime());
+
+        // Still okay.
+        mInjectedRealtimeMillis += DateUtils.DAY_IN_MILLIS - (15 * DateUtils.MINUTE_IN_MILLIS);
+        assertTrue(target.checkLastCheckTime());
+
+        mInjectedRealtimeMillis += 1;
+        assertFalse(target.checkLastCheckTime());
+
+        // Check again right after. This should pass because of WTF_INTERVAL_MS.
+        assertTrue(target.checkLastCheckTime());
+
+        mInjectedRealtimeMillis += 60 * 60 * 1000;
+
+        // Still okay.
+        assertTrue(target.checkLastCheckTime());
+
+        // Now WTF again.
+        mInjectedRealtimeMillis += 1;
+        assertFalse(target.checkLastCheckTime());
+
+        // Reboot.
+        mInjectedRealtimeMillis = 1000000L;
+        mInjectedBootCount++;
+
+        // Unlock.
+        mInjectedUnlockTime = mInjectedRealtimeMillis;
+
+        mInjectedRealtimeMillis += 15 * 60 * 1000;
+        assertTrue(target.checkLastCheckTime());
+
+        mInjectedRealtimeMillis += 1;
+        assertFalse(target.checkLastCheckTime());
+
+        // Check again right after. This should pass because of WTF_INTERVAL_MS.
+        assertTrue(target.checkLastCheckTime());
+    }
+}