SyncStorageEngine.java revision b763ab265130b5aa59e4de561ba3836ac294fe62
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content;
18
19import com.android.internal.os.AtomicFile;
20import com.android.internal.util.ArrayUtils;
21import com.android.internal.util.FastXmlSerializer;
22
23import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25import org.xmlpull.v1.XmlSerializer;
26
27import android.accounts.Account;
28import android.backup.IBackupManager;
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.os.ServiceManager;
41import android.util.Log;
42import android.util.SparseArray;
43import android.util.Xml;
44
45import java.io.File;
46import java.io.FileInputStream;
47import java.io.FileOutputStream;
48import java.util.ArrayList;
49import java.util.Calendar;
50import java.util.HashMap;
51import java.util.Iterator;
52import java.util.TimeZone;
53
54/**
55 * Singleton that tracks the sync data and overall sync
56 * history on the device.
57 *
58 * @hide
59 */
60public class SyncStorageEngine extends Handler {
61    private static final String TAG = "SyncManager";
62    private static final boolean DEBUG = false;
63    private static final boolean DEBUG_FILE = false;
64
65    // @VisibleForTesting
66    static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
67
68    /** Enum value for a sync start event. */
69    public static final int EVENT_START = 0;
70
71    /** Enum value for a sync stop event. */
72    public static final int EVENT_STOP = 1;
73
74    // TODO: i18n -- grab these out of resources.
75    /** String names for the sync event types. */
76    public static final String[] EVENTS = { "START", "STOP" };
77
78    /** Enum value for a server-initiated sync. */
79    public static final int SOURCE_SERVER = 0;
80
81    /** Enum value for a local-initiated sync. */
82    public static final int SOURCE_LOCAL = 1;
83    /**
84     * Enum value for a poll-based sync (e.g., upon connection to
85     * network)
86     */
87    public static final int SOURCE_POLL = 2;
88
89    /** Enum value for a user-initiated sync. */
90    public static final int SOURCE_USER = 3;
91
92    private static final Intent SYNC_CONNECTION_SETTING_CHANGED_INTENT =
93            new Intent("com.android.sync.SYNC_CONN_STATUS_CHANGED");
94
95    // TODO: i18n -- grab these out of resources.
96    /** String names for the sync source types. */
97    public static final String[] SOURCES = { "SERVER",
98                                             "LOCAL",
99                                             "POLL",
100                                             "USER" };
101
102    // The MESG column will contain one of these or one of the Error types.
103    public static final String MESG_SUCCESS = "success";
104    public static final String MESG_CANCELED = "canceled";
105
106    public static final int MAX_HISTORY = 15;
107
108    private static final int MSG_WRITE_STATUS = 1;
109    private static final long WRITE_STATUS_DELAY = 1000*60*10; // 10 minutes
110
111    private static final int MSG_WRITE_STATISTICS = 2;
112    private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour
113
114    private static final boolean SYNC_ENABLED_DEFAULT = false;
115
116    public static class PendingOperation {
117        final Account account;
118        final int syncSource;
119        final String authority;
120        final Bundle extras;        // note: read-only.
121
122        int authorityId;
123        byte[] flatExtras;
124
125        PendingOperation(Account account, int source,
126                String authority, Bundle extras) {
127            this.account = account;
128            this.syncSource = source;
129            this.authority = authority;
130            this.extras = extras != null ? new Bundle(extras) : extras;
131            this.authorityId = -1;
132        }
133
134        PendingOperation(PendingOperation other) {
135            this.account = other.account;
136            this.syncSource = other.syncSource;
137            this.authority = other.authority;
138            this.extras = other.extras;
139            this.authorityId = other.authorityId;
140        }
141    }
142
143    static class AccountInfo {
144        final Account account;
145        final HashMap<String, AuthorityInfo> authorities =
146                new HashMap<String, AuthorityInfo>();
147
148        AccountInfo(Account account) {
149            this.account = account;
150        }
151    }
152
153    public static class AuthorityInfo {
154        final Account account;
155        final String authority;
156        final int ident;
157        boolean enabled;
158        int syncable;
159
160        AuthorityInfo(Account account, String authority, int ident) {
161            this.account = account;
162            this.authority = authority;
163            this.ident = ident;
164            enabled = SYNC_ENABLED_DEFAULT;
165            syncable = -1; // default to "unknown"
166        }
167    }
168
169    public static class SyncHistoryItem {
170        int authorityId;
171        int historyId;
172        long eventTime;
173        long elapsedTime;
174        int source;
175        int event;
176        long upstreamActivity;
177        long downstreamActivity;
178        String mesg;
179    }
180
181    public static class DayStats {
182        public final int day;
183        public int successCount;
184        public long successTime;
185        public int failureCount;
186        public long failureTime;
187
188        public DayStats(int day) {
189            this.day = day;
190        }
191    }
192
193    // Primary list of all syncable authorities.  Also our global lock.
194    private final SparseArray<AuthorityInfo> mAuthorities =
195            new SparseArray<AuthorityInfo>();
196
197    private final HashMap<Account, AccountInfo> mAccounts =
198        new HashMap<Account, AccountInfo>();
199
200    private final ArrayList<PendingOperation> mPendingOperations =
201            new ArrayList<PendingOperation>();
202
203    private ActiveSyncInfo mActiveSync;
204
205    private final SparseArray<SyncStatusInfo> mSyncStatus =
206            new SparseArray<SyncStatusInfo>();
207
208    private final ArrayList<SyncHistoryItem> mSyncHistory =
209            new ArrayList<SyncHistoryItem>();
210
211    private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners
212            = new RemoteCallbackList<ISyncStatusObserver>();
213
214    // We keep 4 weeks of stats.
215    private final DayStats[] mDayStats = new DayStats[7*4];
216    private final Calendar mCal;
217    private int mYear;
218    private int mYearInDays;
219
220    private final Context mContext;
221    private static volatile SyncStorageEngine sSyncStorageEngine = null;
222
223    /**
224     * This file contains the core engine state: all accounts and the
225     * settings for them.  It must never be lost, and should be changed
226     * infrequently, so it is stored as an XML file.
227     */
228    private final AtomicFile mAccountInfoFile;
229
230    /**
231     * This file contains the current sync status.  We would like to retain
232     * it across boots, but its loss is not the end of the world, so we store
233     * this information as binary data.
234     */
235    private final AtomicFile mStatusFile;
236
237    /**
238     * This file contains sync statistics.  This is purely debugging information
239     * so is written infrequently and can be thrown away at any time.
240     */
241    private final AtomicFile mStatisticsFile;
242
243    /**
244     * This file contains the pending sync operations.  It is a binary file,
245     * which must be updated every time an operation is added or removed,
246     * so we have special handling of it.
247     */
248    private final AtomicFile mPendingFile;
249    private static final int PENDING_FINISH_TO_WRITE = 4;
250    private int mNumPendingFinished = 0;
251
252    private int mNextHistoryId = 0;
253    private boolean mMasterSyncAutomatically = true;
254
255    private SyncStorageEngine(Context context) {
256        mContext = context;
257        sSyncStorageEngine = this;
258
259        mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
260
261        File dataDir = Environment.getDataDirectory();
262        File systemDir = new File(dataDir, "system");
263        File syncDir = new File(systemDir, "sync");
264        mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"));
265        mStatusFile = new AtomicFile(new File(syncDir, "status.bin"));
266        mPendingFile = new AtomicFile(new File(syncDir, "pending.bin"));
267        mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin"));
268
269        readAccountInfoLocked();
270        readStatusLocked();
271        readPendingOperationsLocked();
272        readStatisticsLocked();
273        readLegacyAccountInfoLocked();
274    }
275
276    public static SyncStorageEngine newTestInstance(Context context) {
277        return new SyncStorageEngine(context);
278    }
279
280    public static void init(Context context) {
281        if (sSyncStorageEngine != null) {
282            throw new IllegalStateException("already initialized");
283        }
284        sSyncStorageEngine = new SyncStorageEngine(context);
285    }
286
287    public static SyncStorageEngine getSingleton() {
288        if (sSyncStorageEngine == null) {
289            throw new IllegalStateException("not initialized");
290        }
291        return sSyncStorageEngine;
292    }
293
294    @Override public void handleMessage(Message msg) {
295        if (msg.what == MSG_WRITE_STATUS) {
296            synchronized (mAccounts) {
297                writeStatusLocked();
298            }
299        } else if (msg.what == MSG_WRITE_STATISTICS) {
300            synchronized (mAccounts) {
301                writeStatisticsLocked();
302            }
303        }
304    }
305
306    public void addStatusChangeListener(int mask, ISyncStatusObserver callback) {
307        synchronized (mAuthorities) {
308            mChangeListeners.register(callback, mask);
309        }
310    }
311
312    public void removeStatusChangeListener(ISyncStatusObserver callback) {
313        synchronized (mAuthorities) {
314            mChangeListeners.unregister(callback);
315        }
316    }
317
318    private void reportChange(int which) {
319        ArrayList<ISyncStatusObserver> reports = null;
320        synchronized (mAuthorities) {
321            int i = mChangeListeners.beginBroadcast();
322            while (i > 0) {
323                i--;
324                Integer mask = (Integer)mChangeListeners.getBroadcastCookie(i);
325                if ((which & mask.intValue()) == 0) {
326                    continue;
327                }
328                if (reports == null) {
329                    reports = new ArrayList<ISyncStatusObserver>(i);
330                }
331                reports.add(mChangeListeners.getBroadcastItem(i));
332            }
333            mChangeListeners.finishBroadcast();
334        }
335
336        if (DEBUG) Log.v(TAG, "reportChange " + which + " to: " + reports);
337
338        if (reports != null) {
339            int i = reports.size();
340            while (i > 0) {
341                i--;
342                try {
343                    reports.get(i).onStatusChanged(which);
344                } catch (RemoteException e) {
345                    // The remote callback list will take care of this for us.
346                }
347            }
348        }
349        // Inform the backup manager about a data change
350        IBackupManager ibm = IBackupManager.Stub.asInterface(
351                ServiceManager.getService(Context.BACKUP_SERVICE));
352        if (ibm != null) {
353            try {
354                ibm.dataChanged("com.android.providers.settings");
355            } catch (RemoteException e) {
356                // Try again later
357            }
358        }
359    }
360
361    public boolean getSyncAutomatically(Account account, String providerName) {
362        synchronized (mAuthorities) {
363            if (account != null) {
364                AuthorityInfo authority = getAuthorityLocked(account, providerName,
365                        "getSyncAutomatically");
366                return authority != null && authority.enabled;
367            }
368
369            int i = mAuthorities.size();
370            while (i > 0) {
371                i--;
372                AuthorityInfo authority = mAuthorities.get(i);
373                if (authority.authority.equals(providerName)
374                        && authority.enabled) {
375                    return true;
376                }
377            }
378            return false;
379        }
380    }
381
382    public void setSyncAutomatically(Account account, String providerName, boolean sync) {
383        boolean wasEnabled;
384        synchronized (mAuthorities) {
385            AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false);
386            wasEnabled = authority.enabled;
387            authority.enabled = sync;
388            writeAccountInfoLocked();
389        }
390
391        if (!wasEnabled && sync) {
392            mContext.getContentResolver().requestSync(account, providerName, new Bundle());
393        }
394        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
395    }
396
397    public int getIsSyncable(Account account, String providerName) {
398        synchronized (mAuthorities) {
399            if (account != null) {
400                AuthorityInfo authority = getAuthorityLocked(account, providerName,
401                        "getIsSyncable");
402                if (authority == null) {
403                    return -1;
404                }
405                return authority.syncable;
406            }
407
408            int i = mAuthorities.size();
409            while (i > 0) {
410                i--;
411                AuthorityInfo authority = mAuthorities.get(i);
412                if (authority.authority.equals(providerName)) {
413                    return authority.syncable;
414                }
415            }
416            return -1;
417        }
418    }
419
420    public void setIsSyncable(Account account, String providerName, int syncable) {
421        int oldState;
422        if (syncable > 1) {
423            syncable = 1;
424        } else if (syncable < -1) {
425            syncable = -1;
426        }
427        Log.d(TAG, "setIsSyncable: " + account + ", provider " + providerName + " -> " + syncable);
428        synchronized (mAuthorities) {
429            AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false);
430            oldState = authority.syncable;
431            authority.syncable = syncable;
432            writeAccountInfoLocked();
433        }
434
435        if (oldState <= 0 && syncable > 0) {
436            mContext.getContentResolver().requestSync(account, providerName, new Bundle());
437        }
438        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
439    }
440
441    public void setMasterSyncAutomatically(boolean flag) {
442        boolean old;
443        synchronized (mAuthorities) {
444            old = mMasterSyncAutomatically;
445            mMasterSyncAutomatically = flag;
446            writeAccountInfoLocked();
447        }
448        if (!old && flag) {
449            mContext.getContentResolver().requestSync(null, null, new Bundle());
450        }
451        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
452        mContext.sendBroadcast(SYNC_CONNECTION_SETTING_CHANGED_INTENT);
453    }
454
455    public boolean getMasterSyncAutomatically() {
456        synchronized (mAuthorities) {
457            return mMasterSyncAutomatically;
458        }
459    }
460
461    public AuthorityInfo getAuthority(Account account, String authority) {
462        synchronized (mAuthorities) {
463            return getAuthorityLocked(account, authority, null);
464        }
465    }
466
467    public AuthorityInfo getAuthority(int authorityId) {
468        synchronized (mAuthorities) {
469            return mAuthorities.get(authorityId);
470        }
471    }
472
473    /**
474     * Returns true if there is currently a sync operation for the given
475     * account or authority in the pending list, or actively being processed.
476     */
477    public boolean isSyncActive(Account account, String authority) {
478        synchronized (mAuthorities) {
479            int i = mPendingOperations.size();
480            while (i > 0) {
481                i--;
482                // TODO(fredq): this probably shouldn't be considering
483                // pending operations.
484                PendingOperation op = mPendingOperations.get(i);
485                if (op.account.equals(account) && op.authority.equals(authority)) {
486                    return true;
487                }
488            }
489
490            if (mActiveSync != null) {
491                AuthorityInfo ainfo = getAuthority(mActiveSync.authorityId);
492                if (ainfo != null && ainfo.account.equals(account)
493                        && ainfo.authority.equals(authority)) {
494                    return true;
495                }
496            }
497        }
498
499        return false;
500    }
501
502    public PendingOperation insertIntoPending(PendingOperation op) {
503        synchronized (mAuthorities) {
504            if (DEBUG) Log.v(TAG, "insertIntoPending: account=" + op.account
505                    + " auth=" + op.authority
506                    + " src=" + op.syncSource
507                    + " extras=" + op.extras);
508
509            AuthorityInfo authority = getOrCreateAuthorityLocked(op.account,
510                    op.authority,
511                    -1 /* desired identifier */,
512                    true /* write accounts to storage */);
513            if (authority == null) {
514                return null;
515            }
516
517            op = new PendingOperation(op);
518            op.authorityId = authority.ident;
519            mPendingOperations.add(op);
520            appendPendingOperationLocked(op);
521
522            SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
523            status.pending = true;
524        }
525
526        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING);
527        return op;
528    }
529
530    public boolean deleteFromPending(PendingOperation op) {
531        boolean res = false;
532        synchronized (mAuthorities) {
533            if (DEBUG) Log.v(TAG, "deleteFromPending: account=" + op.account
534                    + " auth=" + op.authority
535                    + " src=" + op.syncSource
536                    + " extras=" + op.extras);
537            if (mPendingOperations.remove(op)) {
538                if (mPendingOperations.size() == 0
539                        || mNumPendingFinished >= PENDING_FINISH_TO_WRITE) {
540                    writePendingOperationsLocked();
541                    mNumPendingFinished = 0;
542                } else {
543                    mNumPendingFinished++;
544                }
545
546                AuthorityInfo authority = getAuthorityLocked(op.account, op.authority,
547                        "deleteFromPending");
548                if (authority != null) {
549                    if (DEBUG) Log.v(TAG, "removing - " + authority);
550                    final int N = mPendingOperations.size();
551                    boolean morePending = false;
552                    for (int i=0; i<N; i++) {
553                        PendingOperation cur = mPendingOperations.get(i);
554                        if (cur.account.equals(op.account)
555                                && cur.authority.equals(op.authority)) {
556                            morePending = true;
557                            break;
558                        }
559                    }
560
561                    if (!morePending) {
562                        if (DEBUG) Log.v(TAG, "no more pending!");
563                        SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
564                        status.pending = false;
565                    }
566                }
567
568                res = true;
569            }
570        }
571
572        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING);
573        return res;
574    }
575
576    public int clearPending() {
577        int num;
578        synchronized (mAuthorities) {
579            if (DEBUG) Log.v(TAG, "clearPending");
580            num = mPendingOperations.size();
581            mPendingOperations.clear();
582            final int N = mSyncStatus.size();
583            for (int i=0; i<N; i++) {
584                mSyncStatus.get(i).pending = false;
585            }
586            writePendingOperationsLocked();
587        }
588        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING);
589        return num;
590    }
591
592    /**
593     * Return a copy of the current array of pending operations.  The
594     * PendingOperation objects are the real objects stored inside, so that
595     * they can be used with deleteFromPending().
596     */
597    public ArrayList<PendingOperation> getPendingOperations() {
598        synchronized (mAuthorities) {
599            return new ArrayList<PendingOperation>(mPendingOperations);
600        }
601    }
602
603    /**
604     * Return the number of currently pending operations.
605     */
606    public int getPendingOperationCount() {
607        synchronized (mAuthorities) {
608            return mPendingOperations.size();
609        }
610    }
611
612    /**
613     * Called when the set of account has changed, given the new array of
614     * active accounts.
615     */
616    public void doDatabaseCleanup(Account[] accounts) {
617        synchronized (mAuthorities) {
618            if (DEBUG) Log.w(TAG, "Updating for new accounts...");
619            SparseArray<AuthorityInfo> removing = new SparseArray<AuthorityInfo>();
620            Iterator<AccountInfo> accIt = mAccounts.values().iterator();
621            while (accIt.hasNext()) {
622                AccountInfo acc = accIt.next();
623                if (!ArrayUtils.contains(accounts, acc.account)) {
624                    // This account no longer exists...
625                    if (DEBUG) Log.w(TAG, "Account removed: " + acc.account);
626                    for (AuthorityInfo auth : acc.authorities.values()) {
627                        removing.put(auth.ident, auth);
628                    }
629                    accIt.remove();
630                }
631            }
632
633            // Clean out all data structures.
634            int i = removing.size();
635            if (i > 0) {
636                while (i > 0) {
637                    i--;
638                    int ident = removing.keyAt(i);
639                    mAuthorities.remove(ident);
640                    int j = mSyncStatus.size();
641                    while (j > 0) {
642                        j--;
643                        if (mSyncStatus.keyAt(j) == ident) {
644                            mSyncStatus.remove(mSyncStatus.keyAt(j));
645                        }
646                    }
647                    j = mSyncHistory.size();
648                    while (j > 0) {
649                        j--;
650                        if (mSyncHistory.get(j).authorityId == ident) {
651                            mSyncHistory.remove(j);
652                        }
653                    }
654                }
655                writeAccountInfoLocked();
656                writeStatusLocked();
657                writePendingOperationsLocked();
658                writeStatisticsLocked();
659            }
660        }
661    }
662
663    /**
664     * Called when the currently active sync is changing (there can only be
665     * one at a time).  Either supply a valid ActiveSyncContext with information
666     * about the sync, or null to stop the currently active sync.
667     */
668    public void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
669        synchronized (mAuthorities) {
670            if (activeSyncContext != null) {
671                if (DEBUG) Log.v(TAG, "setActiveSync: account="
672                        + activeSyncContext.mSyncOperation.account
673                        + " auth=" + activeSyncContext.mSyncOperation.authority
674                        + " src=" + activeSyncContext.mSyncOperation.syncSource
675                        + " extras=" + activeSyncContext.mSyncOperation.extras);
676                if (mActiveSync != null) {
677                    Log.w(TAG, "setActiveSync called with existing active sync!");
678                }
679                AuthorityInfo authority = getAuthorityLocked(
680                        activeSyncContext.mSyncOperation.account,
681                        activeSyncContext.mSyncOperation.authority,
682                        "setActiveSync");
683                if (authority == null) {
684                    return;
685                }
686                mActiveSync = new ActiveSyncInfo(authority.ident,
687                        authority.account, authority.authority,
688                        activeSyncContext.mStartTime);
689            } else {
690                if (DEBUG) Log.v(TAG, "setActiveSync: null");
691                mActiveSync = null;
692            }
693        }
694
695        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE);
696    }
697
698    /**
699     * To allow others to send active change reports, to poke clients.
700     */
701    public void reportActiveChange() {
702        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE);
703    }
704
705    /**
706     * Note that sync has started for the given account and authority.
707     */
708    public long insertStartSyncEvent(Account accountName, String authorityName,
709            long now, int source) {
710        long id;
711        synchronized (mAuthorities) {
712            if (DEBUG) Log.v(TAG, "insertStartSyncEvent: account=" + accountName
713                    + " auth=" + authorityName + " source=" + source);
714            AuthorityInfo authority = getAuthorityLocked(accountName, authorityName,
715                    "insertStartSyncEvent");
716            if (authority == null) {
717                return -1;
718            }
719            SyncHistoryItem item = new SyncHistoryItem();
720            item.authorityId = authority.ident;
721            item.historyId = mNextHistoryId++;
722            if (mNextHistoryId < 0) mNextHistoryId = 0;
723            item.eventTime = now;
724            item.source = source;
725            item.event = EVENT_START;
726            mSyncHistory.add(0, item);
727            while (mSyncHistory.size() > MAX_HISTORY) {
728                mSyncHistory.remove(mSyncHistory.size()-1);
729            }
730            id = item.historyId;
731            if (DEBUG) Log.v(TAG, "returning historyId " + id);
732        }
733
734        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS);
735        return id;
736    }
737
738    public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage,
739            long downstreamActivity, long upstreamActivity) {
740        synchronized (mAuthorities) {
741            if (DEBUG) Log.v(TAG, "stopSyncEvent: historyId=" + historyId);
742            SyncHistoryItem item = null;
743            int i = mSyncHistory.size();
744            while (i > 0) {
745                i--;
746                item = mSyncHistory.get(i);
747                if (item.historyId == historyId) {
748                    break;
749                }
750                item = null;
751            }
752
753            if (item == null) {
754                Log.w(TAG, "stopSyncEvent: no history for id " + historyId);
755                return;
756            }
757
758            item.elapsedTime = elapsedTime;
759            item.event = EVENT_STOP;
760            item.mesg = resultMessage;
761            item.downstreamActivity = downstreamActivity;
762            item.upstreamActivity = upstreamActivity;
763
764            SyncStatusInfo status = getOrCreateSyncStatusLocked(item.authorityId);
765
766            status.numSyncs++;
767            status.totalElapsedTime += elapsedTime;
768            switch (item.source) {
769                case SOURCE_LOCAL:
770                    status.numSourceLocal++;
771                    break;
772                case SOURCE_POLL:
773                    status.numSourcePoll++;
774                    break;
775                case SOURCE_USER:
776                    status.numSourceUser++;
777                    break;
778                case SOURCE_SERVER:
779                    status.numSourceServer++;
780                    break;
781            }
782
783            boolean writeStatisticsNow = false;
784            int day = getCurrentDayLocked();
785            if (mDayStats[0] == null) {
786                mDayStats[0] = new DayStats(day);
787            } else if (day != mDayStats[0].day) {
788                System.arraycopy(mDayStats, 0, mDayStats, 1, mDayStats.length-1);
789                mDayStats[0] = new DayStats(day);
790                writeStatisticsNow = true;
791            } else if (mDayStats[0] == null) {
792            }
793            final DayStats ds = mDayStats[0];
794
795            final long lastSyncTime = (item.eventTime + elapsedTime);
796            boolean writeStatusNow = false;
797            if (MESG_SUCCESS.equals(resultMessage)) {
798                // - if successful, update the successful columns
799                if (status.lastSuccessTime == 0 || status.lastFailureTime != 0) {
800                    writeStatusNow = true;
801                }
802                status.lastSuccessTime = lastSyncTime;
803                status.lastSuccessSource = item.source;
804                status.lastFailureTime = 0;
805                status.lastFailureSource = -1;
806                status.lastFailureMesg = null;
807                status.initialFailureTime = 0;
808                ds.successCount++;
809                ds.successTime += elapsedTime;
810            } else if (!MESG_CANCELED.equals(resultMessage)) {
811                if (status.lastFailureTime == 0) {
812                    writeStatusNow = true;
813                }
814                status.lastFailureTime = lastSyncTime;
815                status.lastFailureSource = item.source;
816                status.lastFailureMesg = resultMessage;
817                if (status.initialFailureTime == 0) {
818                    status.initialFailureTime = lastSyncTime;
819                }
820                ds.failureCount++;
821                ds.failureTime += elapsedTime;
822            }
823
824            if (writeStatusNow) {
825                writeStatusLocked();
826            } else if (!hasMessages(MSG_WRITE_STATUS)) {
827                sendMessageDelayed(obtainMessage(MSG_WRITE_STATUS),
828                        WRITE_STATUS_DELAY);
829            }
830            if (writeStatisticsNow) {
831                writeStatisticsLocked();
832            } else if (!hasMessages(MSG_WRITE_STATISTICS)) {
833                sendMessageDelayed(obtainMessage(MSG_WRITE_STATISTICS),
834                        WRITE_STATISTICS_DELAY);
835            }
836        }
837
838        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS);
839    }
840
841    /**
842     * Return the currently active sync information, or null if there is no
843     * active sync.  Note that the returned object is the real, live active
844     * sync object, so be careful what you do with it.
845     */
846    public ActiveSyncInfo getActiveSync() {
847        synchronized (mAuthorities) {
848            return mActiveSync;
849        }
850    }
851
852    /**
853     * Return an array of the current sync status for all authorities.  Note
854     * that the objects inside the array are the real, live status objects,
855     * so be careful what you do with them.
856     */
857    public ArrayList<SyncStatusInfo> getSyncStatus() {
858        synchronized (mAuthorities) {
859            final int N = mSyncStatus.size();
860            ArrayList<SyncStatusInfo> ops = new ArrayList<SyncStatusInfo>(N);
861            for (int i=0; i<N; i++) {
862                ops.add(mSyncStatus.valueAt(i));
863            }
864            return ops;
865        }
866    }
867
868    /**
869     * Returns the status that matches the authority. If there are multiples accounts for
870     * the authority, the one with the latest "lastSuccessTime" status is returned.
871     * @param authority the authority whose row should be selected
872     * @return the SyncStatusInfo for the authority, or null if none exists
873     */
874    public SyncStatusInfo getStatusByAuthority(String authority) {
875        synchronized (mAuthorities) {
876            SyncStatusInfo best = null;
877            final int N = mSyncStatus.size();
878            for (int i=0; i<N; i++) {
879                SyncStatusInfo cur = mSyncStatus.get(i);
880                AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
881                if (ainfo != null && ainfo.authority.equals(authority)) {
882                    if (best == null) {
883                        best = cur;
884                    } else if (best.lastSuccessTime > cur.lastSuccessTime) {
885                        best = cur;
886                    }
887                }
888            }
889            return best;
890        }
891    }
892
893    /**
894     * Return true if the pending status is true of any matching authorities.
895     */
896    public boolean isSyncPending(Account account, String authority) {
897        synchronized (mAuthorities) {
898            final int N = mSyncStatus.size();
899            for (int i=0; i<N; i++) {
900                SyncStatusInfo cur = mSyncStatus.get(i);
901                AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
902                if (ainfo == null) {
903                    continue;
904                }
905                if (account != null && !ainfo.account.equals(account)) {
906                    continue;
907                }
908                if (ainfo.authority.equals(authority) && cur.pending) {
909                    return true;
910                }
911            }
912            return false;
913        }
914    }
915
916    /**
917     * Return an array of the current sync status for all authorities.  Note
918     * that the objects inside the array are the real, live status objects,
919     * so be careful what you do with them.
920     */
921    public ArrayList<SyncHistoryItem> getSyncHistory() {
922        synchronized (mAuthorities) {
923            final int N = mSyncHistory.size();
924            ArrayList<SyncHistoryItem> items = new ArrayList<SyncHistoryItem>(N);
925            for (int i=0; i<N; i++) {
926                items.add(mSyncHistory.get(i));
927            }
928            return items;
929        }
930    }
931
932    /**
933     * Return an array of the current per-day statistics.  Note
934     * that the objects inside the array are the real, live status objects,
935     * so be careful what you do with them.
936     */
937    public DayStats[] getDayStatistics() {
938        synchronized (mAuthorities) {
939            DayStats[] ds = new DayStats[mDayStats.length];
940            System.arraycopy(mDayStats, 0, ds, 0, ds.length);
941            return ds;
942        }
943    }
944
945    /**
946     * If sync is failing for any of the provider/accounts then determine the time at which it
947     * started failing and return the earliest time over all the provider/accounts. If none are
948     * failing then return 0.
949     */
950    public long getInitialSyncFailureTime() {
951        synchronized (mAuthorities) {
952            if (!mMasterSyncAutomatically) {
953                return 0;
954            }
955
956            long oldest = 0;
957            int i = mSyncStatus.size();
958            while (i > 0) {
959                i--;
960                SyncStatusInfo stats = mSyncStatus.valueAt(i);
961                AuthorityInfo authority = mAuthorities.get(stats.authorityId);
962                if (authority != null && authority.enabled) {
963                    if (oldest == 0 || stats.initialFailureTime < oldest) {
964                        oldest = stats.initialFailureTime;
965                    }
966                }
967            }
968
969            return oldest;
970        }
971    }
972
973    private int getCurrentDayLocked() {
974        mCal.setTimeInMillis(System.currentTimeMillis());
975        final int dayOfYear = mCal.get(Calendar.DAY_OF_YEAR);
976        if (mYear != mCal.get(Calendar.YEAR)) {
977            mYear = mCal.get(Calendar.YEAR);
978            mCal.clear();
979            mCal.set(Calendar.YEAR, mYear);
980            mYearInDays = (int)(mCal.getTimeInMillis()/86400000);
981        }
982        return dayOfYear + mYearInDays;
983    }
984
985    /**
986     * Retrieve an authority, returning null if one does not exist.
987     *
988     * @param accountName The name of the account for the authority.
989     * @param authorityName The name of the authority itself.
990     * @param tag If non-null, this will be used in a log message if the
991     * requested authority does not exist.
992     */
993    private AuthorityInfo getAuthorityLocked(Account accountName, String authorityName,
994            String tag) {
995        AccountInfo account = mAccounts.get(accountName);
996        if (account == null) {
997            if (tag != null) {
998                if (Log.isLoggable(TAG, Log.VERBOSE)) {
999                    Log.v(TAG, tag + ": unknown account " + accountName);
1000                }
1001            }
1002            return null;
1003        }
1004        AuthorityInfo authority = account.authorities.get(authorityName);
1005        if (authority == null) {
1006            if (tag != null) {
1007                if (Log.isLoggable(TAG, Log.VERBOSE)) {
1008                    Log.v(TAG, tag + ": unknown authority " + authorityName);
1009                }
1010            }
1011            return null;
1012        }
1013
1014        return authority;
1015    }
1016
1017    private AuthorityInfo getOrCreateAuthorityLocked(Account accountName,
1018            String authorityName, int ident, boolean doWrite) {
1019        AccountInfo account = mAccounts.get(accountName);
1020        if (account == null) {
1021            account = new AccountInfo(accountName);
1022            mAccounts.put(accountName, account);
1023        }
1024        AuthorityInfo authority = account.authorities.get(authorityName);
1025        if (authority == null) {
1026            if (ident < 0) {
1027                // Look for a new identifier for this authority.
1028                final int N = mAuthorities.size();
1029                ident = 0;
1030                for (int i=0; i<N; i++) {
1031                    if (mAuthorities.valueAt(i).ident > ident) {
1032                        break;
1033                    }
1034                    ident++;
1035                }
1036            }
1037            Log.d(TAG, "created a new AuthorityInfo for " + accountName
1038                    + ", provider " + authorityName);
1039            authority = new AuthorityInfo(accountName, authorityName, ident);
1040            account.authorities.put(authorityName, authority);
1041            mAuthorities.put(ident, authority);
1042            if (doWrite) {
1043                writeAccountInfoLocked();
1044            }
1045        }
1046
1047        return authority;
1048    }
1049
1050    private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) {
1051        SyncStatusInfo status = mSyncStatus.get(authorityId);
1052        if (status == null) {
1053            status = new SyncStatusInfo(authorityId);
1054            mSyncStatus.put(authorityId, status);
1055        }
1056        return status;
1057    }
1058
1059    public void writeAllState() {
1060        synchronized (mAuthorities) {
1061            // Account info is always written so no need to do it here.
1062
1063            if (mNumPendingFinished > 0) {
1064                // Only write these if they are out of date.
1065                writePendingOperationsLocked();
1066            }
1067
1068            // Just always write these...  they are likely out of date.
1069            writeStatusLocked();
1070            writeStatisticsLocked();
1071        }
1072    }
1073
1074    /**
1075     * Read all account information back in to the initial engine state.
1076     */
1077    private void readAccountInfoLocked() {
1078        FileInputStream fis = null;
1079        try {
1080            fis = mAccountInfoFile.openRead();
1081            if (DEBUG_FILE) Log.v(TAG, "Reading " + mAccountInfoFile.getBaseFile());
1082            XmlPullParser parser = Xml.newPullParser();
1083            parser.setInput(fis, null);
1084            int eventType = parser.getEventType();
1085            while (eventType != XmlPullParser.START_TAG) {
1086                eventType = parser.next();
1087            }
1088            String tagName = parser.getName();
1089            if ("accounts".equals(tagName)) {
1090                String listen = parser.getAttributeValue(
1091                        null, "listen-for-tickles");
1092                mMasterSyncAutomatically = listen == null
1093                            || Boolean.parseBoolean(listen);
1094                eventType = parser.next();
1095                do {
1096                    if (eventType == XmlPullParser.START_TAG
1097                            && parser.getDepth() == 2) {
1098                        tagName = parser.getName();
1099                        if ("authority".equals(tagName)) {
1100                            int id = -1;
1101                            try {
1102                                id = Integer.parseInt(parser.getAttributeValue(
1103                                        null, "id"));
1104                            } catch (NumberFormatException e) {
1105                            } catch (NullPointerException e) {
1106                            }
1107                            if (id >= 0) {
1108                                String accountName = parser.getAttributeValue(
1109                                        null, "account");
1110                                String accountType = parser.getAttributeValue(
1111                                        null, "type");
1112                                if (accountType == null) {
1113                                    accountType = "com.google.GAIA";
1114                                }
1115                                String authorityName = parser.getAttributeValue(
1116                                        null, "authority");
1117                                String enabled = parser.getAttributeValue(
1118                                        null, "enabled");
1119                                String syncable = parser.getAttributeValue(null, "syncable");
1120                                AuthorityInfo authority = mAuthorities.get(id);
1121                                if (DEBUG_FILE) Log.v(TAG, "Adding authority: account="
1122                                        + accountName + " auth=" + authorityName
1123                                        + " enabled=" + enabled
1124                                        + " syncable=" + syncable);
1125                                if (authority == null) {
1126                                    if (DEBUG_FILE) Log.v(TAG, "Creating entry");
1127                                    authority = getOrCreateAuthorityLocked(
1128                                            new Account(accountName, accountType),
1129                                            authorityName, id, false);
1130                                }
1131                                if (authority != null) {
1132                                    authority.enabled = enabled == null
1133                                            || Boolean.parseBoolean(enabled);
1134                                    if ("unknown".equals(syncable)) {
1135                                        authority.syncable = -1;
1136                                    } else {
1137                                        authority.syncable =
1138                                                (syncable == null || Boolean.parseBoolean(enabled))
1139                                                        ? 1
1140                                                        : 0;
1141                                    }
1142                                } else {
1143                                    Log.w(TAG, "Failure adding authority: account="
1144                                            + accountName + " auth=" + authorityName
1145                                            + " enabled=" + enabled
1146                                            + " syncable=" + syncable);
1147                                }
1148                            }
1149                        }
1150                    }
1151                    eventType = parser.next();
1152                } while (eventType != XmlPullParser.END_DOCUMENT);
1153            }
1154        } catch (XmlPullParserException e) {
1155            Log.w(TAG, "Error reading accounts", e);
1156        } catch (java.io.IOException e) {
1157            if (fis == null) Log.i(TAG, "No initial accounts");
1158            else Log.w(TAG, "Error reading accounts", e);
1159        } finally {
1160            if (fis != null) {
1161                try {
1162                    fis.close();
1163                } catch (java.io.IOException e1) {
1164                }
1165            }
1166        }
1167    }
1168
1169    /**
1170     * Write all account information to the account file.
1171     */
1172    private void writeAccountInfoLocked() {
1173        if (DEBUG_FILE) Log.v(TAG, "Writing new " + mAccountInfoFile.getBaseFile());
1174        FileOutputStream fos = null;
1175
1176        try {
1177            fos = mAccountInfoFile.startWrite();
1178            XmlSerializer out = new FastXmlSerializer();
1179            out.setOutput(fos, "utf-8");
1180            out.startDocument(null, true);
1181            out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
1182
1183            out.startTag(null, "accounts");
1184            if (!mMasterSyncAutomatically) {
1185                out.attribute(null, "listen-for-tickles", "false");
1186            }
1187
1188            final int N = mAuthorities.size();
1189            for (int i=0; i<N; i++) {
1190                AuthorityInfo authority = mAuthorities.get(i);
1191                out.startTag(null, "authority");
1192                out.attribute(null, "id", Integer.toString(authority.ident));
1193                out.attribute(null, "account", authority.account.name);
1194                out.attribute(null, "type", authority.account.type);
1195                out.attribute(null, "authority", authority.authority);
1196                if (!authority.enabled) {
1197                    out.attribute(null, "enabled", "false");
1198                }
1199                if (authority.syncable < 0) {
1200                    out.attribute(null, "syncable", "unknown");
1201                } else if (authority.syncable == 0) {
1202                    out.attribute(null, "syncable", "false");
1203                }
1204                out.endTag(null, "authority");
1205            }
1206
1207            out.endTag(null, "accounts");
1208
1209            out.endDocument();
1210
1211            mAccountInfoFile.finishWrite(fos);
1212        } catch (java.io.IOException e1) {
1213            Log.w(TAG, "Error writing accounts", e1);
1214            if (fos != null) {
1215                mAccountInfoFile.failWrite(fos);
1216            }
1217        }
1218    }
1219
1220    static int getIntColumn(Cursor c, String name) {
1221        return c.getInt(c.getColumnIndex(name));
1222    }
1223
1224    static long getLongColumn(Cursor c, String name) {
1225        return c.getLong(c.getColumnIndex(name));
1226    }
1227
1228    /**
1229     * Load sync engine state from the old syncmanager database, and then
1230     * erase it.  Note that we don't deal with pending operations, active
1231     * sync, or history.
1232     */
1233    private void readLegacyAccountInfoLocked() {
1234        // Look for old database to initialize from.
1235        File file = mContext.getDatabasePath("syncmanager.db");
1236        if (!file.exists()) {
1237            return;
1238        }
1239        String path = file.getPath();
1240        SQLiteDatabase db = null;
1241        try {
1242            db = SQLiteDatabase.openDatabase(path, null,
1243                    SQLiteDatabase.OPEN_READONLY);
1244        } catch (SQLiteException e) {
1245        }
1246
1247        if (db != null) {
1248            final boolean hasType = db.getVersion() >= 11;
1249
1250            // Copy in all of the status information, as well as accounts.
1251            if (DEBUG_FILE) Log.v(TAG, "Reading legacy sync accounts db");
1252            SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1253            qb.setTables("stats, status");
1254            HashMap<String,String> map = new HashMap<String,String>();
1255            map.put("_id", "status._id as _id");
1256            map.put("account", "stats.account as account");
1257            if (hasType) {
1258                map.put("account_type", "stats.account_type as account_type");
1259            }
1260            map.put("authority", "stats.authority as authority");
1261            map.put("totalElapsedTime", "totalElapsedTime");
1262            map.put("numSyncs", "numSyncs");
1263            map.put("numSourceLocal", "numSourceLocal");
1264            map.put("numSourcePoll", "numSourcePoll");
1265            map.put("numSourceServer", "numSourceServer");
1266            map.put("numSourceUser", "numSourceUser");
1267            map.put("lastSuccessSource", "lastSuccessSource");
1268            map.put("lastSuccessTime", "lastSuccessTime");
1269            map.put("lastFailureSource", "lastFailureSource");
1270            map.put("lastFailureTime", "lastFailureTime");
1271            map.put("lastFailureMesg", "lastFailureMesg");
1272            map.put("pending", "pending");
1273            qb.setProjectionMap(map);
1274            qb.appendWhere("stats._id = status.stats_id");
1275            Cursor c = qb.query(db, null, null, null, null, null, null);
1276            while (c.moveToNext()) {
1277                String accountName = c.getString(c.getColumnIndex("account"));
1278                String accountType = hasType
1279                        ? c.getString(c.getColumnIndex("account_type")) : null;
1280                if (accountType == null) {
1281                    accountType = "com.google.GAIA";
1282                }
1283                String authorityName = c.getString(c.getColumnIndex("authority"));
1284                AuthorityInfo authority = this.getOrCreateAuthorityLocked(
1285                        new Account(accountName, accountType),
1286                        authorityName, -1, false);
1287                if (authority != null) {
1288                    int i = mSyncStatus.size();
1289                    boolean found = false;
1290                    SyncStatusInfo st = null;
1291                    while (i > 0) {
1292                        i--;
1293                        st = mSyncStatus.get(i);
1294                        if (st.authorityId == authority.ident) {
1295                            found = true;
1296                            break;
1297                        }
1298                    }
1299                    if (!found) {
1300                        st = new SyncStatusInfo(authority.ident);
1301                        mSyncStatus.put(authority.ident, st);
1302                    }
1303                    st.totalElapsedTime = getLongColumn(c, "totalElapsedTime");
1304                    st.numSyncs = getIntColumn(c, "numSyncs");
1305                    st.numSourceLocal = getIntColumn(c, "numSourceLocal");
1306                    st.numSourcePoll = getIntColumn(c, "numSourcePoll");
1307                    st.numSourceServer = getIntColumn(c, "numSourceServer");
1308                    st.numSourceUser = getIntColumn(c, "numSourceUser");
1309                    st.lastSuccessSource = getIntColumn(c, "lastSuccessSource");
1310                    st.lastSuccessTime = getLongColumn(c, "lastSuccessTime");
1311                    st.lastFailureSource = getIntColumn(c, "lastFailureSource");
1312                    st.lastFailureTime = getLongColumn(c, "lastFailureTime");
1313                    st.lastFailureMesg = c.getString(c.getColumnIndex("lastFailureMesg"));
1314                    st.pending = getIntColumn(c, "pending") != 0;
1315                }
1316            }
1317
1318            c.close();
1319
1320            // Retrieve the settings.
1321            qb = new SQLiteQueryBuilder();
1322            qb.setTables("settings");
1323            c = qb.query(db, null, null, null, null, null, null);
1324            while (c.moveToNext()) {
1325                String name = c.getString(c.getColumnIndex("name"));
1326                String value = c.getString(c.getColumnIndex("value"));
1327                if (name == null) continue;
1328                if (name.equals("listen_for_tickles")) {
1329                    setMasterSyncAutomatically(value == null || Boolean.parseBoolean(value));
1330                } else if (name.startsWith("sync_provider_")) {
1331                    String provider = name.substring("sync_provider_".length(),
1332                            name.length());
1333                    int i = mAuthorities.size();
1334                    while (i > 0) {
1335                        i--;
1336                        AuthorityInfo authority = mAuthorities.get(i);
1337                        if (authority.authority.equals(provider)) {
1338                            authority.enabled = value == null || Boolean.parseBoolean(value);
1339                            authority.syncable = 1;
1340                        }
1341                    }
1342                }
1343            }
1344
1345            c.close();
1346
1347            db.close();
1348
1349            writeAccountInfoLocked();
1350            writeStatusLocked();
1351            (new File(path)).delete();
1352        }
1353    }
1354
1355    public static final int STATUS_FILE_END = 0;
1356    public static final int STATUS_FILE_ITEM = 100;
1357
1358    /**
1359     * Read all sync status back in to the initial engine state.
1360     */
1361    private void readStatusLocked() {
1362        if (DEBUG_FILE) Log.v(TAG, "Reading " + mStatusFile.getBaseFile());
1363        try {
1364            byte[] data = mStatusFile.readFully();
1365            Parcel in = Parcel.obtain();
1366            in.unmarshall(data, 0, data.length);
1367            in.setDataPosition(0);
1368            int token;
1369            while ((token=in.readInt()) != STATUS_FILE_END) {
1370                if (token == STATUS_FILE_ITEM) {
1371                    SyncStatusInfo status = new SyncStatusInfo(in);
1372                    if (mAuthorities.indexOfKey(status.authorityId) >= 0) {
1373                        status.pending = false;
1374                        if (DEBUG_FILE) Log.v(TAG, "Adding status for id "
1375                                + status.authorityId);
1376                        mSyncStatus.put(status.authorityId, status);
1377                    }
1378                } else {
1379                    // Ooops.
1380                    Log.w(TAG, "Unknown status token: " + token);
1381                    break;
1382                }
1383            }
1384        } catch (java.io.IOException e) {
1385            Log.i(TAG, "No initial status");
1386        }
1387    }
1388
1389    /**
1390     * Write all sync status to the sync status file.
1391     */
1392    private void writeStatusLocked() {
1393        if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatusFile.getBaseFile());
1394
1395        // The file is being written, so we don't need to have a scheduled
1396        // write until the next change.
1397        removeMessages(MSG_WRITE_STATUS);
1398
1399        FileOutputStream fos = null;
1400        try {
1401            fos = mStatusFile.startWrite();
1402            Parcel out = Parcel.obtain();
1403            final int N = mSyncStatus.size();
1404            for (int i=0; i<N; i++) {
1405                SyncStatusInfo status = mSyncStatus.valueAt(i);
1406                out.writeInt(STATUS_FILE_ITEM);
1407                status.writeToParcel(out, 0);
1408            }
1409            out.writeInt(STATUS_FILE_END);
1410            fos.write(out.marshall());
1411            out.recycle();
1412
1413            mStatusFile.finishWrite(fos);
1414        } catch (java.io.IOException e1) {
1415            Log.w(TAG, "Error writing status", e1);
1416            if (fos != null) {
1417                mStatusFile.failWrite(fos);
1418            }
1419        }
1420    }
1421
1422    public static final int PENDING_OPERATION_VERSION = 1;
1423
1424    /**
1425     * Read all pending operations back in to the initial engine state.
1426     */
1427    private void readPendingOperationsLocked() {
1428        if (DEBUG_FILE) Log.v(TAG, "Reading " + mPendingFile.getBaseFile());
1429        try {
1430            byte[] data = mPendingFile.readFully();
1431            Parcel in = Parcel.obtain();
1432            in.unmarshall(data, 0, data.length);
1433            in.setDataPosition(0);
1434            final int SIZE = in.dataSize();
1435            while (in.dataPosition() < SIZE) {
1436                int version = in.readInt();
1437                if (version != PENDING_OPERATION_VERSION) {
1438                    Log.w(TAG, "Unknown pending operation version "
1439                            + version + "; dropping all ops");
1440                    break;
1441                }
1442                int authorityId = in.readInt();
1443                int syncSource = in.readInt();
1444                byte[] flatExtras = in.createByteArray();
1445                AuthorityInfo authority = mAuthorities.get(authorityId);
1446                if (authority != null) {
1447                    Bundle extras = null;
1448                    if (flatExtras != null) {
1449                        extras = unflattenBundle(flatExtras);
1450                    }
1451                    PendingOperation op = new PendingOperation(
1452                            authority.account, syncSource,
1453                            authority.authority, extras);
1454                    op.authorityId = authorityId;
1455                    op.flatExtras = flatExtras;
1456                    if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account
1457                            + " auth=" + op.authority
1458                            + " src=" + op.syncSource
1459                            + " extras=" + op.extras);
1460                    mPendingOperations.add(op);
1461                }
1462            }
1463        } catch (java.io.IOException e) {
1464            Log.i(TAG, "No initial pending operations");
1465        }
1466    }
1467
1468    private void writePendingOperationLocked(PendingOperation op, Parcel out) {
1469        out.writeInt(PENDING_OPERATION_VERSION);
1470        out.writeInt(op.authorityId);
1471        out.writeInt(op.syncSource);
1472        if (op.flatExtras == null && op.extras != null) {
1473            op.flatExtras = flattenBundle(op.extras);
1474        }
1475        out.writeByteArray(op.flatExtras);
1476    }
1477
1478    /**
1479     * Write all currently pending ops to the pending ops file.
1480     */
1481    private void writePendingOperationsLocked() {
1482        final int N = mPendingOperations.size();
1483        FileOutputStream fos = null;
1484        try {
1485            if (N == 0) {
1486                if (DEBUG_FILE) Log.v(TAG, "Truncating " + mPendingFile.getBaseFile());
1487                mPendingFile.truncate();
1488                return;
1489            }
1490
1491            if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile());
1492            fos = mPendingFile.startWrite();
1493
1494            Parcel out = Parcel.obtain();
1495            for (int i=0; i<N; i++) {
1496                PendingOperation op = mPendingOperations.get(i);
1497                writePendingOperationLocked(op, out);
1498            }
1499            fos.write(out.marshall());
1500            out.recycle();
1501
1502            mPendingFile.finishWrite(fos);
1503        } catch (java.io.IOException e1) {
1504            Log.w(TAG, "Error writing pending operations", e1);
1505            if (fos != null) {
1506                mPendingFile.failWrite(fos);
1507            }
1508        }
1509    }
1510
1511    /**
1512     * Append the given operation to the pending ops file; if unable to,
1513     * write all pending ops.
1514     */
1515    private void appendPendingOperationLocked(PendingOperation op) {
1516        if (DEBUG_FILE) Log.v(TAG, "Appending to " + mPendingFile.getBaseFile());
1517        FileOutputStream fos = null;
1518        try {
1519            fos = mPendingFile.openAppend();
1520        } catch (java.io.IOException e) {
1521            if (DEBUG_FILE) Log.v(TAG, "Failed append; writing full file");
1522            writePendingOperationsLocked();
1523            return;
1524        }
1525
1526        try {
1527            Parcel out = Parcel.obtain();
1528            writePendingOperationLocked(op, out);
1529            fos.write(out.marshall());
1530            out.recycle();
1531        } catch (java.io.IOException e1) {
1532            Log.w(TAG, "Error writing pending operations", e1);
1533        } finally {
1534            try {
1535                fos.close();
1536            } catch (java.io.IOException e2) {
1537            }
1538        }
1539    }
1540
1541    static private byte[] flattenBundle(Bundle bundle) {
1542        byte[] flatData = null;
1543        Parcel parcel = Parcel.obtain();
1544        try {
1545            bundle.writeToParcel(parcel, 0);
1546            flatData = parcel.marshall();
1547        } finally {
1548            parcel.recycle();
1549        }
1550        return flatData;
1551    }
1552
1553    static private Bundle unflattenBundle(byte[] flatData) {
1554        Bundle bundle;
1555        Parcel parcel = Parcel.obtain();
1556        try {
1557            parcel.unmarshall(flatData, 0, flatData.length);
1558            parcel.setDataPosition(0);
1559            bundle = parcel.readBundle();
1560        } catch (RuntimeException e) {
1561            // A RuntimeException is thrown if we were unable to parse the parcel.
1562            // Create an empty parcel in this case.
1563            bundle = new Bundle();
1564        } finally {
1565            parcel.recycle();
1566        }
1567        return bundle;
1568    }
1569
1570    public static final int STATISTICS_FILE_END = 0;
1571    public static final int STATISTICS_FILE_ITEM_OLD = 100;
1572    public static final int STATISTICS_FILE_ITEM = 101;
1573
1574    /**
1575     * Read all sync statistics back in to the initial engine state.
1576     */
1577    private void readStatisticsLocked() {
1578        try {
1579            byte[] data = mStatisticsFile.readFully();
1580            Parcel in = Parcel.obtain();
1581            in.unmarshall(data, 0, data.length);
1582            in.setDataPosition(0);
1583            int token;
1584            int index = 0;
1585            while ((token=in.readInt()) != STATISTICS_FILE_END) {
1586                if (token == STATISTICS_FILE_ITEM
1587                        || token == STATISTICS_FILE_ITEM_OLD) {
1588                    int day = in.readInt();
1589                    if (token == STATISTICS_FILE_ITEM_OLD) {
1590                        day = day - 2009 + 14245;  // Magic!
1591                    }
1592                    DayStats ds = new DayStats(day);
1593                    ds.successCount = in.readInt();
1594                    ds.successTime = in.readLong();
1595                    ds.failureCount = in.readInt();
1596                    ds.failureTime = in.readLong();
1597                    if (index < mDayStats.length) {
1598                        mDayStats[index] = ds;
1599                        index++;
1600                    }
1601                } else {
1602                    // Ooops.
1603                    Log.w(TAG, "Unknown stats token: " + token);
1604                    break;
1605                }
1606            }
1607        } catch (java.io.IOException e) {
1608            Log.i(TAG, "No initial statistics");
1609        }
1610    }
1611
1612    /**
1613     * Write all sync statistics to the sync status file.
1614     */
1615    private void writeStatisticsLocked() {
1616        if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatisticsFile.getBaseFile());
1617
1618        // The file is being written, so we don't need to have a scheduled
1619        // write until the next change.
1620        removeMessages(MSG_WRITE_STATISTICS);
1621
1622        FileOutputStream fos = null;
1623        try {
1624            fos = mStatisticsFile.startWrite();
1625            Parcel out = Parcel.obtain();
1626            final int N = mDayStats.length;
1627            for (int i=0; i<N; i++) {
1628                DayStats ds = mDayStats[i];
1629                if (ds == null) {
1630                    break;
1631                }
1632                out.writeInt(STATISTICS_FILE_ITEM);
1633                out.writeInt(ds.day);
1634                out.writeInt(ds.successCount);
1635                out.writeLong(ds.successTime);
1636                out.writeInt(ds.failureCount);
1637                out.writeLong(ds.failureTime);
1638            }
1639            out.writeInt(STATISTICS_FILE_END);
1640            fos.write(out.marshall());
1641            out.recycle();
1642
1643            mStatisticsFile.finishWrite(fos);
1644        } catch (java.io.IOException e1) {
1645            Log.w(TAG, "Error writing stats", e1);
1646            if (fos != null) {
1647                mStatisticsFile.failWrite(fos);
1648            }
1649        }
1650    }
1651}
1652