OSDN Git Service

add sync polling
authorFred Quintana <fredq@google.com>
Wed, 27 Jan 2010 20:17:49 +0000 (12:17 -0800)
committerFred Quintana <fredq@google.com>
Fri, 5 Feb 2010 23:16:20 +0000 (15:16 -0800)
- added the ability to specify that a sync (of account/authority/extras)
  should occur at a given frequency
- the existing daily poll code was replaced with seeding each
  account/authority with a 24 hour periodic sync
- enhanced the "adb shell dumpsys content" output to show the
  periodic syncs and when they will next run

14 files changed:
Android.mk
api/current.xml
core/java/android/accounts/AccountManager.java
core/java/android/content/ContentResolver.java
core/java/android/content/ContentService.java
core/java/android/content/IContentService.aidl
core/java/android/content/PeriodicSync.aidl [new file with mode: 0644]
core/java/android/content/PeriodicSync.java [new file with mode: 0644]
core/java/android/content/SyncManager.java
core/java/android/content/SyncQueue.java
core/java/android/content/SyncStatusInfo.java
core/java/android/content/SyncStorageEngine.java
core/tests/coretests/src/android/content/SyncStorageEngineTest.java
tests/CoreTests/android/content/SyncQueueTest.java

index 682f286..ab1e7ea 100644 (file)
@@ -222,6 +222,7 @@ aidl_files := \
        frameworks/base/core/java/android/content/ComponentName.aidl \
        frameworks/base/core/java/android/content/Intent.aidl \
        frameworks/base/core/java/android/content/IntentSender.aidl \
+       frameworks/base/core/java/android/content/PeriodicSync.aidl \
        frameworks/base/core/java/android/content/SyncStats.aidl \
        frameworks/base/core/java/android/content/res/Configuration.aidl \
        frameworks/base/core/java/android/appwidget/AppWidgetProviderInfo.aidl \
index b3f9190..691bde4 100644 (file)
 <parameter name="name" type="java.lang.String">
 </parameter>
 </method>
+<method name="addPeriodicSync"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="account" type="android.accounts.Account">
+</parameter>
+<parameter name="authority" type="java.lang.String">
+</parameter>
+<parameter name="extras" type="android.os.Bundle">
+</parameter>
+<parameter name="pollFrequency" type="long">
+</parameter>
+</method>
 <method name="addStatusChangeListener"
  return="java.lang.Object"
  abstract="false"
  visibility="public"
 >
 </method>
+<method name="getPeriodicSyncs"
+ return="java.util.List&lt;android.content.PeriodicSync&gt;"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="account" type="android.accounts.Account">
+</parameter>
+<parameter name="authority" type="java.lang.String">
+</parameter>
+</method>
 <method name="getSyncAdapterTypes"
  return="android.content.SyncAdapterType[]"
  abstract="false"
 <parameter name="observer" type="android.database.ContentObserver">
 </parameter>
 </method>
+<method name="removePeriodicSync"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="account" type="android.accounts.Account">
+</parameter>
+<parameter name="authority" type="java.lang.String">
+</parameter>
+<parameter name="extras" type="android.os.Bundle">
+</parameter>
+</method>
 <method name="removeStatusChangeListener"
  return="void"
  abstract="false"
 >
 </method>
 </class>
+<class name="PeriodicSync"
+ extends="java.lang.Object"
+ abstract="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<implements name="android.os.Parcelable">
+</implements>
+<constructor name="PeriodicSync"
+ type="android.content.PeriodicSync"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="account" type="android.accounts.Account">
+</parameter>
+<parameter name="authority" type="java.lang.String">
+</parameter>
+<parameter name="extras" type="android.os.Bundle">
+</parameter>
+<parameter name="period" type="long">
+</parameter>
+</constructor>
+<method name="describeContents"
+ return="int"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="writeToParcel"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="dest" type="android.os.Parcel">
+</parameter>
+<parameter name="flags" type="int">
+</parameter>
+</method>
+<field name="CREATOR"
+ type="android.os.Parcelable.Creator"
+ transient="false"
+ volatile="false"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="account"
+ type="android.accounts.Account"
+ transient="false"
+ volatile="false"
+ static="false"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="authority"
+ type="java.lang.String"
+ transient="false"
+ volatile="false"
+ static="false"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="extras"
+ type="android.os.Bundle"
+ transient="false"
+ volatile="false"
+ static="false"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="period"
+ type="long"
+ transient="false"
+ volatile="false"
+ static="false"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+</class>
 <class name="ReceiverCallNotAllowedException"
  extends="android.util.AndroidRuntimeException"
  abstract="false"
  type="float"
  transient="false"
  volatile="false"
- value="0.001f"
+ value="0.0010f"
  static="true"
  final="true"
  deprecated="not deprecated"
  deprecated="not deprecated"
  visibility="public"
 >
-<parameter name="t" type="T">
+<parameter name="arg0" type="T">
 </parameter>
 </method>
 </interface>
index 414d963..19e741a 100644 (file)
@@ -271,7 +271,7 @@ public class AccountManager {
     }
 
     /**
-     * Add an account to the AccountManager's set of known accounts. 
+     * Add an account to the AccountManager's set of known accounts.
      * <p>
      * Requires that the caller has permission
      * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running
@@ -560,9 +560,13 @@ public class AccountManager {
      * user to enter credentials. If it is able to retrieve the authtoken it will be returned
      * in the result.
      * <p>
-     * If the authenticator needs to prompt the user for credentials it will return an intent for
+     * If the authenticator needs to prompt the user for credentials, rather than returning the
+     * authtoken it will instead return an intent for
      * an activity that will do the prompting. If an intent is returned and notifyAuthFailure
-     * is true then a notification will be created that launches this intent.
+     * is true then a notification will be created that launches this intent. This intent can be
+     * invoked by the caller directly to start the activity that prompts the user for the
+     * updated credentials. Otherwise this activity will not be run until the user activates
+     * the notification.
      * <p>
      * This call returns immediately but runs asynchronously and the result is accessed via the
      * {@link AccountManagerFuture} that is returned. This future is also passed as the sole
@@ -653,7 +657,7 @@ public class AccountManager {
                 if (accountType == null) {
                     Log.e(TAG, "the account must not be null");
                     // to unblock caller waiting on Future.get()
-                    set(new Bundle()); 
+                    set(new Bundle());
                     return;
                 }
                 mService.addAcount(mResponse, accountType, authTokenType,
@@ -1372,7 +1376,7 @@ public class AccountManager {
                 IntentFilter intentFilter = new IntentFilter();
                 intentFilter.addAction(LOGIN_ACCOUNTS_CHANGED_ACTION);
                 // To recover from disk-full.
-                intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); 
+                intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
                 mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter);
             }
         }
index eb2d7b1..b5587ed 100644 (file)
@@ -42,6 +42,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Collection;
 
 
 /**
@@ -966,6 +967,65 @@ public abstract class ContentResolver {
     }
 
     /**
+     * Specifies that a sync should be requested with the specified the account, authority,
+     * and extras at the given frequency. If there is already another periodic sync scheduled
+     * with the account, authority and extras then a new periodic sync won't be added, instead
+     * the frequency of the previous one will be updated.
+     * <p>
+     * These periodic syncs honor the "syncAutomatically" and "masterSyncAutomatically" settings.
+     * Although these sync are scheduled at the specified frequency, it may take longer for it to
+     * actually be started if other syncs are ahead of it in the sync operation queue. This means
+     * that the actual start time may drift.
+     *
+     * @param account the account to specify in the sync
+     * @param authority the provider to specify in the sync request
+     * @param extras extra parameters to go along with the sync request
+     * @param pollFrequency how frequently the sync should be performed, in seconds.
+     */
+    public static void addPeriodicSync(Account account, String authority, Bundle extras,
+            long pollFrequency) {
+        validateSyncExtrasBundle(extras);
+        try {
+            getContentService().addPeriodicSync(account, authority, extras, pollFrequency);
+        } catch (RemoteException e) {
+            // exception ignored; if this is thrown then it means the runtime is in the midst of
+            // being restarted
+        }
+    }
+
+    /**
+     * Remove a periodic sync. Has no affect if account, authority and extras don't match
+     * an existing periodic sync.
+     *
+     * @param account the account of the periodic sync to remove
+     * @param authority the provider of the periodic sync to remove
+     * @param extras the extras of the periodic sync to remove
+     */
+    public static void removePeriodicSync(Account account, String authority, Bundle extras) {
+        validateSyncExtrasBundle(extras);
+        try {
+            getContentService().removePeriodicSync(account, authority, extras);
+        } catch (RemoteException e) {
+            throw new RuntimeException("the ContentService should always be reachable", e);
+        }
+    }
+
+    /**
+     * Get the list of information about the periodic syncs for the given account and authority.
+     *
+     * @param account the account whose periodic syncs we are querying
+     * @param authority the provider whose periodic syncs we are querying
+     * @return a list of PeriodicSync objects. This list may be empty but will never be null.
+     */
+    public static List<PeriodicSync> getPeriodicSyncs(Account account, String authority) {
+        try {
+            return getContentService().getPeriodicSyncs(account, authority);
+        } catch (RemoteException e) {
+            throw new RuntimeException("the ContentService should always be reachable", e);
+        }
+    }
+
+    /**
      * Check if this account/provider is syncable.
      * @return >0 if it is syncable, 0 if not, and <0 if the state isn't known yet.
      */
index 974a667..e0dfab5 100644 (file)
@@ -32,6 +32,8 @@ import android.Manifest;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 
 /**
  * {@hide}
@@ -273,6 +275,42 @@ public final class ContentService extends IContentService.Stub {
         }
     }
 
+    public void addPeriodicSync(Account account, String authority, Bundle extras,
+            long pollFrequency) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+                "no permission to write the sync settings");
+        long identityToken = clearCallingIdentity();
+        try {
+            getSyncManager().getSyncStorageEngine().addPeriodicSync(
+                    account, authority, extras, pollFrequency);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void removePeriodicSync(Account account, String authority, Bundle extras) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+                "no permission to write the sync settings");
+        long identityToken = clearCallingIdentity();
+        try {
+            getSyncManager().getSyncStorageEngine().removePeriodicSync(account, authority, extras);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public List<PeriodicSync> getPeriodicSyncs(Account account, String providerName) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
+                "no permission to read the sync settings");
+        long identityToken = clearCallingIdentity();
+        try {
+            return getSyncManager().getSyncStorageEngine().getPeriodicSyncs(
+                    account, providerName);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
     public int getIsSyncable(Account account, String providerName) {
         mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
                 "no permission to read the sync settings");
index b0f14c1..2d906ed 100644 (file)
@@ -21,6 +21,7 @@ import android.content.ActiveSyncInfo;
 import android.content.ISyncStatusObserver;
 import android.content.SyncAdapterType;
 import android.content.SyncStatusInfo;
+import android.content.PeriodicSync;
 import android.net.Uri;
 import android.os.Bundle;
 import android.database.IContentObserver;
@@ -38,11 +39,11 @@ interface IContentService {
 
     void requestSync(in Account account, String authority, in Bundle extras);
     void cancelSync(in Account account, String authority);
-    
+
     /**
      * Check if the provider should be synced when a network tickle is received
      * @param providerName the provider whose setting we are querying
-     * @return true of the provider should be synced when a network tickle is received
+     * @return true if the provider should be synced when a network tickle is received
      */
     boolean getSyncAutomatically(in Account account, String providerName);
 
