blob: 4c532018f3730128b3edb55379e07b30e683a536 [file] [log] [blame]
/*
* 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 com.android.internal.os.AtomicFile;
import com.android.internal.util.ArrayUtils;
import com.android.common.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.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteQueryBuilder;
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.os.SystemProperties;
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.Iterator;
import java.util.TimeZone;
/**
* Singleton that tracks the sync data and overall sync
* history on the device.
*
* @hide
*/
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;
/** Enum value for a sync start event. */
public static final int EVENT_START = 0;
/** Enum value for a sync stop event. */
public static final int EVENT_STOP = 1;
// TODO: i18n -- grab these out of resources.
/** String names for the sync event types. */
public static final String[] EVENTS = { "START", "STOP" };
/** Enum value for a server-initiated sync. */
public static final int SOURCE_SERVER = 0;
/** 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;
/** Enum value for a user-initiated sync. */
public static final int SOURCE_USER = 3;
private static final Intent SYNC_CONNECTION_SETTING_CHANGED_INTENT =
new Intent("com.android.sync.SYNC_CONN_STATUS_CHANGED");
// TODO: i18n -- grab these out of resources.
/** String names for the sync source types. */
public static final String[] SOURCES = { "SERVER",
"LOCAL",
"POLL",
"USER" };
// 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";
public static final int MAX_HISTORY = 100;
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
private static final boolean SYNC_ENABLED_DEFAULT = false;
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;
int syncable;
AuthorityInfo(Account account, String authority, int ident) {
this.account = account;
this.authority = authority;
this.ident = ident;
enabled = SYNC_ENABLED_DEFAULT;
syncable = -1; // default to "unknown"
}
}
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 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 mMasterSyncAutomatically = true;
private SyncStorageEngine(Context context) {
mContext = context;
sSyncStorageEngine = this;
mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
// This call will return the correct directory whether Encrypted File Systems is
// enabled or not.
File dataDir = Environment.getSecureDataDirectory();
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) {
return new SyncStorageEngine(context);
}
public static void init(Context context) {
if (sSyncStorageEngine != null) {
throw new IllegalStateException("already initialized");
}
sSyncStorageEngine = new SyncStorageEngine(context);
}
public static SyncStorageEngine getSingleton() {
if (sSyncStorageEngine == null) {
throw new IllegalStateException("not initialized");
}
return sSyncStorageEngine;
}
@Override public void handleMessage(Message msg) {
if (msg.what == MSG_WRITE_STATUS) {
synchronized (mAccounts) {
writeStatusLocked();
}
} 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));
}
mChangeListeners.finishBroadcast();
}
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 getSyncAutomatically(Account account, String providerName) {
synchronized (mAuthorities) {
if (account != null) {
AuthorityInfo authority = getAuthorityLocked(account, providerName,
"getSyncAutomatically");
return authority != null && authority.enabled;
}
int i = mAuthorities.size();
while (i > 0) {
i--;
AuthorityInfo authority = mAuthorities.valueAt(i);
if (authority.authority.equals(providerName)
&& authority.enabled) {
return true;
}
}
return false;
}
}
public void setSyncAutomatically(Account account, String providerName, boolean sync) {
boolean wasEnabled;
synchronized (mAuthorities) {
AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false);
wasEnabled = authority.enabled;
authority.enabled = sync;
writeAccountInfoLocked();
}
if (!wasEnabled && sync) {
mContext.getContentResolver().requestSync(account, providerName, new Bundle());
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
}
public int getIsSyncable(Account account, String providerName) {
synchronized (mAuthorities) {
if (account != null) {
AuthorityInfo authority = getAuthorityLocked(account, providerName,
"getIsSyncable");
if (authority == null) {
return -1;
}
return authority.syncable;
}
int i = mAuthorities.size();
while (i > 0) {
i--;
AuthorityInfo authority = mAuthorities.valueAt(i);
if (authority.authority.equals(providerName)) {
return authority.syncable;
}
}
return -1;
}
}
public void setIsSyncable(Account account, String providerName, int syncable) {
int oldState;
if (syncable > 1) {
syncable = 1;
} else if (syncable < -1) {
syncable = -1;
}
Log.d(TAG, "setIsSyncable: " + account + ", provider " + providerName + " -> " + syncable);
synchronized (mAuthorities) {
AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false);
oldState = authority.syncable;
authority.syncable = syncable;
writeAccountInfoLocked();
}
if (oldState <= 0 && syncable > 0) {
mContext.getContentResolver().requestSync(account, providerName, new Bundle());
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
}
public void setMasterSyncAutomatically(boolean flag) {
boolean old;
synchronized (mAuthorities) {
old = mMasterSyncAutomatically;
mMasterSyncAutomatically = flag;
writeAccountInfoLocked();
}
if (!old && flag) {
mContext.getContentResolver().requestSync(null, null, new Bundle());
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
mContext.sendBroadcast(SYNC_CONNECTION_SETTING_CHANGED_INTENT);
}
public boolean getMasterSyncAutomatically() {
synchronized (mAuthorities) {
return mMasterSyncAutomatically;
}
}
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;
status.initialize = op.extras != null &&
op.extras.containsKey(ContentResolver.SYNC_EXTRAS_INITIALIZE) &&
op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE);
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_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(ContentResolver.SYNC_OBSERVER_TYPE_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.valueAt(i).pending = false;
}
writePendingOperationsLocked();
}
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_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(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE);
}
/**
* To allow others to send active change reports, to poke clients.
*/
public void reportActiveChange() {
reportChange(ContentResolver.SYNC_OBSERVER_TYPE_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(ContentResolver.SYNC_OBSERVER_TYPE_STATUS);
return id;
}
public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage,
long downstreamActivity, long upstreamActivity) {
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;
}
item = null;
}
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 = getCurrentDayLocked();
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(ContentResolver.SYNC_OBSERVER_TYPE_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 and account.
*
* @param account the account we want to check
* @param authority the authority whose row should be selected
* @return the SyncStatusInfo for the authority, or null if none exists
*/
public SyncStatusInfo getStatusByAccountAndAuthority(Account account, String authority) {
if (account == null || authority == null) {
throw new IllegalArgumentException();
}
synchronized (mAuthorities) {
final int N = mSyncStatus.size();
for (int i=0; i<N; i++) {
SyncStatusInfo cur = mSyncStatus.valueAt(i);
AuthorityInfo ainfo = mAuthorities.get(cur.authorityId);
if (ainfo != null && ainfo.authority.equals(authority) &&
account.equals(ainfo.account)) {
return cur;
}
}
return null;
}
}
/**
* Return true if the pending status is true of any matching authorities.
*/
public boolean isSyncPending(Account account, String authority) {
synchronized (mAuthorities) {
final int N = mSyncStatus.size();
for (int i=0; i<N; i++) {
SyncStatusInfo cur = mSyncStatus.valueAt(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() {
synchronized (mAuthorities) {
if (!mMasterSyncAutomatically) {
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 getCurrentDayLocked() {
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) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, tag + ": unknown account " + accountName);
}
}
return null;
}
AuthorityInfo authority = account.authorities.get(authorityName);
if (authority == null) {
if (tag != null) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(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++;
}
}
if (DEBUG) Log.v(TAG, "created a new AuthorityInfo for " + accountName
+ ", provider " + authorityName);
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;
}
public void writeAllState() {
synchronized (mAuthorities) {
// Account info is always written so no need to do it here.
if (mNumPendingFinished > 0) {
// Only write these if they are out of date.
writePendingOperationsLocked();
}
// Just always write these... they are likely out of date.
writeStatusLocked();
writeStatisticsLocked();
}
}
/**
* 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");
mMasterSyncAutomatically = 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";
}
String authorityName = parser.getAttributeValue(
null, "authority");
String enabled = parser.getAttributeValue(
null, "enabled");
String syncable = parser.getAttributeValue(null, "syncable");
AuthorityInfo authority = mAuthorities.get(id);
if (DEBUG_FILE) Log.v(TAG, "Adding authority: account="
+ accountName + " auth=" + authorityName
+ " enabled=" + enabled
+ " syncable=" + syncable);
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);
if ("unknown".equals(syncable)) {
authority.syncable = -1;
} else {
authority.syncable =
(syncable == null || Boolean.parseBoolean(enabled))
? 1
: 0;
}
} else {
Log.w(TAG, "Failure adding authority: account="
+ accountName + " auth=" + authorityName
+ " enabled=" + enabled
+ " syncable=" + syncable);
}
}
}
}
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 (!mMasterSyncAutomatically) {
out.attribute(null, "listen-for-tickles", "false");
}
final int N = mAuthorities.size();
for (int i=0; i<N; i++) {
AuthorityInfo authority = mAuthorities.valueAt(i);
out.startTag(null, "authority");
out.attribute(null, "id", Integer.toString(authority.ident));
out.attribute(null, "account", authority.account.name);
out.attribute(null, "type", authority.account.type);
out.attribute(null, "authority", authority.authority);
if (!authority.enabled) {
out.attribute(null, "enabled", "false");
}
if (authority.syncable < 0) {
out.attribute(null, "syncable", "unknown");
} else if (authority.syncable == 0) {
out.attribute(null, "syncable", "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) {
final boolean hasType = db.getVersion() >= 11;
// 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");
if (hasType) {
map.put("account_type", "stats.account_type as account_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()) {
String accountName = c.getString(c.getColumnIndex("account"));
String accountType = hasType
? c.getString(c.getColumnIndex("account_type")) : null;
if (accountType == null) {
accountType = "com.google";
}
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.valueAt(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;
}
}
c.close();
// 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")) {
setMasterSyncAutomatically(value == null || Boolean.parseBoolean(value));
} else if (name.startsWith("sync_provider_")) {
String provider = name.substring("sync_provider_".length(),
name.length());
int i = mAuthorities.size();
while (i > 0) {
i--;
AuthorityInfo authority = mAuthorities.valueAt(i);
if (authority.authority.equals(provider)) {
authority.enabled = value == null || Boolean.parseBoolean(value);
authority.syncable = 1;
}
}
}
}
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 {
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);
}
}
}
}