Merge "packages/providers/CalendarProvider: Set LOCAL_SDK_VERSION where possible." am: ff030e8884 am: 9e831292f2
am: 71f7d1ca2d

Change-Id: I77939a9720058bab268e40d4be8622da308a40c1
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 255f39a..d0be2e9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -38,14 +38,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..9e0bb5f 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) {
@@ -207,6 +208,10 @@
         setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTimeMillis, pending);
     }
 
+    void scheduleNextAlarmCheckRightNow() {
+        scheduleNextAlarmCheck(System.currentTimeMillis());
+    }
+
     void rescheduleMissedAlarms() {
         rescheduleMissedAlarms(mContext.getContentResolver());
     }
@@ -264,6 +269,8 @@
      * @param cp2 TODO
      */
     private void scheduleNextAlarmLocked(SQLiteDatabase db, CalendarProvider2 cp2) {
+        CalendarSanityChecker.getInstance(mContext).updateLastCheckTime();
+
         Time time = new Time();
 
         final long currentMillis = System.currentTimeMillis();
@@ -473,7 +480,7 @@
         // 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));
+            scheduleNextAlarmCheck(currentMillis + NEXT_ALARM_CHECK_TIME_MS);
         }
     }
 
diff --git a/src/com/android/providers/calendar/CalendarProvider2.java b/src/com/android/providers/calendar/CalendarProvider2.java
index 55e8c6d..91d247d 100644
--- a/src/com/android/providers/calendar/CalendarProvider2.java
+++ b/src/com/android/providers/calendar/CalendarProvider2.java
@@ -805,6 +805,8 @@
     @Override
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
+        CalendarSanityChecker.getInstance(mContext).checkLastCheckTime();
+
         final long identity = clearCallingIdentityInternal();
         try {
             return queryInternal(uri, projection, selection, selectionArgs, sortOrder);
@@ -2076,6 +2078,8 @@
         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,
@@ -3053,6 +3057,8 @@
         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,
@@ -3918,6 +3924,8 @@
         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,
diff --git a/src/com/android/providers/calendar/CalendarSanityChecker.java b/src/com/android/providers/calendar/CalendarSanityChecker.java
new file mode 100644
index 0000000..f6f9b21
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarSanityChecker.java
@@ -0,0 +1,218 @@
+/*
+ * 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;
+
+    @Nullable // only null when initialization failed.
+    private final CalendarProvider2 mCalendarProvider2;
+
+    protected CalendarSanityChecker(Context context) {
+        mContext = context;
+        mPrefs = mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+        mCalendarProvider2 = getProvider(mContext);
+    }
+
+    protected static CalendarProvider2 getProvider(Context context) {
+        final IContentProvider iprovider =
+                context.getContentResolver().acquireProvider(CalendarContract.AUTHORITY);
+        final ContentProvider cprovider = ContentProvider.coerceToLocalContentProvider(iprovider);
+
+        if (!(cprovider instanceof CalendarProvider2)) {
+            Slog.wtf(TAG, "CalendarProvider2 not found in CalendarSanityChecker.");
+            return null;
+        }
+
+        return (CalendarProvider2) cprovider;
+    }
+
+    @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#scheduleNextAlarmCheckRightNow()}.
+     */
+    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.
+            if (mCalendarProvider2 != null) {
+                mCalendarProvider2.getOrCreateCalendarAlarmManager()
+                        .scheduleNextAlarmCheckRightNow();
+            }
+        }
+        return false;
+    }
+
+    void debug(String message) {
+        if (DEBUG) {
+            Log.d(TAG, message);
+        }
+    }
+}
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/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());
+    }
+}