diff options
Diffstat (limited to 'core/java/android/content/TempProviderSyncAdapter.java')
-rw-r--r-- | core/java/android/content/TempProviderSyncAdapter.java | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/core/java/android/content/TempProviderSyncAdapter.java b/core/java/android/content/TempProviderSyncAdapter.java new file mode 100644 index 0000000..eb3a5da --- /dev/null +++ b/core/java/android/content/TempProviderSyncAdapter.java @@ -0,0 +1,550 @@ +package android.content; + +import android.database.SQLException; +import android.os.Bundle; +import android.os.Debug; +import android.os.NetStat; +import android.os.Parcelable; +import android.os.Process; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Config; +import android.util.EventLog; +import android.util.Log; +import android.util.TimingLogger; + +/** + * @hide + */ +public abstract class TempProviderSyncAdapter extends SyncAdapter { + private static final String TAG = "Sync"; + + private static final int MAX_GET_SERVER_DIFFS_LOOP_COUNT = 20; + private static final int MAX_UPLOAD_CHANGES_LOOP_COUNT = 10; + private static final int NUM_ALLOWED_SIMULTANEOUS_DELETIONS = 5; + private static final long PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS = 20; + + private volatile SyncableContentProvider mProvider; + private volatile SyncThread mSyncThread = null; + private volatile boolean mProviderSyncStarted; + private volatile boolean mAdapterSyncStarted; + + public TempProviderSyncAdapter(SyncableContentProvider provider) { + super(); + mProvider = provider; + } + + /** + * Used by getServerDiffs() to track the sync progress for a given + * sync adapter. Implementations of SyncAdapter generally specialize + * this class in order to track specific data about that SyncAdapter's + * sync. If an implementation of SyncAdapter doesn't need to store + * any data for a sync it may use TrivialSyncData. + */ + public static abstract class SyncData implements Parcelable { + + } + + public final void setContext(Context context) { + mContext = context; + } + + /** + * Retrieve the Context this adapter is running in. Only available + * once onSyncStarting() is called (not available from constructor). + */ + final public Context getContext() { + return mContext; + } + + /** + * Called right before a sync is started. + * + * @param context allows you to publish status and interact with the + * @param account the account to sync + * @param forced if true then the sync was forced + * @param result information to track what happened during this sync attempt + * @return true, if the sync was successfully started. One reason it can + * fail to start is if there is no user configured on the device. + */ + public abstract void onSyncStarting(SyncContext context, String account, boolean forced, + SyncResult result); + + /** + * Called right after a sync is completed + * + * @param context allows you to publish status and interact with the + * user during interactive syncs. + * @param success true if the sync suceeded, false if an error occured + */ + public abstract void onSyncEnding(SyncContext context, boolean success); + + /** + * Implement this to return true if the data in your content provider + * is read only. + */ + public abstract boolean isReadOnly(); + + /** + * Get diffs from the server since the last completed sync and put them + * into a temporary provider. + * + * @param context allows you to publish status and interact with the + * user during interactive syncs. + * @param syncData used to track the progress this client has made in syncing data + * from the server + * @param tempProvider this is where the diffs should be stored + * @param extras any extra data describing the sync that is desired + * @param syncInfo sync adapter-specific data that is used during a single sync operation + * @param syncResult information to track what happened during this sync attempt + */ + public abstract void getServerDiffs(SyncContext context, + SyncData syncData, SyncableContentProvider tempProvider, + Bundle extras, Object syncInfo, SyncResult syncResult); + + /** + * Send client diffs to the server, optionally receiving more diffs from the server + * + * @param context allows you to publish status and interact with the + * user during interactive syncs. + * @param clientDiffs the diffs from the client + * @param serverDiffs the SyncableContentProvider that should be populated with +* the entries that were returned in response to an insert/update/delete request +* to the server + * @param syncResult information to track what happened during this sync attempt + * @param dontActuallySendDeletes + */ + public abstract void sendClientDiffs(SyncContext context, + SyncableContentProvider clientDiffs, + SyncableContentProvider serverDiffs, SyncResult syncResult, + boolean dontActuallySendDeletes); + + /** + * Reads the sync data from the ContentProvider + * @param contentProvider the ContentProvider to read from + * @return the SyncData for the provider. This may be null. + */ + public SyncData readSyncData(SyncableContentProvider contentProvider) { + return null; + } + + /** + * Create and return a new, empty SyncData object + */ + public SyncData newSyncData() { + return null; + } + + /** + * Stores the sync data in the Sync Stats database, keying it by + * the account that was set in the last call to onSyncStarting() + */ + public void writeSyncData(SyncData syncData, SyncableContentProvider contentProvider) {} + + /** + * Indicate to the SyncAdapter that the last sync that was started has + * been cancelled. + */ + public abstract void onSyncCanceled(); + + /** + * Initializes the temporary content providers used during + * {@link TempProviderSyncAdapter#sendClientDiffs}. + * May copy relevant data from the underlying db into this provider so + * joins, etc., can work. + * + * @param cp The ContentProvider to initialize. + */ + protected void initTempProvider(SyncableContentProvider cp) {} + + protected Object createSyncInfo() { + return null; + } + + /** + * Called when the accounts list possibly changed, to give the + * SyncAdapter a chance to do any necessary bookkeeping, e.g. + * to make sure that any required SubscribedFeeds subscriptions + * exist. + * @param accounts the list of accounts + */ + public abstract void onAccountsChanged(String[] accounts); + + private Context mContext; + + private class SyncThread extends Thread { + private final String mAccount; + private final Bundle mExtras; + private final SyncContext mSyncContext; + private volatile boolean mIsCanceled = false; + private long mInitialTxBytes; + private long mInitialRxBytes; + private final SyncResult mResult; + + SyncThread(SyncContext syncContext, String account, Bundle extras) { + super("SyncThread"); + mAccount = account; + mExtras = extras; + mSyncContext = syncContext; + mResult = new SyncResult(); + } + + void cancelSync() { + mIsCanceled = true; + if (mAdapterSyncStarted) onSyncCanceled(); + if (mProviderSyncStarted) mProvider.onSyncCanceled(); + // We may lose the last few sync events when canceling. Oh well. + int uid = Process.myUid(); + logSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes, + NetStat.getUidRxBytes(uid) - mInitialRxBytes, mResult); + } + + @Override + public void run() { + Process.setThreadPriority(Process.myTid(), + Process.THREAD_PRIORITY_BACKGROUND); + int uid = Process.myUid(); + mInitialTxBytes = NetStat.getUidTxBytes(uid); + mInitialRxBytes = NetStat.getUidRxBytes(uid); + try { + sync(mSyncContext, mAccount, mExtras); + } catch (SQLException e) { + Log.e(TAG, "Sync failed", e); + mResult.databaseError = true; + } finally { + mSyncThread = null; + if (!mIsCanceled) { + logSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes, + NetStat.getUidRxBytes(uid) - mInitialRxBytes, mResult); + mSyncContext.onFinished(mResult); + } + } + } + + private void sync(SyncContext syncContext, String account, Bundle extras) { + mIsCanceled = false; + + mProviderSyncStarted = false; + mAdapterSyncStarted = false; + String message = null; + + boolean syncForced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); + + try { + mProvider.onSyncStart(syncContext, account); + mProviderSyncStarted = true; + onSyncStarting(syncContext, account, syncForced, mResult); + if (mResult.hasError()) { + message = "SyncAdapter failed while trying to start sync"; + return; + } + mAdapterSyncStarted = true; + if (mIsCanceled) { + return; + } + final String syncTracingEnabledValue = SystemProperties.get(TAG + "Tracing"); + final boolean syncTracingEnabled = !TextUtils.isEmpty(syncTracingEnabledValue); + try { + if (syncTracingEnabled) { + System.gc(); + System.gc(); + Debug.startMethodTracing("synctrace." + System.currentTimeMillis()); + } + runSyncLoop(syncContext, account, extras); + } finally { + if (syncTracingEnabled) Debug.stopMethodTracing(); + } + onSyncEnding(syncContext, !mResult.hasError()); + mAdapterSyncStarted = false; + mProvider.onSyncStop(syncContext, true); + mProviderSyncStarted = false; + } finally { + if (mAdapterSyncStarted) { + mAdapterSyncStarted = false; + onSyncEnding(syncContext, false); + } + if (mProviderSyncStarted) { + mProviderSyncStarted = false; + mProvider.onSyncStop(syncContext, false); + } + if (!mIsCanceled) { + if (message != null) syncContext.setStatusText(message); + } + } + } + + private void runSyncLoop(SyncContext syncContext, String account, Bundle extras) { + TimingLogger syncTimer = new TimingLogger(TAG + "Profiling", "sync"); + syncTimer.addSplit("start"); + int loopCount = 0; + boolean tooManyGetServerDiffsAttempts = false; + + final boolean overrideTooManyDeletions = + extras.getBoolean(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS, + false); + final boolean discardLocalDeletions = + extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS, false); + boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, + false /* default this flag to false */); + SyncableContentProvider serverDiffs = null; + TempProviderSyncResult result = new TempProviderSyncResult(); + try { + if (!uploadOnly) { + /** + * This loop repeatedly calls SyncAdapter.getServerDiffs() + * (to get changes from the feed) followed by + * ContentProvider.merge() (to incorporate these changes + * into the provider), stopping when the SyncData returned + * from getServerDiffs() indicates that all the data was + * fetched. + */ + while (!mIsCanceled) { + // Don't let a bad sync go forever + if (loopCount++ == MAX_GET_SERVER_DIFFS_LOOP_COUNT) { + Log.e(TAG, "runSyncLoop: Hit max loop count while getting server diffs " + + getClass().getName()); + // TODO: change the structure here to schedule a new sync + // with a backoff time, keeping track to be sure + // we don't keep doing this forever (due to some bug or + // mismatch between the client and the server) + tooManyGetServerDiffsAttempts = true; + break; + } + + // Get an empty content provider to put the diffs into + if (serverDiffs != null) serverDiffs.close(); + serverDiffs = mProvider.getTemporaryInstance(); + + // Get records from the server which will be put into the serverDiffs + initTempProvider(serverDiffs); + Object syncInfo = createSyncInfo(); + SyncData syncData = readSyncData(serverDiffs); + // syncData will only be null if there was a demarshalling error + // while reading the sync data. + if (syncData == null) { + mProvider.wipeAccount(account); + syncData = newSyncData(); + } + mResult.clear(); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: running getServerDiffs using syncData " + + syncData.toString()); + } + getServerDiffs(syncContext, syncData, serverDiffs, extras, syncInfo, + mResult); + + if (mIsCanceled) return; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: result: " + mResult); + } + if (mResult.hasError()) return; + if (mResult.partialSyncUnavailable) { + if (Config.LOGD) { + Log.d(TAG, "partialSyncUnavailable is set, setting " + + "ignoreSyncData and retrying"); + } + mProvider.wipeAccount(account); + continue; + } + + // write the updated syncData back into the temp provider + writeSyncData(syncData, serverDiffs); + + // apply the downloaded changes to the provider + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: running merge"); + } + mProvider.merge(syncContext, serverDiffs, + null /* don't return client diffs */, mResult); + if (mIsCanceled) return; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: result: " + mResult); + } + + // if the server has no more changes then break out of the loop + if (!mResult.moreRecordsToGet) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: fetched all data, moving on"); + } + break; + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: more data to fetch, looping"); + } + } + } + + /** + * This loop repeatedly calls ContentProvider.merge() followed + * by SyncAdapter.merge() until either indicate that there is + * no more work to do by returning null. + * <p> + * The initial ContentProvider.merge() returns a temporary + * ContentProvider that contains any local changes that need + * to be committed to the server. + * <p> + * The SyncAdapter.merge() calls upload the changes to the server + * and populates temporary provider (the serverDiffs) with the + * result. + * <p> + * Subsequent calls to ContentProvider.merge() incoporate the + * result of previous SyncAdapter.merge() calls into the + * real ContentProvider and again return a temporary + * ContentProvider that contains any local changes that need + * to be committed to the server. + */ + loopCount = 0; + boolean readOnly = isReadOnly(); + long previousNumModifications = 0; + if (serverDiffs != null) { + serverDiffs.close(); + serverDiffs = null; + } + + // If we are discarding local deletions then we need to redownload all the items + // again (since some of them might have been deleted). We do this by deleting the + // sync data for the current account by writing in a null one. + if (discardLocalDeletions) { + serverDiffs = mProvider.getTemporaryInstance(); + initTempProvider(serverDiffs); + writeSyncData(null, serverDiffs); + } + + while (!mIsCanceled) { + if (Config.LOGV) { + Log.v(TAG, "runSyncLoop: Merging diffs from server to client"); + } + if (result.tempContentProvider != null) { + result.tempContentProvider.close(); + result.tempContentProvider = null; + } + mResult.clear(); + mProvider.merge(syncContext, serverDiffs, readOnly ? null : result, + mResult); + if (mIsCanceled) return; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: result: " + mResult); + } + + SyncableContentProvider clientDiffs = + readOnly ? null : result.tempContentProvider; + if (clientDiffs == null) { + // Nothing to commit back to the server + if (Config.LOGV) Log.v(TAG, "runSyncLoop: No client diffs"); + break; + } + + long numModifications = mResult.stats.numUpdates + + mResult.stats.numDeletes + + mResult.stats.numInserts; + + // as long as we are making progress keep resetting the loop count + if (numModifications < previousNumModifications) { + loopCount = 0; + } + previousNumModifications = numModifications; + + // Don't let a bad sync go forever + if (loopCount++ >= MAX_UPLOAD_CHANGES_LOOP_COUNT) { + Log.e(TAG, "runSyncLoop: Hit max loop count while syncing " + + getClass().getName()); + mResult.tooManyRetries = true; + break; + } + + if (!overrideTooManyDeletions && !discardLocalDeletions + && hasTooManyDeletions(mResult.stats)) { + if (Config.LOGD) { + Log.d(TAG, "runSyncLoop: Too many deletions were found in provider " + + getClass().getName() + ", not doing any more updates"); + } + long numDeletes = mResult.stats.numDeletes; + mResult.stats.clear(); + mResult.tooManyDeletions = true; + mResult.stats.numDeletes = numDeletes; + break; + } + + if (Config.LOGV) Log.v(TAG, "runSyncLoop: Merging diffs from client to server"); + if (serverDiffs != null) serverDiffs.close(); + serverDiffs = clientDiffs.getTemporaryInstance(); + initTempProvider(serverDiffs); + mResult.clear(); + sendClientDiffs(syncContext, clientDiffs, serverDiffs, mResult, + discardLocalDeletions); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: result: " + mResult); + } + + if (!mResult.madeSomeProgress()) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: No data from client diffs merge"); + } + break; + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: made some progress, looping"); + } + } + + // add in any status codes that we saved from earlier + mResult.tooManyRetries |= tooManyGetServerDiffsAttempts; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: final result: " + mResult); + } + } finally { + // do this in the finally block to guarantee that is is set and not overwritten + if (discardLocalDeletions) { + mResult.fullSyncRequested = true; + } + if (serverDiffs != null) serverDiffs.close(); + if (result.tempContentProvider != null) result.tempContentProvider.close(); + syncTimer.addSplit("stop"); + syncTimer.dumpToLog(); + } + } + } + + /** + * Logs details on the sync. + * Normally this will be overridden by a subclass that will provide + * provider-specific details. + * + * @param bytesSent number of bytes the sync sent over the network + * @param bytesReceived number of bytes the sync received over the network + * @param result The SyncResult object holding info on the sync + */ + protected void logSyncDetails(long bytesSent, long bytesReceived, SyncResult result) { + EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, ""); + } + + public void startSync(SyncContext syncContext, String account, Bundle extras) { + if (mSyncThread != null) { + syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS); + return; + } + + mSyncThread = new SyncThread(syncContext, account, extras); + mSyncThread.start(); + } + + public void cancelSync() { + if (mSyncThread != null) { + mSyncThread.cancelSync(); + } + } + + protected boolean hasTooManyDeletions(SyncStats stats) { + long numEntries = stats.numEntries; + long numDeletedEntries = stats.numDeletes; + + long percentDeleted = (numDeletedEntries == 0) + ? 0 + : (100 * numDeletedEntries / + (numEntries + numDeletedEntries)); + boolean tooManyDeletions = + (numDeletedEntries > NUM_ALLOWED_SIMULTANEOUS_DELETIONS) + && (percentDeleted > PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS); + return tooManyDeletions; + } +} |