blob: eb3a5da4a5b12e4c300fb6abc38dd39d44c14855 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001package android.content;
2
3import android.database.SQLException;
4import android.os.Bundle;
5import android.os.Debug;
6import android.os.NetStat;
7import android.os.Parcelable;
8import android.os.Process;
9import android.os.SystemProperties;
10import android.text.TextUtils;
11import android.util.Config;
12import android.util.EventLog;
13import android.util.Log;
14import android.util.TimingLogger;
15
16/**
17 * @hide
18 */
19public abstract class TempProviderSyncAdapter extends SyncAdapter {
20 private static final String TAG = "Sync";
21
22 private static final int MAX_GET_SERVER_DIFFS_LOOP_COUNT = 20;
23 private static final int MAX_UPLOAD_CHANGES_LOOP_COUNT = 10;
24 private static final int NUM_ALLOWED_SIMULTANEOUS_DELETIONS = 5;
25 private static final long PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS = 20;
26
27 private volatile SyncableContentProvider mProvider;
28 private volatile SyncThread mSyncThread = null;
29 private volatile boolean mProviderSyncStarted;
30 private volatile boolean mAdapterSyncStarted;
31
32 public TempProviderSyncAdapter(SyncableContentProvider provider) {
33 super();
34 mProvider = provider;
35 }
36
37 /**
38 * Used by getServerDiffs() to track the sync progress for a given
39 * sync adapter. Implementations of SyncAdapter generally specialize
40 * this class in order to track specific data about that SyncAdapter's
41 * sync. If an implementation of SyncAdapter doesn't need to store
42 * any data for a sync it may use TrivialSyncData.
43 */
44 public static abstract class SyncData implements Parcelable {
45
46 }
47
48 public final void setContext(Context context) {
49 mContext = context;
50 }
51
52 /**
53 * Retrieve the Context this adapter is running in. Only available
54 * once onSyncStarting() is called (not available from constructor).
55 */
56 final public Context getContext() {
57 return mContext;
58 }
59
60 /**
61 * Called right before a sync is started.
62 *
63 * @param context allows you to publish status and interact with the
64 * @param account the account to sync
65 * @param forced if true then the sync was forced
66 * @param result information to track what happened during this sync attempt
67 * @return true, if the sync was successfully started. One reason it can
68 * fail to start is if there is no user configured on the device.
69 */
70 public abstract void onSyncStarting(SyncContext context, String account, boolean forced,
71 SyncResult result);
72
73 /**
74 * Called right after a sync is completed
75 *
76 * @param context allows you to publish status and interact with the
77 * user during interactive syncs.
78 * @param success true if the sync suceeded, false if an error occured
79 */
80 public abstract void onSyncEnding(SyncContext context, boolean success);
81
82 /**
83 * Implement this to return true if the data in your content provider
84 * is read only.
85 */
86 public abstract boolean isReadOnly();
87
88 /**
89 * Get diffs from the server since the last completed sync and put them
90 * into a temporary provider.
91 *
92 * @param context allows you to publish status and interact with the
93 * user during interactive syncs.
94 * @param syncData used to track the progress this client has made in syncing data
95 * from the server
96 * @param tempProvider this is where the diffs should be stored
97 * @param extras any extra data describing the sync that is desired
98 * @param syncInfo sync adapter-specific data that is used during a single sync operation
99 * @param syncResult information to track what happened during this sync attempt
100 */
101 public abstract void getServerDiffs(SyncContext context,
102 SyncData syncData, SyncableContentProvider tempProvider,
103 Bundle extras, Object syncInfo, SyncResult syncResult);
104
105 /**
106 * Send client diffs to the server, optionally receiving more diffs from the server
107 *
108 * @param context allows you to publish status and interact with the
109 * user during interactive syncs.
110 * @param clientDiffs the diffs from the client
111 * @param serverDiffs the SyncableContentProvider that should be populated with
112* the entries that were returned in response to an insert/update/delete request
113* to the server
114 * @param syncResult information to track what happened during this sync attempt
115 * @param dontActuallySendDeletes
116 */
117 public abstract void sendClientDiffs(SyncContext context,
118 SyncableContentProvider clientDiffs,
119 SyncableContentProvider serverDiffs, SyncResult syncResult,
120 boolean dontActuallySendDeletes);
121
122 /**
123 * Reads the sync data from the ContentProvider
124 * @param contentProvider the ContentProvider to read from
125 * @return the SyncData for the provider. This may be null.
126 */
127 public SyncData readSyncData(SyncableContentProvider contentProvider) {
128 return null;
129 }
130
131 /**
132 * Create and return a new, empty SyncData object
133 */
134 public SyncData newSyncData() {
135 return null;
136 }
137
138 /**
139 * Stores the sync data in the Sync Stats database, keying it by
140 * the account that was set in the last call to onSyncStarting()
141 */
142 public void writeSyncData(SyncData syncData, SyncableContentProvider contentProvider) {}
143
144 /**
145 * Indicate to the SyncAdapter that the last sync that was started has
146 * been cancelled.
147 */
148 public abstract void onSyncCanceled();
149
150 /**
151 * Initializes the temporary content providers used during
152 * {@link TempProviderSyncAdapter#sendClientDiffs}.
153 * May copy relevant data from the underlying db into this provider so
154 * joins, etc., can work.
155 *
156 * @param cp The ContentProvider to initialize.
157 */
158 protected void initTempProvider(SyncableContentProvider cp) {}
159
160 protected Object createSyncInfo() {
161 return null;
162 }
163
164 /**
165 * Called when the accounts list possibly changed, to give the
166 * SyncAdapter a chance to do any necessary bookkeeping, e.g.
167 * to make sure that any required SubscribedFeeds subscriptions
168 * exist.
169 * @param accounts the list of accounts
170 */
171 public abstract void onAccountsChanged(String[] accounts);
172
173 private Context mContext;
174
175 private class SyncThread extends Thread {
176 private final String mAccount;
177 private final Bundle mExtras;
178 private final SyncContext mSyncContext;
179 private volatile boolean mIsCanceled = false;
180 private long mInitialTxBytes;
181 private long mInitialRxBytes;
182 private final SyncResult mResult;
183
184 SyncThread(SyncContext syncContext, String account, Bundle extras) {
185 super("SyncThread");
186 mAccount = account;
187 mExtras = extras;
188 mSyncContext = syncContext;
189 mResult = new SyncResult();
190 }
191
192 void cancelSync() {
193 mIsCanceled = true;
194 if (mAdapterSyncStarted) onSyncCanceled();
195 if (mProviderSyncStarted) mProvider.onSyncCanceled();
196 // We may lose the last few sync events when canceling. Oh well.
197 int uid = Process.myUid();
198 logSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes,
199 NetStat.getUidRxBytes(uid) - mInitialRxBytes, mResult);
200 }
201
202 @Override
203 public void run() {
204 Process.setThreadPriority(Process.myTid(),
205 Process.THREAD_PRIORITY_BACKGROUND);
206 int uid = Process.myUid();
207 mInitialTxBytes = NetStat.getUidTxBytes(uid);
208 mInitialRxBytes = NetStat.getUidRxBytes(uid);
209 try {
210 sync(mSyncContext, mAccount, mExtras);
211 } catch (SQLException e) {
212 Log.e(TAG, "Sync failed", e);
213 mResult.databaseError = true;
214 } finally {
215 mSyncThread = null;
216 if (!mIsCanceled) {
217 logSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes,
218 NetStat.getUidRxBytes(uid) - mInitialRxBytes, mResult);
219 mSyncContext.onFinished(mResult);
220 }
221 }
222 }
223
224 private void sync(SyncContext syncContext, String account, Bundle extras) {
225 mIsCanceled = false;
226
227 mProviderSyncStarted = false;
228 mAdapterSyncStarted = false;
229 String message = null;
230
231 boolean syncForced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false);
232
233 try {
234 mProvider.onSyncStart(syncContext, account);
235 mProviderSyncStarted = true;
236 onSyncStarting(syncContext, account, syncForced, mResult);
237 if (mResult.hasError()) {
238 message = "SyncAdapter failed while trying to start sync";
239 return;
240 }
241 mAdapterSyncStarted = true;
242 if (mIsCanceled) {
243 return;
244 }
245 final String syncTracingEnabledValue = SystemProperties.get(TAG + "Tracing");
246 final boolean syncTracingEnabled = !TextUtils.isEmpty(syncTracingEnabledValue);
247 try {
248 if (syncTracingEnabled) {
249 System.gc();
250 System.gc();
251 Debug.startMethodTracing("synctrace." + System.currentTimeMillis());
252 }
253 runSyncLoop(syncContext, account, extras);
254 } finally {
255 if (syncTracingEnabled) Debug.stopMethodTracing();
256 }
257 onSyncEnding(syncContext, !mResult.hasError());
258 mAdapterSyncStarted = false;
259 mProvider.onSyncStop(syncContext, true);
260 mProviderSyncStarted = false;
261 } finally {
262 if (mAdapterSyncStarted) {
263 mAdapterSyncStarted = false;
264 onSyncEnding(syncContext, false);
265 }
266 if (mProviderSyncStarted) {
267 mProviderSyncStarted = false;
268 mProvider.onSyncStop(syncContext, false);
269 }
270 if (!mIsCanceled) {
271 if (message != null) syncContext.setStatusText(message);
272 }
273 }
274 }
275
276 private void runSyncLoop(SyncContext syncContext, String account, Bundle extras) {
277 TimingLogger syncTimer = new TimingLogger(TAG + "Profiling", "sync");
278 syncTimer.addSplit("start");
279 int loopCount = 0;
280 boolean tooManyGetServerDiffsAttempts = false;
281
282 final boolean overrideTooManyDeletions =
283 extras.getBoolean(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS,
284 false);
285 final boolean discardLocalDeletions =
286 extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS, false);
287 boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD,
288 false /* default this flag to false */);
289 SyncableContentProvider serverDiffs = null;
290 TempProviderSyncResult result = new TempProviderSyncResult();
291 try {
292 if (!uploadOnly) {
293 /**
294 * This loop repeatedly calls SyncAdapter.getServerDiffs()
295 * (to get changes from the feed) followed by
296 * ContentProvider.merge() (to incorporate these changes
297 * into the provider), stopping when the SyncData returned
298 * from getServerDiffs() indicates that all the data was
299 * fetched.
300 */
301 while (!mIsCanceled) {
302 // Don't let a bad sync go forever
303 if (loopCount++ == MAX_GET_SERVER_DIFFS_LOOP_COUNT) {
304 Log.e(TAG, "runSyncLoop: Hit max loop count while getting server diffs "
305 + getClass().getName());
306 // TODO: change the structure here to schedule a new sync
307 // with a backoff time, keeping track to be sure
308 // we don't keep doing this forever (due to some bug or
309 // mismatch between the client and the server)
310 tooManyGetServerDiffsAttempts = true;
311 break;
312 }
313
314 // Get an empty content provider to put the diffs into
315 if (serverDiffs != null) serverDiffs.close();
316 serverDiffs = mProvider.getTemporaryInstance();
317
318 // Get records from the server which will be put into the serverDiffs
319 initTempProvider(serverDiffs);
320 Object syncInfo = createSyncInfo();
321 SyncData syncData = readSyncData(serverDiffs);
322 // syncData will only be null if there was a demarshalling error
323 // while reading the sync data.
324 if (syncData == null) {
325 mProvider.wipeAccount(account);
326 syncData = newSyncData();
327 }
328 mResult.clear();
329 if (Log.isLoggable(TAG, Log.VERBOSE)) {
330 Log.v(TAG, "runSyncLoop: running getServerDiffs using syncData "
331 + syncData.toString());
332 }
333 getServerDiffs(syncContext, syncData, serverDiffs, extras, syncInfo,
334 mResult);
335
336 if (mIsCanceled) return;
337 if (Log.isLoggable(TAG, Log.VERBOSE)) {
338 Log.v(TAG, "runSyncLoop: result: " + mResult);
339 }
340 if (mResult.hasError()) return;
341 if (mResult.partialSyncUnavailable) {
342 if (Config.LOGD) {
343 Log.d(TAG, "partialSyncUnavailable is set, setting "
344 + "ignoreSyncData and retrying");
345 }
346 mProvider.wipeAccount(account);
347 continue;
348 }
349
350 // write the updated syncData back into the temp provider
351 writeSyncData(syncData, serverDiffs);
352
353 // apply the downloaded changes to the provider
354 if (Log.isLoggable(TAG, Log.VERBOSE)) {
355 Log.v(TAG, "runSyncLoop: running merge");
356 }
357 mProvider.merge(syncContext, serverDiffs,
358 null /* don't return client diffs */, mResult);
359 if (mIsCanceled) return;
360 if (Log.isLoggable(TAG, Log.VERBOSE)) {
361 Log.v(TAG, "runSyncLoop: result: " + mResult);
362 }
363
364 // if the server has no more changes then break out of the loop
365 if (!mResult.moreRecordsToGet) {
366 if (Log.isLoggable(TAG, Log.VERBOSE)) {
367 Log.v(TAG, "runSyncLoop: fetched all data, moving on");
368 }
369 break;
370 }
371 if (Log.isLoggable(TAG, Log.VERBOSE)) {
372 Log.v(TAG, "runSyncLoop: more data to fetch, looping");
373 }
374 }
375 }
376
377 /**
378 * This loop repeatedly calls ContentProvider.merge() followed
379 * by SyncAdapter.merge() until either indicate that there is
380 * no more work to do by returning null.
381 * <p>
382 * The initial ContentProvider.merge() returns a temporary
383 * ContentProvider that contains any local changes that need
384 * to be committed to the server.
385 * <p>
386 * The SyncAdapter.merge() calls upload the changes to the server
387 * and populates temporary provider (the serverDiffs) with the
388 * result.
389 * <p>
390 * Subsequent calls to ContentProvider.merge() incoporate the
391 * result of previous SyncAdapter.merge() calls into the
392 * real ContentProvider and again return a temporary
393 * ContentProvider that contains any local changes that need
394 * to be committed to the server.
395 */
396 loopCount = 0;
397 boolean readOnly = isReadOnly();
398 long previousNumModifications = 0;
399 if (serverDiffs != null) {
400 serverDiffs.close();
401 serverDiffs = null;
402 }
403
404 // If we are discarding local deletions then we need to redownload all the items
405 // again (since some of them might have been deleted). We do this by deleting the
406 // sync data for the current account by writing in a null one.
407 if (discardLocalDeletions) {
408 serverDiffs = mProvider.getTemporaryInstance();
409 initTempProvider(serverDiffs);
410 writeSyncData(null, serverDiffs);
411 }
412
413 while (!mIsCanceled) {
414 if (Config.LOGV) {
415 Log.v(TAG, "runSyncLoop: Merging diffs from server to client");
416 }
417 if (result.tempContentProvider != null) {
418 result.tempContentProvider.close();
419 result.tempContentProvider = null;
420 }
421 mResult.clear();
422 mProvider.merge(syncContext, serverDiffs, readOnly ? null : result,
423 mResult);
424 if (mIsCanceled) return;
425 if (Log.isLoggable(TAG, Log.VERBOSE)) {
426 Log.v(TAG, "runSyncLoop: result: " + mResult);
427 }
428
429 SyncableContentProvider clientDiffs =
430 readOnly ? null : result.tempContentProvider;
431 if (clientDiffs == null) {
432 // Nothing to commit back to the server
433 if (Config.LOGV) Log.v(TAG, "runSyncLoop: No client diffs");
434 break;
435 }
436
437 long numModifications = mResult.stats.numUpdates
438 + mResult.stats.numDeletes
439 + mResult.stats.numInserts;
440
441 // as long as we are making progress keep resetting the loop count
442 if (numModifications < previousNumModifications) {
443 loopCount = 0;
444 }
445 previousNumModifications = numModifications;
446
447 // Don't let a bad sync go forever
448 if (loopCount++ >= MAX_UPLOAD_CHANGES_LOOP_COUNT) {
449 Log.e(TAG, "runSyncLoop: Hit max loop count while syncing "
450 + getClass().getName());
451 mResult.tooManyRetries = true;
452 break;
453 }
454
455 if (!overrideTooManyDeletions && !discardLocalDeletions
456 && hasTooManyDeletions(mResult.stats)) {
457 if (Config.LOGD) {
458 Log.d(TAG, "runSyncLoop: Too many deletions were found in provider "
459 + getClass().getName() + ", not doing any more updates");
460 }
461 long numDeletes = mResult.stats.numDeletes;
462 mResult.stats.clear();
463 mResult.tooManyDeletions = true;
464 mResult.stats.numDeletes = numDeletes;
465 break;
466 }
467
468 if (Config.LOGV) Log.v(TAG, "runSyncLoop: Merging diffs from client to server");
469 if (serverDiffs != null) serverDiffs.close();
470 serverDiffs = clientDiffs.getTemporaryInstance();
471 initTempProvider(serverDiffs);
472 mResult.clear();
473 sendClientDiffs(syncContext, clientDiffs, serverDiffs, mResult,
474 discardLocalDeletions);
475 if (Log.isLoggable(TAG, Log.VERBOSE)) {
476 Log.v(TAG, "runSyncLoop: result: " + mResult);
477 }
478
479 if (!mResult.madeSomeProgress()) {
480 if (Log.isLoggable(TAG, Log.VERBOSE)) {
481 Log.v(TAG, "runSyncLoop: No data from client diffs merge");
482 }
483 break;
484 }
485 if (Log.isLoggable(TAG, Log.VERBOSE)) {
486 Log.v(TAG, "runSyncLoop: made some progress, looping");
487 }
488 }
489
490 // add in any status codes that we saved from earlier
491 mResult.tooManyRetries |= tooManyGetServerDiffsAttempts;
492 if (Log.isLoggable(TAG, Log.VERBOSE)) {
493 Log.v(TAG, "runSyncLoop: final result: " + mResult);
494 }
495 } finally {
496 // do this in the finally block to guarantee that is is set and not overwritten
497 if (discardLocalDeletions) {
498 mResult.fullSyncRequested = true;
499 }
500 if (serverDiffs != null) serverDiffs.close();
501 if (result.tempContentProvider != null) result.tempContentProvider.close();
502 syncTimer.addSplit("stop");
503 syncTimer.dumpToLog();
504 }
505 }
506 }
507
508 /**
509 * Logs details on the sync.
510 * Normally this will be overridden by a subclass that will provide
511 * provider-specific details.
512 *
513 * @param bytesSent number of bytes the sync sent over the network
514 * @param bytesReceived number of bytes the sync received over the network
515 * @param result The SyncResult object holding info on the sync
516 */
517 protected void logSyncDetails(long bytesSent, long bytesReceived, SyncResult result) {
518 EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, "");
519 }
520
521 public void startSync(SyncContext syncContext, String account, Bundle extras) {
522 if (mSyncThread != null) {
523 syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS);
524 return;
525 }
526
527 mSyncThread = new SyncThread(syncContext, account, extras);
528 mSyncThread.start();
529 }
530
531 public void cancelSync() {
532 if (mSyncThread != null) {
533 mSyncThread.cancelSync();
534 }
535 }
536
537 protected boolean hasTooManyDeletions(SyncStats stats) {
538 long numEntries = stats.numEntries;
539 long numDeletedEntries = stats.numDeletes;
540
541 long percentDeleted = (numDeletedEntries == 0)
542 ? 0
543 : (100 * numDeletedEntries /
544 (numEntries + numDeletedEntries));
545 boolean tooManyDeletions =
546 (numDeletedEntries > NUM_ALLOWED_SIMULTANEOUS_DELETIONS)
547 && (percentDeleted > PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS);
548 return tooManyDeletions;
549 }
550}