@@ -55,6 +56,33 @@ interface IContentService {
     void setSyncAutomatically(in Account account, String providerName, boolean sync);
 
     /**
+     * Get the frequency of the periodic poll, if any.
+     * @param providerName the provider whose setting we are querying
+     * @return the frequency of the periodic sync in seconds. If 0 then no periodic syncs
+     * will take place.
+     */
+    List<PeriodicSync> getPeriodicSyncs(in Account account, String providerName);
+
+    /**
+     * Set whether or not the provider is to be synced on a periodic basis.
+     *
+     * @param providerName the provider whose behavior is being controlled
+     * @param pollFrequency the period that a sync should be performed, in seconds. If this is
+     * zero or less then no periodic syncs will be performed.
+     */
+    void addPeriodicSync(in Account account, String providerName, in Bundle extras,
+      long pollFrequency);
+
+    /**
+     * Set whether or not the provider is to be synced on a periodic basis.
+     *
+     * @param providerName the provider whose behavior is being controlled
+     * @param pollFrequency the period that a sync should be performed, in seconds. If this is
+     * zero or less then no periodic syncs will be performed.
+     */
+    void removePeriodicSync(in Account account, String providerName, in Bundle extras);
+
+    /**
      * Check if this account/provider is syncable.
      * @return >0 if it is syncable, 0 if not, and <0 if the state isn't known yet.
      */
@@ -69,15 +97,15 @@ interface IContentService {
     void setMasterSyncAutomatically(boolean flag);
 
     boolean getMasterSyncAutomatically();
-    
+
     /**
      * Returns true if there is currently a sync operation for the given
      * account or authority in the pending list, or actively being processed.
      */
     boolean isSyncActive(in Account account, String authority);
-    
+
     ActiveSyncInfo getActiveSync();
-    
+
     /**
      * Returns the types of the SyncAdapters that are registered with the system.
      * @return Returns the types of the SyncAdapters that are registered with the system.
@@ -96,8 +124,8 @@ interface IContentService {
      * Return true if the pending status is true of any matching authorities.
      */
     boolean isSyncPending(in Account account, String authority);
-    
+
     void addStatusChangeListener(int mask, ISyncStatusObserver callback);
-    
+
     void removeStatusChangeListener(ISyncStatusObserver callback);
 }
diff --git a/core/java/android/content/PeriodicSync.aidl b/core/java/android/content/PeriodicSync.aidl
new file mode 100644 (file)
index 0000000..4530591
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2010 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 android.content;
+
+parcelable PeriodicSync;
diff --git a/core/java/android/content/PeriodicSync.java b/core/java/android/content/PeriodicSync.java
new file mode 100644 (file)
index 0000000..17813ec
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2010 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 android.content;
+
+import android.os.Parcelable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.accounts.Account;
+
+/**
+ * Value type that contains information about a periodic sync. Is parcelable, making it suitable
+ * for passing in an IPC.
+ */
+public class PeriodicSync implements Parcelable {
+    /** The account to be synced */
+    public final Account account;
+    /** The authority of the sync */
+    public final String authority;
+    /** Any extras that parameters that are to be passed to the sync adapter. */
+    public final Bundle extras;
+    /** How frequently the sync should be scheduled, in seconds. */
+    public final long period;
+
+    /** Creates a new PeriodicSync, copying the Bundle */
+    public PeriodicSync(Account account, String authority, Bundle extras, long period) {
+        this.account = account;
+        this.authority = authority;
+        this.extras = new Bundle(extras);
+        this.period = period;
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel dest, int flags) {
+        account.writeToParcel(dest, flags);
+        dest.writeString(authority);
+        dest.writeBundle(extras);
+        dest.writeLong(period);
+    }
+
+    public static final Creator<PeriodicSync> CREATOR = new Creator<PeriodicSync>() {
+        public PeriodicSync createFromParcel(Parcel source) {
+            return new PeriodicSync(Account.CREATOR.createFromParcel(source),
+                    source.readString(), source.readBundle(), source.readLong());
+        }
+
+        public PeriodicSync[] newArray(int size) {
+            return new PeriodicSync[size];
+        }
+    };
+
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+
+        if (!(o instanceof PeriodicSync)) {
+            return false;
+        }
+
+        final PeriodicSync other = (PeriodicSync) o;
+
+        return account.equals(other.account)
+                && authority.equals(other.authority)
+                && period == other.period
+                && SyncStorageEngine.equals(extras, other.extras);
+    }
+}
index 699b61d..619c7d5 100644 (file)
@@ -52,14 +52,7 @@ import android.util.EventLog;
 import android.util.Log;
 import android.util.Pair;
 
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.File;
 import java.io.FileDescriptor;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -74,12 +67,6 @@ import java.util.concurrent.CountDownLatch;
 public class SyncManager implements OnAccountsUpdateListener {
     private static final String TAG = "SyncManager";
 
-    // used during dumping of the Sync history
-    private static final long MILLIS_IN_HOUR = 1000 * 60 * 60;
-    private static final long MILLIS_IN_DAY = MILLIS_IN_HOUR * 24;
-    private static final long MILLIS_IN_WEEK = MILLIS_IN_DAY * 7;
-    private static final long MILLIS_IN_4WEEKS = MILLIS_IN_WEEK * 4;
-
     /** Delay a sync due to local changes this long. In milliseconds */
     private static final long LOCAL_SYNC_DELAY;
 
@@ -157,9 +144,7 @@ public class SyncManager implements OnAccountsUpdateListener {
     // set if the sync active indicator should be reported
     private boolean mNeedSyncActiveNotification = false;
 
-    private volatile boolean mSyncPollInitialized;
     private final PendingIntent mSyncAlarmIntent;
-    private final PendingIntent mSyncPollAlarmIntent;
     // Synchronized on "this". Instead of using this directly one should instead call
     // its accessor, getConnManager().
     private ConnectivityManager mConnManagerDoNotUseDirectly;
@@ -276,7 +261,6 @@ public class SyncManager implements OnAccountsUpdateListener {
                     // ignore the rest of the states -- leave our boolean alone.
             }
             if (mDataConnectionIsConnected) {
-                initializeSyncPoll();
                 sendCheckAlarmsMessage();
             }
         }
@@ -291,14 +275,8 @@ public class SyncManager implements OnAccountsUpdateListener {
     };
 
     private static final String ACTION_SYNC_ALARM = "android.content.syncmanager.SYNC_ALARM";
-    private static final String SYNC_POLL_ALARM = "android.content.syncmanager.SYNC_POLL_ALARM";
     private final SyncHandler mSyncHandler;
 
-    private static final int MAX_SYNC_POLL_DELAY_SECONDS = 36 * 60 * 60; // 36 hours
-    private static final int MIN_SYNC_POLL_DELAY_SECONDS = 24 * 60 * 60; // 24 hours
-
-    private static final String SYNCMANAGER_PREFS_FILENAME = "/data/system/syncmanager.prefs";
-
     private volatile boolean mBootCompleted = false;
 
     private ConnectivityManager getConnectivityManager() {
@@ -338,9 +316,6 @@ public class SyncManager implements OnAccountsUpdateListener {
         mSyncAlarmIntent = PendingIntent.getBroadcast(
                 mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0);
 
-        mSyncPollAlarmIntent = PendingIntent.getBroadcast(
-                mContext, 0 /* ignored */, new Intent(SYNC_POLL_ALARM), 0);
-
         IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
         context.registerReceiver(mConnectivityIntentReceiver, intentFilter);
 
@@ -396,49 +371,6 @@ public class SyncManager implements OnAccountsUpdateListener {
         }
     }
 
-    private synchronized void initializeSyncPoll() {
-        if (mSyncPollInitialized) return;
-        mSyncPollInitialized = true;
-
-        mContext.registerReceiver(new SyncPollAlarmReceiver(), new IntentFilter(SYNC_POLL_ALARM));
-
-        // load the next poll time from shared preferences
-        long absoluteAlarmTime = readSyncPollTime();
-
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "initializeSyncPoll: absoluteAlarmTime is " + absoluteAlarmTime);
-        }
-
-        // Convert absoluteAlarmTime to elapsed realtime. If this time was in the past then
-        // schedule the poll immediately, if it is too far in the future then cap it at
-        // MAX_SYNC_POLL_DELAY_SECONDS.
-        long absoluteNow = System.currentTimeMillis();
-        long relativeNow = SystemClock.elapsedRealtime();
-        long relativeAlarmTime = relativeNow;
-        if (absoluteAlarmTime > absoluteNow) {
-            long delayInMs = absoluteAlarmTime - absoluteNow;
-            final int maxDelayInMs = MAX_SYNC_POLL_DELAY_SECONDS * 1000;
-            if (delayInMs > maxDelayInMs) {
-                delayInMs = MAX_SYNC_POLL_DELAY_SECONDS * 1000;
-            }
-            relativeAlarmTime += delayInMs;
-        }
-
-        // schedule an alarm for the next poll time
-        scheduleSyncPollAlarm(relativeAlarmTime);
-    }
-
-    private void scheduleSyncPollAlarm(long relativeAlarmTime) {
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "scheduleSyncPollAlarm: relativeAlarmTime is " + relativeAlarmTime
-                    + ", now is " + SystemClock.elapsedRealtime()
-                    + ", delay is " + (relativeAlarmTime - SystemClock.elapsedRealtime()));
-        }
-        ensureAlarmService();
-        mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, relativeAlarmTime,
-                mSyncPollAlarmIntent);
-    }
-
     /**
      * Return a random value v that satisfies minValue <= v < maxValue. The difference between
      * maxValue and minValue must be less than Integer.MAX_VALUE.
@@ -453,68 +385,6 @@ public class SyncManager implements OnAccountsUpdateListener {
         return minValue + random.nextInt((int)spread);
     }
 
-    private void handleSyncPollAlarm() {
-        // determine the next poll time
-        long delayMs = jitterize(MIN_SYNC_POLL_DELAY_SECONDS, MAX_SYNC_POLL_DELAY_SECONDS) * 1000;
-        long nextRelativePollTimeMs = SystemClock.elapsedRealtime() + delayMs;
-
-        if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "handleSyncPollAlarm: delay " + delayMs);
-
-        // write the absolute time to shared preferences
-        writeSyncPollTime(System.currentTimeMillis() + delayMs);
-
-        // schedule an alarm for the next poll time
-        scheduleSyncPollAlarm(nextRelativePollTimeMs);
-
-        // perform a poll
-        scheduleSync(null /* sync all syncable accounts */, null /* sync all syncable providers */,
-                new Bundle(), 0 /* no delay */, false /* onlyThoseWithUnkownSyncableState */);
-    }
-
-    private void writeSyncPollTime(long when) {
-        File f = new File(SYNCMANAGER_PREFS_FILENAME);
-        DataOutputStream str = null;
-        try {
-            str = new DataOutputStream(new FileOutputStream(f));
-            str.writeLong(when);
-        } catch (FileNotFoundException e) {
-            Log.w(TAG, "error writing to file " + f, e);
-        } catch (IOException e) {
-            Log.w(TAG, "error writing to file " + f, e);
-        } finally {
-            if (str != null) {
-                try {
-                    str.close();
-                } catch (IOException e) {
-                    Log.w(TAG, "error closing file " + f, e);
-                }
-            }
-        }
-    }
-
-    private long readSyncPollTime() {
-        File f = new File(SYNCMANAGER_PREFS_FILENAME);
-
-        DataInputStream str = null;
-        try {
-            str = new DataInputStream(new FileInputStream(f));
-            return str.readLong();
-        } catch (FileNotFoundException e) {
-            writeSyncPollTime(0);
-        } catch (IOException e) {
-            Log.w(TAG, "error reading file " + f, e);
-        } finally {
-            if (str != null) {
-                try {
-                    str.close();
-                } catch (IOException e) {
-                    Log.w(TAG, "error closing file " + f, e);
-                }
-            }
-        }
-        return 0;
-    }
-
     public ActiveSyncContext getActiveSyncContext() {
         return mActiveSyncContext;
     }
