SyncStorageEngine.java revision fb084400d6afa6443a421117fbcaee0265d38fb6
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content;
18
19import com.android.internal.os.AtomicFile;
20import com.android.internal.util.ArrayUtils;
21import com.android.internal.util.FastXmlSerializer;
22
23import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25import org.xmlpull.v1.XmlSerializer;
26
27import android.accounts.Account;
28import android.database.Cursor;
29import android.database.sqlite.SQLiteDatabase;
30import android.database.sqlite.SQLiteException;
31import android.database.sqlite.SQLiteQueryBuilder;
32import android.os.Bundle;
33import android.os.Environment;
34import android.os.Handler;
35import android.os.Message;
36import android.os.Parcel;
37import android.os.RemoteCallbackList;
38import android.os.RemoteException;
39import android.util.Log;
40import android.util.SparseArray;
41import android.util.Xml;
42import android.util.Pair;
43
44import java.io.File;
45import java.io.FileInputStream;
46import java.io.FileOutputStream;
47import java.util.ArrayList;
48import java.util.Calendar;
49import java.util.HashMap;
50import java.util.Iterator;
51import java.util.TimeZone;
52import java.util.List;
53
54/**
55 * Singleton that tracks the sync data and overall sync
56 * history on the device.
57 *
58 * @hide
59 */
60public class SyncStorageEngine extends Handler {
61    private static final String TAG = "SyncManager";
62    private static final boolean DEBUG = false;
63    private static final boolean DEBUG_FILE = false;
64
65    private static final long DEFAULT_POLL_FREQUENCY_SECONDS = 60 * 60 * 24; // One day
66
67    // @VisibleForTesting
68    static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
69
70    /** Enum value for a sync start event. */
71    public static final int EVENT_START = 0;
72
73    /** Enum value for a sync stop event. */
74    public static final int EVENT_STOP = 1;
75
76    // TODO: i18n -- grab these out of resources.
77    /** String names for the sync event types. */
78    public static final String[] EVENTS = { "START", "STOP" };
79
80    /** Enum value for a server-initiated sync. */
81    public static final int SOURCE_SERVER = 0;
82
83    /** Enum value for a local-initiated sync. */
84    public static final int SOURCE_LOCAL = 1;
85    /**
86     * Enum value for a poll-based sync (e.g., upon connection to
87     * network)
88     */
89    public static final int SOURCE_POLL = 2;
90
91    /** Enum value for a user-initiated sync. */
92    public static final int SOURCE_USER = 3;
93
94    /** Enum value for a periodic sync. */
95    public static final int SOURCE_PERIODIC = 4;
96
97    public static final long NOT_IN_BACKOFF_MODE = -1;
98
99    private static final Intent SYNC_CONNECTION_SETTING_CHANGED_INTENT =
100            new Intent("com.android.sync.SYNC_CONN_STATUS_CHANGED");
101
102    // TODO: i18n -- grab these out of resources.
103    /** String names for the sync source types. */
104    public static final String[] SOURCES = { "SERVER",
105                                             "LOCAL",
106                                             "POLL",
107                                             "USER",
108                                             "PERIODIC" };
109
110    // The MESG column will contain one of these or one of the Error types.
111    public static final String MESG_SUCCESS = "success";
112    public static final String MESG_CANCELED = "canceled";
113
114    public static final int MAX_HISTORY = 100;
115
116    private static final int MSG_WRITE_STATUS = 1;
117    private static final long WRITE_STATUS_DELAY = 1000*60*10; // 10 minutes
118
119    private static final int MSG_WRITE_STATISTICS = 2;
120    private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour
121
122    private static final boolean SYNC_ENABLED_DEFAULT = false;
123
124    // the version of the accounts xml file format
125    private static final int ACCOUNTS_VERSION = 2;
126
127    private static HashMap<String, String> sAuthorityRenames;
128
129    static {
130        sAuthorityRenames = new HashMap<String, String>();
131        sAuthorityRenames.put("contacts", "com.android.contacts");
132        sAuthorityRenames.put("calendar", "com.android.calendar");
133    }
134
135    public static class PendingOperation {
136        final Account account;
137        final int syncSource;
138        final String authority;
139        final Bundle extras;        // note: read-only.
140        final boolean expedited;
141
142        int authorityId;
143        byte[] flatExtras;
144
145        PendingOperation(Account account, int source,
146                String authority, Bundle extras, boolean expedited) {
147            this.account = account;
148            this.syncSource = source;
149            this.authority = authority;
150            this.extras = extras != null ? new Bundle(extras) : extras;
151            this.expedited = expedited;
152            this.authorityId = -1;
153        }
154
155        PendingOperation(PendingOperation other) {
156            this.account = other.account;
157            this.syncSource = other.syncSource;
158            this.authority = other.authority;
159            this.extras = other.extras;
160            this.authorityId = other.authorityId;
161            this.expedited = other.expedited;
162        }
163    }
164
165    static class AccountInfo {
166        final Account account;
167        final HashMap<String, AuthorityInfo> authorities =
168                new HashMap<String, AuthorityInfo>();
169
170        AccountInfo(Account account) {
171            this.account = account;
172        }
173    }
174
175    public static class AuthorityInfo {
176        final Account account;
177        final String authority;
178        final int ident;
179        boolean enabled;
180        int syncable;
181        long backoffTime;
182        long backoffDelay;
183        long delayUntil;
184        final ArrayList<Pair<Bundle, Long>> periodicSyncs;
185
186        AuthorityInfo(Account account, String authority, int ident) {
187            this.account = account;
188            this.authority = authority;
189            this.ident = ident;
190            enabled = SYNC_ENABLED_DEFAULT;
191            syncable = -1; // default to "unknown"
192            backoffTime = -1; // if < 0 then we aren't in backoff mode
193            backoffDelay = -1; // if < 0 then we aren't in backoff mode
194            periodicSyncs = new ArrayList<Pair<Bundle, Long>>();
195            periodicSyncs.add(Pair.create(new Bundle(), DEFAULT_POLL_FREQUENCY_SECONDS));
196        }
197    }
198
199    public static class SyncHistoryItem {
200        int authorityId;
201        int historyId;
202        long eventTime;
203        long elapsedTime;
204        int source;
205        int event;
206        long upstreamActivity;
207        long downstreamActivity;
208        String mesg;
209    }
210
211    public static class DayStats {
212        public final int day;
213        public int successCount;
214        public long successTime;
215        public int failureCount;
216        public long failureTime;
217
218        public DayStats(int day) {
219            this.day = day;
220        }
221    }
222
223    // Primary list of all syncable authorities.  Also our global lock.
224    private final SparseArray<AuthorityInfo> mAuthorities =
225            new SparseArray<AuthorityInfo>();
226
227    private final HashMap<Account, AccountInfo> mAccounts =
228        new HashMap<Account, AccountInfo>();
229
230    private final ArrayList<PendingOperation> mPendingOperations =
231            new ArrayList<PendingOperation>();
232
233    private ActiveSyncInfo mActiveSync;
234
235    private final SparseArray<SyncStatusInfo> mSyncStatus =
236            new SparseArray<SyncStatusInfo>();
237
238    private final ArrayList<SyncHistoryItem> mSyncHistory =
239            new ArrayList<SyncHistoryItem>();
240
241    private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners
242            = new RemoteCallbackList<ISyncStatusObserver>();
243
244    // We keep 4 weeks of stats.
245    private final DayStats[] mDayStats = new DayStats[7*4];
246    private final Calendar mCal;
247    private int mYear;
248    private int mYearInDays;
249
250    private final Context mContext;
251
252    private static volatile SyncStorageEngine sSyncStorageEngine = null;
253
254    /**
255     * This file contains the core engine state: all accounts and the
256     * settings for them.  It must never be lost, and should be changed
257     * infrequently, so it is stored as an XML file.
258     */
259    private final AtomicFile mAccountInfoFile;
260
261    /**
262     * This file contains the current sync status.  We would like to retain
263     * it across boots, but its loss is not the end of the world, so we store
264     * this information as binary data.
265     */
266    private final AtomicFile mStatusFile;
267
268    /**
269     * This file contains sync statistics.  This is purely debugging information
270     * so is written infrequently and can be thrown away at any time.
271     */
272    private final AtomicFile mStatisticsFile;
273
274    /**
275     * This file contains the pending sync operations.  It is a binary file,
276     * which must be updated every time an operation is added or removed,
277     * so we have special handling of it.
278     */
279    private final AtomicFile mPendingFile;
280    private static final int PENDING_FINISH_TO_WRITE = 4;
281    private int mNumPendingFinished = 0;
282
283    private int mNextHistoryId = 0;
284    private boolean mMasterSyncAutomatically = true;
285
286    private SyncStorageEngine(Context context, File dataDir) {
287        mContext = context;
288        sSyncStorageEngine = this;
289
290        mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
291
292        File systemDir = new File(dataDir, "system");
293        File syncDir = new File(systemDir, "sync");
294        syncDir.mkdirs();
295        mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"));
296        mStatusFile = new AtomicFile(new File(syncDir, "status.bin"));
297        mPendingFile = new AtomicFile(new File(syncDir, "pending.bin"));
298        mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin"));
299
300        readAccountInfoLocked();
301        readStatusLocked();
302        readPendingOperationsLocked();
303        readStatisticsLocked();
304        readLegacyAccountInfoLocked();
305    }
306
307    public static SyncStorageEngine newTestInstance(Context context) {
308        return new SyncStorageEngine(context, context.getFilesDir());
309    }
310
311    public static void init(Context context) {
312        if (sSyncStorageEngine != null) {
313            return;
314        }
315        // This call will return the correct directory whether Encrypted File Systems is
316        // enabled or not.
317        File dataDir = Environment.getSecureDataDirectory();
318        sSyncStorageEngine = new SyncStorageEngine(context, dataDir);
319    }
320
321    public static SyncStorageEngine getSingleton() {
322        if (sSyncStorageEngine == null) {
323            throw new IllegalStateException("not initialized");
324        }
325        return sSyncStorageEngine;
326    }
327
328    @Override public void handleMessage(Message msg) {
329        if (msg.what == MSG_WRITE_STATUS) {
330            synchronized (mAccounts) {
331                writeStatusLocked();
332            }
333        } else if (msg.what == MSG_WRITE_STATISTICS) {
334            synchronized (mAccounts) {
335                writeStatisticsLocked();
336            }
337        }
338    }
339
340    public void addStatusChangeListener(int mask, ISyncStatusObserver callback) {
341        synchronized (mAuthorities) {
342            mChangeListeners.register(callback, mask);
343        }
344    }
345
346    public void removeStatusChangeListener(ISyncStatusObserver callback) {
347        synchronized (mAuthorities) {
348            mChangeListeners.unregister(callback);
349        }
350    }
351
352    private void reportChange(int which) {
353        ArrayList<ISyncStatusObserver> reports = null;
354        synchronized (mAuthorities) {
355            int i = mChangeListeners.beginBroadcast();
356            while (i > 0) {
357                i--;
358                Integer mask = (Integer)mChangeListeners.getBroadcastCookie(i);
359                if ((which & mask.intValue()) == 0) {
360                    continue;
361                }
362                if (reports == null) {
363                    reports = new ArrayList<ISyncStatusObserver>(i);
364                }
365                reports.add(mChangeListeners.getBroadcastItem(i));
366            }
367            mChangeListeners.finishBroadcast();
368        }
369
370        if (DEBUG) Log.v(TAG, "reportChange " + which + " to: " + reports);
371
372        if (reports != null) {
373            int i = reports.size();
374            while (i > 0) {
375                i--;
376                try {
377                    reports.get(i).onStatusChanged(which);
378                } catch (RemoteException e) {
379                    // The remote callback list will take care of this for us.
380                }
381            }
382        }
383    }
384
385    public boolean getSyncAutomatically(Account account, String providerName) {
386        synchronized (mAuthorities) {
387            if (account != null) {
388                AuthorityInfo authority = getAuthorityLocked(account, providerName,
389                        "getSyncAutomatically");
390                return authority != null && authority.enabled;
391            }
392
393            int i = mAuthorities.size();
394            while (i > 0) {
395                i--;
396                AuthorityInfo authority = mAuthorities.valueAt(i);
397                if (authority.authority.equals(providerName)
398                        && authority.enabled) {
399                    return true;
400                }
401            }
402            return false;
403        }
404    }
405
406    public void setSyncAutomatically(Account account, String providerName, boolean sync) {
407        boolean wasEnabled;
408        synchronized (mAuthorities) {
409            AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false);
410            wasEnabled = authority.enabled;
411            authority.enabled = sync;
412            writeAccountInfoLocked();
413        }
414
415        if (!wasEnabled && sync) {
416            ContentResolver.requestSync(account, providerName, new Bundle());
417        }
418        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
419    }
420
421    public int getIsSyncable(Account account, String providerName) {
422        synchronized (mAuthorities) {
423            if (account != null) {
424                AuthorityInfo authority = getAuthorityLocked(account, providerName,
425                        "getIsSyncable");
426                if (authority == null) {
427                    return -1;
428                }
429                return authority.syncable;
430            }
431
432            int i = mAuthorities.size();
433            while (i > 0) {
434                i--;
435                AuthorityInfo authority = mAuthorities.valueAt(i);
436                if (authority.authority.equals(providerName)) {
437                    return authority.syncable;
438                }
439            }
440            return -1;
441        }
442    }
443
444    public void setIsSyncable(Account account, String providerName, int syncable) {
445        int oldState;
446        if (syncable > 1) {
447            syncable = 1;
448        } else if (syncable < -1) {
449            syncable = -1;
450        }
451        Log.d(TAG, "setIsSyncable: " + account + ", provider " + providerName + " -> " + syncable);
452        synchronized (mAuthorities) {
453            AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false);
454            oldState = authority.syncable;
455            authority.syncable = syncable;
456            writeAccountInfoLocked();
457        }
458
459        if (oldState <= 0 && syncable > 0) {
460            ContentResolver.requestSync(account, providerName, new Bundle());
461        }
462        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
463    }
464
465    public Pair<Long, Long> getBackoff(Account account, String providerName) {
466        synchronized (mAuthorities) {
467            AuthorityInfo authority = getAuthorityLocked(account, providerName, "getBackoff");
468            if (authority == null || authority.backoffTime < 0) {
469                return null;
470            }
471            return Pair.create(authority.backoffTime, authority.backoffDelay);
472        }
473    }
474
475    public void setBackoff(Account account, String providerName,
476            long nextSyncTime, long nextDelay) {
477        if (Log.isLoggable(TAG, Log.VERBOSE)) {
478            Log.v(TAG, "setBackoff: " + account + ", provider " + providerName
479                    + " -> nextSyncTime " + nextSyncTime + ", nextDelay " + nextDelay);
480        }
481        boolean changed = false;
482        synchronized (mAuthorities) {
483            if (account == null || providerName == null) {
484                for (AccountInfo accountInfo : mAccounts.values()) {
485                    if (account != null && !account.equals(accountInfo.account)) continue;
486                    for (AuthorityInfo authorityInfo : accountInfo.authorities.values()) {
487                        if (providerName != null && !providerName.equals(authorityInfo.authority)) {
488                            continue;
489                        }
490                        if (authorityInfo.backoffTime != nextSyncTime
491                                || authorityInfo.backoffDelay != nextDelay) {
492                            authorityInfo.backoffTime = nextSyncTime;
493                            authorityInfo.backoffDelay = nextDelay;
494                            changed = true;
495                        }
496                    }
497                }
498            } else {
499                AuthorityInfo authority =
500                        getOrCreateAuthorityLocked(account, providerName, -1 /* ident */, true);
501                if (authority.backoffTime == nextSyncTime && authority.backoffDelay == nextDelay) {
502                    return;
503                }
504                authority.backoffTime = nextSyncTime;
505                authority.backoffDelay = nextDelay;
506                changed = true;
507            }
508        }
509
510        if (changed) {
511            reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
512        }
513    }
514
515    public void setDelayUntilTime(Account account, String providerName, long delayUntil) {
516        if (Log.isLoggable(TAG, Log.VERBOSE)) {
517            Log.v(TAG, "setDelayUntil: " + account + ", provider " + providerName
518                    + " -> delayUntil " + delayUntil);
519        }
520        synchronized (mAuthorities) {
521            AuthorityInfo authority = getOrCreateAuthorityLocked(
522                    account, providerName, -1 /* ident */, true);
523            if (authority.delayUntil == delayUntil) {
524                return;
525            }
526            authority.delayUntil = delayUntil;
527        }
528
529        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
530    }
531
532    public long getDelayUntilTime(Account account, String providerName) {
533        synchronized (mAuthorities) {
534            AuthorityInfo authority = getAuthorityLocked(account, providerName, "getDelayUntil");
535            if (authority == null) {
536                return 0;
537            }
538            return authority.delayUntil;
539        }
540    }
541
542    private void updateOrRemovePeriodicSync(Account account, String providerName, Bundle extras,
543            long period, boolean add) {
544        if (period <= 0) {
545            period = 0;
546        }
547        if (extras == null) {
548            extras = new Bundle();
549        }
550        if (Log.isLoggable(TAG, Log.VERBOSE)) {
551            Log.v(TAG, "addOrRemovePeriodicSync: " + account + ", provider " + providerName
552                    + " -> period " + period + ", extras " + extras);
553        }
554        synchronized (mAuthorities) {
555            AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false);
556            if (add) {
557                boolean alreadyPresent = false;
558                for (int i = 0, N = authority.periodicSyncs.size(); i < N; i++) {
559                    Pair<Bundle, Long> syncInfo = authority.periodicSyncs.get(i);
560                    final Bundle existingExtras = syncInfo.first;
561                    if (equals(existingExtras, extras)) {
562                        if (syncInfo.second == period) {
563                            return;
564                        }
565                        authority.periodicSyncs.set(i, Pair.create(extras, period));
566                        alreadyPresent = true;
567                        break;
568                    }
569                }
570                if (!alreadyPresent) {
571                    authority.periodicSyncs.add(Pair.create(extras, period));
572                    SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
573                    status.setPeriodicSyncTime(authority.periodicSyncs.size() - 1, 0);
574                }
575            } else {
576                SyncStatusInfo status = mSyncStatus.get(authority.ident);
577                boolean changed = false;
578                Iterator<Pair<Bundle, Long>> iterator = authority.periodicSyncs.iterator();
579                int i = 0;
580                while (iterator.hasNext()) {
581                    Pair<Bundle, Long> syncInfo = iterator.next();
582                    if (equals(syncInfo.first, extras)) {
583                        iterator.remove();
584                        changed = true;
585                        if (status != null) {
586                            status.removePeriodicSyncTime(i);
587                        }
588                    } else {
589                        i++;
590                    }
591                }
592                if (!changed) {
593                    return;
594                }
595            }
596            writeAccountInfoLocked();
597            writeStatusLocked();
598        }
599
600        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
601    }
602
603    public void addPeriodicSync(Account account, String providerName, Bundle extras,
604            long pollFrequency) {
605        updateOrRemovePeriodicSync(account, providerName, extras, pollFrequency, true /* add */);
606    }
607
608    public void removePeriodicSync(Account account, String providerName, Bundle extras) {
609        updateOrRemovePeriodicSync(account, providerName, extras, 0 /* period, ignored */,
610                false /* remove */);
611    }
612
613    public List<PeriodicSync> getPeriodicSyncs(Account account, String providerName) {
614        ArrayList<PeriodicSync> syncs = new ArrayList<PeriodicSync>();
615        synchronized (mAuthorities) {
616            AuthorityInfo authority = getAuthorityLocked(account, providerName, "getPeriodicSyncs");
617            if (authority != null) {
618                for (Pair<Bundle, Long> item : authority.periodicSyncs) {
619                    syncs.add(new PeriodicSync(account, providerName, item.first, item.second));
620                }
621            }
622        }
623        return syncs;
624    }
625
626    public void setMasterSyncAutomatically(boolean flag) {
627        boolean old;
628        synchronized (mAuthorities) {
629            old = mMasterSyncAutomatically;
630            mMasterSyncAutomatically = flag;
631            writeAccountInfoLocked();
632        }
633        if (!old && flag) {
634            ContentResolver.requestSync(null, null, new Bundle());
635        }
636        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
637        mContext.sendBroadcast(SYNC_CONNECTION_SETTING_CHANGED_INTENT);
638    }
639
640    public boolean getMasterSyncAutomatically() {
641        synchronized (mAuthorities) {
642            return mMasterSyncAutomatically;
643        }
644    }
645
646    public AuthorityInfo getOrCreateAuthority(Account account, String authority) {
647        synchronized (mAuthorities) {
648            return getOrCreateAuthorityLocked(account, authority,
649                    -1 /* assign a new identifier if creating a new authority */,
650                    true /* write to storage if this results in a change */);
651        }
652    }
653
654    public void removeAuthority(Account account, String authority) {
655        synchronized (mAuthorities) {
656            removeAuthorityLocked(account, authority);
657        }
658    }
659
660    public AuthorityInfo getAuthority(int authorityId) {
661        synchronized (mAuthorities) {
662            return mAuthorities.get(authorityId);
663        }
664    }
665
666    /**
667     * Returns true if there is currently a sync operation for the given
668     * account or authority in the pending list, or actively being processed.
669     */
670    public boolean isSyncActive(Account account, String authority) {
671        synchronized (mAuthorities) {
672            int i = mPendingOperations.size();
673            while (i > 0) {
674                i--;
675                // TODO(fredq): this probably shouldn't be considering
676                // pending operations.
677                PendingOperation op = mPendingOperations.get(i);
678                if (op.account.equals(account) && op.authority.equals(authority)) {
679                    return true;
680                }
681            }
682
683            if (mActiveSync != null) {
684                AuthorityInfo ainfo = getAuthority(mActiveSync.getAuthorityId());
685                if (ainfo != null && ainfo.account.equals(account)
686                        && ainfo.authority.equals(authority)) {
687                    return true;
688                }
689            }
690        }
691
692        return false;
693    }
694
695    public PendingOperation insertIntoPending(PendingOperation op) {
696        synchronized (mAuthorities) {
697            if (DEBUG) Log.v(TAG, "insertIntoPending: account=" + op.account
698                    + " auth=" + op.authority
699                    + " src=" + op.syncSource
700                    + " extras=" + op.extras);
701
702            AuthorityInfo authority = getOrCreateAuthorityLocked(op.account,
703                    op.authority,
704                    -1 /* desired identifier */,
705                    true /* write accounts to storage */);
706            if (authority == null) {
707                return null;
708            }
709
710            op = new PendingOperation(op);
711            op.authorityId = authority.ident;
712            mPendingOperations.add(op);
713            appendPendingOperationLocked(op);
714
715            SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
716            status.pending = true;
717        }
718
719        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING);
720        return op;
721    }
722
723    public boolean deleteFromPending(PendingOperation op) {
724        boolean res = false;
725        synchronized (mAuthorities) {
726            if (DEBUG) Log.v(TAG, "deleteFromPending: account=" + op.account
727                    + " auth=" + op.authority
728                    + " src=" + op.syncSource
729                    + " extras=" + op.extras);
730            if (mPendingOperations.remove(op)) {
731                if (mPendingOperations.size() == 0
732                        || mNumPendingFinished >= PENDING_FINISH_TO_WRITE) {
733                    writePendingOperationsLocked();
734                    mNumPendingFinished = 0;
735                } else {
736                    mNumPendingFinished++;
737                }
738
739                AuthorityInfo authority = getAuthorityLocked(op.account, op.authority,
740                        "deleteFromPending");
741                if (authority != null) {
742                    if (DEBUG) Log.v(TAG, "removing - " + authority);
743                    final int N = mPendingOperations.size();
744                    boolean morePending = false;
745                    for (int i=0; i<N; i++) {
746                        PendingOperation cur = mPendingOperations.get(i);
747                        if (cur.account.equals(op.account)
748                                && cur.authority.equals(op.authority)) {
749                            morePending = true;
750                            break;
751                        }
752                    }
753
754                    if (!morePending) {
755                        if (DEBUG) Log.v(TAG, "no more pending!");
756                        SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
757                        status.pending = false;
758                    }
759                }
760
761                res = true;
762            }
763        }
764
765        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING);
766        return res;
767    }
768
769    public int clearPending() {
770        int num;
771        synchronized (mAuthorities) {
772            if (DEBUG) Log.v(TAG, "clearPending");
773            num = mPendingOperations.size();
774            mPendingOperations.clear();
775            final int N = mSyncStatus.size();
776            for (int i=0; i<N; i++) {
777                mSyncStatus.valueAt(i).pending = false;
778            }
779            writePendingOperationsLocked();
780        }
781        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING);
782        return num;
783    }
784
785    /**
786     * Return a copy of the current array of pending operations.  The
787     * PendingOperation objects are the real objects stored inside, so that
788     * they can be used with deleteFromPending().
789     */
790    public ArrayList<PendingOperation> getPendingOperations() {
791        synchronized (mAuthorities) {
792            return new ArrayList<PendingOperation>(mPendingOperations);
793        }
794    }
795
796    /**
797     * Return the number of currently pending operations.
798     */
799    public int getPendingOperationCount() {
800        synchronized (mAuthorities) {
801            return mPendingOperations.size();
802        }
803    }
804
805    /**
806     * Called when the set of account has changed, given the new array of
807     * active accounts.
808     */
809    public void doDatabaseCleanup(Account[] accounts) {
810        synchronized (mAuthorities) {
811            if (DEBUG) Log.w(TAG, "Updating for new accounts...");
812            SparseArray<AuthorityInfo> removing = new SparseArray<AuthorityInfo>();
813            Iterator<AccountInfo> accIt = mAccounts.values().iterator();
814            while (accIt.hasNext()) {
815                AccountInfo acc = accIt.next();
816                if (!ArrayUtils.contains(accounts, acc.account)) {
817                    // This account no longer exists...
818                    if (DEBUG) Log.w(TAG, "Account removed: " + acc.account);
819                    for (AuthorityInfo auth : acc.authorities.values()) {
820                        removing.put(auth.ident, auth);
821                    }
822                    accIt.remove();
823                }
824            }
825
826            // Clean out all data structures.
827            int i = removing.size();
828            if (i > 0) {
829                while (i > 0) {
830                    i--;
831                    int ident = removing.keyAt(i);
832                    mAuthorities.remove(ident);
833                    int j = mSyncStatus.size();
834                    while (j > 0) {
835                        j--;
836                        if (mSyncStatus.keyAt(j) == ident) {
837                            mSyncStatus.remove(mSyncStatus.keyAt(j));
838                        }
839                    }
840                    j = mSyncHistory.size();
841                    while (j > 0) {
842                        j--;
843                        if (mSyncHistory.get(j).authorityId == ident) {
844                            mSyncHistory.remove(j);
845                        }
846                    }
847                }
848                writeAccountInfoLocked();
849                writeStatusLocked();
850                writePendingOperationsLocked();
851                writeStatisticsLocked();
852            }
853        }
854    }
855
856    /**
857     * Called when the currently active sync is changing (there can only be
858     * one at a time).  Either supply a valid ActiveSyncContext with information
859     * about the sync, or null to stop the currently active sync.
860     */
861    public void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
862        synchronized (mAuthorities) {
863            if (activeSyncContext != null) {
864                if (DEBUG) Log.v(TAG, "setActiveSync: account="
865                        + activeSyncContext.mSyncOperation.account
866                        + " auth=" + activeSyncContext.mSyncOperation.authority
867                        + " src=" + activeSyncContext.mSyncOperation.syncSource
868                        + " extras=" + activeSyncContext.mSyncOperation.extras);
869                if (mActiveSync != null) {
870                    Log.w(TAG, "setActiveSync called with existing active sync!");
871                }
872                AuthorityInfo authority = getAuthorityLocked(
873                        activeSyncContext.mSyncOperation.account,
874                        activeSyncContext.mSyncOperation.authority,
875                        "setActiveSync");
876                if (authority == null) {
877                    return;
878                }
879                mActiveSync = new ActiveSyncInfo(authority.ident,
880                        authority.account, authority.authority,
881                        activeSyncContext.mStartTime);
882            } else {
883                if (DEBUG) Log.v(TAG, "setActiveSync: null");
884                mActiveSync = null;
885            }
886        }
887
888        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE);
889    }
890
891    /**
892     * To allow others to send active change reports, to poke clients.
893     */
894    public void reportActiveChange() {
895        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE);
896    }
897
898    /**
899     * Note that sync has started for the given account and authority.
900     */
901    public long insertStartSyncEvent(Account accountName, String authorityName,
902            long now, int source) {
903        long id;
904        synchronized (mAuthorities) {
905            if (DEBUG) Log.v(TAG, "insertStartSyncEvent: account=" + accountName
906                    + " auth=" + authorityName + " source=" + source);
907            AuthorityInfo authority = getAuthorityLocked(accountName, authorityName,
908                    "insertStartSyncEvent");
909            if (authority == null) {
910                return -1;
911            }
912            SyncHistoryItem item = new SyncHistoryItem();
913            item.authorityId = authority.ident;
914            item.historyId = mNextHistoryId++;
915            if (mNextHistoryId < 0) mNextHistoryId = 0;
916            item.eventTime = now;
917            item.source = source;
918            item.event = EVENT_START;
919            mSyncHistory.add(0, item);
920            while (mSyncHistory.size() > MAX_HISTORY) {
921                mSyncHistory.remove(mSyncHistory.size()-1);
922            }
923            id = item.historyId;
924            if (DEBUG) Log.v(TAG, "returning historyId " + id);
925        }
926
927        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS);
928        return id;
929    }
930
931    public static boolean equals(Bundle b1, Bundle b2) {
932        if (b1.size() != b2.size()) {
933            return false;
934        }
935        if (b1.isEmpty()) {
936            return true;
937        }
938        for (String key : b1.keySet()) {
939            if (!b2.containsKey(key)) {
940                return false;
941            }
942            if (!b1.get(key).equals(b2.get(key))) {
943                return false;
944            }
945        }
946        return true;
947    }
948
949    public void stopSyncEvent(long historyId, Bundle extras, long elapsedTime, String resultMessage,
950            long downstreamActivity, long upstreamActivity) {
951        synchronized (mAuthorities) {
952            if (DEBUG) Log.v(TAG, "stopSyncEvent: historyId=" + historyId);
953            SyncHistoryItem item = null;
954            int i = mSyncHistory.size();
955            while (i > 0) {
956                i--;
957                item = mSyncHistory.get(i);
958                if (item.historyId == historyId) {
959                    break;
960                }
961                item = null;
962            }
963
964            if (item == null) {
965                Log.w(TAG, "stopSyncEvent: no history for id " + historyId);
966                return;
967            }
968
969            item.elapsedTime = elapsedTime;
970            item.event = EVENT_STOP;
971            item.mesg = resultMessage;
972            item.downstreamActivity = downstreamActivity;
973            item.upstreamActivity = upstreamActivity;
974
975            SyncStatusInfo status = getOrCreateSyncStatusLocked(item.authorityId);
976
977            status.numSyncs++;
978            status.totalElapsedTime += elapsedTime;
979            switch (item.source) {
980                case SOURCE_LOCAL:
981                    status.numSourceLocal++;
982                    break;
983                case SOURCE_POLL:
984                    status.numSourcePoll++;
985                    break;
986                case SOURCE_USER:
987                    status.numSourceUser++;
988                    break;
989                case SOURCE_SERVER:
990                    status.numSourceServer++;
991                    break;
992                case SOURCE_PERIODIC:
993                    status.numSourcePeriodic++;
994                    AuthorityInfo authority = mAuthorities.get(item.authorityId);
995                    for (int periodicSyncIndex = 0;
996                            periodicSyncIndex < authority.periodicSyncs.size();
997                            periodicSyncIndex++) {
998                        if (equals(extras, authority.periodicSyncs.get(periodicSyncIndex).first)) {
999                            status.setPeriodicSyncTime(periodicSyncIndex, item.eventTime);
1000                        }
1001                    }
1002                    break;
1003            }
1004
1005            boolean writeStatisticsNow = false;
1006            int day = getCurrentDayLocked();
1007            if (mDayStats[0] == null) {
1008                mDayStats[0] = new DayStats(day);
1009            } else if (day != mDayStats[0].day) {
1010                System.arraycopy(mDayStats, 0, mDayStats, 1, mDayStats.length-1);
1011                mDayStats[0] = new DayStats(day);
1012                writeStatisticsNow = true;
1013            } else if (mDayStats[0] == null) {
1014            }
1015            final DayStats ds = mDayStats[0];
1016
1017            final long lastSyncTime = (item.eventTime + elapsedTime);
1018            boolean writeStatusNow = false;
1019            if (MESG_SUCCESS.equals(resultMessage)) {
1020                // - if successful, update the successful columns
1021                if (status.lastSuccessTime == 0 || status.lastFailureTime != 0) {
1022                    writeStatusNow = true;
1023                }
1024                status.lastSuccessTime = lastSyncTime;
1025                status.lastSuccessSource = item.source;
1026                status.lastFailureTime = 0;
1027                status.lastFailureSource = -1;
1028                status.lastFailureMesg = null;
1029                status.initialFailureTime = 0;
1030                ds.successCount++;
1031                ds.successTime += elapsedTime;
1032            } else if (!MESG_CANCELED.equals(resultMessage)) {
1033                if (status.lastFailureTime == 0) {
1034                    writeStatusNow = true;
1035                }
1036                status.lastFailureTime = lastSyncTime;
1037                status.lastFailureSource = item.source;
1038                status.lastFailureMesg = resultMessage;
1039                if (status.initialFailureTime == 0) {
1040                    status.initialFailureTime = lastSyncTime;
1041                }
1042                ds.failureCount++;
1043                ds.failureTime += elapsedTime;
1044            }
1045
1046            if (writeStatusNow) {
1047                writeStatusLocked();
1048            } else if (!hasMessages(MSG_WRITE_STATUS)) {
1049                sendMessageDelayed(obtainMessage(MSG_WRITE_STATUS),
1050                        WRITE_STATUS_DELAY);
1051            }
1052            if (writeStatisticsNow) {
1053                writeStatisticsLocked();
1054            } else if (!hasMessages(MSG_WRITE_STATISTICS)) {
1055                sendMessageDelayed(obtainMessage(MSG_WRITE_STATISTICS),
1056                        WRITE_STATISTICS_DELAY);
1057            }
1058        }
1059
1060        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS);
1061    }
1062
1063    /**
1064     * Return the currently active sync information, or null if there is no
1065     * active sync.  Note that the returned object is the real, live active
1066     * sync object, so be careful what you do with it.
1067     */
1068    public ActiveSyncInfo getActiveSync() {
1069        synchronized (mAuthorities) {
1070            return mActiveSync;
1071        }
1072    }
1073
1074    /**
1075     * Return an array of the current sync status for all authorities.  Note
1076     * that the objects inside the array are the real, live status objects,
1077     * so be careful what you do with them.
1078     */
1079    public ArrayList<SyncStatusInfo> getSyncStatus() {
1080        synchronized (mAuthorities) {
1081            final int N = mSyncStatus.size();
1082            ArrayList<SyncStatusInfo> ops = new ArrayList<SyncStatusInfo>(N);
1083            for (int i=0; i<N; i++) {
1084                ops.add(mSyncStatus.valueAt(i));
1085            }
1086            return ops;
1087        }
1088    }
1089
1090    /**
1091     * Return an array of the current authorities. Note
1092     * that the objects inside the array are the real, live objects,
1093     * so be careful what you do with them.
1094     */
1095    public ArrayList<AuthorityInfo> getAuthorities() {
1096        synchronized (mAuthorities) {
1097            final int N = mAuthorities.size();
1098            ArrayList<AuthorityInfo> infos = new ArrayList<AuthorityInfo>(N);
1099            for (int i=0; i<N; i++) {
1100                infos.add(mAuthorities.valueAt(i));
1101            }
1102            return infos;
1103        }
1104    }
1105
1106    /**
1107     * Returns the status that matches the authority and account.
1108     *
1109     * @param account the account we want to check
1110     * @param authority the authority whose row should be selected
1111     * @return the SyncStatusInfo for the authority
1112     */
1113    public SyncStatusInfo getStatusByAccountAndAuthority(Account account, String authority) {
1114        if (account == null || authority == null) {
1115          throw new IllegalArgumentException();
1116        }
1117        synchronized (mAuthorities) {
1118            final int N = mSyncStatus.size();
1119            for (int i=0; i<N; i++) {
1120                SyncStatusInfo cur = mSyncStatus.valueAt(i);
1121                AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
1122
1123                if (ainfo != null && ainfo.authority.equals(authority) &&
1124                    account.equals(ainfo.account)) {
1125                  return cur;
1126                }
1127            }
1128            return null;
1129        }
1130    }
1131
1132    /**
1133     * Return true if the pending status is true of any matching authorities.
1134     */
1135    public boolean isSyncPending(Account account, String authority) {
1136        synchronized (mAuthorities) {
1137            final int N = mSyncStatus.size();
1138            for (int i=0; i<N; i++) {
1139                SyncStatusInfo cur = mSyncStatus.valueAt(i);
1140                AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
1141                if (ainfo == null) {
1142                    continue;
1143                }
1144                if (account != null && !ainfo.account.equals(account)) {
1145                    continue;
1146                }
1147                if (ainfo.authority.equals(authority) && cur.pending) {
1148                    return true;
1149                }
1150            }
1151            return false;
1152        }
1153    }
1154
1155    /**
1156     * Return an array of the current sync status for all authorities.  Note
1157     * that the objects inside the array are the real, live status objects,
1158     * so be careful what you do with them.
1159     */
1160    public ArrayList<SyncHistoryItem> getSyncHistory() {
1161        synchronized (mAuthorities) {
1162            final int N = mSyncHistory.size();
1163            ArrayList<SyncHistoryItem> items = new ArrayList<SyncHistoryItem>(N);
1164            for (int i=0; i<N; i++) {
1165                items.add(mSyncHistory.get(i));
1166            }
1167            return items;
1168        }
1169    }
1170
1171    /**
1172     * Return an array of the current per-day statistics.  Note
1173     * that the objects inside the array are the real, live status objects,
1174     * so be careful what you do with them.
1175     */
1176    public DayStats[] getDayStatistics() {
1177        synchronized (mAuthorities) {
1178            DayStats[] ds = new DayStats[mDayStats.length];
1179            System.arraycopy(mDayStats, 0, ds, 0, ds.length);
1180            return ds;
1181        }
1182    }
1183
1184    /**
1185     * If sync is failing for any of the provider/accounts then determine the time at which it
1186     * started failing and return the earliest time over all the provider/accounts. If none are
1187     * failing then return 0.
1188     */
1189    public long getInitialSyncFailureTime() {
1190        synchronized (mAuthorities) {
1191            if (!mMasterSyncAutomatically) {
1192                return 0;
1193            }
1194
1195            long oldest = 0;
1196            int i = mSyncStatus.size();
1197            while (i > 0) {
1198                i--;
1199                SyncStatusInfo stats = mSyncStatus.valueAt(i);
1200                AuthorityInfo authority = mAuthorities.get(stats.authorityId);
1201                if (authority != null && authority.enabled) {
1202                    if (oldest == 0 || stats.initialFailureTime < oldest) {
1203                        oldest = stats.initialFailureTime;
1204                    }
1205                }
1206            }
1207
1208            return oldest;
1209        }
1210    }
1211
1212    private int getCurrentDayLocked() {
1213        mCal.setTimeInMillis(System.currentTimeMillis());
1214        final int dayOfYear = mCal.get(Calendar.DAY_OF_YEAR);
1215        if (mYear != mCal.get(Calendar.YEAR)) {
1216            mYear = mCal.get(Calendar.YEAR);
1217            mCal.clear();
1218            mCal.set(Calendar.YEAR, mYear);
1219            mYearInDays = (int)(mCal.getTimeInMillis()/86400000);
1220        }
1221        return dayOfYear + mYearInDays;
1222    }
1223
1224    /**
1225     * Retrieve an authority, returning null if one does not exist.
1226     *
1227     * @param accountName The name of the account for the authority.
1228     * @param authorityName The name of the authority itself.
1229     * @param tag If non-null, this will be used in a log message if the
1230     * requested authority does not exist.
1231     */
1232    private AuthorityInfo getAuthorityLocked(Account accountName, String authorityName,
1233            String tag) {
1234        AccountInfo account = mAccounts.get(accountName);
1235        if (account == null) {
1236            if (tag != null) {
1237                if (Log.isLoggable(TAG, Log.VERBOSE)) {
1238                    Log.v(TAG, tag + ": unknown account " + accountName);
1239                }
1240            }
1241            return null;
1242        }
1243        AuthorityInfo authority = account.authorities.get(authorityName);
1244        if (authority == null) {
1245            if (tag != null) {
1246                if (Log.isLoggable(TAG, Log.VERBOSE)) {
1247                    Log.v(TAG, tag + ": unknown authority " + authorityName);
1248                }
1249            }
1250            return null;
1251        }
1252
1253        return authority;
1254    }
1255
1256    private AuthorityInfo getOrCreateAuthorityLocked(Account accountName,
1257            String authorityName, int ident, boolean doWrite) {
1258        AccountInfo account = mAccounts.get(accountName);
1259        if (account == null) {
1260            account = new AccountInfo(accountName);
1261            mAccounts.put(accountName, account);
1262        }
1263        AuthorityInfo authority = account.authorities.get(authorityName);
1264        if (authority == null) {
1265            if (ident < 0) {
1266                // Look for a new identifier for this authority.
1267                final int N = mAuthorities.size();
1268                ident = 0;
1269                for (int i=0; i<N; i++) {
1270                    if (mAuthorities.valueAt(i).ident > ident) {
1271                        break;
1272                    }
1273                    ident++;
1274                }
1275            }
1276            if (DEBUG) Log.v(TAG, "created a new AuthorityInfo for " + accountName
1277                    + ", provider " + authorityName);
1278            authority = new AuthorityInfo(accountName, authorityName, ident);
1279            account.authorities.put(authorityName, authority);
1280            mAuthorities.put(ident, authority);
1281            if (doWrite) {
1282                writeAccountInfoLocked();
1283            }
1284        }
1285
1286        return authority;
1287    }
1288
1289    private void removeAuthorityLocked(Account account, String authorityName) {
1290        AccountInfo accountInfo = mAccounts.get(account);
1291        if (accountInfo != null) {
1292            final AuthorityInfo authorityInfo = accountInfo.authorities.remove(authorityName);
1293            if (authorityInfo != null) {
1294                mAuthorities.remove(authorityInfo.ident);
1295                writeAccountInfoLocked();
1296            }
1297        }
1298    }
1299
1300    public SyncStatusInfo getOrCreateSyncStatus(AuthorityInfo authority) {
1301        synchronized (mAuthorities) {
1302            return getOrCreateSyncStatusLocked(authority.ident);
1303        }
1304    }
1305
1306    private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) {
1307        SyncStatusInfo status = mSyncStatus.get(authorityId);
1308        if (status == null) {
1309            status = new SyncStatusInfo(authorityId);
1310            mSyncStatus.put(authorityId, status);
1311        }
1312        return status;
1313    }
1314
1315    public void writeAllState() {
1316        synchronized (mAuthorities) {
1317            // Account info is always written so no need to do it here.
1318
1319            if (mNumPendingFinished > 0) {
1320                // Only write these if they are out of date.
1321                writePendingOperationsLocked();
1322            }
1323
1324            // Just always write these...  they are likely out of date.
1325            writeStatusLocked();
1326            writeStatisticsLocked();
1327        }
1328    }
1329
1330    /**
1331     * public for testing
1332     */
1333    public void clearAndReadState() {
1334        synchronized (mAuthorities) {
1335            mAuthorities.clear();
1336            mAccounts.clear();
1337            mPendingOperations.clear();
1338            mSyncStatus.clear();
1339            mSyncHistory.clear();
1340
1341            readAccountInfoLocked();
1342            readStatusLocked();
1343            readPendingOperationsLocked();
1344            readStatisticsLocked();
1345            readLegacyAccountInfoLocked();
1346        }
1347    }
1348
1349    /**
1350     * Read all account information back in to the initial engine state.
1351     */
1352    private void readAccountInfoLocked() {
1353        boolean writeNeeded = false;
1354        FileInputStream fis = null;
1355        try {
1356            fis = mAccountInfoFile.openRead();
1357            if (DEBUG_FILE) Log.v(TAG, "Reading " + mAccountInfoFile.getBaseFile());
1358            XmlPullParser parser = Xml.newPullParser();
1359            parser.setInput(fis, null);
1360            int eventType = parser.getEventType();
1361            while (eventType != XmlPullParser.START_TAG) {
1362                eventType = parser.next();
1363            }
1364            String tagName = parser.getName();
1365            if ("accounts".equals(tagName)) {
1366                String listen = parser.getAttributeValue(
1367                        null, "listen-for-tickles");
1368                String versionString = parser.getAttributeValue(null, "version");
1369                int version;
1370                try {
1371                    version = (versionString == null) ? 0 : Integer.parseInt(versionString);
1372                } catch (NumberFormatException e) {
1373                    version = 0;
1374                }
1375                if (version < ACCOUNTS_VERSION) {
1376                    writeNeeded = true;
1377                }
1378                mMasterSyncAutomatically = listen == null
1379                            || Boolean.parseBoolean(listen);
1380                eventType = parser.next();
1381                AuthorityInfo authority = null;
1382                Pair<Bundle, Long> periodicSync = null;
1383                do {
1384                    if (eventType == XmlPullParser.START_TAG) {
1385                        tagName = parser.getName();
1386                        if (parser.getDepth() == 2) {
1387                            if ("authority".equals(tagName)) {
1388                                authority = parseAuthority(parser, version);
1389                                periodicSync = null;
1390                            }
1391                        } else if (parser.getDepth() == 3) {
1392                            if ("periodicSync".equals(tagName) && authority != null) {
1393                                periodicSync = parsePeriodicSync(parser, authority);
1394                            }
1395                        } else if (parser.getDepth() == 4 && periodicSync != null) {
1396                            if ("extra".equals(tagName)) {
1397                                parseExtra(parser, periodicSync);
1398                            }
1399                        }
1400                    }
1401                    eventType = parser.next();
1402                } while (eventType != XmlPullParser.END_DOCUMENT);
1403            }
1404        } catch (XmlPullParserException e) {
1405            Log.w(TAG, "Error reading accounts", e);
1406            return;
1407        } catch (java.io.IOException e) {
1408            if (fis == null) Log.i(TAG, "No initial accounts");
1409            else Log.w(TAG, "Error reading accounts", e);
1410            return;
1411        } finally {
1412            if (fis != null) {
1413                try {
1414                    fis.close();
1415                } catch (java.io.IOException e1) {
1416                }
1417            }
1418        }
1419
1420        if (maybeMigrateSettingsForRenamedAuthorities()) {
1421            writeNeeded = true;
1422        }
1423
1424        if (writeNeeded) {
1425            writeAccountInfoLocked();
1426        }
1427    }
1428
1429    /**
1430     * some authority names have changed. copy over their settings and delete the old ones
1431     * @return true if a change was made
1432     */
1433    private boolean maybeMigrateSettingsForRenamedAuthorities() {
1434        boolean writeNeeded = false;
1435
1436        ArrayList<AuthorityInfo> authoritiesToRemove = new ArrayList<AuthorityInfo>();
1437        final int N = mAuthorities.size();
1438        for (int i=0; i<N; i++) {
1439            AuthorityInfo authority = mAuthorities.valueAt(i);
1440            // skip this authority if it isn't one of the renamed ones
1441            final String newAuthorityName = sAuthorityRenames.get(authority.authority);
1442            if (newAuthorityName == null) {
1443                continue;
1444            }
1445
1446            // remember this authority so we can remove it later. we can't remove it
1447            // now without messing up this loop iteration
1448            authoritiesToRemove.add(authority);
1449
1450            // this authority isn't enabled, no need to copy it to the new authority name since
1451            // the default is "disabled"
1452            if (!authority.enabled) {
1453                continue;
1454            }
1455
1456            // if we already have a record of this new authority then don't copy over the settings
1457            if (getAuthorityLocked(authority.account, newAuthorityName, "cleanup") != null) {
1458                continue;
1459            }
1460
1461            AuthorityInfo newAuthority = getOrCreateAuthorityLocked(authority.account,
1462                    newAuthorityName, -1 /* ident */, false /* doWrite */);
1463            newAuthority.enabled = true;
1464            writeNeeded = true;
1465        }
1466
1467        for (AuthorityInfo authorityInfo : authoritiesToRemove) {
1468            removeAuthorityLocked(authorityInfo.account, authorityInfo.authority);
1469            writeNeeded = true;
1470        }
1471
1472        return writeNeeded;
1473    }
1474
1475    private AuthorityInfo parseAuthority(XmlPullParser parser, int version) {
1476        AuthorityInfo authority = null;
1477        int id = -1;
1478        try {
1479            id = Integer.parseInt(parser.getAttributeValue(
1480                    null, "id"));
1481        } catch (NumberFormatException e) {
1482            Log.e(TAG, "error parsing the id of the authority", e);
1483        } catch (NullPointerException e) {
1484            Log.e(TAG, "the id of the authority is null", e);
1485        }
1486        if (id >= 0) {
1487            String authorityName = parser.getAttributeValue(null, "authority");
1488            String enabled = parser.getAttributeValue(null, "enabled");
1489            String syncable = parser.getAttributeValue(null, "syncable");
1490            String accountName = parser.getAttributeValue(null, "account");
1491            String accountType = parser.getAttributeValue(null, "type");
1492            if (accountType == null) {
1493                accountType = "com.google";
1494                syncable = "unknown";
1495            }
1496            authority = mAuthorities.get(id);
1497            if (DEBUG_FILE) Log.v(TAG, "Adding authority: account="
1498                    + accountName + " auth=" + authorityName
1499                    + " enabled=" + enabled
1500                    + " syncable=" + syncable);
1501            if (authority == null) {
1502                if (DEBUG_FILE) Log.v(TAG, "Creating entry");
1503                authority = getOrCreateAuthorityLocked(
1504                        new Account(accountName, accountType), authorityName, id, false);
1505                // If the version is 0 then we are upgrading from a file format that did not
1506                // know about periodic syncs. In that case don't clear the list since we
1507                // want the default, which is a daily periodioc sync.
1508                // Otherwise clear out this default list since we will populate it later with
1509                // the periodic sync descriptions that are read from the configuration file.
1510                if (version > 0) {
1511                    authority.periodicSyncs.clear();
1512                }
1513            }
1514            if (authority != null) {
1515                authority.enabled = enabled == null || Boolean.parseBoolean(enabled);
1516                if ("unknown".equals(syncable)) {
1517                    authority.syncable = -1;
1518                } else {
1519                    authority.syncable =
1520                            (syncable == null || Boolean.parseBoolean(syncable)) ? 1 : 0;
1521                }
1522            } else {
1523                Log.w(TAG, "Failure adding authority: account="
1524                        + accountName + " auth=" + authorityName
1525                        + " enabled=" + enabled
1526                        + " syncable=" + syncable);
1527            }
1528        }
1529
1530        return authority;
1531    }
1532
1533    private Pair<Bundle, Long> parsePeriodicSync(XmlPullParser parser, AuthorityInfo authority) {
1534        Bundle extras = new Bundle();
1535        String periodValue = parser.getAttributeValue(null, "period");
1536        final long period;
1537        try {
1538            period = Long.parseLong(periodValue);
1539        } catch (NumberFormatException e) {
1540            Log.e(TAG, "error parsing the period of a periodic sync", e);
1541            return null;
1542        } catch (NullPointerException e) {
1543            Log.e(TAG, "the period of a periodic sync is null", e);
1544            return null;
1545        }
1546        final Pair<Bundle, Long> periodicSync = Pair.create(extras, period);
1547        authority.periodicSyncs.add(periodicSync);
1548
1549        return periodicSync;
1550    }
1551
1552    private void parseExtra(XmlPullParser parser, Pair<Bundle, Long> periodicSync) {
1553        final Bundle extras = periodicSync.first;
1554        String name = parser.getAttributeValue(null, "name");
1555        String type = parser.getAttributeValue(null, "type");
1556        String value1 = parser.getAttributeValue(null, "value1");
1557        String value2 = parser.getAttributeValue(null, "value2");
1558
1559        try {
1560            if ("long".equals(type)) {
1561                extras.putLong(name, Long.parseLong(value1));
1562            } else if ("integer".equals(type)) {
1563                extras.putInt(name, Integer.parseInt(value1));
1564            } else if ("double".equals(type)) {
1565                extras.putDouble(name, Double.parseDouble(value1));
1566            } else if ("float".equals(type)) {
1567                extras.putFloat(name, Float.parseFloat(value1));
1568            } else if ("boolean".equals(type)) {
1569                extras.putBoolean(name, Boolean.parseBoolean(value1));
1570            } else if ("string".equals(type)) {
1571                extras.putString(name, value1);
1572            } else if ("account".equals(type)) {
1573                extras.putParcelable(name, new Account(value1, value2));
1574            }
1575        } catch (NumberFormatException e) {
1576            Log.e(TAG, "error parsing bundle value", e);
1577        } catch (NullPointerException e) {
1578            Log.e(TAG, "error parsing bundle value", e);
1579        }
1580    }
1581
1582    /**
1583     * Write all account information to the account file.
1584     */
1585    private void writeAccountInfoLocked() {
1586        if (DEBUG_FILE) Log.v(TAG, "Writing new " + mAccountInfoFile.getBaseFile());
1587        FileOutputStream fos = null;
1588
1589        try {
1590            fos = mAccountInfoFile.startWrite();
1591            XmlSerializer out = new FastXmlSerializer();
1592            out.setOutput(fos, "utf-8");
1593            out.startDocument(null, true);
1594            out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
1595
1596            out.startTag(null, "accounts");
1597            out.attribute(null, "version", Integer.toString(ACCOUNTS_VERSION));
1598            if (!mMasterSyncAutomatically) {
1599                out.attribute(null, "listen-for-tickles", "false");
1600            }
1601
1602            final int N = mAuthorities.size();
1603            for (int i=0; i<N; i++) {
1604                AuthorityInfo authority = mAuthorities.valueAt(i);
1605                out.startTag(null, "authority");
1606                out.attribute(null, "id", Integer.toString(authority.ident));
1607                out.attribute(null, "account", authority.account.name);
1608                out.attribute(null, "type", authority.account.type);
1609                out.attribute(null, "authority", authority.authority);
1610                out.attribute(null, "enabled", Boolean.toString(authority.enabled));
1611                if (authority.syncable < 0) {
1612                    out.attribute(null, "syncable", "unknown");
1613                } else {
1614                    out.attribute(null, "syncable", Boolean.toString(authority.syncable != 0));
1615                }
1616                for (Pair<Bundle, Long> periodicSync : authority.periodicSyncs) {
1617                    out.startTag(null, "periodicSync");
1618                    out.attribute(null, "period", Long.toString(periodicSync.second));
1619                    final Bundle extras = periodicSync.first;
1620                    for (String key : extras.keySet()) {
1621                        out.startTag(null, "extra");
1622                        out.attribute(null, "name", key);
1623                        final Object value = extras.get(key);
1624                        if (value instanceof Long) {
1625                            out.attribute(null, "type", "long");
1626                            out.attribute(null, "value1", value.toString());
1627                        } else if (value instanceof Integer) {
1628                            out.attribute(null, "type", "integer");
1629                            out.attribute(null, "value1", value.toString());
1630                        } else if (value instanceof Boolean) {
1631                            out.attribute(null, "type", "boolean");
1632                            out.attribute(null, "value1", value.toString());
1633                        } else if (value instanceof Float) {
1634                            out.attribute(null, "type", "float");
1635                            out.attribute(null, "value1", value.toString());
1636                        } else if (value instanceof Double) {
1637                            out.attribute(null, "type", "double");
1638                            out.attribute(null, "value1", value.toString());
1639                        } else if (value instanceof String) {
1640                            out.attribute(null, "type", "string");
1641                            out.attribute(null, "value1", value.toString());
1642                        } else if (value instanceof Account) {
1643                            out.attribute(null, "type", "account");
1644                            out.attribute(null, "value1", ((Account)value).name);
1645                            out.attribute(null, "value2", ((Account)value).type);
1646                        }
1647                        out.endTag(null, "extra");
1648                    }
1649                    out.endTag(null, "periodicSync");
1650                }
1651                out.endTag(null, "authority");
1652            }
1653
1654            out.endTag(null, "accounts");
1655
1656            out.endDocument();
1657
1658            mAccountInfoFile.finishWrite(fos);
1659        } catch (java.io.IOException e1) {
1660            Log.w(TAG, "Error writing accounts", e1);
1661            if (fos != null) {
1662                mAccountInfoFile.failWrite(fos);
1663            }
1664        }
1665    }
1666
1667    static int getIntColumn(Cursor c, String name) {
1668        return c.getInt(c.getColumnIndex(name));
1669    }
1670
1671    static long getLongColumn(Cursor c, String name) {
1672        return c.getLong(c.getColumnIndex(name));
1673    }
1674
1675    /**
1676     * Load sync engine state from the old syncmanager database, and then
1677     * erase it.  Note that we don't deal with pending operations, active
1678     * sync, or history.
1679     */
1680    private void readLegacyAccountInfoLocked() {
1681        // Look for old database to initialize from.
1682        File file = mContext.getDatabasePath("syncmanager.db");
1683        if (!file.exists()) {
1684            return;
1685        }
1686        String path = file.getPath();
1687        SQLiteDatabase db = null;
1688        try {
1689            db = SQLiteDatabase.openDatabase(path, null,
1690                    SQLiteDatabase.OPEN_READONLY);
1691        } catch (SQLiteException e) {
1692        }
1693
1694        if (db != null) {
1695            final boolean hasType = db.getVersion() >= 11;
1696
1697            // Copy in all of the status information, as well as accounts.
1698            if (DEBUG_FILE) Log.v(TAG, "Reading legacy sync accounts db");
1699            SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1700            qb.setTables("stats, status");
1701            HashMap<String,String> map = new HashMap<String,String>();
1702            map.put("_id", "status._id as _id");
1703            map.put("account", "stats.account as account");
1704            if (hasType) {
1705                map.put("account_type", "stats.account_type as account_type");
1706            }
1707            map.put("authority", "stats.authority as authority");
1708            map.put("totalElapsedTime", "totalElapsedTime");
1709            map.put("numSyncs", "numSyncs");
1710            map.put("numSourceLocal", "numSourceLocal");
1711            map.put("numSourcePoll", "numSourcePoll");
1712            map.put("numSourceServer", "numSourceServer");
1713            map.put("numSourceUser", "numSourceUser");
1714            map.put("lastSuccessSource", "lastSuccessSource");
1715            map.put("lastSuccessTime", "lastSuccessTime");
1716            map.put("lastFailureSource", "lastFailureSource");
1717            map.put("lastFailureTime", "lastFailureTime");
1718            map.put("lastFailureMesg", "lastFailureMesg");
1719            map.put("pending", "pending");
1720            qb.setProjectionMap(map);
1721            qb.appendWhere("stats._id = status.stats_id");
1722            Cursor c = qb.query(db, null, null, null, null, null, null);
1723            while (c.moveToNext()) {
1724                String accountName = c.getString(c.getColumnIndex("account"));
1725                String accountType = hasType
1726                        ? c.getString(c.getColumnIndex("account_type")) : null;
1727                if (accountType == null) {
1728                    accountType = "com.google";
1729                }
1730                String authorityName = c.getString(c.getColumnIndex("authority"));
1731                AuthorityInfo authority = this.getOrCreateAuthorityLocked(
1732                        new Account(accountName, accountType),
1733                        authorityName, -1, false);
1734                if (authority != null) {
1735                    int i = mSyncStatus.size();
1736                    boolean found = false;
1737                    SyncStatusInfo st = null;
1738                    while (i > 0) {
1739                        i--;
1740                        st = mSyncStatus.valueAt(i);
1741                        if (st.authorityId == authority.ident) {
1742                            found = true;
1743                            break;
1744                        }
1745                    }
1746                    if (!found) {
1747                        st = new SyncStatusInfo(authority.ident);
1748                        mSyncStatus.put(authority.ident, st);
1749                    }
1750                    st.totalElapsedTime = getLongColumn(c, "totalElapsedTime");
1751                    st.numSyncs = getIntColumn(c, "numSyncs");
1752                    st.numSourceLocal = getIntColumn(c, "numSourceLocal");
1753                    st.numSourcePoll = getIntColumn(c, "numSourcePoll");
1754                    st.numSourceServer = getIntColumn(c, "numSourceServer");
1755                    st.numSourceUser = getIntColumn(c, "numSourceUser");
1756                    st.numSourcePeriodic = 0;
1757                    st.lastSuccessSource = getIntColumn(c, "lastSuccessSource");
1758                    st.lastSuccessTime = getLongColumn(c, "lastSuccessTime");
1759                    st.lastFailureSource = getIntColumn(c, "lastFailureSource");
1760                    st.lastFailureTime = getLongColumn(c, "lastFailureTime");
1761                    st.lastFailureMesg = c.getString(c.getColumnIndex("lastFailureMesg"));
1762                    st.pending = getIntColumn(c, "pending") != 0;
1763                }
1764            }
1765
1766            c.close();
1767
1768            // Retrieve the settings.
1769            qb = new SQLiteQueryBuilder();
1770            qb.setTables("settings");
1771            c = qb.query(db, null, null, null, null, null, null);
1772            while (c.moveToNext()) {
1773                String name = c.getString(c.getColumnIndex("name"));
1774                String value = c.getString(c.getColumnIndex("value"));
1775                if (name == null) continue;
1776                if (name.equals("listen_for_tickles")) {
1777                    setMasterSyncAutomatically(value == null || Boolean.parseBoolean(value));
1778                } else if (name.startsWith("sync_provider_")) {
1779                    String provider = name.substring("sync_provider_".length(),
1780                            name.length());
1781                    int i = mAuthorities.size();
1782                    while (i > 0) {
1783                        i--;
1784                        AuthorityInfo authority = mAuthorities.valueAt(i);
1785                        if (authority.authority.equals(provider)) {
1786                            authority.enabled = value == null || Boolean.parseBoolean(value);
1787                            authority.syncable = 1;
1788                        }
1789                    }
1790                }
1791            }
1792
1793            c.close();
1794
1795            db.close();
1796
1797            writeAccountInfoLocked();
1798            writeStatusLocked();
1799            (new File(path)).delete();
1800        }
1801    }
1802
1803    public static final int STATUS_FILE_END = 0;
1804    public static final int STATUS_FILE_ITEM = 100;
1805
1806    /**
1807     * Read all sync status back in to the initial engine state.
1808     */
1809    private void readStatusLocked() {
1810        if (DEBUG_FILE) Log.v(TAG, "Reading " + mStatusFile.getBaseFile());
1811        try {
1812            byte[] data = mStatusFile.readFully();
1813            Parcel in = Parcel.obtain();
1814            in.unmarshall(data, 0, data.length);
1815            in.setDataPosition(0);
1816            int token;
1817            while ((token=in.readInt()) != STATUS_FILE_END) {
1818                if (token == STATUS_FILE_ITEM) {
1819                    SyncStatusInfo status = new SyncStatusInfo(in);
1820                    if (mAuthorities.indexOfKey(status.authorityId) >= 0) {
1821                        status.pending = false;
1822                        if (DEBUG_FILE) Log.v(TAG, "Adding status for id "
1823                                + status.authorityId);
1824                        mSyncStatus.put(status.authorityId, status);
1825                    }
1826                } else {
1827                    // Ooops.
1828                    Log.w(TAG, "Unknown status token: " + token);
1829                    break;
1830                }
1831            }
1832        } catch (java.io.IOException e) {
1833            Log.i(TAG, "No initial status");
1834        }
1835    }
1836
1837    /**
1838     * Write all sync status to the sync status file.
1839     */
1840    private void writeStatusLocked() {
1841        if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatusFile.getBaseFile());
1842
1843        // The file is being written, so we don't need to have a scheduled
1844        // write until the next change.
1845        removeMessages(MSG_WRITE_STATUS);
1846
1847        FileOutputStream fos = null;
1848        try {
1849            fos = mStatusFile.startWrite();
1850            Parcel out = Parcel.obtain();
1851            final int N = mSyncStatus.size();
1852            for (int i=0; i<N; i++) {
1853                SyncStatusInfo status = mSyncStatus.valueAt(i);
1854                out.writeInt(STATUS_FILE_ITEM);
1855                status.writeToParcel(out, 0);
1856            }
1857            out.writeInt(STATUS_FILE_END);
1858            fos.write(out.marshall());
1859            out.recycle();
1860
1861            mStatusFile.finishWrite(fos);
1862        } catch (java.io.IOException e1) {
1863            Log.w(TAG, "Error writing status", e1);
1864            if (fos != null) {
1865                mStatusFile.failWrite(fos);
1866            }
1867        }
1868    }
1869
1870    public static final int PENDING_OPERATION_VERSION = 2;
1871
1872    /**
1873     * Read all pending operations back in to the initial engine state.
1874     */
1875    private void readPendingOperationsLocked() {
1876        if (DEBUG_FILE) Log.v(TAG, "Reading " + mPendingFile.getBaseFile());
1877        try {
1878            byte[] data = mPendingFile.readFully();
1879            Parcel in = Parcel.obtain();
1880            in.unmarshall(data, 0, data.length);
1881            in.setDataPosition(0);
1882            final int SIZE = in.dataSize();
1883            while (in.dataPosition() < SIZE) {
1884                int version = in.readInt();
1885                if (version != PENDING_OPERATION_VERSION && version != 1) {
1886                    Log.w(TAG, "Unknown pending operation version "
1887                            + version + "; dropping all ops");
1888                    break;
1889                }
1890                int authorityId = in.readInt();
1891                int syncSource = in.readInt();
1892                byte[] flatExtras = in.createByteArray();
1893                boolean expedited;
1894                if (version == PENDING_OPERATION_VERSION) {
1895                    expedited = in.readInt() != 0;
1896                } else {
1897                    expedited = false;
1898                }
1899                AuthorityInfo authority = mAuthorities.get(authorityId);
1900                if (authority != null) {
1901                    Bundle extras = null;
1902                    if (flatExtras != null) {
1903                        extras = unflattenBundle(flatExtras);
1904                    }
1905                    PendingOperation op = new PendingOperation(
1906                            authority.account, syncSource,
1907                            authority.authority, extras, expedited);
1908                    op.authorityId = authorityId;
1909                    op.flatExtras = flatExtras;
1910                    if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account
1911                            + " auth=" + op.authority
1912                            + " src=" + op.syncSource
1913                            + " expedited=" + op.expedited
1914                            + " extras=" + op.extras);
1915                    mPendingOperations.add(op);
1916                }
1917            }
1918        } catch (java.io.IOException e) {
1919            Log.i(TAG, "No initial pending operations");
1920        }
1921    }
1922
1923    private void writePendingOperationLocked(PendingOperation op, Parcel out) {
1924        out.writeInt(PENDING_OPERATION_VERSION);
1925        out.writeInt(op.authorityId);
1926        out.writeInt(op.syncSource);
1927        if (op.flatExtras == null && op.extras != null) {
1928            op.flatExtras = flattenBundle(op.extras);
1929        }
1930        out.writeByteArray(op.flatExtras);
1931        out.writeInt(op.expedited ? 1 : 0);
1932    }
1933
1934    /**
1935     * Write all currently pending ops to the pending ops file.
1936     */
1937    private void writePendingOperationsLocked() {
1938        final int N = mPendingOperations.size();
1939        FileOutputStream fos = null;
1940        try {
1941            if (N == 0) {
1942                if (DEBUG_FILE) Log.v(TAG, "Truncating " + mPendingFile.getBaseFile());
1943                mPendingFile.truncate();
1944                return;
1945            }
1946
1947            if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile());
1948            fos = mPendingFile.startWrite();
1949
1950            Parcel out = Parcel.obtain();
1951            for (int i=0; i<N; i++) {
1952                PendingOperation op = mPendingOperations.get(i);
1953                writePendingOperationLocked(op, out);
1954            }
1955            fos.write(out.marshall());
1956            out.recycle();
1957
1958            mPendingFile.finishWrite(fos);
1959        } catch (java.io.IOException e1) {
1960            Log.w(TAG, "Error writing pending operations", e1);
1961            if (fos != null) {
1962                mPendingFile.failWrite(fos);
1963            }
1964        }
1965    }
1966
1967    /**
1968     * Append the given operation to the pending ops file; if unable to,
1969     * write all pending ops.
1970     */
1971    private void appendPendingOperationLocked(PendingOperation op) {
1972        if (DEBUG_FILE) Log.v(TAG, "Appending to " + mPendingFile.getBaseFile());
1973        FileOutputStream fos = null;
1974        try {
1975            fos = mPendingFile.openAppend();
1976        } catch (java.io.IOException e) {
1977            if (DEBUG_FILE) Log.v(TAG, "Failed append; writing full file");
1978            writePendingOperationsLocked();
1979            return;
1980        }
1981
1982        try {
1983            Parcel out = Parcel.obtain();
1984            writePendingOperationLocked(op, out);
1985            fos.write(out.marshall());
1986            out.recycle();
1987        } catch (java.io.IOException e1) {
1988            Log.w(TAG, "Error writing pending operations", e1);
1989        } finally {
1990            try {
1991                fos.close();
1992            } catch (java.io.IOException e2) {
1993            }
1994        }
1995    }
1996
1997    static private byte[] flattenBundle(Bundle bundle) {
1998        byte[] flatData = null;
1999        Parcel parcel = Parcel.obtain();
2000        try {
2001            bundle.writeToParcel(parcel, 0);
2002            flatData = parcel.marshall();
2003        } finally {
2004            parcel.recycle();
2005        }
2006        return flatData;
2007    }
2008
2009    static private Bundle unflattenBundle(byte[] flatData) {
2010        Bundle bundle;
2011        Parcel parcel = Parcel.obtain();
2012        try {
2013            parcel.unmarshall(flatData, 0, flatData.length);
2014            parcel.setDataPosition(0);
2015            bundle = parcel.readBundle();
2016        } catch (RuntimeException e) {
2017            // A RuntimeException is thrown if we were unable to parse the parcel.
2018            // Create an empty parcel in this case.
2019            bundle = new Bundle();
2020        } finally {
2021            parcel.recycle();
2022        }
2023        return bundle;
2024    }
2025
2026    public static final int STATISTICS_FILE_END = 0;
2027    public static final int STATISTICS_FILE_ITEM_OLD = 100;
2028    public static final int STATISTICS_FILE_ITEM = 101;
2029
2030    /**
2031     * Read all sync statistics back in to the initial engine state.
2032     */
2033    private void readStatisticsLocked() {
2034        try {
2035            byte[] data = mStatisticsFile.readFully();
2036            Parcel in = Parcel.obtain();
2037            in.unmarshall(data, 0, data.length);
2038            in.setDataPosition(0);
2039            int token;
2040            int index = 0;
2041            while ((token=in.readInt()) != STATISTICS_FILE_END) {
2042                if (token == STATISTICS_FILE_ITEM
2043                        || token == STATISTICS_FILE_ITEM_OLD) {
2044                    int day = in.readInt();
2045                    if (token == STATISTICS_FILE_ITEM_OLD) {
2046                        day = day - 2009 + 14245;  // Magic!
2047                    }
2048                    DayStats ds = new DayStats(day);
2049                    ds.successCount = in.readInt();
2050                    ds.successTime = in.readLong();
2051                    ds.failureCount = in.readInt();
2052                    ds.failureTime = in.readLong();
2053                    if (index < mDayStats.length) {
2054                        mDayStats[index] = ds;
2055                        index++;
2056                    }
2057                } else {
2058                    // Ooops.
2059                    Log.w(TAG, "Unknown stats token: " + token);
2060                    break;
2061                }
2062            }
2063        } catch (java.io.IOException e) {
2064            Log.i(TAG, "No initial statistics");
2065        }
2066    }
2067
2068    /**
2069     * Write all sync statistics to the sync status file.
2070     */
2071    private void writeStatisticsLocked() {
2072        if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatisticsFile.getBaseFile());
2073
2074        // The file is being written, so we don't need to have a scheduled
2075        // write until the next change.
2076        removeMessages(MSG_WRITE_STATISTICS);
2077
2078        FileOutputStream fos = null;
2079        try {
2080            fos = mStatisticsFile.startWrite();
2081            Parcel out = Parcel.obtain();
2082            final int N = mDayStats.length;
2083            for (int i=0; i<N; i++) {
2084                DayStats ds = mDayStats[i];
2085                if (ds == null) {
2086                    break;
2087                }
2088                out.writeInt(STATISTICS_FILE_ITEM);
2089                out.writeInt(ds.day);
2090                out.writeInt(ds.successCount);
2091                out.writeLong(ds.successTime);
2092                out.writeInt(ds.failureCount);
2093                out.writeLong(ds.failureTime);
2094            }
2095            out.writeInt(STATISTICS_FILE_END);
2096            fos.write(out.marshall());
2097            out.recycle();
2098
2099            mStatisticsFile.finishWrite(fos);
2100        } catch (java.io.IOException e1) {
2101            Log.w(TAG, "Error writing stats", e1);
2102            if (fos != null) {
2103                mStatisticsFile.failWrite(fos);
2104            }
2105        }
2106    }
2107}
2108