diff options
18 files changed, 2503 insertions, 406 deletions
@@ -104,6 +104,7 @@ LOCAL_SRC_FILES += \ core/java/android/content/IIntentReceiver.aidl \ core/java/android/content/IIntentSender.aidl \ core/java/android/content/IOnPrimaryClipChangedListener.aidl \ + core/java/android/content/IAnonymousSyncAdapter.aidl \ core/java/android/content/ISyncAdapter.aidl \ core/java/android/content/ISyncContext.aidl \ core/java/android/content/ISyncStatusObserver.aidl \ @@ -339,6 +340,7 @@ aidl_files := \ 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/SyncRequest.aidl \ frameworks/base/core/java/android/content/SyncStats.aidl \ frameworks/base/core/java/android/content/res/Configuration.aidl \ frameworks/base/core/java/android/database/CursorWindow.aidl \ diff --git a/api/current.txt b/api/current.txt index ee0c395..d675ab0 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5572,6 +5572,7 @@ package android.content { method public final android.os.Bundle call(android.net.Uri, java.lang.String, java.lang.String, android.os.Bundle); method public deprecated void cancelSync(android.net.Uri); method public static void cancelSync(android.accounts.Account, java.lang.String); + method public static void cancelSync(android.content.SyncRequest); method public final int delete(android.net.Uri, java.lang.String, java.lang.String[]); method public static deprecated android.content.SyncInfo getCurrentSync(); method public static java.util.List<android.content.SyncInfo> getCurrentSyncs(); @@ -5599,6 +5600,7 @@ package android.content { method public static void removePeriodicSync(android.accounts.Account, java.lang.String, android.os.Bundle); method public static void removeStatusChangeListener(java.lang.Object); method public static void requestSync(android.accounts.Account, java.lang.String, android.os.Bundle); + method public static void requestSync(android.content.SyncRequest); method public static void setIsSyncable(android.accounts.Account, java.lang.String, int); method public static void setMasterSyncAutomatically(boolean); method public static void setSyncAutomatically(android.accounts.Account, java.lang.String, boolean); @@ -6534,7 +6536,9 @@ package android.content { field public final android.accounts.Account account; field public final java.lang.String authority; field public final android.os.Bundle extras; + field public final boolean isService; field public final long period; + field public final android.content.ComponentName service; } public class ReceiverCallNotAllowedException extends android.util.AndroidRuntimeException { @@ -6653,6 +6657,31 @@ package android.content { field public final long startTime; } + public class SyncRequest implements android.os.Parcelable { + method public int describeContents(); + method public boolean isExpedited(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class SyncRequest.Builder { + ctor public SyncRequest.Builder(); + method public android.content.SyncRequest build(); + method public android.content.SyncRequest.Builder setAllowMetered(boolean); + method public android.content.SyncRequest.Builder setExpedited(boolean); + method public android.content.SyncRequest.Builder setExtras(android.os.Bundle); + method public android.content.SyncRequest.Builder setIgnoreBackoff(boolean); + method public android.content.SyncRequest.Builder setIgnoreSettings(boolean); + method public android.content.SyncRequest.Builder setManual(boolean); + method public android.content.SyncRequest.Builder setNoRetry(boolean); + method public android.content.SyncRequest.Builder setPriority(int); + method public android.content.SyncRequest.Builder setSyncAdapter(android.accounts.Account, java.lang.String); + method public android.content.SyncRequest.Builder setSyncAdapter(android.content.ComponentName); + method public android.content.SyncRequest.Builder setTransferSize(long, long); + method public android.content.SyncRequest.Builder syncOnce(long, long); + method public android.content.SyncRequest.Builder syncPeriodic(long, long); + } + public final class SyncResult implements android.os.Parcelable { ctor public SyncResult(); method public void clear(); @@ -6676,6 +6705,12 @@ package android.content { field public boolean tooManyRetries; } + public abstract class SyncService extends android.app.Service { + ctor public SyncService(); + method public android.os.IBinder onBind(android.content.Intent); + method public abstract void onPerformSync(android.os.Bundle, android.content.SyncResult); + } + public class SyncStats implements android.os.Parcelable { ctor public SyncStats(); ctor public SyncStats(android.os.Parcel); diff --git a/core/java/android/content/AbstractThreadedSyncAdapter.java b/core/java/android/content/AbstractThreadedSyncAdapter.java index bafe67d..613450b 100644 --- a/core/java/android/content/AbstractThreadedSyncAdapter.java +++ b/core/java/android/content/AbstractThreadedSyncAdapter.java @@ -147,6 +147,7 @@ public abstract class AbstractThreadedSyncAdapter { } private class ISyncAdapterImpl extends ISyncAdapter.Stub { + @Override public void startSync(ISyncContext syncContext, String authority, Account account, Bundle extras) { final SyncContext syncContextClient = new SyncContext(syncContext); @@ -184,6 +185,7 @@ public abstract class AbstractThreadedSyncAdapter { } } + @Override public void cancelSync(ISyncContext syncContext) { // synchronize to make sure that mSyncThreads doesn't change between when we // check it and when we use it diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index f090e07..243c91a 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -44,6 +44,7 @@ import android.os.UserHandle; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; +import android.util.Pair; import java.io.File; import java.io.FileInputStream; @@ -131,6 +132,19 @@ public abstract class ContentResolver { */ public static final String SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS = "discard_deletions"; + /* Extensions to API. TODO: Not clear if we will keep these as public flags. */ + /** {@hide} User-specified flag for expected upload size. */ + public static final String SYNC_EXTRAS_EXPECTED_UPLOAD = "expected_upload"; + + /** {@hide} User-specified flag for expected download size. */ + public static final String SYNC_EXTRAS_EXPECTED_DOWNLOAD = "expected_download"; + + /** {@hide} Priority of this sync with respect to other syncs scheduled for this application. */ + public static final String SYNC_EXTRAS_PRIORITY = "sync_priority"; + + /** {@hide} Flag to allow sync to occur on metered network. */ + public static final String SYNC_EXTRAS_ALLOW_METERED = "allow_metered"; + /** * Set by the SyncManager to request that the SyncAdapter initialize itself for * the given account/authority pair. One required initialization step is to @@ -1385,6 +1399,8 @@ public abstract class ContentResolver { * <li>Float</li> * <li>Double</li> * <li>String</li> + * <li>Account</li> + * <li>null</li> * </ul> * * @param uri the uri of the provider to sync or null to sync all providers. @@ -1416,6 +1432,8 @@ public abstract class ContentResolver { * <li>Float</li> * <li>Double</li> * <li>String</li> + * <li>Account</li> + * <li>null</li> * </ul> * * @param account which account should be synced @@ -1423,10 +1441,24 @@ public abstract class ContentResolver { * @param extras any extras to pass to the SyncAdapter. */ public static void requestSync(Account account, String authority, Bundle extras) { - validateSyncExtrasBundle(extras); + SyncRequest request = + new SyncRequest.Builder() + .setSyncAdapter(account, authority) + .setExtras(extras) + .syncOnce(0, 0) // Immediate sync. + .build(); + requestSync(request); + } + + /** + * Register a sync with the SyncManager. These requests are built using the + * {@link SyncRequest.Builder}. + */ + public static void requestSync(SyncRequest request) { try { - getContentService().requestSync(account, authority, extras); - } catch (RemoteException e) { + getContentService().sync(request); + } catch(RemoteException e) { + // Shouldn't happen. } } @@ -1586,7 +1618,7 @@ public abstract class ContentResolver { throw new IllegalArgumentException("illegal extras were set"); } try { - getContentService().addPeriodicSync(account, authority, extras, pollFrequency); + 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 @@ -1619,6 +1651,22 @@ public abstract class ContentResolver { } /** + * Remove the specified sync. This will remove any syncs that have been scheduled to run, but + * will not cancel any running syncs. + * <p>This method requires the caller to hold the permission</p> + * If the request is for a periodic sync this will cancel future occurrences of the sync. + * + * It is possible to cancel a sync using a SyncRequest object that is different from the object + * with which you requested the sync. Do so by building a SyncRequest with exactly the same + * service/adapter, frequency, <b>and</b> extras bundle. + * + * @param request SyncRequest object containing information about sync to cancel. + */ + public static void cancelSync(SyncRequest request) { + // TODO: Finish this implementation. + } + + /** * Get the list of information about the periodic syncs for the given account and authority. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#READ_SYNC_SETTINGS}. diff --git a/core/java/android/content/IAnonymousSyncAdapter.aidl b/core/java/android/content/IAnonymousSyncAdapter.aidl new file mode 100644 index 0000000..a80cea3 --- /dev/null +++ b/core/java/android/content/IAnonymousSyncAdapter.aidl @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2013 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.Bundle; +import android.content.ISyncContext; + +/** + * Interface to define an anonymous service that is extended by developers + * in order to perform anonymous syncs (syncs without an Account or Content + * Provider specified). See {@link android.content.AbstractThreadedSyncAdapter}. + * {@hide} + */ +oneway interface IAnonymousSyncAdapter { + + /** + * Initiate a sync. SyncAdapter-specific parameters may be specified in + * extras, which is guaranteed to not be null. + * + * @param syncContext the ISyncContext used to indicate the progress of the sync. When + * the sync is finished (successfully or not) ISyncContext.onFinished() must be called. + * @param extras SyncAdapter-specific parameters. + * + */ + void startSync(ISyncContext syncContext, in Bundle extras); + + /** + * Cancel the currently ongoing sync. + */ + void cancelSync(ISyncContext syncContext); + +} diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl index f956bcf..9ad5a19 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -20,6 +20,7 @@ import android.accounts.Account; import android.content.SyncInfo; import android.content.ISyncStatusObserver; import android.content.SyncAdapterType; +import android.content.SyncRequest; import android.content.SyncStatusInfo; import android.content.PeriodicSync; import android.net.Uri; @@ -54,6 +55,7 @@ interface IContentService { int userHandle); void requestSync(in Account account, String authority, in Bundle extras); + void sync(in SyncRequest request); void cancelSync(in Account account, String authority); /** diff --git a/core/java/android/content/PeriodicSync.java b/core/java/android/content/PeriodicSync.java index 513a556..6aca151 100644 --- a/core/java/android/content/PeriodicSync.java +++ b/core/java/android/content/PeriodicSync.java @@ -22,67 +22,170 @@ 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. + * Value type that contains information about a periodic sync. */ public class PeriodicSync implements Parcelable { - /** The account to be synced */ + /** The account to be synced. Can be null. */ public final Account account; - /** The authority of the sync */ + /** The authority of the sync. Can be null. */ public final String authority; + /** The service for syncing, if this is an anonymous sync. Can be null.*/ + public final ComponentName service; /** 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. */ + /** How frequently the sync should be scheduled, in seconds. Kept around for API purposes. */ public final long period; + /** Whether this periodic sync uses a service. */ + public final boolean isService; + /** + * How much flexibility can be taken in scheduling the sync, in seconds. + * {@hide} + */ + public final long flexTime; - /** Creates a new PeriodicSync, copying the Bundle */ - public PeriodicSync(Account account, String authority, Bundle extras, long period) { + /** + * Creates a new PeriodicSync, copying the Bundle. SM no longer uses this ctor - kept around + * becuse it is part of the API. + * Note - even calls to the old API will not use this ctor, as + * they are given a default flex time. + */ + public PeriodicSync(Account account, String authority, Bundle extras, long periodInSeconds) { this.account = account; this.authority = authority; + this.service = null; + this.isService = false; + if (extras == null) { + this.extras = new Bundle(); + } else { + this.extras = new Bundle(extras); + } + this.period = periodInSeconds; + // Old API uses default flex time. No-one should be using this ctor anyway. + this.flexTime = 0L; + } + + // TODO: Add copy ctor from SyncRequest? + + /** + * Create a copy of a periodic sync. + * {@hide} + */ + public PeriodicSync(PeriodicSync other) { + this.account = other.account; + this.authority = other.authority; + this.service = other.service; + this.isService = other.isService; + this.extras = new Bundle(other.extras); + this.period = other.period; + this.flexTime = other.flexTime; + } + + /** + * A PeriodicSync for a sync with a specified provider. + * {@hide} + */ + public PeriodicSync(Account account, String authority, Bundle extras, + long period, long flexTime) { + this.account = account; + this.authority = authority; + this.service = null; + this.isService = false; + this.extras = new Bundle(extras); + this.period = period; + this.flexTime = flexTime; + } + + /** + * A PeriodicSync for a sync with a specified SyncService. + * {@hide} + */ + public PeriodicSync(ComponentName service, Bundle extras, + long period, + long flexTime) { + this.account = null; + this.authority = null; + this.service = service; + this.isService = true; this.extras = new Bundle(extras); this.period = period; + this.flexTime = flexTime; } + private PeriodicSync(Parcel in) { + this.isService = (in.readInt() != 0); + if (this.isService) { + this.service = in.readParcelable(null); + this.account = null; + this.authority = null; + } else { + this.account = in.readParcelable(null); + this.authority = in.readString(); + this.service = null; + } + this.extras = in.readBundle(); + this.period = in.readLong(); + this.flexTime = in.readLong(); + } + + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel dest, int flags) { - account.writeToParcel(dest, flags); - dest.writeString(authority); + dest.writeInt(isService ? 1 : 0); + if (account == null && authority == null) { + dest.writeParcelable(service, flags); + } else { + dest.writeParcelable(account, flags); + dest.writeString(authority); + } dest.writeBundle(extras); dest.writeLong(period); + dest.writeLong(flexTime); } public static final Creator<PeriodicSync> CREATOR = new Creator<PeriodicSync>() { + @Override public PeriodicSync createFromParcel(Parcel source) { - return new PeriodicSync(Account.CREATOR.createFromParcel(source), - source.readString(), source.readBundle(), source.readLong()); + return new PeriodicSync(source); } + @Override public PeriodicSync[] newArray(int size) { return new PeriodicSync[size]; } }; + @Override 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 - && syncExtrasEquals(extras, other.extras); + if (this.isService != other.isService) { + return false; + } + boolean equal = false; + if (this.isService) { + equal = service.equals(other.service); + } else { + equal = account.equals(other.account) + && authority.equals(other.authority); + } + return equal + && period == other.period + && syncExtrasEquals(extras, other.extras); } - /** {@hide} */ + /** + * Periodic sync extra comparison function. + * {@hide} + */ public static boolean syncExtrasEquals(Bundle b1, Bundle b2) { if (b1.size() != b2.size()) { return false; @@ -100,4 +203,13 @@ public class PeriodicSync implements Parcelable { } return true; } + + @Override + public String toString() { + return "account: " + account + + ", authority: " + authority + + ", service: " + service + + ". period: " + period + "s " + + ", flex: " + flexTime; + } } diff --git a/core/java/android/content/SyncRequest.aidl b/core/java/android/content/SyncRequest.aidl new file mode 100644 index 0000000..8321fac --- /dev/null +++ b/core/java/android/content/SyncRequest.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2013 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 SyncRequest; diff --git a/core/java/android/content/SyncRequest.java b/core/java/android/content/SyncRequest.java new file mode 100644 index 0000000..336371e --- /dev/null +++ b/core/java/android/content/SyncRequest.java @@ -0,0 +1,625 @@ +/* + * Copyright (C) 2013 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.accounts.Account; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Pair; + +public class SyncRequest implements Parcelable { + private static final String TAG = "SyncRequest"; + /** Account to pass to the sync adapter. Can be null. */ + private final Account mAccountToSync; + /** Authority string that corresponds to a ContentProvider. */ + private final String mAuthority; + /** {@link SyncService} identifier. */ + private final ComponentName mComponentInfo; + /** Bundle containing user info as well as sync settings. */ + private final Bundle mExtras; + /** Allow this sync request on metered networks. */ + private final boolean mAllowMetered; + /** + * Anticipated upload size in bytes. + * TODO: Not yet used - we put this information into the bundle for simplicity. + */ + private final long mTxBytes; + /** + * Anticipated download size in bytes. + * TODO: Not yet used - we put this information into the bundle. + */ + private final long mRxBytes; + /** + * Amount of time before {@link mSyncRunTimeSecs} from which the sync may optionally be + * started. + */ + private final long mSyncFlexTimeSecs; + /** + * Specifies a point in the future at which the sync must have been scheduled to run. + */ + private final long mSyncRunTimeSecs; + /** Periodic versus one-off. */ + private final boolean mIsPeriodic; + /** Service versus provider. */ + private final boolean mIsAuthority; + /** Sync should be run in lieu of other syncs. */ + private final boolean mIsExpedited; + + /** + * {@hide} + * @return whether this sync is periodic or one-time. A Sync Request must be + * either one of these or an InvalidStateException will be thrown in + * Builder.build(). + */ + public boolean isPeriodic() { + return mIsPeriodic; + } + + public boolean isExpedited() { + return mIsExpedited; + } + + /** + * {@hide} + * @return true if this sync uses an account/authority pair, or false if + * this is an anonymous sync bound to an @link AnonymousSyncService. + */ + public boolean hasAuthority() { + return mIsAuthority; + } + + /** + * {@hide} + * Throws a runtime IllegalArgumentException if this function is called for an + * anonymous sync. + * + * @return (Account, Provider) for this SyncRequest. + */ + public Pair<Account, String> getProviderInfo() { + if (!hasAuthority()) { + throw new IllegalArgumentException("Cannot getProviderInfo() for an anonymous sync."); + } + return Pair.create(mAccountToSync, mAuthority); + } + + /** + * {@hide} + * Throws a runtime IllegalArgumentException if this function is called for a + * SyncRequest that is bound to an account/provider. + * + * @return ComponentName for the service that this sync will bind to. + */ + public ComponentName getService() { + if (hasAuthority()) { + throw new IllegalArgumentException( + "Cannot getAnonymousService() for a sync that has specified a provider."); + } + return mComponentInfo; + } + + /** + * {@hide} + * Retrieve bundle for this SyncRequest. Will not be null. + */ + public Bundle getBundle() { + return mExtras; + } + + /** + * {@hide} + * @return the earliest point in time that this sync can be scheduled. + */ + public long getSyncFlexTime() { + return mSyncFlexTimeSecs; + } + /** + * {@hide} + * @return the last point in time at which this sync must scheduled. + */ + public long getSyncRunTime() { + return mSyncRunTimeSecs; + } + + public static final Creator<SyncRequest> CREATOR = new Creator<SyncRequest>() { + + @Override + public SyncRequest createFromParcel(Parcel in) { + return new SyncRequest(in); + } + + @Override + public SyncRequest[] newArray(int size) { + return new SyncRequest[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeBundle(mExtras); + parcel.writeLong(mSyncFlexTimeSecs); + parcel.writeLong(mSyncRunTimeSecs); + parcel.writeInt((mIsPeriodic ? 1 : 0)); + parcel.writeInt((mAllowMetered ? 1 : 0)); + parcel.writeLong(mTxBytes); + parcel.writeLong(mRxBytes); + parcel.writeInt((mIsAuthority ? 1 : 0)); + parcel.writeInt((mIsExpedited? 1 : 0)); + if (mIsAuthority) { + parcel.writeParcelable(mAccountToSync, flags); + parcel.writeString(mAuthority); + } else { + parcel.writeParcelable(mComponentInfo, flags); + } + } + + private SyncRequest(Parcel in) { + mExtras = in.readBundle(); + mSyncFlexTimeSecs = in.readLong(); + mSyncRunTimeSecs = in.readLong(); + mIsPeriodic = (in.readInt() != 0); + mAllowMetered = (in.readInt() != 0); + mTxBytes = in.readLong(); + mRxBytes = in.readLong(); + mIsAuthority = (in.readInt() != 0); + mIsExpedited = (in.readInt() != 0); + if (mIsAuthority) { + mComponentInfo = null; + mAccountToSync = in.readParcelable(null); + mAuthority = in.readString(); + } else { + mComponentInfo = in.readParcelable(null); + mAccountToSync = null; + mAuthority = null; + } + } + + /** {@hide} Protected ctor to instantiate anonymous SyncRequest. */ + protected SyncRequest(SyncRequest.Builder b) { + mSyncFlexTimeSecs = b.mSyncFlexTimeSecs; + mSyncRunTimeSecs = b.mSyncRunTimeSecs; + mAccountToSync = b.mAccount; + mAuthority = b.mAuthority; + mComponentInfo = b.mComponentName; + mIsPeriodic = (b.mSyncType == Builder.SYNC_TYPE_PERIODIC); + mIsAuthority = (b.mSyncTarget == Builder.SYNC_TARGET_ADAPTER); + mIsExpedited = b.mExpedited; + mExtras = new Bundle(b.mCustomExtras); + mAllowMetered = b.mAllowMetered; + mTxBytes = b.mTxBytes; + mRxBytes = b.mRxBytes; + } + + /** + * Builder class for a @link SyncRequest. As you build your SyncRequest this class will also + * perform validation. + */ + public static class Builder { + /** Unknown sync type. */ + private static final int SYNC_TYPE_UNKNOWN = 0; + /** Specify that this is a periodic sync. */ + private static final int SYNC_TYPE_PERIODIC = 1; + /** Specify that this is a one-time sync. */ + private static final int SYNC_TYPE_ONCE = 2; + /** Unknown sync target. */ + private static final int SYNC_TARGET_UNKNOWN = 0; + /** Specify that this is an anonymous sync. */ + private static final int SYNC_TARGET_SERVICE = 1; + /** Specify that this is a sync with a provider. */ + private static final int SYNC_TARGET_ADAPTER = 2; + /** + * Earliest point of displacement into the future at which this sync can + * occur. + */ + private long mSyncFlexTimeSecs; + /** Displacement into the future at which this sync must occur. */ + private long mSyncRunTimeSecs; + /** + * Sync configuration information - custom user data explicitly provided by the developer. + * This data is handed over to the sync operation. + */ + private Bundle mCustomExtras; + /** + * Sync system configuration - used to store system sync configuration. Corresponds to + * ContentResolver.SYNC_EXTRAS_* flags. + * TODO: Use this instead of dumping into one bundle. Need to decide if these flags should + * discriminate between equivalent syncs. + */ + private Bundle mSyncConfigExtras; + /** Expected upload transfer in bytes. */ + private long mTxBytes = -1L; + /** Expected download transfer in bytes. */ + private long mRxBytes = -1L; + /** Whether or not this sync can occur on metered networks. Default false. */ + private boolean mAllowMetered; + /** Priority of this sync relative to others from calling app [-2, 2]. Default 0. */ + private int mPriority = 0; + /** + * Whether this builder is building a periodic sync, or a one-time sync. + */ + private int mSyncType = SYNC_TYPE_UNKNOWN; + /** Whether this will go to a sync adapter or to a sync service. */ + private int mSyncTarget = SYNC_TARGET_UNKNOWN; + /** Whether this is a user-activated sync. */ + private boolean mIsManual; + /** + * Whether to retry this one-time sync if the sync fails. Not valid for + * periodic syncs. See {@link ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY}. + */ + private boolean mNoRetry; + /** + * Whether to respect back-off for this one-time sync. Not valid for + * periodic syncs. See + * {@link android.content.ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF}; + */ + private boolean mIgnoreBackoff; + + /** Ignore sync system settings and perform sync anyway. */ + private boolean mIgnoreSettings; + + /** This sync will run in preference to other non-expedited syncs. */ + private boolean mExpedited; + + /** + * The @link android.content.AnonymousSyncService component that + * contains the sync logic if this is a provider-less sync, otherwise + * null. + */ + private ComponentName mComponentName; + /** + * The Account object that together with an Authority name define the SyncAdapter (if + * this sync is bound to a provider), otherwise null. This gets resolved + * against a {@link com.android.server.content.SyncStorageEngine}. + */ + private Account mAccount; + /** + * The Authority name that together with an Account define the SyncAdapter (if + * this sync is bound to a provider), otherwise null. This gets resolved + * against a {@link com.android.server.content.SyncStorageEngine}. + */ + private String mAuthority; + + public Builder() { + } + + /** + * Developer can define timing constraints for this one-shot request. + * These values are elapsed real-time. + * + * @param whenSeconds The time in seconds at which you want this + * sync to occur. + * @param beforeSeconds The amount of time in advance of whenSeconds that this + * sync may be permitted to occur. This is rounded up to a minimum of 5 + * seconds, for any sync for which whenSeconds > 5. + * + * Example + * <pre> + * Perform an immediate sync. + * SyncRequest.Builder builder = (new SyncRequest.Builder()).syncOnce(0, 0); + * That is, a sync 0 seconds from now with 0 seconds of flex. + * + * Perform a sync in exactly 5 minutes. + * SyncRequest.Builder builder = + * new SyncRequest.Builder().syncOnce(5 * MIN_IN_SECS, 0); + * + * Perform a sync in 5 minutes, with one minute of leeway (between 4 and 5 minutes from + * now). + * SyncRequest.Builder builder = + * new SyncRequest.Builder().syncOnce(5 * MIN_IN_SECS, 1 * MIN_IN_SECS); + * </pre> + */ + public Builder syncOnce(long whenSeconds, long beforeSeconds) { + if (mSyncType != SYNC_TYPE_UNKNOWN) { + throw new IllegalArgumentException("Sync type has already been defined."); + } + mSyncType = SYNC_TYPE_ONCE; + setupInterval(whenSeconds, beforeSeconds); + return this; + } + + /** + * Build a periodic sync. Either this or syncOnce() <b>must</b> be called for this builder. + * Syncs are identified by target {@link SyncService}/{@link android.provider} and by the + * contents of the extras bundle. + * You cannot reuse the same builder for one-time syncs after having specified a periodic + * sync (by calling this function). If you do, an {@link IllegalArgumentException} will be + * thrown. + * + * Example usage. + * + * <pre> + * Request a periodic sync every 5 hours with 20 minutes of flex. + * SyncRequest.Builder builder = + * (new SyncRequest.Builder()).syncPeriodic(5 * HOUR_IN_SECS, 20 * MIN_IN_SECS); + * + * Schedule a periodic sync every hour at any point in time during that hour. + * SyncRequest.Builder builder = + * (new SyncRequest.Builder()).syncPeriodic(1 * HOUR_IN_SECS, 1 * HOUR_IN_SECS); + * </pre> + * + * N.B.: Periodic syncs are not allowed to have any of + * {@link #SYNC_EXTRAS_DO_NOT_RETRY}, + * {@link #SYNC_EXTRAS_IGNORE_BACKOFF}, + * {@link #SYNC_EXTRAS_IGNORE_SETTINGS}, + * {@link #SYNC_EXTRAS_INITIALIZE}, + * {@link #SYNC_EXTRAS_FORCE}, + * {@link #SYNC_EXTRAS_EXPEDITED}, + * {@link #SYNC_EXTRAS_MANUAL} + * set to true. If any are supplied then an {@link IllegalArgumentException} will + * be thrown. + * + * @param pollFrequency the amount of time in seconds that you wish + * to elapse between periodic syncs. + * @param beforeSeconds the amount of flex time in seconds before + * {@code pollFrequency} that you permit for the sync to take + * place. Must be less than {@code pollFrequency}. + */ + public Builder syncPeriodic(long pollFrequency, long beforeSeconds) { + if (mSyncType != SYNC_TYPE_UNKNOWN) { + throw new IllegalArgumentException("Sync type has already been defined."); + } + mSyncType = SYNC_TYPE_PERIODIC; + setupInterval(pollFrequency, beforeSeconds); + return this; + } + + /** {@hide} */ + private void setupInterval(long at, long before) { + if (before > at) { + throw new IllegalArgumentException("Specified run time for the sync must be" + + " after the specified flex time."); + } + mSyncRunTimeSecs = at; + mSyncFlexTimeSecs = before; + } + + /** + * Developer can provide insight into their payload size; optional. -1 specifies + * unknown, so that you are not restricted to defining both fields. + * + * @param rxBytes Bytes expected to be downloaded. + * @param txBytes Bytes expected to be uploaded. + */ + public Builder setTransferSize(long rxBytes, long txBytes) { + mRxBytes = rxBytes; + mTxBytes = txBytes; + return this; + } + + /** + * @param allow false to allow this transfer on metered networks. + * Default true. + */ + public Builder setAllowMetered(boolean allow) { + mAllowMetered = true; + return this; + } + + /** + * Give ourselves a concrete way of binding. Use an explicit + * authority+account SyncAdapter for this transfer, otherwise we bind + * anonymously to given componentname. + * + * @param authority + * @param account Account to sync. Can be null unless this is a periodic + * sync, for which verification by the ContentResolver will + * fail. If a sync is performed without an account, the + */ + public Builder setSyncAdapter(Account account, String authority) { + if (mSyncTarget != SYNC_TARGET_UNKNOWN) { + throw new IllegalArgumentException("Sync target has already been defined."); + } + mSyncTarget = SYNC_TARGET_ADAPTER; + mAccount = account; + mAuthority = authority; + mComponentName = null; + return this; + } + + /** + * Set Service component name for anonymous sync. This is not validated + * until sync time so providing an incorrect component name here will + * not fail. + * + * @param cname ComponentName to identify your Anonymous service + */ + public Builder setSyncAdapter(ComponentName cname) { + if (mSyncTarget != SYNC_TARGET_UNKNOWN) { + throw new IllegalArgumentException("Sync target has already been defined."); + } + mSyncTarget = SYNC_TARGET_SERVICE; + mComponentName = cname; + mAccount = null; + mAuthority = null; + return this; + } + + /** + * Developer-provided extras handed back when sync actually occurs. This bundle is copied + * into the SyncRequest returned by build(). + * + * Example: + * <pre> + * String[] syncItems = {"dog", "cat", "frog", "child"}; + * SyncRequest.Builder builder = + * new SyncRequest.Builder() + * .setSyncAdapter(dummyAccount, dummyProvider) + * .syncOnce(5 * MINUTES_IN_SECS); + * + * for (String syncData : syncItems) { + * Bundle extras = new Bundle(); + * extras.setString("data", syncData); + * builder.setExtras(extras); + * ContentResolver.sync(builder.build()); // Each sync() request creates a unique sync. + * } + * </pre> + * + * Only values of the following types may be used in the extras bundle: + * <ul> + * <li>Integer</li> + * <li>Long</li> + * <li>Boolean</li> + * <li>Float</li> + * <li>Double</li> + * <li>String</li> + * <li>Account</li> + * <li>null</li> + * </ul> + * If any data is present in the bundle not of this type, build() will + * throw a runtime exception. + * + * @param bundle + */ + public Builder setExtras(Bundle bundle) { + mCustomExtras = bundle; + return this; + } + + /** + * Convenience function for setting {@link ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY}. A + * one-off sync operation that fails will be retried at a later date unless this is + * set to false. Default is true. Not valid for periodic sync and will throw an + * IllegalArgumentException in Builder.build(). + * + * @param retry false to not retry a failed sync. Default true. + */ + public Builder setNoRetry(boolean retry) { + mNoRetry = retry; + return this; + } + + /** + * {@link ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS}. Not valid for + * periodic sync and will throw an IllegalArgumentException in + * Builder.build(). Default false. + */ + public Builder setIgnoreSettings(boolean ignoreSettings) { + mIgnoreSettings = ignoreSettings; + return this; + } + + /** + * Convenience function for setting {@link ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF}. + * + * @param ignoreBackoff + */ + public Builder setIgnoreBackoff(boolean ignoreBackoff) { + mIgnoreBackoff = ignoreBackoff; + return this; + } + + /** + * {@link ContentResolver.SYNC_EXTRAS_MANUAL}. Default false. + */ + public Builder setManual(boolean isManual) { + mIsManual = isManual; + return this; + } + + /** + * {@link ContentResolver.SYNC_EXTRAS_} Default false. + */ + public Builder setExpedited(boolean expedited) { + mExpedited = expedited; + return this; + } + + /** + * Priority of this request among all requests from the calling app. + * Range of [-2,2] similar to {@link android.app.Notification.priority}. + */ + public Builder setPriority(int priority) { + if (priority < -2 || priority > 2) { + throw new IllegalArgumentException("Priority must be within range [-2, 2]"); + } + mPriority = priority; + return this; + } + + /** + * Performs validation over the request and throws the runtime exception + * IllegalArgumentException if this validation fails. TODO: Add + * validation of SyncRequest here. 1) Cannot specify both periodic & + * one-off (fails above). 2) Cannot specify both service and + * account/provider (fails above). + * + * @return a SyncRequest with the information contained within this + * builder. + */ + public SyncRequest build() { + // Validate the extras bundle + try { + ContentResolver.validateSyncExtrasBundle(mCustomExtras); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(e.getMessage()); + } + if (mCustomExtras == null) { + mCustomExtras = new Bundle(); + } + // Combine the builder extra flags into the copy of the bundle. + if (mIgnoreBackoff) { + mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true); + } + if (mAllowMetered) { + mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_ALLOW_METERED, true); + } + if (mIgnoreSettings) { + mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); + } + if (mNoRetry) { + mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); + } + if (mExpedited) { + mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); + } + if (mIsManual) { + mCustomExtras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); + } + // Upload/download expectations. + mCustomExtras.putLong(ContentResolver.SYNC_EXTRAS_EXPECTED_UPLOAD, mTxBytes); + mCustomExtras.putLong(ContentResolver.SYNC_EXTRAS_EXPECTED_DOWNLOAD, mRxBytes); + // Priority. + mCustomExtras.putInt(ContentResolver.SYNC_EXTRAS_PRIORITY, mPriority); + if (mSyncType == SYNC_TYPE_PERIODIC) { + // If this is a periodic sync ensure than invalid extras were + // not set. + if (mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false) + || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false) + || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false) + || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false) + || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false) + || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false) + || mCustomExtras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) { + throw new IllegalArgumentException("Illegal extras were set"); + } + } else if (mSyncType == SYNC_TYPE_UNKNOWN) { + throw new IllegalArgumentException("Must call either syncOnce() or syncPeriodic()"); + } + // Ensure that a target for the sync has been set. + if (mSyncTarget == SYNC_TARGET_UNKNOWN) { + throw new IllegalArgumentException("Must specify an adapter with one of" + + "setSyncAdapter(ComponentName) or setSyncAdapter(Account, String"); + } + return new SyncRequest(this); + } + } +} diff --git a/core/java/android/content/SyncResult.java b/core/java/android/content/SyncResult.java index 6cb0d02..4f86af9 100644 --- a/core/java/android/content/SyncResult.java +++ b/core/java/android/content/SyncResult.java @@ -181,7 +181,7 @@ public final class SyncResult implements Parcelable { * <li> {@link SyncStats#numIoExceptions} > 0 * <li> {@link #syncAlreadyInProgress} * </ul> - * @return true if a hard error is indicated + * @return true if a soft error is indicated */ public boolean hasSoftError() { return syncAlreadyInProgress || stats.numIoExceptions > 0; @@ -195,6 +195,11 @@ public final class SyncResult implements Parcelable { return hasSoftError() || hasHardError(); } + /** + * Convenience method for determining if the Sync should be rescheduled after failing for some + * reason. + * @return true if the SyncManager should reschedule this sync. + */ public boolean madeSomeProgress() { return ((stats.numDeletes > 0) && !tooManyDeletions) || stats.numInserts > 0 diff --git a/core/java/android/content/SyncService.java b/core/java/android/content/SyncService.java new file mode 100644 index 0000000..100fd40 --- /dev/null +++ b/core/java/android/content/SyncService.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2013 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.app.Service; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Process; +import android.os.Trace; + +import com.android.internal.annotations.GuardedBy; + +import java.util.HashMap; + +/** + * Simplified @link android.content.AbstractThreadedSyncAdapter. Folds that + * behaviour into a service to which the system can bind when requesting an + * anonymous (providerless/accountless) sync. + * <p> + * In order to perform an anonymous sync operation you must extend this service, + * implementing the abstract methods. This service must then be declared in the + * application's manifest as usual. You can use this service for other work, however you + * <b> must not </b> override the onBind() method unless you know what you're doing, + * which limits the usefulness of this service for other work. + * + * <pre> + * <service ndroid:name=".MyAnonymousSyncService" android:permission="android.permission.SYNC" /> + * </pre> + * Like @link android.content.AbstractThreadedSyncAdapter this service supports + * multiple syncs at the same time. Each incoming startSync() with a unique tag + * will spawn a thread to do the work of that sync. If startSync() is called + * with a tag that already exists, a SyncResult.ALREADY_IN_PROGRESS is returned. + * Remember that your service will spawn multiple threads if you schedule multiple syncs + * at once, so if you mutate local objects you must ensure synchronization. + */ +public abstract class SyncService extends Service { + + /** SyncAdapter Instantiation that any anonymous syncs call. */ + private final AnonymousSyncAdapterImpl mSyncAdapter = new AnonymousSyncAdapterImpl(); + + /** Keep track of on-going syncs, keyed by tag. */ + @GuardedBy("mLock") + private final HashMap<Bundle, AnonymousSyncThread> + mSyncThreads = new HashMap<Bundle, AnonymousSyncThread>(); + /** Lock object for accessing the SyncThreads HashMap. */ + private final Object mSyncThreadLock = new Object(); + + @Override + public IBinder onBind(Intent intent) { + return mSyncAdapter.asBinder(); + } + + /** {@hide} */ + private class AnonymousSyncAdapterImpl extends IAnonymousSyncAdapter.Stub { + + @Override + public void startSync(ISyncContext syncContext, Bundle extras) { + // Wrap the provided Sync Context because it may go away by the time + // we call it. + final SyncContext syncContextClient = new SyncContext(syncContext); + boolean alreadyInProgress = false; + synchronized (mSyncThreadLock) { + if (mSyncThreads.containsKey(extras)) { + // Don't want to call back to SyncManager while still + // holding lock. + alreadyInProgress = true; + } else { + AnonymousSyncThread syncThread = new AnonymousSyncThread( + syncContextClient, extras); + mSyncThreads.put(extras, syncThread); + syncThread.start(); + } + } + if (alreadyInProgress) { + syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); + } + } + + /** + * Used by the SM to cancel a specific sync using the {@link + * com.android.server.content.SyncManager.ActiveSyncContext} as a handle. + */ + @Override + public void cancelSync(ISyncContext syncContext) { + AnonymousSyncThread runningSync = null; + synchronized (mSyncThreadLock) { + for (AnonymousSyncThread thread : mSyncThreads.values()) { + if (thread.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { + runningSync = thread; + break; + } + } + } + if (runningSync != null) { + runningSync.interrupt(); + } + } + } + + /** + * {@hide} + * Similar to {@link android.content.AbstractThreadedSyncAdapter.SyncThread}. However while + * the ATSA considers an already in-progress sync to be if the account provided is currently + * syncing, this anonymous sync has no notion of account and therefore considers a sync unique + * if the provided bundle is different. + */ + private class AnonymousSyncThread extends Thread { + private final SyncContext mSyncContext; + private final Bundle mExtras; + + public AnonymousSyncThread(SyncContext syncContext, Bundle extras) { + mSyncContext = syncContext; + mExtras = extras; + } + + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + Trace.traceBegin(Trace.TRACE_TAG_SYNC_MANAGER, getApplication().getPackageName()); + + SyncResult syncResult = new SyncResult(); + try { + if (isCancelled()) { + return; + } + // Run the sync based off of the provided code. + SyncService.this.onPerformSync(mExtras, syncResult); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_SYNC_MANAGER); + if (!isCancelled()) { + mSyncContext.onFinished(syncResult); + } + // Synchronize so that the assignment will be seen by other + // threads + // that also synchronize accesses to mSyncThreads. + synchronized (mSyncThreadLock) { + mSyncThreads.remove(mExtras); + } + } + } + + private boolean isCancelled() { + return Thread.currentThread().isInterrupted(); + } + } + + /** + * Initiate an anonymous sync using this service. SyncAdapter-specific + * parameters may be specified in extras, which is guaranteed to not be + * null. + */ + public abstract void onPerformSync(Bundle extras, SyncResult syncResult); + +} diff --git a/services/java/com/android/server/content/ContentService.java b/services/java/com/android/server/content/ContentService.java index 4a5c0d5..a56af08 100644 --- a/services/java/com/android/server/content/ContentService.java +++ b/services/java/com/android/server/content/ContentService.java @@ -19,6 +19,7 @@ package com.android.server.content; import android.Manifest; import android.accounts.Account; import android.app.ActivityManager; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.IContentService; @@ -26,6 +27,7 @@ import android.content.ISyncStatusObserver; import android.content.PeriodicSync; import android.content.SyncAdapterType; import android.content.SyncInfo; +import android.content.SyncRequest; import android.content.SyncStatusInfo; import android.database.IContentObserver; import android.database.sqlite.SQLiteException; @@ -39,6 +41,7 @@ import android.os.ServiceManager; import android.os.SystemProperties; import android.os.UserHandle; import android.util.Log; +import android.util.Pair; import android.util.Slog; import android.util.SparseIntArray; @@ -312,6 +315,7 @@ public final class ContentService extends IContentService.Stub { } } + @Override public void requestSync(Account account, String authority, Bundle extras) { ContentResolver.validateSyncExtrasBundle(extras); int userId = UserHandle.getCallingUserId(); @@ -323,7 +327,8 @@ public final class ContentService extends IContentService.Stub { try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.scheduleSync(account, userId, uId, authority, extras, 0 /* no delay */, + syncManager.scheduleSync(account, userId, uId, authority, extras, + 0 /* no delay */, 0 /* no delay */, false /* onlyThoseWithUnkownSyncableState */); } } finally { @@ -332,11 +337,83 @@ public final class ContentService extends IContentService.Stub { } /** + * Request a sync with a generic {@link android.content.SyncRequest} object. This will be + * either: + * periodic OR one-off sync. + * and + * anonymous OR provider sync. + * Depending on the request, we enqueue to suit in the SyncManager. + * @param request + */ + @Override + public void sync(SyncRequest request) { + Bundle extras = request.getBundle(); + ContentResolver.validateSyncExtrasBundle(extras); + + long flextime = request.getSyncFlexTime(); + long runAtTime = request.getSyncRunTime(); + int userId = UserHandle.getCallingUserId(); + int uId = Binder.getCallingUid(); + + // This makes it so that future permission checks will be in the context of this + // process rather than the caller's process. We will restore this before returning. + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) { + if (request.hasAuthority()) { + // Sync Adapter registered with the system - old API. + final Account account = request.getProviderInfo().first; + final String provider = request.getProviderInfo().second; + if (request.isPeriodic()) { + mContext.enforceCallingOrSelfPermission( + Manifest.permission.WRITE_SYNC_SETTINGS, + "no permission to write the sync settings"); + if (runAtTime < 60) { + Slog.w(TAG, "Requested poll frequency of " + runAtTime + + " seconds being rounded up to 60 seconds."); + runAtTime = 60; + } + PeriodicSync syncToAdd = + new PeriodicSync(account, provider, extras, runAtTime, flextime); + getSyncManager().getSyncStorageEngine().addPeriodicSync(syncToAdd, userId); + } else { + long beforeRuntimeMillis = (flextime) * 1000; + long runtimeMillis = runAtTime * 1000; + syncManager.scheduleSync( + account, userId, uId, provider, extras, + beforeRuntimeMillis, runtimeMillis, + false /* onlyThoseWithUnknownSyncableState */); + } + } else { + // Anonymous sync - new API. + final ComponentName syncService = request.getService(); + if (request.isPeriodic()) { + throw new RuntimeException("Periodic anonymous syncs not implemented yet."); + } else { + long beforeRuntimeMillis = (flextime) * 1000; + long runtimeMillis = runAtTime * 1000; + syncManager.scheduleSync( + syncService, userId, uId, extras, + beforeRuntimeMillis, + runtimeMillis, + false /* onlyThoseWithUnknownSyncableState */); // Empty function. + throw new RuntimeException("One-off anonymous syncs not implemented yet."); + } + } + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + /** * Clear all scheduled sync operations that match the uri and cancel the active sync * if they match the authority and account, if they are present. * @param account filter the pending and active syncs to cancel using this account * @param authority filter the pending and active syncs to cancel using this authority */ + @Override public void cancelSync(Account account, String authority) { int userId = UserHandle.getCallingUserId(); @@ -358,6 +435,7 @@ public final class ContentService extends IContentService.Stub { * Get information about the SyncAdapters that are known to the system. * @return an array of SyncAdapters that have registered with the system */ + @Override public SyncAdapterType[] getSyncAdapterTypes() { // This makes it so that future permission checks will be in the context of this // process rather than the caller's process. We will restore this before returning. @@ -371,6 +449,7 @@ public final class ContentService extends IContentService.Stub { } } + @Override public boolean getSyncAutomatically(Account account, String providerName) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, "no permission to read the sync settings"); @@ -389,6 +468,7 @@ public final class ContentService extends IContentService.Stub { return false; } + @Override public void setSyncAutomatically(Account account, String providerName, boolean sync) { mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, "no permission to write the sync settings"); @@ -406,6 +486,10 @@ public final class ContentService extends IContentService.Stub { } } + /** + * Old API. Schedule periodic sync with default flex time. + */ + @Override public void addPeriodicSync(Account account, String authority, Bundle extras, long pollFrequency) { mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, @@ -420,13 +504,18 @@ public final class ContentService extends IContentService.Stub { long identityToken = clearCallingIdentity(); try { - getSyncManager().getSyncStorageEngine().addPeriodicSync( - account, userId, authority, extras, pollFrequency); + // Add default flex time to this sync. + PeriodicSync syncToAdd = + new PeriodicSync(account, authority, extras, + pollFrequency, + SyncStorageEngine.calculateDefaultFlexTime(pollFrequency)); + getSyncManager().getSyncStorageEngine().addPeriodicSync(syncToAdd, userId); } finally { restoreCallingIdentity(identityToken); } } + @Override public void removePeriodicSync(Account account, String authority, Bundle extras) { mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, "no permission to write the sync settings"); @@ -434,13 +523,23 @@ public final class ContentService extends IContentService.Stub { long identityToken = clearCallingIdentity(); try { - getSyncManager().getSyncStorageEngine().removePeriodicSync(account, userId, authority, - extras); + PeriodicSync syncToRemove = new PeriodicSync(account, authority, extras, + 0 /* Not read for removal */, 0 /* Not read for removal */); + getSyncManager().getSyncStorageEngine().removePeriodicSync(syncToRemove, userId); } finally { restoreCallingIdentity(identityToken); } } + /** + * TODO: Implement. + * @param request Sync to remove. + */ + public void removeSync(SyncRequest request) { + + } + + @Override public List<PeriodicSync> getPeriodicSyncs(Account account, String providerName) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, "no permission to read the sync settings"); @@ -473,6 +572,7 @@ public final class ContentService extends IContentService.Stub { return -1; } + @Override public void setIsSyncable(Account account, String providerName, int syncable) { mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, "no permission to write the sync settings"); @@ -490,6 +590,7 @@ public final class ContentService extends IContentService.Stub { } } + @Override public boolean getMasterSyncAutomatically() { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, "no permission to read the sync settings"); @@ -507,6 +608,7 @@ public final class ContentService extends IContentService.Stub { return false; } + @Override public void setMasterSyncAutomatically(boolean flag) { mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, "no permission to write the sync settings"); diff --git a/services/java/com/android/server/content/SyncManager.java b/services/java/com/android/server/content/SyncManager.java index 2da95c3..ee5b890 100644 --- a/services/java/com/android/server/content/SyncManager.java +++ b/services/java/com/android/server/content/SyncManager.java @@ -34,6 +34,7 @@ import android.content.ISyncContext; import android.content.ISyncStatusObserver; import android.content.Intent; import android.content.IntentFilter; +import android.content.PeriodicSync; import android.content.ServiceConnection; import android.content.SyncActivityTooManyDeletes; import android.content.SyncAdapterType; @@ -83,6 +84,7 @@ import com.google.android.collect.Sets; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -190,6 +192,7 @@ public class SyncManager { private BroadcastReceiver mStorageIntentReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { @@ -210,36 +213,39 @@ public class SyncManager { }; private BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { mSyncHandler.onBootCompleted(); } }; private BroadcastReceiver mBackgroundDataSettingChanged = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { if (getConnectivityManager().getBackgroundDataSetting()) { scheduleSync(null /* account */, UserHandle.USER_ALL, SyncOperation.REASON_BACKGROUND_DATA_SETTINGS_CHANGED, null /* authority */, - new Bundle(), 0 /* delay */, + new Bundle(), 0 /* delay */, 0 /* delay */, false /* onlyThoseWithUnknownSyncableState */); } } }; private BroadcastReceiver mAccountsUpdatedReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { updateRunningAccounts(); // Kick off sync for everyone, since this was a radical account change scheduleSync(null, UserHandle.USER_ALL, SyncOperation.REASON_ACCOUNTS_UPDATED, null, - null, 0 /* no delay */, false); + null, 0 /* no delay */, 0/* no delay */, false); } }; private final PowerManager mPowerManager; - // Use this as a random offset to seed all periodic syncs + // Use this as a random offset to seed all periodic syncs. private int mSyncRandomOffsetMillis; private final UserManager mUserManager; @@ -296,6 +302,7 @@ public class SyncManager { private BroadcastReceiver mConnectivityIntentReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { final boolean wasConnected = mDataConnectionIsConnected; @@ -321,6 +328,7 @@ public class SyncManager { private BroadcastReceiver mShutdownIntentReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { Log.w(TAG, "Writing sync state before shutdown..."); getSyncStorageEngine().writeAllState(); @@ -371,9 +379,13 @@ public class SyncManager { SyncStorageEngine.init(context); mSyncStorageEngine = SyncStorageEngine.getSingleton(); mSyncStorageEngine.setOnSyncRequestListener(new OnSyncRequestListener() { + @Override public void onSyncRequest(Account account, int userId, int reason, String authority, Bundle extras) { - scheduleSync(account, userId, reason, authority, extras, 0, false); + scheduleSync(account, userId, reason, authority, extras, + 0 /* no delay */, + 0 /* no delay */, + false); } }); @@ -388,7 +400,7 @@ public class SyncManager { if (!removed) { scheduleSync(null, UserHandle.USER_ALL, SyncOperation.REASON_SERVICE_CHANGED, - type.authority, null, 0 /* no delay */, + type.authority, null, 0 /* no delay */, 0 /* no delay */, false /* onlyThoseWithUnkownSyncableState */); } } @@ -453,6 +465,7 @@ public class SyncManager { mSyncStorageEngine.addStatusChangeListener( ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, new ISyncStatusObserver.Stub() { + @Override public void onStatusChanged(int which) { // force the sync loop to run if the settings change sendCheckAlarmsMessage(); @@ -526,6 +539,177 @@ public class SyncManager { } /** + * Initiate a sync using the new anonymous service API. + * TODO: Implement. + * @param cname SyncService component bound to in order to perform the sync. + * @param userId the id of the user whose accounts are to be synced. If userId is USER_ALL, + * then all users' accounts are considered. + * @param uid Linux uid of the application that is performing the sync. + * @param extras a Map of SyncAdapter-specific information to control + * syncs of a specific provider. Can be null. + * @param beforeRunTimeMillis + * @param runtimeMillis + */ + public void scheduleSync(ComponentName cname, int userId, int uid, Bundle extras, + long beforeRunTimeMillis, long runtimeMillis, + boolean onlyThoseWithUnknownSyncableState) { +/** + boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); + + final boolean backgroundDataUsageAllowed = !mBootCompleted || + getConnectivityManager().getBackgroundDataSetting(); + + if (extras == null) { + extras = new Bundle(); + } + if (isLoggable) { + Log.e(TAG, requestedAccount + " " + extras.toString() + " " + requestedAuthority); + } + Boolean expedited = extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false); + if (expedited) { + runtimeMillis = -1; // this means schedule at the front of the queue + } + + AccountAndUser[] accounts; + if (requestedAccount != null && userId != UserHandle.USER_ALL) { + accounts = new AccountAndUser[] { new AccountAndUser(requestedAccount, userId) }; + } else { + // if the accounts aren't configured yet then we can't support an account-less + // sync request + accounts = mRunningAccounts; + if (accounts.length == 0) { + if (isLoggable) { + Log.v(TAG, "scheduleSync: no accounts configured, dropping"); + } + return; + } + } + + final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false); + final boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); + if (manualSync) { + extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); + } + final boolean ignoreSettings = + extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false); + + int source; + if (uploadOnly) { + source = SyncStorageEngine.SOURCE_LOCAL; + } else if (manualSync) { + source = SyncStorageEngine.SOURCE_USER; + } else if (requestedAuthority == null) { + source = SyncStorageEngine.SOURCE_POLL; + } else { + // this isn't strictly server, since arbitrary callers can (and do) request + // a non-forced two-way sync on a specific url + source = SyncStorageEngine.SOURCE_SERVER; + } + + for (AccountAndUser account : accounts) { + // Compile a list of authorities that have sync adapters. + // For each authority sync each account that matches a sync adapter. + final HashSet<String> syncableAuthorities = new HashSet<String>(); + for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapter : + mSyncAdapters.getAllServices(account.userId)) { + syncableAuthorities.add(syncAdapter.type.authority); + } + + // if the url was specified then replace the list of authorities + // with just this authority or clear it if this authority isn't + // syncable + if (requestedAuthority != null) { + final boolean hasSyncAdapter = syncableAuthorities.contains(requestedAuthority); + syncableAuthorities.clear(); + if (hasSyncAdapter) syncableAuthorities.add(requestedAuthority); + } + + for (String authority : syncableAuthorities) { + int isSyncable = getIsSyncable(account.account, account.userId, + authority); + if (isSyncable == 0) { + continue; + } + final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo; + syncAdapterInfo = mSyncAdapters.getServiceInfo( + SyncAdapterType.newKey(authority, account.account.type), account.userId); + if (syncAdapterInfo == null) { + continue; + } + final boolean allowParallelSyncs = syncAdapterInfo.type.allowParallelSyncs(); + final boolean isAlwaysSyncable = syncAdapterInfo.type.isAlwaysSyncable(); + if (isSyncable < 0 && isAlwaysSyncable) { + mSyncStorageEngine.setIsSyncable(account.account, account.userId, authority, 1); + isSyncable = 1; + } + if (onlyThoseWithUnkownSyncableState && isSyncable >= 0) { + continue; + } + if (!syncAdapterInfo.type.supportsUploading() && uploadOnly) { + continue; + } + + // always allow if the isSyncable state is unknown + boolean syncAllowed = + (isSyncable < 0) + || ignoreSettings + || (backgroundDataUsageAllowed + && mSyncStorageEngine.getMasterSyncAutomatically(account.userId) + && mSyncStorageEngine.getSyncAutomatically(account.account, + account.userId, authority)); + if (!syncAllowed) { + if (isLoggable) { + Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority + + " is not allowed, dropping request"); + } + continue; + } + + Pair<Long, Long> backoff = mSyncStorageEngine + .getBackoff(account.account, account.userId, authority); + long delayUntil = mSyncStorageEngine.getDelayUntilTime(account.account, + account.userId, authority); + final long backoffTime = backoff != null ? backoff.first : 0; + if (isSyncable < 0) { + // Initialisation sync. + Bundle newExtras = new Bundle(); + newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); + if (isLoggable) { + Log.v(TAG, "schedule initialisation Sync:" + + ", delay until " + delayUntil + + ", run by " + 0 + + ", source " + source + + ", account " + account + + ", authority " + authority + + ", extras " + newExtras); + } + scheduleSyncOperation( + new SyncOperation(account.account, account.userId, reason, source, + authority, newExtras, 0 /* immediate , 0 /* No flex time, + backoffTime, delayUntil, allowParallelSyncs)); + } + if (!onlyThoseWithUnkownSyncableState) { + if (isLoggable) { + Log.v(TAG, "scheduleSync:" + + " delay until " + delayUntil + + " run by " + runtimeMillis + + " flex " + beforeRuntimeMillis + + ", source " + source + + ", account " + account + + ", authority " + authority + + ", extras " + extras); + } + scheduleSyncOperation( + new SyncOperation(account.account, account.userId, reason, source, + authority, extras, runtimeMillis, beforeRuntimeMillis, + backoffTime, delayUntil, allowParallelSyncs)); + } + } + }*/ + } + + /** * Initiate a sync. This can start a sync for all providers * (pass null to url, set onlyTicklable to false), only those * providers that are marked as ticklable (pass null to url, @@ -562,22 +746,28 @@ public class SyncManager { * @param extras a Map of SyncAdapter-specific information to control * syncs of a specific provider. Can be null. Is ignored * if the url is null. - * @param delay how many milliseconds in the future to wait before performing this - * @param onlyThoseWithUnkownSyncableState + * @param beforeRuntimeMillis milliseconds before runtimeMillis that this sync can run. + * @param runtimeMillis maximum milliseconds in the future to wait before performing sync. + * @param onlyThoseWithUnkownSyncableState Only sync authorities that have unknown state. */ public void scheduleSync(Account requestedAccount, int userId, int reason, - String requestedAuthority, Bundle extras, long delay, - boolean onlyThoseWithUnkownSyncableState) { + String requestedAuthority, Bundle extras, long beforeRuntimeMillis, + long runtimeMillis, boolean onlyThoseWithUnkownSyncableState) { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); final boolean backgroundDataUsageAllowed = !mBootCompleted || getConnectivityManager().getBackgroundDataSetting(); - if (extras == null) extras = new Bundle(); - + if (extras == null) { + extras = new Bundle(); + } + if (isLoggable) { + Log.d(TAG, "one-time sync for: " + requestedAccount + " " + extras.toString() + " " + + requestedAuthority); + } Boolean expedited = extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false); if (expedited) { - delay = -1; // this means schedule at the front of the queue + runtimeMillis = -1; // this means schedule at the front of the queue } AccountAndUser[] accounts; @@ -682,11 +872,13 @@ public class SyncManager { account.userId, authority); final long backoffTime = backoff != null ? backoff.first : 0; if (isSyncable < 0) { + // Initialisation sync. Bundle newExtras = new Bundle(); newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); if (isLoggable) { - Log.v(TAG, "scheduleSync:" - + " delay " + delay + Log.v(TAG, "schedule initialisation Sync:" + + ", delay until " + delayUntil + + ", run by " + 0 + ", source " + source + ", account " + account + ", authority " + authority @@ -694,13 +886,15 @@ public class SyncManager { } scheduleSyncOperation( new SyncOperation(account.account, account.userId, reason, source, - authority, newExtras, 0, backoffTime, delayUntil, - allowParallelSyncs)); + authority, newExtras, 0 /* immediate */, 0 /* No flex time*/, + backoffTime, delayUntil, allowParallelSyncs)); } if (!onlyThoseWithUnkownSyncableState) { if (isLoggable) { Log.v(TAG, "scheduleSync:" - + " delay " + delay + + " delay until " + delayUntil + + " run by " + runtimeMillis + + " flex " + beforeRuntimeMillis + ", source " + source + ", account " + account + ", authority " + authority @@ -708,17 +902,23 @@ public class SyncManager { } scheduleSyncOperation( new SyncOperation(account.account, account.userId, reason, source, - authority, extras, delay, backoffTime, delayUntil, - allowParallelSyncs)); + authority, extras, runtimeMillis, beforeRuntimeMillis, + backoffTime, delayUntil, allowParallelSyncs)); } } } } + /** + * Schedule sync based on local changes to a provider. Occurs within interval + * [LOCAL_SYNC_DELAY, 2*LOCAL_SYNC_DELAY]. + */ public void scheduleLocalSync(Account account, int userId, int reason, String authority) { final Bundle extras = new Bundle(); extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true); - scheduleSync(account, userId, reason, authority, extras, LOCAL_SYNC_DELAY, + scheduleSync(account, userId, reason, authority, extras, + LOCAL_SYNC_DELAY /* earliest run time */, + 2 * LOCAL_SYNC_DELAY /* latest sync time. */, false /* onlyThoseWithUnkownSyncableState */); } @@ -775,6 +975,7 @@ public class SyncManager { } class SyncAlarmIntentReceiver extends BroadcastReceiver { + @Override public void onReceive(Context context, Intent intent) { mHandleAlarmWakeLock.acquire(); sendSyncAlarmMessage(); @@ -943,11 +1144,13 @@ public class SyncManager { Log.d(TAG, "retrying sync operation that failed because there was already a " + "sync in progress: " + operation); } - scheduleSyncOperation(new SyncOperation(operation.account, operation.userId, + scheduleSyncOperation( + new SyncOperation( + operation.account, operation.userId, operation.reason, operation.syncSource, operation.authority, operation.extras, - DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS * 1000, + DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS * 1000, operation.flexTime, operation.backoff, operation.delayUntil, operation.allowParallelSyncs)); } else if (syncResult.hasSoftError()) { if (isLoggable) { @@ -977,7 +1180,8 @@ public class SyncManager { final Account[] accounts = AccountManagerService.getSingleton().getAccounts(userId); for (Account account : accounts) { scheduleSync(account, userId, SyncOperation.REASON_USER_START, null, null, - 0 /* no delay */, true /* onlyThoseWithUnknownSyncableState */); + 0 /* no delay */, 0 /* No flex */, + true /* onlyThoseWithUnknownSyncableState */); } sendCheckAlarmsMessage(); @@ -1204,7 +1408,10 @@ public class SyncManager { synchronized (mSyncQueue) { sb.setLength(0); mSyncQueue.dump(sb); + // Dump Pending Operations. + getSyncStorageEngine().dumpPendingOperations(sb); } + pw.println(); pw.print(sb.toString()); @@ -1271,12 +1478,15 @@ public class SyncManager { for (int i = 0; i < settings.periodicSyncs.size(); i++) { - final Pair<Bundle, Long> pair = settings.periodicSyncs.get(i); - final String period = String.valueOf(pair.second); - final String extras = pair.first.size() > 0 ? " " + pair.first.toString() : ""; - final String next = formatTime(status.getPeriodicSyncTime(i) - + pair.second * 1000); - table.set(row + i * 2, 12, period + extras); + final PeriodicSync sync = settings.periodicSyncs.get(i); + final String period = + String.format("[p:%d s, f: %d s]", sync.period, sync.flexTime); + final String extras = + sync.extras.size() > 0 ? + sync.extras.toString() : "Bundle[]"; + final String next = "Next sync: " + formatTime(status.getPeriodicSyncTime(i) + + sync.period * 1000); + table.set(row + i * 2, 12, period + " " + extras); table.set(row + i * 2 + 1, 12, next); } @@ -1810,6 +2020,7 @@ public class SyncManager { super(looper); } + @Override public void handleMessage(Message msg) { long earliestFuturePollTime = Long.MAX_VALUE; long nextPendingSyncTime = Long.MAX_VALUE; @@ -1827,7 +2038,7 @@ public class SyncManager { earliestFuturePollTime = scheduleReadyPeriodicSyncs(); switch (msg.what) { case SyncHandler.MESSAGE_CANCEL: { - Pair<Account, String> payload = (Pair<Account, String>)msg.obj; + Pair<Account, String> payload = (Pair<Account, String>) msg.obj; if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CANCEL: " + payload.first + ", " + payload.second); @@ -1934,6 +2145,10 @@ public class SyncManager { * in milliseconds since boot */ private long scheduleReadyPeriodicSyncs() { + final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); + if (isLoggable) { + Log.v(TAG, "scheduleReadyPeriodicSyncs"); + } final boolean backgroundDataUsageAllowed = getConnectivityManager().getBackgroundDataSetting(); long earliestFuturePollTime = Long.MAX_VALUE; @@ -1973,37 +2188,59 @@ public class SyncManager { } for (int i = 0, N = authorityInfo.periodicSyncs.size(); i < N; i++) { - final Bundle extras = authorityInfo.periodicSyncs.get(i).first; - final Long periodInMillis = authorityInfo.periodicSyncs.get(i).second * 1000; - // Skip if the period is invalid + final PeriodicSync sync = authorityInfo.periodicSyncs.get(i); + final Bundle extras = sync.extras; + final Long periodInMillis = sync.period * 1000; + final Long flexInMillis = sync.flexTime * 1000; + // Skip if the period is invalid. if (periodInMillis <= 0) { continue; } - // find when this periodic sync was last scheduled to run + // Find when this periodic sync was last scheduled to run. final long lastPollTimeAbsolute = status.getPeriodicSyncTime(i); - + final long shiftedLastPollTimeAbsolute = + (0 < lastPollTimeAbsolute - mSyncRandomOffsetMillis) ? + (lastPollTimeAbsolute - mSyncRandomOffsetMillis) : 0; long remainingMillis - = periodInMillis - (shiftedNowAbsolute % periodInMillis); - + = periodInMillis - (shiftedNowAbsolute % periodInMillis); + long timeSinceLastRunMillis + = (nowAbsolute - lastPollTimeAbsolute); + // Schedule this periodic sync to run early if it's close enough to its next + // runtime, and far enough from its last run time. + // If we are early, there will still be time remaining in this period. + boolean runEarly = remainingMillis <= flexInMillis + && timeSinceLastRunMillis > periodInMillis - flexInMillis; + if (isLoggable) { + Log.v(TAG, "sync: " + i + " for " + authorityInfo.authority + "." + + " period: " + (periodInMillis) + + " flex: " + (flexInMillis) + + " remaining: " + (remainingMillis) + + " time_since_last: " + timeSinceLastRunMillis + + " last poll absol: " + lastPollTimeAbsolute + + " last poll shifed: " + shiftedLastPollTimeAbsolute + + " shifted now: " + shiftedNowAbsolute + + " run_early: " + runEarly); + } /* * Sync scheduling strategy: Set the next periodic sync * based on a random offset (in seconds). Also sync right * now if any of the following cases hold and mark it as * having been scheduled - * Case 1: This sync is ready to run - * now. + * Case 1: This sync is ready to run now. * Case 2: If the lastPollTimeAbsolute is in the * future, sync now and reinitialize. This can happen for * example if the user changed the time, synced and changed * back. * Case 3: If we failed to sync at the last scheduled - * time + * time. + * Case 4: This sync is close enough to the time that we can schedule it. */ - if (remainingMillis == periodInMillis // Case 1 + if (runEarly // Case 4 + || remainingMillis == periodInMillis // Case 1 || lastPollTimeAbsolute > nowAbsolute // Case 2 - || (nowAbsolute - lastPollTimeAbsolute - >= periodInMillis)) { // Case 3 + || timeSinceLastRunMillis >= periodInMillis) { // Case 3 // Sync now + final Pair<Long, Long> backoff = mSyncStorageEngine.getBackoff( authorityInfo.account, authorityInfo.userId, authorityInfo.authority); @@ -2015,24 +2252,29 @@ public class SyncManager { if (syncAdapterInfo == null) { continue; } + mSyncStorageEngine.setPeriodicSyncTime(authorityInfo.ident, + authorityInfo.periodicSyncs.get(i), nowAbsolute); scheduleSyncOperation( new SyncOperation(authorityInfo.account, authorityInfo.userId, SyncOperation.REASON_PERIODIC, SyncStorageEngine.SOURCE_PERIODIC, - authorityInfo.authority, extras, 0 /* delay */, + authorityInfo.authority, extras, + 0 /* runtime */, 0 /* flex */, backoff != null ? backoff.first : 0, mSyncStorageEngine.getDelayUntilTime( authorityInfo.account, authorityInfo.userId, authorityInfo.authority), syncAdapterInfo.type.allowParallelSyncs())); - mSyncStorageEngine.setPeriodicSyncTime(authorityInfo.ident, - authorityInfo.periodicSyncs.get(i), nowAbsolute); + + } + // Compute when this periodic sync should next run. + long nextPollTimeAbsolute; + if (runEarly) { + // Add the time remaining so we don't get out of phase. + nextPollTimeAbsolute = nowAbsolute + periodInMillis + remainingMillis; + } else { + nextPollTimeAbsolute = nowAbsolute + remainingMillis; } - // Compute when this periodic sync should next run - final long nextPollTimeAbsolute = nowAbsolute + remainingMillis; - - // remember this time if it is earlier than - // earliestFuturePollTime if (nextPollTimeAbsolute < earliestFuturePollTime) { earliestFuturePollTime = nextPollTimeAbsolute; } @@ -2044,10 +2286,9 @@ public class SyncManager { } // convert absolute time to elapsed time - return SystemClock.elapsedRealtime() - + ((earliestFuturePollTime < nowAbsolute) - ? 0 - : (earliestFuturePollTime - nowAbsolute)); + return SystemClock.elapsedRealtime() + + ((earliestFuturePollTime < nowAbsolute) ? + 0 : (earliestFuturePollTime - nowAbsolute)); } private long maybeStartNextSyncLocked() { @@ -2097,8 +2338,8 @@ public class SyncManager { Log.v(TAG, "build the operation array, syncQueue size is " + mSyncQueue.getOperations().size()); } - final Iterator<SyncOperation> operationIterator = mSyncQueue.getOperations() - .iterator(); + final Iterator<SyncOperation> operationIterator = + mSyncQueue.getOperations().iterator(); final ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); @@ -2106,40 +2347,52 @@ public class SyncManager { while (operationIterator.hasNext()) { final SyncOperation op = operationIterator.next(); - // drop the sync if the account of this operation no longer exists + // Drop the sync if the account of this operation no longer exists. if (!containsAccountAndUser(accounts, op.account, op.userId)) { operationIterator.remove(); mSyncStorageEngine.deleteFromPending(op.pendingOperation); + if (isLoggable) { + Log.v(TAG, " Dropping sync operation: account doesn't exist."); + } continue; } - // drop this sync request if it isn't syncable + // Drop this sync request if it isn't syncable. int syncableState = getIsSyncable( op.account, op.userId, op.authority); if (syncableState == 0) { operationIterator.remove(); mSyncStorageEngine.deleteFromPending(op.pendingOperation); + if (isLoggable) { + Log.v(TAG, " Dropping sync operation: isSyncable == 0."); + } continue; } - // if the user in not running, drop the request + // If the user is not running, drop the request. if (!activityManager.isUserRunning(op.userId)) { final UserInfo userInfo = mUserManager.getUserInfo(op.userId); if (userInfo == null) { removedUsers.add(op.userId); } + if (isLoggable) { + Log.v(TAG, " Dropping sync operation: user not running."); + } continue; } - // if the next run time is in the future, meaning there are no syncs ready - // to run, return the time - if (op.effectiveRunTime > now) { + // If the next run time is in the future, even given the flexible scheduling, + // return the time. + if (op.effectiveRunTime - op.flexTime > now) { if (nextReadyToRunTime > op.effectiveRunTime) { nextReadyToRunTime = op.effectiveRunTime; } + if (isLoggable) { + Log.v(TAG, " Dropping sync operation: Sync too far in future."); + } continue; } - + // TODO: change this behaviour for non-registered syncs. final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo; syncAdapterInfo = mSyncAdapters.getServiceInfo( SyncAdapterType.newKey(op.authority, op.account.type), op.userId); @@ -2180,7 +2433,7 @@ public class SyncManager { } // find the next operation to dispatch, if one is ready - // iterate from the top, keep issuing (while potentially cancelling existing syncs) + // iterate from the top, keep issuing (while potentially canceling existing syncs) // until the quotas are filled. // once the quotas are filled iterate once more to find when the next one would be // (also considering pre-emption reasons). @@ -2460,11 +2713,13 @@ public class SyncManager { } if (syncResult != null && syncResult.fullSyncRequested) { - scheduleSyncOperation(new SyncOperation(syncOperation.account, syncOperation.userId, - syncOperation.reason, - syncOperation.syncSource, syncOperation.authority, new Bundle(), 0, - syncOperation.backoff, syncOperation.delayUntil, - syncOperation.allowParallelSyncs)); + scheduleSyncOperation( + new SyncOperation(syncOperation.account, syncOperation.userId, + syncOperation.reason, + syncOperation.syncSource, syncOperation.authority, new Bundle(), + 0 /* delay */, 0 /* flex */, + syncOperation.backoff, syncOperation.delayUntil, + syncOperation.allowParallelSyncs)); } // no need to schedule an alarm, as that will be done by our caller. } @@ -2637,6 +2892,8 @@ public class SyncManager { final boolean alarmIsActive = mAlarmScheduleTime != null; final boolean needAlarm = alarmTime != Long.MAX_VALUE; if (needAlarm) { + // Need the alarm if it's currently not set, or if our time is before the currently + // set time. if (!alarmIsActive || alarmTime < mAlarmScheduleTime) { shouldSet = true; } @@ -2644,7 +2901,7 @@ public class SyncManager { shouldCancel = alarmIsActive; } - // set or cancel the alarm as directed + // Set or cancel the alarm as directed. ensureAlarmService(); if (shouldSet) { if (Log.isLoggable(TAG, Log.VERBOSE)) { diff --git a/services/java/com/android/server/content/SyncOperation.java b/services/java/com/android/server/content/SyncOperation.java index eaad982..b688535 100644 --- a/services/java/com/android/server/content/SyncOperation.java +++ b/services/java/com/android/server/content/SyncOperation.java @@ -18,13 +18,18 @@ package com.android.server.content; import android.accounts.Account; import android.content.pm.PackageManager; +import android.content.ComponentName; import android.content.ContentResolver; +import android.content.SyncRequest; import android.os.Bundle; import android.os.SystemClock; +import android.util.Pair; /** * Value type that represents a sync operation. - * @hide + * TODO: This is the class to flesh out with all the scheduling data - metered/unmetered, + * transfer-size, etc. + * {@hide} */ public class SyncOperation implements Comparable { public static final int REASON_BACKGROUND_DATA_SETTINGS_CHANGED = -1; @@ -32,7 +37,9 @@ public class SyncOperation implements Comparable { public static final int REASON_SERVICE_CHANGED = -3; public static final int REASON_PERIODIC = -4; public static final int REASON_IS_SYNCABLE = -5; + /** Sync started because it has just been set to sync automatically. */ public static final int REASON_SYNC_AUTO = -6; + /** Sync started because master sync automatically has been set to true. */ public static final int REASON_MASTER_SYNC_AUTO = -7; public static final int REASON_USER_START = -8; @@ -47,75 +54,143 @@ public class SyncOperation implements Comparable { "UserStart", }; + /** Account info to identify a SyncAdapter registered with the system. */ public final Account account; + /** Authority info to identify a SyncAdapter registered with the system. */ + public final String authority; + /** Service to which this operation will bind to perform the sync. */ + public final ComponentName service; public final int userId; public final int reason; public int syncSource; - public String authority; public final boolean allowParallelSyncs; public Bundle extras; public final String key; - public long earliestRunTime; public boolean expedited; public SyncStorageEngine.PendingOperation pendingOperation; + /** Elapsed real time in millis at which to run this sync. */ + public long latestRunTime; + /** Set by the SyncManager in order to delay retries. */ public Long backoff; + /** Specified by the adapter to delay subsequent sync operations. */ public long delayUntil; + /** + * Elapsed real time in millis when this sync will be run. + * Depends on max(backoff, latestRunTime, and delayUntil). + */ public long effectiveRunTime; + /** Amount of time before {@link effectiveRunTime} from which this sync can run. */ + public long flexTime; public SyncOperation(Account account, int userId, int reason, int source, String authority, - Bundle extras, long delayInMs, long backoff, long delayUntil, - boolean allowParallelSyncs) { + Bundle extras, long runTimeFromNow, long flexTime, long backoff, + long delayUntil, boolean allowParallelSyncs) { + this.service = null; this.account = account; + this.authority = authority; this.userId = userId; this.reason = reason; this.syncSource = source; - this.authority = authority; this.allowParallelSyncs = allowParallelSyncs; this.extras = new Bundle(extras); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_UPLOAD); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_MANUAL); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_EXPEDITED); - removeFalseExtra(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS); + cleanBundle(this.extras); this.delayUntil = delayUntil; this.backoff = backoff; final long now = SystemClock.elapsedRealtime(); - if (delayInMs < 0) { + // Check the extras bundle. Must occur after we set the internal bundle. + if (runTimeFromNow < 0 || isExpedited()) { this.expedited = true; - this.earliestRunTime = now; + this.latestRunTime = now; + this.flexTime = 0; } else { this.expedited = false; - this.earliestRunTime = now + delayInMs; + this.latestRunTime = now + runTimeFromNow; + this.flexTime = flexTime; } updateEffectiveRunTime(); this.key = toKey(); } - private void removeFalseExtra(String extraName) { - if (!extras.getBoolean(extraName, false)) { - extras.remove(extraName); + public SyncOperation(SyncRequest request, int userId, int reason, int source, long backoff, + long delayUntil, boolean allowParallelSyncs) { + if (request.hasAuthority()) { + Pair<Account, String> providerInfo = request.getProviderInfo(); + this.account = providerInfo.first; + this.authority = providerInfo.second; + this.service = null; + } else { + this.service = request.getService(); + this.account = null; + this.authority = null; } + this.userId = userId; + this.reason = reason; + this.syncSource = source; + this.allowParallelSyncs = allowParallelSyncs; + this.extras = new Bundle(extras); + cleanBundle(this.extras); + this.delayUntil = delayUntil; + this.backoff = backoff; + final long now = SystemClock.elapsedRealtime(); + if (request.isExpedited()) { + this.expedited = true; + this.latestRunTime = now; + this.flexTime = 0; + } else { + this.expedited = false; + this.latestRunTime = now + (request.getSyncRunTime() * 1000); + this.flexTime = request.getSyncFlexTime() * 1000; + } + updateEffectiveRunTime(); + this.key = toKey(); } + /** + * Make sure the bundle attached to this SyncOperation doesn't have unnecessary + * flags set. + * @param bundle to clean. + */ + private void cleanBundle(Bundle bundle) { + removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_UPLOAD); + removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_MANUAL); + removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS); + removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF); + removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY); + removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS); + removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_EXPEDITED); + removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS); + removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_ALLOW_METERED); + + // Remove Config data. + bundle.remove(ContentResolver.SYNC_EXTRAS_EXPECTED_UPLOAD); + bundle.remove(ContentResolver.SYNC_EXTRAS_EXPECTED_DOWNLOAD); + } + + private void removeFalseExtra(Bundle bundle, String extraName) { + if (!bundle.getBoolean(extraName, false)) { + bundle.remove(extraName); + } + } + + /** Only used to immediately reschedule a sync. */ SyncOperation(SyncOperation other) { + this.service = other.service; this.account = other.account; + this.authority = other.authority; this.userId = other.userId; this.reason = other.reason; this.syncSource = other.syncSource; - this.authority = other.authority; this.extras = new Bundle(other.extras); this.expedited = other.expedited; - this.earliestRunTime = SystemClock.elapsedRealtime(); + this.latestRunTime = SystemClock.elapsedRealtime(); + this.flexTime = 0L; this.backoff = other.backoff; - this.delayUntil = other.delayUntil; this.allowParallelSyncs = other.allowParallelSyncs; this.updateEffectiveRunTime(); this.key = toKey(); } + @Override public String toString() { return dump(null, true); } @@ -131,8 +206,8 @@ public class SyncOperation implements Comparable { .append(authority) .append(", ") .append(SyncStorageEngine.SOURCES[syncSource]) - .append(", earliestRunTime ") - .append(earliestRunTime); + .append(", latestRunTime ") + .append(latestRunTime); if (expedited) { sb.append(", EXPEDITED"); } @@ -170,23 +245,38 @@ public class SyncOperation implements Comparable { } } + public boolean isMetered() { + return extras.getBoolean(ContentResolver.SYNC_EXTRAS_ALLOW_METERED, false); + } + public boolean isInitialization() { return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false); } public boolean isExpedited() { - return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false); + return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false) || expedited; } public boolean ignoreBackoff() { return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false); } + /** Changed in V3. */ private String toKey() { StringBuilder sb = new StringBuilder(); - sb.append("authority: ").append(authority); - sb.append(" account {name=" + account.name + ", user=" + userId + ", type=" + account.type - + "}"); + if (service == null) { + sb.append("authority: ").append(authority); + sb.append(" account {name=" + account.name + ", user=" + userId + ", type=" + account.type + + "}"); + } else { + sb.append("service {package=" ) + .append(service.getPackageName()) + .append(" user=") + .append(userId) + .append(", class=") + .append(service.getClassName()) + .append("}"); + } sb.append(" extras: "); extrasToStringBuilder(extras, sb); return sb.toString(); @@ -200,25 +290,40 @@ public class SyncOperation implements Comparable { sb.append("]"); } + /** + * Update the effective run time of this Operation based on latestRunTime (specified at + * creation time of sync), delayUntil (specified by SyncAdapter), or backoff (specified by + * SyncManager on soft failures). + */ public void updateEffectiveRunTime() { - effectiveRunTime = ignoreBackoff() - ? earliestRunTime - : Math.max( - Math.max(earliestRunTime, delayUntil), - backoff); + // Regardless of whether we're in backoff or honouring a delayUntil, we still incorporate + // the flex time provided by the developer. + effectiveRunTime = ignoreBackoff() ? + latestRunTime : + Math.max(Math.max(latestRunTime, delayUntil), backoff); } + /** + * If two SyncOperation intervals are disjoint, the smaller is the interval that occurs before. + * If the intervals overlap, the two are considered equal. + */ + @Override public int compareTo(Object o) { - SyncOperation other = (SyncOperation)o; - + SyncOperation other = (SyncOperation) o; if (expedited != other.expedited) { return expedited ? -1 : 1; } - - if (effectiveRunTime == other.effectiveRunTime) { + long x1 = effectiveRunTime - flexTime; + long y1 = effectiveRunTime; + long x2 = other.effectiveRunTime - other.flexTime; + long y2 = other.effectiveRunTime; + // Overlapping intervals. + if ((x1 <= y2 && x1 >= x2) || (x2 <= y1 && x2 >= x1)) { return 0; } - - return effectiveRunTime < other.effectiveRunTime ? -1 : 1; + if (x1 < x2 && y1 < x2) { + return -1; + } + return 1; } } diff --git a/services/java/com/android/server/content/SyncQueue.java b/services/java/com/android/server/content/SyncQueue.java index 951e92c..6f3fe6e 100644 --- a/services/java/com/android/server/content/SyncQueue.java +++ b/services/java/com/android/server/content/SyncQueue.java @@ -73,7 +73,7 @@ public class SyncQueue { } SyncOperation syncOperation = new SyncOperation( op.account, op.userId, op.reason, op.syncSource, op.authority, op.extras, - 0 /* delay */, backoff != null ? backoff.first : 0, + 0 /* delay */, 0 /* flex */, backoff != null ? backoff.first : 0, mSyncStorageEngine.getDelayUntilTime(op.account, op.userId, op.authority), syncAdapterInfo.type.allowParallelSyncs()); syncOperation.expedited = op.expedited; @@ -86,35 +86,40 @@ public class SyncQueue { return add(operation, null /* this is not coming from the database */); } + /** + * Adds a SyncOperation to the queue and creates a PendingOperation object to track that sync. + * If an operation is added that already exists, the existing operation is updated if the newly + * added operation occurs before (or the interval overlaps). + */ private boolean add(SyncOperation operation, SyncStorageEngine.PendingOperation pop) { - // - if an operation with the same key exists and this one should run earlier, - // update the earliestRunTime of the existing to the new time - // - if an operation with the same key exists and if this one should run - // later, ignore it - // - if no operation exists then add the new one + // If an operation with the same key exists and this one should run sooner/overlaps, + // replace the run interval of the existing operation with this new one. + // Complications: what if the existing operation is expedited but the new operation has an + // earlier run time? Will not be a problem for periodic syncs (no expedited flag), and for + // one-off syncs we only change it if the new sync is sooner. final String operationKey = operation.key; final SyncOperation existingOperation = mOperationsMap.get(operationKey); if (existingOperation != null) { boolean changed = false; - if (existingOperation.expedited == operation.expedited) { - final long newRunTime = - Math.min(existingOperation.earliestRunTime, operation.earliestRunTime); - if (existingOperation.earliestRunTime != newRunTime) { - existingOperation.earliestRunTime = newRunTime; - changed = true; - } - } else { - if (operation.expedited) { - existingOperation.expedited = true; - changed = true; - } + if (operation.compareTo(existingOperation) <= 0 ) { + existingOperation.expedited = operation.expedited; + long newRunTime = + Math.min(existingOperation.latestRunTime, operation.latestRunTime); + // Take smaller runtime. + existingOperation.latestRunTime = newRunTime; + // Take newer flextime. + existingOperation.flexTime = operation.flexTime; + changed = true; } return changed; } operation.pendingOperation = pop; + // Don't update the PendingOp if one already exists. This really is just a placeholder, + // no actual scheduling info is placed here. + // TODO: Change this to support service components. if (operation.pendingOperation == null) { pop = new SyncStorageEngine.PendingOperation( operation.account, operation.userId, operation.reason, operation.syncSource, diff --git a/services/java/com/android/server/content/SyncStorageEngine.java b/services/java/com/android/server/content/SyncStorageEngine.java index c4dc575..0b99fca 100644 --- a/services/java/com/android/server/content/SyncStorageEngine.java +++ b/services/java/com/android/server/content/SyncStorageEngine.java @@ -18,6 +18,7 @@ package com.android.server.content; import android.accounts.Account; import android.accounts.AccountAndUser; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.ISyncStatusObserver; @@ -44,7 +45,6 @@ import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastXmlSerializer; -import com.android.server.content.SyncStorageEngine.AuthorityInfo; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -70,8 +70,8 @@ import java.util.TimeZone; public class SyncStorageEngine extends Handler { private static final String TAG = "SyncManager"; - private static final boolean DEBUG = false; - private static final boolean DEBUG_FILE = false; + private static final boolean DEBUG = true; + private static final boolean DEBUG_FILE = true; private static final String XML_ATTR_NEXT_AUTHORITY_ID = "nextAuthorityId"; private static final String XML_ATTR_LISTEN_FOR_TICKLES = "listen-for-tickles"; @@ -80,8 +80,15 @@ public class SyncStorageEngine extends Handler { private static final String XML_ATTR_USER = "user"; private static final String XML_TAG_LISTEN_FOR_TICKLES = "listenForTickles"; + /** Default time for a periodic sync. */ private static final long DEFAULT_POLL_FREQUENCY_SECONDS = 60 * 60 * 24; // One day + /** Percentage of period that is flex by default, if no flex is set. */ + private static final double DEFAULT_FLEX_PERCENT_SYNC = 0.04; + + /** Lower bound on sync time from which we assign a default flex time. */ + private static final long DEFAULT_MIN_FLEX_ALLOWED_SECS = 5; + @VisibleForTesting static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4; @@ -154,12 +161,13 @@ public class SyncStorageEngine extends Handler { final int syncSource; final String authority; final Bundle extras; // note: read-only. + final ComponentName serviceName; final boolean expedited; int authorityId; byte[] flatExtras; - PendingOperation(Account account, int userId, int reason,int source, + PendingOperation(Account account, int userId, int reason, int source, String authority, Bundle extras, boolean expedited) { this.account = account; this.userId = userId; @@ -169,6 +177,7 @@ public class SyncStorageEngine extends Handler { this.extras = extras != null ? new Bundle(extras) : extras; this.expedited = expedited; this.authorityId = -1; + this.serviceName = null; } PendingOperation(PendingOperation other) { @@ -180,6 +189,7 @@ public class SyncStorageEngine extends Handler { this.extras = other.extras; this.authorityId = other.authorityId; this.expedited = other.expedited; + this.serviceName = other.serviceName; } } @@ -194,6 +204,7 @@ public class SyncStorageEngine extends Handler { } public static class AuthorityInfo { + final ComponentName service; final Account account; final int userId; final String authority; @@ -203,7 +214,7 @@ public class SyncStorageEngine extends Handler { long backoffTime; long backoffDelay; long delayUntil; - final ArrayList<Pair<Bundle, Long>> periodicSyncs; + final ArrayList<PeriodicSync> periodicSyncs; /** * Copy constructor for making deep-ish copies. Only the bundles stored @@ -215,30 +226,70 @@ public class SyncStorageEngine extends Handler { account = toCopy.account; userId = toCopy.userId; authority = toCopy.authority; + service = toCopy.service; ident = toCopy.ident; enabled = toCopy.enabled; syncable = toCopy.syncable; backoffTime = toCopy.backoffTime; backoffDelay = toCopy.backoffDelay; delayUntil = toCopy.delayUntil; - periodicSyncs = new ArrayList<Pair<Bundle, Long>>(); - for (Pair<Bundle, Long> sync : toCopy.periodicSyncs) { + periodicSyncs = new ArrayList<PeriodicSync>(); + for (PeriodicSync sync : toCopy.periodicSyncs) { // Still not a perfect copy, because we are just copying the mappings. - periodicSyncs.add(Pair.create(new Bundle(sync.first), sync.second)); + periodicSyncs.add(new PeriodicSync(sync)); } } + /** + * Create an authority with one periodic sync scheduled with an empty bundle and syncing + * every day. An empty bundle is considered equal to any other bundle see + * {@link PeriodicSync.syncExtrasEquals}. + * @param account Account that this authority syncs. + * @param userId which user this sync is registered for. + * @param userId user for which this authority is registered. + * @param ident id of this authority. + */ AuthorityInfo(Account account, int userId, String authority, int ident) { this.account = account; this.userId = userId; this.authority = authority; + this.service = null; this.ident = ident; enabled = SYNC_ENABLED_DEFAULT; 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)); + periodicSyncs = new ArrayList<PeriodicSync>(); + // Old version adds one periodic sync a day. + periodicSyncs.add(new PeriodicSync(account, authority, + new Bundle(), + DEFAULT_POLL_FREQUENCY_SECONDS, + calculateDefaultFlexTime(DEFAULT_POLL_FREQUENCY_SECONDS))); + } + + /** + * Create an authority with one periodic sync scheduled with an empty bundle and syncing + * every day using a sync service. + * @param cname sync service identifier. + * @param userId user for which this authority is registered. + * @param ident id of this authority. + */ + AuthorityInfo(ComponentName cname, int userId, int ident) { + this.account = null; + this.userId = userId; + this.authority = null; + this.service = cname; + this.ident = ident; + // Sync service is always enabled. + enabled = true; + 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<PeriodicSync>(); + periodicSyncs.add(new PeriodicSync(account, authority, + new Bundle(), + DEFAULT_POLL_FREQUENCY_SECONDS, + calculateDefaultFlexTime(DEFAULT_POLL_FREQUENCY_SECONDS))); } } @@ -304,6 +355,10 @@ public class SyncStorageEngine extends Handler { private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners = new RemoteCallbackList<ISyncStatusObserver>(); + /** Reverse mapping for component name -> <userid -> authority id>. */ + private final HashMap<ComponentName, SparseArray<AuthorityInfo>> mServices = + new HashMap<ComponentName, SparseArray<AuthorityInfo>>(); + private int mNextAuthorityId = 0; // We keep 4 weeks of stats. @@ -436,6 +491,28 @@ public class SyncStorageEngine extends Handler { } } + /** + * Figure out a reasonable flex time for cases where none is provided (old api calls). + * @param syncTimeSeconds requested sync time from now. + * @return amount of seconds before syncTimeSeconds that the sync can occur. + * I.e. + * earliest_sync_time = syncTimeSeconds - calculateDefaultFlexTime(syncTimeSeconds) + * The flex time is capped at a percentage of the {@link DEFAULT_POLL_FREQUENCY_SECONDS}. + */ + public static long calculateDefaultFlexTime(long syncTimeSeconds) { + if (syncTimeSeconds < DEFAULT_MIN_FLEX_ALLOWED_SECS) { + // Small enough sync request time that we don't add flex time - developer probably + // wants to wait for an operation to occur before syncing so we honour the + // request time. + return 0L; + } else if (syncTimeSeconds < DEFAULT_POLL_FREQUENCY_SECONDS) { + return (long) (syncTimeSeconds * DEFAULT_FLEX_PERCENT_SYNC); + } else { + // Large enough sync request time that we cap the flex time. + return (long) (DEFAULT_POLL_FREQUENCY_SECONDS * DEFAULT_FLEX_PERCENT_SYNC); + } + } + private void reportChange(int which) { ArrayList<ISyncStatusObserver> reports = null; synchronized (mAuthorities) { @@ -553,8 +630,8 @@ public class SyncStorageEngine extends Handler { + ", user " + userId + " -> " + syncable); } synchronized (mAuthorities) { - AuthorityInfo authority = getOrCreateAuthorityLocked(account, userId, providerName, -1, - false); + AuthorityInfo authority = + getOrCreateAuthorityLocked(account, userId, providerName, -1, false); if (authority.syncable == syncable) { if (DEBUG) { Log.d(TAG, "setIsSyncable: already set to " + syncable + ", doing nothing"); @@ -689,62 +766,65 @@ public class SyncStorageEngine extends Handler { } } - private void updateOrRemovePeriodicSync(Account account, int userId, String providerName, - Bundle extras, - long period, boolean add) { - if (period <= 0) { - period = 0; - } - if (extras == null) { - extras = new Bundle(); - } + private void updateOrRemovePeriodicSync(PeriodicSync toUpdate, int userId, boolean add) { if (DEBUG) { - Log.v(TAG, "addOrRemovePeriodicSync: " + account + ", user " + userId - + ", provider " + providerName - + " -> period " + period + ", extras " + extras); + Log.v(TAG, "addOrRemovePeriodicSync: " + toUpdate.account + ", user " + userId + + ", provider " + toUpdate.authority + + " -> period " + toUpdate.period + ", extras " + toUpdate.extras); } synchronized (mAuthorities) { + if (toUpdate.period <= 0 && add) { + Log.e(TAG, "period < 0, should never happen in updateOrRemovePeriodicSync: add-" + add); + } + if (toUpdate.extras == null) { + Log.e(TAG, "period < 0, should never happen in updateOrRemovePeriodicSync: add-" + add); + } try { AuthorityInfo authority = - getOrCreateAuthorityLocked(account, userId, providerName, -1, false); + getOrCreateAuthorityLocked(toUpdate.account, userId, toUpdate.authority, + -1, false); if (add) { - // add this periodic sync if one with the same extras doesn't already - // exist in the periodicSyncs array + // add this periodic sync if an equivalent periodic doesn't already exist. 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 (PeriodicSync.syncExtrasEquals(existingExtras, extras)) { - if (syncInfo.second == period) { + PeriodicSync syncInfo = authority.periodicSyncs.get(i); + if (PeriodicSync.syncExtrasEquals( + toUpdate.extras, + syncInfo.extras)) { + if (toUpdate.period == syncInfo.period && + toUpdate.flexTime == syncInfo.flexTime) { + // Absolutely the same. return; } - authority.periodicSyncs.set(i, Pair.create(extras, period)); + authority.periodicSyncs.set(i, new PeriodicSync(toUpdate)); alreadyPresent = true; break; } } - // if we added an entry to the periodicSyncs array also add an entry to - // the periodic syncs status to correspond to it + // If we added an entry to the periodicSyncs array also add an entry to + // the periodic syncs status to correspond to it. if (!alreadyPresent) { - authority.periodicSyncs.add(Pair.create(extras, period)); + authority.periodicSyncs.add(new PeriodicSync(toUpdate)); SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); status.setPeriodicSyncTime(authority.periodicSyncs.size() - 1, 0); } } else { - // remove any periodic syncs that match the authority and extras + // Remove any periodic syncs that match the authority and extras. SyncStatusInfo status = mSyncStatus.get(authority.ident); boolean changed = false; - Iterator<Pair<Bundle, Long>> iterator = authority.periodicSyncs.iterator(); + Iterator<PeriodicSync> iterator = authority.periodicSyncs.iterator(); int i = 0; while (iterator.hasNext()) { - Pair<Bundle, Long> syncInfo = iterator.next(); - if (PeriodicSync.syncExtrasEquals(syncInfo.first, extras)) { + PeriodicSync syncInfo = iterator.next(); + if (PeriodicSync.syncExtrasEquals(syncInfo.extras, toUpdate.extras)) { iterator.remove(); changed = true; - // if we removed an entry from the periodicSyncs array also + // If we removed an entry from the periodicSyncs array also // remove the corresponding entry from the status if (status != null) { status.removePeriodicSyncTime(i); + } else { + Log.e(TAG, "Tried removing sync status on remove periodic sync but did not find it."); } } else { i++; @@ -763,16 +843,12 @@ public class SyncStorageEngine extends Handler { reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); } - public void addPeriodicSync(Account account, int userId, String providerName, Bundle extras, - long pollFrequency) { - updateOrRemovePeriodicSync(account, userId, providerName, extras, pollFrequency, - true /* add */); + public void addPeriodicSync(PeriodicSync toAdd, int userId) { + updateOrRemovePeriodicSync(toAdd, userId, true /* add */); } - public void removePeriodicSync(Account account, int userId, String providerName, - Bundle extras) { - updateOrRemovePeriodicSync(account, userId, providerName, extras, 0 /* period, ignored */, - false /* remove */); + public void removePeriodicSync(PeriodicSync toRemove, int userId) { + updateOrRemovePeriodicSync(toRemove, userId, false /* remove */); } public List<PeriodicSync> getPeriodicSyncs(Account account, int userId, String providerName) { @@ -781,9 +857,9 @@ public class SyncStorageEngine extends Handler { AuthorityInfo authority = getAuthorityLocked(account, userId, providerName, "getPeriodicSyncs"); if (authority != null) { - for (Pair<Bundle, Long> item : authority.periodicSyncs) { - syncs.add(new PeriodicSync(account, providerName, item.first, - item.second)); + for (PeriodicSync item : authority.periodicSyncs) { + // Copy and send out. Necessary for thread-safety although it's parceled. + syncs.add(new PeriodicSync(item)); } } } @@ -866,7 +942,7 @@ public class SyncStorageEngine extends Handler { op = new PendingOperation(op); op.authorityId = authority.ident; mPendingOperations.add(op); - appendPendingOperationLocked(op); + writePendingOperationsLocked(); SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); status.pending = true; @@ -876,6 +952,14 @@ public class SyncStorageEngine extends Handler { return op; } + /** + * Remove from list of pending operations. If successful, search through list for matching + * authorities. If there are no more pending syncs for the same authority/account/userid, + * update the SyncStatusInfo for that authority(authority here is the internal representation + * of a 'sync operation'. + * @param op + * @return + */ public boolean deleteFromPending(PendingOperation op) { boolean res = false; synchronized (mAuthorities) { @@ -898,7 +982,7 @@ public class SyncStorageEngine extends Handler { AuthorityInfo authority = getAuthorityLocked(op.account, op.userId, op.authority, "deleteFromPending"); if (authority != null) { - if (DEBUG) Log.v(TAG, "removing - " + authority); + if (DEBUG) Log.v(TAG, "removing - " + authority.toString()); final int N = mPendingOperations.size(); boolean morePending = false; for (int i=0; i<N; i++) { @@ -1391,6 +1475,65 @@ public class SyncStorageEngine extends Handler { return authority; } + /** + * Retrieve an authority, returning null if one does not exist. + * + * @param service The service name used for this sync. + * @param userId The user for whom this sync is scheduled. + * @param tag If non-null, this will be used in a log message if the + * requested authority does not exist. + */ + private AuthorityInfo getAuthorityLocked(ComponentName service, int userId, String tag) { + AuthorityInfo authority = mServices.get(service).get(userId); + if (authority == null) { + if (tag != null) { + if (DEBUG) { + Log.v(TAG, tag + " No authority info found for " + service + " for user " + + userId); + } + } + return null; + } + return authority; + } + + /** + * @param cname identifier for the service. + * @param userId for the syncs corresponding to this authority. + * @param ident unique identifier for authority. -1 for none. + * @param doWrite if true, update the accounts.xml file on the disk. + * @return the authority that corresponds to the provided sync service, creating it if none + * exists. + */ + private AuthorityInfo getOrCreateAuthorityLocked(ComponentName cname, int userId, int ident, + boolean doWrite) { + SparseArray<AuthorityInfo> aInfo = mServices.get(cname); + if (aInfo == null) { + aInfo = new SparseArray<AuthorityInfo>(); + mServices.put(cname, aInfo); + } + AuthorityInfo authority = aInfo.get(userId); + if (authority == null) { + if (ident < 0) { + ident = mNextAuthorityId; + mNextAuthorityId++; + doWrite = true; + } + if (DEBUG) { + Log.v(TAG, "created a new AuthorityInfo for " + cname.getPackageName() + + ", " + cname.getClassName() + + ", user: " + userId); + } + authority = new AuthorityInfo(cname, userId, ident); + aInfo.put(userId, authority); + mAuthorities.put(ident, authority); + if (doWrite) { + writeAccountInfoLocked(); + } + } + return authority; + } + private AuthorityInfo getOrCreateAuthorityLocked(Account accountName, int userId, String authorityName, int ident, boolean doWrite) { AccountAndUser au = new AccountAndUser(accountName, userId); @@ -1441,22 +1584,20 @@ public class SyncStorageEngine extends Handler { * authority id and target periodic sync */ public void setPeriodicSyncTime( - int authorityId, Pair<Bundle, Long> targetPeriodicSync, long when) { + int authorityId, PeriodicSync targetPeriodicSync, long when) { boolean found = false; final AuthorityInfo authorityInfo; synchronized (mAuthorities) { authorityInfo = mAuthorities.get(authorityId); for (int i = 0; i < authorityInfo.periodicSyncs.size(); i++) { - Pair<Bundle, Long> periodicSync = authorityInfo.periodicSyncs.get(i); - if (PeriodicSync.syncExtrasEquals(periodicSync.first, targetPeriodicSync.first) - && periodicSync.second == targetPeriodicSync.second) { + PeriodicSync periodicSync = authorityInfo.periodicSyncs.get(i); + if (targetPeriodicSync.equals(periodicSync)) { mSyncStatus.get(authorityId).setPeriodicSyncTime(i, when); found = true; break; } } } - if (!found) { Log.w(TAG, "Ignoring setPeriodicSyncTime request for a sync that does not exist. " + "Authority: " + authorityInfo.authority); @@ -1494,6 +1635,7 @@ public class SyncStorageEngine extends Handler { synchronized (mAuthorities) { mAuthorities.clear(); mAccounts.clear(); + mServices.clear(); mPendingOperations.clear(); mSyncStatus.clear(); mSyncHistory.clear(); @@ -1555,7 +1697,7 @@ public class SyncStorageEngine extends Handler { mMasterSyncAutomatically.put(0, listen == null || Boolean.parseBoolean(listen)); eventType = parser.next(); AuthorityInfo authority = null; - Pair<Bundle, Long> periodicSync = null; + PeriodicSync periodicSync = null; do { if (eventType == XmlPullParser.START_TAG) { tagName = parser.getName(); @@ -1575,7 +1717,7 @@ public class SyncStorageEngine extends Handler { } } else if (parser.getDepth() == 4 && periodicSync != null) { if ("extra".equals(tagName)) { - parseExtra(parser, periodicSync); + parseExtra(parser, periodicSync.extras); } } } @@ -1669,8 +1811,7 @@ public class SyncStorageEngine extends Handler { AuthorityInfo authority = null; int id = -1; try { - id = Integer.parseInt(parser.getAttributeValue( - null, "id")); + id = Integer.parseInt(parser.getAttributeValue(null, "id")); } catch (NumberFormatException e) { Log.e(TAG, "error parsing the id of the authority", e); } catch (NullPointerException e) { @@ -1683,6 +1824,8 @@ public class SyncStorageEngine extends Handler { String accountName = parser.getAttributeValue(null, "account"); String accountType = parser.getAttributeValue(null, "type"); String user = parser.getAttributeValue(null, XML_ATTR_USER); + String packageName = parser.getAttributeValue(null, "package"); + String className = parser.getAttributeValue(null, "class"); int userId = user == null ? 0 : Integer.parseInt(user); if (accountType == null) { accountType = "com.google"; @@ -1695,12 +1838,19 @@ public class SyncStorageEngine extends Handler { + " enabled=" + enabled + " syncable=" + syncable); if (authority == null) { - if (DEBUG_FILE) Log.v(TAG, "Creating entry"); - authority = getOrCreateAuthorityLocked( - new Account(accountName, accountType), userId, authorityName, id, false); + if (DEBUG_FILE) { + Log.v(TAG, "Creating entry"); + } + if (accountName != null && accountType != null) { + authority = getOrCreateAuthorityLocked( + new Account(accountName, accountType), userId, authorityName, id, false); + } else { + authority = getOrCreateAuthorityLocked( + new ComponentName(packageName, className), userId, id, false); + } // If the version is 0 then we are upgrading from a file format that did not // know about periodic syncs. In that case don't clear the list since we - // want the default, which is a daily periodioc sync. + // want the default, which is a daily periodic sync. // Otherwise clear out this default list since we will populate it later with // the periodic sync descriptions that are read from the configuration file. if (version > 0) { @@ -1722,14 +1872,18 @@ public class SyncStorageEngine extends Handler { + " syncable=" + syncable); } } - return authority; } - private Pair<Bundle, Long> parsePeriodicSync(XmlPullParser parser, AuthorityInfo authority) { - Bundle extras = new Bundle(); + /** + * Parse a periodic sync from accounts.xml. Sets the bundle to be empty. + */ + private PeriodicSync parsePeriodicSync(XmlPullParser parser, AuthorityInfo authority) { + Bundle extras = new Bundle(); // Gets filled in later. String periodValue = parser.getAttributeValue(null, "period"); + String flexValue = parser.getAttributeValue(null, "flex"); final long period; + long flextime; try { period = Long.parseLong(periodValue); } catch (NumberFormatException e) { @@ -1739,14 +1893,24 @@ public class SyncStorageEngine extends Handler { Log.e(TAG, "the period of a periodic sync is null", e); return null; } - final Pair<Bundle, Long> periodicSync = Pair.create(extras, period); + try { + flextime = Long.parseLong(flexValue); + } catch (NumberFormatException e) { + Log.e(TAG, "Error formatting value parsed for periodic sync flex: " + flexValue); + flextime = calculateDefaultFlexTime(period); + } catch (NullPointerException expected) { + flextime = calculateDefaultFlexTime(period); + Log.d(TAG, "No flex time specified for this sync, using a default. period: " + + period + " flex: " + flextime); + } + final PeriodicSync periodicSync = + new PeriodicSync(authority.account, authority.authority, extras, + period, flextime); authority.periodicSyncs.add(periodicSync); - return periodicSync; } - private void parseExtra(XmlPullParser parser, Pair<Bundle, Long> periodicSync) { - final Bundle extras = periodicSync.first; + private void parseExtra(XmlPullParser parser, Bundle extras) { String name = parser.getAttributeValue(null, "name"); String type = parser.getAttributeValue(null, "type"); String value1 = parser.getAttributeValue(null, "value1"); @@ -1806,62 +1970,37 @@ public class SyncStorageEngine extends Handler { } final int N = mAuthorities.size(); - for (int i=0; i<N; i++) { + for (int i = 0; i < N; i++) { AuthorityInfo authority = mAuthorities.valueAt(i); out.startTag(null, "authority"); out.attribute(null, "id", Integer.toString(authority.ident)); - out.attribute(null, "account", authority.account.name); out.attribute(null, XML_ATTR_USER, Integer.toString(authority.userId)); - out.attribute(null, "type", authority.account.type); - out.attribute(null, "authority", authority.authority); out.attribute(null, XML_ATTR_ENABLED, Boolean.toString(authority.enabled)); + if (authority.service == null) { + out.attribute(null, "account", authority.account.name); + out.attribute(null, "type", authority.account.type); + out.attribute(null, "authority", authority.authority); + } else { + out.attribute(null, "package", authority.service.getPackageName()); + out.attribute(null, "class", authority.service.getClassName()); + } if (authority.syncable < 0) { out.attribute(null, "syncable", "unknown"); } else { out.attribute(null, "syncable", Boolean.toString(authority.syncable != 0)); } - for (Pair<Bundle, Long> periodicSync : authority.periodicSyncs) { + for (PeriodicSync 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.attribute(null, "period", Long.toString(periodicSync.period)); + out.attribute(null, "flex", Long.toString(periodicSync.flexTime)); + final Bundle extras = periodicSync.extras; + extrasToXml(out, extras); out.endTag(null, "periodicSync"); } out.endTag(null, "authority"); } - out.endTag(null, "accounts"); - out.endDocument(); - mAccountInfoFile.finishWrite(fos); } catch (java.io.IOException e1) { Log.w(TAG, "Error writing accounts", e1); @@ -2072,7 +2211,7 @@ public class SyncStorageEngine extends Handler { } } - public static final int PENDING_OPERATION_VERSION = 3; + public static final int PENDING_OPERATION_VERSION = 4; /** * Read all pending operations back in to the initial engine state. @@ -2080,128 +2219,162 @@ public class SyncStorageEngine extends Handler { private void readPendingOperationsLocked() { if (DEBUG_FILE) Log.v(TAG, "Reading " + mPendingFile.getBaseFile()); try { - byte[] data = mPendingFile.readFully(); - Parcel in = Parcel.obtain(); - in.unmarshall(data, 0, data.length); - in.setDataPosition(0); - final int SIZE = in.dataSize(); - while (in.dataPosition() < SIZE) { - int version = in.readInt(); - if (version != PENDING_OPERATION_VERSION && version != 1) { - Log.w(TAG, "Unknown pending operation version " - + version + "; dropping all ops"); - break; - } - int authorityId = in.readInt(); - int syncSource = in.readInt(); - byte[] flatExtras = in.createByteArray(); - boolean expedited; - if (version == PENDING_OPERATION_VERSION) { - expedited = in.readInt() != 0; - } else { - expedited = false; - } - int reason = in.readInt(); - AuthorityInfo authority = mAuthorities.get(authorityId); - if (authority != null) { - Bundle extras; - if (flatExtras != null) { - extras = unflattenBundle(flatExtras); - } else { - // if we are unable to parse the extras for whatever reason convert this - // to a regular sync by creating an empty extras - extras = new Bundle(); - } - PendingOperation op = new PendingOperation( - authority.account, authority.userId, reason, syncSource, - authority.authority, extras, expedited); - op.authorityId = authorityId; - op.flatExtras = flatExtras; - if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account - + " auth=" + op.authority - + " src=" + op.syncSource - + " reason=" + op.reason - + " expedited=" + op.expedited - + " extras=" + op.extras); - mPendingOperations.add(op); - } + readPendingAsXml(); + } catch (XmlPullParserException e) { + Log.d(TAG, "Error parsing pending as xml, trying as parcel."); + try { + readPendingAsParcelled(); + } catch (java.io.IOException e1) { + Log.i(TAG, "No initial pending operations"); } - } catch (java.io.IOException e) { - Log.i(TAG, "No initial pending operations"); - } - } - - private void writePendingOperationLocked(PendingOperation op, Parcel out) { - out.writeInt(PENDING_OPERATION_VERSION); - out.writeInt(op.authorityId); - out.writeInt(op.syncSource); - if (op.flatExtras == null && op.extras != null) { - op.flatExtras = flattenBundle(op.extras); } - out.writeByteArray(op.flatExtras); - out.writeInt(op.expedited ? 1 : 0); - out.writeInt(op.reason); } - /** - * Write all currently pending ops to the pending ops file. - */ - private void writePendingOperationsLocked() { - final int N = mPendingOperations.size(); - FileOutputStream fos = null; + private void readPendingAsXml() throws XmlPullParserException { + FileInputStream fis = null; try { - if (N == 0) { - if (DEBUG_FILE) Log.v(TAG, "Truncating " + mPendingFile.getBaseFile()); - mPendingFile.truncate(); - return; + Log.v(TAG, "is this thing on"); + fis = mPendingFile.openRead(); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, null); + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG && + eventType != XmlPullParser.END_DOCUMENT) { + eventType = parser.next(); + Log.v(TAG, "go: " + eventType); } + if (eventType == XmlPullParser.END_DOCUMENT) return; - if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile()); - fos = mPendingFile.startWrite(); - - Parcel out = Parcel.obtain(); - for (int i=0; i<N; i++) { - PendingOperation op = mPendingOperations.get(i); - writePendingOperationLocked(op, out); + String tagName = parser.getName(); + if (DEBUG_FILE) { + Log.v(TAG, "got " + tagName); } - fos.write(out.marshall()); - out.recycle(); - - mPendingFile.finishWrite(fos); - } catch (java.io.IOException e1) { - Log.w(TAG, "Error writing pending operations", e1); - if (fos != null) { - mPendingFile.failWrite(fos); + if ("pending".equals(tagName)) { + int version = -1; + String versionString = parser.getAttributeValue(null, "version"); + if (versionString == null || + Integer.parseInt(versionString) != PENDING_OPERATION_VERSION) { + Log.w(TAG, "Unknown pending operation version " + + version + "; trying to read as binary."); + throw new XmlPullParserException("Unknown version."); + } + eventType = parser.next(); + PendingOperation pop = null; + do { + if (DEBUG_FILE) { + Log.v(TAG, "parsing xml file"); + } + if (eventType == XmlPullParser.START_TAG) { + try { + tagName = parser.getName(); + if (parser.getDepth() == 2 && "op".equals(tagName)) { + int authorityId = Integer.valueOf(parser.getAttributeValue( + null, XML_ATTR_AUTHORITYID)); + boolean expedited = Boolean.valueOf(parser.getAttributeValue( + null, XML_ATTR_EXPEDITED)); + int syncSource = Integer.valueOf(parser.getAttributeValue( + null, XML_ATTR_SOURCE)); + int reason = Integer.valueOf(parser.getAttributeValue( + null, XML_ATTR_REASON)); + AuthorityInfo authority = mAuthorities.get(authorityId); + if (DEBUG_FILE) { + Log.v(TAG, authorityId + " " + expedited + " " + syncSource + " " + reason); + } + if (authority != null) { + pop = new PendingOperation( + authority.account, authority.userId, reason, syncSource, + authority.authority, new Bundle(), expedited); + pop.authorityId = authorityId; + pop.flatExtras = null; // No longer used. + mPendingOperations.add(pop); + if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + pop.account + + " auth=" + pop.authority + + " src=" + pop.syncSource + + " reason=" + pop.reason + + " expedited=" + pop.expedited); + } else { + // Skip non-existent authority; + pop = null; + if (DEBUG_FILE) { + Log.v(TAG, "No authority found for " + authorityId + + ", skipping"); + } + } + } else if (parser.getDepth() == 3 && + pop != null && + "extra".equals(tagName)) { + parseExtra(parser, pop.extras); + } + } catch (NumberFormatException e) { + Log.d(TAG, "Invalid data in xml file.", e); + } + } + eventType = parser.next(); + } while(eventType != XmlPullParser.END_DOCUMENT); + } + } catch (java.io.IOException e) { + if (fis == null) Log.i(TAG, "No initial pending operations."); + else Log.w(TAG, "Error reading pending data.", e); + return; + } finally { + if (DEBUG_FILE) Log.v(TAG, "Done reading pending ops"); + if (fis != null) { + try { + fis.close(); + } catch (java.io.IOException e1) {} } } } - /** - * Append the given operation to the pending ops file; if unable to, - * write all pending ops. + * Old format of reading pending.bin as a parcelled file. Replaced in lieu of JSON because + * persisting parcels is unsafe. + * @throws java.io.IOException */ - private void appendPendingOperationLocked(PendingOperation op) { - if (DEBUG_FILE) Log.v(TAG, "Appending to " + mPendingFile.getBaseFile()); - FileOutputStream fos = null; - try { - fos = mPendingFile.openAppend(); - } catch (java.io.IOException e) { - if (DEBUG_FILE) Log.v(TAG, "Failed append; writing full file"); - writePendingOperationsLocked(); - return; - } - - try { - Parcel out = Parcel.obtain(); - writePendingOperationLocked(op, out); - fos.write(out.marshall()); - out.recycle(); - } catch (java.io.IOException e1) { - Log.w(TAG, "Error writing pending operations", e1); - } finally { - try { - fos.close(); - } catch (java.io.IOException e2) { + private void readPendingAsParcelled() throws java.io.IOException { + byte[] data = mPendingFile.readFully(); + Parcel in = Parcel.obtain(); + in.unmarshall(data, 0, data.length); + in.setDataPosition(0); + final int SIZE = in.dataSize(); + while (in.dataPosition() < SIZE) { + int version = in.readInt(); + if (version != 3 && version != 1) { + Log.w(TAG, "Unknown pending operation version " + + version + "; dropping all ops"); + break; + } + int authorityId = in.readInt(); + int syncSource = in.readInt(); + byte[] flatExtras = in.createByteArray(); + boolean expedited; + if (version == PENDING_OPERATION_VERSION) { + expedited = in.readInt() != 0; + } else { + expedited = false; + } + int reason = in.readInt(); + AuthorityInfo authority = mAuthorities.get(authorityId); + if (authority != null) { + Bundle extras; + if (flatExtras != null) { + extras = unflattenBundle(flatExtras); + } else { + // if we are unable to parse the extras for whatever reason convert this + // to a regular sync by creating an empty extras + extras = new Bundle(); + } + PendingOperation op = new PendingOperation( + authority.account, authority.userId, reason, syncSource, + authority.authority, extras, expedited); + op.authorityId = authorityId; + op.flatExtras = flatExtras; + if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account + + " auth=" + op.authority + + " src=" + op.syncSource + + " reason=" + op.reason + + " expedited=" + op.expedited + + " extras=" + op.extras); + mPendingOperations.add(op); } } } @@ -2235,6 +2408,115 @@ public class SyncStorageEngine extends Handler { return bundle; } + private static final String XML_ATTR_AUTHORITYID = "authority_id"; + private static final String XML_ATTR_SOURCE = "source"; + private static final String XML_ATTR_EXPEDITED = "expedited"; + private static final String XML_ATTR_REASON = "reason"; + /** + * Write all currently pending ops to the pending ops file. TODO: Change this from xml + * so that we can append to this file as before. + */ + private void writePendingOperationsLocked() { + final int N = mPendingOperations.size(); + FileOutputStream fos = null; + try { + if (N == 0) { + if (DEBUG_FILE) Log.v(TAG, "Truncating " + mPendingFile.getBaseFile()); + mPendingFile.truncate(); + return; + } + if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile()); + fos = mPendingFile.startWrite(); + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(fos, "utf-8"); + out.startDocument(null, true); + out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + + out.startTag(null, "pending"); + out.attribute(null, "version", Integer.toString(PENDING_OPERATION_VERSION)); + + for (int i = 0; i < N; i++) { + PendingOperation pop = mPendingOperations.get(i); + out.startTag(null, "op"); + out.attribute(null, XML_ATTR_AUTHORITYID, Integer.toString(pop.authorityId)); + out.attribute(null, XML_ATTR_SOURCE, Integer.toString(pop.syncSource)); + out.attribute(null, XML_ATTR_EXPEDITED, Boolean.toString(pop.expedited)); + out.attribute(null, XML_ATTR_REASON, Integer.toString(pop.reason)); + extrasToXml(out, pop.extras); + out.endTag(null, "op"); + } + out.endTag(null, "pending"); + out.endDocument(); + mPendingFile.finishWrite(fos); + } catch (java.io.IOException e1) { + Log.w(TAG, "Error writing pending operations", e1); + if (fos != null) { + mPendingFile.failWrite(fos); + } + } + } + + private void extrasToXml(XmlSerializer out, Bundle extras) throws java.io.IOException { + 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"); + } + } + +// /** +// * Update the pending ops file, if e +// */ +// private void appendPendingOperationLocked(PendingOperation op) { +// if (DEBUG_FILE) Log.v(TAG, "Appending to " + mPendingFile.getBaseFile()); +// FileOutputStream fos = null; +// try { +// fos = mPendingFile.openAppend(); +// } catch (java.io.IOException e) { +// if (DEBUG_FILE) Log.v(TAG, "Failed append; writing full file"); +// writePendingOperationsLocked(); +// return; +// } +// +// try { +// Parcel out = Parcel.obtain(); +// writePendingOperationLocked(op, out); +// fos.write(out.marshall()); +// out.recycle(); +// } catch (java.io.IOException e1) { +// Log.w(TAG, "Error writing pending operations", e1); +// } finally { +// try { +// fos.close(); +// } catch (java.io.IOException e2) { +// } +// } +// } + private void requestSync(Account account, int userId, int reason, String authority, Bundle extras) { // If this is happening in the system process, then call the syncrequest listener @@ -2330,4 +2612,18 @@ public class SyncStorageEngine extends Handler { } } } + + /** + * Dump state of PendingOperations. + */ + public void dumpPendingOperations(StringBuilder sb) { + sb.append("Pending Ops: ").append(mPendingOperations.size()).append(" operation(s)\n"); + for (PendingOperation pop : mPendingOperations) { + sb.append("(" + pop.account) + .append(", " + pop.userId) + .append(", " + pop.authority) + .append(", " + pop.extras) + .append(")\n"); + } + } } diff --git a/services/tests/servicestests/src/com/android/server/content/SyncOperationTest.java b/services/tests/servicestests/src/com/android/server/content/SyncOperationTest.java index f2772c8..37176d6 100644 --- a/services/tests/servicestests/src/com/android/server/content/SyncOperationTest.java +++ b/services/tests/servicestests/src/com/android/server/content/SyncOperationTest.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.content; import android.accounts.Account; +import android.content.ContentResolver; import android.os.Bundle; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; @@ -48,7 +49,8 @@ public class SyncOperationTest extends AndroidTestCase { SyncOperation.REASON_PERIODIC, "authority1", b1, - 100, + 100, /* run time from now*/ + 10, /* flex */ 1000, 10000, false); @@ -60,6 +62,7 @@ public class SyncOperationTest extends AndroidTestCase { "authority1", b1, 200, + 20, 2000, 20000, false); @@ -71,6 +74,7 @@ public class SyncOperationTest extends AndroidTestCase { "authority2", b1, 100, + 10, 1000, 10000, false); @@ -82,6 +86,7 @@ public class SyncOperationTest extends AndroidTestCase { "authority1", b1, 100, + 10, 1000, 10000, false); @@ -93,6 +98,7 @@ public class SyncOperationTest extends AndroidTestCase { "authority1", b2, 100, + 10, 1000, 10000, false); @@ -102,4 +108,38 @@ public class SyncOperationTest extends AndroidTestCase { assertNotSame(op1.key, op4.key); assertNotSame(op1.key, op5.key); } + + @SmallTest + public void testCompareTo() { + Account dummy = new Account("account1", "type1"); + Bundle b1 = new Bundle(); + final long unimportant = 0L; + long soon = 1000; + long soonFlex = 50; + long after = 1500; + long afterFlex = 100; + SyncOperation op1 = new SyncOperation(dummy, 0, 0, SyncOperation.REASON_PERIODIC, + "authority1", b1, soon, soonFlex, unimportant, unimportant, true); + + // Interval disjoint from and after op1. + SyncOperation op2 = new SyncOperation(dummy, 0, 0, SyncOperation.REASON_PERIODIC, + "authority1", b1, after, afterFlex, unimportant, unimportant, true); + + // Interval equivalent to op1, but expedited. + Bundle b2 = new Bundle(); + b2.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); + SyncOperation op3 = new SyncOperation(dummy, 0, 0, 0, + "authority1", b2, soon, soonFlex, unimportant, unimportant, true); + + // Interval overlaps but not equivalent to op1. + SyncOperation op4 = new SyncOperation(dummy, 0, 0, SyncOperation.REASON_PERIODIC, + "authority1", b1, soon + 100, soonFlex + 100, unimportant, unimportant, true); + + assertTrue(op1.compareTo(op2) == -1); + assertTrue("less than not transitive.", op2.compareTo(op1) == 1); + assertTrue(op1.compareTo(op3) == 1); + assertTrue("greater than not transitive. ", op3.compareTo(op1) == -1); + assertTrue("overlapping intervals not the same.", op1.compareTo(op4) == 0); + assertTrue("equality not transitive.", op4.compareTo(op1) == 0); + } } diff --git a/services/tests/servicestests/src/com/android/server/content/SyncStorageEngineTest.java b/services/tests/servicestests/src/com/android/server/content/SyncStorageEngineTest.java index 8b00f2c..dff6661 100644 --- a/services/tests/servicestests/src/com/android/server/content/SyncStorageEngineTest.java +++ b/services/tests/servicestests/src/com/android/server/content/SyncStorageEngineTest.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.PeriodicSync; +import android.content.res.Resources; import android.os.Bundle; import android.test.AndroidTestCase; import android.test.RenamingDelegatingContext; @@ -39,10 +40,31 @@ import java.util.List; public class SyncStorageEngineTest extends AndroidTestCase { + protected Account account1; + protected String authority1 = "testprovider"; + protected Bundle defaultBundle; + protected final int DEFAULT_USER = 0; + + MockContentResolver mockResolver; + SyncStorageEngine engine; + private File getSyncDir() { return new File(new File(getContext().getFilesDir(), "system"), "sync"); } + @Override + public void setUp() { + account1 = new Account("a@example.com", "example.type"); + // Default bundle. + defaultBundle = new Bundle(); + defaultBundle.putInt("int_key", 0); + defaultBundle.putString("string_key", "hello"); + // Set up storage engine. + mockResolver = new MockContentResolver(); + engine = SyncStorageEngine.newTestInstance( + new TestContext(mockResolver, getContext())); + } + /** * 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. @@ -68,7 +90,25 @@ public class SyncStorageEngineTest extends AndroidTestCase { } /** - * Test that we can create, remove and retrieve periodic syncs + * Test persistence of pending operations. + */ + @MediumTest + public void testPending() throws Exception { + SyncStorageEngine.PendingOperation pop = + new SyncStorageEngine.PendingOperation(account1, DEFAULT_USER, + SyncOperation.REASON_PERIODIC, SyncStorageEngine.SOURCE_LOCAL, + authority1, defaultBundle, false); + + engine.insertIntoPending(pop); + // Force engine to read from disk. + engine.clearAndReadState(); + + assert(engine.getPendingOperationCount() == 1); + } + + /** + * Test that we can create, remove and retrieve periodic syncs. Backwards compatibility - + * periodic syncs with no flex time are no longer used. */ @MediumTest public void testPeriodics() throws Exception { @@ -87,6 +127,64 @@ public class SyncStorageEngineTest extends AndroidTestCase { PeriodicSync sync3 = new PeriodicSync(account1, authority, extras2, period2); PeriodicSync sync4 = new PeriodicSync(account2, authority, extras2, period2); + + + removePeriodicSyncs(engine, account1, 0, authority); + removePeriodicSyncs(engine, account2, 0, authority); + removePeriodicSyncs(engine, account1, 1, authority); + + // this should add two distinct periodic syncs for account1 and one for account2 + engine.addPeriodicSync(sync1, 0); + engine.addPeriodicSync(sync2, 0); + engine.addPeriodicSync(sync3, 0); + engine.addPeriodicSync(sync4, 0); + // add a second user + engine.addPeriodicSync(sync2, 1); + + List<PeriodicSync> syncs = engine.getPeriodicSyncs(account1, 0, authority); + + assertEquals(2, syncs.size()); + + assertEquals(sync1, syncs.get(0)); + assertEquals(sync3, syncs.get(1)); + + engine.removePeriodicSync(sync1, 0); + + syncs = engine.getPeriodicSyncs(account1, 0, authority); + assertEquals(1, syncs.size()); + assertEquals(sync3, syncs.get(0)); + + syncs = engine.getPeriodicSyncs(account2, 0, authority); + assertEquals(1, syncs.size()); + assertEquals(sync4, syncs.get(0)); + + syncs = engine.getPeriodicSyncs(sync2.account, 1, sync2.authority); + assertEquals(1, syncs.size()); + assertEquals(sync2, syncs.get(0)); + } + + /** + * Test that we can create, remove and retrieve periodic syncs with a provided flex time. + */ + @MediumTest + public void testPeriodicsV2() 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; + final int flex1 = 10; + final int flex2 = 100; + + PeriodicSync sync1 = new PeriodicSync(account1, authority, extras1, period1, flex1); + PeriodicSync sync2 = new PeriodicSync(account1, authority, extras2, period1, flex1); + PeriodicSync sync3 = new PeriodicSync(account1, authority, extras2, period2, flex2); + PeriodicSync sync4 = new PeriodicSync(account2, authority, extras2, period2, flex2); + MockContentResolver mockResolver = new MockContentResolver(); SyncStorageEngine engine = SyncStorageEngine.newTestInstance( @@ -96,13 +194,13 @@ public class SyncStorageEngineTest extends AndroidTestCase { removePeriodicSyncs(engine, account2, 0, authority); removePeriodicSyncs(engine, account1, 1, authority); - // this should add two distinct periodic syncs for account1 and one for account2 - engine.addPeriodicSync(sync1.account, 0, sync1.authority, sync1.extras, sync1.period); - engine.addPeriodicSync(sync2.account, 0, sync2.authority, sync2.extras, sync2.period); - engine.addPeriodicSync(sync3.account, 0, sync3.authority, sync3.extras, sync3.period); - engine.addPeriodicSync(sync4.account, 0, sync4.authority, sync4.extras, sync4.period); + // This should add two distinct periodic syncs for account1 and one for account2 + engine.addPeriodicSync(sync1, 0); + engine.addPeriodicSync(sync2, 0); + engine.addPeriodicSync(sync3, 0); // Should edit sync2 and update the period. + engine.addPeriodicSync(sync4, 0); // add a second user - engine.addPeriodicSync(sync2.account, 1, sync2.authority, sync2.extras, sync2.period); + engine.addPeriodicSync(sync2, 1); List<PeriodicSync> syncs = engine.getPeriodicSyncs(account1, 0, authority); @@ -111,7 +209,7 @@ public class SyncStorageEngineTest extends AndroidTestCase { assertEquals(sync1, syncs.get(0)); assertEquals(sync3, syncs.get(1)); - engine.removePeriodicSync(sync1.account, 0, sync1.authority, sync1.extras); + engine.removePeriodicSync(sync1, 0); syncs = engine.getPeriodicSyncs(account1, 0, authority); assertEquals(1, syncs.size()); @@ -126,13 +224,11 @@ public class SyncStorageEngineTest extends AndroidTestCase { assertEquals(sync2, syncs.get(0)); } - private void removePeriodicSyncs(SyncStorageEngine engine, Account account, int userId, - String authority) { - engine.setIsSyncable(account, userId, authority, - engine.getIsSyncable(account, 0, authority)); + private void removePeriodicSyncs(SyncStorageEngine engine, Account account, int userId, String authority) { + engine.setIsSyncable(account, userId, authority, engine.getIsSyncable(account, 0, authority)); List<PeriodicSync> syncs = engine.getPeriodicSyncs(account, userId, authority); for (PeriodicSync sync : syncs) { - engine.removePeriodicSync(sync.account, userId, sync.authority, sync.extras); + engine.removePeriodicSync(sync, userId); } } @@ -154,12 +250,14 @@ public class SyncStorageEngineTest extends AndroidTestCase { extras2.putParcelable("g", account1); final int period1 = 200; final int period2 = 1000; + final int flex1 = 10; + final int flex2 = 100; - 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); + PeriodicSync sync1 = new PeriodicSync(account1, authority1, extras1, period1, flex1); + PeriodicSync sync2 = new PeriodicSync(account1, authority1, extras2, period1, flex1); + PeriodicSync sync3 = new PeriodicSync(account1, authority2, extras1, period1, flex1); + PeriodicSync sync4 = new PeriodicSync(account1, authority2, extras2, period2, flex2); + PeriodicSync sync5 = new PeriodicSync(account2, authority1, extras1, period1, flex1); MockContentResolver mockResolver = new MockContentResolver(); @@ -185,11 +283,11 @@ public class SyncStorageEngineTest extends AndroidTestCase { engine.setIsSyncable(account2, 0, authority2, 0); engine.setSyncAutomatically(account2, 0, authority2, true); - engine.addPeriodicSync(sync1.account, 0, sync1.authority, sync1.extras, sync1.period); - engine.addPeriodicSync(sync2.account, 0, sync2.authority, sync2.extras, sync2.period); - engine.addPeriodicSync(sync3.account, 0, sync3.authority, sync3.extras, sync3.period); - engine.addPeriodicSync(sync4.account, 0, sync4.authority, sync4.extras, sync4.period); - engine.addPeriodicSync(sync5.account, 0, sync5.authority, sync5.extras, sync5.period); + engine.addPeriodicSync(sync1, 0); + engine.addPeriodicSync(sync2, 0); + engine.addPeriodicSync(sync3, 0); + engine.addPeriodicSync(sync4, 0); + engine.addPeriodicSync(sync5, 0); engine.writeAllState(); engine.clearAndReadState(); @@ -220,6 +318,131 @@ public class SyncStorageEngineTest extends AndroidTestCase { } @MediumTest + /** + * V2 introduces flex time as well as service components. + * @throws Exception + */ + public void testAuthorityParsingV2() throws Exception { + final Account account = new Account("account1", "type1"); + final String authority1 = "auth1"; + final String authority2 = "auth2"; + final String authority3 = "auth3"; + + final long dayPoll = (60 * 60 * 24); + final long dayFuzz = 60; + final long thousandSecs = 1000; + final long thousandSecsFuzz = 100; + final Bundle extras = new Bundle(); + PeriodicSync sync1 = new PeriodicSync(account, authority1, extras, dayPoll, dayFuzz); + PeriodicSync sync2 = new PeriodicSync(account, authority2, extras, dayPoll, dayFuzz); + PeriodicSync sync3 = new PeriodicSync(account, authority3, extras, dayPoll, dayFuzz); + PeriodicSync sync1s = new PeriodicSync(account, authority1, extras, thousandSecs, thousandSecsFuzz); + PeriodicSync sync2s = new PeriodicSync(account, authority2, extras, thousandSecs, thousandSecsFuzz); + PeriodicSync sync3s = new PeriodicSync(account, authority3, extras, thousandSecs, thousandSecsFuzz); + MockContentResolver mockResolver = new MockContentResolver(); + + final TestContext testContext = new TestContext(mockResolver, getContext()); + + byte[] accountsFileData = ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<accounts version=\"2\" >\n" + + "<authority id=\"0\" user=\"0\" account=\"account1\" type=\"type1\" authority=\"auth1\" >" + + "\n<periodicSync period=\"" + dayPoll + "\" flex=\"" + dayFuzz + "\"/>" + + "\n</authority>" + + "<authority id=\"1\" user=\"0\" account=\"account1\" type=\"type1\" authority=\"auth2\" >" + + "\n<periodicSync period=\"" + dayPoll + "\" flex=\"" + dayFuzz + "\"/>" + + "\n</authority>" + // No user defaults to user 0 - all users. + + "<authority id=\"2\" account=\"account1\" type=\"type1\" authority=\"auth3\" >" + + "\n<periodicSync period=\"" + dayPoll + "\" flex=\"" + dayFuzz + "\"/>" + + "\n</authority>" + + "<authority id=\"3\" user=\"1\" account=\"account1\" type=\"type1\" authority=\"auth3\" >" + + "\n<periodicSync period=\"" + dayPoll + "\" flex=\"" + dayFuzz + "\"/>" + + "\n</authority>" + + "</accounts>").getBytes(); + + File syncDir = getSyncDir(); + syncDir.mkdirs(); + AtomicFile accountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml")); + FileOutputStream fos = accountInfoFile.startWrite(); + fos.write(accountsFileData); + accountInfoFile.finishWrite(fos); + + SyncStorageEngine engine = SyncStorageEngine.newTestInstance(testContext); + + List<PeriodicSync> syncs = engine.getPeriodicSyncs(account, 0, authority1); + assertEquals("Got incorrect # of syncs", 1, syncs.size()); + assertEquals(sync1, syncs.get(0)); + + syncs = engine.getPeriodicSyncs(account, 0, authority2); + assertEquals(1, syncs.size()); + assertEquals(sync2, syncs.get(0)); + + syncs = engine.getPeriodicSyncs(account, 0, authority3); + assertEquals(1, syncs.size()); + assertEquals(sync3, syncs.get(0)); + + syncs = engine.getPeriodicSyncs(account, 1, authority3); + assertEquals(1, syncs.size()); + assertEquals(sync3, syncs.get(0)); + + // Test empty periodic data. + accountsFileData = ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<accounts version=\"2\">\n" + + "<authority id=\"0\" account=\"account1\" type=\"type1\" authority=\"auth1\" />\n" + + "<authority id=\"1\" account=\"account1\" type=\"type1\" authority=\"auth2\" />\n" + + "<authority id=\"2\" account=\"account1\" type=\"type1\" authority=\"auth3\" />\n" + + "</accounts>\n").getBytes(); + + accountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml")); + fos = accountInfoFile.startWrite(); + fos.write(accountsFileData); + accountInfoFile.finishWrite(fos); + + engine.clearAndReadState(); + + syncs = engine.getPeriodicSyncs(account, 0, authority1); + assertEquals(0, syncs.size()); + + syncs = engine.getPeriodicSyncs(account, 0, authority2); + assertEquals(0, syncs.size()); + + syncs = engine.getPeriodicSyncs(account, 0, authority3); + assertEquals(0, syncs.size()); + + accountsFileData = ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<accounts version=\"2\">\n" + + "<authority id=\"0\" account=\"account1\" type=\"type1\" authority=\"auth1\">\n" + + "<periodicSync period=\"1000\" />\n" + + "</authority>" + + "<authority id=\"1\" account=\"account1\" type=\"type1\" authority=\"auth2\">\n" + + "<periodicSync period=\"1000\" />\n" + + "</authority>" + + "<authority id=\"2\" account=\"account1\" type=\"type1\" authority=\"auth3\">\n" + + "<periodicSync period=\"1000\" />\n" + + "</authority>" + + "</accounts>\n").getBytes(); + + accountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml")); + fos = accountInfoFile.startWrite(); + fos.write(accountsFileData); + accountInfoFile.finishWrite(fos); + + engine.clearAndReadState(); + + syncs = engine.getPeriodicSyncs(account, 0, authority1); + assertEquals(1, syncs.size()); + assertEquals(sync1s, syncs.get(0)); + + syncs = engine.getPeriodicSyncs(account, 0, authority2); + assertEquals(1, syncs.size()); + assertEquals(sync2s, syncs.get(0)); + + syncs = engine.getPeriodicSyncs(account, 0, authority3); + assertEquals(1, syncs.size()); + assertEquals(sync3s, syncs.get(0)); + } + + @MediumTest public void testAuthorityParsing() throws Exception { final Account account = new Account("account1", "type1"); final String authority1 = "auth1"; @@ -256,7 +479,7 @@ public class SyncStorageEngineTest extends AndroidTestCase { List<PeriodicSync> syncs = engine.getPeriodicSyncs(account, 0, authority1); assertEquals(1, syncs.size()); - assertEquals(sync1, syncs.get(0)); + assertEquals("expected sync1: " + sync1.toString() + " == sync 2" + syncs.get(0).toString(), sync1, syncs.get(0)); syncs = engine.getPeriodicSyncs(account, 0, authority2); assertEquals(1, syncs.size()); @@ -451,6 +674,11 @@ class TestContext extends ContextWrapper { } @Override + public Resources getResources() { + return mRealContext.getResources(); + } + + @Override public File getFilesDir() { return mRealContext.getFilesDir(); } |