@@ -799,12 +669,6 @@ public class SyncManager implements OnAccountsUpdateListener {
         }
     }
 
-    class SyncPollAlarmReceiver extends BroadcastReceiver {
-        public void onReceive(Context context, Intent intent) {
-            handleSyncPollAlarm();
-        }
-    }
-
     private void clearBackoffSetting(SyncOperation op) {
         mSyncStorageEngine.setBackoff(op.account, op.authority,
                 SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE);
@@ -923,7 +787,7 @@ public class SyncManager implements OnAccountsUpdateListener {
         mSyncStorageEngine.setBackoff(account, authority,
                 SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE);
         synchronized (mSyncQueue) {
-            mSyncQueue.clear(account, authority);
+            mSyncQueue.remove(account, authority);
         }
     }
 
@@ -1084,7 +948,8 @@ public class SyncManager implements OnAccountsUpdateListener {
             pw.println("none");
         }
         final long now = SystemClock.elapsedRealtime();
-        pw.print("now: "); pw.println(now);
+        pw.print("now: "); pw.print(now);
+        pw.println(" (" + formatTime(System.currentTimeMillis()) + ")");
         pw.print("uptime: "); pw.print(DateUtils.formatElapsedTime(now/1000));
                 pw.println(" (HH:MM:SS)");
         pw.print("time spent syncing: ");
@@ -1102,7 +967,9 @@ public class SyncManager implements OnAccountsUpdateListener {
             pw.println("no alarm is scheduled (there had better not be any pending syncs)");
         }
 
-        pw.print("active sync: "); pw.println(mActiveSyncContext);
+        final SyncManager.ActiveSyncContext activeSyncContext = mActiveSyncContext;
+
+        pw.print("active sync: "); pw.println(activeSyncContext);
 
         pw.print("notification info: ");
         sb.setLength(0);
@@ -1125,6 +992,11 @@ public class SyncManager implements OnAccountsUpdateListener {
                     pw.print(authority != null ? authority.account : "<no account>");
                     pw.print(" ");
                     pw.print(authority != null ? authority.authority : "<no account>");
+                    if (activeSyncContext != null) {
+                        pw.print(" ");
+                        pw.print(SyncStorageEngine.SOURCES[
+                                activeSyncContext.mSyncOperation.syncSource]);
+                    }
                     pw.print(", duration is ");
                     pw.println(DateUtils.formatElapsedTime(durationInSeconds));
         } else {
@@ -1152,80 +1024,76 @@ public class SyncManager implements OnAccountsUpdateListener {
             }
         }
 
-        HashSet<Account> processedAccounts = new HashSet<Account>();
-        ArrayList<SyncStatusInfo> statuses
-                = mSyncStorageEngine.getSyncStatus();
-        if (statuses != null && statuses.size() > 0) {
-            pw.println();
-            pw.println("Sync Status");
-            final int N = statuses.size();
-            for (int i=0; i<N; i++) {
-                SyncStatusInfo status = statuses.get(i);
-                SyncStorageEngine.AuthorityInfo authority
-                        = mSyncStorageEngine.getAuthority(status.authorityId);
-                if (authority != null) {
-                    Account curAccount = authority.account;
-
-                    if (processedAccounts.contains(curAccount)) {
-                        continue;
-                    }
-
-                    processedAccounts.add(curAccount);
+        // join the installed sync adapter with the accounts list and emit for everything
+        pw.println();
+        pw.println("Sync Status");
+        for (Account account : accounts) {
+            pw.print("  Account "); pw.print(account.name);
+                    pw.print(" "); pw.print(account.type);
+                    pw.println(":");
+            for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterType :
+                    mSyncAdapters.getAllServices()) {
+                if (!syncAdapterType.type.accountType.equals(account.type)) {
+                    continue;
+                }
 
-                    pw.print("  Account "); pw.print(authority.account.name);
-                            pw.print(" "); pw.print(authority.account.type);
-                            pw.println(":");
-                    for (int j=i; j<N; j++) {
-                        status = statuses.get(j);
-                        authority = mSyncStorageEngine.getAuthority(status.authorityId);
-                        if (!curAccount.equals(authority.account)) {
-                            continue;
-                        }
-                        pw.print("    "); pw.print(authority.authority);
-                        pw.println(":");
-                        final String syncable = authority.syncable > 0
-                                ? "syncable"
-                                : (authority.syncable == 0 ? "not syncable" : "not initialized");
-                        final String enabled = authority.enabled ? "enabled" : "disabled";
-                        final String delayUntil = authority.delayUntil > now
-                                ? "delay for " + ((authority.delayUntil - now) / 1000) + " sec"
-                                : "no delay required";
-                        final String backoff = authority.backoffTime > now
-                                ? "backoff for " + ((authority.backoffTime - now) / 1000)
-                                  + " sec"
-                                : "no backoff required";
-                        final String backoffDelay = authority.backoffDelay > 0
-                                ? ("the backoff increment is " + authority.backoffDelay / 1000
-                                        + " sec")
-                                : "no backoff increment";
-                        pw.println(String.format(
-                                "      settings: %s, %s, %s, %s, %s",
-                                enabled, syncable, backoff, backoffDelay, delayUntil));
-                        pw.print("      count: local="); pw.print(status.numSourceLocal);
-                                pw.print(" poll="); pw.print(status.numSourcePoll);
-                                pw.print(" server="); pw.print(status.numSourceServer);
-                                pw.print(" user="); pw.print(status.numSourceUser);
-                                pw.print(" total="); pw.println(status.numSyncs);
-                        pw.print("      total duration: ");
-                                pw.println(DateUtils.formatElapsedTime(
-                                        status.totalElapsedTime/1000));
-                        if (status.lastSuccessTime != 0) {
-                            pw.print("      SUCCESS: source=");
-                                    pw.print(SyncStorageEngine.SOURCES[
-                                            status.lastSuccessSource]);
-                                    pw.print(" time=");
-                                    pw.println(formatTime(status.lastSuccessTime));
-                        } else {
-                            pw.print("      FAILURE: source=");
-                                    pw.print(SyncStorageEngine.SOURCES[
-                                            status.lastFailureSource]);
-                                    pw.print(" initialTime=");
-                                    pw.print(formatTime(status.initialFailureTime));
-                                    pw.print(" lastTime=");
-                                    pw.println(formatTime(status.lastFailureTime));
-                            pw.print("      message: "); pw.println(status.lastFailureMesg);
-                        }
-                    }
+                SyncStorageEngine.AuthorityInfo settings = mSyncStorageEngine.getAuthority(
+                        account, syncAdapterType.type.authority);
+                SyncStatusInfo status = mSyncStorageEngine.getOrCreateSyncStatus(settings);
+                pw.print("    "); pw.print(settings.authority);
+                pw.println(":");
+                pw.print("      settings:");
+                pw.print(" " + (settings.syncable > 0
+                        ? "syncable"
+                        : (settings.syncable == 0 ? "not syncable" : "not initialized")));
+                pw.print(", " + (settings.enabled ? "enabled" : "disabled"));
+                if (settings.delayUntil > now) {
+                    pw.print(", delay for "
+                            + ((settings.delayUntil - now) / 1000) + " sec");
+                }
+                if (settings.backoffTime > now) {
+                    pw.print(", backoff for "
+                            + ((settings.backoffTime - now) / 1000) + " sec");
+                }
+                if (settings.backoffDelay > 0) {
+                    pw.print(", the backoff increment is " + settings.backoffDelay / 1000
+                                + " sec");
+                }
+                pw.println();
+                for (int periodicIndex = 0;
+                        periodicIndex < settings.periodicSyncs.size();
+                        periodicIndex++) {
+                    Pair<Bundle, Long> info = settings.periodicSyncs.get(periodicIndex);
+                    long lastPeriodicTime = status.getPeriodicSyncTime(periodicIndex);
+                    long nextPeriodicTime = lastPeriodicTime + info.second * 1000;
+                    pw.println("      periodic period=" + info.second
+                            + ", extras=" + info.first
+                            + ", next=" + formatTime(nextPeriodicTime));
+                }
+                pw.print("      count: local="); pw.print(status.numSourceLocal);
+                pw.print(" poll="); pw.print(status.numSourcePoll);
+                pw.print(" periodic="); pw.print(status.numSourcePeriodic);
+                pw.print(" server="); pw.print(status.numSourceServer);
+                pw.print(" user="); pw.print(status.numSourceUser);
+                pw.print(" total="); pw.print(status.numSyncs);
+                pw.println();
+                pw.print("      total duration: ");
+                pw.println(DateUtils.formatElapsedTime(status.totalElapsedTime/1000));
+                if (status.lastSuccessTime != 0) {
+                    pw.print("      SUCCESS: source=");
+                    pw.print(SyncStorageEngine.SOURCES[status.lastSuccessSource]);
+                    pw.print(" time=");
+                    pw.println(formatTime(status.lastSuccessTime));
+                }
+                if (status.lastFailureTime != 0) {
+                    pw.print("      FAILURE: source=");
+                    pw.print(SyncStorageEngine.SOURCES[
+                            status.lastFailureSource]);
+                    pw.print(" initialTime=");
+                    pw.print(formatTime(status.initialFailureTime));
+                    pw.print(" lastTime=");
+                    pw.println(formatTime(status.lastFailureTime));
+                    pw.print("      message: "); pw.println(status.lastFailureMesg);
                 }
             }
         }
@@ -1580,6 +1448,36 @@ public class SyncManager implements OnAccountsUpdateListener {
             }
         }
 
+        private boolean isSyncAllowed(Account account, String authority, boolean manualSync,
+                boolean backgroundDataUsageAllowed) {
+            Account[] accounts = mAccounts;
+
+            // Sync is disabled, drop this operation.
+            if (!isSyncEnabled()) {
+                return false;
+            }
+
+            // skip the sync if the account of this operation no longer exists
+            if (accounts == null || !ArrayUtils.contains(accounts, account)) {
+                return false;
+            }
+
+            // skip the sync if it isn't manual and auto sync is disabled
+            final boolean syncAutomatically =
+                    mSyncStorageEngine.getSyncAutomatically(account, authority)
+                            && mSyncStorageEngine.getMasterSyncAutomatically();
+            if (!(manualSync || (backgroundDataUsageAllowed && syncAutomatically))) {
+                return false;
+            }
+
+            if (mSyncStorageEngine.getIsSyncable(account, authority) <= 0) {
+                // if not syncable or if the syncable is unknown (< 0), don't allow
+                return false;
+            }
+
+            return true;
+        }
+
         private void runStateSyncing() {
             // if the sync timeout has been reached then cancel it
 
@@ -1589,7 +1487,7 @@ public class SyncManager implements OnAccountsUpdateListener {
             if (now > activeSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC) {
                 SyncOperation nextSyncOperation;
                 synchronized (mSyncQueue) {
-                    nextSyncOperation = mSyncQueue.nextReadyToRun(now);
+                    nextSyncOperation = getNextReadyToRunSyncOperation(now);
                 }
                 if (nextSyncOperation != null) {
                     Log.d(TAG, "canceling and rescheduling sync because it ran too long: "
@@ -1643,7 +1541,7 @@ public class SyncManager implements OnAccountsUpdateListener {
             synchronized (mSyncQueue) {
                 final long now = SystemClock.elapsedRealtime();
                 while (true) {
-                    op = mSyncQueue.nextReadyToRun(now);
+                    op = getNextReadyToRunSyncOperation(now);
                     if (op == null) {
                         if (isLoggable) {
                             Log.v(TAG, "runStateIdle: no more sync operations, returning");
@@ -1655,42 +1553,9 @@ public class SyncManager implements OnAccountsUpdateListener {
                     // from the queue now
                     mSyncQueue.remove(op);
 
-                    // Sync is disabled, drop this operation.
-                    if (!isSyncEnabled()) {
-                        if (isLoggable) {
-                            Log.v(TAG, "runStateIdle: sync disabled, dropping " + op);
-                        }
-                        continue;
-                    }
-
-                    // skip the sync if the account of this operation no longer exists
-                    if (!ArrayUtils.contains(accounts, op.account)) {
-                        if (isLoggable) {
-                            Log.v(TAG, "runStateIdle: account not present, dropping " + op);
-                        }
-                        continue;
-                    }
-
-                    // skip the sync if it isn't manual and auto sync is disabled
-                    final boolean manualSync =
-                            op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
-                    final boolean syncAutomatically =
-                            mSyncStorageEngine.getSyncAutomatically(op.account, op.authority)
-                                    && mSyncStorageEngine.getMasterSyncAutomatically();
-                    if (!(manualSync || (backgroundDataUsageAllowed && syncAutomatically))) {
-                        if (isLoggable) {
-                            Log.v(TAG, "runStateIdle: sync of this operation is not allowed, "
-                                    + "dropping " + op);
-                        }
-                        continue;
-                    }
-
-                    if (mSyncStorageEngine.getIsSyncable(op.account, op.authority) <= 0) {
-                        // if not syncable or if the syncable is unknown (< 0), don't allow
-                        if (isLoggable) {
-                            Log.v(TAG, "runStateIdle: sync of this operation is not allowed, "
-                                    + "dropping " + op);
-                        }
+                    if (!isSyncAllowed(op.account, op.authority,
+                            op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false),
+                            backgroundDataUsageAllowed)) {
                         continue;
                     }
 
@@ -1736,6 +1601,74 @@ public class SyncManager implements OnAccountsUpdateListener {
             // MESSAGE_SERVICE_CONNECTED or MESSAGE_SERVICE_DISCONNECTED message
         }
 
+        private SyncOperation getNextPeriodicSyncOperation() {
+            final boolean backgroundDataUsageAllowed =
+                    getConnectivityManager().getBackgroundDataSetting();
+            SyncStorageEngine.AuthorityInfo best = null;
+            long bestPollTimeAbsolute = Long.MAX_VALUE;
+            Bundle bestExtras = null;
+            ArrayList<SyncStorageEngine.AuthorityInfo> infos = mSyncStorageEngine.getAuthorities();
+            for (SyncStorageEngine.AuthorityInfo info : infos) {
+                if (!isSyncAllowed(info.account, info.authority, false /* manualSync */,
+                        backgroundDataUsageAllowed)) {
+                    continue;
+                }
+                SyncStatusInfo status = mSyncStorageEngine.getStatusByAccountAndAuthority(
+                        info.account, info.authority);
+                int i = 0;
+                for (Pair<Bundle, Long> periodicSync : info.periodicSyncs) {
+                    long lastPollTimeAbsolute = status != null ? status.getPeriodicSyncTime(i) : 0;
+                    final Bundle extras = periodicSync.first;
+                    final Long periodInSeconds = periodicSync.second;
+                    long nextPollTimeAbsolute = lastPollTimeAbsolute + periodInSeconds * 1000;
+                    if (nextPollTimeAbsolute < bestPollTimeAbsolute) {
+                        best = info;
+                        bestPollTimeAbsolute = nextPollTimeAbsolute;
+                        bestExtras = extras;
+                    }
+                    i++;
+                }
+            }
+
+            if (best == null) {
+                return null;
+            }
+
+            final long nowAbsolute = System.currentTimeMillis();
+            final SyncOperation syncOperation = new SyncOperation(best.account,
+                    SyncStorageEngine.SOURCE_PERIODIC,
+                    best.authority, bestExtras, 0 /* delay */);
+            syncOperation.earliestRunTime = SystemClock.elapsedRealtime()
+                    + (bestPollTimeAbsolute - nowAbsolute);
+            if (syncOperation.earliestRunTime < 0) {
+                syncOperation.earliestRunTime = 0;
+            }
+            return syncOperation;
+        }
+
+        public Pair<SyncOperation, Long> bestSyncOperationCandidate() {
+            Pair<SyncOperation, Long> nextOpAndRunTime = mSyncQueue.nextOperation();
+            SyncOperation nextOp = nextOpAndRunTime != null ? nextOpAndRunTime.first : null;
+            Long nextRunTime = nextOpAndRunTime != null ? nextOpAndRunTime.second : null;
+            SyncOperation pollOp = getNextPeriodicSyncOperation();
+            if (nextOp != null
+                    && (pollOp == null || nextOp.expedited
+                        || nextRunTime <= pollOp.earliestRunTime)) {
+                return nextOpAndRunTime;
+            } else if (pollOp != null) {
+                return Pair.create(pollOp, pollOp.earliestRunTime);
+            } else {
+                return null;
+            }
+        }
+
+        private SyncOperation getNextReadyToRunSyncOperation(long now) {
+            Pair<SyncOperation, Long> nextOpAndRunTime = bestSyncOperationCandidate();
+            return nextOpAndRunTime != null && nextOpAndRunTime.second <= now
+                    ? nextOpAndRunTime.first
+                    : null;
+        }
+
         private void runBoundToSyncAdapter(ISyncAdapter syncAdapter) {
             mActiveSyncContext.mSyncAdapter = syncAdapter;
             final SyncOperation syncOperation = mActiveSyncContext.mSyncOperation;
@@ -1961,7 +1894,8 @@ public class SyncManager implements OnAccountsUpdateListener {
             ActiveSyncContext activeSyncContext = mActiveSyncContext;
             if (activeSyncContext == null) {
                 synchronized (mSyncQueue) {
-                    alarmTime = mSyncQueue.nextRunTime(now);
+                    Pair<SyncOperation, Long> candidate = bestSyncOperationCandidate();
+                    alarmTime = candidate != null ? candidate.second : 0;
                 }
             } else {
                 final long notificationTime =
@@ -2102,9 +2036,22 @@ public class SyncManager implements OnAccountsUpdateListener {
                                 SyncStorageEngine.EVENT_STOP, syncOperation.syncSource,
                                 syncOperation.account.name.hashCode());
 
-            mSyncStorageEngine.stopSyncEvent(rowId, elapsedTime, resultMessage,
-                    downstreamActivity, upstreamActivity);
+            mSyncStorageEngine.stopSyncEvent(rowId, syncOperation.extras, elapsedTime,
+                    resultMessage, downstreamActivity, upstreamActivity);
         }
     }
 
+    public static long runTimeWithBackoffs(SyncStorageEngine syncStorageEngine,
+            Account account, String authority, boolean isManualSync, long runTime) {
+        // if this is a manual sync, the run time is unchanged
+        // otherwise, the run time is the max of the backoffs and the run time.
+        if (isManualSync) {
+            return runTime;
+        }
+
+        Pair<Long, Long> backoff = syncStorageEngine.getBackoff(account, authority);
+        long delayUntilTime = syncStorageEngine.getDelayUntilTime(account, authority);
+
+        return Math.max(Math.max(runTime, delayUntilTime), backoff != null ? backoff.first : 0);
+    }
 }
index a9f15d9..2eead3a 100644 (file)
@@ -2,8 +2,6 @@ package android.content;
 
 import com.google.android.collect.Maps;
 
-import android.os.Bundle;
-import android.os.SystemClock;
 import android.util.Pair;
 import android.util.Log;
 import android.accounts.Account;
@@ -32,10 +30,9 @@ public class SyncQueue {
         final int N = ops.size();
         for (int i=0; i<N; i++) {
             SyncStorageEngine.PendingOperation op = ops.get(i);
-            // -1 is a special value that means expedited
-            final int delay = op.expedited ? -1 : 0;
             SyncOperation syncOperation = new SyncOperation(
-                    op.account, op.syncSource, op.authority, op.extras, delay);
+                    op.account, op.syncSource, op.authority, op.extras, 0 /* delay */);
+            syncOperation.expedited = op.expedited;
             syncOperation.pendingOperation = op;
             add(syncOperation, op);
         }
@@ -90,8 +87,15 @@ public class SyncQueue {
         return true;
     }
 
+    /**
+     * Remove the specified operation if it is in the queue.
+     * @param operation the operation to remove
+     */
     public void remove(SyncOperation operation) {
         SyncOperation operationToRemove = mOperationsMap.remove(operation.key);
+        if (operationToRemove == null) {
+            return;
+        }
         if (!mSyncStorageEngine.deleteFromPending(operationToRemove.pendingOperation)) {
             final String errorMessage = "unable to find pending row for " + operationToRemove;
             Log.e(TAG, errorMessage, new IllegalStateException(errorMessage));
@@ -102,54 +106,30 @@ public class SyncQueue {
      * Find the operation that should run next. Operations are sorted by their earliestRunTime,
      * prioritizing expedited operations. The earliestRunTime is adjusted by the sync adapter's
      * backoff and delayUntil times, if any.
-     * @param now the current {@link android.os.SystemClock#elapsedRealtime()}
      * @return the operation that should run next and when it should run. The time may be in
      * the future. It is expressed in milliseconds since boot.
      */
-    private Pair<SyncOperation, Long> nextOperation(long now) {
-        SyncOperation lowestOp = null;
-        long lowestOpRunTime = 0;
+    public Pair<SyncOperation, Long> nextOperation() {
+        SyncOperation best = null;
+        long bestRunTime = 0;
         for (SyncOperation op : mOperationsMap.values()) {
-            // effectiveRunTime:
-            //   - backoffTime > currentTime : backoffTime
-            //   - backoffTime <= currentTime : op.runTime
-            Pair<Long, Long> backoff = null;
-            long delayUntilTime = 0;
-            final boolean isManualSync =
-                    op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
-            if (!isManualSync) {
-                backoff = mSyncStorageEngine.getBackoff(op.account, op.authority);
-                delayUntilTime = mSyncStorageEngine.getDelayUntilTime(op.account, op.authority);
-            }
-            long backoffTime = Math.max(backoff != null ? backoff.first : 0, delayUntilTime);
-            long opRunTime = backoffTime > now ? backoffTime : op.earliestRunTime;
-            if (lowestOp == null
-                    || (lowestOp.expedited == op.expedited
-                        ? opRunTime < lowestOpRunTime
-                        : op.expedited)) {
-                lowestOp = op;
-                lowestOpRunTime = opRunTime;
+            long opRunTime = SyncManager.runTimeWithBackoffs(mSyncStorageEngine, op.account,
+                    op.authority,
+                    op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false),
+                    op.earliestRunTime);
+            // if the expedited state of both ops are the same then compare their runtime.
+            // Otherwise the candidate is only better than the current best if the candidate
+            // is expedited.
+            if (best == null
+                    || (best.expedited == op.expedited ? opRunTime < bestRunTime : op.expedited)) {
+                best = op;
+                bestRunTime = opRunTime;
             }
         }
-        if (lowestOp == null) {
-            return null;
-        }
-        return Pair.create(lowestOp, lowestOpRunTime);
-    }
-
-    /**
-     * Return when the next SyncOperation will be ready to run or null if there are
-     * none.
-     * @param now the current {@link android.os.SystemClock#elapsedRealtime()}, used to
-     * decide if the sync operation is ready to run
-     * @return when the next SyncOperation will be ready to run, expressed in elapsedRealtime()
-     */
-    public Long nextRunTime(long now) {
-        Pair<SyncOperation, Long> nextOpAndRunTime = nextOperation(now);
-        if (nextOpAndRunTime == null) {
+        if (best == null) {
             return null;
         }
-        return nextOpAndRunTime.second;
+        return Pair.create(best, bestRunTime);
     }
 
     /**
@@ -158,21 +138,25 @@ public class SyncQueue {
      * decide if the sync operation is ready to run
      * @return the SyncOperation that should be run next and is ready to run.
      */
-    public SyncOperation nextReadyToRun(long now) {
-        Pair<SyncOperation, Long> nextOpAndRunTime = nextOperation(now);
+    public Pair<SyncOperation, Long> nextReadyToRun(long now) {
+        Pair<SyncOperation, Long> nextOpAndRunTime = nextOperation();
         if (nextOpAndRunTime == null || nextOpAndRunTime.second > now) {
             return null;
         }
-        return nextOpAndRunTime.first;
+        return nextOpAndRunTime;
     }
 
-    public void clear(Account account, String authority) {
+    public void remove(Account account, String authority) {
         Iterator<Map.Entry<String, SyncOperation>> entries = mOperationsMap.entrySet().iterator();
         while (entries.hasNext()) {
             Map.Entry<String, SyncOperation> entry = entries.next();
             SyncOperation syncOperation = entry.getValue();
-            if (account != null && !syncOperation.account.equals(account)) continue;
-            if (authority != null && !syncOperation.authority.equals(authority)) continue;
+            if (account != null && !syncOperation.account.equals(account)) {
+                continue;
+            }
+            if (authority != null && !syncOperation.authority.equals(authority)) {
+                continue;
+            }
             entries.remove();
             if (!mSyncStorageEngine.deleteFromPending(syncOperation.pendingOperation)) {
                 final String errorMessage = "unable to find pending row for " + syncOperation;
index b8fda03..bb2b2da 100644 (file)
@@ -20,10 +20,12 @@ import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Log;
 
+import java.util.ArrayList;
+
 /** @hide */
 public class SyncStatusInfo implements Parcelable {
-    static final int VERSION = 1;
-    
+    static final int VERSION = 2;
+
     public final int authorityId;
     public long totalElapsedTime;
     public int numSyncs;
@@ -31,6 +33,7 @@ public class SyncStatusInfo implements Parcelable {
     public int numSourceServer;
     public int numSourceLocal;
     public int numSourceUser;
+    public int numSourcePeriodic;
     public long lastSuccessTime;
     public int lastSuccessSource;
     public long lastFailureTime;
@@ -39,7 +42,10 @@ public class SyncStatusInfo implements Parcelable {
     public long initialFailureTime;
     public boolean pending;
     public boolean initialize;
-    
+    public ArrayList<Long> periodicSyncTimes;
+
+    private static final String TAG = "Sync";
+
     SyncStatusInfo(int authorityId) {
         this.authorityId = authorityId;
     }
@@ -50,10 +56,11 @@ public class SyncStatusInfo implements Parcelable {
                 return Integer.parseInt(lastFailureMesg);
             }
         } catch (NumberFormatException e) {
+            Log.d(TAG, "error parsing lastFailureMesg of " + lastFailureMesg, e);
         }
         return def;
     }
-    
+
     public int describeContents() {
         return 0;
     }
@@ -75,11 +82,19 @@ public class SyncStatusInfo implements Parcelable {
         parcel.writeLong(initialFailureTime);
         parcel.writeInt(pending ? 1 : 0);
         parcel.writeInt(initialize ? 1 : 0);
+        if (periodicSyncTimes != null) {
+            parcel.writeInt(periodicSyncTimes.size());
+            for (long periodicSyncTime : periodicSyncTimes) {
+                parcel.writeLong(periodicSyncTime);
+            }
+        } else {
+            parcel.writeInt(-1);
+        }
     }
 
     SyncStatusInfo(Parcel parcel) {
         int version = parcel.readInt();
-        if (version != VERSION) {
+        if (version != VERSION && version != 1) {
             Log.w("SyncStatusInfo", "Unknown version: " + version);
         }
         authorityId = parcel.readInt();
@@ -97,8 +112,51 @@ public class SyncStatusInfo implements Parcelable {
         initialFailureTime = parcel.readLong();
         pending = parcel.readInt() != 0;
         initialize = parcel.readInt() != 0;
+        if (version == 1) {
+            periodicSyncTimes = null;
+        } else {
+            int N = parcel.readInt();
+            if (N < 0) {
+                periodicSyncTimes = null;
+            } else {
+                periodicSyncTimes = new ArrayList<Long>();
+                for (int i=0; i<N; i++) {
+                    periodicSyncTimes.add(parcel.readLong());
+                }
+            }
+        }
     }
-    
+
+    public void setPeriodicSyncTime(int index, long when) {
+        ensurePeriodicSyncTimeSize(index);
+        periodicSyncTimes.set(index, when);
+    }
+
+    private void ensurePeriodicSyncTimeSize(int index) {
+        if (periodicSyncTimes == null) {
+            periodicSyncTimes = new ArrayList<Long>(0);
+        }
+
+        final int requiredSize = index + 1;
+        if (periodicSyncTimes.size() < requiredSize) {
+            for (int i = periodicSyncTimes.size(); i < requiredSize; i++) {
+                periodicSyncTimes.add((long) 0);
+            }
+        }
+    }
+
+    public long getPeriodicSyncTime(int index) {
+        if (periodicSyncTimes == null || periodicSyncTimes.size() < (index + 1)) {
+            return 0;
+        }
+        return periodicSyncTimes.get(index);
+    }
+
+    public void removePeriodicSyncTime(int index) {
+        ensurePeriodicSyncTimeSize(index);
+        periodicSyncTimes.remove(index);
+    }
+
     public static final Creator<SyncStatusInfo> CREATOR = new Creator<SyncStatusInfo>() {
         public SyncStatusInfo createFromParcel(Parcel in) {
             return new SyncStatusInfo(in);
index db70096..07a1f46 100644 (file)
@@ -36,7 +36,6 @@ import android.os.Message;
 import android.os.Parcel;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
-import android.os.SystemClock;
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.Xml;
@@ -50,6 +49,7 @@ import java.util.Calendar;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.TimeZone;
+import java.util.List;
 
 /**
  * Singleton that tracks the sync data and overall sync
@@ -62,6 +62,8 @@ public class SyncStorageEngine extends Handler {
     private static final boolean DEBUG = false;
     private static final boolean DEBUG_FILE = false;
 
+    private static final long DEFAULT_POLL_FREQUENCY_SECONDS = 60 * 60 * 24; // One day
+
     // @VisibleForTesting
     static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
 
@@ -89,6 +91,9 @@ public class SyncStorageEngine extends Handler {
     /** Enum value for a user-initiated sync. */
     public static final int SOURCE_USER = 3;
 
+    /** Enum value for a periodic sync. */
+    public static final int SOURCE_PERIODIC = 4;
+
     public static final long NOT_IN_BACKOFF_MODE = -1;
 
     private static final Intent SYNC_CONNECTION_SETTING_CHANGED_INTENT =
@@ -99,7 +104,8 @@ public class SyncStorageEngine extends Handler {
     public static final String[] SOURCES = { "SERVER",
                                              "LOCAL",
                                              "POLL",
-                                             "USER" };
+                                             "USER",
+                                             "PERIODIC" };
 
     // The MESG column will contain one of these or one of the Error types.
     public static final String MESG_SUCCESS = "success";
@@ -164,6 +170,7 @@ public class SyncStorageEngine extends Handler {
         long backoffTime;
         long backoffDelay;
         long delayUntil;
+        final ArrayList<Pair<Bundle, Long>> periodicSyncs;
 
         AuthorityInfo(Account account, String authority, int ident) {
             this.account = account;
@@ -173,6 +180,8 @@ public class SyncStorageEngine extends Handler {
             syncable = -1; // default to "unknown"
             backoffTime = -1; // if < 0 then we aren't in backoff mode
             backoffDelay = -1; // if < 0 then we aren't in backoff mode
+            periodicSyncs = new ArrayList<Pair<Bundle, Long>>();
+            periodicSyncs.add(Pair.create(new Bundle(), DEFAULT_POLL_FREQUENCY_SECONDS));
         }
     }
 
@@ -228,6 +237,7 @@ public class SyncStorageEngine extends Handler {
     private int mYearInDays;
 
     private final Context mContext;
+
     private static volatile SyncStorageEngine sSyncStorageEngine = null;
 
     /**
@@ -262,17 +272,15 @@ public class SyncStorageEngine extends Handler {
     private int mNextHistoryId = 0;
     private boolean mMasterSyncAutomatically = true;
 
-    private SyncStorageEngine(Context context) {
+    private SyncStorageEngine(Context context, File dataDir) {
         mContext = context;
         sSyncStorageEngine = this;
 
         mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
 
-        // This call will return the correct directory whether Encrypted File Systems is
-        // enabled or not.
-        File dataDir = Environment.getSecureDataDirectory();
         File systemDir = new File(dataDir, "system");
         File syncDir = new File(systemDir, "sync");
+        syncDir.mkdirs();
         mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"));
         mStatusFile = new AtomicFile(new File(syncDir, "status.bin"));
         mPendingFile = new AtomicFile(new File(syncDir, "pending.bin"));
@@ -286,14 +294,17 @@ public class SyncStorageEngine extends Handler {
     }
 
     public static SyncStorageEngine newTestInstance(Context context) {
-        return new SyncStorageEngine(context);
+        return new SyncStorageEngine(context, context.getFilesDir());
     }
 
     public static void init(Context context) {
         if (sSyncStorageEngine != null) {
             return;
         }
-        sSyncStorageEngine = new SyncStorageEngine(context);
+        // This call will return the correct directory whether Encrypted File Systems is
+        // enabled or not.
+        File dataDir = Environment.getSecureDataDirectory();
+        sSyncStorageEngine = new SyncStorageEngine(context, dataDir);
     }
 
     public static SyncStorageEngine getSingleton() {
@@ -475,7 +486,7 @@ public class SyncStorageEngine extends Handler {
                 }
             } else {
                 AuthorityInfo authority =
-                        getOrCreateAuthorityLocked(account, providerName, -1, false);
+                        getOrCreateAuthorityLocked(account, providerName, -1 /* ident */, true);
                 if (authority.backoffTime == nextSyncTime && authority.backoffDelay == nextDelay) {
                     return;
                 }
@@ -483,9 +494,6 @@ public class SyncStorageEngine extends Handler {
                 authority.backoffDelay = nextDelay;
                 changed = true;
             }
-            if (changed) {
-                writeAccountInfoLocked();
-            }
         }
 
         if (changed) {
@@ -499,12 +507,12 @@ public class SyncStorageEngine extends Handler {
                     + " -> delayUntil " + delayUntil);
         }
         synchronized (mAuthorities) {
-            AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false);
+            AuthorityInfo authority = getOrCreateAuthorityLocked(
+                    account, providerName, -1 /* ident */, true);
             if (authority.delayUntil == delayUntil) {
                 return;
             }
             authority.delayUntil = delayUntil;
-            writeAccountInfoLocked();
         }
 
         reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
@@ -520,6 +528,90 @@ public class SyncStorageEngine extends Handler {
         }
     }
 
+    private void updateOrRemovePeriodicSync(Account account, String providerName, Bundle extras,
+            long period, boolean add) {
+        if (period <= 0) {
+            period = 0;
+        }
+        if (extras == null) {
+            extras = new Bundle();
+        }
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "addOrRemovePeriodicSync: " + account + ", provider " + providerName
+                    + " -> period " + period + ", extras " + extras);
+        }
+        synchronized (mAuthorities) {
+            AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false);
+            if (add) {
+                boolean alreadyPresent = false;
+                for (int i = 0, N = authority.periodicSyncs.size(); i < N; i++) {
+                    Pair<Bundle, Long> syncInfo = authority.periodicSyncs.get(i);
+                    final Bundle existingExtras = syncInfo.first;
+                    if (equals(existingExtras, extras)) {
+                        if (syncInfo.second == period) {
+                            return;
+                        }
+                        authority.periodicSyncs.set(i, Pair.create(extras, period));
+                        alreadyPresent = true;
+                        break;
+                    }
+                }
+                if (!alreadyPresent) {
+                    authority.periodicSyncs.add(Pair.create(extras, period));
+                    SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
+                    status.setPeriodicSyncTime(authority.periodicSyncs.size() - 1, 0);
+                }
+            } else {
+                SyncStatusInfo status = mSyncStatus.get(authority.ident);
+                boolean changed = false;
+                Iterator<Pair<Bundle, Long>> iterator = authority.periodicSyncs.iterator();
+                int i = 0;
+                while (iterator.hasNext()) {
+                    Pair<Bundle, Long> syncInfo = iterator.next();
+                    if (equals(syncInfo.first, extras)) {
+                        iterator.remove();
+                        changed = true;
+                        if (status != null) {
+                            status.removePeriodicSyncTime(i);
+                        }
+                    } else {
+                        i++;
+                    }
+                }
+                if (!changed) {
+                    return;
+                }
+            }
+            writeAccountInfoLocked();
+            writeStatusLocked();
+        }
+
+        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+    }
+
+    public void addPeriodicSync(Account account, String providerName, Bundle extras,
+            long pollFrequency) {
+        updateOrRemovePeriodicSync(account, providerName, extras, pollFrequency, true /* add */);
+    }
+
+    public void removePeriodicSync(Account account, String providerName, Bundle extras) {
+        updateOrRemovePeriodicSync(account, providerName, extras, 0 /* period, ignored */,
+                false /* remove */);
+    }
+
+    public List<PeriodicSync> getPeriodicSyncs(Account account, String providerName) {
+        ArrayList<PeriodicSync> syncs = new ArrayList<PeriodicSync>();
+        synchronized (mAuthorities) {
+            AuthorityInfo authority = getAuthorityLocked(account, providerName, "getPeriodicSyncs");
+            if (authority != null) {
+                for (Pair<Bundle, Long> item : authority.periodicSyncs) {
+                    syncs.add(new PeriodicSync(account, providerName, item.first, item.second));
+                }
+            }
+        }
+        return syncs;
+    }
+
     public void setMasterSyncAutomatically(boolean flag) {
         boolean old;
         synchronized (mAuthorities) {
@@ -817,7 +909,25 @@ public class SyncStorageEngine extends Handler {
         return id;
     }
 
-    public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage,
+    public static boolean equals(Bundle b1, Bundle b2) {
+        if (b1.size() != b2.size()) {
+            return false;
+        }
+        if (b1.isEmpty()) {
+            return true;
+        }
+        for (String key : b1.keySet()) {
+            if (!b2.containsKey(key)) {
+                return false;
+            }
+            if (!b1.get(key).equals(b2.get(key))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public void stopSyncEvent(long historyId, Bundle extras, long elapsedTime, String resultMessage,
             long downstreamActivity, long upstreamActivity) {
         synchronized (mAuthorities) {
             if (DEBUG) Log.v(TAG, "stopSyncEvent: historyId=" + historyId);
@@ -860,6 +970,17 @@ public class SyncStorageEngine extends Handler {
                 case SOURCE_SERVER:
                     status.numSourceServer++;
                     break;
+                case SOURCE_PERIODIC:
+                    status.numSourcePeriodic++;
+                    AuthorityInfo authority = mAuthorities.get(item.authorityId);
+                    for (int periodicSyncIndex = 0;
+                            periodicSyncIndex < authority.periodicSyncs.size();
+                            periodicSyncIndex++) {
+                        if (equals(extras, authority.periodicSyncs.get(periodicSyncIndex).first)) {
+                            status.setPeriodicSyncTime(periodicSyncIndex, item.eventTime);
+                        }
+                    }
+                    break;
             }
 
             boolean writeStatisticsNow = false;
@@ -948,11 +1069,27 @@ public class SyncStorageEngine extends Handler {
     }
 
     /**
+     * Return an array of the current authorities. Note
+     * that the objects inside the array are the real, live objects,
+     * so be careful what you do with them.
+     */
+    public ArrayList<AuthorityInfo> getAuthorities() {
+        synchronized (mAuthorities) {
+            final int N = mAuthorities.size();
+            ArrayList<AuthorityInfo> infos = new ArrayList<AuthorityInfo>(N);
+            for (int i=0; i<N; i++) {
+                infos.add(mAuthorities.valueAt(i));
+            }
+            return infos;
+        }
+    }
+
+    /**
      * Returns the status that matches the authority and account.
      *
      * @param account the account we want to check
      * @param authority the authority whose row should be selected
-     * @return the SyncStatusInfo for the authority, or null if none exists
+     * @return the SyncStatusInfo for the authority
      */
     public SyncStatusInfo getStatusByAccountAndAuthority(Account account, String authority) {
         if (account == null || authority == null) {
@@ -1130,6 +1267,12 @@ public class SyncStorageEngine extends Handler {
         return authority;
     }
 
+    public SyncStatusInfo getOrCreateSyncStatus(AuthorityInfo authority) {
+        synchronized (mAuthorities) {
+            return getOrCreateSyncStatusLocked(authority.ident);
+        }
+    }
+
     private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) {
         SyncStatusInfo status = mSyncStatus.get(authorityId);
         if (status == null) {
@@ -1155,6 +1298,25 @@ public class SyncStorageEngine extends Handler {
     }
 
     /**
+     * public for testing
+     */
+    public void clearAndReadState() {
+        synchronized (mAuthorities) {
+            mAuthorities.clear();
+            mAccounts.clear();
+            mPendingOperations.clear();
+            mSyncStatus.clear();
+            mSyncHistory.clear();
+
+            readAccountInfoLocked();
+            readStatusLocked();
+            readPendingOperationsLocked();
+            readStatisticsLocked();
+            readLegacyAccountInfoLocked();
+        }
+    }
+
+    /**
      * Read all account information back in to the initial engine state.
      */
     private void readAccountInfoLocked() {
@@ -1175,59 +1337,23 @@ public class SyncStorageEngine extends Handler {
                 mMasterSyncAutomatically = listen == null
                             || Boolean.parseBoolean(listen);
                 eventType = parser.next();
+                AuthorityInfo authority = null;
+                Pair<Bundle, Long> periodicSync = null;
                 do {
-                    if (eventType == XmlPullParser.START_TAG
-                            && parser.getDepth() == 2) {
+                    if (eventType == XmlPullParser.START_TAG) {
                         tagName = parser.getName();
-                        if ("authority".equals(tagName)) {
-                            int id = -1;
-                            try {
-                                id = Integer.parseInt(parser.getAttributeValue(
-                                        null, "id"));
-                            } catch (NumberFormatException e) {
-                            } catch (NullPointerException e) {
+                        if (parser.getDepth() == 2) {
+                            if ("authority".equals(tagName)) {
+                                authority = parseAuthority(parser);
+                                periodicSync = null;
+                            }
+                        } else if (parser.getDepth() == 3) {
+                            if ("periodicSync".equals(tagName) && authority != null) {
+                                periodicSync = parsePeriodicSync(parser, authority);
                             }
-                            if (id >= 0) {
-                                String accountName = parser.getAttributeValue(
-                                        null, "account");
-                                String accountType = parser.getAttributeValue(
-                                        null, "type");
-                                if (accountType == null) {
-                                    accountType = "com.google";
-                                }
-                                String authorityName = parser.getAttributeValue(
-                                        null, "authority");
-                                String enabled = parser.getAttributeValue(
-                                        null, "enabled");
-                                String syncable = parser.getAttributeValue(null, "syncable");
-                                AuthorityInfo authority = mAuthorities.get(id);
-                                if (DEBUG_FILE) Log.v(TAG, "Adding authority: account="
-                                        + accountName + " auth=" + authorityName
-                                        + " enabled=" + enabled
-                                        + " syncable=" + syncable);
-                                if (authority == null) {
-                                    if (DEBUG_FILE) Log.v(TAG, "Creating entry");
-                                    authority = getOrCreateAuthorityLocked(
-                                            new Account(accountName, accountType),
-                                            authorityName, id, false);
-                                }
-                                if (authority != null) {
-                                    authority.enabled = enabled == null
-                                            || Boolean.parseBoolean(enabled);
-                                    if ("unknown".equals(syncable)) {
-                                        authority.syncable = -1;
-                                    } else {
-                                        authority.syncable =
-                                                (syncable == null || Boolean.parseBoolean(enabled))
-                                                        ? 1
-                                                        : 0;
-                                    }
-                                } else {
-                                    Log.w(TAG, "Failure adding authority: account="
-                                            + accountName + " auth=" + authorityName
-                                            + " enabled=" + enabled
-                                            + " syncable=" + syncable);
-                                }
+                        } else if (parser.getDepth() == 4 && periodicSync != null) {
+                            if ("extra".equals(tagName)) {
+                                parseExtra(parser, periodicSync);
                             }
                         }
                     }
@@ -1249,6 +1375,105 @@ public class SyncStorageEngine extends Handler {
         }
     }
 
+    private AuthorityInfo parseAuthority(XmlPullParser parser) {
+        AuthorityInfo authority = null;
+        int id = -1;
+        try {
+            id = Integer.parseInt(parser.getAttributeValue(
+                    null, "id"));
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "error parsing the id of the authority", e);
+        } catch (NullPointerException e) {
+            Log.e(TAG, "the id of the authority is null", e);
+        }
+        if (id >= 0) {
+            String accountName = parser.getAttributeValue(null, "account");
+            String accountType = parser.getAttributeValue(null, "type");
+            if (accountType == null) {
+                accountType = "com.google";
+            }
+            String authorityName = parser.getAttributeValue(null, "authority");
+            String enabled = parser.getAttributeValue(null, "enabled");
+            String syncable = parser.getAttributeValue(null, "syncable");
+            authority = mAuthorities.get(id);
+            if (DEBUG_FILE) Log.v(TAG, "Adding authority: account="
+                    + accountName + " auth=" + authorityName
+                    + " enabled=" + enabled
+                    + " syncable=" + syncable);
+            if (authority == null) {
+                if (DEBUG_FILE) Log.v(TAG, "Creating entry");
+                authority = getOrCreateAuthorityLocked(
+                        new Account(accountName, accountType), authorityName, id, false);
+                // clear this since we will read these later on
+                authority.periodicSyncs.clear();
+            }
+            if (authority != null) {
+                authority.enabled = enabled == null || Boolean.parseBoolean(enabled);
+                if ("unknown".equals(syncable)) {
+                    authority.syncable = -1;
+                } else {
+                    authority.syncable =
+                            (syncable == null || Boolean.parseBoolean(enabled)) ? 1 : 0;
+                }
+            } else {
+                Log.w(TAG, "Failure adding authority: account="
+                        + accountName + " auth=" + authorityName
+                        + " enabled=" + enabled
+                        + " syncable=" + syncable);
+            }
+        }
+
+        return authority;
+    }
+
+    private Pair<Bundle, Long> parsePeriodicSync(XmlPullParser parser, AuthorityInfo authority) {
+        Bundle extras = new Bundle();
+        String periodValue = parser.getAttributeValue(null, "period");
+        final long period;
+        try {
+            period = Long.parseLong(periodValue);
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "error parsing the period of a periodic sync", e);
+            return null;
+        } catch (NullPointerException e) {
+            Log.e(TAG, "the period of a periodic sync is null", e);
+            return null;
+        }
+        final Pair<Bundle, Long> periodicSync = Pair.create(extras, period);
+        authority.periodicSyncs.add(periodicSync);
+        return periodicSync;
+    }
+
+    private void parseExtra(XmlPullParser parser, Pair<Bundle, Long> periodicSync) {
+        final Bundle extras = periodicSync.first;
+        String name = parser.getAttributeValue(null, "name");
+        String type = parser.getAttributeValue(null, "type");
+        String value1 = parser.getAttributeValue(null, "value1");
+        String value2 = parser.getAttributeValue(null, "value2");
+
+        try {
+            if ("long".equals(type)) {
+                extras.putLong(name, Long.parseLong(value1));
+            } else if ("integer".equals(type)) {
+                extras.putInt(name, Integer.parseInt(value1));
+            } else if ("double".equals(type)) {
+                extras.putDouble(name, Double.parseDouble(value1));
+            } else if ("float".equals(type)) {
+                extras.putFloat(name, Float.parseFloat(value1));
+            } else if ("boolean".equals(type)) {
+                extras.putBoolean(name, Boolean.parseBoolean(value1));
+            } else if ("string".equals(type)) {
+                extras.putString(name, value1);
+            } else if ("account".equals(type)) {
+                extras.putParcelable(name, new Account(value1, value2));
+            }
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "error parsing bundle value", e);
+        } catch (NullPointerException e) {
+            Log.e(TAG, "error parsing bundle value", e);
+        }
+    }
+
     /**
      * Write all account information to the account file.
      */
@@ -1284,6 +1509,41 @@ public class SyncStorageEngine extends Handler {
                 } else if (authority.syncable == 0) {
                     out.attribute(null, "syncable", "false");
                 }
+                for (Pair<Bundle, Long> periodicSync : authority.periodicSyncs) {
+                    out.startTag(null, "periodicSync");
+                    out.attribute(null, "period", Long.toString(periodicSync.second));
+                    final Bundle extras = periodicSync.first;
+                    for (String key : extras.keySet()) {
+                        out.startTag(null, "extra");
+                        out.attribute(null, "name", key);
+                        final Object value = extras.get(key);
+                        if (value instanceof Long) {
+                            out.attribute(null, "type", "long");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof Integer) {
+                            out.attribute(null, "type", "integer");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof Boolean) {
+                            out.attribute(null, "type", "boolean");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof Float) {
+                            out.attribute(null, "type", "float");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof Double) {
+                            out.attribute(null, "type", "double");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof String) {
+                            out.attribute(null, "type", "string");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof Account) {
+                            out.attribute(null, "type", "account");
+                            out.attribute(null, "value1", ((Account)value).name);
+                            out.attribute(null, "value2", ((Account)value).type);
+                        }
+                        out.endTag(null, "extra");
+                    }
+                    out.endTag(null, "periodicSync");
+                }
                 out.endTag(null, "authority");
             }
 
@@ -1389,6 +1649,7 @@ public class SyncStorageEngine extends Handler {
                     st.numSourcePoll = getIntColumn(c, "numSourcePoll");
                     st.numSourceServer = getIntColumn(c, "numSourceServer");
                     st.numSourceUser = getIntColumn(c, "numSourceUser");
+                    st.numSourcePeriodic = 0;
                     st.lastSuccessSource = getIntColumn(c, "lastSuccessSource");
                     st.lastSuccessTime = getLongColumn(c, "lastSuccessTime");
                     st.lastFailureSource = getIntColumn(c, "lastFailureSource");
index 533338e..1505a7c 100644 (file)
@@ -18,16 +18,23 @@ package android.content;
 
 import android.test.AndroidTestCase;
 import android.test.RenamingDelegatingContext;
+import android.test.suitebuilder.annotation.SmallTest;
 import android.test.mock.MockContext;
 import android.test.mock.MockContentResolver;
 import android.accounts.Account;
+import android.os.Bundle;
+
+import java.util.List;
+import java.io.File;
 
 public class SyncStorageEngineTest extends AndroidTestCase {
 
     /**
      * Test that we handle the case of a history row being old enough to purge before the
      * correcponding sync is finished. This can happen if the clock changes while we are syncing.
+     *
      */
+    @SmallTest
     public void testPurgeActiveSync() throws Exception {
         final Account account = new Account("a@example.com", "example.type");
         final String authority = "testprovider";
@@ -41,7 +48,150 @@ public class SyncStorageEngineTest extends AndroidTestCase {
         long historyId = engine.insertStartSyncEvent(
                 account, authority, time0, SyncStorageEngine.SOURCE_LOCAL);
         long time1 = time0 + SyncStorageEngine.MILLIS_IN_4WEEKS * 2;
-        engine.stopSyncEvent(historyId, time1 - time0, "yay", 0, 0);
+        engine.stopSyncEvent(historyId, new Bundle(), time1 - time0, "yay", 0, 0);
+    }
+
+    /**
+     * Test that we can create, remove and retrieve periodic syncs
+     */
+    @SmallTest
+    public void testPeriodics() throws Exception {
+        final Account account1 = new Account("a@example.com", "example.type");
+        final Account account2 = new Account("b@example.com", "example.type.2");
+        final String authority = "testprovider";
+        final Bundle extras1 = new Bundle();
+        extras1.putString("a", "1");
+        final Bundle extras2 = new Bundle();
+        extras2.putString("a", "2");
+        final int period1 = 200;
+        final int period2 = 1000;
+
+        PeriodicSync sync1 = new PeriodicSync(account1, authority, extras1, period1);
+        PeriodicSync sync2 = new PeriodicSync(account1, authority, extras2, period1);
+        PeriodicSync sync3 = new PeriodicSync(account1, authority, extras2, period2);
+        PeriodicSync sync4 = new PeriodicSync(account2, authority, extras2, period2);
+
+        MockContentResolver mockResolver = new MockContentResolver();
+
+        SyncStorageEngine engine = SyncStorageEngine.newTestInstance(
+                new TestContext(mockResolver, getContext()));
+
+        removePeriodicSyncs(engine, account1, authority);
+        removePeriodicSyncs(engine, account2, authority);
+
+        // this should add two distinct periodic syncs for account1 and one for account2
+        engine.addPeriodicSync(sync1.account, sync1.authority, sync1.extras, sync1.period);
+        engine.addPeriodicSync(sync2.account, sync2.authority, sync2.extras, sync2.period);
+        engine.addPeriodicSync(sync3.account, sync3.authority, sync3.extras, sync3.period);
+        engine.addPeriodicSync(sync4.account, sync4.authority, sync4.extras, sync4.period);
+
+        List<PeriodicSync> syncs = engine.getPeriodicSyncs(account1, authority);
+
+        assertEquals(2, syncs.size());
+
+        assertEquals(sync1, syncs.get(0));
+        assertEquals(sync3, syncs.get(1));
+
+        engine.removePeriodicSync(sync1.account, sync1.authority, sync1.extras);
+
+        syncs = engine.getPeriodicSyncs(account1, authority);
+        assertEquals(1, syncs.size());
+        assertEquals(sync3, syncs.get(0));
+
+        syncs = engine.getPeriodicSyncs(account2, authority);
+        assertEquals(1, syncs.size());
+        assertEquals(sync4, syncs.get(0));
+    }
+
+    private void removePeriodicSyncs(SyncStorageEngine engine, Account account, String authority) {
+        engine.setIsSyncable(account, authority, engine.getIsSyncable(account, authority));
+        List<PeriodicSync> syncs = engine.getPeriodicSyncs(account, authority);
+        for (PeriodicSync sync : syncs) {
+            engine.removePeriodicSync(sync.account, sync.authority, sync.extras);
+        }
+    }
+
+    @SmallTest
+    public void testAuthorityPersistence() throws Exception {
+        final Account account1 = new Account("a@example.com", "example.type");
+        final Account account2 = new Account("b@example.com", "example.type.2");
+        final String authority1 = "testprovider1";
+        final String authority2 = "testprovider2";
+        final Bundle extras1 = new Bundle();
+        extras1.putString("a", "1");
+        final Bundle extras2 = new Bundle();
+        extras2.putString("a", "2");
+        extras2.putLong("b", 2);
+        extras2.putInt("c", 1);
+        extras2.putBoolean("d", true);
+        extras2.putDouble("e", 1.2);
+        extras2.putFloat("f", 4.5f);
+        extras2.putParcelable("g", account1);
+        final int period1 = 200;
+        final int period2 = 1000;
+
+        PeriodicSync sync1 = new PeriodicSync(account1, authority1, extras1, period1);
+        PeriodicSync sync2 = new PeriodicSync(account1, authority1, extras2, period1);
+        PeriodicSync sync3 = new PeriodicSync(account1, authority2, extras1, period1);
+        PeriodicSync sync4 = new PeriodicSync(account1, authority2, extras2, period2);
+        PeriodicSync sync5 = new PeriodicSync(account2, authority1, extras1, period1);
+
+        MockContentResolver mockResolver = new MockContentResolver();
+
+        SyncStorageEngine engine = SyncStorageEngine.newTestInstance(
+                new TestContext(mockResolver, getContext()));
+
+        removePeriodicSyncs(engine, account1, authority1);
+        removePeriodicSyncs(engine, account2, authority1);
+        removePeriodicSyncs(engine, account1, authority2);
+        removePeriodicSyncs(engine, account2, authority2);
+
+        engine.setMasterSyncAutomatically(false);
+
+        engine.setIsSyncable(account1, authority1, 1);
+        engine.setSyncAutomatically(account1, authority1, true);
+
+        engine.setIsSyncable(account2, authority1, 1);
+        engine.setSyncAutomatically(account2, authority1, true);
+
+        engine.setIsSyncable(account1, authority2, 1);
+        engine.setSyncAutomatically(account1, authority2, false);
+
+        engine.setIsSyncable(account2, authority2, 0);
+        engine.setSyncAutomatically(account2, authority2, true);
+
+        engine.addPeriodicSync(sync1.account, sync1.authority, sync1.extras, sync1.period);
+        engine.addPeriodicSync(sync2.account, sync2.authority, sync2.extras, sync2.period);
+        engine.addPeriodicSync(sync3.account, sync3.authority, sync3.extras, sync3.period);
+        engine.addPeriodicSync(sync4.account, sync4.authority, sync4.extras, sync4.period);
+        engine.addPeriodicSync(sync5.account, sync5.authority, sync5.extras, sync5.period);
+
+        engine.writeAllState();
+        engine.clearAndReadState();
+
+        List<PeriodicSync> syncs = engine.getPeriodicSyncs(account1, authority1);
+        assertEquals(2, syncs.size());
+        assertEquals(sync1, syncs.get(0));
+        assertEquals(sync2, syncs.get(1));
+
+        syncs = engine.getPeriodicSyncs(account1, authority2);
+        assertEquals(2, syncs.size());
+        assertEquals(sync3, syncs.get(0));
+        assertEquals(sync4, syncs.get(1));
+
+        syncs = engine.getPeriodicSyncs(account2, authority1);
+        assertEquals(1, syncs.size());
+        assertEquals(sync5, syncs.get(0));
+
+        assertEquals(true, engine.getSyncAutomatically(account1, authority1));
+        assertEquals(true, engine.getSyncAutomatically(account2, authority1));
+        assertEquals(false, engine.getSyncAutomatically(account1, authority2));
+        assertEquals(true, engine.getSyncAutomatically(account2, authority2));
+
+        assertEquals(1, engine.getIsSyncable(account1, authority1));
+        assertEquals(1, engine.getIsSyncable(account2, authority1));
+        assertEquals(1, engine.getIsSyncable(account1, authority2));
+        assertEquals(0, engine.getIsSyncable(account2, authority2));
     }
 }
 
@@ -49,15 +199,26 @@ class TestContext extends ContextWrapper {
 
     ContentResolver mResolver;
 
+    private final Context mRealContext;
+
     public TestContext(ContentResolver resolver, Context realContext) {
         super(new RenamingDelegatingContext(new MockContext(), realContext, "test."));
+        mRealContext = realContext;
         mResolver = resolver;
     }
 
     @Override
+    public File getFilesDir() {
+        return mRealContext.getFilesDir();
+    }
+
+    @Override
     public void enforceCallingOrSelfPermission(String permission, String message) {
     }
 
+    @Override
+    public void sendBroadcast(Intent intent) {
+    }
 
     @Override
     public ContentResolver getContentResolver() {
index 5f4ab78..1da59d1 100644 (file)
@@ -66,22 +66,22 @@ public class SyncQueueTest extends AndroidTestCase {
 
         long now = SystemClock.elapsedRealtime() + 200;
 
-        assertEquals(op6, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op6, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op6);
 
-        assertEquals(op1, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op1, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op1);
 
-        assertEquals(op4, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op4, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op4);
 
-        assertEquals(op5, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op5, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op5);
 
-        assertEquals(op2, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op2, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op2);
 
-        assertEquals(op3, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op3, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op3);
     }
 
@@ -109,32 +109,32 @@ public class SyncQueueTest extends AndroidTestCase {
 
         long now = SystemClock.elapsedRealtime() + 200;
 
-        assertEquals(op6, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op6, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op6);
 
-        assertEquals(op1, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op1, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op1);
 
         mSettings.setBackoff(ACCOUNT2,  AUTHORITY3, now + 200, 5);
-        assertEquals(op5, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op5, mSyncQueue.nextReadyToRun(now).first);
 
         mSettings.setBackoff(ACCOUNT2,  AUTHORITY3, SyncStorageEngine.NOT_IN_BACKOFF_MODE, 0);
-        assertEquals(op4, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op4, mSyncQueue.nextReadyToRun(now).first);
 
         mSettings.setDelayUntilTime(ACCOUNT2,  AUTHORITY3, now + 200);
-        assertEquals(op5, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op5, mSyncQueue.nextReadyToRun(now).first);
 
         mSettings.setDelayUntilTime(ACCOUNT2,  AUTHORITY3, 0);
-        assertEquals(op4, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op4, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op4);
 
-        assertEquals(op5, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op5, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op5);
 
-        assertEquals(op2, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op2, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op2);
 
-        assertEquals(op3, mSyncQueue.nextReadyToRun(now));
+        assertEquals(op3, mSyncQueue.nextReadyToRun(now).first);
         mSyncQueue.remove(op3);
     }