merged 231cc608d06ffc31c24bf8aa8c8275bdd2636581
diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java
index 2ad44d2..64b626d 100644
--- a/core/java/android/content/SyncStorageEngine.java
+++ b/core/java/android/content/SyncStorageEngine.java
@@ -1,132 +1,285 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package android.content;
-import android.Manifest;
+import com.android.internal.os.AtomicFile;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FastXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
import android.accounts.Account;
import android.database.Cursor;
-import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteQueryBuilder;
-import android.net.Uri;
-import android.provider.Sync;
-import android.text.TextUtils;
-import android.util.Config;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
import android.util.Log;
+import android.util.SparseArray;
+import android.util.Xml;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.util.ArrayList;
+import java.util.Calendar;
import java.util.HashMap;
-import java.util.HashSet;
+import java.util.Iterator;
+import java.util.TimeZone;
import com.google.android.collect.Sets;
/**
- * ContentProvider that tracks the sync data and overall sync
+ * Singleton that tracks the sync data and overall sync
* history on the device.
*
* @hide
*/
-public class SyncStorageEngine {
+public class SyncStorageEngine extends Handler {
private static final String TAG = "SyncManager";
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_FILE = false;
+
+ // @VisibleForTesting
+ static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
- private static final String DATABASE_NAME = "syncmanager.db";
- private static final int DATABASE_VERSION = 11;
+ /** Enum value for a sync start event. */
+ public static final int EVENT_START = 0;
- private static final int STATS = 1;
- private static final int STATS_ID = 2;
- private static final int HISTORY = 3;
- private static final int HISTORY_ID = 4;
- private static final int SETTINGS = 5;
- private static final int PENDING = 7;
- private static final int ACTIVE = 8;
- private static final int STATUS = 9;
+ /** Enum value for a sync stop event. */
+ public static final int EVENT_STOP = 1;
- private static final UriMatcher sURLMatcher =
- new UriMatcher(UriMatcher.NO_MATCH);
+ // TODO: i18n -- grab these out of resources.
+ /** String names for the sync event types. */
+ public static final String[] EVENTS = { "START", "STOP" };
- private static final HashMap<String,String> HISTORY_PROJECTION_MAP;
- private static final HashMap<String,String> PENDING_PROJECTION_MAP;
- private static final HashMap<String,String> ACTIVE_PROJECTION_MAP;
- private static final HashMap<String,String> STATUS_PROJECTION_MAP;
+ /** Enum value for a server-initiated sync. */
+ public static final int SOURCE_SERVER = 0;
- private final Context mContext;
- private final SQLiteOpenHelper mOpenHelper;
- private static SyncStorageEngine sSyncStorageEngine = null;
+ /** Enum value for a local-initiated sync. */
+ public static final int SOURCE_LOCAL = 1;
+ /**
+ * Enum value for a poll-based sync (e.g., upon connection to
+ * network)
+ */
+ public static final int SOURCE_POLL = 2;
- static {
- sURLMatcher.addURI("sync", "stats", STATS);
- sURLMatcher.addURI("sync", "stats/#", STATS_ID);
- sURLMatcher.addURI("sync", "history", HISTORY);
- sURLMatcher.addURI("sync", "history/#", HISTORY_ID);
- sURLMatcher.addURI("sync", "settings", SETTINGS);
- sURLMatcher.addURI("sync", "status", STATUS);
- sURLMatcher.addURI("sync", "active", ACTIVE);
- sURLMatcher.addURI("sync", "pending", PENDING);
+ /** Enum value for a user-initiated sync. */
+ public static final int SOURCE_USER = 3;
- HashMap<String,String> map;
- PENDING_PROJECTION_MAP = map = new HashMap<String,String>();
- map.put(Sync.History._ID, Sync.History._ID);
- map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT);
- map.put(Sync.History.ACCOUNT_TYPE, Sync.History.ACCOUNT_TYPE);
- map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY);
+ // TODO: i18n -- grab these out of resources.
+ /** String names for the sync source types. */
+ public static final String[] SOURCES = { "SERVER",
+ "LOCAL",
+ "POLL",
+ "USER" };
- ACTIVE_PROJECTION_MAP = map = new HashMap<String,String>();
- map.put(Sync.History._ID, Sync.History._ID);
- map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT);
- map.put(Sync.History.ACCOUNT_TYPE, Sync.History.ACCOUNT_TYPE);
- map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY);
- map.put("startTime", "startTime");
+ // Error types
+ public static final int ERROR_SYNC_ALREADY_IN_PROGRESS = 1;
+ public static final int ERROR_AUTHENTICATION = 2;
+ public static final int ERROR_IO = 3;
+ public static final int ERROR_PARSE = 4;
+ public static final int ERROR_CONFLICT = 5;
+ public static final int ERROR_TOO_MANY_DELETIONS = 6;
+ public static final int ERROR_TOO_MANY_RETRIES = 7;
+ public static final int ERROR_INTERNAL = 8;
- HISTORY_PROJECTION_MAP = map = new HashMap<String,String>();
- map.put(Sync.History._ID, "history._id as _id");
- map.put(Sync.History.ACCOUNT, "stats.account as account");
- map.put(Sync.History.ACCOUNT_TYPE, "stats.account_type as account_type");
- map.put(Sync.History.AUTHORITY, "stats.authority as authority");
- map.put(Sync.History.EVENT, Sync.History.EVENT);
- map.put(Sync.History.EVENT_TIME, Sync.History.EVENT_TIME);
- map.put(Sync.History.ELAPSED_TIME, Sync.History.ELAPSED_TIME);
- map.put(Sync.History.SOURCE, Sync.History.SOURCE);
- map.put(Sync.History.UPSTREAM_ACTIVITY, Sync.History.UPSTREAM_ACTIVITY);
- map.put(Sync.History.DOWNSTREAM_ACTIVITY, Sync.History.DOWNSTREAM_ACTIVITY);
- map.put(Sync.History.MESG, Sync.History.MESG);
+ // The MESG column will contain one of these or one of the Error types.
+ public static final String MESG_SUCCESS = "success";
+ public static final String MESG_CANCELED = "canceled";
- STATUS_PROJECTION_MAP = map = new HashMap<String,String>();
- map.put(Sync.Status._ID, "status._id as _id");
- map.put(Sync.Status.ACCOUNT, "stats.account as account");
- map.put(Sync.Status.ACCOUNT_TYPE, "stats.account_type as account_type");
- map.put(Sync.Status.AUTHORITY, "stats.authority as authority");
- map.put(Sync.Status.TOTAL_ELAPSED_TIME, Sync.Status.TOTAL_ELAPSED_TIME);
- map.put(Sync.Status.NUM_SYNCS, Sync.Status.NUM_SYNCS);
- map.put(Sync.Status.NUM_SOURCE_LOCAL, Sync.Status.NUM_SOURCE_LOCAL);
- map.put(Sync.Status.NUM_SOURCE_POLL, Sync.Status.NUM_SOURCE_POLL);
- map.put(Sync.Status.NUM_SOURCE_SERVER, Sync.Status.NUM_SOURCE_SERVER);
- map.put(Sync.Status.NUM_SOURCE_USER, Sync.Status.NUM_SOURCE_USER);
- map.put(Sync.Status.LAST_SUCCESS_SOURCE, Sync.Status.LAST_SUCCESS_SOURCE);
- map.put(Sync.Status.LAST_SUCCESS_TIME, Sync.Status.LAST_SUCCESS_TIME);
- map.put(Sync.Status.LAST_FAILURE_SOURCE, Sync.Status.LAST_FAILURE_SOURCE);
- map.put(Sync.Status.LAST_FAILURE_TIME, Sync.Status.LAST_FAILURE_TIME);
- map.put(Sync.Status.LAST_FAILURE_MESG, Sync.Status.LAST_FAILURE_MESG);
- map.put(Sync.Status.PENDING, Sync.Status.PENDING);
+ public static final int CHANGE_SETTINGS = 1<<0;
+ public static final int CHANGE_PENDING = 1<<1;
+ public static final int CHANGE_ACTIVE = 1<<2;
+ public static final int CHANGE_STATUS = 1<<3;
+ public static final int CHANGE_ALL = 0x7fffffff;
+
+ public static final int MAX_HISTORY = 15;
+
+ private static final int MSG_WRITE_STATUS = 1;
+ private static final long WRITE_STATUS_DELAY = 1000*60*10; // 10 minutes
+
+ private static final int MSG_WRITE_STATISTICS = 2;
+ private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour
+
+ public static class PendingOperation {
+ final Account account;
+ final int syncSource;
+ final String authority;
+ final Bundle extras; // note: read-only.
+
+ int authorityId;
+ byte[] flatExtras;
+
+ PendingOperation(Account account, int source,
+ String authority, Bundle extras) {
+ this.account = account;
+ this.syncSource = source;
+ this.authority = authority;
+ this.extras = extras != null ? new Bundle(extras) : extras;
+ this.authorityId = -1;
+ }
+
+ PendingOperation(PendingOperation other) {
+ this.account = other.account;
+ this.syncSource = other.syncSource;
+ this.authority = other.authority;
+ this.extras = other.extras;
+ this.authorityId = other.authorityId;
+ }
}
+
+ static class AccountInfo {
+ final Account account;
+ final HashMap<String, AuthorityInfo> authorities =
+ new HashMap<String, AuthorityInfo>();
+
+ AccountInfo(Account account) {
+ this.account = account;
+ }
+ }
+
+ public static class AuthorityInfo {
+ final Account account;
+ final String authority;
+ final int ident;
+ boolean enabled;
+
+ AuthorityInfo(Account account, String authority, int ident) {
+ this.account = account;
+ this.authority = authority;
+ this.ident = ident;
+ enabled = true;
+ }
+ }
+
+ public static class SyncHistoryItem {
+ int authorityId;
+ int historyId;
+ long eventTime;
+ long elapsedTime;
+ int source;
+ int event;
+ long upstreamActivity;
+ long downstreamActivity;
+ String mesg;
+ }
+
+ public static class DayStats {
+ public final int day;
+ public int successCount;
+ public long successTime;
+ public int failureCount;
+ public long failureTime;
+
+ public DayStats(int day) {
+ this.day = day;
+ }
+ }
+
+ // Primary list of all syncable authorities. Also our global lock.
+ private final SparseArray<AuthorityInfo> mAuthorities =
+ new SparseArray<AuthorityInfo>();
+
+ private final HashMap<Account, AccountInfo> mAccounts =
+ new HashMap<Account, AccountInfo>();
- private static final String[] STATS_ACCOUNT_PROJECTION =
- new String[] { Sync.Stats.ACCOUNT, Sync.Stats.ACCOUNT_TYPE };
-
- private static final int MAX_HISTORY_EVENTS_TO_KEEP = 5000;
-
- private static final String SELECT_INITIAL_FAILURE_TIME_QUERY_STRING = ""
- + "SELECT min(a) "
- + "FROM ("
- + " SELECT initialFailureTime AS a "
- + " FROM status "
- + " WHERE stats_id=? AND a IS NOT NULL "
- + " UNION "
- + " SELECT ? AS a"
- + " )";
-
+ private final ArrayList<PendingOperation> mPendingOperations =
+ new ArrayList<PendingOperation>();
+
+ private ActiveSyncInfo mActiveSync;
+
+ private final SparseArray<SyncStatusInfo> mSyncStatus =
+ new SparseArray<SyncStatusInfo>();
+
+ private final ArrayList<SyncHistoryItem> mSyncHistory =
+ new ArrayList<SyncHistoryItem>();
+
+ private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners
+ = new RemoteCallbackList<ISyncStatusObserver>();
+
+ // We keep 4 weeks of stats.
+ private final DayStats[] mDayStats = new DayStats[7*4];
+ private final Calendar mCal;
+ private int mYear;
+ private int mYearInDays;
+
+ private final Context mContext;
+ private static volatile SyncStorageEngine sSyncStorageEngine = null;
+
+ /**
+ * This file contains the core engine state: all accounts and the
+ * settings for them. It must never be lost, and should be changed
+ * infrequently, so it is stored as an XML file.
+ */
+ private final AtomicFile mAccountInfoFile;
+
+ /**
+ * This file contains the current sync status. We would like to retain
+ * it across boots, but its loss is not the end of the world, so we store
+ * this information as binary data.
+ */
+ private final AtomicFile mStatusFile;
+
+ /**
+ * This file contains sync statistics. This is purely debugging information
+ * so is written infrequently and can be thrown away at any time.
+ */
+ private final AtomicFile mStatisticsFile;
+
+ /**
+ * This file contains the pending sync operations. It is a binary file,
+ * which must be updated every time an operation is added or removed,
+ * so we have special handling of it.
+ */
+ private final AtomicFile mPendingFile;
+ private static final int PENDING_FINISH_TO_WRITE = 4;
+ private int mNumPendingFinished = 0;
+
+ private int mNextHistoryId = 0;
+ private boolean mListenForTickles = true;
+
private SyncStorageEngine(Context context) {
mContext = context;
- mOpenHelper = new SyncStorageEngine.DatabaseHelper(context);
sSyncStorageEngine = this;
+
+ mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
+
+ File dataDir = Environment.getDataDirectory();
+ File systemDir = new File(dataDir, "system");
+ File syncDir = new File(systemDir, "sync");
+ mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"));
+ mStatusFile = new AtomicFile(new File(syncDir, "status.bin"));
+ mPendingFile = new AtomicFile(new File(syncDir, "pending.bin"));
+ mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin"));
+
+ readAccountInfoLocked();
+ readStatusLocked();
+ readPendingOperationsLocked();
+ readStatisticsLocked();
+ readLegacyAccountInfoLocked();
}
public static SyncStorageEngine newTestInstance(Context context) {
@@ -147,650 +300,1263 @@
return sSyncStorageEngine;
}
- private class DatabaseHelper extends SQLiteOpenHelper {
- DatabaseHelper(Context context) {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE pending ("
- + "_id INTEGER PRIMARY KEY,"
- + "authority TEXT NOT NULL,"
- + "account TEXT NOT NULL,"
- + "account_type TEXT NOT NULL,"
- + "extras BLOB NOT NULL,"
- + "source INTEGER NOT NULL"
- + ");");
-
- db.execSQL("CREATE TABLE stats (" +
- "_id INTEGER PRIMARY KEY," +
- "account TEXT, " +
- "account_type TEXT, " +
- "authority TEXT, " +
- "syncdata TEXT, " +
- "UNIQUE (account, authority)" +
- ");");
-
- db.execSQL("CREATE TABLE history (" +
- "_id INTEGER PRIMARY KEY," +
- "stats_id INTEGER," +
- "eventTime INTEGER," +
- "elapsedTime INTEGER," +
- "source INTEGER," +
- "event INTEGER," +
- "upstreamActivity INTEGER," +
- "downstreamActivity INTEGER," +
- "mesg TEXT);");
-
- db.execSQL("CREATE TABLE status ("
- + "_id INTEGER PRIMARY KEY,"
- + "stats_id INTEGER NOT NULL,"
- + "totalElapsedTime INTEGER NOT NULL DEFAULT 0,"
- + "numSyncs INTEGER NOT NULL DEFAULT 0,"
- + "numSourcePoll INTEGER NOT NULL DEFAULT 0,"
- + "numSourceServer INTEGER NOT NULL DEFAULT 0,"
- + "numSourceLocal INTEGER NOT NULL DEFAULT 0,"
- + "numSourceUser INTEGER NOT NULL DEFAULT 0,"
- + "lastSuccessTime INTEGER,"
- + "lastSuccessSource INTEGER,"
- + "lastFailureTime INTEGER,"
- + "lastFailureSource INTEGER,"
- + "lastFailureMesg STRING,"
- + "initialFailureTime INTEGER,"
- + "pending INTEGER NOT NULL DEFAULT 0);");
-
- db.execSQL("CREATE TABLE active ("
- + "_id INTEGER PRIMARY KEY,"
- + "authority TEXT,"
- + "account TEXT,"
- + "account_type TEXT,"
- + "startTime INTEGER);");
-
- db.execSQL("CREATE INDEX historyEventTime ON history (eventTime)");
-
- db.execSQL("CREATE TABLE settings (" +
- "name TEXT PRIMARY KEY," +
- "value TEXT);");
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- if (oldVersion == 9) {
- Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
- + newVersion + ", which will preserve old data");
- db.execSQL("ALTER TABLE status ADD COLUMN initialFailureTime INTEGER");
- oldVersion++;
+ @Override public void handleMessage(Message msg) {
+ if (msg.what == MSG_WRITE_STATUS) {
+ synchronized (mAccounts) {
+ writeStatusLocked();
}
-
- if (oldVersion == 10) {
- Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
- + newVersion + ", which will preserve old data");
- db.execSQL("ALTER TABLE pending ADD COLUMN account_type TEXT");
- db.execSQL("ALTER TABLE stats ADD COLUMN account_type TEXT");
- db.execSQL("ALTER TABLE active ADD COLUMN account_type TEXT");
-
- db.execSQL("UPDATE pending SET account_type='com.google.GAIA'");
- db.execSQL("UPDATE stats SET account_type='com.google.GAIA'");
- db.execSQL("UPDATE active SET account_type='com.google.GAIA'");
- oldVersion++;
- }
-
- if (oldVersion == newVersion) {
- return;
- }
-
- Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
- + newVersion + ", which will destroy all old data");
- db.execSQL("DROP TABLE IF EXISTS pending");
- db.execSQL("DROP TABLE IF EXISTS stats");
- db.execSQL("DROP TABLE IF EXISTS history");
- db.execSQL("DROP TABLE IF EXISTS settings");
- db.execSQL("DROP TABLE IF EXISTS active");
- db.execSQL("DROP TABLE IF EXISTS status");
- onCreate(db);
- }
-
- @Override
- public void onOpen(SQLiteDatabase db) {
- if (!db.isReadOnly()) {
- db.delete("active", null, null);
- db.insert("active", "account", null);
+ } else if (msg.what == MSG_WRITE_STATISTICS) {
+ synchronized (mAccounts) {
+ writeStatisticsLocked();
}
}
}
+
+ public void addStatusChangeListener(int mask, ISyncStatusObserver callback) {
+ synchronized (mAuthorities) {
+ mChangeListeners.register(callback, mask);
+ }
+ }
+
+ public void removeStatusChangeListener(ISyncStatusObserver callback) {
+ synchronized (mAuthorities) {
+ mChangeListeners.unregister(callback);
+ }
+ }
+
+ private void reportChange(int which) {
+ ArrayList<ISyncStatusObserver> reports = null;
+ synchronized (mAuthorities) {
+ int i = mChangeListeners.beginBroadcast();
+ while (i > 0) {
+ i--;
+ Integer mask = (Integer)mChangeListeners.getBroadcastCookie(i);
+ if ((which & mask.intValue()) == 0) {
+ continue;
+ }
+ if (reports == null) {
+ reports = new ArrayList<ISyncStatusObserver>(i);
+ }
+ reports.add(mChangeListeners.getBroadcastItem(i));
+ }
+ }
+
+ if (DEBUG) Log.v(TAG, "reportChange " + which + " to: " + reports);
+
+ if (reports != null) {
+ int i = reports.size();
+ while (i > 0) {
+ i--;
+ try {
+ reports.get(i).onStatusChanged(which);
+ } catch (RemoteException e) {
+ // The remote callback list will take care of this for us.
+ }
+ }
+ }
+ }
+
+ public boolean getSyncProviderAutomatically(Account account, String providerName) {
+ synchronized (mAuthorities) {
+ if (account != null) {
+ AuthorityInfo authority = getAuthorityLocked(account, providerName,
+ "getSyncProviderAutomatically");
+ return authority != null ? authority.enabled : false;
+ }
+
+ int i = mAuthorities.size();
+ while (i > 0) {
+ i--;
+ AuthorityInfo authority = mAuthorities.get(i);
+ if (authority.authority.equals(providerName)
+ && authority.enabled) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
- protected void doDatabaseCleanup(Account[] accounts) {
- HashSet<Account> currentAccounts = Sets.newHashSet(accounts);
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- Cursor cursor = db.query("stats", STATS_ACCOUNT_PROJECTION,
- null /* where */, null /* where args */,
- Sync.Stats.ACCOUNT + "," + Sync.Stats.ACCOUNT_TYPE,
- null /* having */, null /* order by */);
- try {
- while (cursor.moveToNext()) {
- String accountName = cursor.getString(0);
- String accountType = cursor.getString(1);
- final Account account = new Account(accountName, accountType);
- if (!currentAccounts.contains(account)) {
- String where = Sync.Stats.ACCOUNT + "=? AND " + Sync.Stats.ACCOUNT_TYPE + "=?";
- int numDeleted;
- numDeleted = db.delete("stats", where,
- new String[]{account.mName, account.mType});
- if (Config.LOGD) {
- Log.d(TAG, "deleted " + numDeleted
- + " records from stats table"
- + " for account " + account);
+ public void setSyncProviderAutomatically(Account account, String providerName,
+ boolean sync) {
+ synchronized (mAuthorities) {
+ if (account != null) {
+ AuthorityInfo authority = getAuthorityLocked(account, providerName,
+ "setSyncProviderAutomatically");
+ if (authority != null) {
+ authority.enabled = sync;
+ }
+ } else {
+ int i = mAuthorities.size();
+ while (i > 0) {
+ i--;
+ AuthorityInfo authority = mAuthorities.get(i);
+ if (authority.account.equals(account)
+ && authority.authority.equals(providerName)) {
+ authority.enabled = sync;
}
}
}
- } finally {
- cursor.close();
+ writeAccountInfoLocked();
}
- }
-
- protected void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
- if (activeSyncContext != null) {
- updateActiveSync(activeSyncContext.mSyncOperation.account,
- activeSyncContext.mSyncOperation.authority, activeSyncContext.mStartTime);
- } else {
- // we indicate that the sync is not active by passing null for all the parameters
- updateActiveSync(null, null, null);
- }
- }
-
- private int updateActiveSync(Account account, String authority, Long startTime) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- ContentValues values = new ContentValues();
- values.put("account", account == null ? null : account.mName);
- values.put("account_type", account == null ? null : account.mType);
- values.put("authority", authority);
- values.put("startTime", startTime);
- int numChanges = db.update("active", values, null, null);
- if (numChanges > 0) {
- mContext.getContentResolver().notifyChange(Sync.Active.CONTENT_URI,
- null /* this change wasn't made through an observer */);
- }
- return numChanges;
- }
-
- /**
- * Implements the {@link ContentProvider#query} method
- */
- public Cursor query(Uri url, String[] projectionIn,
- String selection, String[] selectionArgs, String sort) {
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-
- // Generate the body of the query
- int match = sURLMatcher.match(url);
- String groupBy = null;
- switch (match) {
- case STATS:
- qb.setTables("stats");
- break;
- case STATS_ID:
- qb.setTables("stats");
- qb.appendWhere("_id=");
- qb.appendWhere(url.getPathSegments().get(1));
- break;
- case HISTORY:
- // join the stats and history tables, so the caller can get
- // the account and authority information as part of this query.
- qb.setTables("stats, history");
- qb.setProjectionMap(HISTORY_PROJECTION_MAP);
- qb.appendWhere("stats._id = history.stats_id");
- break;
- case ACTIVE:
- qb.setTables("active");
- qb.setProjectionMap(ACTIVE_PROJECTION_MAP);
- qb.appendWhere("account is not null");
- break;
- case PENDING:
- qb.setTables("pending");
- qb.setProjectionMap(PENDING_PROJECTION_MAP);
- groupBy = "account, authority";
- break;
- case STATUS:
- // join the stats and status tables, so the caller can get
- // the account and authority information as part of this query.
- qb.setTables("stats, status");
- qb.setProjectionMap(STATUS_PROJECTION_MAP);
- qb.appendWhere("stats._id = status.stats_id");
- break;
- case HISTORY_ID:
- // join the stats and history tables, so the caller can get
- // the account and authority information as part of this query.
- qb.setTables("stats, history");
- qb.setProjectionMap(HISTORY_PROJECTION_MAP);
- qb.appendWhere("stats._id = history.stats_id");
- qb.appendWhere("AND history._id=");
- qb.appendWhere(url.getPathSegments().get(1));
- break;
- case SETTINGS:
- qb.setTables("settings");
- break;
- default:
- throw new IllegalArgumentException("Unknown URL " + url);
- }
-
- if (match == SETTINGS) {
- mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
- "no permission to read the sync settings");
- } else {
- mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
- "no permission to read the sync stats");
- }
- SQLiteDatabase db = mOpenHelper.getReadableDatabase();
- Cursor c = qb.query(db, projectionIn, selection, selectionArgs, groupBy, null, sort);
- c.setNotificationUri(mContext.getContentResolver(), url);
- return c;
- }
-
- /**
- * Implements the {@link ContentProvider#insert} method
- * @param callerIsTheProvider true if this is being called via the
- * {@link ContentProvider#insert} in method rather than directly.
- * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
- * for the Settings table.
- */
- public Uri insert(boolean callerIsTheProvider, Uri url, ContentValues values) {
- String table;
- long rowID;
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- final int match = sURLMatcher.match(url);
- checkCaller(callerIsTheProvider, match);
- switch (match) {
- case SETTINGS:
- mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
- "no permission to write the sync settings");
- table = "settings";
- rowID = db.replace(table, null, values);
- break;
- default:
- throw new IllegalArgumentException("Unknown URL " + url);
- }
-
-
- if (rowID > 0) {
- mContext.getContentResolver().notifyChange(url, null /* observer */);
- return Uri.parse("content://sync/" + table + "/" + rowID);
- }
-
- return null;
- }
-
- private static void checkCaller(boolean callerIsTheProvider, int match) {
- if (callerIsTheProvider && match != SETTINGS) {
- throw new UnsupportedOperationException(
- "only the settings are modifiable via the ContentProvider interface");
- }
- }
-
- /**
- * Implements the {@link ContentProvider#delete} method
- * @param callerIsTheProvider true if this is being called via the
- * {@link ContentProvider#delete} in method rather than directly.
- * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
- * for the Settings table.
- */
- public int delete(boolean callerIsTheProvider, Uri url, String where, String[] whereArgs) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- int match = sURLMatcher.match(url);
-
- int numRows;
- switch (match) {
- case SETTINGS:
- mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
- "no permission to write the sync settings");
- numRows = db.delete("settings", where, whereArgs);
- break;
- default:
- throw new UnsupportedOperationException("Cannot delete URL: " + url);
- }
-
- if (numRows > 0) {
- mContext.getContentResolver().notifyChange(url, null /* observer */);
- }
- return numRows;
- }
-
- /**
- * Implements the {@link ContentProvider#update} method
- * @param callerIsTheProvider true if this is being called via the
- * {@link ContentProvider#update} in method rather than directly.
- * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't
- * for the Settings table.
- */
- public int update(boolean callerIsTheProvider, Uri url, ContentValues initialValues,
- String where, String[] whereArgs) {
- switch (sURLMatcher.match(url)) {
- case SETTINGS:
- throw new UnsupportedOperationException("updating url " + url
- + " is not allowed, use insert instead");
- default:
- throw new UnsupportedOperationException("Cannot update URL: " + url);
- }
- }
-
- /**
- * Implements the {@link ContentProvider#getType} method
- */
- public String getType(Uri url) {
- int match = sURLMatcher.match(url);
- switch (match) {
- case SETTINGS:
- return "vnd.android.cursor.dir/sync-settings";
- default:
- throw new IllegalArgumentException("Unknown URL");
- }
- }
-
- protected Uri insertIntoPending(ContentValues values) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- try {
- db.beginTransaction();
- long rowId = db.insert("pending", Sync.Pending.ACCOUNT, values);
- if (rowId < 0) return null;
- String accountName = values.getAsString(Sync.Pending.ACCOUNT);
- String accountType = values.getAsString(Sync.Pending.ACCOUNT_TYPE);
- final Account account = new Account(accountName, accountType);
- String authority = values.getAsString(Sync.Pending.AUTHORITY);
-
- long statsId = createStatsRowIfNecessary(account, authority);
- createStatusRowIfNecessary(statsId);
-
- values.clear();
- values.put(Sync.Status.PENDING, 1);
- int numUpdatesStatus = db.update("status", values, "stats_id=" + statsId, null);
-
- db.setTransactionSuccessful();
-
- mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
- null /* no observer initiated this change */);
- if (numUpdatesStatus > 0) {
- mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
- null /* no observer initiated this change */);
- }
- return ContentUris.withAppendedId(Sync.Pending.CONTENT_URI, rowId);
- } finally {
- db.endTransaction();
- }
- }
-
- int deleteFromPending(long rowId) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- db.beginTransaction();
- try {
- Account account;
- String authority;
- Cursor c = db.query("pending",
- new String[]{Sync.Pending.ACCOUNT, Sync.Pending.ACCOUNT_TYPE,
- Sync.Pending.AUTHORITY},
- "_id=" + rowId, null, null, null, null);
- try {
- if (c.getCount() != 1) {
- return 0;
- }
- c.moveToNext();
- String accountName = c.getString(0);
- String accountType = c.getString(1);
- account = new Account(accountName, accountType);
- authority = c.getString(2);
- } finally {
- c.close();
- }
- db.delete("pending", "_id=" + rowId, null /* no where args */);
- final String[] accountAuthorityWhereArgs =
- new String[]{account.mName, account.mType, authority};
- boolean isPending = 0 < DatabaseUtils.longForQuery(db,
- "SELECT COUNT(*)"
- + " FROM PENDING"
- + " WHERE account=? AND account_type=? AND authority=?",
- accountAuthorityWhereArgs);
- if (!isPending) {
- long statsId = createStatsRowIfNecessary(account, authority);
- db.execSQL("UPDATE status SET pending=0 WHERE stats_id=" + statsId);
- }
- db.setTransactionSuccessful();
-
- mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
- null /* no observer initiated this change */);
- if (!isPending) {
- mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
- null /* no observer initiated this change */);
- }
- return 1;
- } finally {
- db.endTransaction();
- }
- }
-
- int clearPending() {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- db.beginTransaction();
- try {
- int numChanges = db.delete("pending", null, null /* no where args */);
- if (numChanges > 0) {
- db.execSQL("UPDATE status SET pending=0");
- mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI,
- null /* no observer initiated this change */);
- mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
- null /* no observer initiated this change */);
- }
- db.setTransactionSuccessful();
- return numChanges;
- } finally {
- db.endTransaction();
- }
- }
-
- /**
- * Returns a cursor over all the pending syncs in no particular order. This cursor is not
- * "live", in that if changes are made to the pending table any observers on this cursor
- * will not be notified.
- * @param projection Return only these columns. If null then all columns are returned.
- * @return the cursor of pending syncs
- */
- public Cursor getPendingSyncsCursor(String[] projection) {
- SQLiteDatabase db = mOpenHelper.getReadableDatabase();
- return db.query("pending", projection, null, null, null, null, null);
- }
-
- // @VisibleForTesting
- static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4;
-
- private boolean purgeOldHistoryEvents(long now) {
- // remove events that are older than MILLIS_IN_4WEEKS
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- int numDeletes = db.delete("history", "eventTime<" + (now - MILLIS_IN_4WEEKS), null);
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- if (numDeletes > 0) {
- Log.v(TAG, "deleted " + numDeletes + " old event(s) from the sync history");
- }
- }
-
- // keep only the last MAX_HISTORY_EVENTS_TO_KEEP history events
- numDeletes += db.delete("history", "eventTime < (select min(eventTime) from "
- + "(select eventTime from history order by eventTime desc limit ?))",
- new String[]{String.valueOf(MAX_HISTORY_EVENTS_TO_KEEP)});
- return numDeletes > 0;
+ reportChange(CHANGE_SETTINGS);
}
- public long insertStartSyncEvent(Account account, String authority, long now, int source) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- long statsId = createStatsRowIfNecessary(account, authority);
+ public void setListenForNetworkTickles(boolean flag) {
+ synchronized (mAuthorities) {
+ mListenForTickles = flag;
+ writeAccountInfoLocked();
+ }
+ reportChange(CHANGE_SETTINGS);
+ }
- purgeOldHistoryEvents(now);
- ContentValues values = new ContentValues();
- values.put(Sync.History.STATS_ID, statsId);
- values.put(Sync.History.EVENT_TIME, now);
- values.put(Sync.History.SOURCE, source);
- values.put(Sync.History.EVENT, Sync.History.EVENT_START);
- long rowId = db.insert("history", null, values);
- mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI, null /* observer */);
- mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, null /* observer */);
- return rowId;
+ public boolean getListenForNetworkTickles() {
+ synchronized (mAuthorities) {
+ return mListenForTickles;
+ }
+ }
+
+ public AuthorityInfo getAuthority(Account account, String authority) {
+ synchronized (mAuthorities) {
+ return getAuthorityLocked(account, authority, null);
+ }
+ }
+
+ public AuthorityInfo getAuthority(int authorityId) {
+ synchronized (mAuthorities) {
+ return mAuthorities.get(authorityId);
+ }
+ }
+
+ /**
+ * Returns true if there is currently a sync operation for the given
+ * account or authority in the pending list, or actively being processed.
+ */
+ public boolean isSyncActive(Account account, String authority) {
+ synchronized (mAuthorities) {
+ int i = mPendingOperations.size();
+ while (i > 0) {
+ i--;
+ // TODO(fredq): this probably shouldn't be considering
+ // pending operations.
+ PendingOperation op = mPendingOperations.get(i);
+ if (op.account.equals(account) && op.authority.equals(authority)) {
+ return true;
+ }
+ }
+
+ if (mActiveSync != null) {
+ AuthorityInfo ainfo = getAuthority(mActiveSync.authorityId);
+ if (ainfo != null && ainfo.account.equals(account)
+ && ainfo.authority.equals(authority)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public PendingOperation insertIntoPending(PendingOperation op) {
+ synchronized (mAuthorities) {
+ if (DEBUG) Log.v(TAG, "insertIntoPending: account=" + op.account
+ + " auth=" + op.authority
+ + " src=" + op.syncSource
+ + " extras=" + op.extras);
+
+ AuthorityInfo authority = getOrCreateAuthorityLocked(op.account,
+ op.authority,
+ -1 /* desired identifier */,
+ true /* write accounts to storage */);
+ if (authority == null) {
+ return null;
+ }
+
+ op = new PendingOperation(op);
+ op.authorityId = authority.ident;
+ mPendingOperations.add(op);
+ appendPendingOperationLocked(op);
+
+ SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
+ status.pending = true;
+ }
+
+ reportChange(CHANGE_PENDING);
+ return op;
+ }
+
+ public boolean deleteFromPending(PendingOperation op) {
+ boolean res = false;
+ synchronized (mAuthorities) {
+ if (DEBUG) Log.v(TAG, "deleteFromPending: account=" + op.account
+ + " auth=" + op.authority
+ + " src=" + op.syncSource
+ + " extras=" + op.extras);
+ if (mPendingOperations.remove(op)) {
+ if (mPendingOperations.size() == 0
+ || mNumPendingFinished >= PENDING_FINISH_TO_WRITE) {
+ writePendingOperationsLocked();
+ mNumPendingFinished = 0;
+ } else {
+ mNumPendingFinished++;
+ }
+
+ AuthorityInfo authority = getAuthorityLocked(op.account, op.authority,
+ "deleteFromPending");
+ if (authority != null) {
+ if (DEBUG) Log.v(TAG, "removing - " + authority);
+ final int N = mPendingOperations.size();
+ boolean morePending = false;
+ for (int i=0; i<N; i++) {
+ PendingOperation cur = mPendingOperations.get(i);
+ if (cur.account.equals(op.account)
+ && cur.authority.equals(op.authority)) {
+ morePending = true;
+ break;
+ }
+ }
+
+ if (!morePending) {
+ if (DEBUG) Log.v(TAG, "no more pending!");
+ SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
+ status.pending = false;
+ }
+ }
+
+ res = true;
+ }
+ }
+
+ reportChange(CHANGE_PENDING);
+ return res;
+ }
+
+ public int clearPending() {
+ int num;
+ synchronized (mAuthorities) {
+ if (DEBUG) Log.v(TAG, "clearPending");
+ num = mPendingOperations.size();
+ mPendingOperations.clear();
+ final int N = mSyncStatus.size();
+ for (int i=0; i<N; i++) {
+ mSyncStatus.get(i).pending = false;
+ }
+ writePendingOperationsLocked();
+ }
+ reportChange(CHANGE_PENDING);
+ return num;
+ }
+
+ /**
+ * Return a copy of the current array of pending operations. The
+ * PendingOperation objects are the real objects stored inside, so that
+ * they can be used with deleteFromPending().
+ */
+ public ArrayList<PendingOperation> getPendingOperations() {
+ synchronized (mAuthorities) {
+ return new ArrayList<PendingOperation>(mPendingOperations);
+ }
+ }
+
+ /**
+ * Return the number of currently pending operations.
+ */
+ public int getPendingOperationCount() {
+ synchronized (mAuthorities) {
+ return mPendingOperations.size();
+ }
+ }
+
+ /**
+ * Called when the set of account has changed, given the new array of
+ * active accounts.
+ */
+ public void doDatabaseCleanup(Account[] accounts) {
+ synchronized (mAuthorities) {
+ if (DEBUG) Log.w(TAG, "Updating for new accounts...");
+ SparseArray<AuthorityInfo> removing = new SparseArray<AuthorityInfo>();
+ Iterator<AccountInfo> accIt = mAccounts.values().iterator();
+ while (accIt.hasNext()) {
+ AccountInfo acc = accIt.next();
+ if (!ArrayUtils.contains(accounts, acc.account)) {
+ // This account no longer exists...
+ if (DEBUG) Log.w(TAG, "Account removed: " + acc.account);
+ for (AuthorityInfo auth : acc.authorities.values()) {
+ removing.put(auth.ident, auth);
+ }
+ accIt.remove();
+ }
+ }
+
+ // Clean out all data structures.
+ int i = removing.size();
+ if (i > 0) {
+ while (i > 0) {
+ i--;
+ int ident = removing.keyAt(i);
+ mAuthorities.remove(ident);
+ int j = mSyncStatus.size();
+ while (j > 0) {
+ j--;
+ if (mSyncStatus.keyAt(j) == ident) {
+ mSyncStatus.remove(mSyncStatus.keyAt(j));
+ }
+ }
+ j = mSyncHistory.size();
+ while (j > 0) {
+ j--;
+ if (mSyncHistory.get(j).authorityId == ident) {
+ mSyncHistory.remove(j);
+ }
+ }
+ }
+ writeAccountInfoLocked();
+ writeStatusLocked();
+ writePendingOperationsLocked();
+ writeStatisticsLocked();
+ }
+ }
+ }
+
+ /**
+ * Called when the currently active sync is changing (there can only be
+ * one at a time). Either supply a valid ActiveSyncContext with information
+ * about the sync, or null to stop the currently active sync.
+ */
+ public void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
+ synchronized (mAuthorities) {
+ if (activeSyncContext != null) {
+ if (DEBUG) Log.v(TAG, "setActiveSync: account="
+ + activeSyncContext.mSyncOperation.account
+ + " auth=" + activeSyncContext.mSyncOperation.authority
+ + " src=" + activeSyncContext.mSyncOperation.syncSource
+ + " extras=" + activeSyncContext.mSyncOperation.extras);
+ if (mActiveSync != null) {
+ Log.w(TAG, "setActiveSync called with existing active sync!");
+ }
+ AuthorityInfo authority = getAuthorityLocked(
+ activeSyncContext.mSyncOperation.account,
+ activeSyncContext.mSyncOperation.authority,
+ "setActiveSync");
+ if (authority == null) {
+ return;
+ }
+ mActiveSync = new ActiveSyncInfo(authority.ident,
+ authority.account, authority.authority,
+ activeSyncContext.mStartTime);
+ } else {
+ if (DEBUG) Log.v(TAG, "setActiveSync: null");
+ mActiveSync = null;
+ }
+ }
+
+ reportChange(CHANGE_ACTIVE);
+ }
+
+ /**
+ * To allow others to send active change reports, to poke clients.
+ */
+ public void reportActiveChange() {
+ reportChange(CHANGE_ACTIVE);
+ }
+
+ /**
+ * Note that sync has started for the given account and authority.
+ */
+ public long insertStartSyncEvent(Account accountName, String authorityName,
+ long now, int source) {
+ long id;
+ synchronized (mAuthorities) {
+ if (DEBUG) Log.v(TAG, "insertStartSyncEvent: account=" + accountName
+ + " auth=" + authorityName + " source=" + source);
+ AuthorityInfo authority = getAuthorityLocked(accountName, authorityName,
+ "insertStartSyncEvent");
+ if (authority == null) {
+ return -1;
+ }
+ SyncHistoryItem item = new SyncHistoryItem();
+ item.authorityId = authority.ident;
+ item.historyId = mNextHistoryId++;
+ if (mNextHistoryId < 0) mNextHistoryId = 0;
+ item.eventTime = now;
+ item.source = source;
+ item.event = EVENT_START;
+ mSyncHistory.add(0, item);
+ while (mSyncHistory.size() > MAX_HISTORY) {
+ mSyncHistory.remove(mSyncHistory.size()-1);
+ }
+ id = item.historyId;
+ if (DEBUG) Log.v(TAG, "returning historyId " + id);
+ }
+
+ reportChange(CHANGE_STATUS);
+ return id;
}
public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage,
long downstreamActivity, long upstreamActivity) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- db.beginTransaction();
- try {
- ContentValues values = new ContentValues();
- values.put(Sync.History.ELAPSED_TIME, elapsedTime);
- values.put(Sync.History.EVENT, Sync.History.EVENT_STOP);
- values.put(Sync.History.MESG, resultMessage);
- values.put(Sync.History.DOWNSTREAM_ACTIVITY, downstreamActivity);
- values.put(Sync.History.UPSTREAM_ACTIVITY, upstreamActivity);
-
- int count = db.update("history", values, "_id=?",
- new String[]{Long.toString(historyId)});
- // We think that count should always be 1 but don't want to change this until after
- // launch.
- if (count > 0) {
- int source = (int) DatabaseUtils.longForQuery(db,
- "SELECT source FROM history WHERE _id=" + historyId, null);
- long eventTime = DatabaseUtils.longForQuery(db,
- "SELECT eventTime FROM history WHERE _id=" + historyId, null);
- long statsId = DatabaseUtils.longForQuery(db,
- "SELECT stats_id FROM history WHERE _id=" + historyId, null);
-
- createStatusRowIfNecessary(statsId);
-
- // update the status table to reflect this sync
- StringBuilder sb = new StringBuilder();
- ArrayList<String> bindArgs = new ArrayList<String>();
- sb.append("UPDATE status SET");
- sb.append(" numSyncs=numSyncs+1");
- sb.append(", totalElapsedTime=totalElapsedTime+" + elapsedTime);
- switch (source) {
- case Sync.History.SOURCE_LOCAL:
- sb.append(", numSourceLocal=numSourceLocal+1");
- break;
- case Sync.History.SOURCE_POLL:
- sb.append(", numSourcePoll=numSourcePoll+1");
- break;
- case Sync.History.SOURCE_USER:
- sb.append(", numSourceUser=numSourceUser+1");
- break;
- case Sync.History.SOURCE_SERVER:
- sb.append(", numSourceServer=numSourceServer+1");
- break;
+ synchronized (mAuthorities) {
+ if (DEBUG) Log.v(TAG, "stopSyncEvent: historyId=" + historyId);
+ SyncHistoryItem item = null;
+ int i = mSyncHistory.size();
+ while (i > 0) {
+ i--;
+ item = mSyncHistory.get(i);
+ if (item.historyId == historyId) {
+ break;
}
-
- final String statsIdString = String.valueOf(statsId);
- final long lastSyncTime = (eventTime + elapsedTime);
- if (Sync.History.MESG_SUCCESS.equals(resultMessage)) {
- // - if successful, update the successful columns
- sb.append(", lastSuccessTime=" + lastSyncTime);
- sb.append(", lastSuccessSource=" + source);
- sb.append(", lastFailureTime=null");
- sb.append(", lastFailureSource=null");
- sb.append(", lastFailureMesg=null");
- sb.append(", initialFailureTime=null");
- } else if (!Sync.History.MESG_CANCELED.equals(resultMessage)) {
- sb.append(", lastFailureTime=" + lastSyncTime);
- sb.append(", lastFailureSource=" + source);
- sb.append(", lastFailureMesg=?");
- bindArgs.add(resultMessage);
- long initialFailureTime = DatabaseUtils.longForQuery(db,
- SELECT_INITIAL_FAILURE_TIME_QUERY_STRING,
- new String[]{statsIdString, String.valueOf(lastSyncTime)});
- sb.append(", initialFailureTime=" + initialFailureTime);
- }
- sb.append(" WHERE stats_id=?");
- bindArgs.add(statsIdString);
- db.execSQL(sb.toString(), bindArgs.toArray());
- db.setTransactionSuccessful();
- mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI,
- null /* observer */);
- mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI,
- null /* observer */);
+ item = null;
}
- } finally {
- db.endTransaction();
+
+ if (item == null) {
+ Log.w(TAG, "stopSyncEvent: no history for id " + historyId);
+ return;
+ }
+
+ item.elapsedTime = elapsedTime;
+ item.event = EVENT_STOP;
+ item.mesg = resultMessage;
+ item.downstreamActivity = downstreamActivity;
+ item.upstreamActivity = upstreamActivity;
+
+ SyncStatusInfo status = getOrCreateSyncStatusLocked(item.authorityId);
+
+ status.numSyncs++;
+ status.totalElapsedTime += elapsedTime;
+ switch (item.source) {
+ case SOURCE_LOCAL:
+ status.numSourceLocal++;
+ break;
+ case SOURCE_POLL:
+ status.numSourcePoll++;
+ break;
+ case SOURCE_USER:
+ status.numSourceUser++;
+ break;
+ case SOURCE_SERVER:
+ status.numSourceServer++;
+ break;
+ }
+
+ boolean writeStatisticsNow = false;
+ int day = getCurrentDay();
+ if (mDayStats[0] == null) {
+ mDayStats[0] = new DayStats(day);
+ } else if (day != mDayStats[0].day) {
+ System.arraycopy(mDayStats, 0, mDayStats, 1, mDayStats.length-1);
+ mDayStats[0] = new DayStats(day);
+ writeStatisticsNow = true;
+ } else if (mDayStats[0] == null) {
+ }
+ final DayStats ds = mDayStats[0];
+
+ final long lastSyncTime = (item.eventTime + elapsedTime);
+ boolean writeStatusNow = false;
+ if (MESG_SUCCESS.equals(resultMessage)) {
+ // - if successful, update the successful columns
+ if (status.lastSuccessTime == 0 || status.lastFailureTime != 0) {
+ writeStatusNow = true;
+ }
+ status.lastSuccessTime = lastSyncTime;
+ status.lastSuccessSource = item.source;
+ status.lastFailureTime = 0;
+ status.lastFailureSource = -1;
+ status.lastFailureMesg = null;
+ status.initialFailureTime = 0;
+ ds.successCount++;
+ ds.successTime += elapsedTime;
+ } else if (!MESG_CANCELED.equals(resultMessage)) {
+ if (status.lastFailureTime == 0) {
+ writeStatusNow = true;
+ }
+ status.lastFailureTime = lastSyncTime;
+ status.lastFailureSource = item.source;
+ status.lastFailureMesg = resultMessage;
+ if (status.initialFailureTime == 0) {
+ status.initialFailureTime = lastSyncTime;
+ }
+ ds.failureCount++;
+ ds.failureTime += elapsedTime;
+ }
+
+ if (writeStatusNow) {
+ writeStatusLocked();
+ } else if (!hasMessages(MSG_WRITE_STATUS)) {
+ sendMessageDelayed(obtainMessage(MSG_WRITE_STATUS),
+ WRITE_STATUS_DELAY);
+ }
+ if (writeStatisticsNow) {
+ writeStatisticsLocked();
+ } else if (!hasMessages(MSG_WRITE_STATISTICS)) {
+ sendMessageDelayed(obtainMessage(MSG_WRITE_STATISTICS),
+ WRITE_STATISTICS_DELAY);
+ }
+ }
+
+ reportChange(CHANGE_STATUS);
+ }
+
+ /**
+ * Return the currently active sync information, or null if there is no
+ * active sync. Note that the returned object is the real, live active
+ * sync object, so be careful what you do with it.
+ */
+ public ActiveSyncInfo getActiveSync() {
+ synchronized (mAuthorities) {
+ return mActiveSync;
+ }
+ }
+
+ /**
+ * Return an array of the current sync status for all authorities. Note
+ * that the objects inside the array are the real, live status objects,
+ * so be careful what you do with them.
+ */
+ public ArrayList<SyncStatusInfo> getSyncStatus() {
+ synchronized (mAuthorities) {
+ final int N = mSyncStatus.size();
+ ArrayList<SyncStatusInfo> ops = new ArrayList<SyncStatusInfo>(N);
+ for (int i=0; i<N; i++) {
+ ops.add(mSyncStatus.valueAt(i));
+ }
+ return ops;
+ }
+ }
+
+ /**
+ * Returns the status that matches the authority. If there are multiples accounts for
+ * the authority, the one with the latest "lastSuccessTime" status is returned.
+ * @param authority the authority whose row should be selected
+ * @return the SyncStatusInfo for the authority, or null if none exists
+ */
+ public SyncStatusInfo getStatusByAuthority(String authority) {
+ synchronized (mAuthorities) {
+ SyncStatusInfo best = null;
+ final int N = mSyncStatus.size();
+ for (int i=0; i<N; i++) {
+ SyncStatusInfo cur = mSyncStatus.get(i);
+ AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
+ if (ainfo != null && ainfo.authority.equals(authority)) {
+ if (best == null) {
+ best = cur;
+ } else if (best.lastSuccessTime > cur.lastSuccessTime) {
+ best = cur;
+ }
+ }
+ }
+ return best;
+ }
+ }
+
+ /**
+ * Return true if the pending status is true of any matching authorities.
+ */
+ public boolean isAuthorityPending(Account account, String authority) {
+ synchronized (mAuthorities) {
+ final int N = mSyncStatus.size();
+ for (int i=0; i<N; i++) {
+ SyncStatusInfo cur = mSyncStatus.get(i);
+ AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
+ if (ainfo == null) {
+ continue;
+ }
+ if (account != null && !ainfo.account.equals(account)) {
+ continue;
+ }
+ if (ainfo.authority.equals(authority) && cur.pending) {
+ return true;
+ }
+ }
+ return false;
}
}
/**
+ * Return an array of the current sync status for all authorities. Note
+ * that the objects inside the array are the real, live status objects,
+ * so be careful what you do with them.
+ */
+ public ArrayList<SyncHistoryItem> getSyncHistory() {
+ synchronized (mAuthorities) {
+ final int N = mSyncHistory.size();
+ ArrayList<SyncHistoryItem> items = new ArrayList<SyncHistoryItem>(N);
+ for (int i=0; i<N; i++) {
+ items.add(mSyncHistory.get(i));
+ }
+ return items;
+ }
+ }
+
+ /**
+ * Return an array of the current per-day statistics. Note
+ * that the objects inside the array are the real, live status objects,
+ * so be careful what you do with them.
+ */
+ public DayStats[] getDayStatistics() {
+ synchronized (mAuthorities) {
+ DayStats[] ds = new DayStats[mDayStats.length];
+ System.arraycopy(mDayStats, 0, ds, 0, ds.length);
+ return ds;
+ }
+ }
+
+ /**
* If sync is failing for any of the provider/accounts then determine the time at which it
* started failing and return the earliest time over all the provider/accounts. If none are
* failing then return 0.
*/
public long getInitialSyncFailureTime() {
- SQLiteDatabase db = mOpenHelper.getReadableDatabase();
- // Join the settings for a provider with the status so that we can easily
- // check if each provider is enabled for syncing. We also join in the overall
- // enabled flag ("listen_for_tickles") to each row so that we don't need to
- // make a separate DB lookup to access it.
- Cursor c = db.rawQuery(""
- + "SELECT initialFailureTime, s1.value, s2.value "
- + "FROM status "
- + "LEFT JOIN stats ON status.stats_id=stats._id "
- + "LEFT JOIN settings as s1 ON 'sync_provider_' || authority=s1.name "
- + "LEFT JOIN settings as s2 ON s2.name='listen_for_tickles' "
- + "where initialFailureTime is not null "
- + " AND lastFailureMesg!=" + Sync.History.ERROR_TOO_MANY_DELETIONS
- + " AND lastFailureMesg!=" + Sync.History.ERROR_AUTHENTICATION
- + " AND lastFailureMesg!=" + Sync.History.ERROR_SYNC_ALREADY_IN_PROGRESS
- + " AND authority!='subscribedfeeds' "
- + " ORDER BY initialFailureTime", null);
+ synchronized (mAuthorities) {
+ if (!mListenForTickles) {
+ return 0;
+ }
+
+ long oldest = 0;
+ int i = mSyncStatus.size();
+ while (i > 0) {
+ i--;
+ SyncStatusInfo stats = mSyncStatus.valueAt(i);
+ AuthorityInfo authority = mAuthorities.get(stats.authorityId);
+ if (authority != null && authority.enabled) {
+ if (oldest == 0 || stats.initialFailureTime < oldest) {
+ oldest = stats.initialFailureTime;
+ }
+ }
+ }
+
+ return oldest;
+ }
+ }
+
+ private int getCurrentDay() {
+ mCal.setTimeInMillis(System.currentTimeMillis());
+ final int dayOfYear = mCal.get(Calendar.DAY_OF_YEAR);
+ if (mYear != mCal.get(Calendar.YEAR)) {
+ mYear = mCal.get(Calendar.YEAR);
+ mCal.clear();
+ mCal.set(Calendar.YEAR, mYear);
+ mYearInDays = (int)(mCal.getTimeInMillis()/86400000);
+ }
+ return dayOfYear + mYearInDays;
+ }
+
+ /**
+ * Retrieve an authority, returning null if one does not exist.
+ *
+ * @param accountName The name of the account for the authority.
+ * @param authorityName The name of the authority itself.
+ * @param tag If non-null, this will be used in a log message if the
+ * requested authority does not exist.
+ */
+ private AuthorityInfo getAuthorityLocked(Account accountName, String authorityName,
+ String tag) {
+ AccountInfo account = mAccounts.get(accountName);
+ if (account == null) {
+ if (tag != null) {
+ Log.w(TAG, tag + ": unknown account " + accountName);
+ }
+ return null;
+ }
+ AuthorityInfo authority = account.authorities.get(authorityName);
+ if (authority == null) {
+ if (tag != null) {
+ Log.w(TAG, tag + ": unknown authority " + authorityName);
+ }
+ return null;
+ }
+
+ return authority;
+ }
+
+ private AuthorityInfo getOrCreateAuthorityLocked(Account accountName,
+ String authorityName, int ident, boolean doWrite) {
+ AccountInfo account = mAccounts.get(accountName);
+ if (account == null) {
+ account = new AccountInfo(accountName);
+ mAccounts.put(accountName, account);
+ }
+ AuthorityInfo authority = account.authorities.get(authorityName);
+ if (authority == null) {
+ if (ident < 0) {
+ // Look for a new identifier for this authority.
+ final int N = mAuthorities.size();
+ ident = 0;
+ for (int i=0; i<N; i++) {
+ if (mAuthorities.valueAt(i).ident > ident) {
+ break;
+ }
+ ident++;
+ }
+ }
+ authority = new AuthorityInfo(accountName, authorityName, ident);
+ account.authorities.put(authorityName, authority);
+ mAuthorities.put(ident, authority);
+ if (doWrite) {
+ writeAccountInfoLocked();
+ }
+ }
+
+ return authority;
+ }
+
+ private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) {
+ SyncStatusInfo status = mSyncStatus.get(authorityId);
+ if (status == null) {
+ status = new SyncStatusInfo(authorityId);
+ mSyncStatus.put(authorityId, status);
+ }
+ return status;
+ }
+
+ /**
+ * Read all account information back in to the initial engine state.
+ */
+ private void readAccountInfoLocked() {
+ FileInputStream fis = null;
try {
+ fis = mAccountInfoFile.openRead();
+ if (DEBUG_FILE) Log.v(TAG, "Reading " + mAccountInfoFile.getBaseFile());
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(fis, null);
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.START_TAG) {
+ eventType = parser.next();
+ }
+ String tagName = parser.getName();
+ if ("accounts".equals(tagName)) {
+ String listen = parser.getAttributeValue(
+ null, "listen-for-tickles");
+ mListenForTickles = listen == null
+ || Boolean.parseBoolean(listen);
+ eventType = parser.next();
+ do {
+ if (eventType == XmlPullParser.START_TAG
+ && parser.getDepth() == 2) {
+ tagName = parser.getName();
+ if ("authority".equals(tagName)) {
+ int id = -1;
+ try {
+ id = Integer.parseInt(parser.getAttributeValue(
+ null, "id"));
+ } catch (NumberFormatException e) {
+ } catch (NullPointerException e) {
+ }
+ if (id >= 0) {
+ String accountName = parser.getAttributeValue(
+ null, "account");
+ String accountType = parser.getAttributeValue(
+ null, "type");
+ if (accountType == null) {
+ accountType = "com.google.GAIA";
+ }
+ String authorityName = parser.getAttributeValue(
+ null, "authority");
+ String enabled = parser.getAttributeValue(
+ null, "enabled");
+ AuthorityInfo authority = mAuthorities.get(id);
+ if (DEBUG_FILE) Log.v(TAG, "Adding authority: account="
+ + accountName + " auth=" + authorityName
+ + " enabled=" + enabled);
+ if (authority == null) {
+ if (DEBUG_FILE) Log.v(TAG, "Creating entry");
+ authority = getOrCreateAuthorityLocked(
+ new Account(accountName, accountType),
+ authorityName, id, false);
+ }
+ if (authority != null) {
+ authority.enabled = enabled == null
+ || Boolean.parseBoolean(enabled);
+ } else {
+ Log.w(TAG, "Failure adding authority: account="
+ + accountName + " auth=" + authorityName
+ + " enabled=" + enabled);
+ }
+ }
+ }
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+ }
+ } catch (XmlPullParserException e) {
+ Log.w(TAG, "Error reading accounts", e);
+ } catch (java.io.IOException e) {
+ if (fis == null) Log.i(TAG, "No initial accounts");
+ else Log.w(TAG, "Error reading accounts", e);
+ } finally {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (java.io.IOException e1) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Write all account information to the account file.
+ */
+ private void writeAccountInfoLocked() {
+ if (DEBUG_FILE) Log.v(TAG, "Writing new " + mAccountInfoFile.getBaseFile());
+ FileOutputStream fos = null;
+
+ try {
+ fos = mAccountInfoFile.startWrite();
+ XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(fos, "utf-8");
+ out.startDocument(null, true);
+ out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+
+ out.startTag(null, "accounts");
+ if (!mListenForTickles) {
+ out.attribute(null, "listen-for-tickles", "false");
+ }
+
+ final int N = mAuthorities.size();
+ for (int i=0; i<N; i++) {
+ AuthorityInfo authority = mAuthorities.get(i);
+ out.startTag(null, "authority");
+ out.attribute(null, "id", Integer.toString(authority.ident));
+ out.attribute(null, "account", authority.account.mName);
+ out.attribute(null, "type", authority.account.mType);
+ out.attribute(null, "authority", authority.authority);
+ if (!authority.enabled) {
+ out.attribute(null, "enabled", "false");
+ }
+ out.endTag(null, "authority");
+ }
+
+ out.endTag(null, "accounts");
+
+ out.endDocument();
+
+ mAccountInfoFile.finishWrite(fos);
+ } catch (java.io.IOException e1) {
+ Log.w(TAG, "Error writing accounts", e1);
+ if (fos != null) {
+ mAccountInfoFile.failWrite(fos);
+ }
+ }
+ }
+
+ static int getIntColumn(Cursor c, String name) {
+ return c.getInt(c.getColumnIndex(name));
+ }
+
+ static long getLongColumn(Cursor c, String name) {
+ return c.getLong(c.getColumnIndex(name));
+ }
+
+ /**
+ * Load sync engine state from the old syncmanager database, and then
+ * erase it. Note that we don't deal with pending operations, active
+ * sync, or history.
+ */
+ private void readLegacyAccountInfoLocked() {
+ // Look for old database to initialize from.
+ File file = mContext.getDatabasePath("syncmanager.db");
+ if (!file.exists()) {
+ return;
+ }
+ String path = file.getPath();
+ SQLiteDatabase db = null;
+ try {
+ db = SQLiteDatabase.openDatabase(path, null,
+ SQLiteDatabase.OPEN_READONLY);
+ } catch (SQLiteException e) {
+ }
+
+ if (db != null) {
+ // Copy in all of the status information, as well as accounts.
+ if (DEBUG_FILE) Log.v(TAG, "Reading legacy sync accounts db");
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables("stats, status");
+ HashMap<String,String> map = new HashMap<String,String>();
+ map.put("_id", "status._id as _id");
+ map.put("account", "stats.account as account");
+ map.put("type", "stats.type as type");
+ map.put("authority", "stats.authority as authority");
+ map.put("totalElapsedTime", "totalElapsedTime");
+ map.put("numSyncs", "numSyncs");
+ map.put("numSourceLocal", "numSourceLocal");
+ map.put("numSourcePoll", "numSourcePoll");
+ map.put("numSourceServer", "numSourceServer");
+ map.put("numSourceUser", "numSourceUser");
+ map.put("lastSuccessSource", "lastSuccessSource");
+ map.put("lastSuccessTime", "lastSuccessTime");
+ map.put("lastFailureSource", "lastFailureSource");
+ map.put("lastFailureTime", "lastFailureTime");
+ map.put("lastFailureMesg", "lastFailureMesg");
+ map.put("pending", "pending");
+ qb.setProjectionMap(map);
+ qb.appendWhere("stats._id = status.stats_id");
+ Cursor c = qb.query(db, null, null, null, null, null, null);
while (c.moveToNext()) {
- // these settings default to true, so if they are null treat them as enabled
- final String providerEnabledString = c.getString(1);
- if (providerEnabledString != null && !Boolean.parseBoolean(providerEnabledString)) {
- continue;
+ String accountName = c.getString(c.getColumnIndex("account"));
+ String accountType = c.getString(c.getColumnIndex("type"));
+ if (accountType == null) {
+ accountType = "com.google.GAIA";
}
- final String allEnabledString = c.getString(2);
- if (allEnabledString != null && !Boolean.parseBoolean(allEnabledString)) {
- continue;
+ String authorityName = c.getString(c.getColumnIndex("authority"));
+ AuthorityInfo authority = this.getOrCreateAuthorityLocked(
+ new Account(accountName, accountType),
+ authorityName, -1, false);
+ if (authority != null) {
+ int i = mSyncStatus.size();
+ boolean found = false;
+ SyncStatusInfo st = null;
+ while (i > 0) {
+ i--;
+ st = mSyncStatus.get(i);
+ if (st.authorityId == authority.ident) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ st = new SyncStatusInfo(authority.ident);
+ mSyncStatus.put(authority.ident, st);
+ }
+ st.totalElapsedTime = getLongColumn(c, "totalElapsedTime");
+ st.numSyncs = getIntColumn(c, "numSyncs");
+ st.numSourceLocal = getIntColumn(c, "numSourceLocal");
+ st.numSourcePoll = getIntColumn(c, "numSourcePoll");
+ st.numSourceServer = getIntColumn(c, "numSourceServer");
+ st.numSourceUser = getIntColumn(c, "numSourceUser");
+ st.lastSuccessSource = getIntColumn(c, "lastSuccessSource");
+ st.lastSuccessTime = getLongColumn(c, "lastSuccessTime");
+ st.lastFailureSource = getIntColumn(c, "lastFailureSource");
+ st.lastFailureTime = getLongColumn(c, "lastFailureTime");
+ st.lastFailureMesg = c.getString(c.getColumnIndex("lastFailureMesg"));
+ st.pending = getIntColumn(c, "pending") != 0;
}
- return c.getLong(0);
}
- } finally {
+
c.close();
- }
- return 0;
- }
-
- private void createStatusRowIfNecessary(long statsId) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- boolean statusExists = 0 != DatabaseUtils.longForQuery(db,
- "SELECT count(*) FROM status WHERE stats_id=" + statsId, null);
- if (!statusExists) {
- ContentValues values = new ContentValues();
- values.put("stats_id", statsId);
- db.insert("status", null, values);
- }
- }
-
- private long createStatsRowIfNecessary(Account account, String authority) {
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- StringBuilder where = new StringBuilder();
- where.append(Sync.Stats.ACCOUNT + "= ?");
- where.append(" and " + Sync.Stats.ACCOUNT_TYPE + "= ?");
- where.append(" and " + Sync.Stats.AUTHORITY + "= ?");
- Cursor cursor = query(Sync.Stats.CONTENT_URI,
- Sync.Stats.SYNC_STATS_PROJECTION,
- where.toString(), new String[] { account.mName, account.mType, authority },
- null /* order */);
- try {
- long id;
- if (cursor.moveToFirst()) {
- id = cursor.getLong(cursor.getColumnIndexOrThrow(Sync.Stats._ID));
- } else {
- ContentValues values = new ContentValues();
- values.put(Sync.Stats.ACCOUNT, account.mName);
- values.put(Sync.Stats.ACCOUNT_TYPE, account.mType);
- values.put(Sync.Stats.AUTHORITY, authority);
- id = db.insert("stats", null, values);
+
+ // Retrieve the settings.
+ qb = new SQLiteQueryBuilder();
+ qb.setTables("settings");
+ c = qb.query(db, null, null, null, null, null, null);
+ while (c.moveToNext()) {
+ String name = c.getString(c.getColumnIndex("name"));
+ String value = c.getString(c.getColumnIndex("value"));
+ if (name == null) continue;
+ if (name.equals("listen_for_tickles")) {
+ setListenForNetworkTickles(value == null
+ || Boolean.parseBoolean(value));
+ } else if (name.startsWith("sync_provider_")) {
+ String provider = name.substring("sync_provider_".length(),
+ name.length());
+ setSyncProviderAutomatically(null, provider,
+ value == null || Boolean.parseBoolean(value));
+ }
}
- return id;
+
+ c.close();
+
+ db.close();
+
+ writeAccountInfoLocked();
+ writeStatusLocked();
+ (new File(path)).delete();
+ }
+ }
+
+ public static final int STATUS_FILE_END = 0;
+ public static final int STATUS_FILE_ITEM = 100;
+
+ /**
+ * Read all sync status back in to the initial engine state.
+ */
+ private void readStatusLocked() {
+ if (DEBUG_FILE) Log.v(TAG, "Reading " + mStatusFile.getBaseFile());
+ try {
+ byte[] data = mStatusFile.readFully();
+ Parcel in = Parcel.obtain();
+ in.unmarshall(data, 0, data.length);
+ in.setDataPosition(0);
+ int token;
+ while ((token=in.readInt()) != STATUS_FILE_END) {
+ if (token == STATUS_FILE_ITEM) {
+ SyncStatusInfo status = new SyncStatusInfo(in);
+ if (mAuthorities.indexOfKey(status.authorityId) >= 0) {
+ status.pending = false;
+ if (DEBUG_FILE) Log.v(TAG, "Adding status for id "
+ + status.authorityId);
+ mSyncStatus.put(status.authorityId, status);
+ }
+ } else {
+ // Ooops.
+ Log.w(TAG, "Unknown status token: " + token);
+ break;
+ }
+ }
+ } catch (java.io.IOException e) {
+ Log.i(TAG, "No initial status");
+ }
+ }
+
+ /**
+ * Write all sync status to the sync status file.
+ */
+ private void writeStatusLocked() {
+ if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatusFile.getBaseFile());
+
+ // The file is being written, so we don't need to have a scheduled
+ // write until the next change.
+ removeMessages(MSG_WRITE_STATUS);
+
+ FileOutputStream fos = null;
+ try {
+ fos = mStatusFile.startWrite();
+ Parcel out = Parcel.obtain();
+ final int N = mSyncStatus.size();
+ for (int i=0; i<N; i++) {
+ SyncStatusInfo status = mSyncStatus.valueAt(i);
+ out.writeInt(STATUS_FILE_ITEM);
+ status.writeToParcel(out, 0);
+ }
+ out.writeInt(STATUS_FILE_END);
+ fos.write(out.marshall());
+ out.recycle();
+
+ mStatusFile.finishWrite(fos);
+ } catch (java.io.IOException e1) {
+ Log.w(TAG, "Error writing status", e1);
+ if (fos != null) {
+ mStatusFile.failWrite(fos);
+ }
+ }
+ }
+
+ public static final int PENDING_OPERATION_VERSION = 1;
+
+ /**
+ * Read all pending operations back in to the initial engine state.
+ */
+ private void readPendingOperationsLocked() {
+ if (DEBUG_FILE) Log.v(TAG, "Reading " + mPendingFile.getBaseFile());
+ try {
+ byte[] data = mPendingFile.readFully();
+ Parcel in = Parcel.obtain();
+ in.unmarshall(data, 0, data.length);
+ in.setDataPosition(0);
+ final int SIZE = in.dataSize();
+ while (in.dataPosition() < SIZE) {
+ int version = in.readInt();
+ if (version != PENDING_OPERATION_VERSION) {
+ Log.w(TAG, "Unknown pending operation version "
+ + version + "; dropping all ops");
+ break;
+ }
+ int authorityId = in.readInt();
+ int syncSource = in.readInt();
+ byte[] flatExtras = in.createByteArray();
+ AuthorityInfo authority = mAuthorities.get(authorityId);
+ if (authority != null) {
+ Bundle extras = null;
+ if (flatExtras != null) {
+ extras = unflattenBundle(flatExtras);
+ }
+ PendingOperation op = new PendingOperation(
+ authority.account, syncSource,
+ authority.authority, extras);
+ op.authorityId = authorityId;
+ op.flatExtras = flatExtras;
+ if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account
+ + " auth=" + op.authority
+ + " src=" + op.syncSource
+ + " extras=" + op.extras);
+ mPendingOperations.add(op);
+ }
+ }
+ } catch (java.io.IOException e) {
+ Log.i(TAG, "No initial pending operations");
+ }
+ }
+
+ private void writePendingOperationLocked(PendingOperation op, Parcel out) {
+ out.writeInt(PENDING_OPERATION_VERSION);
+ out.writeInt(op.authorityId);
+ out.writeInt(op.syncSource);
+ if (op.flatExtras == null && op.extras != null) {
+ op.flatExtras = flattenBundle(op.extras);
+ }
+ out.writeByteArray(op.flatExtras);
+ }
+
+ /**
+ * Write all currently pending ops to the pending ops file.
+ */
+ private void writePendingOperationsLocked() {
+ final int N = mPendingOperations.size();
+ FileOutputStream fos = null;
+ try {
+ if (N == 0) {
+ if (DEBUG_FILE) Log.v(TAG, "Truncating " + mPendingFile.getBaseFile());
+ mPendingFile.truncate();
+ return;
+ }
+
+ if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile());
+ fos = mPendingFile.startWrite();
+
+ Parcel out = Parcel.obtain();
+ for (int i=0; i<N; i++) {
+ PendingOperation op = mPendingOperations.get(i);
+ writePendingOperationLocked(op, out);
+ }
+ fos.write(out.marshall());
+ out.recycle();
+
+ mPendingFile.finishWrite(fos);
+ } catch (java.io.IOException e1) {
+ Log.w(TAG, "Error writing pending operations", e1);
+ if (fos != null) {
+ mPendingFile.failWrite(fos);
+ }
+ }
+ }
+
+ /**
+ * Append the given operation to the pending ops file; if unable to,
+ * write all pending ops.
+ */
+ private void appendPendingOperationLocked(PendingOperation op) {
+ if (DEBUG_FILE) Log.v(TAG, "Appending to " + mPendingFile.getBaseFile());
+ FileOutputStream fos = null;
+ try {
+ fos = mPendingFile.openAppend();
+ } catch (java.io.IOException e) {
+ if (DEBUG_FILE) Log.v(TAG, "Failed append; writing full file");
+ writePendingOperationsLocked();
+ return;
+ }
+
+ try {
+ Parcel out = Parcel.obtain();
+ writePendingOperationLocked(op, out);
+ fos.write(out.marshall());
+ out.recycle();
+ } catch (java.io.IOException e1) {
+ Log.w(TAG, "Error writing pending operations", e1);
} finally {
- cursor.close();
+ try {
+ fos.close();
+ } catch (java.io.IOException e2) {
+ }
+ }
+ }
+
+ static private byte[] flattenBundle(Bundle bundle) {
+ byte[] flatData = null;
+ Parcel parcel = Parcel.obtain();
+ try {
+ bundle.writeToParcel(parcel, 0);
+ flatData = parcel.marshall();
+ } finally {
+ parcel.recycle();
+ }
+ return flatData;
+ }
+
+ static private Bundle unflattenBundle(byte[] flatData) {
+ Bundle bundle;
+ Parcel parcel = Parcel.obtain();
+ try {
+ parcel.unmarshall(flatData, 0, flatData.length);
+ parcel.setDataPosition(0);
+ bundle = parcel.readBundle();
+ } catch (RuntimeException e) {
+ // A RuntimeException is thrown if we were unable to parse the parcel.
+ // Create an empty parcel in this case.
+ bundle = new Bundle();
+ } finally {
+ parcel.recycle();
+ }
+ return bundle;
+ }
+
+ public static final int STATISTICS_FILE_END = 0;
+ public static final int STATISTICS_FILE_ITEM_OLD = 100;
+ public static final int STATISTICS_FILE_ITEM = 101;
+
+ /**
+ * Read all sync statistics back in to the initial engine state.
+ */
+ private void readStatisticsLocked() {
+ try {
+ byte[] data = mStatisticsFile.readFully();
+ Parcel in = Parcel.obtain();
+ in.unmarshall(data, 0, data.length);
+ in.setDataPosition(0);
+ int token;
+ int index = 0;
+ while ((token=in.readInt()) != STATISTICS_FILE_END) {
+ if (token == STATISTICS_FILE_ITEM
+ || token == STATISTICS_FILE_ITEM_OLD) {
+ int day = in.readInt();
+ if (token == STATISTICS_FILE_ITEM_OLD) {
+ day = day - 2009 + 14245; // Magic!
+ }
+ DayStats ds = new DayStats(day);
+ ds.successCount = in.readInt();
+ ds.successTime = in.readLong();
+ ds.failureCount = in.readInt();
+ ds.failureTime = in.readLong();
+ if (index < mDayStats.length) {
+ mDayStats[index] = ds;
+ index++;
+ }
+ } else {
+ // Ooops.
+ Log.w(TAG, "Unknown stats token: " + token);
+ break;
+ }
+ }
+ } catch (java.io.IOException e) {
+ Log.i(TAG, "No initial statistics");
+ }
+ }
+
+ /**
+ * Write all sync statistics to the sync status file.
+ */
+ private void writeStatisticsLocked() {
+ if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatisticsFile.getBaseFile());
+
+ // The file is being written, so we don't need to have a scheduled
+ // write until the next change.
+ removeMessages(MSG_WRITE_STATISTICS);
+
+ FileOutputStream fos = null;
+ try {
+ fos = mStatisticsFile.startWrite();
+ Parcel out = Parcel.obtain();
+ final int N = mDayStats.length;
+ for (int i=0; i<N; i++) {
+ DayStats ds = mDayStats[i];
+ if (ds == null) {
+ break;
+ }
+ out.writeInt(STATISTICS_FILE_ITEM);
+ out.writeInt(ds.day);
+ out.writeInt(ds.successCount);
+ out.writeLong(ds.successTime);
+ out.writeInt(ds.failureCount);
+ out.writeLong(ds.failureTime);
+ }
+ out.writeInt(STATISTICS_FILE_END);
+ fos.write(out.marshall());
+ out.recycle();
+
+ mStatisticsFile.finishWrite(fos);
+ } catch (java.io.IOException e1) {
+ Log.w(TAG, "Error writing stats", e1);
+ if (fos != null) {
+ mStatisticsFile.failWrite(fos);
+ }
}
}
}