SyncStorageEngine.java revision 3531fdb1d9a0ca536bd7a7a27d35b3e62c318ad9
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content;
18
19import com.android.internal.os.AtomicFile;
20import com.android.internal.util.ArrayUtils;
21import com.android.internal.util.FastXmlSerializer;
22
23import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25import org.xmlpull.v1.XmlSerializer;
26
27import android.accounts.Account;
28import android.database.Cursor;
29import android.database.sqlite.SQLiteDatabase;
30import android.database.sqlite.SQLiteException;
31import android.database.sqlite.SQLiteQueryBuilder;
32import android.os.Bundle;
33import android.os.Environment;
34import android.os.Handler;
35import android.os.Message;
36import android.os.Parcel;
37import android.os.RemoteCallbackList;
38import android.os.RemoteException;
39import android.util.Log;
40import android.util.SparseArray;
41import android.util.Xml;
42
43import java.io.File;
44import java.io.FileInputStream;
45import java.io.FileOutputStream;
46import java.util.ArrayList;
47import java.util.Calendar;
48import java.util.HashMap;
49import java.util.Iterator;
50import java.util.TimeZone;
51
52import com.google.android.collect.Sets;
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    // TODO: i18n -- grab these out of resources.
93    /** String names for the sync source types. */
94    public static final String[] SOURCES = { "SERVER",
95                                             "LOCAL",
96                                             "POLL",
97                                             "USER" };
98
99    // Error types
100    public static final int ERROR_SYNC_ALREADY_IN_PROGRESS = 1;
101    public static final int ERROR_AUTHENTICATION = 2;
102    public static final int ERROR_IO = 3;
103    public static final int ERROR_PARSE = 4;
104    public static final int ERROR_CONFLICT = 5;
105    public static final int ERROR_TOO_MANY_DELETIONS = 6;
106    public static final int ERROR_TOO_MANY_RETRIES = 7;
107    public static final int ERROR_INTERNAL = 8;
108
109    // The MESG column will contain one of these or one of the Error types.
110    public static final String MESG_SUCCESS = "success";
111    public static final String MESG_CANCELED = "canceled";
112
113    public static final int CHANGE_SETTINGS = 1<<0;
114    public static final int CHANGE_PENDING = 1<<1;
115    public static final int CHANGE_ACTIVE = 1<<2;
116    public static final int CHANGE_STATUS = 1<<3;
117    public static final int CHANGE_ALL = 0x7fffffff;
118
119    public static final int MAX_HISTORY = 15;
120
121    private static final int MSG_WRITE_STATUS = 1;
122    private static final long WRITE_STATUS_DELAY = 1000*60*10; // 10 minutes
123
124    private static final int MSG_WRITE_STATISTICS = 2;
125    private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour
126
127    public static class PendingOperation {
128        final Account account;
129        final int syncSource;
130        final String authority;
131        final Bundle extras;        // note: read-only.
132
133        int authorityId;
134        byte[] flatExtras;
135
136        PendingOperation(Account account, int source,
137                String authority, Bundle extras) {
138            this.account = account;
139            this.syncSource = source;
140            this.authority = authority;
141            this.extras = extras != null ? new Bundle(extras) : extras;
142            this.authorityId = -1;
143        }
144
145        PendingOperation(PendingOperation other) {
146            this.account = other.account;
147            this.syncSource = other.syncSource;
148            this.authority = other.authority;
149            this.extras = other.extras;
150            this.authorityId = other.authorityId;
151        }
152    }
153
154    static class AccountInfo {
155        final Account account;
156        final HashMap<String, AuthorityInfo> authorities =
157                new HashMap<String, AuthorityInfo>();
158
159        AccountInfo(Account account) {
160            this.account = account;
161        }
162    }
163
164    public static class AuthorityInfo {
165        final Account account;
166        final String authority;
167        final int ident;
168        boolean enabled;
169
170        AuthorityInfo(Account account, String authority, int ident) {
171            this.account = account;
172            this.authority = authority;
173            this.ident = ident;
174            enabled = true;
175        }
176    }
177
178    public static class SyncHistoryItem {
179        int authorityId;
180        int historyId;
181        long eventTime;
182        long elapsedTime;
183        int source;
184        int event;
185        long upstreamActivity;
186        long downstreamActivity;
187        String mesg;
188    }
189
190    public static class DayStats {
191        public final int day;
192        public int successCount;
193        public long successTime;
194        public int failureCount;
195        public long failureTime;
196
197        public DayStats(int day) {
198            this.day = day;
199        }
200    }
201
202    // Primary list of all syncable authorities.  Also our global lock.
203    private final SparseArray<AuthorityInfo> mAuthorities =
204            new SparseArray<AuthorityInfo>();
205
206    private final HashMap<Account, AccountInfo> mAccounts =
207        new HashMap<Account, AccountInfo>();
208
209    private final ArrayList<PendingOperation> mPendingOperations =
210            new ArrayList<PendingOperation>();
211
212    private ActiveSyncInfo mActiveSync;
213
214    private final SparseArray<SyncStatusInfo> mSyncStatus =
215            new SparseArray<SyncStatusInfo>();
216
217    private final ArrayList<SyncHistoryItem> mSyncHistory =
218            new ArrayList<SyncHistoryItem>();
219
220    private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners
221            = new RemoteCallbackList<ISyncStatusObserver>();
222
223    // We keep 4 weeks of stats.
224    private final DayStats[] mDayStats = new DayStats[7*4];
225    private final Calendar mCal;
226    private int mYear;
227    private int mYearInDays;
228
229    private final Context mContext;
230    private static volatile SyncStorageEngine sSyncStorageEngine = null;
231
232    /**
233     * This file contains the core engine state: all accounts and the
234     * settings for them.  It must never be lost, and should be changed
235     * infrequently, so it is stored as an XML file.
236     */
237    private final AtomicFile mAccountInfoFile;
238
239    /**
240     * This file contains the current sync status.  We would like to retain
241     * it across boots, but its loss is not the end of the world, so we store
242     * this information as binary data.
243     */
244    private final AtomicFile mStatusFile;
245
246    /**
247     * This file contains sync statistics.  This is purely debugging information
248     * so is written infrequently and can be thrown away at any time.
249     */
250    private final AtomicFile mStatisticsFile;
251
252    /**
253     * This file contains the pending sync operations.  It is a binary file,
254     * which must be updated every time an operation is added or removed,
255     * so we have special handling of it.
256     */
257    private final AtomicFile mPendingFile;
258    private static final int PENDING_FINISH_TO_WRITE = 4;
259    private int mNumPendingFinished = 0;
260
261    private int mNextHistoryId = 0;
262    private boolean mListenForTickles = true;
263
264    private SyncStorageEngine(Context context) {
265        mContext = context;
266        sSyncStorageEngine = this;
267
268        mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
269
270        File dataDir = Environment.getDataDirectory();
271        File systemDir = new File(dataDir, "system");
272        File syncDir = new File(systemDir, "sync");
273        mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"));
274        mStatusFile = new AtomicFile(new File(syncDir, "status.bin"));
275        mPendingFile = new AtomicFile(new File(syncDir, "pending.bin"));
276        mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin"));
277
278        readAccountInfoLocked();
279        readStatusLocked();
280        readPendingOperationsLocked();
281        readStatisticsLocked();
282        readLegacyAccountInfoLocked();
283    }
284
285    public static SyncStorageEngine newTestInstance(Context context) {
286        return new SyncStorageEngine(context);
287    }
288
289    public static void init(Context context) {
290        if (sSyncStorageEngine != null) {
291            throw new IllegalStateException("already initialized");
292        }
293        sSyncStorageEngine = new SyncStorageEngine(context);
294    }
295
296    public static SyncStorageEngine getSingleton() {
297        if (sSyncStorageEngine == null) {
298            throw new IllegalStateException("not initialized");
299        }
300        return sSyncStorageEngine;
301    }
302
303    @Override public void handleMessage(Message msg) {
304        if (msg.what == MSG_WRITE_STATUS) {
305            synchronized (mAccounts) {
306                writeStatusLocked();
307            }
308        } else if (msg.what == MSG_WRITE_STATISTICS) {
309            synchronized (mAccounts) {
310                writeStatisticsLocked();
311            }
312        }
313    }
314
315    public void addStatusChangeListener(int mask, ISyncStatusObserver callback) {
316        synchronized (mAuthorities) {
317            mChangeListeners.register(callback, mask);
318        }
319    }
320
321    public void removeStatusChangeListener(ISyncStatusObserver callback) {
322        synchronized (mAuthorities) {
323            mChangeListeners.unregister(callback);
324        }
325    }
326
327    private void reportChange(int which) {
328        ArrayList<ISyncStatusObserver> reports = null;
329        synchronized (mAuthorities) {
330            int i = mChangeListeners.beginBroadcast();
331            while (i > 0) {
332                i--;
333                Integer mask = (Integer)mChangeListeners.getBroadcastCookie(i);
334                if ((which & mask.intValue()) == 0) {
335                    continue;
336                }
337                if (reports == null) {
338                    reports = new ArrayList<ISyncStatusObserver>(i);
339                }
340                reports.add(mChangeListeners.getBroadcastItem(i));
341            }
342        }
343
344        if (DEBUG) Log.v(TAG, "reportChange " + which + " to: " + reports);
345
346        if (reports != null) {
347            int i = reports.size();
348            while (i > 0) {
349                i--;
350                try {
351                    reports.get(i).onStatusChanged(which);
352                } catch (RemoteException e) {
353                    // The remote callback list will take care of this for us.
354                }
355            }
356        }
357    }
358
359    public boolean getSyncProviderAutomatically(Account account, String providerName) {
360        synchronized (mAuthorities) {
361            if (account != null) {
362                AuthorityInfo authority = getAuthorityLocked(account, providerName,
363                        "getSyncProviderAutomatically");
364                return authority != null ? authority.enabled : false;
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 setSyncProviderAutomatically(Account account, String providerName,
381            boolean sync) {
382        synchronized (mAuthorities) {
383            if (account != null) {
384                AuthorityInfo authority = getAuthorityLocked(account, providerName,
385                        "setSyncProviderAutomatically");
386                if (authority != null) {
387                    authority.enabled = sync;
388                }
389            } else {
390                int i = mAuthorities.size();
391                while (i > 0) {
392                    i--;
393                    AuthorityInfo authority = mAuthorities.get(i);
394                    if (authority.account.equals(account)
395                            && authority.authority.equals(providerName)) {
396                        authority.enabled = sync;
397                    }
398                }
399            }
400            writeAccountInfoLocked();
401        }
402
403        reportChange(CHANGE_SETTINGS);
404    }
405
406    public void setListenForNetworkTickles(boolean flag) {
407        synchronized (mAuthorities) {
408            mListenForTickles = flag;
409            writeAccountInfoLocked();
410        }
411        reportChange(CHANGE_SETTINGS);
412    }
413
414    public boolean getListenForNetworkTickles() {
415        synchronized (mAuthorities) {
416            return mListenForTickles;
417        }
418    }
419
420    public AuthorityInfo getAuthority(Account account, String authority) {
421        synchronized (mAuthorities) {
422            return getAuthorityLocked(account, authority, null);
423        }
424    }
425
426    public AuthorityInfo getAuthority(int authorityId) {
427        synchronized (mAuthorities) {
428            return mAuthorities.get(authorityId);
429        }
430    }
431
432    /**
433     * Returns true if there is currently a sync operation for the given
434     * account or authority in the pending list, or actively being processed.
435     */
436    public boolean isSyncActive(Account account, String authority) {
437        synchronized (mAuthorities) {
438            int i = mPendingOperations.size();
439            while (i > 0) {
440                i--;
441                // TODO(fredq): this probably shouldn't be considering
442                // pending operations.
443                PendingOperation op = mPendingOperations.get(i);
444                if (op.account.equals(account) && op.authority.equals(authority)) {
445                    return true;
446                }
447            }
448
449            if (mActiveSync != null) {
450                AuthorityInfo ainfo = getAuthority(mActiveSync.authorityId);
451                if (ainfo != null && ainfo.account.equals(account)
452                        && ainfo.authority.equals(authority)) {
453                    return true;
454                }
455            }
456        }
457
458        return false;
459    }
460
461    public PendingOperation insertIntoPending(PendingOperation op) {
462        synchronized (mAuthorities) {
463            if (DEBUG) Log.v(TAG, "insertIntoPending: account=" + op.account
464                    + " auth=" + op.authority
465                    + " src=" + op.syncSource
466                    + " extras=" + op.extras);
467
468            AuthorityInfo authority = getOrCreateAuthorityLocked(op.account,
469                    op.authority,
470                    -1 /* desired identifier */,
471                    true /* write accounts to storage */);
472            if (authority == null) {
473                return null;
474            }
475
476            op = new PendingOperation(op);
477            op.authorityId = authority.ident;
478            mPendingOperations.add(op);
479            appendPendingOperationLocked(op);
480
481            SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
482            status.pending = true;
483        }
484
485        reportChange(CHANGE_PENDING);
486        return op;
487    }
488
489    public boolean deleteFromPending(PendingOperation op) {
490        boolean res = false;
491        synchronized (mAuthorities) {
492            if (DEBUG) Log.v(TAG, "deleteFromPending: account=" + op.account
493                    + " auth=" + op.authority
494                    + " src=" + op.syncSource
495                    + " extras=" + op.extras);
496            if (mPendingOperations.remove(op)) {
497                if (mPendingOperations.size() == 0
498                        || mNumPendingFinished >= PENDING_FINISH_TO_WRITE) {
499                    writePendingOperationsLocked();
500                    mNumPendingFinished = 0;
501                } else {
502                    mNumPendingFinished++;
503                }
504
505                AuthorityInfo authority = getAuthorityLocked(op.account, op.authority,
506                        "deleteFromPending");
507                if (authority != null) {
508                    if (DEBUG) Log.v(TAG, "removing - " + authority);
509                    final int N = mPendingOperations.size();
510                    boolean morePending = false;
511                    for (int i=0; i<N; i++) {
512                        PendingOperation cur = mPendingOperations.get(i);
513                        if (cur.account.equals(op.account)
514                                && cur.authority.equals(op.authority)) {
515                            morePending = true;
516                            break;
517                        }
518                    }
519
520                    if (!morePending) {
521                        if (DEBUG) Log.v(TAG, "no more pending!");
522                        SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
523                        status.pending = false;
524                    }
525                }
526
527                res = true;
528            }
529        }
530
531        reportChange(CHANGE_PENDING);
532        return res;
533    }
534
535    public int clearPending() {
536        int num;
537        synchronized (mAuthorities) {
538            if (DEBUG) Log.v(TAG, "clearPending");
539            num = mPendingOperations.size();
540            mPendingOperations.clear();
541            final int N = mSyncStatus.size();
542            for (int i=0; i<N; i++) {
543                mSyncStatus.get(i).pending = false;
544            }
545            writePendingOperationsLocked();
546        }
547        reportChange(CHANGE_PENDING);
548        return num;
549    }
550
551    /**
552     * Return a copy of the current array of pending operations.  The
553     * PendingOperation objects are the real objects stored inside, so that
554     * they can be used with deleteFromPending().
555     */
556    public ArrayList<PendingOperation> getPendingOperations() {
557        synchronized (mAuthorities) {
558            return new ArrayList<PendingOperation>(mPendingOperations);
559        }
560    }
561
562    /**
563     * Return the number of currently pending operations.
564     */
565    public int getPendingOperationCount() {
566        synchronized (mAuthorities) {
567            return mPendingOperations.size();
568        }
569    }
570
571    /**
572     * Called when the set of account has changed, given the new array of
573     * active accounts.
574     */
575    public void doDatabaseCleanup(Account[] accounts) {
576        synchronized (mAuthorities) {
577            if (DEBUG) Log.w(TAG, "Updating for new accounts...");
578            SparseArray<AuthorityInfo> removing = new SparseArray<AuthorityInfo>();
579            Iterator<AccountInfo> accIt = mAccounts.values().iterator();
580            while (accIt.hasNext()) {
581                AccountInfo acc = accIt.next();
582                if (!ArrayUtils.contains(accounts, acc.account)) {
583                    // This account no longer exists...
584                    if (DEBUG) Log.w(TAG, "Account removed: " + acc.account);
585                    for (AuthorityInfo auth : acc.authorities.values()) {
586                        removing.put(auth.ident, auth);
587                    }
588                    accIt.remove();
589                }
590            }
591
592            // Clean out all data structures.
593            int i = removing.size();
594            if (i > 0) {
595                while (i > 0) {
596                    i--;
597                    int ident = removing.keyAt(i);
598                    mAuthorities.remove(ident);
599                    int j = mSyncStatus.size();
600                    while (j > 0) {
601                        j--;
602                        if (mSyncStatus.keyAt(j) == ident) {
603                            mSyncStatus.remove(mSyncStatus.keyAt(j));
604                        }
605                    }
606                    j = mSyncHistory.size();
607                    while (j > 0) {
608                        j--;
609                        if (mSyncHistory.get(j).authorityId == ident) {
610                            mSyncHistory.remove(j);
611                        }
612                    }
613                }
614                writeAccountInfoLocked();
615                writeStatusLocked();
616                writePendingOperationsLocked();
617                writeStatisticsLocked();
618            }
619        }
620    }
621
622    /**
623     * Called when the currently active sync is changing (there can only be
624     * one at a time).  Either supply a valid ActiveSyncContext with information
625     * about the sync, or null to stop the currently active sync.
626     */
627    public void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
628        synchronized (mAuthorities) {
629            if (activeSyncContext != null) {
630                if (DEBUG) Log.v(TAG, "setActiveSync: account="
631                        + activeSyncContext.mSyncOperation.account
632                        + " auth=" + activeSyncContext.mSyncOperation.authority
633                        + " src=" + activeSyncContext.mSyncOperation.syncSource
634                        + " extras=" + activeSyncContext.mSyncOperation.extras);
635                if (mActiveSync != null) {
636                    Log.w(TAG, "setActiveSync called with existing active sync!");
637                }
638                AuthorityInfo authority = getAuthorityLocked(
639                        activeSyncContext.mSyncOperation.account,
640                        activeSyncContext.mSyncOperation.authority,
641                        "setActiveSync");
642                if (authority == null) {
643                    return;
644                }
645                mActiveSync = new ActiveSyncInfo(authority.ident,
646                        authority.account, authority.authority,
647                        activeSyncContext.mStartTime);
648            } else {
649                if (DEBUG) Log.v(TAG, "setActiveSync: null");
650                mActiveSync = null;
651            }
652        }
653
654        reportChange(CHANGE_ACTIVE);
655    }
656
657    /**
658     * To allow others to send active change reports, to poke clients.
659     */
660    public void reportActiveChange() {
661        reportChange(CHANGE_ACTIVE);
662    }
663
664    /**
665     * Note that sync has started for the given account and authority.
666     */
667    public long insertStartSyncEvent(Account accountName, String authorityName,
668            long now, int source) {
669        long id;
670        synchronized (mAuthorities) {
671            if (DEBUG) Log.v(TAG, "insertStartSyncEvent: account=" + accountName
672                    + " auth=" + authorityName + " source=" + source);
673            AuthorityInfo authority = getAuthorityLocked(accountName, authorityName,
674                    "insertStartSyncEvent");
675            if (authority == null) {
676                return -1;
677            }
678            SyncHistoryItem item = new SyncHistoryItem();
679            item.authorityId = authority.ident;
680            item.historyId = mNextHistoryId++;
681            if (mNextHistoryId < 0) mNextHistoryId = 0;
682            item.eventTime = now;
683            item.source = source;
684            item.event = EVENT_START;
685            mSyncHistory.add(0, item);
686            while (mSyncHistory.size() > MAX_HISTORY) {
687                mSyncHistory.remove(mSyncHistory.size()-1);
688            }
689            id = item.historyId;
690            if (DEBUG) Log.v(TAG, "returning historyId " + id);
691        }
692
693        reportChange(CHANGE_STATUS);
694        return id;
695    }
696
697    public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage,
698            long downstreamActivity, long upstreamActivity) {
699        synchronized (mAuthorities) {
700            if (DEBUG) Log.v(TAG, "stopSyncEvent: historyId=" + historyId);
701            SyncHistoryItem item = null;
702            int i = mSyncHistory.size();
703            while (i > 0) {
704                i--;
705                item = mSyncHistory.get(i);
706                if (item.historyId == historyId) {
707                    break;
708                }
709                item = null;
710            }
711
712            if (item == null) {
713                Log.w(TAG, "stopSyncEvent: no history for id " + historyId);
714                return;
715            }
716
717            item.elapsedTime = elapsedTime;
718            item.event = EVENT_STOP;
719            item.mesg = resultMessage;
720            item.downstreamActivity = downstreamActivity;
721            item.upstreamActivity = upstreamActivity;
722
723            SyncStatusInfo status = getOrCreateSyncStatusLocked(item.authorityId);
724
725            status.numSyncs++;
726            status.totalElapsedTime += elapsedTime;
727            switch (item.source) {
728                case SOURCE_LOCAL:
729                    status.numSourceLocal++;
730                    break;
731                case SOURCE_POLL:
732                    status.numSourcePoll++;
733                    break;
734                case SOURCE_USER:
735                    status.numSourceUser++;
736                    break;
737                case SOURCE_SERVER:
738                    status.numSourceServer++;
739                    break;
740            }
741
742            boolean writeStatisticsNow = false;
743            int day = getCurrentDayLocked();
744            if (mDayStats[0] == null) {
745                mDayStats[0] = new DayStats(day);
746            } else if (day != mDayStats[0].day) {
747                System.arraycopy(mDayStats, 0, mDayStats, 1, mDayStats.length-1);
748                mDayStats[0] = new DayStats(day);
749                writeStatisticsNow = true;
750            } else if (mDayStats[0] == null) {
751            }
752            final DayStats ds = mDayStats[0];
753
754            final long lastSyncTime = (item.eventTime + elapsedTime);
755            boolean writeStatusNow = false;
756            if (MESG_SUCCESS.equals(resultMessage)) {
757                // - if successful, update the successful columns
758                if (status.lastSuccessTime == 0 || status.lastFailureTime != 0) {
759                    writeStatusNow = true;
760                }
761                status.lastSuccessTime = lastSyncTime;
762                status.lastSuccessSource = item.source;
763                status.lastFailureTime = 0;
764                status.lastFailureSource = -1;
765                status.lastFailureMesg = null;
766                status.initialFailureTime = 0;
767                ds.successCount++;
768                ds.successTime += elapsedTime;
769            } else if (!MESG_CANCELED.equals(resultMessage)) {
770                if (status.lastFailureTime == 0) {
771                    writeStatusNow = true;
772                }
773                status.lastFailureTime = lastSyncTime;
774                status.lastFailureSource = item.source;
775                status.lastFailureMesg = resultMessage;
776                if (status.initialFailureTime == 0) {
777                    status.initialFailureTime = lastSyncTime;
778                }
779                ds.failureCount++;
780                ds.failureTime += elapsedTime;
781            }
782
783            if (writeStatusNow) {
784                writeStatusLocked();
785            } else if (!hasMessages(MSG_WRITE_STATUS)) {
786                sendMessageDelayed(obtainMessage(MSG_WRITE_STATUS),
787                        WRITE_STATUS_DELAY);
788            }
789            if (writeStatisticsNow) {
790                writeStatisticsLocked();
791            } else if (!hasMessages(MSG_WRITE_STATISTICS)) {
792                sendMessageDelayed(obtainMessage(MSG_WRITE_STATISTICS),
793                        WRITE_STATISTICS_DELAY);
794            }
795        }
796
797        reportChange(CHANGE_STATUS);
798    }
799
800    /**
801     * Return the currently active sync information, or null if there is no
802     * active sync.  Note that the returned object is the real, live active
803     * sync object, so be careful what you do with it.
804     */
805    public ActiveSyncInfo getActiveSync() {
806        synchronized (mAuthorities) {
807            return mActiveSync;
808        }
809    }
810
811    /**
812     * Return an array of the current sync status for all authorities.  Note
813     * that the objects inside the array are the real, live status objects,
814     * so be careful what you do with them.
815     */
816    public ArrayList<SyncStatusInfo> getSyncStatus() {
817        synchronized (mAuthorities) {
818            final int N = mSyncStatus.size();
819            ArrayList<SyncStatusInfo> ops = new ArrayList<SyncStatusInfo>(N);
820            for (int i=0; i<N; i++) {
821                ops.add(mSyncStatus.valueAt(i));
822            }
823            return ops;
824        }
825    }
826
827    /**
828     * Returns the status that matches the authority. If there are multiples accounts for
829     * the authority, the one with the latest "lastSuccessTime" status is returned.
830     * @param authority the authority whose row should be selected
831     * @return the SyncStatusInfo for the authority, or null if none exists
832     */
833    public SyncStatusInfo getStatusByAuthority(String authority) {
834        synchronized (mAuthorities) {
835            SyncStatusInfo best = null;
836            final int N = mSyncStatus.size();
837            for (int i=0; i<N; i++) {
838                SyncStatusInfo cur = mSyncStatus.get(i);
839                AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
840                if (ainfo != null && ainfo.authority.equals(authority)) {
841                    if (best == null) {
842                        best = cur;
843                    } else if (best.lastSuccessTime > cur.lastSuccessTime) {
844                        best = cur;
845                    }
846                }
847            }
848            return best;
849        }
850    }
851
852    /**
853     * Return true if the pending status is true of any matching authorities.
854     */
855    public boolean isAuthorityPending(Account account, String authority) {
856        synchronized (mAuthorities) {
857            final int N = mSyncStatus.size();
858            for (int i=0; i<N; i++) {
859                SyncStatusInfo cur = mSyncStatus.get(i);
860                AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
861                if (ainfo == null) {
862                    continue;
863                }
864                if (account != null && !ainfo.account.equals(account)) {
865                    continue;
866                }
867                if (ainfo.authority.equals(authority) && cur.pending) {
868                    return true;
869                }
870            }
871            return false;
872        }
873    }
874
875    /**
876     * Return an array of the current sync status for all authorities.  Note
877     * that the objects inside the array are the real, live status objects,
878     * so be careful what you do with them.
879     */
880    public ArrayList<SyncHistoryItem> getSyncHistory() {
881        synchronized (mAuthorities) {
882            final int N = mSyncHistory.size();
883            ArrayList<SyncHistoryItem> items = new ArrayList<SyncHistoryItem>(N);
884            for (int i=0; i<N; i++) {
885                items.add(mSyncHistory.get(i));
886            }
887            return items;
888        }
889    }
890
891    /**
892     * Return an array of the current per-day statistics.  Note
893     * that the objects inside the array are the real, live status objects,
894     * so be careful what you do with them.
895     */
896    public DayStats[] getDayStatistics() {
897        synchronized (mAuthorities) {
898            DayStats[] ds = new DayStats[mDayStats.length];
899            System.arraycopy(mDayStats, 0, ds, 0, ds.length);
900            return ds;
901        }
902    }
903
904    /**
905     * If sync is failing for any of the provider/accounts then determine the time at which it
906     * started failing and return the earliest time over all the provider/accounts. If none are
907     * failing then return 0.
908     */
909    public long getInitialSyncFailureTime() {
910        synchronized (mAuthorities) {
911            if (!mListenForTickles) {
912                return 0;
913            }
914
915            long oldest = 0;
916            int i = mSyncStatus.size();
917            while (i > 0) {
918                i--;
919                SyncStatusInfo stats = mSyncStatus.valueAt(i);
920                AuthorityInfo authority = mAuthorities.get(stats.authorityId);
921                if (authority != null && authority.enabled) {
922                    if (oldest == 0 || stats.initialFailureTime < oldest) {
923                        oldest = stats.initialFailureTime;
924                    }
925                }
926            }
927
928            return oldest;
929        }
930    }
931
932    private int getCurrentDayLocked() {
933        mCal.setTimeInMillis(System.currentTimeMillis());
934        final int dayOfYear = mCal.get(Calendar.DAY_OF_YEAR);
935        if (mYear != mCal.get(Calendar.YEAR)) {
936            mYear = mCal.get(Calendar.YEAR);
937            mCal.clear();
938            mCal.set(Calendar.YEAR, mYear);
939            mYearInDays = (int)(mCal.getTimeInMillis()/86400000);
940        }
941        return dayOfYear + mYearInDays;
942    }
943
944    /**
945     * Retrieve an authority, returning null if one does not exist.
946     *
947     * @param accountName The name of the account for the authority.
948     * @param authorityName The name of the authority itself.
949     * @param tag If non-null, this will be used in a log message if the
950     * requested authority does not exist.
951     */
952    private AuthorityInfo getAuthorityLocked(Account accountName, String authorityName,
953            String tag) {
954        AccountInfo account = mAccounts.get(accountName);
955        if (account == null) {
956            if (tag != null) {
957                Log.w(TAG, tag + ": unknown account " + accountName);
958            }
959            return null;
960        }
961        AuthorityInfo authority = account.authorities.get(authorityName);
962        if (authority == null) {
963            if (tag != null) {
964                Log.w(TAG, tag + ": unknown authority " + authorityName);
965            }
966            return null;
967        }
968
969        return authority;
970    }
971
972    private AuthorityInfo getOrCreateAuthorityLocked(Account accountName,
973            String authorityName, int ident, boolean doWrite) {
974        AccountInfo account = mAccounts.get(accountName);
975        if (account == null) {
976            account = new AccountInfo(accountName);
977            mAccounts.put(accountName, account);
978        }
979        AuthorityInfo authority = account.authorities.get(authorityName);
980        if (authority == null) {
981            if (ident < 0) {
982                // Look for a new identifier for this authority.
983                final int N = mAuthorities.size();
984                ident = 0;
985                for (int i=0; i<N; i++) {
986                    if (mAuthorities.valueAt(i).ident > ident) {
987                        break;
988                    }
989                    ident++;
990                }
991            }
992            authority = new AuthorityInfo(accountName, authorityName, ident);
993            account.authorities.put(authorityName, authority);
994            mAuthorities.put(ident, authority);
995            if (doWrite) {
996                writeAccountInfoLocked();
997            }
998        }
999
1000        return authority;
1001    }
1002
1003    private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) {
1004        SyncStatusInfo status = mSyncStatus.get(authorityId);
1005        if (status == null) {
1006            status = new SyncStatusInfo(authorityId);
1007            mSyncStatus.put(authorityId, status);
1008        }
1009        return status;
1010    }
1011
1012    public void writeAllState() {
1013        synchronized (mAuthorities) {
1014            // Account info is always written so no need to do it here.
1015
1016            if (mNumPendingFinished > 0) {
1017                // Only write these if they are out of date.
1018                writePendingOperationsLocked();
1019            }
1020
1021            // Just always write these...  they are likely out of date.
1022            writeStatusLocked();
1023            writeStatisticsLocked();
1024        }
1025    }
1026
1027    /**
1028     * Read all account information back in to the initial engine state.
1029     */
1030    private void readAccountInfoLocked() {
1031        FileInputStream fis = null;
1032        try {
1033            fis = mAccountInfoFile.openRead();
1034            if (DEBUG_FILE) Log.v(TAG, "Reading " + mAccountInfoFile.getBaseFile());
1035            XmlPullParser parser = Xml.newPullParser();
1036            parser.setInput(fis, null);
1037            int eventType = parser.getEventType();
1038            while (eventType != XmlPullParser.START_TAG) {
1039                eventType = parser.next();
1040            }
1041            String tagName = parser.getName();
1042            if ("accounts".equals(tagName)) {
1043                String listen = parser.getAttributeValue(
1044                        null, "listen-for-tickles");
1045                mListenForTickles = listen == null
1046                            || Boolean.parseBoolean(listen);
1047                eventType = parser.next();
1048                do {
1049                    if (eventType == XmlPullParser.START_TAG
1050                            && parser.getDepth() == 2) {
1051                        tagName = parser.getName();
1052                        if ("authority".equals(tagName)) {
1053                            int id = -1;
1054                            try {
1055                                id = Integer.parseInt(parser.getAttributeValue(
1056                                        null, "id"));
1057                            } catch (NumberFormatException e) {
1058                            } catch (NullPointerException e) {
1059                            }
1060                            if (id >= 0) {
1061                                String accountName = parser.getAttributeValue(
1062                                        null, "account");
1063                                String accountType = parser.getAttributeValue(
1064                                        null, "type");
1065                                if (accountType == null) {
1066                                    accountType = "com.google.GAIA";
1067                                }
1068                                String authorityName = parser.getAttributeValue(
1069                                        null, "authority");
1070                                String enabled = parser.getAttributeValue(
1071                                        null, "enabled");
1072                                AuthorityInfo authority = mAuthorities.get(id);
1073                                if (DEBUG_FILE) Log.v(TAG, "Adding authority: account="
1074                                        + accountName + " auth=" + authorityName
1075                                        + " enabled=" + enabled);
1076                                if (authority == null) {
1077                                    if (DEBUG_FILE) Log.v(TAG, "Creating entry");
1078                                    authority = getOrCreateAuthorityLocked(
1079                                            new Account(accountName, accountType),
1080                                            authorityName, id, false);
1081                                }
1082                                if (authority != null) {
1083                                    authority.enabled = enabled == null
1084                                            || Boolean.parseBoolean(enabled);
1085                                } else {
1086                                    Log.w(TAG, "Failure adding authority: account="
1087                                            + accountName + " auth=" + authorityName
1088                                            + " enabled=" + enabled);
1089                                }
1090                            }
1091                        }
1092                    }
1093                    eventType = parser.next();
1094                } while (eventType != XmlPullParser.END_DOCUMENT);
1095            }
1096        } catch (XmlPullParserException e) {
1097            Log.w(TAG, "Error reading accounts", e);
1098        } catch (java.io.IOException e) {
1099            if (fis == null) Log.i(TAG, "No initial accounts");
1100            else Log.w(TAG, "Error reading accounts", e);
1101        } finally {
1102            if (fis != null) {
1103                try {
1104                    fis.close();
1105                } catch (java.io.IOException e1) {
1106                }
1107            }
1108        }
1109    }
1110
1111    /**
1112     * Write all account information to the account file.
1113     */
1114    private void writeAccountInfoLocked() {
1115        if (DEBUG_FILE) Log.v(TAG, "Writing new " + mAccountInfoFile.getBaseFile());
1116        FileOutputStream fos = null;
1117
1118        try {
1119            fos = mAccountInfoFile.startWrite();
1120            XmlSerializer out = new FastXmlSerializer();
1121            out.setOutput(fos, "utf-8");
1122            out.startDocument(null, true);
1123            out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
1124
1125            out.startTag(null, "accounts");
1126            if (!mListenForTickles) {
1127                out.attribute(null, "listen-for-tickles", "false");
1128            }
1129
1130            final int N = mAuthorities.size();
1131            for (int i=0; i<N; i++) {
1132                AuthorityInfo authority = mAuthorities.get(i);
1133                out.startTag(null, "authority");
1134                out.attribute(null, "id", Integer.toString(authority.ident));
1135                out.attribute(null, "account", authority.account.mName);
1136                out.attribute(null, "type", authority.account.mType);
1137                out.attribute(null, "authority", authority.authority);
1138                if (!authority.enabled) {
1139                    out.attribute(null, "enabled", "false");
1140                }
1141                out.endTag(null, "authority");
1142            }
1143
1144            out.endTag(null, "accounts");
1145
1146            out.endDocument();
1147
1148            mAccountInfoFile.finishWrite(fos);
1149        } catch (java.io.IOException e1) {
1150            Log.w(TAG, "Error writing accounts", e1);
1151            if (fos != null) {
1152                mAccountInfoFile.failWrite(fos);
1153            }
1154        }
1155    }
1156
1157    static int getIntColumn(Cursor c, String name) {
1158        return c.getInt(c.getColumnIndex(name));
1159    }
1160
1161    static long getLongColumn(Cursor c, String name) {
1162        return c.getLong(c.getColumnIndex(name));
1163    }
1164
1165    /**
1166     * Load sync engine state from the old syncmanager database, and then
1167     * erase it.  Note that we don't deal with pending operations, active
1168     * sync, or history.
1169     */
1170    private void readLegacyAccountInfoLocked() {
1171        // Look for old database to initialize from.
1172        File file = mContext.getDatabasePath("syncmanager.db");
1173        if (!file.exists()) {
1174            return;
1175        }
1176        String path = file.getPath();
1177        SQLiteDatabase db = null;
1178        try {
1179            db = SQLiteDatabase.openDatabase(path, null,
1180                    SQLiteDatabase.OPEN_READONLY);
1181        } catch (SQLiteException e) {
1182        }
1183
1184        if (db != null) {
1185            final boolean hasType = db.getVersion() >= 11;
1186
1187            // Copy in all of the status information, as well as accounts.
1188            if (DEBUG_FILE) Log.v(TAG, "Reading legacy sync accounts db");
1189            SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1190            qb.setTables("stats, status");
1191            HashMap<String,String> map = new HashMap<String,String>();
1192            map.put("_id", "status._id as _id");
1193            map.put("account", "stats.account as account");
1194            if (hasType) {
1195                map.put("account_type", "stats.account_type as account_type");
1196            }
1197            map.put("authority", "stats.authority as authority");
1198            map.put("totalElapsedTime", "totalElapsedTime");
1199            map.put("numSyncs", "numSyncs");
1200            map.put("numSourceLocal", "numSourceLocal");
1201            map.put("numSourcePoll", "numSourcePoll");
1202            map.put("numSourceServer", "numSourceServer");
1203            map.put("numSourceUser", "numSourceUser");
1204            map.put("lastSuccessSource", "lastSuccessSource");
1205            map.put("lastSuccessTime", "lastSuccessTime");
1206            map.put("lastFailureSource", "lastFailureSource");
1207            map.put("lastFailureTime", "lastFailureTime");
1208            map.put("lastFailureMesg", "lastFailureMesg");
1209            map.put("pending", "pending");
1210            qb.setProjectionMap(map);
1211            qb.appendWhere("stats._id = status.stats_id");
1212            Cursor c = qb.query(db, null, null, null, null, null, null);
1213            while (c.moveToNext()) {
1214                String accountName = c.getString(c.getColumnIndex("account"));
1215                String accountType = hasType
1216                        ? c.getString(c.getColumnIndex("account_type")) : null;
1217                if (accountType == null) {
1218                    accountType = "com.google.GAIA";
1219                }
1220                String authorityName = c.getString(c.getColumnIndex("authority"));
1221                AuthorityInfo authority = this.getOrCreateAuthorityLocked(
1222                        new Account(accountName, accountType),
1223                        authorityName, -1, false);
1224                if (authority != null) {
1225                    int i = mSyncStatus.size();
1226                    boolean found = false;
1227                    SyncStatusInfo st = null;
1228                    while (i > 0) {
1229                        i--;
1230                        st = mSyncStatus.get(i);
1231                        if (st.authorityId == authority.ident) {
1232                            found = true;
1233                            break;
1234                        }
1235                    }
1236                    if (!found) {
1237                        st = new SyncStatusInfo(authority.ident);
1238                        mSyncStatus.put(authority.ident, st);
1239                    }
1240                    st.totalElapsedTime = getLongColumn(c, "totalElapsedTime");
1241                    st.numSyncs = getIntColumn(c, "numSyncs");
1242                    st.numSourceLocal = getIntColumn(c, "numSourceLocal");
1243                    st.numSourcePoll = getIntColumn(c, "numSourcePoll");
1244                    st.numSourceServer = getIntColumn(c, "numSourceServer");
1245                    st.numSourceUser = getIntColumn(c, "numSourceUser");
1246                    st.lastSuccessSource = getIntColumn(c, "lastSuccessSource");
1247                    st.lastSuccessTime = getLongColumn(c, "lastSuccessTime");
1248                    st.lastFailureSource = getIntColumn(c, "lastFailureSource");
1249                    st.lastFailureTime = getLongColumn(c, "lastFailureTime");
1250                    st.lastFailureMesg = c.getString(c.getColumnIndex("lastFailureMesg"));
1251                    st.pending = getIntColumn(c, "pending") != 0;
1252                }
1253            }
1254
1255            c.close();
1256
1257            // Retrieve the settings.
1258            qb = new SQLiteQueryBuilder();
1259            qb.setTables("settings");
1260            c = qb.query(db, null, null, null, null, null, null);
1261            while (c.moveToNext()) {
1262                String name = c.getString(c.getColumnIndex("name"));
1263                String value = c.getString(c.getColumnIndex("value"));
1264                if (name == null) continue;
1265                if (name.equals("listen_for_tickles")) {
1266                    setListenForNetworkTickles(value == null
1267                            || Boolean.parseBoolean(value));
1268                } else if (name.startsWith("sync_provider_")) {
1269                    String provider = name.substring("sync_provider_".length(),
1270                            name.length());
1271                    setSyncProviderAutomatically(null, provider,
1272                            value == null || Boolean.parseBoolean(value));
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