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