From c5d1c6db61f208b206b260f897bb5bbc64be4d97 Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Wed, 27 Jan 2010 12:17:49 -0800 Subject: [PATCH] add sync polling - 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 --- Android.mk | 1 + api/current.xml | 158 ++++++- core/java/android/accounts/AccountManager.java | 14 +- core/java/android/content/ContentResolver.java | 60 +++ core/java/android/content/ContentService.java | 38 ++ core/java/android/content/IContentService.aidl | 42 +- core/java/android/content/PeriodicSync.aidl | 19 + core/java/android/content/PeriodicSync.java | 84 ++++ core/java/android/content/SyncManager.java | 453 +++++++++------------ core/java/android/content/SyncQueue.java | 86 ++-- core/java/android/content/SyncStatusInfo.java | 70 +++- core/java/android/content/SyncStorageEngine.java | 393 +++++++++++++++--- .../src/android/content/SyncStorageEngineTest.java | 163 +++++++- tests/CoreTests/android/content/SyncQueueTest.java | 30 +- 14 files changed, 1205 insertions(+), 406 deletions(-) create mode 100644 core/java/android/content/PeriodicSync.aidl create mode 100644 core/java/android/content/PeriodicSync.java diff --git a/Android.mk b/Android.mk index 682f2865d1ad..ab1e7ea0ea12 100644 --- a/Android.mk +++ b/Android.mk @@ -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 \ diff --git a/api/current.xml b/api/current.xml index b3f9190c11b9..691bde49a74a 100644 --- a/api/current.xml +++ b/api/current.xml @@ -31107,6 +31107,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java index 414d9633b11e..19e741ab55ff 100644 --- a/core/java/android/accounts/AccountManager.java +++ b/core/java/android/accounts/AccountManager.java @@ -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. *

* 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. *

- * 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. *

* 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); } } diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index eb2d7b143dc7..b5587eda7fd6 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -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. + *

+ * 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 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. */ diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java index 974a6670aab4..e0dfab59ee22 100644 --- a/core/java/android/content/ContentService.java +++ b/core/java/android/content/ContentService.java @@ -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 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"); diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl index b0f14c15cbb4..2d906ed0d725 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -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 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 index 000000000000..4530591bb551 --- /dev/null +++ b/core/java/android/content/PeriodicSync.aidl @@ -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 index 000000000000..17813ec3aaf0 --- /dev/null +++ b/core/java/android/content/PeriodicSync.java @@ -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 CREATOR = new Creator() { + 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); + } +} diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index 699b61d2b5d4..619c7d5e2067 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -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 : ""); pw.print(" "); pw.print(authority != null ? authority.authority : ""); + 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 processedAccounts = new HashSet(); - ArrayList 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 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 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 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 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 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 bestSyncOperationCandidate() { + Pair 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 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 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 backoff = syncStorageEngine.getBackoff(account, authority); + long delayUntilTime = syncStorageEngine.getDelayUntilTime(account, authority); + + return Math.max(Math.max(runTime, delayUntilTime), backoff != null ? backoff.first : 0); + } } diff --git a/core/java/android/content/SyncQueue.java b/core/java/android/content/SyncQueue.java index a9f15d9f9c26..2eead3abdbf5 100644 --- a/core/java/android/content/SyncQueue.java +++ b/core/java/android/content/SyncQueue.java @@ -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 nextOperation(long now) { - SyncOperation lowestOp = null; - long lowestOpRunTime = 0; + public Pair nextOperation() { + SyncOperation best = null; + long bestRunTime = 0; for (SyncOperation op : mOperationsMap.values()) { - // effectiveRunTime: - // - backoffTime > currentTime : backoffTime - // - backoffTime <= currentTime : op.runTime - Pair 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 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 nextOpAndRunTime = nextOperation(now); + public Pair nextReadyToRun(long now) { + Pair 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> entries = mOperationsMap.entrySet().iterator(); while (entries.hasNext()) { Map.Entry 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; diff --git a/core/java/android/content/SyncStatusInfo.java b/core/java/android/content/SyncStatusInfo.java index b8fda030b11c..bb2b2dace8bf 100644 --- a/core/java/android/content/SyncStatusInfo.java +++ b/core/java/android/content/SyncStatusInfo.java @@ -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 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(); + for (int i=0; i(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 CREATOR = new Creator() { public SyncStatusInfo createFromParcel(Parcel in) { return new SyncStatusInfo(in); diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java index db70096f535c..07a1f46aa91b 100644 --- a/core/java/android/content/SyncStorageEngine.java +++ b/core/java/android/content/SyncStorageEngine.java @@ -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> 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>(); + 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 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> iterator = authority.periodicSyncs.iterator(); + int i = 0; + while (iterator.hasNext()) { + Pair 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 getPeriodicSyncs(Account account, String providerName) { + ArrayList syncs = new ArrayList(); + synchronized (mAuthorities) { + AuthorityInfo authority = getAuthorityLocked(account, providerName, "getPeriodicSyncs"); + if (authority != null) { + for (Pair 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 getAuthorities() { + synchronized (mAuthorities) { + final int N = mAuthorities.size(); + ArrayList infos = new ArrayList(N); + for (int i=0; i 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 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 periodicSync = Pair.create(extras, period); + authority.periodicSyncs.add(periodicSync); + return periodicSync; + } + + private void parseExtra(XmlPullParser parser, Pair 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 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"); diff --git a/core/tests/coretests/src/android/content/SyncStorageEngineTest.java b/core/tests/coretests/src/android/content/SyncStorageEngineTest.java index 533338e027e2..1505a7c0ff3a 100644 --- a/core/tests/coretests/src/android/content/SyncStorageEngineTest.java +++ b/core/tests/coretests/src/android/content/SyncStorageEngineTest.java @@ -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 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 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 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() { diff --git a/tests/CoreTests/android/content/SyncQueueTest.java b/tests/CoreTests/android/content/SyncQueueTest.java index 5f4ab789bd0a..1da59d18e1a8 100644 --- a/tests/CoreTests/android/content/SyncQueueTest.java +++ b/tests/CoreTests/android/content/SyncQueueTest.java @@ -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); } -- 2.11.0