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