Move lingering services to services.jar.

This helps reduce the pressure on framework.jar, and makes it clear
that it should only be used by the system_server.

Bug: 7333397
Change-Id: I0858904239535380fbf30562b793e277d8c3f054
diff --git a/services/java/com/android/server/LockSettingsService.java b/services/java/com/android/server/LockSettingsService.java
new file mode 100644
index 0000000..e20a21f
--- /dev/null
+++ b/services/java/com/android/server/LockSettingsService.java
@@ -0,0 +1,408 @@
+/*
+ * Copyright (C) 2012 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 com.android.server;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.provider.Settings.Secure;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.widget.ILockSettings;
+import com.android.internal.widget.LockPatternUtils;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.Arrays;
+
+/**
+ * Keeps the lock pattern/password data and related settings for each user.
+ * Used by LockPatternUtils. Needs to be a service because Settings app also needs
+ * to be able to save lockscreen information for secondary users.
+ * @hide
+ */
+public class LockSettingsService extends ILockSettings.Stub {
+
+    private final DatabaseHelper mOpenHelper;
+    private static final String TAG = "LockSettingsService";
+
+    private static final String TABLE = "locksettings";
+    private static final String COLUMN_KEY = "name";
+    private static final String COLUMN_USERID = "user";
+    private static final String COLUMN_VALUE = "value";
+
+    private static final String[] COLUMNS_FOR_QUERY = {
+        COLUMN_VALUE
+    };
+
+    private static final String SYSTEM_DIRECTORY = "/system/";
+    private static final String LOCK_PATTERN_FILE = "gesture.key";
+    private static final String LOCK_PASSWORD_FILE = "password.key";
+
+    private final Context mContext;
+
+    public LockSettingsService(Context context) {
+        mContext = context;
+        // Open the database
+        mOpenHelper = new DatabaseHelper(mContext);
+    }
+
+    public void systemReady() {
+        migrateOldData();
+    }
+
+    private void migrateOldData() {
+        try {
+            if (getString("migrated", null, 0) != null) {
+                // Already migrated
+                return;
+            }
+
+            final ContentResolver cr = mContext.getContentResolver();
+            for (String validSetting : VALID_SETTINGS) {
+                String value = Settings.Secure.getString(cr, validSetting);
+                if (value != null) {
+                    setString(validSetting, value, 0);
+                }
+            }
+            // No need to move the password / pattern files. They're already in the right place.
+            setString("migrated", "true", 0);
+            Slog.i(TAG, "Migrated lock settings to new location");
+        } catch (RemoteException re) {
+            Slog.e(TAG, "Unable to migrate old data");
+        }
+    }
+
+    private static final void checkWritePermission(int userId) {
+        final int callingUid = Binder.getCallingUid();
+        if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID) {
+            throw new SecurityException("uid=" + callingUid
+                    + " not authorized to write lock settings");
+        }
+    }
+
+    private static final void checkPasswordReadPermission(int userId) {
+        final int callingUid = Binder.getCallingUid();
+        if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID) {
+            throw new SecurityException("uid=" + callingUid
+                    + " not authorized to read lock password");
+        }
+    }
+
+    private static final void checkReadPermission(int userId) {
+        final int callingUid = Binder.getCallingUid();
+        if (UserHandle.getAppId(callingUid) != android.os.Process.SYSTEM_UID
+                && UserHandle.getUserId(callingUid) != userId) {
+            throw new SecurityException("uid=" + callingUid
+                    + " not authorized to read settings of user " + userId);
+        }
+    }
+
+    @Override
+    public void setBoolean(String key, boolean value, int userId) throws RemoteException {
+        checkWritePermission(userId);
+
+        writeToDb(key, value ? "1" : "0", userId);
+    }
+
+    @Override
+    public void setLong(String key, long value, int userId) throws RemoteException {
+        checkWritePermission(userId);
+
+        writeToDb(key, Long.toString(value), userId);
+    }
+
+    @Override
+    public void setString(String key, String value, int userId) throws RemoteException {
+        checkWritePermission(userId);
+
+        writeToDb(key, value, userId);
+    }
+
+    @Override
+    public boolean getBoolean(String key, boolean defaultValue, int userId) throws RemoteException {
+        //checkReadPermission(userId);
+
+        String value = readFromDb(key, null, userId);
+        return TextUtils.isEmpty(value) ?
+                defaultValue : (value.equals("1") || value.equals("true"));
+    }
+
+    @Override
+    public long getLong(String key, long defaultValue, int userId) throws RemoteException {
+        //checkReadPermission(userId);
+
+        String value = readFromDb(key, null, userId);
+        return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value);
+    }
+
+    @Override
+    public String getString(String key, String defaultValue, int userId) throws RemoteException {
+        //checkReadPermission(userId);
+
+        return readFromDb(key, defaultValue, userId);
+    }
+
+    private String getLockPatternFilename(int userId) {
+        String dataSystemDirectory =
+                android.os.Environment.getDataDirectory().getAbsolutePath() +
+                SYSTEM_DIRECTORY;
+        if (userId == 0) {
+            // Leave it in the same place for user 0
+            return dataSystemDirectory + LOCK_PATTERN_FILE;
+        } else {
+            return  new File(Environment.getUserSystemDirectory(userId), LOCK_PATTERN_FILE)
+                    .getAbsolutePath();
+        }
+    }
+
+    private String getLockPasswordFilename(int userId) {
+        String dataSystemDirectory =
+                android.os.Environment.getDataDirectory().getAbsolutePath() +
+                SYSTEM_DIRECTORY;
+        if (userId == 0) {
+            // Leave it in the same place for user 0
+            return dataSystemDirectory + LOCK_PASSWORD_FILE;
+        } else {
+            return  new File(Environment.getUserSystemDirectory(userId), LOCK_PASSWORD_FILE)
+                    .getAbsolutePath();
+        }
+    }
+
+    @Override
+    public boolean havePassword(int userId) throws RemoteException {
+        // Do we need a permissions check here?
+
+        return new File(getLockPasswordFilename(userId)).length() > 0;
+    }
+
+    @Override
+    public boolean havePattern(int userId) throws RemoteException {
+        // Do we need a permissions check here?
+
+        return new File(getLockPatternFilename(userId)).length() > 0;
+    }
+
+    @Override
+    public void setLockPattern(byte[] hash, int userId) throws RemoteException {
+        checkWritePermission(userId);
+
+        writeFile(getLockPatternFilename(userId), hash);
+    }
+
+    @Override
+    public boolean checkPattern(byte[] hash, int userId) throws RemoteException {
+        checkPasswordReadPermission(userId);
+        try {
+            // Read all the bytes from the file
+            RandomAccessFile raf = new RandomAccessFile(getLockPatternFilename(userId), "r");
+            final byte[] stored = new byte[(int) raf.length()];
+            int got = raf.read(stored, 0, stored.length);
+            raf.close();
+            if (got <= 0) {
+                return true;
+            }
+            // Compare the hash from the file with the entered pattern's hash
+            return Arrays.equals(stored, hash);
+        } catch (FileNotFoundException fnfe) {
+            Slog.e(TAG, "Cannot read file " + fnfe);
+            return true;
+        } catch (IOException ioe) {
+            Slog.e(TAG, "Cannot read file " + ioe);
+            return true;
+        }
+    }
+
+    @Override
+    public void setLockPassword(byte[] hash, int userId) throws RemoteException {
+        checkWritePermission(userId);
+
+        writeFile(getLockPasswordFilename(userId), hash);
+    }
+
+    @Override
+    public boolean checkPassword(byte[] hash, int userId) throws RemoteException {
+        checkPasswordReadPermission(userId);
+
+        try {
+            // Read all the bytes from the file
+            RandomAccessFile raf = new RandomAccessFile(getLockPasswordFilename(userId), "r");
+            final byte[] stored = new byte[(int) raf.length()];
+            int got = raf.read(stored, 0, stored.length);
+            raf.close();
+            if (got <= 0) {
+                return true;
+            }
+            // Compare the hash from the file with the entered password's hash
+            return Arrays.equals(stored, hash);
+        } catch (FileNotFoundException fnfe) {
+            Slog.e(TAG, "Cannot read file " + fnfe);
+            return true;
+        } catch (IOException ioe) {
+            Slog.e(TAG, "Cannot read file " + ioe);
+            return true;
+        }
+    }
+
+    @Override
+    public void removeUser(int userId) {
+        checkWritePermission(userId);
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        try {
+            File file = new File(getLockPasswordFilename(userId));
+            if (file.exists()) {
+                file.delete();
+            }
+            file = new File(getLockPatternFilename(userId));
+            if (file.exists()) {
+                file.delete();
+            }
+
+            db.beginTransaction();
+            db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    private void writeFile(String name, byte[] hash) {
+        try {
+            // Write the hash to file
+            RandomAccessFile raf = new RandomAccessFile(name, "rw");
+            // Truncate the file if pattern is null, to clear the lock
+            if (hash == null || hash.length == 0) {
+                raf.setLength(0);
+            } else {
+                raf.write(hash, 0, hash.length);
+            }
+            raf.close();
+        } catch (IOException ioe) {
+            Slog.e(TAG, "Error writing to file " + ioe);
+        }
+    }
+
+    private void writeToDb(String key, String value, int userId) {
+        writeToDb(mOpenHelper.getWritableDatabase(), key, value, userId);
+    }
+
+    private void writeToDb(SQLiteDatabase db, String key, String value, int userId) {
+        ContentValues cv = new ContentValues();
+        cv.put(COLUMN_KEY, key);
+        cv.put(COLUMN_USERID, userId);
+        cv.put(COLUMN_VALUE, value);
+
+        db.beginTransaction();
+        try {
+            db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
+                    new String[] {key, Integer.toString(userId)});
+            db.insert(TABLE, null, cv);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    private String readFromDb(String key, String defaultValue, int userId) {
+        Cursor cursor;
+        String result = defaultValue;
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
+                COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
+                new String[] { Integer.toString(userId), key },
+                null, null, null)) != null) {
+            if (cursor.moveToFirst()) {
+                result = cursor.getString(0);
+            }
+            cursor.close();
+        }
+        return result;
+    }
+
+    class DatabaseHelper extends SQLiteOpenHelper {
+        private static final String TAG = "LockSettingsDB";
+        private static final String DATABASE_NAME = "locksettings.db";
+
+        private static final int DATABASE_VERSION = 1;
+
+        public DatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+            setWriteAheadLoggingEnabled(true);
+        }
+
+        private void createTable(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE " + TABLE + " (" +
+                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
+                    COLUMN_KEY + " TEXT," +
+                    COLUMN_USERID + " INTEGER," +
+                    COLUMN_VALUE + " TEXT" +
+                    ");");
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            createTable(db);
+            initializeDefaults(db);
+        }
+
+        private void initializeDefaults(SQLiteDatabase db) {
+            // Get the lockscreen default from a system property, if available
+            boolean lockScreenDisable = SystemProperties.getBoolean("ro.lockscreen.disable.default",
+                    false);
+            if (lockScreenDisable) {
+                writeToDb(db, LockPatternUtils.DISABLE_LOCKSCREEN_KEY, "1", 0);
+            }
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
+            // Nothing yet
+        }
+    }
+
+    private static final String[] VALID_SETTINGS = new String[] {
+        LockPatternUtils.LOCKOUT_PERMANENT_KEY,
+        LockPatternUtils.LOCKOUT_ATTEMPT_DEADLINE,
+        LockPatternUtils.PATTERN_EVER_CHOSEN_KEY,
+        LockPatternUtils.PASSWORD_TYPE_KEY,
+        LockPatternUtils.PASSWORD_TYPE_ALTERNATE_KEY,
+        LockPatternUtils.LOCK_PASSWORD_SALT_KEY,
+        LockPatternUtils.DISABLE_LOCKSCREEN_KEY,
+        LockPatternUtils.LOCKSCREEN_OPTIONS,
+        LockPatternUtils.LOCKSCREEN_BIOMETRIC_WEAK_FALLBACK,
+        LockPatternUtils.BIOMETRIC_WEAK_EVER_CHOSEN_KEY,
+        LockPatternUtils.LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS,
+        LockPatternUtils.PASSWORD_HISTORY_KEY,
+        Secure.LOCK_PATTERN_ENABLED,
+        Secure.LOCK_BIOMETRIC_WEAK_FLAGS,
+        Secure.LOCK_PATTERN_VISIBLE,
+        Secure.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED
+        };
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 55885e6..a7b502a 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -16,12 +16,10 @@
 
 package com.android.server;
 
-import android.accounts.AccountManagerService;
 import android.app.ActivityManagerNative;
 import android.bluetooth.BluetoothAdapter;
 import android.content.ComponentName;
 import android.content.ContentResolver;
-import android.content.ContentService;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.IPackageManager;
@@ -32,13 +30,11 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.RemoteException;
-import android.os.SchedulingPolicyService;
 import android.os.ServiceManager;
 import android.os.StrictMode;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
-import android.server.search.SearchManagerService;
 import android.service.dreams.DreamService;
 import android.util.DisplayMetrics;
 import android.util.EventLog;
@@ -48,20 +44,23 @@
 
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.SamplingProfilerIntegration;
-import com.android.internal.widget.LockSettingsService;
 import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accounts.AccountManagerService;
 import com.android.server.am.ActivityManagerService;
 import com.android.server.am.BatteryStatsService;
+import com.android.server.content.ContentService;
 import com.android.server.display.DisplayManagerService;
 import com.android.server.dreams.DreamManagerService;
 import com.android.server.input.InputManagerService;
 import com.android.server.net.NetworkPolicyManagerService;
 import com.android.server.net.NetworkStatsService;
+import com.android.server.os.SchedulingPolicyService;
 import com.android.server.pm.Installer;
 import com.android.server.pm.PackageManagerService;
 import com.android.server.pm.UserManagerService;
 import com.android.server.power.PowerManagerService;
 import com.android.server.power.ShutdownThread;
+import com.android.server.search.SearchManagerService;
 import com.android.server.usb.UsbService;
 import com.android.server.wm.WindowManagerService;
 
diff --git a/services/java/com/android/server/accounts/AccountAuthenticatorCache.java b/services/java/com/android/server/accounts/AccountAuthenticatorCache.java
new file mode 100644
index 0000000..7552368
--- /dev/null
+++ b/services/java/com/android/server/accounts/AccountAuthenticatorCache.java
@@ -0,0 +1,94 @@
+/*
+ * 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 com.android.server.accounts;
+
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.accounts.IAccountAuthenticator;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.RegisteredServicesCache;
+import android.content.pm.XmlSerializerAndParser;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * A cache of services that export the {@link IAccountAuthenticator} interface. This cache
+ * is built by interrogating the {@link PackageManager} and is updated as packages are added,
+ * removed and changed. The authenticators are referred to by their account type and
+ * are made available via the {@link RegisteredServicesCache#getServiceInfo} method.
+ * @hide
+ */
+/* package private */ class AccountAuthenticatorCache
+        extends RegisteredServicesCache<AuthenticatorDescription>
+        implements IAccountAuthenticatorCache {
+    private static final String TAG = "Account";
+    private static final MySerializer sSerializer = new MySerializer();
+
+    public AccountAuthenticatorCache(Context context) {
+        super(context, AccountManager.ACTION_AUTHENTICATOR_INTENT,
+                AccountManager.AUTHENTICATOR_META_DATA_NAME,
+                AccountManager.AUTHENTICATOR_ATTRIBUTES_NAME, sSerializer);
+    }
+
+    public AuthenticatorDescription parseServiceAttributes(Resources res,
+            String packageName, AttributeSet attrs) {
+        TypedArray sa = res.obtainAttributes(attrs,
+                com.android.internal.R.styleable.AccountAuthenticator);
+        try {
+            final String accountType =
+                    sa.getString(com.android.internal.R.styleable.AccountAuthenticator_accountType);
+            final int labelId = sa.getResourceId(
+                    com.android.internal.R.styleable.AccountAuthenticator_label, 0);
+            final int iconId = sa.getResourceId(
+                    com.android.internal.R.styleable.AccountAuthenticator_icon, 0);
+            final int smallIconId = sa.getResourceId(
+                    com.android.internal.R.styleable.AccountAuthenticator_smallIcon, 0);
+            final int prefId = sa.getResourceId(
+                    com.android.internal.R.styleable.AccountAuthenticator_accountPreferences, 0);
+            final boolean customTokens = sa.getBoolean(
+                    com.android.internal.R.styleable.AccountAuthenticator_customTokens, false);
+            if (TextUtils.isEmpty(accountType)) {
+                return null;
+            }
+            return new AuthenticatorDescription(accountType, packageName, labelId, iconId,
+                    smallIconId, prefId, customTokens);
+        } finally {
+            sa.recycle();
+        }
+    }
+
+    private static class MySerializer implements XmlSerializerAndParser<AuthenticatorDescription> {
+        public void writeAsXml(AuthenticatorDescription item, XmlSerializer out)
+                throws IOException {
+            out.attribute(null, "type", item.type);
+        }
+
+        public AuthenticatorDescription createFromXml(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            return AuthenticatorDescription.newKey(parser.getAttributeValue(null, "type"));
+        }
+    }
+}
diff --git a/services/java/com/android/server/accounts/AccountManagerService.java b/services/java/com/android/server/accounts/AccountManagerService.java
new file mode 100644
index 0000000..150df9e
--- /dev/null
+++ b/services/java/com/android/server/accounts/AccountManagerService.java
@@ -0,0 +1,2558 @@
+/*
+ * 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 com.android.server.accounts;
+
+import android.Manifest;
+import android.accounts.Account;
+import android.accounts.AccountAndUser;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.accounts.GrantCredentialsPermissionActivity;
+import android.accounts.IAccountAuthenticator;
+import android.accounts.IAccountAuthenticatorResponse;
+import android.accounts.IAccountManager;
+import android.accounts.IAccountManagerResponse;
+import android.app.ActivityManager;
+import android.app.ActivityManagerNative;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.RegisteredServicesCache;
+import android.content.pm.RegisteredServicesCacheListener;
+import android.content.pm.UserInfo;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.R;
+import com.android.internal.util.IndentingPrintWriter;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Sets;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A system service that provides  account, password, and authtoken management for all
+ * accounts on the device. Some of these calls are implemented with the help of the corresponding
+ * {@link IAccountAuthenticator} services. This service is not accessed by users directly,
+ * instead one uses an instance of {@link AccountManager}, which can be accessed as follows:
+ *    AccountManager accountManager = AccountManager.get(context);
+ * @hide
+ */
+public class AccountManagerService
+        extends IAccountManager.Stub
+        implements RegisteredServicesCacheListener<AuthenticatorDescription> {
+    private static final String TAG = "AccountManagerService";
+
+    private static final int TIMEOUT_DELAY_MS = 1000 * 60;
+    private static final String DATABASE_NAME = "accounts.db";
+    private static final int DATABASE_VERSION = 4;
+
+    private final Context mContext;
+
+    private final PackageManager mPackageManager;
+    private UserManager mUserManager;
+
+    private HandlerThread mMessageThread;
+    private final MessageHandler mMessageHandler;
+
+    // Messages that can be sent on mHandler
+    private static final int MESSAGE_TIMED_OUT = 3;
+
+    private final IAccountAuthenticatorCache mAuthenticatorCache;
+
+    private static final String TABLE_ACCOUNTS = "accounts";
+    private static final String ACCOUNTS_ID = "_id";
+    private static final String ACCOUNTS_NAME = "name";
+    private static final String ACCOUNTS_TYPE = "type";
+    private static final String ACCOUNTS_TYPE_COUNT = "count(type)";
+    private static final String ACCOUNTS_PASSWORD = "password";
+
+    private static final String TABLE_AUTHTOKENS = "authtokens";
+    private static final String AUTHTOKENS_ID = "_id";
+    private static final String AUTHTOKENS_ACCOUNTS_ID = "accounts_id";
+    private static final String AUTHTOKENS_TYPE = "type";
+    private static final String AUTHTOKENS_AUTHTOKEN = "authtoken";
+
+    private static final String TABLE_GRANTS = "grants";
+    private static final String GRANTS_ACCOUNTS_ID = "accounts_id";
+    private static final String GRANTS_AUTH_TOKEN_TYPE = "auth_token_type";
+    private static final String GRANTS_GRANTEE_UID = "uid";
+
+    private static final String TABLE_EXTRAS = "extras";
+    private static final String EXTRAS_ID = "_id";
+    private static final String EXTRAS_ACCOUNTS_ID = "accounts_id";
+    private static final String EXTRAS_KEY = "key";
+    private static final String EXTRAS_VALUE = "value";
+
+    private static final String TABLE_META = "meta";
+    private static final String META_KEY = "key";
+    private static final String META_VALUE = "value";
+
+    private static final String[] ACCOUNT_TYPE_COUNT_PROJECTION =
+            new String[] { ACCOUNTS_TYPE, ACCOUNTS_TYPE_COUNT};
+    private static final Intent ACCOUNTS_CHANGED_INTENT;
+
+    private static final String COUNT_OF_MATCHING_GRANTS = ""
+            + "SELECT COUNT(*) FROM " + TABLE_GRANTS + ", " + TABLE_ACCOUNTS
+            + " WHERE " + GRANTS_ACCOUNTS_ID + "=" + ACCOUNTS_ID
+            + " AND " + GRANTS_GRANTEE_UID + "=?"
+            + " AND " + GRANTS_AUTH_TOKEN_TYPE + "=?"
+            + " AND " + ACCOUNTS_NAME + "=?"
+            + " AND " + ACCOUNTS_TYPE + "=?";
+
+    private static final String SELECTION_AUTHTOKENS_BY_ACCOUNT =
+            AUTHTOKENS_ACCOUNTS_ID + "=(select _id FROM accounts WHERE name=? AND type=?)";
+    private static final String[] COLUMNS_AUTHTOKENS_TYPE_AND_AUTHTOKEN = {AUTHTOKENS_TYPE,
+            AUTHTOKENS_AUTHTOKEN};
+
+    private static final String SELECTION_USERDATA_BY_ACCOUNT =
+            EXTRAS_ACCOUNTS_ID + "=(select _id FROM accounts WHERE name=? AND type=?)";
+    private static final String[] COLUMNS_EXTRAS_KEY_AND_VALUE = {EXTRAS_KEY, EXTRAS_VALUE};
+
+    private final LinkedHashMap<String, Session> mSessions = new LinkedHashMap<String, Session>();
+    private final AtomicInteger mNotificationIds = new AtomicInteger(1);
+
+    static class UserAccounts {
+        private final int userId;
+        private final DatabaseHelper openHelper;
+        private final HashMap<Pair<Pair<Account, String>, Integer>, Integer>
+                credentialsPermissionNotificationIds =
+                new HashMap<Pair<Pair<Account, String>, Integer>, Integer>();
+        private final HashMap<Account, Integer> signinRequiredNotificationIds =
+                new HashMap<Account, Integer>();
+        private final Object cacheLock = new Object();
+        /** protected by the {@link #cacheLock} */
+        private final HashMap<String, Account[]> accountCache =
+                new LinkedHashMap<String, Account[]>();
+        /** protected by the {@link #cacheLock} */
+        private HashMap<Account, HashMap<String, String>> userDataCache =
+                new HashMap<Account, HashMap<String, String>>();
+        /** protected by the {@link #cacheLock} */
+        private HashMap<Account, HashMap<String, String>> authTokenCache =
+                new HashMap<Account, HashMap<String, String>>();
+
+        UserAccounts(Context context, int userId) {
+            this.userId = userId;
+            synchronized (cacheLock) {
+                openHelper = new DatabaseHelper(context, userId);
+            }
+        }
+    }
+
+    private final SparseArray<UserAccounts> mUsers = new SparseArray<UserAccounts>();
+
+    private static AtomicReference<AccountManagerService> sThis =
+            new AtomicReference<AccountManagerService>();
+    private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[]{};
+
+    static {
+        ACCOUNTS_CHANGED_INTENT = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION);
+        ACCOUNTS_CHANGED_INTENT.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+    }
+
+
+    /**
+     * This should only be called by system code. One should only call this after the service
+     * has started.
+     * @return a reference to the AccountManagerService instance
+     * @hide
+     */
+    public static AccountManagerService getSingleton() {
+        return sThis.get();
+    }
+
+    public AccountManagerService(Context context) {
+        this(context, context.getPackageManager(), new AccountAuthenticatorCache(context));
+    }
+
+    public AccountManagerService(Context context, PackageManager packageManager,
+            IAccountAuthenticatorCache authenticatorCache) {
+        mContext = context;
+        mPackageManager = packageManager;
+
+        mMessageThread = new HandlerThread("AccountManagerService");
+        mMessageThread.start();
+        mMessageHandler = new MessageHandler(mMessageThread.getLooper());
+
+        mAuthenticatorCache = authenticatorCache;
+        mAuthenticatorCache.setListener(this, null /* Handler */);
+
+        sThis.set(this);
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        intentFilter.addDataScheme("package");
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context1, Intent intent) {
+                purgeOldGrantsAll();
+            }
+        }, intentFilter);
+
+        IntentFilter userFilter = new IntentFilter();
+        userFilter.addAction(Intent.ACTION_USER_REMOVED);
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                onUserRemoved(intent);
+            }
+        }, userFilter);
+    }
+
+    public void systemReady() {
+    }
+
+    private UserManager getUserManager() {
+        if (mUserManager == null) {
+            mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+        }
+        return mUserManager;
+    }
+
+    private UserAccounts initUser(int userId) {
+        synchronized (mUsers) {
+            UserAccounts accounts = mUsers.get(userId);
+            if (accounts == null) {
+                accounts = new UserAccounts(mContext, userId);
+                mUsers.append(userId, accounts);
+                purgeOldGrants(accounts);
+                validateAccountsInternal(accounts, true /* invalidateAuthenticatorCache */);
+            }
+            return accounts;
+        }
+    }
+
+    private void purgeOldGrantsAll() {
+        synchronized (mUsers) {
+            for (int i = 0; i < mUsers.size(); i++) {
+                purgeOldGrants(mUsers.valueAt(i));
+            }
+        }
+    }
+
+    private void purgeOldGrants(UserAccounts accounts) {
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+            final Cursor cursor = db.query(TABLE_GRANTS,
+                    new String[]{GRANTS_GRANTEE_UID},
+                    null, null, GRANTS_GRANTEE_UID, null, null);
+            try {
+                while (cursor.moveToNext()) {
+                    final int uid = cursor.getInt(0);
+                    final boolean packageExists = mPackageManager.getPackagesForUid(uid) != null;
+                    if (packageExists) {
+                        continue;
+                    }
+                    Log.d(TAG, "deleting grants for UID " + uid
+                            + " because its package is no longer installed");
+                    db.delete(TABLE_GRANTS, GRANTS_GRANTEE_UID + "=?",
+                            new String[]{Integer.toString(uid)});
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    /**
+     * Validate internal set of accounts against installed authenticators for
+     * given user. Clears cached authenticators before validating.
+     */
+    public void validateAccounts(int userId) {
+        final UserAccounts accounts = getUserAccounts(userId);
+
+        // Invalidate user-specific cache to make sure we catch any
+        // removed authenticators.
+        validateAccountsInternal(accounts, true /* invalidateAuthenticatorCache */);
+    }
+
+    /**
+     * Validate internal set of accounts against installed authenticators for
+     * given user. Clear cached authenticators before validating when requested.
+     */
+    private void validateAccountsInternal(
+            UserAccounts accounts, boolean invalidateAuthenticatorCache) {
+        if (invalidateAuthenticatorCache) {
+            mAuthenticatorCache.invalidateCache(accounts.userId);
+        }
+
+        final HashSet<AuthenticatorDescription> knownAuth = Sets.newHashSet();
+        for (RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> service :
+                mAuthenticatorCache.getAllServices(accounts.userId)) {
+            knownAuth.add(service.type);
+        }
+
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+            boolean accountDeleted = false;
+            Cursor cursor = db.query(TABLE_ACCOUNTS,
+                    new String[]{ACCOUNTS_ID, ACCOUNTS_TYPE, ACCOUNTS_NAME},
+                    null, null, null, null, null);
+            try {
+                accounts.accountCache.clear();
+                final HashMap<String, ArrayList<String>> accountNamesByType =
+                        new LinkedHashMap<String, ArrayList<String>>();
+                while (cursor.moveToNext()) {
+                    final long accountId = cursor.getLong(0);
+                    final String accountType = cursor.getString(1);
+                    final String accountName = cursor.getString(2);
+
+                    if (!knownAuth.contains(AuthenticatorDescription.newKey(accountType))) {
+                        Slog.w(TAG, "deleting account " + accountName + " because type "
+                                + accountType + " no longer has a registered authenticator");
+                        db.delete(TABLE_ACCOUNTS, ACCOUNTS_ID + "=" + accountId, null);
+                        accountDeleted = true;
+                        final Account account = new Account(accountName, accountType);
+                        accounts.userDataCache.remove(account);
+                        accounts.authTokenCache.remove(account);
+                    } else {
+                        ArrayList<String> accountNames = accountNamesByType.get(accountType);
+                        if (accountNames == null) {
+                            accountNames = new ArrayList<String>();
+                            accountNamesByType.put(accountType, accountNames);
+                        }
+                        accountNames.add(accountName);
+                    }
+                }
+                for (Map.Entry<String, ArrayList<String>> cur
+                        : accountNamesByType.entrySet()) {
+                    final String accountType = cur.getKey();
+                    final ArrayList<String> accountNames = cur.getValue();
+                    final Account[] accountsForType = new Account[accountNames.size()];
+                    int i = 0;
+                    for (String accountName : accountNames) {
+                        accountsForType[i] = new Account(accountName, accountType);
+                        ++i;
+                    }
+                    accounts.accountCache.put(accountType, accountsForType);
+                }
+            } finally {
+                cursor.close();
+                if (accountDeleted) {
+                    sendAccountsChangedBroadcast(accounts.userId);
+                }
+            }
+        }
+    }
+
+    private UserAccounts getUserAccountsForCaller() {
+        return getUserAccounts(UserHandle.getCallingUserId());
+    }
+
+    protected UserAccounts getUserAccounts(int userId) {
+        synchronized (mUsers) {
+            UserAccounts accounts = mUsers.get(userId);
+            if (accounts == null) {
+                accounts = initUser(userId);
+                mUsers.append(userId, accounts);
+            }
+            return accounts;
+        }
+    }
+
+    private void onUserRemoved(Intent intent) {
+        int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+        if (userId < 1) return;
+
+        UserAccounts accounts;
+        synchronized (mUsers) {
+            accounts = mUsers.get(userId);
+            mUsers.remove(userId);
+        }
+        if (accounts == null) {
+            File dbFile = new File(getDatabaseName(userId));
+            dbFile.delete();
+            return;
+        }
+
+        synchronized (accounts.cacheLock) {
+            accounts.openHelper.close();
+            File dbFile = new File(getDatabaseName(userId));
+            dbFile.delete();
+        }
+    }
+
+    @Override
+    public void onServiceChanged(AuthenticatorDescription desc, int userId, boolean removed) {
+        Slog.d(TAG, "onServiceChanged() for userId " + userId);
+        validateAccountsInternal(getUserAccounts(userId), false /* invalidateAuthenticatorCache */);
+    }
+
+    public String getPassword(Account account) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "getPassword: " + account
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (account == null) throw new IllegalArgumentException("account is null");
+        checkAuthenticateAccountsPermission(account);
+
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            return readPasswordInternal(accounts, account);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private String readPasswordInternal(UserAccounts accounts, Account account) {
+        if (account == null) {
+            return null;
+        }
+
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getReadableDatabase();
+            Cursor cursor = db.query(TABLE_ACCOUNTS, new String[]{ACCOUNTS_PASSWORD},
+                    ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?",
+                    new String[]{account.name, account.type}, null, null, null);
+            try {
+                if (cursor.moveToNext()) {
+                    return cursor.getString(0);
+                }
+                return null;
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    public String getUserData(Account account, String key) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "getUserData: " + account
+                    + ", key " + key
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (account == null) throw new IllegalArgumentException("account is null");
+        if (key == null) throw new IllegalArgumentException("key is null");
+        checkAuthenticateAccountsPermission(account);
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            return readUserDataInternal(accounts, account, key);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public AuthenticatorDescription[] getAuthenticatorTypes() {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "getAuthenticatorTypes: "
+                    + "caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        final int userId = UserHandle.getCallingUserId();
+        final long identityToken = clearCallingIdentity();
+        try {
+            Collection<AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription>>
+                    authenticatorCollection = mAuthenticatorCache.getAllServices(userId);
+            AuthenticatorDescription[] types =
+                    new AuthenticatorDescription[authenticatorCollection.size()];
+            int i = 0;
+            for (AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription> authenticator
+                    : authenticatorCollection) {
+                types[i] = authenticator.type;
+                i++;
+            }
+            return types;
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public boolean addAccount(Account account, String password, Bundle extras) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "addAccount: " + account
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (account == null) throw new IllegalArgumentException("account is null");
+        checkAuthenticateAccountsPermission(account);
+
+        UserAccounts accounts = getUserAccountsForCaller();
+        // fails if the account already exists
+        long identityToken = clearCallingIdentity();
+        try {
+            return addAccountInternal(accounts, account, password, extras);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private boolean addAccountInternal(UserAccounts accounts, Account account, String password,
+            Bundle extras) {
+        if (account == null) {
+            return false;
+        }
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+            db.beginTransaction();
+            try {
+                long numMatches = DatabaseUtils.longForQuery(db,
+                        "select count(*) from " + TABLE_ACCOUNTS
+                                + " WHERE " + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?",
+                        new String[]{account.name, account.type});
+                if (numMatches > 0) {
+                    Log.w(TAG, "insertAccountIntoDatabase: " + account
+                            + ", skipping since the account already exists");
+                    return false;
+                }
+                ContentValues values = new ContentValues();
+                values.put(ACCOUNTS_NAME, account.name);
+                values.put(ACCOUNTS_TYPE, account.type);
+                values.put(ACCOUNTS_PASSWORD, password);
+                long accountId = db.insert(TABLE_ACCOUNTS, ACCOUNTS_NAME, values);
+                if (accountId < 0) {
+                    Log.w(TAG, "insertAccountIntoDatabase: " + account
+                            + ", skipping the DB insert failed");
+                    return false;
+                }
+                if (extras != null) {
+                    for (String key : extras.keySet()) {
+                        final String value = extras.getString(key);
+                        if (insertExtraLocked(db, accountId, key, value) < 0) {
+                            Log.w(TAG, "insertAccountIntoDatabase: " + account
+                                    + ", skipping since insertExtra failed for key " + key);
+                            return false;
+                        }
+                    }
+                }
+                db.setTransactionSuccessful();
+                insertAccountIntoCacheLocked(accounts, account);
+            } finally {
+                db.endTransaction();
+            }
+            sendAccountsChangedBroadcast(accounts.userId);
+            return true;
+        }
+    }
+
+    private long insertExtraLocked(SQLiteDatabase db, long accountId, String key, String value) {
+        ContentValues values = new ContentValues();
+        values.put(EXTRAS_KEY, key);
+        values.put(EXTRAS_ACCOUNTS_ID, accountId);
+        values.put(EXTRAS_VALUE, value);
+        return db.insert(TABLE_EXTRAS, EXTRAS_KEY, values);
+    }
+
+    public void hasFeatures(IAccountManagerResponse response,
+            Account account, String[] features) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "hasFeatures: " + account
+                    + ", response " + response
+                    + ", features " + stringArrayToString(features)
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (response == null) throw new IllegalArgumentException("response is null");
+        if (account == null) throw new IllegalArgumentException("account is null");
+        if (features == null) throw new IllegalArgumentException("features is null");
+        checkReadAccountsPermission();
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            new TestFeaturesSession(accounts, response, account, features).bind();
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private class TestFeaturesSession extends Session {
+        private final String[] mFeatures;
+        private final Account mAccount;
+
+        public TestFeaturesSession(UserAccounts accounts, IAccountManagerResponse response,
+                Account account, String[] features) {
+            super(accounts, response, account.type, false /* expectActivityLaunch */,
+                    true /* stripAuthTokenFromResult */);
+            mFeatures = features;
+            mAccount = account;
+        }
+
+        public void run() throws RemoteException {
+            try {
+                mAuthenticator.hasFeatures(this, mAccount, mFeatures);
+            } catch (RemoteException e) {
+                onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception");
+            }
+        }
+
+        public void onResult(Bundle result) {
+            IAccountManagerResponse response = getResponseAndClose();
+            if (response != null) {
+                try {
+                    if (result == null) {
+                        response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle");
+                        return;
+                    }
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response "
+                                + response);
+                    }
+                    final Bundle newResult = new Bundle();
+                    newResult.putBoolean(AccountManager.KEY_BOOLEAN_RESULT,
+                            result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false));
+                    response.onResult(newResult);
+                } catch (RemoteException e) {
+                    // if the caller is dead then there is no one to care about remote exceptions
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "failure while notifying response", e);
+                    }
+                }
+            }
+        }
+
+        protected String toDebugString(long now) {
+            return super.toDebugString(now) + ", hasFeatures"
+                    + ", " + mAccount
+                    + ", " + (mFeatures != null ? TextUtils.join(",", mFeatures) : null);
+        }
+    }
+
+    public void removeAccount(IAccountManagerResponse response, Account account) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "removeAccount: " + account
+                    + ", response " + response
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (response == null) throw new IllegalArgumentException("response is null");
+        if (account == null) throw new IllegalArgumentException("account is null");
+        checkManageAccountsPermission();
+        UserHandle user = Binder.getCallingUserHandle();
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+
+        cancelNotification(getSigninRequiredNotificationId(accounts, account), user);
+        synchronized(accounts.credentialsPermissionNotificationIds) {
+            for (Pair<Pair<Account, String>, Integer> pair:
+                accounts.credentialsPermissionNotificationIds.keySet()) {
+                if (account.equals(pair.first.first)) {
+                    int id = accounts.credentialsPermissionNotificationIds.get(pair);
+                    cancelNotification(id, user);
+                }
+            }
+        }
+
+        try {
+            new RemoveAccountSession(accounts, response, account).bind();
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private class RemoveAccountSession extends Session {
+        final Account mAccount;
+        public RemoveAccountSession(UserAccounts accounts, IAccountManagerResponse response,
+                Account account) {
+            super(accounts, response, account.type, false /* expectActivityLaunch */,
+                    true /* stripAuthTokenFromResult */);
+            mAccount = account;
+        }
+
+        protected String toDebugString(long now) {
+            return super.toDebugString(now) + ", removeAccount"
+                    + ", account " + mAccount;
+        }
+
+        public void run() throws RemoteException {
+            mAuthenticator.getAccountRemovalAllowed(this, mAccount);
+        }
+
+        public void onResult(Bundle result) {
+            if (result != null && result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
+                    && !result.containsKey(AccountManager.KEY_INTENT)) {
+                final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
+                if (removalAllowed) {
+                    removeAccountInternal(mAccounts, mAccount);
+                }
+                IAccountManagerResponse response = getResponseAndClose();
+                if (response != null) {
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response "
+                                + response);
+                    }
+                    Bundle result2 = new Bundle();
+                    result2.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, removalAllowed);
+                    try {
+                        response.onResult(result2);
+                    } catch (RemoteException e) {
+                        // ignore
+                    }
+                }
+            }
+            super.onResult(result);
+        }
+    }
+
+    /* For testing */
+    protected void removeAccountInternal(Account account) {
+        removeAccountInternal(getUserAccountsForCaller(), account);
+    }
+
+    private void removeAccountInternal(UserAccounts accounts, Account account) {
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+            db.delete(TABLE_ACCOUNTS, ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?",
+                    new String[]{account.name, account.type});
+            removeAccountFromCacheLocked(accounts, account);
+            sendAccountsChangedBroadcast(accounts.userId);
+        }
+    }
+
+    public void invalidateAuthToken(String accountType, String authToken) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "invalidateAuthToken: accountType " + accountType
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (accountType == null) throw new IllegalArgumentException("accountType is null");
+        if (authToken == null) throw new IllegalArgumentException("authToken is null");
+        checkManageAccountsOrUseCredentialsPermissions();
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            synchronized (accounts.cacheLock) {
+                final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+                db.beginTransaction();
+                try {
+                    invalidateAuthTokenLocked(accounts, db, accountType, authToken);
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private void invalidateAuthTokenLocked(UserAccounts accounts, SQLiteDatabase db,
+            String accountType, String authToken) {
+        if (authToken == null || accountType == null) {
+            return;
+        }
+        Cursor cursor = db.rawQuery(
+                "SELECT " + TABLE_AUTHTOKENS + "." + AUTHTOKENS_ID
+                        + ", " + TABLE_ACCOUNTS + "." + ACCOUNTS_NAME
+                        + ", " + TABLE_AUTHTOKENS + "." + AUTHTOKENS_TYPE
+                        + " FROM " + TABLE_ACCOUNTS
+                        + " JOIN " + TABLE_AUTHTOKENS
+                        + " ON " + TABLE_ACCOUNTS + "." + ACCOUNTS_ID
+                        + " = " + AUTHTOKENS_ACCOUNTS_ID
+                        + " WHERE " + AUTHTOKENS_AUTHTOKEN + " = ? AND "
+                        + TABLE_ACCOUNTS + "." + ACCOUNTS_TYPE + " = ?",
+                new String[]{authToken, accountType});
+        try {
+            while (cursor.moveToNext()) {
+                long authTokenId = cursor.getLong(0);
+                String accountName = cursor.getString(1);
+                String authTokenType = cursor.getString(2);
+                db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ID + "=" + authTokenId, null);
+                writeAuthTokenIntoCacheLocked(accounts, db, new Account(accountName, accountType),
+                        authTokenType, null);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private boolean saveAuthTokenToDatabase(UserAccounts accounts, Account account, String type,
+            String authToken) {
+        if (account == null || type == null) {
+            return false;
+        }
+        cancelNotification(getSigninRequiredNotificationId(accounts, account),
+                new UserHandle(accounts.userId));
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+            db.beginTransaction();
+            try {
+                long accountId = getAccountIdLocked(db, account);
+                if (accountId < 0) {
+                    return false;
+                }
+                db.delete(TABLE_AUTHTOKENS,
+                        AUTHTOKENS_ACCOUNTS_ID + "=" + accountId + " AND " + AUTHTOKENS_TYPE + "=?",
+                        new String[]{type});
+                ContentValues values = new ContentValues();
+                values.put(AUTHTOKENS_ACCOUNTS_ID, accountId);
+                values.put(AUTHTOKENS_TYPE, type);
+                values.put(AUTHTOKENS_AUTHTOKEN, authToken);
+                if (db.insert(TABLE_AUTHTOKENS, AUTHTOKENS_AUTHTOKEN, values) >= 0) {
+                    db.setTransactionSuccessful();
+                    writeAuthTokenIntoCacheLocked(accounts, db, account, type, authToken);
+                    return true;
+                }
+                return false;
+            } finally {
+                db.endTransaction();
+            }
+        }
+    }
+
+    public String peekAuthToken(Account account, String authTokenType) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "peekAuthToken: " + account
+                    + ", authTokenType " + authTokenType
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (account == null) throw new IllegalArgumentException("account is null");
+        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+        checkAuthenticateAccountsPermission(account);
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            return readAuthTokenInternal(accounts, account, authTokenType);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void setAuthToken(Account account, String authTokenType, String authToken) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "setAuthToken: " + account
+                    + ", authTokenType " + authTokenType
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (account == null) throw new IllegalArgumentException("account is null");
+        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+        checkAuthenticateAccountsPermission(account);
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            saveAuthTokenToDatabase(accounts, account, authTokenType, authToken);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void setPassword(Account account, String password) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "setAuthToken: " + account
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (account == null) throw new IllegalArgumentException("account is null");
+        checkAuthenticateAccountsPermission(account);
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            setPasswordInternal(accounts, account, password);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private void setPasswordInternal(UserAccounts accounts, Account account, String password) {
+        if (account == null) {
+            return;
+        }
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+            db.beginTransaction();
+            try {
+                final ContentValues values = new ContentValues();
+                values.put(ACCOUNTS_PASSWORD, password);
+                final long accountId = getAccountIdLocked(db, account);
+                if (accountId >= 0) {
+                    final String[] argsAccountId = {String.valueOf(accountId)};
+                    db.update(TABLE_ACCOUNTS, values, ACCOUNTS_ID + "=?", argsAccountId);
+                    db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ACCOUNTS_ID + "=?", argsAccountId);
+                    accounts.authTokenCache.remove(account);
+                    db.setTransactionSuccessful();
+                }
+            } finally {
+                db.endTransaction();
+            }
+            sendAccountsChangedBroadcast(accounts.userId);
+        }
+    }
+
+    private void sendAccountsChangedBroadcast(int userId) {
+        Log.i(TAG, "the accounts changed, sending broadcast of "
+                + ACCOUNTS_CHANGED_INTENT.getAction());
+        mContext.sendBroadcastAsUser(ACCOUNTS_CHANGED_INTENT, new UserHandle(userId));
+    }
+
+    public void clearPassword(Account account) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "clearPassword: " + account
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (account == null) throw new IllegalArgumentException("account is null");
+        checkManageAccountsPermission();
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            setPasswordInternal(accounts, account, null);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void setUserData(Account account, String key, String value) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "setUserData: " + account
+                    + ", key " + key
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (key == null) throw new IllegalArgumentException("key is null");
+        if (account == null) throw new IllegalArgumentException("account is null");
+        checkAuthenticateAccountsPermission(account);
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            setUserdataInternal(accounts, account, key, value);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private void setUserdataInternal(UserAccounts accounts, Account account, String key,
+            String value) {
+        if (account == null || key == null) {
+            return;
+        }
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+            db.beginTransaction();
+            try {
+                long accountId = getAccountIdLocked(db, account);
+                if (accountId < 0) {
+                    return;
+                }
+                long extrasId = getExtrasIdLocked(db, accountId, key);
+                if (extrasId < 0 ) {
+                    extrasId = insertExtraLocked(db, accountId, key, value);
+                    if (extrasId < 0) {
+                        return;
+                    }
+                } else {
+                    ContentValues values = new ContentValues();
+                    values.put(EXTRAS_VALUE, value);
+                    if (1 != db.update(TABLE_EXTRAS, values, EXTRAS_ID + "=" + extrasId, null)) {
+                        return;
+                    }
+
+                }
+                writeUserDataIntoCacheLocked(accounts, db, account, key, value);
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
+        }
+    }
+
+    private void onResult(IAccountManagerResponse response, Bundle result) {
+        if (result == null) {
+            Log.e(TAG, "the result is unexpectedly null", new Exception());
+        }
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response "
+                    + response);
+        }
+        try {
+            response.onResult(result);
+        } catch (RemoteException e) {
+            // if the caller is dead then there is no one to care about remote
+            // exceptions
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "failure while notifying response", e);
+            }
+        }
+    }
+
+    public void getAuthTokenLabel(IAccountManagerResponse response, final String accountType,
+                                  final String authTokenType)
+            throws RemoteException {
+        if (accountType == null) throw new IllegalArgumentException("accountType is null");
+        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+
+        final int callingUid = getCallingUid();
+        clearCallingIdentity();
+        if (callingUid != android.os.Process.SYSTEM_UID) {
+            throw new SecurityException("can only call from system");
+        }
+        UserAccounts accounts = getUserAccounts(UserHandle.getUserId(callingUid));
+        long identityToken = clearCallingIdentity();
+        try {
+            new Session(accounts, response, accountType, false,
+                    false /* stripAuthTokenFromResult */) {
+                protected String toDebugString(long now) {
+                    return super.toDebugString(now) + ", getAuthTokenLabel"
+                            + ", " + accountType
+                            + ", authTokenType " + authTokenType;
+                }
+
+                public void run() throws RemoteException {
+                    mAuthenticator.getAuthTokenLabel(this, authTokenType);
+                }
+
+                public void onResult(Bundle result) {
+                    if (result != null) {
+                        String label = result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL);
+                        Bundle bundle = new Bundle();
+                        bundle.putString(AccountManager.KEY_AUTH_TOKEN_LABEL, label);
+                        super.onResult(bundle);
+                        return;
+                    } else {
+                        super.onResult(result);
+                    }
+                }
+            }.bind();
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void getAuthToken(IAccountManagerResponse response, final Account account,
+            final String authTokenType, final boolean notifyOnAuthFailure,
+            final boolean expectActivityLaunch, Bundle loginOptionsIn) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "getAuthToken: " + account
+                    + ", response " + response
+                    + ", authTokenType " + authTokenType
+                    + ", notifyOnAuthFailure " + notifyOnAuthFailure
+                    + ", expectActivityLaunch " + expectActivityLaunch
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (response == null) throw new IllegalArgumentException("response is null");
+        if (account == null) throw new IllegalArgumentException("account is null");
+        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+        checkBinderPermission(Manifest.permission.USE_CREDENTIALS);
+        final UserAccounts accounts = getUserAccountsForCaller();
+        final RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo;
+        authenticatorInfo = mAuthenticatorCache.getServiceInfo(
+                AuthenticatorDescription.newKey(account.type), accounts.userId);
+        final boolean customTokens =
+            authenticatorInfo != null && authenticatorInfo.type.customTokens;
+
+        // skip the check if customTokens
+        final int callerUid = Binder.getCallingUid();
+        final boolean permissionGranted = customTokens ||
+            permissionIsGranted(account, authTokenType, callerUid);
+
+        final Bundle loginOptions = (loginOptionsIn == null) ? new Bundle() :
+            loginOptionsIn;
+        // let authenticator know the identity of the caller
+        loginOptions.putInt(AccountManager.KEY_CALLER_UID, callerUid);
+        loginOptions.putInt(AccountManager.KEY_CALLER_PID, Binder.getCallingPid());
+        if (notifyOnAuthFailure) {
+            loginOptions.putBoolean(AccountManager.KEY_NOTIFY_ON_FAILURE, true);
+        }
+
+        long identityToken = clearCallingIdentity();
+        try {
+            // if the caller has permission, do the peek. otherwise go the more expensive
+            // route of starting a Session
+            if (!customTokens && permissionGranted) {
+                String authToken = readAuthTokenInternal(accounts, account, authTokenType);
+                if (authToken != null) {
+                    Bundle result = new Bundle();
+                    result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
+                    result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+                    result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+                    onResult(response, result);
+                    return;
+                }
+            }
+
+            new Session(accounts, response, account.type, expectActivityLaunch,
+                    false /* stripAuthTokenFromResult */) {
+                protected String toDebugString(long now) {
+                    if (loginOptions != null) loginOptions.keySet();
+                    return super.toDebugString(now) + ", getAuthToken"
+                            + ", " + account
+                            + ", authTokenType " + authTokenType
+                            + ", loginOptions " + loginOptions
+                            + ", notifyOnAuthFailure " + notifyOnAuthFailure;
+                }
+
+                public void run() throws RemoteException {
+                    // If the caller doesn't have permission then create and return the
+                    // "grant permission" intent instead of the "getAuthToken" intent.
+                    if (!permissionGranted) {
+                        mAuthenticator.getAuthTokenLabel(this, authTokenType);
+                    } else {
+                        mAuthenticator.getAuthToken(this, account, authTokenType, loginOptions);
+                    }
+                }
+
+                public void onResult(Bundle result) {
+                    if (result != null) {
+                        if (result.containsKey(AccountManager.KEY_AUTH_TOKEN_LABEL)) {
+                            Intent intent = newGrantCredentialsPermissionIntent(account, callerUid,
+                                    new AccountAuthenticatorResponse(this),
+                                    authTokenType,
+                                    result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL));
+                            Bundle bundle = new Bundle();
+                            bundle.putParcelable(AccountManager.KEY_INTENT, intent);
+                            onResult(bundle);
+                            return;
+                        }
+                        String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
+                        if (authToken != null) {
+                            String name = result.getString(AccountManager.KEY_ACCOUNT_NAME);
+                            String type = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
+                            if (TextUtils.isEmpty(type) || TextUtils.isEmpty(name)) {
+                                onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
+                                        "the type and name should not be empty");
+                                return;
+                            }
+                            if (!customTokens) {
+                                saveAuthTokenToDatabase(mAccounts, new Account(name, type),
+                                        authTokenType, authToken);
+                            }
+                        }
+
+                        Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
+                        if (intent != null && notifyOnAuthFailure && !customTokens) {
+                            doNotification(mAccounts,
+                                    account, result.getString(AccountManager.KEY_AUTH_FAILED_MESSAGE),
+                                    intent, accounts.userId);
+                        }
+                    }
+                    super.onResult(result);
+                }
+            }.bind();
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private void createNoCredentialsPermissionNotification(Account account, Intent intent,
+            int userId) {
+        int uid = intent.getIntExtra(
+                GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, -1);
+        String authTokenType = intent.getStringExtra(
+                GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE);
+        String authTokenLabel = intent.getStringExtra(
+                GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_LABEL);
+
+        Notification n = new Notification(android.R.drawable.stat_sys_warning, null,
+                0 /* when */);
+        final String titleAndSubtitle =
+                mContext.getString(R.string.permission_request_notification_with_subtitle,
+                account.name);
+        final int index = titleAndSubtitle.indexOf('\n');
+        String title = titleAndSubtitle;
+        String subtitle = "";
+        if (index > 0) {
+            title = titleAndSubtitle.substring(0, index);
+            subtitle = titleAndSubtitle.substring(index + 1);            
+        }
+        UserHandle user = new UserHandle(userId);
+        n.setLatestEventInfo(mContext, title, subtitle,
+                PendingIntent.getActivityAsUser(mContext, 0, intent,
+                        PendingIntent.FLAG_CANCEL_CURRENT, null, user));
+        installNotification(getCredentialPermissionNotificationId(
+                account, authTokenType, uid), n, user);
+    }
+
+    private Intent newGrantCredentialsPermissionIntent(Account account, int uid,
+            AccountAuthenticatorResponse response, String authTokenType, String authTokenLabel) {
+
+        Intent intent = new Intent(mContext, GrantCredentialsPermissionActivity.class);
+        // See FLAG_ACTIVITY_NEW_TASK docs for limitations and benefits of the flag.
+        // Since it was set in Eclair+ we can't change it without breaking apps using
+        // the intent from a non-Activity context.
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addCategory(
+                String.valueOf(getCredentialPermissionNotificationId(account, authTokenType, uid)));
+
+        intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_ACCOUNT, account);
+        intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE, authTokenType);
+        intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_RESPONSE, response);
+        intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, uid);
+
+        return intent;
+    }
+
+    private Integer getCredentialPermissionNotificationId(Account account, String authTokenType,
+            int uid) {
+        Integer id;
+        UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
+        synchronized (accounts.credentialsPermissionNotificationIds) {
+            final Pair<Pair<Account, String>, Integer> key =
+                    new Pair<Pair<Account, String>, Integer>(
+                            new Pair<Account, String>(account, authTokenType), uid);
+            id = accounts.credentialsPermissionNotificationIds.get(key);
+            if (id == null) {
+                id = mNotificationIds.incrementAndGet();
+                accounts.credentialsPermissionNotificationIds.put(key, id);
+            }
+        }
+        return id;
+    }
+
+    private Integer getSigninRequiredNotificationId(UserAccounts accounts, Account account) {
+        Integer id;
+        synchronized (accounts.signinRequiredNotificationIds) {
+            id = accounts.signinRequiredNotificationIds.get(account);
+            if (id == null) {
+                id = mNotificationIds.incrementAndGet();
+                accounts.signinRequiredNotificationIds.put(account, id);
+            }
+        }
+        return id;
+    }
+
+    public void addAcount(final IAccountManagerResponse response, final String accountType,
+            final String authTokenType, final String[] requiredFeatures,
+            final boolean expectActivityLaunch, final Bundle optionsIn) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "addAccount: accountType " + accountType
+                    + ", response " + response
+                    + ", authTokenType " + authTokenType
+                    + ", requiredFeatures " + stringArrayToString(requiredFeatures)
+                    + ", expectActivityLaunch " + expectActivityLaunch
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (response == null) throw new IllegalArgumentException("response is null");
+        if (accountType == null) throw new IllegalArgumentException("accountType is null");
+        checkManageAccountsPermission();
+
+        UserAccounts accounts = getUserAccountsForCaller();
+        final int pid = Binder.getCallingPid();
+        final int uid = Binder.getCallingUid();
+        final Bundle options = (optionsIn == null) ? new Bundle() : optionsIn;
+        options.putInt(AccountManager.KEY_CALLER_UID, uid);
+        options.putInt(AccountManager.KEY_CALLER_PID, pid);
+
+        long identityToken = clearCallingIdentity();
+        try {
+            new Session(accounts, response, accountType, expectActivityLaunch,
+                    true /* stripAuthTokenFromResult */) {
+                public void run() throws RemoteException {
+                    mAuthenticator.addAccount(this, mAccountType, authTokenType, requiredFeatures,
+                            options);
+                }
+
+                protected String toDebugString(long now) {
+                    return super.toDebugString(now) + ", addAccount"
+                            + ", accountType " + accountType
+                            + ", requiredFeatures "
+                            + (requiredFeatures != null
+                              ? TextUtils.join(",", requiredFeatures)
+                              : null);
+                }
+            }.bind();
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    @Override
+    public void confirmCredentialsAsUser(IAccountManagerResponse response,
+            final Account account, final Bundle options, final boolean expectActivityLaunch,
+            int userId) {
+        // Only allow the system process to read accounts of other users
+        if (userId != UserHandle.getCallingUserId()
+                && Binder.getCallingUid() != android.os.Process.myUid()) {
+            throw new SecurityException("User " + UserHandle.getCallingUserId()
+                    + " trying to confirm account credentials for " + userId);
+        }
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "confirmCredentials: " + account
+                    + ", response " + response
+                    + ", expectActivityLaunch " + expectActivityLaunch
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (response == null) throw new IllegalArgumentException("response is null");
+        if (account == null) throw new IllegalArgumentException("account is null");
+        checkManageAccountsPermission();
+        UserAccounts accounts = getUserAccounts(userId);
+        long identityToken = clearCallingIdentity();
+        try {
+            new Session(accounts, response, account.type, expectActivityLaunch,
+                    true /* stripAuthTokenFromResult */) {
+                public void run() throws RemoteException {
+                    mAuthenticator.confirmCredentials(this, account, options);
+                }
+                protected String toDebugString(long now) {
+                    return super.toDebugString(now) + ", confirmCredentials"
+                            + ", " + account;
+                }
+            }.bind();
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void updateCredentials(IAccountManagerResponse response, final Account account,
+            final String authTokenType, final boolean expectActivityLaunch,
+            final Bundle loginOptions) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "updateCredentials: " + account
+                    + ", response " + response
+                    + ", authTokenType " + authTokenType
+                    + ", expectActivityLaunch " + expectActivityLaunch
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (response == null) throw new IllegalArgumentException("response is null");
+        if (account == null) throw new IllegalArgumentException("account is null");
+        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
+        checkManageAccountsPermission();
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            new Session(accounts, response, account.type, expectActivityLaunch,
+                    true /* stripAuthTokenFromResult */) {
+                public void run() throws RemoteException {
+                    mAuthenticator.updateCredentials(this, account, authTokenType, loginOptions);
+                }
+                protected String toDebugString(long now) {
+                    if (loginOptions != null) loginOptions.keySet();
+                    return super.toDebugString(now) + ", updateCredentials"
+                            + ", " + account
+                            + ", authTokenType " + authTokenType
+                            + ", loginOptions " + loginOptions;
+                }
+            }.bind();
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void editProperties(IAccountManagerResponse response, final String accountType,
+            final boolean expectActivityLaunch) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "editProperties: accountType " + accountType
+                    + ", response " + response
+                    + ", expectActivityLaunch " + expectActivityLaunch
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (response == null) throw new IllegalArgumentException("response is null");
+        if (accountType == null) throw new IllegalArgumentException("accountType is null");
+        checkManageAccountsPermission();
+        UserAccounts accounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            new Session(accounts, response, accountType, expectActivityLaunch,
+                    true /* stripAuthTokenFromResult */) {
+                public void run() throws RemoteException {
+                    mAuthenticator.editProperties(this, mAccountType);
+                }
+                protected String toDebugString(long now) {
+                    return super.toDebugString(now) + ", editProperties"
+                            + ", accountType " + accountType;
+                }
+            }.bind();
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private class GetAccountsByTypeAndFeatureSession extends Session {
+        private final String[] mFeatures;
+        private volatile Account[] mAccountsOfType = null;
+        private volatile ArrayList<Account> mAccountsWithFeatures = null;
+        private volatile int mCurrentAccount = 0;
+
+        public GetAccountsByTypeAndFeatureSession(UserAccounts accounts,
+                IAccountManagerResponse response, String type, String[] features) {
+            super(accounts, response, type, false /* expectActivityLaunch */,
+                    true /* stripAuthTokenFromResult */);
+            mFeatures = features;
+        }
+
+        public void run() throws RemoteException {
+            synchronized (mAccounts.cacheLock) {
+                mAccountsOfType = getAccountsFromCacheLocked(mAccounts, mAccountType);
+            }
+            // check whether each account matches the requested features
+            mAccountsWithFeatures = new ArrayList<Account>(mAccountsOfType.length);
+            mCurrentAccount = 0;
+
+            checkAccount();
+        }
+
+        public void checkAccount() {
+            if (mCurrentAccount >= mAccountsOfType.length) {
+                sendResult();
+                return;
+            }
+
+            final IAccountAuthenticator accountAuthenticator = mAuthenticator;
+            if (accountAuthenticator == null) {
+                // It is possible that the authenticator has died, which is indicated by
+                // mAuthenticator being set to null. If this happens then just abort.
+                // There is no need to send back a result or error in this case since
+                // that already happened when mAuthenticator was cleared.
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "checkAccount: aborting session since we are no longer"
+                            + " connected to the authenticator, " + toDebugString());
+                }
+                return;
+            }
+            try {
+                accountAuthenticator.hasFeatures(this, mAccountsOfType[mCurrentAccount], mFeatures);
+            } catch (RemoteException e) {
+                onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "remote exception");
+            }
+        }
+
+        public void onResult(Bundle result) {
+            mNumResults++;
+            if (result == null) {
+                onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle");
+                return;
+            }
+            if (result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)) {
+                mAccountsWithFeatures.add(mAccountsOfType[mCurrentAccount]);
+            }
+            mCurrentAccount++;
+            checkAccount();
+        }
+
+        public void sendResult() {
+            IAccountManagerResponse response = getResponseAndClose();
+            if (response != null) {
+                try {
+                    Account[] accounts = new Account[mAccountsWithFeatures.size()];
+                    for (int i = 0; i < accounts.length; i++) {
+                        accounts[i] = mAccountsWithFeatures.get(i);
+                    }
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, getClass().getSimpleName() + " calling onResult() on response "
+                                + response);
+                    }
+                    Bundle result = new Bundle();
+                    result.putParcelableArray(AccountManager.KEY_ACCOUNTS, accounts);
+                    response.onResult(result);
+                } catch (RemoteException e) {
+                    // if the caller is dead then there is no one to care about remote exceptions
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "failure while notifying response", e);
+                    }
+                }
+            }
+        }
+
+
+        protected String toDebugString(long now) {
+            return super.toDebugString(now) + ", getAccountsByTypeAndFeatures"
+                    + ", " + (mFeatures != null ? TextUtils.join(",", mFeatures) : null);
+        }
+    }
+
+    /**
+     * Returns the accounts for a specific user
+     * @hide
+     */
+    public Account[] getAccounts(int userId) {
+        checkReadAccountsPermission();
+        UserAccounts accounts = getUserAccounts(userId);
+        long identityToken = clearCallingIdentity();
+        try {
+            synchronized (accounts.cacheLock) {
+                return getAccountsFromCacheLocked(accounts, null);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    /**
+     * Returns accounts for all running users.
+     *
+     * @hide
+     */
+    public AccountAndUser[] getRunningAccounts() {
+        final int[] runningUserIds;
+        try {
+            runningUserIds = ActivityManagerNative.getDefault().getRunningUserIds();
+        } catch (RemoteException e) {
+            // Running in system_server; should never happen
+            throw new RuntimeException(e);
+        }
+        return getAccounts(runningUserIds);
+    }
+
+    /** {@hide} */
+    public AccountAndUser[] getAllAccounts() {
+        final List<UserInfo> users = getUserManager().getUsers();
+        final int[] userIds = new int[users.size()];
+        for (int i = 0; i < userIds.length; i++) {
+            userIds[i] = users.get(i).id;
+        }
+        return getAccounts(userIds);
+    }
+
+    private AccountAndUser[] getAccounts(int[] userIds) {
+        final ArrayList<AccountAndUser> runningAccounts = Lists.newArrayList();
+        synchronized (mUsers) {
+            for (int userId : userIds) {
+                UserAccounts userAccounts = getUserAccounts(userId);
+                if (userAccounts == null) continue;
+                synchronized (userAccounts.cacheLock) {
+                    Account[] accounts = getAccountsFromCacheLocked(userAccounts, null);
+                    for (int a = 0; a < accounts.length; a++) {
+                        runningAccounts.add(new AccountAndUser(accounts[a], userId));
+                    }
+                }
+            }
+        }
+
+        AccountAndUser[] accountsArray = new AccountAndUser[runningAccounts.size()];
+        return runningAccounts.toArray(accountsArray);
+    }
+
+    @Override
+    public Account[] getAccountsAsUser(String type, int userId) {
+        // Only allow the system process to read accounts of other users
+        if (userId != UserHandle.getCallingUserId()
+                && Binder.getCallingUid() != android.os.Process.myUid()) {
+            throw new SecurityException("User " + UserHandle.getCallingUserId()
+                    + " trying to get account for " + userId);
+        }
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "getAccounts: accountType " + type
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        checkReadAccountsPermission();
+        UserAccounts accounts = getUserAccounts(userId);
+        long identityToken = clearCallingIdentity();
+        try {
+            synchronized (accounts.cacheLock) {
+                return getAccountsFromCacheLocked(accounts, type);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    @Override
+    public Account[] getAccounts(String type) {
+        return getAccountsAsUser(type, UserHandle.getCallingUserId());
+    }
+
+    public void getAccountsByFeatures(IAccountManagerResponse response,
+            String type, String[] features) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "getAccounts: accountType " + type
+                    + ", response " + response
+                    + ", features " + stringArrayToString(features)
+                    + ", caller's uid " + Binder.getCallingUid()
+                    + ", pid " + Binder.getCallingPid());
+        }
+        if (response == null) throw new IllegalArgumentException("response is null");
+        if (type == null) throw new IllegalArgumentException("accountType is null");
+        checkReadAccountsPermission();
+        UserAccounts userAccounts = getUserAccountsForCaller();
+        long identityToken = clearCallingIdentity();
+        try {
+            if (features == null || features.length == 0) {
+                Account[] accounts;
+                synchronized (userAccounts.cacheLock) {
+                    accounts = getAccountsFromCacheLocked(userAccounts, type);
+                }
+                Bundle result = new Bundle();
+                result.putParcelableArray(AccountManager.KEY_ACCOUNTS, accounts);
+                onResult(response, result);
+                return;
+            }
+            new GetAccountsByTypeAndFeatureSession(userAccounts, response, type, features).bind();
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    private long getAccountIdLocked(SQLiteDatabase db, Account account) {
+        Cursor cursor = db.query(TABLE_ACCOUNTS, new String[]{ACCOUNTS_ID},
+                "name=? AND type=?", new String[]{account.name, account.type}, null, null, null);
+        try {
+            if (cursor.moveToNext()) {
+                return cursor.getLong(0);
+            }
+            return -1;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private long getExtrasIdLocked(SQLiteDatabase db, long accountId, String key) {
+        Cursor cursor = db.query(TABLE_EXTRAS, new String[]{EXTRAS_ID},
+                EXTRAS_ACCOUNTS_ID + "=" + accountId + " AND " + EXTRAS_KEY + "=?",
+                new String[]{key}, null, null, null);
+        try {
+            if (cursor.moveToNext()) {
+                return cursor.getLong(0);
+            }
+            return -1;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private abstract class Session extends IAccountAuthenticatorResponse.Stub
+            implements IBinder.DeathRecipient, ServiceConnection {
+        IAccountManagerResponse mResponse;
+        final String mAccountType;
+        final boolean mExpectActivityLaunch;
+        final long mCreationTime;
+
+        public int mNumResults = 0;
+        private int mNumRequestContinued = 0;
+        private int mNumErrors = 0;
+
+
+        IAccountAuthenticator mAuthenticator = null;
+
+        private final boolean mStripAuthTokenFromResult;
+        protected final UserAccounts mAccounts;
+
+        public Session(UserAccounts accounts, IAccountManagerResponse response, String accountType,
+                boolean expectActivityLaunch, boolean stripAuthTokenFromResult) {
+            super();
+            if (response == null) throw new IllegalArgumentException("response is null");
+            if (accountType == null) throw new IllegalArgumentException("accountType is null");
+            mAccounts = accounts;
+            mStripAuthTokenFromResult = stripAuthTokenFromResult;
+            mResponse = response;
+            mAccountType = accountType;
+            mExpectActivityLaunch = expectActivityLaunch;
+            mCreationTime = SystemClock.elapsedRealtime();
+            synchronized (mSessions) {
+                mSessions.put(toString(), this);
+            }
+            try {
+                response.asBinder().linkToDeath(this, 0 /* flags */);
+            } catch (RemoteException e) {
+                mResponse = null;
+                binderDied();
+            }
+        }
+
+        IAccountManagerResponse getResponseAndClose() {
+            if (mResponse == null) {
+                // this session has already been closed
+                return null;
+            }
+            IAccountManagerResponse response = mResponse;
+            close(); // this clears mResponse so we need to save the response before this call
+            return response;
+        }
+
+        private void close() {
+            synchronized (mSessions) {
+                if (mSessions.remove(toString()) == null) {
+                    // the session was already closed, so bail out now
+                    return;
+                }
+            }
+            if (mResponse != null) {
+                // stop listening for response deaths
+                mResponse.asBinder().unlinkToDeath(this, 0 /* flags */);
+
+                // clear this so that we don't accidentally send any further results
+                mResponse = null;
+            }
+            cancelTimeout();
+            unbind();
+        }
+
+        public void binderDied() {
+            mResponse = null;
+            close();
+        }
+
+        protected String toDebugString() {
+            return toDebugString(SystemClock.elapsedRealtime());
+        }
+
+        protected String toDebugString(long now) {
+            return "Session: expectLaunch " + mExpectActivityLaunch
+                    + ", connected " + (mAuthenticator != null)
+                    + ", stats (" + mNumResults + "/" + mNumRequestContinued
+                    + "/" + mNumErrors + ")"
+                    + ", lifetime " + ((now - mCreationTime) / 1000.0);
+        }
+
+        void bind() {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "initiating bind to authenticator type " + mAccountType);
+            }
+            if (!bindToAuthenticator(mAccountType)) {
+                Log.d(TAG, "bind attempt failed for " + toDebugString());
+                onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "bind failure");
+            }
+        }
+
+        private void unbind() {
+            if (mAuthenticator != null) {
+                mAuthenticator = null;
+                mContext.unbindService(this);
+            }
+        }
+
+        public void scheduleTimeout() {
+            mMessageHandler.sendMessageDelayed(
+                    mMessageHandler.obtainMessage(MESSAGE_TIMED_OUT, this), TIMEOUT_DELAY_MS);
+        }
+
+        public void cancelTimeout() {
+            mMessageHandler.removeMessages(MESSAGE_TIMED_OUT, this);
+        }
+
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            mAuthenticator = IAccountAuthenticator.Stub.asInterface(service);
+            try {
+                run();
+            } catch (RemoteException e) {
+                onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
+                        "remote exception");
+            }
+        }
+
+        public void onServiceDisconnected(ComponentName name) {
+            mAuthenticator = null;
+            IAccountManagerResponse response = getResponseAndClose();
+            if (response != null) {
+                try {
+                    response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
+                            "disconnected");
+                } catch (RemoteException e) {
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "Session.onServiceDisconnected: "
+                                + "caught RemoteException while responding", e);
+                    }
+                }
+            }
+        }
+
+        public abstract void run() throws RemoteException;
+
+        public void onTimedOut() {
+            IAccountManagerResponse response = getResponseAndClose();
+            if (response != null) {
+                try {
+                    response.onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
+                            "timeout");
+                } catch (RemoteException e) {
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "Session.onTimedOut: caught RemoteException while responding",
+                                e);
+                    }
+                }
+            }
+        }
+
+        public void onResult(Bundle result) {
+            mNumResults++;
+            if (result != null && !TextUtils.isEmpty(result.getString(AccountManager.KEY_AUTHTOKEN))) {
+                String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME);
+                String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
+                if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+                    Account account = new Account(accountName, accountType);
+                    cancelNotification(getSigninRequiredNotificationId(mAccounts, account),
+                            new UserHandle(mAccounts.userId));
+                }
+            }
+            IAccountManagerResponse response;
+            if (mExpectActivityLaunch && result != null
+                    && result.containsKey(AccountManager.KEY_INTENT)) {
+                response = mResponse;
+            } else {
+                response = getResponseAndClose();
+            }
+            if (response != null) {
+                try {
+                    if (result == null) {
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, getClass().getSimpleName()
+                                    + " calling onError() on response " + response);
+                        }
+                        response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
+                                "null bundle returned");
+                    } else {
+                        if (mStripAuthTokenFromResult) {
+                            result.remove(AccountManager.KEY_AUTHTOKEN);
+                        }
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, getClass().getSimpleName()
+                                    + " calling onResult() on response " + response);
+                        }
+                        response.onResult(result);
+                    }
+                } catch (RemoteException e) {
+                    // if the caller is dead then there is no one to care about remote exceptions
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "failure while notifying response", e);
+                    }
+                }
+            }
+        }
+
+        public void onRequestContinued() {
+            mNumRequestContinued++;
+        }
+
+        public void onError(int errorCode, String errorMessage) {
+            mNumErrors++;
+            IAccountManagerResponse response = getResponseAndClose();
+            if (response != null) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, getClass().getSimpleName()
+                            + " calling onError() on response " + response);
+                }
+                try {
+                    response.onError(errorCode, errorMessage);
+                } catch (RemoteException e) {
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "Session.onError: caught RemoteException while responding", e);
+                    }
+                }
+            } else {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "Session.onError: already closed");
+                }
+            }
+        }
+
+        /**
+         * find the component name for the authenticator and initiate a bind
+         * if no authenticator or the bind fails then return false, otherwise return true
+         */
+        private boolean bindToAuthenticator(String authenticatorType) {
+            final AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo;
+            authenticatorInfo = mAuthenticatorCache.getServiceInfo(
+                    AuthenticatorDescription.newKey(authenticatorType), mAccounts.userId);
+            if (authenticatorInfo == null) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "there is no authenticator for " + authenticatorType
+                            + ", bailing out");
+                }
+                return false;
+            }
+
+            Intent intent = new Intent();
+            intent.setAction(AccountManager.ACTION_AUTHENTICATOR_INTENT);
+            intent.setComponent(authenticatorInfo.componentName);
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "performing bindService to " + authenticatorInfo.componentName);
+            }
+            if (!mContext.bindService(intent, this, Context.BIND_AUTO_CREATE, mAccounts.userId)) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "bindService to " + authenticatorInfo.componentName + " failed");
+                }
+                return false;
+            }
+
+
+            return true;
+        }
+    }
+
+    private class MessageHandler extends Handler {
+        MessageHandler(Looper looper) {
+            super(looper);
+        }
+
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MESSAGE_TIMED_OUT:
+                    Session session = (Session)msg.obj;
+                    session.onTimedOut();
+                    break;
+
+                default:
+                    throw new IllegalStateException("unhandled message: " + msg.what);
+            }
+        }
+    }
+
+    private static String getDatabaseName(int userId) {
+        File systemDir = Environment.getSystemSecureDirectory();
+        File databaseFile = new File(Environment.getUserSystemDirectory(userId), DATABASE_NAME);
+        if (userId == 0) {
+            // Migrate old file, if it exists, to the new location.
+            // Make sure the new file doesn't already exist. A dummy file could have been
+            // accidentally created in the old location, causing the new one to become corrupted
+            // as well.
+            File oldFile = new File(systemDir, DATABASE_NAME);
+            if (oldFile.exists() && !databaseFile.exists()) {
+                // Check for use directory; create if it doesn't exist, else renameTo will fail
+                File userDir = Environment.getUserSystemDirectory(userId);
+                if (!userDir.exists()) {
+                    if (!userDir.mkdirs()) {
+                        throw new IllegalStateException("User dir cannot be created: " + userDir);
+                    }
+                }
+                if (!oldFile.renameTo(databaseFile)) {
+                    throw new IllegalStateException("User dir cannot be migrated: " + databaseFile);
+                }
+            }
+        }
+        return databaseFile.getPath();
+    }
+
+    static class DatabaseHelper extends SQLiteOpenHelper {
+
+        public DatabaseHelper(Context context, int userId) {
+            super(context, AccountManagerService.getDatabaseName(userId), null, DATABASE_VERSION);
+        }
+
+        /**
+         * This call needs to be made while the mCacheLock is held. The way to
+         * ensure this is to get the lock any time a method is called ont the DatabaseHelper
+         * @param db The database.
+         */
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE " + TABLE_ACCOUNTS + " ( "
+                    + ACCOUNTS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+                    + ACCOUNTS_NAME + " TEXT NOT NULL, "
+                    + ACCOUNTS_TYPE + " TEXT NOT NULL, "
+                    + ACCOUNTS_PASSWORD + " TEXT, "
+                    + "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))");
+
+            db.execSQL("CREATE TABLE " + TABLE_AUTHTOKENS + " (  "
+                    + AUTHTOKENS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,  "
+                    + AUTHTOKENS_ACCOUNTS_ID + " INTEGER NOT NULL, "
+                    + AUTHTOKENS_TYPE + " TEXT NOT NULL,  "
+                    + AUTHTOKENS_AUTHTOKEN + " TEXT,  "
+                    + "UNIQUE (" + AUTHTOKENS_ACCOUNTS_ID + "," + AUTHTOKENS_TYPE + "))");
+
+            createGrantsTable(db);
+
+            db.execSQL("CREATE TABLE " + TABLE_EXTRAS + " ( "
+                    + EXTRAS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+                    + EXTRAS_ACCOUNTS_ID + " INTEGER, "
+                    + EXTRAS_KEY + " TEXT NOT NULL, "
+                    + EXTRAS_VALUE + " TEXT, "
+                    + "UNIQUE(" + EXTRAS_ACCOUNTS_ID + "," + EXTRAS_KEY + "))");
+
+            db.execSQL("CREATE TABLE " + TABLE_META + " ( "
+                    + META_KEY + " TEXT PRIMARY KEY NOT NULL, "
+                    + META_VALUE + " TEXT)");
+
+            createAccountsDeletionTrigger(db);
+        }
+
+        private void createAccountsDeletionTrigger(SQLiteDatabase db) {
+            db.execSQL(""
+                    + " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS
+                    + " BEGIN"
+                    + "   DELETE FROM " + TABLE_AUTHTOKENS
+                    + "     WHERE " + AUTHTOKENS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;"
+                    + "   DELETE FROM " + TABLE_EXTRAS
+                    + "     WHERE " + EXTRAS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;"
+                    + "   DELETE FROM " + TABLE_GRANTS
+                    + "     WHERE " + GRANTS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;"
+                    + " END");
+        }
+
+        private void createGrantsTable(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE " + TABLE_GRANTS + " (  "
+                    + GRANTS_ACCOUNTS_ID + " INTEGER NOT NULL, "
+                    + GRANTS_AUTH_TOKEN_TYPE + " STRING NOT NULL,  "
+                    + GRANTS_GRANTEE_UID + " INTEGER NOT NULL,  "
+                    + "UNIQUE (" + GRANTS_ACCOUNTS_ID + "," + GRANTS_AUTH_TOKEN_TYPE
+                    +   "," + GRANTS_GRANTEE_UID + "))");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            Log.e(TAG, "upgrade from version " + oldVersion + " to version " + newVersion);
+
+            if (oldVersion == 1) {
+                // no longer need to do anything since the work is done
+                // when upgrading from version 2
+                oldVersion++;
+            }
+
+            if (oldVersion == 2) {
+                createGrantsTable(db);
+                db.execSQL("DROP TRIGGER " + TABLE_ACCOUNTS + "Delete");
+                createAccountsDeletionTrigger(db);
+                oldVersion++;
+            }
+
+            if (oldVersion == 3) {
+                db.execSQL("UPDATE " + TABLE_ACCOUNTS + " SET " + ACCOUNTS_TYPE +
+                        " = 'com.google' WHERE " + ACCOUNTS_TYPE + " == 'com.google.GAIA'");
+                oldVersion++;
+            }
+        }
+
+        @Override
+        public void onOpen(SQLiteDatabase db) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "opened database " + DATABASE_NAME);
+        }
+    }
+
+    public IBinder onBind(Intent intent) {
+        return asBinder();
+    }
+
+    /**
+     * Searches array of arguments for the specified string
+     * @param args array of argument strings
+     * @param value value to search for
+     * @return true if the value is contained in the array
+     */
+    private static boolean scanArgs(String[] args, String value) {
+        if (args != null) {
+            for (String arg : args) {
+                if (value.equals(arg)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+            fout.println("Permission Denial: can't dump AccountsManager from from pid="
+                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+                    + " without permission " + android.Manifest.permission.DUMP);
+            return;
+        }
+        final boolean isCheckinRequest = scanArgs(args, "--checkin") || scanArgs(args, "-c");
+        final IndentingPrintWriter ipw = new IndentingPrintWriter(fout, "  ");
+
+        final List<UserInfo> users = getUserManager().getUsers();
+        for (UserInfo user : users) {
+            ipw.println("User " + user + ":");
+            ipw.increaseIndent();
+            dumpUser(getUserAccounts(user.id), fd, ipw, args, isCheckinRequest);
+            ipw.println();
+            ipw.decreaseIndent();
+        }
+    }
+
+    private void dumpUser(UserAccounts userAccounts, FileDescriptor fd, PrintWriter fout,
+            String[] args, boolean isCheckinRequest) {
+        synchronized (userAccounts.cacheLock) {
+            final SQLiteDatabase db = userAccounts.openHelper.getReadableDatabase();
+
+            if (isCheckinRequest) {
+                // This is a checkin request. *Only* upload the account types and the count of each.
+                Cursor cursor = db.query(TABLE_ACCOUNTS, ACCOUNT_TYPE_COUNT_PROJECTION,
+                        null, null, ACCOUNTS_TYPE, null, null);
+                try {
+                    while (cursor.moveToNext()) {
+                        // print type,count
+                        fout.println(cursor.getString(0) + "," + cursor.getString(1));
+                    }
+                } finally {
+                    if (cursor != null) {
+                        cursor.close();
+                    }
+                }
+            } else {
+                Account[] accounts = getAccountsFromCacheLocked(userAccounts, null /* type */);
+                fout.println("Accounts: " + accounts.length);
+                for (Account account : accounts) {
+                    fout.println("  " + account);
+                }
+
+                fout.println();
+                synchronized (mSessions) {
+                    final long now = SystemClock.elapsedRealtime();
+                    fout.println("Active Sessions: " + mSessions.size());
+                    for (Session session : mSessions.values()) {
+                        fout.println("  " + session.toDebugString(now));
+                    }
+                }
+
+                fout.println();
+                mAuthenticatorCache.dump(fd, fout, args, userAccounts.userId);
+            }
+        }
+    }
+
+    private void doNotification(UserAccounts accounts, Account account, CharSequence message,
+            Intent intent, int userId) {
+        long identityToken = clearCallingIdentity();
+        try {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "doNotification: " + message + " intent:" + intent);
+            }
+
+            if (intent.getComponent() != null &&
+                    GrantCredentialsPermissionActivity.class.getName().equals(
+                            intent.getComponent().getClassName())) {
+                createNoCredentialsPermissionNotification(account, intent, userId);
+            } else {
+                final Integer notificationId = getSigninRequiredNotificationId(accounts, account);
+                intent.addCategory(String.valueOf(notificationId));
+                Notification n = new Notification(android.R.drawable.stat_sys_warning, null,
+                        0 /* when */);
+                UserHandle user = new UserHandle(userId);
+                final String notificationTitleFormat =
+                        mContext.getText(R.string.notification_title).toString();
+                n.setLatestEventInfo(mContext,
+                        String.format(notificationTitleFormat, account.name),
+                        message, PendingIntent.getActivityAsUser(
+                        mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT,
+                        null, user));
+                installNotification(notificationId, n, user);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    protected void installNotification(final int notificationId, final Notification n,
+            UserHandle user) {
+        ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE))
+                .notifyAsUser(null, notificationId, n, user);
+    }
+
+    protected void cancelNotification(int id, UserHandle user) {
+        long identityToken = clearCallingIdentity();
+        try {
+            ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE))
+                .cancelAsUser(null, id, user);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    /** Succeeds if any of the specified permissions are granted. */
+    private void checkBinderPermission(String... permissions) {
+        final int uid = Binder.getCallingUid();
+
+        for (String perm : permissions) {
+            if (mContext.checkCallingOrSelfPermission(perm) == PackageManager.PERMISSION_GRANTED) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "  caller uid " + uid + " has " + perm);
+                }
+                return;
+            }
+        }
+
+        String msg = "caller uid " + uid + " lacks any of " + TextUtils.join(",", permissions);
+        Log.w(TAG, "  " + msg);
+        throw new SecurityException(msg);
+    }
+
+    private boolean inSystemImage(int callingUid) {
+        final int callingUserId = UserHandle.getUserId(callingUid);
+
+        final PackageManager userPackageManager;
+        try {
+            userPackageManager = mContext.createPackageContextAsUser(
+                    "android", 0, new UserHandle(callingUserId)).getPackageManager();
+        } catch (NameNotFoundException e) {
+            return false;
+        }
+
+        String[] packages = userPackageManager.getPackagesForUid(callingUid);
+        for (String name : packages) {
+            try {
+                PackageInfo packageInfo = userPackageManager.getPackageInfo(name, 0 /* flags */);
+                if (packageInfo != null
+                        && (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+                    return true;
+                }
+            } catch (PackageManager.NameNotFoundException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    private boolean permissionIsGranted(Account account, String authTokenType, int callerUid) {
+        final boolean inSystemImage = inSystemImage(callerUid);
+        final boolean fromAuthenticator = account != null
+                && hasAuthenticatorUid(account.type, callerUid);
+        final boolean hasExplicitGrants = account != null
+                && hasExplicitlyGrantedPermission(account, authTokenType, callerUid);
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "checkGrantsOrCallingUidAgainstAuthenticator: caller uid "
+                    + callerUid + ", " + account
+                    + ": is authenticator? " + fromAuthenticator
+                    + ", has explicit permission? " + hasExplicitGrants);
+        }
+        return fromAuthenticator || hasExplicitGrants || inSystemImage;
+    }
+
+    private boolean hasAuthenticatorUid(String accountType, int callingUid) {
+        final int callingUserId = UserHandle.getUserId(callingUid);
+        for (RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> serviceInfo :
+                mAuthenticatorCache.getAllServices(callingUserId)) {
+            if (serviceInfo.type.type.equals(accountType)) {
+                return (serviceInfo.uid == callingUid) ||
+                        (mPackageManager.checkSignatures(serviceInfo.uid, callingUid)
+                                == PackageManager.SIGNATURE_MATCH);
+            }
+        }
+        return false;
+    }
+
+    private boolean hasExplicitlyGrantedPermission(Account account, String authTokenType,
+            int callerUid) {
+        if (callerUid == android.os.Process.SYSTEM_UID) {
+            return true;
+        }
+        UserAccounts accounts = getUserAccountsForCaller();
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getReadableDatabase();
+            String[] args = { String.valueOf(callerUid), authTokenType,
+                    account.name, account.type};
+            final boolean permissionGranted =
+                    DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS, args) != 0;
+            if (!permissionGranted && ActivityManager.isRunningInTestHarness()) {
+                // TODO: Skip this check when running automated tests. Replace this
+                // with a more general solution.
+                Log.d(TAG, "no credentials permission for usage of " + account + ", "
+                        + authTokenType + " by uid " + callerUid
+                        + " but ignoring since device is in test harness.");
+                return true;
+            }
+            return permissionGranted;
+        }
+    }
+
+    private void checkCallingUidAgainstAuthenticator(Account account) {
+        final int uid = Binder.getCallingUid();
+        if (account == null || !hasAuthenticatorUid(account.type, uid)) {
+            String msg = "caller uid " + uid + " is different than the authenticator's uid";
+            Log.w(TAG, msg);
+            throw new SecurityException(msg);
+        }
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "caller uid " + uid + " is the same as the authenticator's uid");
+        }
+    }
+
+    private void checkAuthenticateAccountsPermission(Account account) {
+        checkBinderPermission(Manifest.permission.AUTHENTICATE_ACCOUNTS);
+        checkCallingUidAgainstAuthenticator(account);
+    }
+
+    private void checkReadAccountsPermission() {
+        checkBinderPermission(Manifest.permission.GET_ACCOUNTS);
+    }
+
+    private void checkManageAccountsPermission() {
+        checkBinderPermission(Manifest.permission.MANAGE_ACCOUNTS);
+    }
+
+    private void checkManageAccountsOrUseCredentialsPermissions() {
+        checkBinderPermission(Manifest.permission.MANAGE_ACCOUNTS,
+                Manifest.permission.USE_CREDENTIALS);
+    }
+
+    public void updateAppPermission(Account account, String authTokenType, int uid, boolean value)
+            throws RemoteException {
+        final int callingUid = getCallingUid();
+
+        if (callingUid != android.os.Process.SYSTEM_UID) {
+            throw new SecurityException();
+        }
+
+        if (value) {
+            grantAppPermission(account, authTokenType, uid);
+        } else {
+            revokeAppPermission(account, authTokenType, uid);
+        }
+    }
+
+    /**
+     * Allow callers with the given uid permission to get credentials for account/authTokenType.
+     * <p>
+     * Although this is public it can only be accessed via the AccountManagerService object
+     * which is in the system. This means we don't need to protect it with permissions.
+     * @hide
+     */
+    private void grantAppPermission(Account account, String authTokenType, int uid) {
+        if (account == null || authTokenType == null) {
+            Log.e(TAG, "grantAppPermission: called with invalid arguments", new Exception());
+            return;
+        }
+        UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+            db.beginTransaction();
+            try {
+                long accountId = getAccountIdLocked(db, account);
+                if (accountId >= 0) {
+                    ContentValues values = new ContentValues();
+                    values.put(GRANTS_ACCOUNTS_ID, accountId);
+                    values.put(GRANTS_AUTH_TOKEN_TYPE, authTokenType);
+                    values.put(GRANTS_GRANTEE_UID, uid);
+                    db.insert(TABLE_GRANTS, GRANTS_ACCOUNTS_ID, values);
+                    db.setTransactionSuccessful();
+                }
+            } finally {
+                db.endTransaction();
+            }
+            cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid),
+                    new UserHandle(accounts.userId));
+        }
+    }
+
+    /**
+     * Don't allow callers with the given uid permission to get credentials for
+     * account/authTokenType.
+     * <p>
+     * Although this is public it can only be accessed via the AccountManagerService object
+     * which is in the system. This means we don't need to protect it with permissions.
+     * @hide
+     */
+    private void revokeAppPermission(Account account, String authTokenType, int uid) {
+        if (account == null || authTokenType == null) {
+            Log.e(TAG, "revokeAppPermission: called with invalid arguments", new Exception());
+            return;
+        }
+        UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
+        synchronized (accounts.cacheLock) {
+            final SQLiteDatabase db = accounts.openHelper.getWritableDatabase();
+            db.beginTransaction();
+            try {
+                long accountId = getAccountIdLocked(db, account);
+                if (accountId >= 0) {
+                    db.delete(TABLE_GRANTS,
+                            GRANTS_ACCOUNTS_ID + "=? AND " + GRANTS_AUTH_TOKEN_TYPE + "=? AND "
+                                    + GRANTS_GRANTEE_UID + "=?",
+                            new String[]{String.valueOf(accountId), authTokenType,
+                                    String.valueOf(uid)});
+                    db.setTransactionSuccessful();
+                }
+            } finally {
+                db.endTransaction();
+            }
+            cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid),
+                    new UserHandle(accounts.userId));
+        }
+    }
+
+    static final private String stringArrayToString(String[] value) {
+        return value != null ? ("[" + TextUtils.join(",", value) + "]") : null;
+    }
+
+    private void removeAccountFromCacheLocked(UserAccounts accounts, Account account) {
+        final Account[] oldAccountsForType = accounts.accountCache.get(account.type);
+        if (oldAccountsForType != null) {
+            ArrayList<Account> newAccountsList = new ArrayList<Account>();
+            for (Account curAccount : oldAccountsForType) {
+                if (!curAccount.equals(account)) {
+                    newAccountsList.add(curAccount);
+                }
+            }
+            if (newAccountsList.isEmpty()) {
+                accounts.accountCache.remove(account.type);
+            } else {
+                Account[] newAccountsForType = new Account[newAccountsList.size()];
+                newAccountsForType = newAccountsList.toArray(newAccountsForType);
+                accounts.accountCache.put(account.type, newAccountsForType);
+            }
+        }
+        accounts.userDataCache.remove(account);
+        accounts.authTokenCache.remove(account);
+    }
+
+    /**
+     * This assumes that the caller has already checked that the account is not already present.
+     */
+    private void insertAccountIntoCacheLocked(UserAccounts accounts, Account account) {
+        Account[] accountsForType = accounts.accountCache.get(account.type);
+        int oldLength = (accountsForType != null) ? accountsForType.length : 0;
+        Account[] newAccountsForType = new Account[oldLength + 1];
+        if (accountsForType != null) {
+            System.arraycopy(accountsForType, 0, newAccountsForType, 0, oldLength);
+        }
+        newAccountsForType[oldLength] = account;
+        accounts.accountCache.put(account.type, newAccountsForType);
+    }
+
+    protected Account[] getAccountsFromCacheLocked(UserAccounts userAccounts, String accountType) {
+        if (accountType != null) {
+            final Account[] accounts = userAccounts.accountCache.get(accountType);
+            if (accounts == null) {
+                return EMPTY_ACCOUNT_ARRAY;
+            } else {
+                return Arrays.copyOf(accounts, accounts.length);
+            }
+        } else {
+            int totalLength = 0;
+            for (Account[] accounts : userAccounts.accountCache.values()) {
+                totalLength += accounts.length;
+            }
+            if (totalLength == 0) {
+                return EMPTY_ACCOUNT_ARRAY;
+            }
+            Account[] accounts = new Account[totalLength];
+            totalLength = 0;
+            for (Account[] accountsOfType : userAccounts.accountCache.values()) {
+                System.arraycopy(accountsOfType, 0, accounts, totalLength,
+                        accountsOfType.length);
+                totalLength += accountsOfType.length;
+            }
+            return accounts;
+        }
+    }
+
+    protected void writeUserDataIntoCacheLocked(UserAccounts accounts, final SQLiteDatabase db,
+            Account account, String key, String value) {
+        HashMap<String, String> userDataForAccount = accounts.userDataCache.get(account);
+        if (userDataForAccount == null) {
+            userDataForAccount = readUserDataForAccountFromDatabaseLocked(db, account);
+            accounts.userDataCache.put(account, userDataForAccount);
+        }
+        if (value == null) {
+            userDataForAccount.remove(key);
+        } else {
+            userDataForAccount.put(key, value);
+        }
+    }
+
+    protected void writeAuthTokenIntoCacheLocked(UserAccounts accounts, final SQLiteDatabase db,
+            Account account, String key, String value) {
+        HashMap<String, String> authTokensForAccount = accounts.authTokenCache.get(account);
+        if (authTokensForAccount == null) {
+            authTokensForAccount = readAuthTokensForAccountFromDatabaseLocked(db, account);
+            accounts.authTokenCache.put(account, authTokensForAccount);
+        }
+        if (value == null) {
+            authTokensForAccount.remove(key);
+        } else {
+            authTokensForAccount.put(key, value);
+        }
+    }
+
+    protected String readAuthTokenInternal(UserAccounts accounts, Account account,
+            String authTokenType) {
+        synchronized (accounts.cacheLock) {
+            HashMap<String, String> authTokensForAccount = accounts.authTokenCache.get(account);
+            if (authTokensForAccount == null) {
+                // need to populate the cache for this account
+                final SQLiteDatabase db = accounts.openHelper.getReadableDatabase();
+                authTokensForAccount = readAuthTokensForAccountFromDatabaseLocked(db, account);
+                accounts.authTokenCache.put(account, authTokensForAccount);
+            }
+            return authTokensForAccount.get(authTokenType);
+        }
+    }
+
+    protected String readUserDataInternal(UserAccounts accounts, Account account, String key) {
+        synchronized (accounts.cacheLock) {
+            HashMap<String, String> userDataForAccount = accounts.userDataCache.get(account);
+            if (userDataForAccount == null) {
+                // need to populate the cache for this account
+                final SQLiteDatabase db = accounts.openHelper.getReadableDatabase();
+                userDataForAccount = readUserDataForAccountFromDatabaseLocked(db, account);
+                accounts.userDataCache.put(account, userDataForAccount);
+            }
+            return userDataForAccount.get(key);
+        }
+    }
+
+    protected HashMap<String, String> readUserDataForAccountFromDatabaseLocked(
+            final SQLiteDatabase db, Account account) {
+        HashMap<String, String> userDataForAccount = new HashMap<String, String>();
+        Cursor cursor = db.query(TABLE_EXTRAS,
+                COLUMNS_EXTRAS_KEY_AND_VALUE,
+                SELECTION_USERDATA_BY_ACCOUNT,
+                new String[]{account.name, account.type},
+                null, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                final String tmpkey = cursor.getString(0);
+                final String value = cursor.getString(1);
+                userDataForAccount.put(tmpkey, value);
+            }
+        } finally {
+            cursor.close();
+        }
+        return userDataForAccount;
+    }
+
+    protected HashMap<String, String> readAuthTokensForAccountFromDatabaseLocked(
+            final SQLiteDatabase db, Account account) {
+        HashMap<String, String> authTokensForAccount = new HashMap<String, String>();
+        Cursor cursor = db.query(TABLE_AUTHTOKENS,
+                COLUMNS_AUTHTOKENS_TYPE_AND_AUTHTOKEN,
+                SELECTION_AUTHTOKENS_BY_ACCOUNT,
+                new String[]{account.name, account.type},
+                null, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                final String type = cursor.getString(0);
+                final String authToken = cursor.getString(1);
+                authTokensForAccount.put(type, authToken);
+            }
+        } finally {
+            cursor.close();
+        }
+        return authTokensForAccount;
+    }
+}
diff --git a/services/java/com/android/server/accounts/IAccountAuthenticatorCache.java b/services/java/com/android/server/accounts/IAccountAuthenticatorCache.java
new file mode 100644
index 0000000..bb09687
--- /dev/null
+++ b/services/java/com/android/server/accounts/IAccountAuthenticatorCache.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010 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 com.android.server.accounts;
+
+import android.accounts.AuthenticatorDescription;
+import android.content.pm.RegisteredServicesCache;
+import android.content.pm.RegisteredServicesCacheListener;
+import android.os.Handler;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Collection;
+
+/**
+ * An interface to the Authenticator specialization of RegisteredServicesCache. The use of
+ * this interface by the AccountManagerService makes it easier to unit test it.
+ * @hide
+ */
+public interface IAccountAuthenticatorCache {
+    /**
+     * Accessor for the {@link android.content.pm.RegisteredServicesCache.ServiceInfo} that
+     * matched the specified {@link android.accounts.AuthenticatorDescription} or null
+     * if none match.
+     * @param type the authenticator type to return
+     * @return the {@link android.content.pm.RegisteredServicesCache.ServiceInfo} that
+     * matches the account type or null if none is present
+     */
+    RegisteredServicesCache.ServiceInfo<AuthenticatorDescription> getServiceInfo(
+            AuthenticatorDescription type, int userId);
+
+    /**
+     * @return A copy of a Collection of all the current Authenticators.
+     */
+    Collection<RegisteredServicesCache.ServiceInfo<AuthenticatorDescription>> getAllServices(
+            int userId);
+
+    /**
+     * Dumps the state of the cache. See
+     * {@link android.os.Binder#dump(java.io.FileDescriptor, java.io.PrintWriter, String[])}
+     */
+    void dump(FileDescriptor fd, PrintWriter fout, String[] args, int userId);
+
+    /**
+     * Sets a listener that will be notified whenever the authenticator set changes
+     * @param listener the listener to notify, or null
+     * @param handler the {@link Handler} on which the notification will be posted. If null
+     * the notification will be posted on the main thread.
+     */
+    void setListener(RegisteredServicesCacheListener<AuthenticatorDescription> listener,
+            Handler handler);
+
+    void invalidateCache(int userId);
+}
diff --git a/services/java/com/android/server/content/ContentService.java b/services/java/com/android/server/content/ContentService.java
new file mode 100644
index 0000000..3b92338
--- /dev/null
+++ b/services/java/com/android/server/content/ContentService.java
@@ -0,0 +1,848 @@
+/*
+ * Copyright (C) 2006 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 com.android.server.content;
+
+import android.Manifest;
+import android.accounts.Account;
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.IContentService;
+import android.content.ISyncStatusObserver;
+import android.content.PeriodicSync;
+import android.content.SyncAdapterType;
+import android.content.SyncInfo;
+import android.content.SyncStatusInfo;
+import android.database.IContentObserver;
+import android.database.sqlite.SQLiteException;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * {@hide}
+ */
+public final class ContentService extends IContentService.Stub {
+    private static final String TAG = "ContentService";
+    private Context mContext;
+    private boolean mFactoryTest;
+    private final ObserverNode mRootNode = new ObserverNode("");
+    private SyncManager mSyncManager = null;
+    private final Object mSyncManagerLock = new Object();
+
+    private SyncManager getSyncManager() {
+        synchronized(mSyncManagerLock) {
+            try {
+                // Try to create the SyncManager, return null if it fails (e.g. the disk is full).
+                if (mSyncManager == null) mSyncManager = new SyncManager(mContext, mFactoryTest);
+            } catch (SQLiteException e) {
+                Log.e(TAG, "Can't create SyncManager", e);
+            }
+            return mSyncManager;
+        }
+    }
+
+    @Override
+    protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.DUMP,
+                "caller doesn't have the DUMP permission");
+
+        // This makes it so that future permission checks will be in the context of this
+        // process rather than the caller's process. We will restore this before returning.
+        long identityToken = clearCallingIdentity();
+        try {
+            if (mSyncManager == null) {
+                pw.println("No SyncManager created!  (Disk full?)");
+            } else {
+                mSyncManager.dump(fd, pw);
+            }
+            pw.println();
+            pw.println("Observer tree:");
+            synchronized (mRootNode) {
+                int[] counts = new int[2];
+                final SparseIntArray pidCounts = new SparseIntArray();
+                mRootNode.dumpLocked(fd, pw, args, "", "  ", counts, pidCounts);
+                pw.println();
+                ArrayList<Integer> sorted = new ArrayList<Integer>();
+                for (int i=0; i<pidCounts.size(); i++) {
+                    sorted.add(pidCounts.keyAt(i));
+                }
+                Collections.sort(sorted, new Comparator<Integer>() {
+                    @Override
+                    public int compare(Integer lhs, Integer rhs) {
+                        int lc = pidCounts.get(lhs);
+                        int rc = pidCounts.get(rhs);
+                        if (lc < rc) {
+                            return 1;
+                        } else if (lc > rc) {
+                            return -1;
+                        }
+                        return 0;
+                    }
+
+                });
+                for (int i=0; i<sorted.size(); i++) {
+                    int pid = sorted.get(i);
+                    pw.print("  pid "); pw.print(pid); pw.print(": ");
+                            pw.print(pidCounts.get(pid)); pw.println(" observers");
+                }
+                pw.println();
+                pw.print(" Total number of nodes: "); pw.println(counts[0]);
+                pw.print(" Total number of observers: "); pw.println(counts[1]);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    @Override
+    public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+            throws RemoteException {
+        try {
+            return super.onTransact(code, data, reply, flags);
+        } catch (RuntimeException e) {
+            // The content service only throws security exceptions, so let's
+            // log all others.
+            if (!(e instanceof SecurityException)) {
+                Log.e(TAG, "Content Service Crash", e);
+            }
+            throw e;
+        }
+    }
+
+    /*package*/ ContentService(Context context, boolean factoryTest) {
+        mContext = context;
+        mFactoryTest = factoryTest;
+    }
+
+    public void systemReady() {
+        getSyncManager();
+    }
+
+    /**
+     * Register a content observer tied to a specific user's view of the provider.
+     * @param userHandle the user whose view of the provider is to be observed.  May be
+     *     the calling user without requiring any permission, otherwise the caller needs to
+     *     hold the INTERACT_ACROSS_USERS_FULL permission.  Pseudousers USER_ALL and
+     *     USER_CURRENT are properly handled; all other pseudousers are forbidden.
+     */
+    @Override
+    public void registerContentObserver(Uri uri, boolean notifyForDescendants,
+            IContentObserver observer, int userHandle) {
+        if (observer == null || uri == null) {
+            throw new IllegalArgumentException("You must pass a valid uri and observer");
+        }
+
+        final int callingUser = UserHandle.getCallingUserId();
+        if (callingUser != userHandle) {
+            mContext.enforceCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                    "no permission to observe other users' provider view");
+        }
+
+        if (userHandle < 0) {
+            if (userHandle == UserHandle.USER_CURRENT) {
+                userHandle = ActivityManager.getCurrentUser();
+            } else if (userHandle != UserHandle.USER_ALL) {
+                throw new InvalidParameterException("Bad user handle for registerContentObserver: "
+                        + userHandle);
+            }
+        }
+
+        synchronized (mRootNode) {
+            mRootNode.addObserverLocked(uri, observer, notifyForDescendants, mRootNode,
+                    Binder.getCallingUid(), Binder.getCallingPid(), userHandle);
+            if (false) Log.v(TAG, "Registered observer " + observer + " at " + uri +
+                    " with notifyForDescendants " + notifyForDescendants);
+        }
+    }
+
+    public void registerContentObserver(Uri uri, boolean notifyForDescendants,
+            IContentObserver observer) {
+        registerContentObserver(uri, notifyForDescendants, observer,
+                UserHandle.getCallingUserId());
+    }
+
+    public void unregisterContentObserver(IContentObserver observer) {
+        if (observer == null) {
+            throw new IllegalArgumentException("You must pass a valid observer");
+        }
+        synchronized (mRootNode) {
+            mRootNode.removeObserverLocked(observer);
+            if (false) Log.v(TAG, "Unregistered observer " + observer);
+        }
+    }
+
+    /**
+     * Notify observers of a particular user's view of the provider.
+     * @param userHandle the user whose view of the provider is to be notified.  May be
+     *     the calling user without requiring any permission, otherwise the caller needs to
+     *     hold the INTERACT_ACROSS_USERS_FULL permission.  Pseudousers USER_ALL and
+     *     USER_CURRENT are properly interpreted; no other pseudousers are allowed.
+     */
+    @Override
+    public void notifyChange(Uri uri, IContentObserver observer,
+            boolean observerWantsSelfNotifications, boolean syncToNetwork,
+            int userHandle) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "Notifying update of " + uri + " for user " + userHandle
+                    + " from observer " + observer + ", syncToNetwork " + syncToNetwork);
+        }
+
+        // Notify for any user other than the caller's own requires permission.
+        final int callingUserHandle = UserHandle.getCallingUserId();
+        if (userHandle != callingUserHandle) {
+            mContext.enforceCallingOrSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                    "no permission to notify other users");
+        }
+
+        // We passed the permission check; resolve pseudouser targets as appropriate
+        if (userHandle < 0) {
+            if (userHandle == UserHandle.USER_CURRENT) {
+                userHandle = ActivityManager.getCurrentUser();
+            } else if (userHandle != UserHandle.USER_ALL) {
+                throw new InvalidParameterException("Bad user handle for notifyChange: "
+                        + userHandle);
+            }
+        }
+
+        final int uid = Binder.getCallingUid();
+        // This makes it so that future permission checks will be in the context of this
+        // process rather than the caller's process. We will restore this before returning.
+        long identityToken = clearCallingIdentity();
+        try {
+            ArrayList<ObserverCall> calls = new ArrayList<ObserverCall>();
+            synchronized (mRootNode) {
+                mRootNode.collectObserversLocked(uri, 0, observer, observerWantsSelfNotifications,
+                        userHandle, calls);
+            }
+            final int numCalls = calls.size();
+            for (int i=0; i<numCalls; i++) {
+                ObserverCall oc = calls.get(i);
+                try {
+                    oc.mObserver.onChange(oc.mSelfChange, uri);
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "Notified " + oc.mObserver + " of " + "update at " + uri);
+                    }
+                } catch (RemoteException ex) {
+                    synchronized (mRootNode) {
+                        Log.w(TAG, "Found dead observer, removing");
+                        IBinder binder = oc.mObserver.asBinder();
+                        final ArrayList<ObserverNode.ObserverEntry> list
+                                = oc.mNode.mObservers;
+                        int numList = list.size();
+                        for (int j=0; j<numList; j++) {
+                            ObserverNode.ObserverEntry oe = list.get(j);
+                            if (oe.observer.asBinder() == binder) {
+                                list.remove(j);
+                                j--;
+                                numList--;
+                            }
+                        }
+                    }
+                }
+            }
+            if (syncToNetwork) {
+                SyncManager syncManager = getSyncManager();
+                if (syncManager != null) {
+                    syncManager.scheduleLocalSync(null /* all accounts */, callingUserHandle, uid,
+                            uri.getAuthority());
+                }
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void notifyChange(Uri uri, IContentObserver observer,
+            boolean observerWantsSelfNotifications, boolean syncToNetwork) {
+        notifyChange(uri, observer, observerWantsSelfNotifications, syncToNetwork,
+                UserHandle.getCallingUserId());
+    }
+
+    /**
+     * Hide this class since it is not part of api,
+     * but current unittest framework requires it to be public
+     * @hide
+     *
+     */
+    public static final class ObserverCall {
+        final ObserverNode mNode;
+        final IContentObserver mObserver;
+        final boolean mSelfChange;
+
+        ObserverCall(ObserverNode node, IContentObserver observer, boolean selfChange) {
+            mNode = node;
+            mObserver = observer;
+            mSelfChange = selfChange;
+        }
+    }
+
+    public void requestSync(Account account, String authority, Bundle extras) {
+        ContentResolver.validateSyncExtrasBundle(extras);
+        int userId = UserHandle.getCallingUserId();
+        int uId = Binder.getCallingUid();
+
+        // This makes it so that future permission checks will be in the context of this
+        // process rather than the caller's process. We will restore this before returning.
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                syncManager.scheduleSync(account, userId, uId, authority, extras, 0 /* no delay */,
+                        false /* onlyThoseWithUnkownSyncableState */);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    /**
+     * Clear all scheduled sync operations that match the uri and cancel the active sync
+     * if they match the authority and account, if they are present.
+     * @param account filter the pending and active syncs to cancel using this account
+     * @param authority filter the pending and active syncs to cancel using this authority
+     */
+    public void cancelSync(Account account, String authority) {
+        int userId = UserHandle.getCallingUserId();
+
+        // This makes it so that future permission checks will be in the context of this
+        // process rather than the caller's process. We will restore this before returning.
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                syncManager.clearScheduledSyncOperations(account, userId, authority);
+                syncManager.cancelActiveSync(account, userId, authority);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    /**
+     * Get information about the SyncAdapters that are known to the system.
+     * @return an array of SyncAdapters that have registered with the system
+     */
+    public SyncAdapterType[] getSyncAdapterTypes() {
+        // This makes it so that future permission checks will be in the context of this
+        // process rather than the caller's process. We will restore this before returning.
+        final int userId = UserHandle.getCallingUserId();
+        final long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            return syncManager.getSyncAdapterTypes(userId);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public boolean getSyncAutomatically(Account account, String providerName) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
+                "no permission to read the sync settings");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().getSyncAutomatically(
+                        account, userId, providerName);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return false;
+    }
+
+    public void setSyncAutomatically(Account account, String providerName, boolean sync) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+                "no permission to write the sync settings");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                syncManager.getSyncStorageEngine().setSyncAutomatically(
+                        account, userId, providerName, sync);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void addPeriodicSync(Account account, String authority, Bundle extras,
+            long pollFrequency) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+                "no permission to write the sync settings");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            getSyncManager().getSyncStorageEngine().addPeriodicSync(
+                    account, userId, authority, extras, pollFrequency);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void removePeriodicSync(Account account, String authority, Bundle extras) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+                "no permission to write the sync settings");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            getSyncManager().getSyncStorageEngine().removePeriodicSync(account, userId, authority,
+                    extras);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public List<PeriodicSync> getPeriodicSyncs(Account account, String providerName) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
+                "no permission to read the sync settings");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            return getSyncManager().getSyncStorageEngine().getPeriodicSyncs(
+                    account, userId, providerName);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public int getIsSyncable(Account account, String providerName) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
+                "no permission to read the sync settings");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().getIsSyncable(
+                        account, userId, providerName);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return -1;
+    }
+
+    public void setIsSyncable(Account account, String providerName, int syncable) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+                "no permission to write the sync settings");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                syncManager.getSyncStorageEngine().setIsSyncable(
+                        account, userId, providerName, syncable);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public boolean getMasterSyncAutomatically() {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS,
+                "no permission to read the sync settings");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().getMasterSyncAutomatically(userId);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return false;
+    }
+
+    public void setMasterSyncAutomatically(boolean flag) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+                "no permission to write the sync settings");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                syncManager.getSyncStorageEngine().setMasterSyncAutomatically(flag, userId);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public boolean isSyncActive(Account account, String authority) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
+                "no permission to read the sync stats");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().isSyncActive(
+                        account, userId, authority);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return false;
+    }
+
+    public List<SyncInfo> getCurrentSyncs() {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
+                "no permission to read the sync stats");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            return getSyncManager().getSyncStorageEngine().getCurrentSyncs(userId);
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public SyncStatusInfo getSyncStatus(Account account, String authority) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
+                "no permission to read the sync stats");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().getStatusByAccountAndAuthority(
+                        account, userId, authority);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return null;
+    }
+
+    public boolean isSyncPending(Account account, String authority) {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS,
+                "no permission to read the sync stats");
+        int userId = UserHandle.getCallingUserId();
+
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null) {
+                return syncManager.getSyncStorageEngine().isSyncPending(account, userId, authority);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+        return false;
+    }
+
+    public void addStatusChangeListener(int mask, ISyncStatusObserver callback) {
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null && callback != null) {
+                syncManager.getSyncStorageEngine().addStatusChangeListener(mask, callback);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public void removeStatusChangeListener(ISyncStatusObserver callback) {
+        long identityToken = clearCallingIdentity();
+        try {
+            SyncManager syncManager = getSyncManager();
+            if (syncManager != null && callback != null) {
+                syncManager.getSyncStorageEngine().removeStatusChangeListener(callback);
+            }
+        } finally {
+            restoreCallingIdentity(identityToken);
+        }
+    }
+
+    public static ContentService main(Context context, boolean factoryTest) {
+        ContentService service = new ContentService(context, factoryTest);
+        ServiceManager.addService(ContentResolver.CONTENT_SERVICE_NAME, service);
+        return service;
+    }
+
+    /**
+     * Hide this class since it is not part of api,
+     * but current unittest framework requires it to be public
+     * @hide
+     */
+    public static final class ObserverNode {
+        private class ObserverEntry implements IBinder.DeathRecipient {
+            public final IContentObserver observer;
+            public final int uid;
+            public final int pid;
+            public final boolean notifyForDescendants;
+            private final int userHandle;
+            private final Object observersLock;
+
+            public ObserverEntry(IContentObserver o, boolean n, Object observersLock,
+                    int _uid, int _pid, int _userHandle) {
+                this.observersLock = observersLock;
+                observer = o;
+                uid = _uid;
+                pid = _pid;
+                userHandle = _userHandle;
+                notifyForDescendants = n;
+                try {
+                    observer.asBinder().linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    binderDied();
+                }
+            }
+
+            public void binderDied() {
+                synchronized (observersLock) {
+                    removeObserverLocked(observer);
+                }
+            }
+
+            public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args,
+                    String name, String prefix, SparseIntArray pidCounts) {
+                pidCounts.put(pid, pidCounts.get(pid)+1);
+                pw.print(prefix); pw.print(name); pw.print(": pid=");
+                        pw.print(pid); pw.print(" uid=");
+                        pw.print(uid); pw.print(" user=");
+                        pw.print(userHandle); pw.print(" target=");
+                        pw.println(Integer.toHexString(System.identityHashCode(
+                                observer != null ? observer.asBinder() : null)));
+            }
+        }
+
+        public static final int INSERT_TYPE = 0;
+        public static final int UPDATE_TYPE = 1;
+        public static final int DELETE_TYPE = 2;
+
+        private String mName;
+        private ArrayList<ObserverNode> mChildren = new ArrayList<ObserverNode>();
+        private ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>();
+
+        public ObserverNode(String name) {
+            mName = name;
+        }
+
+        public void dumpLocked(FileDescriptor fd, PrintWriter pw, String[] args,
+                String name, String prefix, int[] counts, SparseIntArray pidCounts) {
+            String innerName = null;
+            if (mObservers.size() > 0) {
+                if ("".equals(name)) {
+                    innerName = mName;
+                } else {
+                    innerName = name + "/" + mName;
+                }
+                for (int i=0; i<mObservers.size(); i++) {
+                    counts[1]++;
+                    mObservers.get(i).dumpLocked(fd, pw, args, innerName, prefix,
+                            pidCounts);
+                }
+            }
+            if (mChildren.size() > 0) {
+                if (innerName == null) {
+                    if ("".equals(name)) {
+                        innerName = mName;
+                    } else {
+                        innerName = name + "/" + mName;
+                    }
+                }
+                for (int i=0; i<mChildren.size(); i++) {
+                    counts[0]++;
+                    mChildren.get(i).dumpLocked(fd, pw, args, innerName, prefix,
+                            counts, pidCounts);
+                }
+            }
+        }
+
+        private String getUriSegment(Uri uri, int index) {
+            if (uri != null) {
+                if (index == 0) {
+                    return uri.getAuthority();
+                } else {
+                    return uri.getPathSegments().get(index - 1);
+                }
+            } else {
+                return null;
+            }
+        }
+
+        private int countUriSegments(Uri uri) {
+            if (uri == null) {
+                return 0;
+            }
+            return uri.getPathSegments().size() + 1;
+        }
+
+        // Invariant:  userHandle is either a hard user number or is USER_ALL
+        public void addObserverLocked(Uri uri, IContentObserver observer,
+                boolean notifyForDescendants, Object observersLock,
+                int uid, int pid, int userHandle) {
+            addObserverLocked(uri, 0, observer, notifyForDescendants, observersLock,
+                    uid, pid, userHandle);
+        }
+
+        private void addObserverLocked(Uri uri, int index, IContentObserver observer,
+                boolean notifyForDescendants, Object observersLock,
+                int uid, int pid, int userHandle) {
+            // If this is the leaf node add the observer
+            if (index == countUriSegments(uri)) {
+                mObservers.add(new ObserverEntry(observer, notifyForDescendants, observersLock,
+                        uid, pid, userHandle));
+                return;
+            }
+
+            // Look to see if the proper child already exists
+            String segment = getUriSegment(uri, index);
+            if (segment == null) {
+                throw new IllegalArgumentException("Invalid Uri (" + uri + ") used for observer");
+            }
+            int N = mChildren.size();
+            for (int i = 0; i < N; i++) {
+                ObserverNode node = mChildren.get(i);
+                if (node.mName.equals(segment)) {
+                    node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,
+                            observersLock, uid, pid, userHandle);
+                    return;
+                }
+            }
+
+            // No child found, create one
+            ObserverNode node = new ObserverNode(segment);
+            mChildren.add(node);
+            node.addObserverLocked(uri, index + 1, observer, notifyForDescendants,
+                    observersLock, uid, pid, userHandle);
+        }
+
+        public boolean removeObserverLocked(IContentObserver observer) {
+            int size = mChildren.size();
+            for (int i = 0; i < size; i++) {
+                boolean empty = mChildren.get(i).removeObserverLocked(observer);
+                if (empty) {
+                    mChildren.remove(i);
+                    i--;
+                    size--;
+                }
+            }
+
+            IBinder observerBinder = observer.asBinder();
+            size = mObservers.size();
+            for (int i = 0; i < size; i++) {
+                ObserverEntry entry = mObservers.get(i);
+                if (entry.observer.asBinder() == observerBinder) {
+                    mObservers.remove(i);
+                    // We no longer need to listen for death notifications. Remove it.
+                    observerBinder.unlinkToDeath(entry, 0);
+                    break;
+                }
+            }
+
+            if (mChildren.size() == 0 && mObservers.size() == 0) {
+                return true;
+            }
+            return false;
+        }
+
+        private void collectMyObserversLocked(boolean leaf, IContentObserver observer,
+                boolean observerWantsSelfNotifications, int targetUserHandle,
+                ArrayList<ObserverCall> calls) {
+            int N = mObservers.size();
+            IBinder observerBinder = observer == null ? null : observer.asBinder();
+            for (int i = 0; i < N; i++) {
+                ObserverEntry entry = mObservers.get(i);
+
+                // Don't notify the observer if it sent the notification and isn't interested
+                // in self notifications
+                boolean selfChange = (entry.observer.asBinder() == observerBinder);
+                if (selfChange && !observerWantsSelfNotifications) {
+                    continue;
+                }
+
+                // Does this observer match the target user?
+                if (targetUserHandle == UserHandle.USER_ALL
+                        || entry.userHandle == UserHandle.USER_ALL
+                        || targetUserHandle == entry.userHandle) {
+                    // Make sure the observer is interested in the notification
+                    if (leaf || (!leaf && entry.notifyForDescendants)) {
+                        calls.add(new ObserverCall(this, entry.observer, selfChange));
+                    }
+                }
+            }
+        }
+
+        /**
+         * targetUserHandle is either a hard user handle or is USER_ALL
+         */
+        public void collectObserversLocked(Uri uri, int index, IContentObserver observer,
+                boolean observerWantsSelfNotifications, int targetUserHandle,
+                ArrayList<ObserverCall> calls) {
+            String segment = null;
+            int segmentCount = countUriSegments(uri);
+            if (index >= segmentCount) {
+                // This is the leaf node, notify all observers
+                collectMyObserversLocked(true, observer, observerWantsSelfNotifications,
+                        targetUserHandle, calls);
+            } else if (index < segmentCount){
+                segment = getUriSegment(uri, index);
+                // Notify any observers at this level who are interested in descendants
+                collectMyObserversLocked(false, observer, observerWantsSelfNotifications,
+                        targetUserHandle, calls);
+            }
+
+            int N = mChildren.size();
+            for (int i = 0; i < N; i++) {
+                ObserverNode node = mChildren.get(i);
+                if (segment == null || node.mName.equals(segment)) {
+                    // We found the child,
+                    node.collectObserversLocked(uri, index + 1,
+                            observer, observerWantsSelfNotifications, targetUserHandle, calls);
+                    if (segment != null) {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/services/java/com/android/server/content/SyncManager.java b/services/java/com/android/server/content/SyncManager.java
new file mode 100644
index 0000000..cd66cf2
--- /dev/null
+++ b/services/java/com/android/server/content/SyncManager.java
@@ -0,0 +1,2786 @@
+/*
+ * Copyright (C) 2008 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 com.android.server.content;
+
+import android.accounts.Account;
+import android.accounts.AccountAndUser;
+import android.accounts.AccountManager;
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ISyncAdapter;
+import android.content.ISyncContext;
+import android.content.ISyncStatusObserver;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.SyncActivityTooManyDeletes;
+import android.content.SyncAdapterType;
+import android.content.SyncAdaptersCache;
+import android.content.SyncInfo;
+import android.content.SyncResult;
+import android.content.SyncStatusInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.RegisteredServicesCache;
+import android.content.pm.RegisteredServicesCacheListener;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.WorkSource;
+import android.provider.Settings;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.accounts.AccountManagerService;
+import com.android.server.content.SyncStorageEngine.OnSyncRequestListener;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
+
+import java.io.FileDescriptor;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * @hide
+ */
+public class SyncManager {
+    private static final String TAG = "SyncManager";
+
+    /** Delay a sync due to local changes this long. In milliseconds */
+    private static final long LOCAL_SYNC_DELAY;
+
+    /**
+     * If a sync takes longer than this and the sync queue is not empty then we will
+     * cancel it and add it back to the end of the sync queue. In milliseconds.
+     */
+    private static final long MAX_TIME_PER_SYNC;
+
+    static {
+        final boolean isLargeRAM = ActivityManager.isLargeRAM();
+        int defaultMaxInitSyncs = isLargeRAM ? 5 : 2;
+        int defaultMaxRegularSyncs = isLargeRAM ? 2 : 1;
+        MAX_SIMULTANEOUS_INITIALIZATION_SYNCS =
+                SystemProperties.getInt("sync.max_init_syncs", defaultMaxInitSyncs);
+        MAX_SIMULTANEOUS_REGULAR_SYNCS =
+                SystemProperties.getInt("sync.max_regular_syncs", defaultMaxRegularSyncs);
+        LOCAL_SYNC_DELAY =
+                SystemProperties.getLong("sync.local_sync_delay", 30 * 1000 /* 30 seconds */);
+        MAX_TIME_PER_SYNC =
+                SystemProperties.getLong("sync.max_time_per_sync", 5 * 60 * 1000 /* 5 minutes */);
+        SYNC_NOTIFICATION_DELAY =
+                SystemProperties.getLong("sync.notification_delay", 30 * 1000 /* 30 seconds */);
+    }
+
+    private static final long SYNC_NOTIFICATION_DELAY;
+
+    /**
+     * When retrying a sync for the first time use this delay. After that
+     * the retry time will double until it reached MAX_SYNC_RETRY_TIME.
+     * In milliseconds.
+     */
+    private static final long INITIAL_SYNC_RETRY_TIME_IN_MS = 30 * 1000; // 30 seconds
+
+    /**
+     * Default the max sync retry time to this value.
+     */
+    private static final long DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS = 60 * 60; // one hour
+
+    /**
+     * How long to wait before retrying a sync that failed due to one already being in progress.
+     */
+    private static final int DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS = 10;
+
+    private static final int INITIALIZATION_UNBIND_DELAY_MS = 5000;
+
+    private static final String SYNC_WAKE_LOCK_PREFIX = "*sync*";
+    private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarm";
+    private static final String SYNC_LOOP_WAKE_LOCK = "SyncLoopWakeLock";
+
+    private static final int MAX_SIMULTANEOUS_REGULAR_SYNCS;
+    private static final int MAX_SIMULTANEOUS_INITIALIZATION_SYNCS;
+
+    private Context mContext;
+
+    private static final AccountAndUser[] INITIAL_ACCOUNTS_ARRAY = new AccountAndUser[0];
+
+    // TODO: add better locking around mRunningAccounts
+    private volatile AccountAndUser[] mRunningAccounts = INITIAL_ACCOUNTS_ARRAY;
+
+    volatile private PowerManager.WakeLock mHandleAlarmWakeLock;
+    volatile private PowerManager.WakeLock mSyncManagerWakeLock;
+    volatile private boolean mDataConnectionIsConnected = false;
+    volatile private boolean mStorageIsLow = false;
+
+    private final NotificationManager mNotificationMgr;
+    private AlarmManager mAlarmService = null;
+
+    private SyncStorageEngine mSyncStorageEngine;
+
+    @GuardedBy("mSyncQueue")
+    private final SyncQueue mSyncQueue;
+
+    protected final ArrayList<ActiveSyncContext> mActiveSyncContexts = Lists.newArrayList();
+
+    // set if the sync active indicator should be reported
+    private boolean mNeedSyncActiveNotification = false;
+
+    private final PendingIntent mSyncAlarmIntent;
+    // Synchronized on "this". Instead of using this directly one should instead call
+    // its accessor, getConnManager().
+    private ConnectivityManager mConnManagerDoNotUseDirectly;
+
+    protected SyncAdaptersCache mSyncAdapters;
+
+    private BroadcastReceiver mStorageIntentReceiver =
+            new BroadcastReceiver() {
+                public void onReceive(Context context, Intent intent) {
+                    String action = intent.getAction();
+                    if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "Internal storage is low.");
+                        }
+                        mStorageIsLow = true;
+                        cancelActiveSync(null /* any account */, UserHandle.USER_ALL,
+                                null /* any authority */);
+                    } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "Internal storage is ok.");
+                        }
+                        mStorageIsLow = false;
+                        sendCheckAlarmsMessage();
+                    }
+                }
+            };
+
+    private BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() {
+        public void onReceive(Context context, Intent intent) {
+            mSyncHandler.onBootCompleted();
+        }
+    };
+
+    private BroadcastReceiver mBackgroundDataSettingChanged = new BroadcastReceiver() {
+        public void onReceive(Context context, Intent intent) {
+            if (getConnectivityManager().getBackgroundDataSetting()) {
+                scheduleSync(null /* account */, UserHandle.USER_ALL,
+                        SyncOperation.REASON_BACKGROUND_DATA_SETTINGS_CHANGED,
+                        null /* authority */,
+                        new Bundle(), 0 /* delay */,
+                        false /* onlyThoseWithUnknownSyncableState */);
+            }
+        }
+    };
+
+    private BroadcastReceiver mAccountsUpdatedReceiver = new BroadcastReceiver() {
+        public void onReceive(Context context, Intent intent) {
+            updateRunningAccounts();
+
+            // Kick off sync for everyone, since this was a radical account change
+            scheduleSync(null, UserHandle.USER_ALL, SyncOperation.REASON_ACCOUNTS_UPDATED, null,
+                    null, 0 /* no delay */, false);
+        }
+    };
+
+    private final PowerManager mPowerManager;
+
+    // Use this as a random offset to seed all periodic syncs
+    private int mSyncRandomOffsetMillis;
+
+    private final UserManager mUserManager;
+
+    private static final long SYNC_ALARM_TIMEOUT_MIN = 30 * 1000; // 30 seconds
+    private static final long SYNC_ALARM_TIMEOUT_MAX = 2 * 60 * 60 * 1000; // two hours
+
+    private List<UserInfo> getAllUsers() {
+        return mUserManager.getUsers();
+    }
+
+    private boolean containsAccountAndUser(AccountAndUser[] accounts, Account account, int userId) {
+        boolean found = false;
+        for (int i = 0; i < accounts.length; i++) {
+            if (accounts[i].userId == userId
+                    && accounts[i].account.equals(account)) {
+                found = true;
+                break;
+            }
+        }
+        return found;
+    }
+
+    public void updateRunningAccounts() {
+        mRunningAccounts = AccountManagerService.getSingleton().getRunningAccounts();
+
+        if (mBootCompleted) {
+            doDatabaseCleanup();
+        }
+
+        for (ActiveSyncContext currentSyncContext : mActiveSyncContexts) {
+            if (!containsAccountAndUser(mRunningAccounts,
+                    currentSyncContext.mSyncOperation.account,
+                    currentSyncContext.mSyncOperation.userId)) {
+                Log.d(TAG, "canceling sync since the account is no longer running");
+                sendSyncFinishedOrCanceledMessage(currentSyncContext,
+                        null /* no result since this is a cancel */);
+            }
+        }
+
+        // we must do this since we don't bother scheduling alarms when
+        // the accounts are not set yet
+        sendCheckAlarmsMessage();
+    }
+
+    private void doDatabaseCleanup() {
+        for (UserInfo user : mUserManager.getUsers(true)) {
+            // Skip any partially created/removed users
+            if (user.partial) continue;
+            Account[] accountsForUser = AccountManagerService.getSingleton().getAccounts(user.id);
+            mSyncStorageEngine.doDatabaseCleanup(accountsForUser, user.id);
+        }
+    }
+
+    private BroadcastReceiver mConnectivityIntentReceiver =
+            new BroadcastReceiver() {
+        public void onReceive(Context context, Intent intent) {
+            final boolean wasConnected = mDataConnectionIsConnected;
+
+            // don't use the intent to figure out if network is connected, just check
+            // ConnectivityManager directly.
+            mDataConnectionIsConnected = readDataConnectionState();
+            if (mDataConnectionIsConnected) {
+                if (!wasConnected) {
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "Reconnection detected: clearing all backoffs");
+                    }
+                    mSyncStorageEngine.clearAllBackoffs(mSyncQueue);
+                }
+                sendCheckAlarmsMessage();
+            }
+        }
+    };
+
+    private boolean readDataConnectionState() {
+        NetworkInfo networkInfo = getConnectivityManager().getActiveNetworkInfo();
+        return (networkInfo != null) && networkInfo.isConnected();
+    }
+
+    private BroadcastReceiver mShutdownIntentReceiver =
+            new BroadcastReceiver() {
+        public void onReceive(Context context, Intent intent) {
+            Log.w(TAG, "Writing sync state before shutdown...");
+            getSyncStorageEngine().writeAllState();
+        }
+    };
+
+    private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+            if (userId == UserHandle.USER_NULL) return;
+
+            if (Intent.ACTION_USER_REMOVED.equals(action)) {
+                onUserRemoved(userId);
+            } else if (Intent.ACTION_USER_STARTING.equals(action)) {
+                onUserStarting(userId);
+            } else if (Intent.ACTION_USER_STOPPING.equals(action)) {
+                onUserStopping(userId);
+            }
+        }
+    };
+
+    private static final String ACTION_SYNC_ALARM = "android.content.syncmanager.SYNC_ALARM";
+    private final SyncHandler mSyncHandler;
+
+    private volatile boolean mBootCompleted = false;
+
+    private ConnectivityManager getConnectivityManager() {
+        synchronized (this) {
+            if (mConnManagerDoNotUseDirectly == null) {
+                mConnManagerDoNotUseDirectly = (ConnectivityManager)mContext.getSystemService(
+                        Context.CONNECTIVITY_SERVICE);
+            }
+            return mConnManagerDoNotUseDirectly;
+        }
+    }
+
+    /**
+     * Should only be created after {@link ContentService#systemReady()} so that
+     * {@link PackageManager} is ready to query.
+     */
+    public SyncManager(Context context, boolean factoryTest) {
+        // Initialize the SyncStorageEngine first, before registering observers
+        // and creating threads and so on; it may fail if the disk is full.
+        mContext = context;
+
+        SyncStorageEngine.init(context);
+        mSyncStorageEngine = SyncStorageEngine.getSingleton();
+        mSyncStorageEngine.setOnSyncRequestListener(new OnSyncRequestListener() {
+            public void onSyncRequest(Account account, int userId, int reason, String authority,
+                    Bundle extras) {
+                scheduleSync(account, userId, reason, authority, extras, 0, false);
+            }
+        });
+
+        mSyncAdapters = new SyncAdaptersCache(mContext);
+        mSyncQueue = new SyncQueue(mContext.getPackageManager(), mSyncStorageEngine, mSyncAdapters);
+
+        HandlerThread syncThread = new HandlerThread("SyncHandlerThread",
+                Process.THREAD_PRIORITY_BACKGROUND);
+        syncThread.start();
+        mSyncHandler = new SyncHandler(syncThread.getLooper());
+
+        mSyncAdapters.setListener(new RegisteredServicesCacheListener<SyncAdapterType>() {
+            @Override
+            public void onServiceChanged(SyncAdapterType type, int userId, boolean removed) {
+                if (!removed) {
+                    scheduleSync(null, UserHandle.USER_ALL,
+                            SyncOperation.REASON_SERVICE_CHANGED,
+                            type.authority, null, 0 /* no delay */,
+                            false /* onlyThoseWithUnkownSyncableState */);
+                }
+            }
+        }, mSyncHandler);
+
+        mSyncAlarmIntent = PendingIntent.getBroadcast(
+                mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0);
+
+        IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+        context.registerReceiver(mConnectivityIntentReceiver, intentFilter);
+
+        if (!factoryTest) {
+            intentFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED);
+            context.registerReceiver(mBootCompletedReceiver, intentFilter);
+        }
+
+        intentFilter = new IntentFilter(ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED);
+        context.registerReceiver(mBackgroundDataSettingChanged, intentFilter);
+
+        intentFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW);
+        intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+        context.registerReceiver(mStorageIntentReceiver, intentFilter);
+
+        intentFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
+        intentFilter.setPriority(100);
+        context.registerReceiver(mShutdownIntentReceiver, intentFilter);
+
+        intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_USER_REMOVED);
+        intentFilter.addAction(Intent.ACTION_USER_STARTING);
+        intentFilter.addAction(Intent.ACTION_USER_STOPPING);
+        mContext.registerReceiverAsUser(
+                mUserIntentReceiver, UserHandle.ALL, intentFilter, null, null);
+
+        if (!factoryTest) {
+            mNotificationMgr = (NotificationManager)
+                context.getSystemService(Context.NOTIFICATION_SERVICE);
+            context.registerReceiver(new SyncAlarmIntentReceiver(),
+                    new IntentFilter(ACTION_SYNC_ALARM));
+        } else {
+            mNotificationMgr = null;
+        }
+        mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+
+        // This WakeLock is used to ensure that we stay awake between the time that we receive
+        // a sync alarm notification and when we finish processing it. We need to do this
+        // because we don't do the work in the alarm handler, rather we do it in a message
+        // handler.
+        mHandleAlarmWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                HANDLE_SYNC_ALARM_WAKE_LOCK);
+        mHandleAlarmWakeLock.setReferenceCounted(false);
+
+        // This WakeLock is used to ensure that we stay awake while running the sync loop
+        // message handler. Normally we will hold a sync adapter wake lock while it is being
+        // synced but during the execution of the sync loop it might finish a sync for
+        // one sync adapter before starting the sync for the other sync adapter and we
+        // don't want the device to go to sleep during that window.
+        mSyncManagerWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                SYNC_LOOP_WAKE_LOCK);
+        mSyncManagerWakeLock.setReferenceCounted(false);
+
+        mSyncStorageEngine.addStatusChangeListener(
+                ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, new ISyncStatusObserver.Stub() {
+            public void onStatusChanged(int which) {
+                // force the sync loop to run if the settings change
+                sendCheckAlarmsMessage();
+            }
+        });
+
+        if (!factoryTest) {
+            // Register for account list updates for all users
+            mContext.registerReceiverAsUser(mAccountsUpdatedReceiver,
+                    UserHandle.ALL,
+                    new IntentFilter(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION),
+                    null, null);
+        }
+
+        // Pick a random second in a day to seed all periodic syncs
+        mSyncRandomOffsetMillis = mSyncStorageEngine.getSyncRandomOffset() * 1000;
+    }
+
+    /**
+     * Return a random value v that satisfies minValue <= v < maxValue. The difference between
+     * maxValue and minValue must be less than Integer.MAX_VALUE.
+     */
+    private long jitterize(long minValue, long maxValue) {
+        Random random = new Random(SystemClock.elapsedRealtime());
+        long spread = maxValue - minValue;
+        if (spread > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("the difference between the maxValue and the "
+                    + "minValue must be less than " + Integer.MAX_VALUE);
+        }
+        return minValue + random.nextInt((int)spread);
+    }
+
+    public SyncStorageEngine getSyncStorageEngine() {
+        return mSyncStorageEngine;
+    }
+
+    private void ensureAlarmService() {
+        if (mAlarmService == null) {
+            mAlarmService = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
+        }
+    }
+
+    /**
+     * Initiate a sync. This can start a sync for all providers
+     * (pass null to url, set onlyTicklable to false), only those
+     * providers that are marked as ticklable (pass null to url,
+     * set onlyTicklable to true), or a specific provider (set url
+     * to the content url of the provider).
+     *
+     * <p>If the ContentResolver.SYNC_EXTRAS_UPLOAD boolean in extras is
+     * true then initiate a sync that just checks for local changes to send
+     * to the server, otherwise initiate a sync that first gets any
+     * changes from the server before sending local changes back to
+     * the server.
+     *
+     * <p>If a specific provider is being synced (the url is non-null)
+     * then the extras can contain SyncAdapter-specific information
+     * to control what gets synced (e.g. which specific feed to sync).
+     *
+     * <p>You'll start getting callbacks after this.
+     *
+     * @param requestedAccount the account to sync, may be null to signify all accounts
+     * @param userId the id of the user whose accounts are to be synced. If userId is USER_ALL,
+     *          then all users' accounts are considered.
+     * @param reason for sync request. If this is a positive integer, it is the Linux uid
+     * assigned to the process that requested the sync. If it's negative, the sync was requested by
+     * the SyncManager itself and could be one of the following:
+     *      {@link SyncOperation#REASON_BACKGROUND_DATA_SETTINGS_CHANGED}
+     *      {@link SyncOperation#REASON_ACCOUNTS_UPDATED}
+     *      {@link SyncOperation#REASON_SERVICE_CHANGED}
+     *      {@link SyncOperation#REASON_PERIODIC}
+     *      {@link SyncOperation#REASON_IS_SYNCABLE}
+     *      {@link SyncOperation#REASON_SYNC_AUTO}
+     *      {@link SyncOperation#REASON_MASTER_SYNC_AUTO}
+     *      {@link SyncOperation#REASON_USER_START}
+     * @param requestedAuthority the authority to sync, may be null to indicate all authorities
+     * @param extras a Map of SyncAdapter-specific information to control
+     *          syncs of a specific provider. Can be null. Is ignored
+     *          if the url is null.
+     * @param delay how many milliseconds in the future to wait before performing this
+     * @param onlyThoseWithUnkownSyncableState
+     */
+    public void scheduleSync(Account requestedAccount, int userId, int reason,
+            String requestedAuthority, Bundle extras, long delay,
+            boolean onlyThoseWithUnkownSyncableState) {
+        boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+
+        final boolean backgroundDataUsageAllowed = !mBootCompleted ||
+                getConnectivityManager().getBackgroundDataSetting();
+
+        if (extras == null) extras = new Bundle();
+
+        Boolean expedited = extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
+        if (expedited) {
+            delay = -1; // this means schedule at the front of the queue
+        }
+
+        AccountAndUser[] accounts;
+        if (requestedAccount != null && userId != UserHandle.USER_ALL) {
+            accounts = new AccountAndUser[] { new AccountAndUser(requestedAccount, userId) };
+        } else {
+            // if the accounts aren't configured yet then we can't support an account-less
+            // sync request
+            accounts = mRunningAccounts;
+            if (accounts.length == 0) {
+                if (isLoggable) {
+                    Log.v(TAG, "scheduleSync: no accounts configured, dropping");
+                }
+                return;
+            }
+        }
+
+        final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false);
+        final boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
+        if (manualSync) {
+            extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true);
+            extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
+        }
+        final boolean ignoreSettings =
+                extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false);
+
+        int source;
+        if (uploadOnly) {
+            source = SyncStorageEngine.SOURCE_LOCAL;
+        } else if (manualSync) {
+            source = SyncStorageEngine.SOURCE_USER;
+        } else if (requestedAuthority == null) {
+            source = SyncStorageEngine.SOURCE_POLL;
+        } else {
+            // this isn't strictly server, since arbitrary callers can (and do) request
+            // a non-forced two-way sync on a specific url
+            source = SyncStorageEngine.SOURCE_SERVER;
+        }
+
+        for (AccountAndUser account : accounts) {
+            // Compile a list of authorities that have sync adapters.
+            // For each authority sync each account that matches a sync adapter.
+            final HashSet<String> syncableAuthorities = new HashSet<String>();
+            for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapter :
+                    mSyncAdapters.getAllServices(account.userId)) {
+                syncableAuthorities.add(syncAdapter.type.authority);
+            }
+
+            // if the url was specified then replace the list of authorities
+            // with just this authority or clear it if this authority isn't
+            // syncable
+            if (requestedAuthority != null) {
+                final boolean hasSyncAdapter = syncableAuthorities.contains(requestedAuthority);
+                syncableAuthorities.clear();
+                if (hasSyncAdapter) syncableAuthorities.add(requestedAuthority);
+            }
+
+            for (String authority : syncableAuthorities) {
+                int isSyncable = mSyncStorageEngine.getIsSyncable(account.account, account.userId,
+                        authority);
+                if (isSyncable == 0) {
+                    continue;
+                }
+                final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo;
+                syncAdapterInfo = mSyncAdapters.getServiceInfo(
+                        SyncAdapterType.newKey(authority, account.account.type), account.userId);
+                if (syncAdapterInfo == null) {
+                    continue;
+                }
+                final boolean allowParallelSyncs = syncAdapterInfo.type.allowParallelSyncs();
+                final boolean isAlwaysSyncable = syncAdapterInfo.type.isAlwaysSyncable();
+                if (isSyncable < 0 && isAlwaysSyncable) {
+                    mSyncStorageEngine.setIsSyncable(account.account, account.userId, authority, 1);
+                    isSyncable = 1;
+                }
+                if (onlyThoseWithUnkownSyncableState && isSyncable >= 0) {
+                    continue;
+                }
+                if (!syncAdapterInfo.type.supportsUploading() && uploadOnly) {
+                    continue;
+                }
+
+                // always allow if the isSyncable state is unknown
+                boolean syncAllowed =
+                        (isSyncable < 0)
+                        || ignoreSettings
+                        || (backgroundDataUsageAllowed
+                                && mSyncStorageEngine.getMasterSyncAutomatically(account.userId)
+                                && mSyncStorageEngine.getSyncAutomatically(account.account,
+                                        account.userId, authority));
+                if (!syncAllowed) {
+                    if (isLoggable) {
+                        Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority
+                                + " is not allowed, dropping request");
+                    }
+                    continue;
+                }
+
+                Pair<Long, Long> backoff = mSyncStorageEngine
+                        .getBackoff(account.account, account.userId, authority);
+                long delayUntil = mSyncStorageEngine.getDelayUntilTime(account.account,
+                        account.userId, authority);
+                final long backoffTime = backoff != null ? backoff.first : 0;
+                if (isSyncable < 0) {
+                    Bundle newExtras = new Bundle();
+                    newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
+                    if (isLoggable) {
+                        Log.v(TAG, "scheduleSync:"
+                                + " delay " + delay
+                                + ", source " + source
+                                + ", account " + account
+                                + ", authority " + authority
+                                + ", extras " + newExtras);
+                    }
+                    scheduleSyncOperation(
+                            new SyncOperation(account.account, account.userId, reason, source,
+                                    authority, newExtras, 0, backoffTime, delayUntil,
+                                    allowParallelSyncs));
+                }
+                if (!onlyThoseWithUnkownSyncableState) {
+                    if (isLoggable) {
+                        Log.v(TAG, "scheduleSync:"
+                                + " delay " + delay
+                                + ", source " + source
+                                + ", account " + account
+                                + ", authority " + authority
+                                + ", extras " + extras);
+                    }
+                    scheduleSyncOperation(
+                            new SyncOperation(account.account, account.userId, reason, source,
+                                    authority, extras, delay, backoffTime, delayUntil,
+                                    allowParallelSyncs));
+                }
+            }
+        }
+    }
+
+    public void scheduleLocalSync(Account account, int userId, int reason, String authority) {
+        final Bundle extras = new Bundle();
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
+        scheduleSync(account, userId, reason, authority, extras, LOCAL_SYNC_DELAY,
+                false /* onlyThoseWithUnkownSyncableState */);
+    }
+
+    public SyncAdapterType[] getSyncAdapterTypes(int userId) {
+        final Collection<RegisteredServicesCache.ServiceInfo<SyncAdapterType>> serviceInfos;
+        serviceInfos = mSyncAdapters.getAllServices(userId);
+        SyncAdapterType[] types = new SyncAdapterType[serviceInfos.size()];
+        int i = 0;
+        for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> serviceInfo : serviceInfos) {
+            types[i] = serviceInfo.type;
+            ++i;
+        }
+        return types;
+    }
+
+    private void sendSyncAlarmMessage() {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_ALARM");
+        mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_SYNC_ALARM);
+    }
+
+    private void sendCheckAlarmsMessage() {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_CHECK_ALARMS");
+        mSyncHandler.removeMessages(SyncHandler.MESSAGE_CHECK_ALARMS);
+        mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_CHECK_ALARMS);
+    }
+
+    private void sendSyncFinishedOrCanceledMessage(ActiveSyncContext syncContext,
+            SyncResult syncResult) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_FINISHED");
+        Message msg = mSyncHandler.obtainMessage();
+        msg.what = SyncHandler.MESSAGE_SYNC_FINISHED;
+        msg.obj = new SyncHandlerMessagePayload(syncContext, syncResult);
+        mSyncHandler.sendMessage(msg);
+    }
+
+    private void sendCancelSyncsMessage(final Account account, final int userId,
+            final String authority) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_CANCEL");
+        Message msg = mSyncHandler.obtainMessage();
+        msg.what = SyncHandler.MESSAGE_CANCEL;
+        msg.obj = Pair.create(account, authority);
+        msg.arg1 = userId;
+        mSyncHandler.sendMessage(msg);
+    }
+
+    class SyncHandlerMessagePayload {
+        public final ActiveSyncContext activeSyncContext;
+        public final SyncResult syncResult;
+
+        SyncHandlerMessagePayload(ActiveSyncContext syncContext, SyncResult syncResult) {
+            this.activeSyncContext = syncContext;
+            this.syncResult = syncResult;
+        }
+    }
+
+    class SyncAlarmIntentReceiver extends BroadcastReceiver {
+        public void onReceive(Context context, Intent intent) {
+            mHandleAlarmWakeLock.acquire();
+            sendSyncAlarmMessage();
+        }
+    }
+
+    private void clearBackoffSetting(SyncOperation op) {
+        mSyncStorageEngine.setBackoff(op.account, op.userId, op.authority,
+                SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE);
+        synchronized (mSyncQueue) {
+            mSyncQueue.onBackoffChanged(op.account, op.userId, op.authority, 0);
+        }
+    }
+
+    private void increaseBackoffSetting(SyncOperation op) {
+        // TODO: Use this function to align it to an already scheduled sync
+        //       operation in the specified window
+        final long now = SystemClock.elapsedRealtime();
+
+        final Pair<Long, Long> previousSettings =
+                mSyncStorageEngine.getBackoff(op.account, op.userId, op.authority);
+        long newDelayInMs = -1;
+        if (previousSettings != null) {
+            // don't increase backoff before current backoff is expired. This will happen for op's
+            // with ignoreBackoff set.
+            if (now < previousSettings.first) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "Still in backoff, do not increase it. "
+                        + "Remaining: " + ((previousSettings.first - now) / 1000) + " seconds.");
+                }
+                return;
+            }
+            // Subsequent delays are the double of the previous delay
+            newDelayInMs = previousSettings.second * 2;
+        }
+        if (newDelayInMs <= 0) {
+            // The initial delay is the jitterized INITIAL_SYNC_RETRY_TIME_IN_MS
+            newDelayInMs = jitterize(INITIAL_SYNC_RETRY_TIME_IN_MS,
+                    (long)(INITIAL_SYNC_RETRY_TIME_IN_MS * 1.1));
+        }
+
+        // Cap the delay
+        long maxSyncRetryTimeInSeconds = Settings.Global.getLong(mContext.getContentResolver(),
+                Settings.Global.SYNC_MAX_RETRY_DELAY_IN_SECONDS,
+                DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS);
+        if (newDelayInMs > maxSyncRetryTimeInSeconds * 1000) {
+            newDelayInMs = maxSyncRetryTimeInSeconds * 1000;
+        }
+
+        final long backoff = now + newDelayInMs;
+
+        mSyncStorageEngine.setBackoff(op.account, op.userId, op.authority,
+                backoff, newDelayInMs);
+
+        op.backoff = backoff;
+        op.updateEffectiveRunTime();
+
+        synchronized (mSyncQueue) {
+            mSyncQueue.onBackoffChanged(op.account, op.userId, op.authority, backoff);
+        }
+    }
+
+    private void setDelayUntilTime(SyncOperation op, long delayUntilSeconds) {
+        final long delayUntil = delayUntilSeconds * 1000;
+        final long absoluteNow = System.currentTimeMillis();
+        long newDelayUntilTime;
+        if (delayUntil > absoluteNow) {
+            newDelayUntilTime = SystemClock.elapsedRealtime() + (delayUntil - absoluteNow);
+        } else {
+            newDelayUntilTime = 0;
+        }
+        mSyncStorageEngine
+                .setDelayUntilTime(op.account, op.userId, op.authority, newDelayUntilTime);
+        synchronized (mSyncQueue) {
+            mSyncQueue.onDelayUntilTimeChanged(op.account, op.authority, newDelayUntilTime);
+        }
+    }
+
+    /**
+     * Cancel the active sync if it matches the authority and account.
+     * @param account limit the cancelations to syncs with this account, if non-null
+     * @param authority limit the cancelations to syncs with this authority, if non-null
+     */
+    public void cancelActiveSync(Account account, int userId, String authority) {
+        sendCancelSyncsMessage(account, userId, authority);
+    }
+
+    /**
+     * Create and schedule a SyncOperation.
+     *
+     * @param syncOperation the SyncOperation to schedule
+     */
+    public void scheduleSyncOperation(SyncOperation syncOperation) {
+        boolean queueChanged;
+        synchronized (mSyncQueue) {
+            queueChanged = mSyncQueue.add(syncOperation);
+        }
+
+        if (queueChanged) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "scheduleSyncOperation: enqueued " + syncOperation);
+            }
+            sendCheckAlarmsMessage();
+        } else {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "scheduleSyncOperation: dropping duplicate sync operation "
+                        + syncOperation);
+            }
+        }
+    }
+
+    /**
+     * Remove scheduled sync operations.
+     * @param account limit the removals to operations with this account, if non-null
+     * @param authority limit the removals to operations with this authority, if non-null
+     */
+    public void clearScheduledSyncOperations(Account account, int userId, String authority) {
+        synchronized (mSyncQueue) {
+            mSyncQueue.remove(account, userId, authority);
+        }
+        mSyncStorageEngine.setBackoff(account, userId, authority,
+                SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE);
+    }
+
+    void maybeRescheduleSync(SyncResult syncResult, SyncOperation operation) {
+        boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG);
+        if (isLoggable) {
+            Log.d(TAG, "encountered error(s) during the sync: " + syncResult + ", " + operation);
+        }
+
+        operation = new SyncOperation(operation);
+
+        // The SYNC_EXTRAS_IGNORE_BACKOFF only applies to the first attempt to sync a given
+        // request. Retries of the request will always honor the backoff, so clear the
+        // flag in case we retry this request.
+        if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)) {
+            operation.extras.remove(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF);
+        }
+
+        // If this sync aborted because the internal sync loop retried too many times then
+        //   don't reschedule. Otherwise we risk getting into a retry loop.
+        // If the operation succeeded to some extent then retry immediately.
+        // If this was a two-way sync then retry soft errors with an exponential backoff.
+        // If this was an upward sync then schedule a two-way sync immediately.
+        // Otherwise do not reschedule.
+        if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false)) {
+            Log.d(TAG, "not retrying sync operation because SYNC_EXTRAS_DO_NOT_RETRY was specified "
+                    + operation);
+        } else if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false)
+                && !syncResult.syncAlreadyInProgress) {
+            operation.extras.remove(ContentResolver.SYNC_EXTRAS_UPLOAD);
+            Log.d(TAG, "retrying sync operation as a two-way sync because an upload-only sync "
+                    + "encountered an error: " + operation);
+            scheduleSyncOperation(operation);
+        } else if (syncResult.tooManyRetries) {
+            Log.d(TAG, "not retrying sync operation because it retried too many times: "
+                    + operation);
+        } else if (syncResult.madeSomeProgress()) {
+            if (isLoggable) {
+                Log.d(TAG, "retrying sync operation because even though it had an error "
+                        + "it achieved some success");
+            }
+            scheduleSyncOperation(operation);
+        } else if (syncResult.syncAlreadyInProgress) {
+            if (isLoggable) {
+                Log.d(TAG, "retrying sync operation that failed because there was already a "
+                        + "sync in progress: " + operation);
+            }
+            scheduleSyncOperation(new SyncOperation(operation.account, operation.userId,
+                    operation.reason,
+                    operation.syncSource,
+                    operation.authority, operation.extras,
+                    DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS * 1000,
+                    operation.backoff, operation.delayUntil, operation.allowParallelSyncs));
+        } else if (syncResult.hasSoftError()) {
+            if (isLoggable) {
+                Log.d(TAG, "retrying sync operation because it encountered a soft error: "
+                        + operation);
+            }
+            scheduleSyncOperation(operation);
+        } else {
+            Log.d(TAG, "not retrying sync operation because the error is a hard error: "
+                    + operation);
+        }
+    }
+
+    private void onUserStarting(int userId) {
+        // Make sure that accounts we're about to use are valid
+        AccountManagerService.getSingleton().validateAccounts(userId);
+
+        mSyncAdapters.invalidateCache(userId);
+
+        updateRunningAccounts();
+
+        synchronized (mSyncQueue) {
+            mSyncQueue.addPendingOperations(userId);
+        }
+
+        // Schedule sync for any accounts under started user
+        final Account[] accounts = AccountManagerService.getSingleton().getAccounts(userId);
+        for (Account account : accounts) {
+            scheduleSync(account, userId, SyncOperation.REASON_USER_START, null, null,
+                    0 /* no delay */, true /* onlyThoseWithUnknownSyncableState */);
+        }
+
+        sendCheckAlarmsMessage();
+    }
+
+    private void onUserStopping(int userId) {
+        updateRunningAccounts();
+
+        cancelActiveSync(
+                null /* any account */,
+                userId,
+                null /* any authority */);
+    }
+
+    private void onUserRemoved(int userId) {
+        updateRunningAccounts();
+
+        // Clean up the storage engine database
+        mSyncStorageEngine.doDatabaseCleanup(new Account[0], userId);
+        synchronized (mSyncQueue) {
+            mSyncQueue.removeUser(userId);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    class ActiveSyncContext extends ISyncContext.Stub
+            implements ServiceConnection, IBinder.DeathRecipient {
+        final SyncOperation mSyncOperation;
+        final long mHistoryRowId;
+        ISyncAdapter mSyncAdapter;
+        final long mStartTime;
+        long mTimeoutStartTime;
+        boolean mBound;
+        final PowerManager.WakeLock mSyncWakeLock;
+        final int mSyncAdapterUid;
+        SyncInfo mSyncInfo;
+        boolean mIsLinkedToDeath = false;
+
+        /**
+         * Create an ActiveSyncContext for an impending sync and grab the wakelock for that
+         * sync adapter. Since this grabs the wakelock you need to be sure to call
+         * close() when you are done with this ActiveSyncContext, whether the sync succeeded
+         * or not.
+         * @param syncOperation the SyncOperation we are about to sync
+         * @param historyRowId the row in which to record the history info for this sync
+         * @param syncAdapterUid the UID of the application that contains the sync adapter
+         * for this sync. This is used to attribute the wakelock hold to that application.
+         */
+        public ActiveSyncContext(SyncOperation syncOperation, long historyRowId,
+                int syncAdapterUid) {
+            super();
+            mSyncAdapterUid = syncAdapterUid;
+            mSyncOperation = syncOperation;
+            mHistoryRowId = historyRowId;
+            mSyncAdapter = null;
+            mStartTime = SystemClock.elapsedRealtime();
+            mTimeoutStartTime = mStartTime;
+            mSyncWakeLock = mSyncHandler.getSyncWakeLock(
+                    mSyncOperation.account, mSyncOperation.authority);
+            mSyncWakeLock.setWorkSource(new WorkSource(syncAdapterUid));
+            mSyncWakeLock.acquire();
+        }
+
+        public void sendHeartbeat() {
+            // heartbeats are no longer used
+        }
+
+        public void onFinished(SyncResult result) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "onFinished: " + this);
+            // include "this" in the message so that the handler can ignore it if this
+            // ActiveSyncContext is no longer the mActiveSyncContext at message handling
+            // time
+            sendSyncFinishedOrCanceledMessage(this, result);
+        }
+
+        public void toString(StringBuilder sb) {
+            sb.append("startTime ").append(mStartTime)
+                    .append(", mTimeoutStartTime ").append(mTimeoutStartTime)
+                    .append(", mHistoryRowId ").append(mHistoryRowId)
+                    .append(", syncOperation ").append(mSyncOperation);
+        }
+
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            Message msg = mSyncHandler.obtainMessage();
+            msg.what = SyncHandler.MESSAGE_SERVICE_CONNECTED;
+            msg.obj = new ServiceConnectionData(this, ISyncAdapter.Stub.asInterface(service));
+            mSyncHandler.sendMessage(msg);
+        }
+
+        public void onServiceDisconnected(ComponentName name) {
+            Message msg = mSyncHandler.obtainMessage();
+            msg.what = SyncHandler.MESSAGE_SERVICE_DISCONNECTED;
+            msg.obj = new ServiceConnectionData(this, null);
+            mSyncHandler.sendMessage(msg);
+        }
+
+        boolean bindToSyncAdapter(RegisteredServicesCache.ServiceInfo info, int userId) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.d(TAG, "bindToSyncAdapter: " + info.componentName + ", connection " + this);
+            }
+            Intent intent = new Intent();
+            intent.setAction("android.content.SyncAdapter");
+            intent.setComponent(info.componentName);
+            intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
+                    com.android.internal.R.string.sync_binding_label);
+            intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivityAsUser(
+                    mContext, 0, new Intent(Settings.ACTION_SYNC_SETTINGS), 0,
+                    null, new UserHandle(userId)));
+            mBound = true;
+            final boolean bindResult = mContext.bindService(intent, this,
+                    Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND
+                    | Context.BIND_ALLOW_OOM_MANAGEMENT,
+                    mSyncOperation.userId);
+            if (!bindResult) {
+                mBound = false;
+            }
+            return bindResult;
+        }
+
+        /**
+         * Performs the required cleanup, which is the releasing of the wakelock and
+         * unbinding from the sync adapter (if actually bound).
+         */
+        protected void close() {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.d(TAG, "unBindFromSyncAdapter: connection " + this);
+            }
+            if (mBound) {
+                mBound = false;
+                mContext.unbindService(this);
+            }
+            mSyncWakeLock.release();
+            mSyncWakeLock.setWorkSource(null);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            toString(sb);
+            return sb.toString();
+        }
+
+        @Override
+        public void binderDied() {
+            sendSyncFinishedOrCanceledMessage(this, null);
+        }
+    }
+
+    protected void dump(FileDescriptor fd, PrintWriter pw) {
+        final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        dumpSyncState(ipw);
+        dumpSyncHistory(ipw);
+        dumpSyncAdapters(ipw);
+    }
+
+    static String formatTime(long time) {
+        Time tobj = new Time();
+        tobj.set(time);
+        return tobj.format("%Y-%m-%d %H:%M:%S");
+    }
+
+    protected void dumpSyncState(PrintWriter pw) {
+        pw.print("data connected: "); pw.println(mDataConnectionIsConnected);
+        pw.print("auto sync: ");
+        List<UserInfo> users = getAllUsers();
+        if (users != null) {
+            for (UserInfo user : users) {
+                pw.print("u" + user.id + "="
+                        + mSyncStorageEngine.getMasterSyncAutomatically(user.id) + " ");
+            }
+            pw.println();
+        }
+        pw.print("memory low: "); pw.println(mStorageIsLow);
+
+        final AccountAndUser[] accounts = AccountManagerService.getSingleton().getAllAccounts();
+
+        pw.print("accounts: ");
+        if (accounts != INITIAL_ACCOUNTS_ARRAY) {
+            pw.println(accounts.length);
+        } else {
+            pw.println("not known yet");
+        }
+        final long now = SystemClock.elapsedRealtime();
+        pw.print("now: "); pw.print(now);
+        pw.println(" (" + formatTime(System.currentTimeMillis()) + ")");
+        pw.print("offset: "); pw.print(DateUtils.formatElapsedTime(mSyncRandomOffsetMillis/1000));
+        pw.println(" (HH:MM:SS)");
+        pw.print("uptime: "); pw.print(DateUtils.formatElapsedTime(now/1000));
+                pw.println(" (HH:MM:SS)");
+        pw.print("time spent syncing: ");
+                pw.print(DateUtils.formatElapsedTime(
+                        mSyncHandler.mSyncTimeTracker.timeSpentSyncing() / 1000));
+                pw.print(" (HH:MM:SS), sync ");
+                pw.print(mSyncHandler.mSyncTimeTracker.mLastWasSyncing ? "" : "not ");
+                pw.println("in progress");
+        if (mSyncHandler.mAlarmScheduleTime != null) {
+            pw.print("next alarm time: "); pw.print(mSyncHandler.mAlarmScheduleTime);
+                    pw.print(" (");
+                    pw.print(DateUtils.formatElapsedTime((mSyncHandler.mAlarmScheduleTime-now)/1000));
+                    pw.println(" (HH:MM:SS) from now)");
+        } else {
+            pw.println("no alarm is scheduled (there had better not be any pending syncs)");
+        }
+
+        pw.print("notification info: ");
+        final StringBuilder sb = new StringBuilder();
+        mSyncHandler.mSyncNotificationInfo.toString(sb);
+        pw.println(sb.toString());
+
+        pw.println();
+        pw.println("Active Syncs: " + mActiveSyncContexts.size());
+        final PackageManager pm = mContext.getPackageManager();
+        for (SyncManager.ActiveSyncContext activeSyncContext : mActiveSyncContexts) {
+            final long durationInSeconds = (now - activeSyncContext.mStartTime) / 1000;
+            pw.print("  ");
+            pw.print(DateUtils.formatElapsedTime(durationInSeconds));
+            pw.print(" - ");
+            pw.print(activeSyncContext.mSyncOperation.dump(pm, false));
+            pw.println();
+        }
+
+        synchronized (mSyncQueue) {
+            sb.setLength(0);
+            mSyncQueue.dump(sb);
+        }
+        pw.println();
+        pw.print(sb.toString());
+
+        // join the installed sync adapter with the accounts list and emit for everything
+        pw.println();
+        pw.println("Sync Status");
+        for (AccountAndUser account : accounts) {
+            pw.printf("Account %s u%d %s\n",
+                    account.account.name, account.userId, account.account.type);
+
+            pw.println("=======================================================================");
+            final PrintTable table = new PrintTable(13);
+            table.set(0, 0,
+                    "Authority", // 0
+                    "Syncable",  // 1
+                    "Enabled",   // 2
+                    "Delay",     // 3
+                    "Loc",       // 4
+                    "Poll",      // 5
+                    "Per",       // 6
+                    "Serv",      // 7
+                    "User",      // 8
+                    "Tot",       // 9
+                    "Time",      // 10
+                    "Last Sync", // 11
+                    "Periodic"   // 12
+            );
+
+            final List<RegisteredServicesCache.ServiceInfo<SyncAdapterType>> sorted =
+                    Lists.newArrayList();
+            sorted.addAll(mSyncAdapters.getAllServices(account.userId));
+            Collections.sort(sorted,
+                    new Comparator<RegisteredServicesCache.ServiceInfo<SyncAdapterType>>() {
+                @Override
+                public int compare(RegisteredServicesCache.ServiceInfo<SyncAdapterType> lhs,
+                        RegisteredServicesCache.ServiceInfo<SyncAdapterType> rhs) {
+                    return lhs.type.authority.compareTo(rhs.type.authority);
+                }
+            });
+            for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterType : sorted) {
+                if (!syncAdapterType.type.accountType.equals(account.account.type)) {
+                    continue;
+                }
+                int row = table.getNumRows();
+                SyncStorageEngine.AuthorityInfo settings =
+                        mSyncStorageEngine.getOrCreateAuthority(
+                                account.account, account.userId, syncAdapterType.type.authority);
+                SyncStatusInfo status = mSyncStorageEngine.getOrCreateSyncStatus(settings);
+
+                String authority = settings.authority;
+                if (authority.length() > 50) {
+                    authority = authority.substring(authority.length() - 50);
+                }
+                table.set(row, 0, authority, settings.syncable, settings.enabled);
+                table.set(row, 4,
+                        status.numSourceLocal,
+                        status.numSourcePoll,
+                        status.numSourcePeriodic,
+                        status.numSourceServer,
+                        status.numSourceUser,
+                        status.numSyncs,
+                        DateUtils.formatElapsedTime(status.totalElapsedTime / 1000));
+
+
+                for (int i = 0; i < settings.periodicSyncs.size(); i++) {
+                    final Pair<Bundle, Long> pair = settings.periodicSyncs.get(0);
+                    final String period = String.valueOf(pair.second);
+                    final String extras = pair.first.size() > 0 ? pair.first.toString() : "";
+                    final String next = formatTime(status.getPeriodicSyncTime(0)
+                            + pair.second * 1000);
+                    table.set(row + i * 2, 12, period + extras);
+                    table.set(row + i * 2 + 1, 12, next);
+                }
+
+                int row1 = row;
+                if (settings.delayUntil > now) {
+                    table.set(row1++, 12, "D: " + (settings.delayUntil - now) / 1000);
+                    if (settings.backoffTime > now) {
+                        table.set(row1++, 12, "B: " + (settings.backoffTime - now) / 1000);
+                        table.set(row1++, 12, settings.backoffDelay / 1000);
+                    }
+                }
+
+                if (status.lastSuccessTime != 0) {
+                    table.set(row1++, 11, SyncStorageEngine.SOURCES[status.lastSuccessSource]
+                            + " " + "SUCCESS");
+                    table.set(row1++, 11, formatTime(status.lastSuccessTime));
+                }
+                if (status.lastFailureTime != 0) {
+                    table.set(row1++, 11, SyncStorageEngine.SOURCES[status.lastFailureSource]
+                            + " " + "FAILURE");
+                    table.set(row1++, 11, formatTime(status.lastFailureTime));
+                    //noinspection UnusedAssignment
+                    table.set(row1++, 11, status.lastFailureMesg);
+                }
+            }
+            table.writeTo(pw);
+        }
+    }
+
+    private String getLastFailureMessage(int code) {
+        switch (code) {
+            case ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS:
+                return "sync already in progress";
+
+            case ContentResolver.SYNC_ERROR_AUTHENTICATION:
+                return "authentication error";
+
+            case ContentResolver.SYNC_ERROR_IO:
+                return "I/O error";
+
+            case ContentResolver.SYNC_ERROR_PARSE:
+                return "parse error";
+
+            case ContentResolver.SYNC_ERROR_CONFLICT:
+                return "conflict error";
+
+            case ContentResolver.SYNC_ERROR_TOO_MANY_DELETIONS:
+                return "too many deletions error";
+
+            case ContentResolver.SYNC_ERROR_TOO_MANY_RETRIES:
+                return "too many retries error";
+
+            case ContentResolver.SYNC_ERROR_INTERNAL:
+                return "internal error";
+
+            default:
+                return "unknown";
+        }
+    }
+
+    private void dumpTimeSec(PrintWriter pw, long time) {
+        pw.print(time/1000); pw.print('.'); pw.print((time/100)%10);
+        pw.print('s');
+    }
+
+    private void dumpDayStatistic(PrintWriter pw, SyncStorageEngine.DayStats ds) {
+        pw.print("Success ("); pw.print(ds.successCount);
+        if (ds.successCount > 0) {
+            pw.print(" for "); dumpTimeSec(pw, ds.successTime);
+            pw.print(" avg="); dumpTimeSec(pw, ds.successTime/ds.successCount);
+        }
+        pw.print(") Failure ("); pw.print(ds.failureCount);
+        if (ds.failureCount > 0) {
+            pw.print(" for "); dumpTimeSec(pw, ds.failureTime);
+            pw.print(" avg="); dumpTimeSec(pw, ds.failureTime/ds.failureCount);
+        }
+        pw.println(")");
+    }
+
+    protected void dumpSyncHistory(PrintWriter pw) {
+        dumpRecentHistory(pw);
+        dumpDayStatistics(pw);
+    }
+
+    private void dumpRecentHistory(PrintWriter pw) {
+        final ArrayList<SyncStorageEngine.SyncHistoryItem> items
+                = mSyncStorageEngine.getSyncHistory();
+        if (items != null && items.size() > 0) {
+            final Map<String, AuthoritySyncStats> authorityMap = Maps.newHashMap();
+            long totalElapsedTime = 0;
+            long totalTimes = 0;
+            final int N = items.size();
+
+            int maxAuthority = 0;
+            int maxAccount = 0;
+            for (SyncStorageEngine.SyncHistoryItem item : items) {
+                SyncStorageEngine.AuthorityInfo authority
+                        = mSyncStorageEngine.getAuthority(item.authorityId);
+                final String authorityName;
+                final String accountKey;
+                if (authority != null) {
+                    authorityName = authority.authority;
+                    accountKey = authority.account.name + "/" + authority.account.type
+                            + " u" + authority.userId;
+                } else {
+                    authorityName = "Unknown";
+                    accountKey = "Unknown";
+                }
+
+                int length = authorityName.length();
+                if (length > maxAuthority) {
+                    maxAuthority = length;
+                }
+                length = accountKey.length();
+                if (length > maxAccount) {
+                    maxAccount = length;
+                }
+
+                final long elapsedTime = item.elapsedTime;
+                totalElapsedTime += elapsedTime;
+                totalTimes++;
+                AuthoritySyncStats authoritySyncStats = authorityMap.get(authorityName);
+                if (authoritySyncStats == null) {
+                    authoritySyncStats = new AuthoritySyncStats(authorityName);
+                    authorityMap.put(authorityName, authoritySyncStats);
+                }
+                authoritySyncStats.elapsedTime += elapsedTime;
+                authoritySyncStats.times++;
+                final Map<String, AccountSyncStats> accountMap = authoritySyncStats.accountMap;
+                AccountSyncStats accountSyncStats = accountMap.get(accountKey);
+                if (accountSyncStats == null) {
+                    accountSyncStats = new AccountSyncStats(accountKey);
+                    accountMap.put(accountKey, accountSyncStats);
+                }
+                accountSyncStats.elapsedTime += elapsedTime;
+                accountSyncStats.times++;
+
+            }
+
+            if (totalElapsedTime > 0) {
+                pw.println();
+                pw.printf("Detailed Statistics (Recent history):  "
+                        + "%d (# of times) %ds (sync time)\n",
+                        totalTimes, totalElapsedTime / 1000);
+
+                final List<AuthoritySyncStats> sortedAuthorities =
+                        new ArrayList<AuthoritySyncStats>(authorityMap.values());
+                Collections.sort(sortedAuthorities, new Comparator<AuthoritySyncStats>() {
+                    @Override
+                    public int compare(AuthoritySyncStats lhs, AuthoritySyncStats rhs) {
+                        // reverse order
+                        int compare = Integer.compare(rhs.times, lhs.times);
+                        if (compare == 0) {
+                            compare = Long.compare(rhs.elapsedTime, lhs.elapsedTime);
+                        }
+                        return compare;
+                    }
+                });
+
+                final int maxLength = Math.max(maxAuthority, maxAccount + 3);
+                final int padLength = 2 + 2 + maxLength + 2 + 10 + 11;
+                final char chars[] = new char[padLength];
+                Arrays.fill(chars, '-');
+                final String separator = new String(chars);
+
+                final String authorityFormat =
+                        String.format("  %%-%ds: %%-9s  %%-11s\n", maxLength + 2);
+                final String accountFormat =
+                        String.format("    %%-%ds:   %%-9s  %%-11s\n", maxLength);
+
+                pw.println(separator);
+                for (AuthoritySyncStats authoritySyncStats : sortedAuthorities) {
+                    String name = authoritySyncStats.name;
+                    long elapsedTime;
+                    int times;
+                    String timeStr;
+                    String timesStr;
+
+                    elapsedTime = authoritySyncStats.elapsedTime;
+                    times = authoritySyncStats.times;
+                    timeStr = String.format("%ds/%d%%",
+                            elapsedTime / 1000,
+                            elapsedTime * 100 / totalElapsedTime);
+                    timesStr = String.format("%d/%d%%",
+                            times,
+                            times * 100 / totalTimes);
+                    pw.printf(authorityFormat, name, timesStr, timeStr);
+
+                    final List<AccountSyncStats> sortedAccounts =
+                            new ArrayList<AccountSyncStats>(
+                                    authoritySyncStats.accountMap.values());
+                    Collections.sort(sortedAccounts, new Comparator<AccountSyncStats>() {
+                        @Override
+                        public int compare(AccountSyncStats lhs, AccountSyncStats rhs) {
+                            // reverse order
+                            int compare = Integer.compare(rhs.times, lhs.times);
+                            if (compare == 0) {
+                                compare = Long.compare(rhs.elapsedTime, lhs.elapsedTime);
+                            }
+                            return compare;
+                        }
+                    });
+                    for (AccountSyncStats stats: sortedAccounts) {
+                        elapsedTime = stats.elapsedTime;
+                        times = stats.times;
+                        timeStr = String.format("%ds/%d%%",
+                                elapsedTime / 1000,
+                                elapsedTime * 100 / totalElapsedTime);
+                        timesStr = String.format("%d/%d%%",
+                                times,
+                                times * 100 / totalTimes);
+                        pw.printf(accountFormat, stats.name, timesStr, timeStr);
+                    }
+                    pw.println(separator);
+                }
+            }
+
+            pw.println();
+            pw.println("Recent Sync History");
+            final String format = "  %-" + maxAccount + "s  %-" + maxAuthority + "s %s\n";
+            final Map<String, Long> lastTimeMap = Maps.newHashMap();
+            final PackageManager pm = mContext.getPackageManager();
+            for (int i = 0; i < N; i++) {
+                SyncStorageEngine.SyncHistoryItem item = items.get(i);
+                SyncStorageEngine.AuthorityInfo authority
+                        = mSyncStorageEngine.getAuthority(item.authorityId);
+                final String authorityName;
+                final String accountKey;
+                if (authority != null) {
+                    authorityName = authority.authority;
+                    accountKey = authority.account.name + "/" + authority.account.type
+                            + " u" + authority.userId;
+                } else {
+                    authorityName = "Unknown";
+                    accountKey = "Unknown";
+                }
+                final long elapsedTime = item.elapsedTime;
+                final Time time = new Time();
+                final long eventTime = item.eventTime;
+                time.set(eventTime);
+
+                final String key = authorityName + "/" + accountKey;
+                final Long lastEventTime = lastTimeMap.get(key);
+                final String diffString;
+                if (lastEventTime == null) {
+                    diffString = "";
+                } else {
+                    final long diff = (lastEventTime - eventTime) / 1000;
+                    if (diff < 60) {
+                        diffString = String.valueOf(diff);
+                    } else if (diff < 3600) {
+                        diffString = String.format("%02d:%02d", diff / 60, diff % 60);
+                    } else {
+                        final long sec = diff % 3600;
+                        diffString = String.format("%02d:%02d:%02d",
+                                diff / 3600, sec / 60, sec % 60);
+                    }
+                }
+                lastTimeMap.put(key, eventTime);
+
+                pw.printf("  #%-3d: %s %8s  %5.1fs  %8s",
+                        i + 1,
+                        formatTime(eventTime),
+                        SyncStorageEngine.SOURCES[item.source],
+                        ((float) elapsedTime) / 1000,
+                        diffString);
+                pw.printf(format, accountKey, authorityName,
+                        SyncOperation.reasonToString(pm, item.reason));
+
+                if (item.event != SyncStorageEngine.EVENT_STOP
+                        || item.upstreamActivity != 0
+                        || item.downstreamActivity != 0) {
+                    pw.printf("    event=%d upstreamActivity=%d downstreamActivity=%d\n",
+                            item.event,
+                            item.upstreamActivity,
+                            item.downstreamActivity);
+                }
+                if (item.mesg != null
+                        && !SyncStorageEngine.MESG_SUCCESS.equals(item.mesg)) {
+                    pw.printf("    mesg=%s\n", item.mesg);
+                }
+            }
+            pw.println();
+            pw.println("Recent Sync History Extras");
+            for (int i = 0; i < N; i++) {
+                final SyncStorageEngine.SyncHistoryItem item = items.get(i);
+                final Bundle extras = item.extras;
+                if (extras == null || extras.size() == 0) {
+                    continue;
+                }
+                final SyncStorageEngine.AuthorityInfo authority
+                        = mSyncStorageEngine.getAuthority(item.authorityId);
+                final String authorityName;
+                final String accountKey;
+                if (authority != null) {
+                    authorityName = authority.authority;
+                    accountKey = authority.account.name + "/" + authority.account.type
+                            + " u" + authority.userId;
+                } else {
+                    authorityName = "Unknown";
+                    accountKey = "Unknown";
+                }
+                final Time time = new Time();
+                final long eventTime = item.eventTime;
+                time.set(eventTime);
+
+                pw.printf("  #%-3d: %s %8s ",
+                        i + 1,
+                        formatTime(eventTime),
+                        SyncStorageEngine.SOURCES[item.source]);
+
+                pw.printf(format, accountKey, authorityName, extras);
+            }
+        }
+    }
+
+    private void dumpDayStatistics(PrintWriter pw) {
+        SyncStorageEngine.DayStats dses[] = mSyncStorageEngine.getDayStatistics();
+        if (dses != null && dses[0] != null) {
+            pw.println();
+            pw.println("Sync Statistics");
+            pw.print("  Today:  "); dumpDayStatistic(pw, dses[0]);
+            int today = dses[0].day;
+            int i;
+            SyncStorageEngine.DayStats ds;
+
+            // Print each day in the current week.
+            for (i=1; i<=6 && i < dses.length; i++) {
+                ds = dses[i];
+                if (ds == null) break;
+                int delta = today-ds.day;
+                if (delta > 6) break;
+
+                pw.print("  Day-"); pw.print(delta); pw.print(":  ");
+                dumpDayStatistic(pw, ds);
+            }
+
+            // Aggregate all following days into weeks and print totals.
+            int weekDay = today;
+            while (i < dses.length) {
+                SyncStorageEngine.DayStats aggr = null;
+                weekDay -= 7;
+                while (i < dses.length) {
+                    ds = dses[i];
+                    if (ds == null) {
+                        i = dses.length;
+                        break;
+                    }
+                    int delta = weekDay-ds.day;
+                    if (delta > 6) break;
+                    i++;
+
+                    if (aggr == null) {
+                        aggr = new SyncStorageEngine.DayStats(weekDay);
+                    }
+                    aggr.successCount += ds.successCount;
+                    aggr.successTime += ds.successTime;
+                    aggr.failureCount += ds.failureCount;
+                    aggr.failureTime += ds.failureTime;
+                }
+                if (aggr != null) {
+                    pw.print("  Week-"); pw.print((today-weekDay)/7); pw.print(": ");
+                    dumpDayStatistic(pw, aggr);
+                }
+            }
+        }
+    }
+
+    private void dumpSyncAdapters(IndentingPrintWriter pw) {
+        pw.println();
+        final List<UserInfo> users = getAllUsers();
+        if (users != null) {
+            for (UserInfo user : users) {
+                pw.println("Sync adapters for " + user + ":");
+                pw.increaseIndent();
+                for (RegisteredServicesCache.ServiceInfo<?> info :
+                        mSyncAdapters.getAllServices(user.id)) {
+                    pw.println(info);
+                }
+                pw.decreaseIndent();
+                pw.println();
+            }
+        }
+    }
+
+    private static class AuthoritySyncStats {
+        String name;
+        long elapsedTime;
+        int times;
+        Map<String, AccountSyncStats> accountMap = Maps.newHashMap();
+
+        private AuthoritySyncStats(String name) {
+            this.name = name;
+        }
+    }
+
+    private static class AccountSyncStats {
+        String name;
+        long elapsedTime;
+        int times;
+
+        private AccountSyncStats(String name) {
+            this.name = name;
+        }
+    }
+
+    /**
+     * A helper object to keep track of the time we have spent syncing since the last boot
+     */
+    private class SyncTimeTracker {
+        /** True if a sync was in progress on the most recent call to update() */
+        boolean mLastWasSyncing = false;
+        /** Used to track when lastWasSyncing was last set */
+        long mWhenSyncStarted = 0;
+        /** The cumulative time we have spent syncing */
+        private long mTimeSpentSyncing;
+
+        /** Call to let the tracker know that the sync state may have changed */
+        public synchronized void update() {
+            final boolean isSyncInProgress = !mActiveSyncContexts.isEmpty();
+            if (isSyncInProgress == mLastWasSyncing) return;
+            final long now = SystemClock.elapsedRealtime();
+            if (isSyncInProgress) {
+                mWhenSyncStarted = now;
+            } else {
+                mTimeSpentSyncing += now - mWhenSyncStarted;
+            }
+            mLastWasSyncing = isSyncInProgress;
+        }
+
+        /** Get how long we have been syncing, in ms */
+        public synchronized long timeSpentSyncing() {
+            if (!mLastWasSyncing) return mTimeSpentSyncing;
+
+            final long now = SystemClock.elapsedRealtime();
+            return mTimeSpentSyncing + (now - mWhenSyncStarted);
+        }
+    }
+
+    class ServiceConnectionData {
+        public final ActiveSyncContext activeSyncContext;
+        public final ISyncAdapter syncAdapter;
+        ServiceConnectionData(ActiveSyncContext activeSyncContext, ISyncAdapter syncAdapter) {
+            this.activeSyncContext = activeSyncContext;
+            this.syncAdapter = syncAdapter;
+        }
+    }
+
+    /**
+     * Handles SyncOperation Messages that are posted to the associated
+     * HandlerThread.
+     */
+    class SyncHandler extends Handler {
+        // Messages that can be sent on mHandler
+        private static final int MESSAGE_SYNC_FINISHED = 1;
+        private static final int MESSAGE_SYNC_ALARM = 2;
+        private static final int MESSAGE_CHECK_ALARMS = 3;
+        private static final int MESSAGE_SERVICE_CONNECTED = 4;
+        private static final int MESSAGE_SERVICE_DISCONNECTED = 5;
+        private static final int MESSAGE_CANCEL = 6;
+
+        public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo();
+        private Long mAlarmScheduleTime = null;
+        public final SyncTimeTracker mSyncTimeTracker = new SyncTimeTracker();
+        private final HashMap<Pair<Account, String>, PowerManager.WakeLock> mWakeLocks =
+                Maps.newHashMap();
+
+        private volatile CountDownLatch mReadyToRunLatch = new CountDownLatch(1);
+
+        public void onBootCompleted() {
+            mBootCompleted = true;
+
+            doDatabaseCleanup();
+
+            if (mReadyToRunLatch != null) {
+                mReadyToRunLatch.countDown();
+            }
+        }
+
+        private PowerManager.WakeLock getSyncWakeLock(Account account, String authority) {
+            final Pair<Account, String> wakeLockKey = Pair.create(account, authority);
+            PowerManager.WakeLock wakeLock = mWakeLocks.get(wakeLockKey);
+            if (wakeLock == null) {
+                final String name = SYNC_WAKE_LOCK_PREFIX + "_" + authority + "_" + account;
+                wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
+                wakeLock.setReferenceCounted(false);
+                mWakeLocks.put(wakeLockKey, wakeLock);
+            }
+            return wakeLock;
+        }
+
+        private void waitUntilReadyToRun() {
+            CountDownLatch latch = mReadyToRunLatch;
+            if (latch != null) {
+                while (true) {
+                    try {
+                        latch.await();
+                        mReadyToRunLatch = null;
+                        return;
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                    }
+                }
+            }
+        }
+        /**
+         * Used to keep track of whether a sync notification is active and who it is for.
+         */
+        class SyncNotificationInfo {
+            // true iff the notification manager has been asked to send the notification
+            public boolean isActive = false;
+
+            // Set when we transition from not running a sync to running a sync, and cleared on
+            // the opposite transition.
+            public Long startTime = null;
+
+            public void toString(StringBuilder sb) {
+                sb.append("isActive ").append(isActive).append(", startTime ").append(startTime);
+            }
+
+            @Override
+            public String toString() {
+                StringBuilder sb = new StringBuilder();
+                toString(sb);
+                return sb.toString();
+            }
+        }
+
+        public SyncHandler(Looper looper) {
+            super(looper);
+        }
+
+        public void handleMessage(Message msg) {
+            long earliestFuturePollTime = Long.MAX_VALUE;
+            long nextPendingSyncTime = Long.MAX_VALUE;
+
+            // Setting the value here instead of a method because we want the dumpsys logs
+            // to have the most recent value used.
+            try {
+                waitUntilReadyToRun();
+                mDataConnectionIsConnected = readDataConnectionState();
+                mSyncManagerWakeLock.acquire();
+                // Always do this first so that we be sure that any periodic syncs that
+                // are ready to run have been converted into pending syncs. This allows the
+                // logic that considers the next steps to take based on the set of pending syncs
+                // to also take into account the periodic syncs.
+                earliestFuturePollTime = scheduleReadyPeriodicSyncs();
+                switch (msg.what) {
+                    case SyncHandler.MESSAGE_CANCEL: {
+                        Pair<Account, String> payload = (Pair<Account, String>)msg.obj;
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CANCEL: "
+                                    + payload.first + ", " + payload.second);
+                        }
+                        cancelActiveSyncLocked(payload.first, msg.arg1, payload.second);
+                        nextPendingSyncTime = maybeStartNextSyncLocked();
+                        break;
+                    }
+
+                    case SyncHandler.MESSAGE_SYNC_FINISHED:
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_SYNC_FINISHED");
+                        }
+                        SyncHandlerMessagePayload payload = (SyncHandlerMessagePayload)msg.obj;
+                        if (!isSyncStillActive(payload.activeSyncContext)) {
+                            Log.d(TAG, "handleSyncHandlerMessage: dropping since the "
+                                    + "sync is no longer active: "
+                                    + payload.activeSyncContext);
+                            break;
+                        }
+                        runSyncFinishedOrCanceledLocked(payload.syncResult, payload.activeSyncContext);
+
+                        // since a sync just finished check if it is time to start a new sync
+                        nextPendingSyncTime = maybeStartNextSyncLocked();
+                        break;
+
+                    case SyncHandler.MESSAGE_SERVICE_CONNECTED: {
+                        ServiceConnectionData msgData = (ServiceConnectionData)msg.obj;
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CONNECTED: "
+                                    + msgData.activeSyncContext);
+                        }
+                        // check that this isn't an old message
+                        if (isSyncStillActive(msgData.activeSyncContext)) {
+                            runBoundToSyncAdapter(msgData.activeSyncContext, msgData.syncAdapter);
+                        }
+                        break;
+                    }
+
+                    case SyncHandler.MESSAGE_SERVICE_DISCONNECTED: {
+                        final ActiveSyncContext currentSyncContext =
+                                ((ServiceConnectionData)msg.obj).activeSyncContext;
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_DISCONNECTED: "
+                                    + currentSyncContext);
+                        }
+                        // check that this isn't an old message
+                        if (isSyncStillActive(currentSyncContext)) {
+                            // cancel the sync if we have a syncadapter, which means one is
+                            // outstanding
+                            if (currentSyncContext.mSyncAdapter != null) {
+                                try {
+                                    currentSyncContext.mSyncAdapter.cancelSync(currentSyncContext);
+                                } catch (RemoteException e) {
+                                    // we don't need to retry this in this case
+                                }
+                            }
+
+                            // pretend that the sync failed with an IOException,
+                            // which is a soft error
+                            SyncResult syncResult = new SyncResult();
+                            syncResult.stats.numIoExceptions++;
+                            runSyncFinishedOrCanceledLocked(syncResult, currentSyncContext);
+
+                            // since a sync just finished check if it is time to start a new sync
+                            nextPendingSyncTime = maybeStartNextSyncLocked();
+                        }
+
+                        break;
+                    }
+
+                    case SyncHandler.MESSAGE_SYNC_ALARM: {
+                        boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+                        if (isLoggable) {
+                            Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_SYNC_ALARM");
+                        }
+                        mAlarmScheduleTime = null;
+                        try {
+                            nextPendingSyncTime = maybeStartNextSyncLocked();
+                        } finally {
+                            mHandleAlarmWakeLock.release();
+                        }
+                        break;
+                    }
+
+                    case SyncHandler.MESSAGE_CHECK_ALARMS:
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_CHECK_ALARMS");
+                        }
+                        nextPendingSyncTime = maybeStartNextSyncLocked();
+                        break;
+                }
+            } finally {
+                manageSyncNotificationLocked();
+                manageSyncAlarmLocked(earliestFuturePollTime, nextPendingSyncTime);
+                mSyncTimeTracker.update();
+                mSyncManagerWakeLock.release();
+            }
+        }
+
+        /**
+         * Turn any periodic sync operations that are ready to run into pending sync operations.
+         * @return the desired start time of the earliest future  periodic sync operation,
+         * in milliseconds since boot
+         */
+        private long scheduleReadyPeriodicSyncs() {
+            final boolean backgroundDataUsageAllowed =
+                    getConnectivityManager().getBackgroundDataSetting();
+            long earliestFuturePollTime = Long.MAX_VALUE;
+            if (!backgroundDataUsageAllowed) {
+                return earliestFuturePollTime;
+            }
+
+            AccountAndUser[] accounts = mRunningAccounts;
+
+            final long nowAbsolute = System.currentTimeMillis();
+            final long shiftedNowAbsolute = (0 < nowAbsolute - mSyncRandomOffsetMillis)
+                                               ? (nowAbsolute  - mSyncRandomOffsetMillis) : 0;
+
+            ArrayList<SyncStorageEngine.AuthorityInfo> infos = mSyncStorageEngine.getAuthorities();
+            for (SyncStorageEngine.AuthorityInfo info : infos) {
+                // skip the sync if the account of this operation no longer exists
+                if (!containsAccountAndUser(accounts, info.account, info.userId)) {
+                    continue;
+                }
+
+                if (!mSyncStorageEngine.getMasterSyncAutomatically(info.userId)
+                        || !mSyncStorageEngine.getSyncAutomatically(info.account, info.userId,
+                                info.authority)) {
+                    continue;
+                }
+
+                if (mSyncStorageEngine.getIsSyncable(info.account, info.userId, info.authority)
+                        == 0) {
+                    continue;
+                }
+
+                SyncStatusInfo status = mSyncStorageEngine.getOrCreateSyncStatus(info);
+                for (int i = 0, N = info.periodicSyncs.size(); i < N; i++) {
+                    final Bundle extras = info.periodicSyncs.get(i).first;
+                    final Long periodInMillis = info.periodicSyncs.get(i).second * 1000;
+                    // find when this periodic sync was last scheduled to run
+                    final long lastPollTimeAbsolute = status.getPeriodicSyncTime(i);
+
+                    long remainingMillis
+                            = periodInMillis - (shiftedNowAbsolute % periodInMillis);
+
+                    /*
+                     * Sync scheduling strategy:
+                     *    Set the next periodic sync based on a random offset (in seconds).
+                     *
+                     *    Also sync right now if any of the following cases hold
+                     *    and mark it as having been scheduled
+                     *
+                     * Case 1:  This sync is ready to run now.
+                     * Case 2:  If the lastPollTimeAbsolute is in the future,
+                     *          sync now and reinitialize. This can happen for
+                     *          example if the user changed the time, synced and
+                     *          changed back.
+                     * Case 3:  If we failed to sync at the last scheduled time
+                     */
+                    if (remainingMillis == periodInMillis  // Case 1
+                            || lastPollTimeAbsolute > nowAbsolute // Case 2
+                            || (nowAbsolute - lastPollTimeAbsolute
+                                    >= periodInMillis)) { // Case 3
+                        // Sync now
+                        final Pair<Long, Long> backoff = mSyncStorageEngine.getBackoff(
+                                info.account, info.userId, info.authority);
+                        final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo;
+                        syncAdapterInfo = mSyncAdapters.getServiceInfo(
+                                SyncAdapterType.newKey(info.authority, info.account.type),
+                                info.userId);
+                        if (syncAdapterInfo == null) {
+                            continue;
+                        }
+                        scheduleSyncOperation(
+                                new SyncOperation(info.account, info.userId,
+                                        SyncOperation.REASON_PERIODIC,
+                                        SyncStorageEngine.SOURCE_PERIODIC,
+                                        info.authority, extras, 0 /* delay */,
+                                        backoff != null ? backoff.first : 0,
+                                        mSyncStorageEngine.getDelayUntilTime(
+                                                info.account, info.userId, info.authority),
+                                        syncAdapterInfo.type.allowParallelSyncs()));
+                        status.setPeriodicSyncTime(i, nowAbsolute);
+                    }
+                    // Compute when this periodic sync should next run
+                    final long nextPollTimeAbsolute = nowAbsolute + remainingMillis;
+
+                    // remember this time if it is earlier than earliestFuturePollTime
+                    if (nextPollTimeAbsolute < earliestFuturePollTime) {
+                        earliestFuturePollTime = nextPollTimeAbsolute;
+                    }
+                }
+            }
+
+            if (earliestFuturePollTime == Long.MAX_VALUE) {
+                return Long.MAX_VALUE;
+            }
+
+            // convert absolute time to elapsed time
+            return SystemClock.elapsedRealtime()
+                    + ((earliestFuturePollTime < nowAbsolute)
+                      ? 0
+                      : (earliestFuturePollTime - nowAbsolute));
+        }
+
+        private long maybeStartNextSyncLocked() {
+            final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+            if (isLoggable) Log.v(TAG, "maybeStartNextSync");
+
+            // If we aren't ready to run (e.g. the data connection is down), get out.
+            if (!mDataConnectionIsConnected) {
+                if (isLoggable) {
+                    Log.v(TAG, "maybeStartNextSync: no data connection, skipping");
+                }
+                return Long.MAX_VALUE;
+            }
+
+            if (mStorageIsLow) {
+                if (isLoggable) {
+                    Log.v(TAG, "maybeStartNextSync: memory low, skipping");
+                }
+                return Long.MAX_VALUE;
+            }
+
+            // If the accounts aren't known yet then we aren't ready to run. We will be kicked
+            // when the account lookup request does complete.
+            AccountAndUser[] accounts = mRunningAccounts;
+            if (accounts == INITIAL_ACCOUNTS_ARRAY) {
+                if (isLoggable) {
+                    Log.v(TAG, "maybeStartNextSync: accounts not known, skipping");
+                }
+                return Long.MAX_VALUE;
+            }
+
+            // Otherwise consume SyncOperations from the head of the SyncQueue until one is
+            // found that is runnable (not disabled, etc). If that one is ready to run then
+            // start it, otherwise just get out.
+            final boolean backgroundDataUsageAllowed =
+                    getConnectivityManager().getBackgroundDataSetting();
+
+            final long now = SystemClock.elapsedRealtime();
+
+            // will be set to the next time that a sync should be considered for running
+            long nextReadyToRunTime = Long.MAX_VALUE;
+
+            // order the sync queue, dropping syncs that are not allowed
+            ArrayList<SyncOperation> operations = new ArrayList<SyncOperation>();
+            synchronized (mSyncQueue) {
+                if (isLoggable) {
+                    Log.v(TAG, "build the operation array, syncQueue size is "
+                        + mSyncQueue.getOperations().size());
+                }
+                final Iterator<SyncOperation> operationIterator = mSyncQueue.getOperations()
+                        .iterator();
+
+                final ActivityManager activityManager
+                        = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+                final Set<Integer> removedUsers = Sets.newHashSet();
+                while (operationIterator.hasNext()) {
+                    final SyncOperation op = operationIterator.next();
+
+                    // drop the sync if the account of this operation no longer exists
+                    if (!containsAccountAndUser(accounts, op.account, op.userId)) {
+                        operationIterator.remove();
+                        mSyncStorageEngine.deleteFromPending(op.pendingOperation);
+                        continue;
+                    }
+
+                    // drop this sync request if it isn't syncable
+                    int syncableState = mSyncStorageEngine.getIsSyncable(
+                            op.account, op.userId, op.authority);
+                    if (syncableState == 0) {
+                        operationIterator.remove();
+                        mSyncStorageEngine.deleteFromPending(op.pendingOperation);
+                        continue;
+                    }
+
+                    // if the user in not running, drop the request
+                    if (!activityManager.isUserRunning(op.userId)) {
+                        final UserInfo userInfo = mUserManager.getUserInfo(op.userId);
+                        if (userInfo == null) {
+                            removedUsers.add(op.userId);
+                        }
+                        continue;
+                    }
+
+                    // if the next run time is in the future, meaning there are no syncs ready
+                    // to run, return the time
+                    if (op.effectiveRunTime > now) {
+                        if (nextReadyToRunTime > op.effectiveRunTime) {
+                            nextReadyToRunTime = op.effectiveRunTime;
+                        }
+                        continue;
+                    }
+
+                    final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo;
+                    syncAdapterInfo = mSyncAdapters.getServiceInfo(
+                            SyncAdapterType.newKey(op.authority, op.account.type), op.userId);
+
+                    // only proceed if network is connected for requesting UID
+                    final boolean uidNetworkConnected;
+                    if (syncAdapterInfo != null) {
+                        final NetworkInfo networkInfo = getConnectivityManager()
+                                .getActiveNetworkInfoForUid(syncAdapterInfo.uid);
+                        uidNetworkConnected = networkInfo != null && networkInfo.isConnected();
+                    } else {
+                        uidNetworkConnected = false;
+                    }
+
+                    // skip the sync if it isn't manual, and auto sync or
+                    // background data usage is disabled or network is
+                    // disconnected for the target UID.
+                    if (!op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false)
+                            && (syncableState > 0)
+                            && (!mSyncStorageEngine.getMasterSyncAutomatically(op.userId)
+                                || !backgroundDataUsageAllowed
+                                || !uidNetworkConnected
+                                || !mSyncStorageEngine.getSyncAutomatically(
+                                       op.account, op.userId, op.authority))) {
+                        operationIterator.remove();
+                        mSyncStorageEngine.deleteFromPending(op.pendingOperation);
+                        continue;
+                    }
+
+                    operations.add(op);
+                }
+                for (Integer user : removedUsers) {
+                    // if it's still removed
+                    if (mUserManager.getUserInfo(user) == null) {
+                        onUserRemoved(user);
+                    }
+                }
+            }
+
+            // find the next operation to dispatch, if one is ready
+            // iterate from the top, keep issuing (while potentially cancelling existing syncs)
+            // until the quotas are filled.
+            // once the quotas are filled iterate once more to find when the next one would be
+            // (also considering pre-emption reasons).
+            if (isLoggable) Log.v(TAG, "sort the candidate operations, size " + operations.size());
+            Collections.sort(operations);
+            if (isLoggable) Log.v(TAG, "dispatch all ready sync operations");
+            for (int i = 0, N = operations.size(); i < N; i++) {
+                final SyncOperation candidate = operations.get(i);
+                final boolean candidateIsInitialization = candidate.isInitialization();
+
+                int numInit = 0;
+                int numRegular = 0;
+                ActiveSyncContext conflict = null;
+                ActiveSyncContext longRunning = null;
+                ActiveSyncContext toReschedule = null;
+                ActiveSyncContext oldestNonExpeditedRegular = null;
+
+                for (ActiveSyncContext activeSyncContext : mActiveSyncContexts) {
+                    final SyncOperation activeOp = activeSyncContext.mSyncOperation;
+                    if (activeOp.isInitialization()) {
+                        numInit++;
+                    } else {
+                        numRegular++;
+                        if (!activeOp.isExpedited()) {
+                            if (oldestNonExpeditedRegular == null
+                                || (oldestNonExpeditedRegular.mStartTime
+                                    > activeSyncContext.mStartTime)) {
+                                oldestNonExpeditedRegular = activeSyncContext;
+                            }
+                        }
+                    }
+                    if (activeOp.account.type.equals(candidate.account.type)
+                            && activeOp.authority.equals(candidate.authority)
+                            && activeOp.userId == candidate.userId
+                            && (!activeOp.allowParallelSyncs
+                                || activeOp.account.name.equals(candidate.account.name))) {
+                        conflict = activeSyncContext;
+                        // don't break out since we want to do a full count of the varieties
+                    } else {
+                        if (candidateIsInitialization == activeOp.isInitialization()
+                                && activeSyncContext.mStartTime + MAX_TIME_PER_SYNC < now) {
+                            longRunning = activeSyncContext;
+                            // don't break out since we want to do a full count of the varieties
+                        }
+                    }
+                }
+
+                if (isLoggable) {
+                    Log.v(TAG, "candidate " + (i + 1) + " of " + N + ": " + candidate);
+                    Log.v(TAG, "  numActiveInit=" + numInit + ", numActiveRegular=" + numRegular);
+                    Log.v(TAG, "  longRunning: " + longRunning);
+                    Log.v(TAG, "  conflict: " + conflict);
+                    Log.v(TAG, "  oldestNonExpeditedRegular: " + oldestNonExpeditedRegular);
+                }
+
+                final boolean roomAvailable = candidateIsInitialization
+                        ? numInit < MAX_SIMULTANEOUS_INITIALIZATION_SYNCS
+                        : numRegular < MAX_SIMULTANEOUS_REGULAR_SYNCS;
+
+                if (conflict != null) {
+                    if (candidateIsInitialization && !conflict.mSyncOperation.isInitialization()
+                            && numInit < MAX_SIMULTANEOUS_INITIALIZATION_SYNCS) {
+                        toReschedule = conflict;
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "canceling and rescheduling sync since an initialization "
+                                    + "takes higher priority, " + conflict);
+                        }
+                    } else if (candidate.expedited && !conflict.mSyncOperation.expedited
+                            && (candidateIsInitialization
+                                == conflict.mSyncOperation.isInitialization())) {
+                        toReschedule = conflict;
+                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                            Log.v(TAG, "canceling and rescheduling sync since an expedited "
+                                    + "takes higher priority, " + conflict);
+                        }
+                    } else {
+                        continue;
+                    }
+                } else if (roomAvailable) {
+                    // dispatch candidate
+                } else if (candidate.isExpedited() && oldestNonExpeditedRegular != null
+                           && !candidateIsInitialization) {
+                    // We found an active, non-expedited regular sync. We also know that the
+                    // candidate doesn't conflict with this active sync since conflict
+                    // is null. Reschedule the active sync and start the candidate.
+                    toReschedule = oldestNonExpeditedRegular;
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "canceling and rescheduling sync since an expedited is ready to run, "
+                                + oldestNonExpeditedRegular);
+                    }
+                } else if (longRunning != null
+                        && (candidateIsInitialization
+                            == longRunning.mSyncOperation.isInitialization())) {
+                    // We found an active, long-running sync. Reschedule the active
+                    // sync and start the candidate.
+                    toReschedule = longRunning;
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "canceling and rescheduling sync since it ran roo long, "
+                              + longRunning);
+                    }
+                } else {
+                    // we were unable to find or make space to run this candidate, go on to
+                    // the next one
+                    continue;
+                }
+
+                if (toReschedule != null) {
+                    runSyncFinishedOrCanceledLocked(null, toReschedule);
+                    scheduleSyncOperation(toReschedule.mSyncOperation);
+                }
+                synchronized (mSyncQueue) {
+                    mSyncQueue.remove(candidate);
+                }
+                dispatchSyncOperation(candidate);
+            }
+
+            return nextReadyToRunTime;
+     }
+
+        private boolean dispatchSyncOperation(SyncOperation op) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "dispatchSyncOperation: we are going to sync " + op);
+                Log.v(TAG, "num active syncs: " + mActiveSyncContexts.size());
+                for (ActiveSyncContext syncContext : mActiveSyncContexts) {
+                    Log.v(TAG, syncContext.toString());
+                }
+            }
+
+            // connect to the sync adapter
+            SyncAdapterType syncAdapterType = SyncAdapterType.newKey(op.authority, op.account.type);
+            final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo;
+            syncAdapterInfo = mSyncAdapters.getServiceInfo(syncAdapterType, op.userId);
+            if (syncAdapterInfo == null) {
+                Log.d(TAG, "can't find a sync adapter for " + syncAdapterType
+                        + ", removing settings for it");
+                mSyncStorageEngine.removeAuthority(op.account, op.userId, op.authority);
+                return false;
+            }
+
+            ActiveSyncContext activeSyncContext =
+                    new ActiveSyncContext(op, insertStartSyncEvent(op), syncAdapterInfo.uid);
+            activeSyncContext.mSyncInfo = mSyncStorageEngine.addActiveSync(activeSyncContext);
+            mActiveSyncContexts.add(activeSyncContext);
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "dispatchSyncOperation: starting " + activeSyncContext);
+            }
+            if (!activeSyncContext.bindToSyncAdapter(syncAdapterInfo, op.userId)) {
+                Log.e(TAG, "Bind attempt failed to " + syncAdapterInfo);
+                closeActiveSyncContext(activeSyncContext);
+                return false;
+            }
+
+            return true;
+        }
+
+        private void runBoundToSyncAdapter(final ActiveSyncContext activeSyncContext,
+              ISyncAdapter syncAdapter) {
+            activeSyncContext.mSyncAdapter = syncAdapter;
+            final SyncOperation syncOperation = activeSyncContext.mSyncOperation;
+            try {
+                activeSyncContext.mIsLinkedToDeath = true;
+                syncAdapter.asBinder().linkToDeath(activeSyncContext, 0);
+
+                syncAdapter.startSync(activeSyncContext, syncOperation.authority,
+                        syncOperation.account, syncOperation.extras);
+            } catch (RemoteException remoteExc) {
+                Log.d(TAG, "maybeStartNextSync: caught a RemoteException, rescheduling", remoteExc);
+                closeActiveSyncContext(activeSyncContext);
+                increaseBackoffSetting(syncOperation);
+                scheduleSyncOperation(new SyncOperation(syncOperation));
+            } catch (RuntimeException exc) {
+                closeActiveSyncContext(activeSyncContext);
+                Log.e(TAG, "Caught RuntimeException while starting the sync " + syncOperation, exc);
+            }
+        }
+
+        private void cancelActiveSyncLocked(Account account, int userId, String authority) {
+            ArrayList<ActiveSyncContext> activeSyncs =
+                    new ArrayList<ActiveSyncContext>(mActiveSyncContexts);
+            for (ActiveSyncContext activeSyncContext : activeSyncs) {
+                if (activeSyncContext != null) {
+                    // if an account was specified then only cancel the sync if it matches
+                    if (account != null) {
+                        if (!account.equals(activeSyncContext.mSyncOperation.account)) {
+                            continue;
+                        }
+                    }
+                    // if an authority was specified then only cancel the sync if it matches
+                    if (authority != null) {
+                        if (!authority.equals(activeSyncContext.mSyncOperation.authority)) {
+                            continue;
+                        }
+                    }
+                    // check if the userid matches
+                    if (userId != UserHandle.USER_ALL
+                            && userId != activeSyncContext.mSyncOperation.userId) {
+                        continue;
+                    }
+                    runSyncFinishedOrCanceledLocked(null /* no result since this is a cancel */,
+                            activeSyncContext);
+                }
+            }
+        }
+
+        private void runSyncFinishedOrCanceledLocked(SyncResult syncResult,
+                ActiveSyncContext activeSyncContext) {
+            boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
+
+            if (activeSyncContext.mIsLinkedToDeath) {
+                activeSyncContext.mSyncAdapter.asBinder().unlinkToDeath(activeSyncContext, 0);
+                activeSyncContext.mIsLinkedToDeath = false;
+            }
+            closeActiveSyncContext(activeSyncContext);
+
+            final SyncOperation syncOperation = activeSyncContext.mSyncOperation;
+
+            final long elapsedTime = SystemClock.elapsedRealtime() - activeSyncContext.mStartTime;
+
+            String historyMessage;
+            int downstreamActivity;
+            int upstreamActivity;
+            if (syncResult != null) {
+                if (isLoggable) {
+                    Log.v(TAG, "runSyncFinishedOrCanceled [finished]: "
+                            + syncOperation + ", result " + syncResult);
+                }
+
+                if (!syncResult.hasError()) {
+                    historyMessage = SyncStorageEngine.MESG_SUCCESS;
+                    // TODO: set these correctly when the SyncResult is extended to include it
+                    downstreamActivity = 0;
+                    upstreamActivity = 0;
+                    clearBackoffSetting(syncOperation);
+                } else {
+                    Log.d(TAG, "failed sync operation " + syncOperation + ", " + syncResult);
+                    // the operation failed so increase the backoff time
+                    if (!syncResult.syncAlreadyInProgress) {
+                        increaseBackoffSetting(syncOperation);
+                    }
+                    // reschedule the sync if so indicated by the syncResult
+                    maybeRescheduleSync(syncResult, syncOperation);
+                    historyMessage = ContentResolver.syncErrorToString(
+                            syncResultToErrorNumber(syncResult));
+                    // TODO: set these correctly when the SyncResult is extended to include it
+                    downstreamActivity = 0;
+                    upstreamActivity = 0;
+                }
+
+                setDelayUntilTime(syncOperation, syncResult.delayUntil);
+            } else {
+                if (isLoggable) {
+                    Log.v(TAG, "runSyncFinishedOrCanceled [canceled]: " + syncOperation);
+                }
+                if (activeSyncContext.mSyncAdapter != null) {
+                    try {
+                        activeSyncContext.mSyncAdapter.cancelSync(activeSyncContext);
+                    } catch (RemoteException e) {
+                        // we don't need to retry this in this case
+                    }
+                }
+                historyMessage = SyncStorageEngine.MESG_CANCELED;
+                downstreamActivity = 0;
+                upstreamActivity = 0;
+            }
+
+            stopSyncEvent(activeSyncContext.mHistoryRowId, syncOperation, historyMessage,
+                    upstreamActivity, downstreamActivity, elapsedTime);
+
+            if (syncResult != null && syncResult.tooManyDeletions) {
+                installHandleTooManyDeletesNotification(syncOperation.account,
+                        syncOperation.authority, syncResult.stats.numDeletes,
+                        syncOperation.userId);
+            } else {
+                mNotificationMgr.cancelAsUser(null,
+                        syncOperation.account.hashCode() ^ syncOperation.authority.hashCode(),
+                        new UserHandle(syncOperation.userId));
+            }
+
+            if (syncResult != null && syncResult.fullSyncRequested) {
+                scheduleSyncOperation(new SyncOperation(syncOperation.account, syncOperation.userId,
+                        syncOperation.reason,
+                        syncOperation.syncSource, syncOperation.authority, new Bundle(), 0,
+                        syncOperation.backoff, syncOperation.delayUntil,
+                        syncOperation.allowParallelSyncs));
+            }
+            // no need to schedule an alarm, as that will be done by our caller.
+        }
+
+        private void closeActiveSyncContext(ActiveSyncContext activeSyncContext) {
+            activeSyncContext.close();
+            mActiveSyncContexts.remove(activeSyncContext);
+            mSyncStorageEngine.removeActiveSync(activeSyncContext.mSyncInfo,
+                    activeSyncContext.mSyncOperation.userId);
+        }
+
+        /**
+         * Convert the error-containing SyncResult into the Sync.History error number. Since
+         * the SyncResult may indicate multiple errors at once, this method just returns the
+         * most "serious" error.
+         * @param syncResult the SyncResult from which to read
+         * @return the most "serious" error set in the SyncResult
+         * @throws IllegalStateException if the SyncResult does not indicate any errors.
+         *   If SyncResult.error() is true then it is safe to call this.
+         */
+        private int syncResultToErrorNumber(SyncResult syncResult) {
+            if (syncResult.syncAlreadyInProgress)
+                return ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS;
+            if (syncResult.stats.numAuthExceptions > 0)
+                return ContentResolver.SYNC_ERROR_AUTHENTICATION;
+            if (syncResult.stats.numIoExceptions > 0)
+                return ContentResolver.SYNC_ERROR_IO;
+            if (syncResult.stats.numParseExceptions > 0)
+                return ContentResolver.SYNC_ERROR_PARSE;
+            if (syncResult.stats.numConflictDetectedExceptions > 0)
+                return ContentResolver.SYNC_ERROR_CONFLICT;
+            if (syncResult.tooManyDeletions)
+                return ContentResolver.SYNC_ERROR_TOO_MANY_DELETIONS;
+            if (syncResult.tooManyRetries)
+                return ContentResolver.SYNC_ERROR_TOO_MANY_RETRIES;
+            if (syncResult.databaseError)
+                return ContentResolver.SYNC_ERROR_INTERNAL;
+            throw new IllegalStateException("we are not in an error state, " + syncResult);
+        }
+
+        private void manageSyncNotificationLocked() {
+            boolean shouldCancel;
+            boolean shouldInstall;
+
+            if (mActiveSyncContexts.isEmpty()) {
+                mSyncNotificationInfo.startTime = null;
+
+                // we aren't syncing. if the notification is active then remember that we need
+                // to cancel it and then clear out the info
+                shouldCancel = mSyncNotificationInfo.isActive;
+                shouldInstall = false;
+            } else {
+                // we are syncing
+                final long now = SystemClock.elapsedRealtime();
+                if (mSyncNotificationInfo.startTime == null) {
+                    mSyncNotificationInfo.startTime = now;
+                }
+
+                // there are three cases:
+                // - the notification is up: do nothing
+                // - the notification is not up but it isn't time yet: don't install
+                // - the notification is not up and it is time: need to install
+
+                if (mSyncNotificationInfo.isActive) {
+                    shouldInstall = shouldCancel = false;
+                } else {
+                    // it isn't currently up, so there is nothing to cancel
+                    shouldCancel = false;
+
+                    final boolean timeToShowNotification =
+                            now > mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY;
+                    if (timeToShowNotification) {
+                        shouldInstall = true;
+                    } else {
+                        // show the notification immediately if this is a manual sync
+                        shouldInstall = false;
+                        for (ActiveSyncContext activeSyncContext : mActiveSyncContexts) {
+                            final boolean manualSync = activeSyncContext.mSyncOperation.extras
+                                    .getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
+                            if (manualSync) {
+                                shouldInstall = true;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (shouldCancel && !shouldInstall) {
+                mNeedSyncActiveNotification = false;
+                sendSyncStateIntent();
+                mSyncNotificationInfo.isActive = false;
+            }
+
+            if (shouldInstall) {
+                mNeedSyncActiveNotification = true;
+                sendSyncStateIntent();
+                mSyncNotificationInfo.isActive = true;
+            }
+        }
+
+        private void manageSyncAlarmLocked(long nextPeriodicEventElapsedTime,
+                long nextPendingEventElapsedTime) {
+            // in each of these cases the sync loop will be kicked, which will cause this
+            // method to be called again
+            if (!mDataConnectionIsConnected) return;
+            if (mStorageIsLow) return;
+
+            // When the status bar notification should be raised
+            final long notificationTime =
+                    (!mSyncHandler.mSyncNotificationInfo.isActive
+                            && mSyncHandler.mSyncNotificationInfo.startTime != null)
+                            ? mSyncHandler.mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY
+                            : Long.MAX_VALUE;
+
+            // When we should consider canceling an active sync
+            long earliestTimeoutTime = Long.MAX_VALUE;
+            for (ActiveSyncContext currentSyncContext : mActiveSyncContexts) {
+                final long currentSyncTimeoutTime =
+                        currentSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC;
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "manageSyncAlarm: active sync, mTimeoutStartTime + MAX is "
+                            + currentSyncTimeoutTime);
+                }
+                if (earliestTimeoutTime > currentSyncTimeoutTime) {
+                    earliestTimeoutTime = currentSyncTimeoutTime;
+                }
+            }
+
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "manageSyncAlarm: notificationTime is " + notificationTime);
+            }
+
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "manageSyncAlarm: earliestTimeoutTime is " + earliestTimeoutTime);
+            }
+
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "manageSyncAlarm: nextPeriodicEventElapsedTime is "
+                        + nextPeriodicEventElapsedTime);
+            }
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "manageSyncAlarm: nextPendingEventElapsedTime is "
+                        + nextPendingEventElapsedTime);
+            }
+
+            long alarmTime = Math.min(notificationTime, earliestTimeoutTime);
+            alarmTime = Math.min(alarmTime, nextPeriodicEventElapsedTime);
+            alarmTime = Math.min(alarmTime, nextPendingEventElapsedTime);
+
+            // Bound the alarm time.
+            final long now = SystemClock.elapsedRealtime();
+            if (alarmTime < now + SYNC_ALARM_TIMEOUT_MIN) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "manageSyncAlarm: the alarmTime is too small, "
+                            + alarmTime + ", setting to " + (now + SYNC_ALARM_TIMEOUT_MIN));
+                }
+                alarmTime = now + SYNC_ALARM_TIMEOUT_MIN;
+            } else if (alarmTime > now + SYNC_ALARM_TIMEOUT_MAX) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "manageSyncAlarm: the alarmTime is too large, "
+                            + alarmTime + ", setting to " + (now + SYNC_ALARM_TIMEOUT_MIN));
+                }
+                alarmTime = now + SYNC_ALARM_TIMEOUT_MAX;
+            }
+
+            // determine if we need to set or cancel the alarm
+            boolean shouldSet = false;
+            boolean shouldCancel = false;
+            final boolean alarmIsActive = mAlarmScheduleTime != null;
+            final boolean needAlarm = alarmTime != Long.MAX_VALUE;
+            if (needAlarm) {
+                if (!alarmIsActive || alarmTime < mAlarmScheduleTime) {
+                    shouldSet = true;
+                }
+            } else {
+                shouldCancel = alarmIsActive;
+            }
+
+            // set or cancel the alarm as directed
+            ensureAlarmService();
+            if (shouldSet) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "requesting that the alarm manager wake us up at elapsed time "
+                            + alarmTime + ", now is " + now + ", " + ((alarmTime - now) / 1000)
+                            + " secs from now");
+                }
+                mAlarmScheduleTime = alarmTime;
+                mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTime,
+                        mSyncAlarmIntent);
+            } else if (shouldCancel) {
+                mAlarmScheduleTime = null;
+                mAlarmService.cancel(mSyncAlarmIntent);
+            }
+        }
+
+        private void sendSyncStateIntent() {
+            Intent syncStateIntent = new Intent(Intent.ACTION_SYNC_STATE_CHANGED);
+            syncStateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+            syncStateIntent.putExtra("active", mNeedSyncActiveNotification);
+            syncStateIntent.putExtra("failing", false);
+            mContext.sendBroadcastAsUser(syncStateIntent, UserHandle.OWNER);
+        }
+
+        private void installHandleTooManyDeletesNotification(Account account, String authority,
+                long numDeletes, int userId) {
+            if (mNotificationMgr == null) return;
+
+            final ProviderInfo providerInfo = mContext.getPackageManager().resolveContentProvider(
+                    authority, 0 /* flags */);
+            if (providerInfo == null) {
+                return;
+            }
+            CharSequence authorityName = providerInfo.loadLabel(mContext.getPackageManager());
+
+            Intent clickIntent = new Intent(mContext, SyncActivityTooManyDeletes.class);
+            clickIntent.putExtra("account", account);
+            clickIntent.putExtra("authority", authority);
+            clickIntent.putExtra("provider", authorityName.toString());
+            clickIntent.putExtra("numDeletes", numDeletes);
+
+            if (!isActivityAvailable(clickIntent)) {
+                Log.w(TAG, "No activity found to handle too many deletes.");
+                return;
+            }
+
+            final PendingIntent pendingIntent = PendingIntent
+                    .getActivityAsUser(mContext, 0, clickIntent,
+                            PendingIntent.FLAG_CANCEL_CURRENT, null, new UserHandle(userId));
+
+            CharSequence tooManyDeletesDescFormat = mContext.getResources().getText(
+                    R.string.contentServiceTooManyDeletesNotificationDesc);
+
+            Notification notification =
+                new Notification(R.drawable.stat_notify_sync_error,
+                        mContext.getString(R.string.contentServiceSync),
+                        System.currentTimeMillis());
+            notification.setLatestEventInfo(mContext,
+                    mContext.getString(R.string.contentServiceSyncNotificationTitle),
+                    String.format(tooManyDeletesDescFormat.toString(), authorityName),
+                    pendingIntent);
+            notification.flags |= Notification.FLAG_ONGOING_EVENT;
+            mNotificationMgr.notifyAsUser(null, account.hashCode() ^ authority.hashCode(),
+                    notification, new UserHandle(userId));
+        }
+
+        /**
+         * Checks whether an activity exists on the system image for the given intent.
+         *
+         * @param intent The intent for an activity.
+         * @return Whether or not an activity exists.
+         */
+        private boolean isActivityAvailable(Intent intent) {
+            PackageManager pm = mContext.getPackageManager();
+            List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+            int listSize = list.size();
+            for (int i = 0; i < listSize; i++) {
+                ResolveInfo resolveInfo = list.get(i);
+                if ((resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
+                        != 0) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        public long insertStartSyncEvent(SyncOperation syncOperation) {
+            final int source = syncOperation.syncSource;
+            final long now = System.currentTimeMillis();
+
+            EventLog.writeEvent(2720, syncOperation.authority,
+                                SyncStorageEngine.EVENT_START, source,
+                                syncOperation.account.name.hashCode());
+
+            return mSyncStorageEngine.insertStartSyncEvent(
+                    syncOperation.account, syncOperation.userId, syncOperation.reason,
+                    syncOperation.authority,
+                    now, source, syncOperation.isInitialization(), syncOperation.extras
+            );
+        }
+
+        public void stopSyncEvent(long rowId, SyncOperation syncOperation, String resultMessage,
+                int upstreamActivity, int downstreamActivity, long elapsedTime) {
+            EventLog.writeEvent(2720, syncOperation.authority,
+                                SyncStorageEngine.EVENT_STOP, syncOperation.syncSource,
+                                syncOperation.account.name.hashCode());
+
+            mSyncStorageEngine.stopSyncEvent(rowId, elapsedTime,
+                    resultMessage, downstreamActivity, upstreamActivity);
+        }
+    }
+
+    private boolean isSyncStillActive(ActiveSyncContext activeSyncContext) {
+        for (ActiveSyncContext sync : mActiveSyncContexts) {
+            if (sync == activeSyncContext) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    static class PrintTable {
+        private ArrayList<Object[]> mTable = Lists.newArrayList();
+        private final int mCols;
+
+        PrintTable(int cols) {
+            mCols = cols;
+        }
+
+        void set(int row, int col, Object... values) {
+            if (col + values.length > mCols) {
+                throw new IndexOutOfBoundsException("Table only has " + mCols +
+                        " columns. can't set " + values.length + " at column " + col);
+            }
+            for (int i = mTable.size(); i <= row; i++) {
+                final Object[] list = new Object[mCols];
+                mTable.add(list);
+                for (int j = 0; j < mCols; j++) {
+                    list[j] = "";
+                }
+            }
+            System.arraycopy(values, 0, mTable.get(row), col, values.length);
+        }
+
+        void writeTo(PrintWriter out) {
+            final String[] formats = new String[mCols];
+            int totalLength = 0;
+            for (int col = 0; col < mCols; ++col) {
+                int maxLength = 0;
+                for (Object[] row : mTable) {
+                    final int length = row[col].toString().length();
+                    if (length > maxLength) {
+                        maxLength = length;
+                    }
+                }
+                totalLength += maxLength;
+                formats[col] = String.format("%%-%ds", maxLength);
+            }
+            printRow(out, formats, mTable.get(0));
+            totalLength += (mCols - 1) * 2;
+            for (int i = 0; i < totalLength; ++i) {
+                out.print("-");
+            }
+            out.println();
+            for (int i = 1, mTableSize = mTable.size(); i < mTableSize; i++) {
+                Object[] row = mTable.get(i);
+                printRow(out, formats, row);
+            }
+        }
+
+        private void printRow(PrintWriter out, String[] formats, Object[] row) {
+            for (int j = 0, rowLength = row.length; j < rowLength; j++) {
+                out.printf(String.format(formats[j], row[j].toString()));
+                out.print("  ");
+            }
+            out.println();
+        }
+
+        public int getNumRows() {
+            return mTable.size();
+        }
+    }
+}
diff --git a/services/java/com/android/server/content/SyncOperation.java b/services/java/com/android/server/content/SyncOperation.java
new file mode 100644
index 0000000..eaad982
--- /dev/null
+++ b/services/java/com/android/server/content/SyncOperation.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2010 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 com.android.server.content;
+
+import android.accounts.Account;
+import android.content.pm.PackageManager;
+import android.content.ContentResolver;
+import android.os.Bundle;
+import android.os.SystemClock;
+
+/**
+ * Value type that represents a sync operation.
+ * @hide
+ */
+public class SyncOperation implements Comparable {
+    public static final int REASON_BACKGROUND_DATA_SETTINGS_CHANGED = -1;
+    public static final int REASON_ACCOUNTS_UPDATED = -2;
+    public static final int REASON_SERVICE_CHANGED = -3;
+    public static final int REASON_PERIODIC = -4;
+    public static final int REASON_IS_SYNCABLE = -5;
+    public static final int REASON_SYNC_AUTO = -6;
+    public static final int REASON_MASTER_SYNC_AUTO = -7;
+    public static final int REASON_USER_START = -8;
+
+    private static String[] REASON_NAMES = new String[] {
+            "DataSettingsChanged",
+            "AccountsUpdated",
+            "ServiceChanged",
+            "Periodic",
+            "IsSyncable",
+            "AutoSync",
+            "MasterSyncAuto",
+            "UserStart",
+    };
+
+    public final Account account;
+    public final int userId;
+    public final int reason;
+    public int syncSource;
+    public String authority;
+    public final boolean allowParallelSyncs;
+    public Bundle extras;
+    public final String key;
+    public long earliestRunTime;
+    public boolean expedited;
+    public SyncStorageEngine.PendingOperation pendingOperation;
+    public Long backoff;
+    public long delayUntil;
+    public long effectiveRunTime;
+
+    public SyncOperation(Account account, int userId, int reason, int source, String authority,
+            Bundle extras, long delayInMs, long backoff, long delayUntil,
+            boolean allowParallelSyncs) {
+        this.account = account;
+        this.userId = userId;
+        this.reason = reason;
+        this.syncSource = source;
+        this.authority = authority;
+        this.allowParallelSyncs = allowParallelSyncs;
+        this.extras = new Bundle(extras);
+        removeFalseExtra(ContentResolver.SYNC_EXTRAS_UPLOAD);
+        removeFalseExtra(ContentResolver.SYNC_EXTRAS_MANUAL);
+        removeFalseExtra(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS);
+        removeFalseExtra(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF);
+        removeFalseExtra(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY);
+        removeFalseExtra(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS);
+        removeFalseExtra(ContentResolver.SYNC_EXTRAS_EXPEDITED);
+        removeFalseExtra(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS);
+        this.delayUntil = delayUntil;
+        this.backoff = backoff;
+        final long now = SystemClock.elapsedRealtime();
+        if (delayInMs < 0) {
+            this.expedited = true;
+            this.earliestRunTime = now;
+        } else {
+            this.expedited = false;
+            this.earliestRunTime = now + delayInMs;
+        }
+        updateEffectiveRunTime();
+        this.key = toKey();
+    }
+
+    private void removeFalseExtra(String extraName) {
+        if (!extras.getBoolean(extraName, false)) {
+            extras.remove(extraName);
+        }
+    }
+
+    SyncOperation(SyncOperation other) {
+        this.account = other.account;
+        this.userId = other.userId;
+        this.reason = other.reason;
+        this.syncSource = other.syncSource;
+        this.authority = other.authority;
+        this.extras = new Bundle(other.extras);
+        this.expedited = other.expedited;
+        this.earliestRunTime = SystemClock.elapsedRealtime();
+        this.backoff = other.backoff;
+        this.delayUntil = other.delayUntil;
+        this.allowParallelSyncs = other.allowParallelSyncs;
+        this.updateEffectiveRunTime();
+        this.key = toKey();
+    }
+
+    public String toString() {
+        return dump(null, true);
+    }
+
+    public String dump(PackageManager pm, boolean useOneLine) {
+        StringBuilder sb = new StringBuilder()
+                .append(account.name)
+                .append(" u")
+                .append(userId).append(" (")
+                .append(account.type)
+                .append(")")
+                .append(", ")
+                .append(authority)
+                .append(", ")
+                .append(SyncStorageEngine.SOURCES[syncSource])
+                .append(", earliestRunTime ")
+                .append(earliestRunTime);
+        if (expedited) {
+            sb.append(", EXPEDITED");
+        }
+        sb.append(", reason: ");
+        sb.append(reasonToString(pm, reason));
+        if (!useOneLine && !extras.keySet().isEmpty()) {
+            sb.append("\n    ");
+            extrasToStringBuilder(extras, sb);
+        }
+        return sb.toString();
+    }
+
+    public static String reasonToString(PackageManager pm, int reason) {
+        if (reason >= 0) {
+            if (pm != null) {
+                final String[] packages = pm.getPackagesForUid(reason);
+                if (packages != null && packages.length == 1) {
+                    return packages[0];
+                }
+                final String name = pm.getNameForUid(reason);
+                if (name != null) {
+                    return name;
+                }
+                return String.valueOf(reason);
+            } else {
+                return String.valueOf(reason);
+            }
+        } else {
+            final int index = -reason - 1;
+            if (index >= REASON_NAMES.length) {
+                return String.valueOf(reason);
+            } else {
+                return REASON_NAMES[index];
+            }
+        }
+    }
+
+    public boolean isInitialization() {
+        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
+    }
+
+    public boolean isExpedited() {
+        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
+    }
+
+    public boolean ignoreBackoff() {
+        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
+    }
+
+    private String toKey() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("authority: ").append(authority);
+        sb.append(" account {name=" + account.name + ", user=" + userId + ", type=" + account.type
+                + "}");
+        sb.append(" extras: ");
+        extrasToStringBuilder(extras, sb);
+        return sb.toString();
+    }
+
+    public static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
+        sb.append("[");
+        for (String key : bundle.keySet()) {
+            sb.append(key).append("=").append(bundle.get(key)).append(" ");
+        }
+        sb.append("]");
+    }
+
+    public void updateEffectiveRunTime() {
+        effectiveRunTime = ignoreBackoff()
+                ? earliestRunTime
+                : Math.max(
+                    Math.max(earliestRunTime, delayUntil),
+                    backoff);
+    }
+
+    public int compareTo(Object o) {
+        SyncOperation other = (SyncOperation)o;
+
+        if (expedited != other.expedited) {
+            return expedited ? -1 : 1;
+        }
+
+        if (effectiveRunTime == other.effectiveRunTime) {
+            return 0;
+        }
+
+        return effectiveRunTime < other.effectiveRunTime ? -1 : 1;
+    }
+}
diff --git a/services/java/com/android/server/content/SyncQueue.java b/services/java/com/android/server/content/SyncQueue.java
new file mode 100644
index 0000000..951e92c
--- /dev/null
+++ b/services/java/com/android/server/content/SyncQueue.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2010 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 com.android.server.content;
+
+import android.accounts.Account;
+import android.content.pm.PackageManager;
+import android.content.pm.RegisteredServicesCache;
+import android.content.SyncAdapterType;
+import android.content.SyncAdaptersCache;
+import android.content.pm.RegisteredServicesCache.ServiceInfo;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.google.android.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Queue of pending sync operations. Not inherently thread safe, external
+ * callers are responsible for locking.
+ *
+ * @hide
+ */
+public class SyncQueue {
+    private static final String TAG = "SyncManager";
+    private final SyncStorageEngine mSyncStorageEngine;
+    private final SyncAdaptersCache mSyncAdapters;
+    private final PackageManager mPackageManager;
+
+    // A Map of SyncOperations operationKey -> SyncOperation that is designed for
+    // quick lookup of an enqueued SyncOperation.
+    private final HashMap<String, SyncOperation> mOperationsMap = Maps.newHashMap();
+
+    public SyncQueue(PackageManager packageManager, SyncStorageEngine syncStorageEngine,
+            final SyncAdaptersCache syncAdapters) {
+        mPackageManager = packageManager;
+        mSyncStorageEngine = syncStorageEngine;
+        mSyncAdapters = syncAdapters;
+    }
+
+    public void addPendingOperations(int userId) {
+        for (SyncStorageEngine.PendingOperation op : mSyncStorageEngine.getPendingOperations()) {
+            if (op.userId != userId) continue;
+
+            final Pair<Long, Long> backoff = mSyncStorageEngine.getBackoff(
+                    op.account, op.userId, op.authority);
+            final ServiceInfo<SyncAdapterType> syncAdapterInfo = mSyncAdapters.getServiceInfo(
+                    SyncAdapterType.newKey(op.authority, op.account.type), op.userId);
+            if (syncAdapterInfo == null) {
+                Log.w(TAG, "Missing sync adapter info for authority " + op.authority + ", userId "
+                        + op.userId);
+                continue;
+            }
+            SyncOperation syncOperation = new SyncOperation(
+                    op.account, op.userId, op.reason, op.syncSource, op.authority, op.extras,
+                    0 /* delay */, backoff != null ? backoff.first : 0,
+                    mSyncStorageEngine.getDelayUntilTime(op.account, op.userId, op.authority),
+                    syncAdapterInfo.type.allowParallelSyncs());
+            syncOperation.expedited = op.expedited;
+            syncOperation.pendingOperation = op;
+            add(syncOperation, op);
+        }
+    }
+
+    public boolean add(SyncOperation operation) {
+        return add(operation, null /* this is not coming from the database */);
+    }
+
+    private boolean add(SyncOperation operation,
+            SyncStorageEngine.PendingOperation pop) {
+        // - if an operation with the same key exists and this one should run earlier,
+        //   update the earliestRunTime of the existing to the new time
+        // - if an operation with the same key exists and if this one should run
+        //   later, ignore it
+        // - if no operation exists then add the new one
+        final String operationKey = operation.key;
+        final SyncOperation existingOperation = mOperationsMap.get(operationKey);
+
+        if (existingOperation != null) {
+            boolean changed = false;
+            if (existingOperation.expedited == operation.expedited) {
+                final long newRunTime =
+                        Math.min(existingOperation.earliestRunTime, operation.earliestRunTime);
+                if (existingOperation.earliestRunTime != newRunTime) {
+                    existingOperation.earliestRunTime = newRunTime;
+                    changed = true;
+                }
+            } else {
+                if (operation.expedited) {
+                    existingOperation.expedited = true;
+                    changed = true;
+                }
+            }
+            return changed;
+        }
+
+        operation.pendingOperation = pop;
+        if (operation.pendingOperation == null) {
+            pop = new SyncStorageEngine.PendingOperation(
+                    operation.account, operation.userId, operation.reason, operation.syncSource,
+                    operation.authority, operation.extras, operation.expedited);
+            pop = mSyncStorageEngine.insertIntoPending(pop);
+            if (pop == null) {
+                throw new IllegalStateException("error adding pending sync operation "
+                        + operation);
+            }
+            operation.pendingOperation = pop;
+        }
+
+        mOperationsMap.put(operationKey, operation);
+        return true;
+    }
+
+    public void removeUser(int userId) {
+        ArrayList<SyncOperation> opsToRemove = new ArrayList<SyncOperation>();
+        for (SyncOperation op : mOperationsMap.values()) {
+            if (op.userId == userId) {
+                opsToRemove.add(op);
+            }
+        }
+
+        for (SyncOperation op : opsToRemove) {
+            remove(op);
+        }
+    }
+
+    /**
+     * Remove the specified operation if it is in the queue.
+     * @param operation the operation to remove
+     */
+    public void remove(SyncOperation operation) {
+        SyncOperation operationToRemove = mOperationsMap.remove(operation.key);
+        if (operationToRemove == null) {
+            return;
+        }
+        if (!mSyncStorageEngine.deleteFromPending(operationToRemove.pendingOperation)) {
+            final String errorMessage = "unable to find pending row for " + operationToRemove;
+            Log.e(TAG, errorMessage, new IllegalStateException(errorMessage));
+        }
+    }
+
+    public void onBackoffChanged(Account account, int userId, String providerName, long backoff) {
+        // for each op that matches the account and provider update its
+        // backoff and effectiveStartTime
+        for (SyncOperation op : mOperationsMap.values()) {
+            if (op.account.equals(account) && op.authority.equals(providerName)
+                    && op.userId == userId) {
+                op.backoff = backoff;
+                op.updateEffectiveRunTime();
+            }
+        }
+    }
+
+    public void onDelayUntilTimeChanged(Account account, String providerName, long delayUntil) {
+        // for each op that matches the account and provider update its
+        // delayUntilTime and effectiveStartTime
+        for (SyncOperation op : mOperationsMap.values()) {
+            if (op.account.equals(account) && op.authority.equals(providerName)) {
+                op.delayUntil = delayUntil;
+                op.updateEffectiveRunTime();
+            }
+        }
+    }
+
+    public void remove(Account account, int userId, String authority) {
+        Iterator<Map.Entry<String, SyncOperation>> entries = mOperationsMap.entrySet().iterator();
+        while (entries.hasNext()) {
+            Map.Entry<String, SyncOperation> entry = entries.next();
+            SyncOperation syncOperation = entry.getValue();
+            if (account != null && !syncOperation.account.equals(account)) {
+                continue;
+            }
+            if (authority != null && !syncOperation.authority.equals(authority)) {
+                continue;
+            }
+            if (userId != syncOperation.userId) {
+                continue;
+            }
+            entries.remove();
+            if (!mSyncStorageEngine.deleteFromPending(syncOperation.pendingOperation)) {
+                final String errorMessage = "unable to find pending row for " + syncOperation;
+                Log.e(TAG, errorMessage, new IllegalStateException(errorMessage));
+            }
+        }
+    }
+
+    public Collection<SyncOperation> getOperations() {
+        return mOperationsMap.values();
+    }
+
+    public void dump(StringBuilder sb) {
+        final long now = SystemClock.elapsedRealtime();
+        sb.append("SyncQueue: ").append(mOperationsMap.size()).append(" operation(s)\n");
+        for (SyncOperation operation : mOperationsMap.values()) {
+            sb.append("  ");
+            if (operation.effectiveRunTime <= now) {
+                sb.append("READY");
+            } else {
+                sb.append(DateUtils.formatElapsedTime((operation.effectiveRunTime - now) / 1000));
+            }
+            sb.append(" - ");
+            sb.append(operation.dump(mPackageManager, false)).append("\n");
+        }
+    }
+}
diff --git a/services/java/com/android/server/content/SyncStorageEngine.java b/services/java/com/android/server/content/SyncStorageEngine.java
new file mode 100644
index 0000000..5b8d26f
--- /dev/null
+++ b/services/java/com/android/server/content/SyncStorageEngine.java
@@ -0,0 +1,2303 @@
+/*
+ * 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 com.android.server.content;
+
+import android.accounts.Account;
+import android.accounts.AccountAndUser;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ISyncStatusObserver;
+import android.content.PeriodicSync;
+import android.content.SyncInfo;
+import android.content.SyncStatusInfo;
+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.util.AtomicFile;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import com.android.internal.annotations.VisibleForTesting;
+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 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.List;
+import java.util.Random;
+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;
+
+    private static final String XML_ATTR_NEXT_AUTHORITY_ID = "nextAuthorityId";
+    private static final String XML_ATTR_LISTEN_FOR_TICKLES = "listen-for-tickles";
+    private static final String XML_ATTR_SYNC_RANDOM_OFFSET = "offsetInSeconds";
+    private static final String XML_ATTR_ENABLED = "enabled";
+    private static final String XML_ATTR_USER = "user";
+    private static final String XML_TAG_LISTEN_FOR_TICKLES = "listenForTickles";
+
+    private static final long DEFAULT_POLL_FREQUENCY_SECONDS = 60 * 60 * 24; // One day
+
+    @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;
+
+    /** Enum value for a periodic sync. */
+    public static final int SOURCE_PERIODIC = 4;
+
+    public static final long NOT_IN_BACKOFF_MODE = -1;
+
+    // TODO: i18n -- grab these out of resources.
+    /** String names for the sync source types. */
+    public static final String[] SOURCES = { "SERVER",
+                                             "LOCAL",
+                                             "POLL",
+                                             "USER",
+                                             "PERIODIC" };
+
+    // 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;
+
+    // the version of the accounts xml file format
+    private static final int ACCOUNTS_VERSION = 2;
+
+    private static HashMap<String, String> sAuthorityRenames;
+
+    static {
+        sAuthorityRenames = new HashMap<String, String>();
+        sAuthorityRenames.put("contacts", "com.android.contacts");
+        sAuthorityRenames.put("calendar", "com.android.calendar");
+    }
+
+    public static class PendingOperation {
+        final Account account;
+        final int userId;
+        final int reason;
+        final int syncSource;
+        final String authority;
+        final Bundle extras;        // note: read-only.
+        final boolean expedited;
+
+        int authorityId;
+        byte[] flatExtras;
+
+        PendingOperation(Account account, int userId, int reason,int source,
+                String authority, Bundle extras, boolean expedited) {
+            this.account = account;
+            this.userId = userId;
+            this.syncSource = source;
+            this.reason = reason;
+            this.authority = authority;
+            this.extras = extras != null ? new Bundle(extras) : extras;
+            this.expedited = expedited;
+            this.authorityId = -1;
+        }
+
+        PendingOperation(PendingOperation other) {
+            this.account = other.account;
+            this.userId = other.userId;
+            this.reason = other.reason;
+            this.syncSource = other.syncSource;
+            this.authority = other.authority;
+            this.extras = other.extras;
+            this.authorityId = other.authorityId;
+            this.expedited = other.expedited;
+        }
+    }
+
+    static class AccountInfo {
+        final AccountAndUser accountAndUser;
+        final HashMap<String, AuthorityInfo> authorities =
+                new HashMap<String, AuthorityInfo>();
+
+        AccountInfo(AccountAndUser accountAndUser) {
+            this.accountAndUser = accountAndUser;
+        }
+    }
+
+    public static class AuthorityInfo {
+        final Account account;
+        final int userId;
+        final String authority;
+        final int ident;
+        boolean enabled;
+        int syncable;
+        long backoffTime;
+        long backoffDelay;
+        long delayUntil;
+        final ArrayList<Pair<Bundle, Long>> periodicSyncs;
+
+        /**
+         * Copy constructor for making deep-ish copies. Only the bundles stored
+         * in periodic syncs can make unexpected changes.
+         *
+         * @param toCopy AuthorityInfo to be copied.
+         */
+        AuthorityInfo(AuthorityInfo toCopy) {
+            account = toCopy.account;
+            userId = toCopy.userId;
+            authority = toCopy.authority;
+            ident = toCopy.ident;
+            enabled = toCopy.enabled;
+            syncable = toCopy.syncable;
+            backoffTime = toCopy.backoffTime;
+            backoffDelay = toCopy.backoffDelay;
+            delayUntil = toCopy.delayUntil;
+            periodicSyncs = new ArrayList<Pair<Bundle, Long>>();
+            for (Pair<Bundle, Long> sync : toCopy.periodicSyncs) {
+                // Still not a perfect copy, because we are just copying the mappings.
+                periodicSyncs.add(Pair.create(new Bundle(sync.first), sync.second));
+            }
+        }
+
+        AuthorityInfo(Account account, int userId, String authority, int ident) {
+            this.account = account;
+            this.userId = userId;
+            this.authority = authority;
+            this.ident = ident;
+            enabled = SYNC_ENABLED_DEFAULT;
+            syncable = -1; // default to "unknown"
+            backoffTime = -1; // if < 0 then we aren't in backoff mode
+            backoffDelay = -1; // if < 0 then we aren't in backoff mode
+            periodicSyncs = new ArrayList<Pair<Bundle, Long>>();
+            periodicSyncs.add(Pair.create(new Bundle(), DEFAULT_POLL_FREQUENCY_SECONDS));
+        }
+    }
+
+    public static class SyncHistoryItem {
+        int authorityId;
+        int historyId;
+        long eventTime;
+        long elapsedTime;
+        int source;
+        int event;
+        long upstreamActivity;
+        long downstreamActivity;
+        String mesg;
+        boolean initialization;
+        Bundle extras;
+        int reason;
+    }
+
+    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;
+        }
+    }
+
+    interface OnSyncRequestListener {
+        /**
+         * Called when a sync is needed on an account(s) due to some change in state.
+         * @param account
+         * @param userId
+         * @param reason
+         * @param authority
+         * @param extras
+         */
+        public void onSyncRequest(Account account, int userId, int reason, String authority,
+                Bundle extras);
+    }
+
+    // Primary list of all syncable authorities.  Also our global lock.
+    private final SparseArray<AuthorityInfo> mAuthorities =
+            new SparseArray<AuthorityInfo>();
+
+    private final HashMap<AccountAndUser, AccountInfo> mAccounts
+            = new HashMap<AccountAndUser, AccountInfo>();
+
+    private final ArrayList<PendingOperation> mPendingOperations =
+            new ArrayList<PendingOperation>();
+
+    private final SparseArray<ArrayList<SyncInfo>> mCurrentSyncs
+            = new SparseArray<ArrayList<SyncInfo>>();
+
+    private final SparseArray<SyncStatusInfo> mSyncStatus =
+            new SparseArray<SyncStatusInfo>();
+
+    private final ArrayList<SyncHistoryItem> mSyncHistory =
+            new ArrayList<SyncHistoryItem>();
+
+    private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners
+            = new RemoteCallbackList<ISyncStatusObserver>();
+
+    private int mNextAuthorityId = 0;
+
+    // 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;
+
+    private int mSyncRandomOffset;
+
+    /**
+     * 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 SparseArray<Boolean> mMasterSyncAutomatically = new SparseArray<Boolean>();
+    private boolean mDefaultMasterSyncAutomatically;
+
+    private OnSyncRequestListener mSyncRequestListener;
+
+    private SyncStorageEngine(Context context, File dataDir) {
+        mContext = context;
+        sSyncStorageEngine = this;
+
+        mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0"));
+
+        mDefaultMasterSyncAutomatically = mContext.getResources().getBoolean(
+               com.android.internal.R.bool.config_syncstorageengine_masterSyncAutomatically);
+
+        File systemDir = new File(dataDir, "system");
+        File syncDir = new File(systemDir, "sync");
+        syncDir.mkdirs();
+        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();
+        readAndDeleteLegacyAccountInfoLocked();
+        writeAccountInfoLocked();
+        writeStatusLocked();
+        writePendingOperationsLocked();
+        writeStatisticsLocked();
+    }
+
+    public static SyncStorageEngine newTestInstance(Context context) {
+        return new SyncStorageEngine(context, context.getFilesDir());
+    }
+
+    public static void init(Context context) {
+        if (sSyncStorageEngine != null) {
+            return;
+        }
+        // This call will return the correct directory whether Encrypted File Systems is
+        // enabled or not.
+        File dataDir = Environment.getSecureDataDirectory();
+        sSyncStorageEngine = new SyncStorageEngine(context, dataDir);
+    }
+
+    public static SyncStorageEngine getSingleton() {
+        if (sSyncStorageEngine == null) {
+            throw new IllegalStateException("not initialized");
+        }
+        return sSyncStorageEngine;
+    }
+
+    protected void setOnSyncRequestListener(OnSyncRequestListener listener) {
+        if (mSyncRequestListener == null) {
+            mSyncRequestListener = listener;
+        }
+    }
+
+    @Override public void handleMessage(Message msg) {
+        if (msg.what == MSG_WRITE_STATUS) {
+            synchronized (mAuthorities) {
+                writeStatusLocked();
+            }
+        } else if (msg.what == MSG_WRITE_STATISTICS) {
+            synchronized (mAuthorities) {
+                writeStatisticsLocked();
+            }
+        }
+    }
+
+    public int getSyncRandomOffset() {
+        return mSyncRandomOffset;
+    }
+
+    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, int userId, String providerName) {
+        synchronized (mAuthorities) {
+            if (account != null) {
+                AuthorityInfo authority = getAuthorityLocked(account, userId, 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.userId == userId
+                        && authority.enabled) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    public void setSyncAutomatically(Account account, int userId, String providerName,
+            boolean sync) {
+        if (DEBUG) {
+            Log.d(TAG, "setSyncAutomatically: " + /* account + */" provider " + providerName
+                    + ", user " + userId + " -> " + sync);
+        }
+        synchronized (mAuthorities) {
+            AuthorityInfo authority = getOrCreateAuthorityLocked(account, userId, providerName, -1,
+                    false);
+            if (authority.enabled == sync) {
+                if (DEBUG) {
+                    Log.d(TAG, "setSyncAutomatically: already set to " + sync + ", doing nothing");
+                }
+                return;
+            }
+            authority.enabled = sync;
+            writeAccountInfoLocked();
+        }
+
+        if (sync) {
+            requestSync(account, userId, SyncOperation.REASON_SYNC_AUTO, providerName,
+                    new Bundle());
+        }
+        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+    }
+
+    public int getIsSyncable(Account account, int userId, String providerName) {
+        synchronized (mAuthorities) {
+            if (account != null) {
+                AuthorityInfo authority = getAuthorityLocked(account, userId, 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, int userId, String providerName, int syncable) {
+        if (syncable > 1) {
+            syncable = 1;
+        } else if (syncable < -1) {
+            syncable = -1;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "setIsSyncable: " + account + ", provider " + providerName
+                    + ", user " + userId + " -> " + syncable);
+        }
+        synchronized (mAuthorities) {
+            AuthorityInfo authority = getOrCreateAuthorityLocked(account, userId, providerName, -1,
+                    false);
+            if (authority.syncable == syncable) {
+                if (DEBUG) {
+                    Log.d(TAG, "setIsSyncable: already set to " + syncable + ", doing nothing");
+                }
+                return;
+            }
+            authority.syncable = syncable;
+            writeAccountInfoLocked();
+        }
+
+        if (syncable > 0) {
+            requestSync(account, userId, SyncOperation.REASON_IS_SYNCABLE,  providerName,
+                    new Bundle());
+        }
+        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+    }
+
+    public Pair<Long, Long> getBackoff(Account account, int userId, String providerName) {
+        synchronized (mAuthorities) {
+            AuthorityInfo authority = getAuthorityLocked(account, userId, providerName,
+                    "getBackoff");
+            if (authority == null || authority.backoffTime < 0) {
+                return null;
+            }
+            return Pair.create(authority.backoffTime, authority.backoffDelay);
+        }
+    }
+
+    public void setBackoff(Account account, int userId, String providerName,
+            long nextSyncTime, long nextDelay) {
+        if (DEBUG) {
+            Log.v(TAG, "setBackoff: " + account + ", provider " + providerName
+                    + ", user " + userId
+                    + " -> nextSyncTime " + nextSyncTime + ", nextDelay " + nextDelay);
+        }
+        boolean changed = false;
+        synchronized (mAuthorities) {
+            if (account == null || providerName == null) {
+                for (AccountInfo accountInfo : mAccounts.values()) {
+                    if (account != null && !account.equals(accountInfo.accountAndUser.account)
+                            && userId != accountInfo.accountAndUser.userId) {
+                        continue;
+                    }
+                    for (AuthorityInfo authorityInfo : accountInfo.authorities.values()) {
+                        if (providerName != null && !providerName.equals(authorityInfo.authority)) {
+                            continue;
+                        }
+                        if (authorityInfo.backoffTime != nextSyncTime
+                                || authorityInfo.backoffDelay != nextDelay) {
+                            authorityInfo.backoffTime = nextSyncTime;
+                            authorityInfo.backoffDelay = nextDelay;
+                            changed = true;
+                        }
+                    }
+                }
+            } else {
+                AuthorityInfo authority =
+                        getOrCreateAuthorityLocked(account, userId, providerName, -1 /* ident */,
+                                true);
+                if (authority.backoffTime == nextSyncTime && authority.backoffDelay == nextDelay) {
+                    return;
+                }
+                authority.backoffTime = nextSyncTime;
+                authority.backoffDelay = nextDelay;
+                changed = true;
+            }
+        }
+
+        if (changed) {
+            reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+        }
+    }
+
+    public void clearAllBackoffs(SyncQueue syncQueue) {
+        boolean changed = false;
+        synchronized (mAuthorities) {
+            synchronized (syncQueue) {
+                for (AccountInfo accountInfo : mAccounts.values()) {
+                    for (AuthorityInfo authorityInfo : accountInfo.authorities.values()) {
+                        if (authorityInfo.backoffTime != NOT_IN_BACKOFF_MODE
+                                || authorityInfo.backoffDelay != NOT_IN_BACKOFF_MODE) {
+                            if (DEBUG) {
+                                Log.v(TAG, "clearAllBackoffs:"
+                                        + " authority:" + authorityInfo.authority
+                                        + " account:" + accountInfo.accountAndUser.account.name
+                                        + " user:" + accountInfo.accountAndUser.userId
+                                        + " backoffTime was: " + authorityInfo.backoffTime
+                                        + " backoffDelay was: " + authorityInfo.backoffDelay);
+                            }
+                            authorityInfo.backoffTime = NOT_IN_BACKOFF_MODE;
+                            authorityInfo.backoffDelay = NOT_IN_BACKOFF_MODE;
+                            syncQueue.onBackoffChanged(accountInfo.accountAndUser.account,
+                                    accountInfo.accountAndUser.userId, authorityInfo.authority, 0);
+                            changed = true;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (changed) {
+            reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+        }
+    }
+
+    public void setDelayUntilTime(Account account, int userId, String providerName,
+            long delayUntil) {
+        if (DEBUG) {
+            Log.v(TAG, "setDelayUntil: " + account + ", provider " + providerName
+                    + ", user " + userId + " -> delayUntil " + delayUntil);
+        }
+        synchronized (mAuthorities) {
+            AuthorityInfo authority = getOrCreateAuthorityLocked(
+                    account, userId, providerName, -1 /* ident */, true);
+            if (authority.delayUntil == delayUntil) {
+                return;
+            }
+            authority.delayUntil = delayUntil;
+        }
+
+        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+    }
+
+    public long getDelayUntilTime(Account account, int userId, String providerName) {
+        synchronized (mAuthorities) {
+            AuthorityInfo authority = getAuthorityLocked(account, userId, providerName,
+                    "getDelayUntil");
+            if (authority == null) {
+                return 0;
+            }
+            return authority.delayUntil;
+        }
+    }
+
+    private void updateOrRemovePeriodicSync(Account account, int userId, String providerName,
+            Bundle extras,
+            long period, boolean add) {
+        if (period <= 0) {
+            period = 0;
+        }
+        if (extras == null) {
+            extras = new Bundle();
+        }
+        if (DEBUG) {
+            Log.v(TAG, "addOrRemovePeriodicSync: " + account + ", user " + userId
+                    + ", provider " + providerName
+                    + " -> period " + period + ", extras " + extras);
+        }
+        synchronized (mAuthorities) {
+            try {
+                AuthorityInfo authority =
+                        getOrCreateAuthorityLocked(account, userId, providerName, -1, false);
+                if (add) {
+                    // add this periodic sync if one with the same extras doesn't already
+                    // exist in the periodicSyncs array
+                    boolean alreadyPresent = false;
+                    for (int i = 0, N = authority.periodicSyncs.size(); i < N; i++) {
+                        Pair<Bundle, Long> syncInfo = authority.periodicSyncs.get(i);
+                        final Bundle existingExtras = syncInfo.first;
+                        if (PeriodicSync.syncExtrasEquals(existingExtras, extras)) {
+                            if (syncInfo.second == period) {
+                                return;
+                            }
+                            authority.periodicSyncs.set(i, Pair.create(extras, period));
+                            alreadyPresent = true;
+                            break;
+                        }
+                    }
+                    // if we added an entry to the periodicSyncs array also add an entry to
+                    // the periodic syncs status to correspond to it
+                    if (!alreadyPresent) {
+                        authority.periodicSyncs.add(Pair.create(extras, period));
+                        SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident);
+                        status.setPeriodicSyncTime(authority.periodicSyncs.size() - 1, 0);
+                    }
+                } else {
+                    // remove any periodic syncs that match the authority and extras
+                    SyncStatusInfo status = mSyncStatus.get(authority.ident);
+                    boolean changed = false;
+                    Iterator<Pair<Bundle, Long>> iterator = authority.periodicSyncs.iterator();
+                    int i = 0;
+                    while (iterator.hasNext()) {
+                        Pair<Bundle, Long> syncInfo = iterator.next();
+                        if (PeriodicSync.syncExtrasEquals(syncInfo.first, extras)) {
+                            iterator.remove();
+                            changed = true;
+                            // if we removed an entry from the periodicSyncs array also
+                            // remove the corresponding entry from the status
+                            if (status != null) {
+                                status.removePeriodicSyncTime(i);
+                            }
+                        } else {
+                            i++;
+                        }
+                    }
+                    if (!changed) {
+                        return;
+                    }
+                }
+            } finally {
+                writeAccountInfoLocked();
+                writeStatusLocked();
+            }
+        }
+
+        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+    }
+
+    public void addPeriodicSync(Account account, int userId, String providerName, Bundle extras,
+            long pollFrequency) {
+        updateOrRemovePeriodicSync(account, userId, providerName, extras, pollFrequency,
+                true /* add */);
+    }
+
+    public void removePeriodicSync(Account account, int userId, String providerName,
+            Bundle extras) {
+        updateOrRemovePeriodicSync(account, userId, providerName, extras, 0 /* period, ignored */,
+                false /* remove */);
+    }
+
+    public List<PeriodicSync> getPeriodicSyncs(Account account, int userId, String providerName) {
+        ArrayList<PeriodicSync> syncs = new ArrayList<PeriodicSync>();
+        synchronized (mAuthorities) {
+            AuthorityInfo authority = getAuthorityLocked(account, userId, providerName,
+                    "getPeriodicSyncs");
+            if (authority != null) {
+                for (Pair<Bundle, Long> item : authority.periodicSyncs) {
+                    syncs.add(new PeriodicSync(account, providerName, item.first,
+                            item.second));
+                }
+            }
+        }
+        return syncs;
+    }
+
+    public void setMasterSyncAutomatically(boolean flag, int userId) {
+        synchronized (mAuthorities) {
+            Boolean auto = mMasterSyncAutomatically.get(userId);
+            if (auto != null && (boolean) auto == flag) {
+                return;
+            }
+            mMasterSyncAutomatically.put(userId, flag);
+            writeAccountInfoLocked();
+        }
+        if (flag) {
+            requestSync(null, userId, SyncOperation.REASON_MASTER_SYNC_AUTO, null,
+                    new Bundle());
+        }
+        reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
+        mContext.sendBroadcast(ContentResolver.ACTION_SYNC_CONN_STATUS_CHANGED);
+    }
+
+    public boolean getMasterSyncAutomatically(int userId) {
+        synchronized (mAuthorities) {
+            Boolean auto = mMasterSyncAutomatically.get(userId);
+            return auto == null ? mDefaultMasterSyncAutomatically : auto;
+        }
+    }
+
+    public AuthorityInfo getOrCreateAuthority(Account account, int userId, String authority) {
+        synchronized (mAuthorities) {
+            return getOrCreateAuthorityLocked(account, userId, authority,
+                    -1 /* assign a new identifier if creating a new authority */,
+                    true /* write to storage if this results in a change */);
+        }
+    }
+
+    public void removeAuthority(Account account, int userId, String authority) {
+        synchronized (mAuthorities) {
+            removeAuthorityLocked(account, userId, authority, true /* doWrite */);
+        }
+    }
+
+    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 actively being processed.
+     */
+    public boolean isSyncActive(Account account, int userId, String authority) {
+        synchronized (mAuthorities) {
+            for (SyncInfo syncInfo : getCurrentSyncs(userId)) {
+                AuthorityInfo ainfo = getAuthority(syncInfo.authorityId);
+                if (ainfo != null && ainfo.account.equals(account)
+                        && ainfo.authority.equals(authority)
+                        && ainfo.userId == userId) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    public PendingOperation insertIntoPending(PendingOperation op) {
+        synchronized (mAuthorities) {
+            if (DEBUG) {
+                Log.v(TAG, "insertIntoPending: account=" + op.account
+                        + " user=" + op.userId
+                        + " auth=" + op.authority
+                        + " src=" + op.syncSource
+                        + " extras=" + op.extras);
+            }
+
+            AuthorityInfo authority = getOrCreateAuthorityLocked(op.account, op.userId,
+                    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(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
+                    + " user=" + op.userId
+                    + " 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.userId, 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)
+                                && cur.userId == op.userId) {
+                            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;
+    }
+
+    /**
+     * 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, int userId) {
+        synchronized (mAuthorities) {
+            if (DEBUG) Log.v(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.accountAndUser.account)
+                        && acc.accountAndUser.userId == userId) {
+                    // This account no longer exists...
+                    if (DEBUG) {
+                        Log.v(TAG, "Account removed: " + acc.accountAndUser);
+                    }
+                    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 a sync is starting. Supply a valid ActiveSyncContext with information
+     * about the sync.
+     */
+    public SyncInfo addActiveSync(SyncManager.ActiveSyncContext activeSyncContext) {
+        final SyncInfo syncInfo;
+        synchronized (mAuthorities) {
+            if (DEBUG) {
+                Log.v(TAG, "setActiveSync: account="
+                    + activeSyncContext.mSyncOperation.account
+                    + " auth=" + activeSyncContext.mSyncOperation.authority
+                    + " src=" + activeSyncContext.mSyncOperation.syncSource
+                    + " extras=" + activeSyncContext.mSyncOperation.extras);
+            }
+            AuthorityInfo authority = getOrCreateAuthorityLocked(
+                    activeSyncContext.mSyncOperation.account,
+                    activeSyncContext.mSyncOperation.userId,
+                    activeSyncContext.mSyncOperation.authority,
+                    -1 /* assign a new identifier if creating a new authority */,
+                    true /* write to storage if this results in a change */);
+            syncInfo = new SyncInfo(authority.ident,
+                    authority.account, authority.authority,
+                    activeSyncContext.mStartTime);
+            getCurrentSyncs(authority.userId).add(syncInfo);
+        }
+
+        reportActiveChange();
+        return syncInfo;
+    }
+
+    /**
+     * Called to indicate that a previously active sync is no longer active.
+     */
+    public void removeActiveSync(SyncInfo syncInfo, int userId) {
+        synchronized (mAuthorities) {
+            if (DEBUG) {
+                Log.v(TAG, "removeActiveSync: account=" + syncInfo.account
+                        + " user=" + userId
+                        + " auth=" + syncInfo.authority);
+            }
+            getCurrentSyncs(userId).remove(syncInfo);
+        }
+
+        reportActiveChange();
+    }
+
+    /**
+     * 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, int userId, int reason,
+            String authorityName, long now, int source, boolean initialization, Bundle extras) {
+        long id;
+        synchronized (mAuthorities) {
+            if (DEBUG) {
+                Log.v(TAG, "insertStartSyncEvent: account=" + accountName + "user=" + userId
+                    + " auth=" + authorityName + " source=" + source);
+            }
+            AuthorityInfo authority = getAuthorityLocked(accountName, userId, authorityName,
+                    "insertStartSyncEvent");
+            if (authority == null) {
+                return -1;
+            }
+            SyncHistoryItem item = new SyncHistoryItem();
+            item.initialization = initialization;
+            item.authorityId = authority.ident;
+            item.historyId = mNextHistoryId++;
+            if (mNextHistoryId < 0) mNextHistoryId = 0;
+            item.eventTime = now;
+            item.source = source;
+            item.reason = reason;
+            item.extras = extras;
+            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;
+                case SOURCE_PERIODIC:
+                    status.numSourcePeriodic++;
+                    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 a list of the currently active syncs. Note that the returned items are the
+     * real, live active sync objects, so be careful what you do with it.
+     */
+    public List<SyncInfo> getCurrentSyncs(int userId) {
+        synchronized (mAuthorities) {
+            ArrayList<SyncInfo> syncs = mCurrentSyncs.get(userId);
+            if (syncs == null) {
+                syncs = new ArrayList<SyncInfo>();
+                mCurrentSyncs.put(userId, syncs);
+            }
+            return syncs;
+        }
+    }
+
+    /**
+     * 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;
+        }
+    }
+
+    /**
+     * Return an array of the current authorities. Note
+     * that the objects inside the array are the real, live objects,
+     * so be careful what you do with them.
+     */
+    public ArrayList<AuthorityInfo> getAuthorities() {
+        synchronized (mAuthorities) {
+            final int N = mAuthorities.size();
+            ArrayList<AuthorityInfo> infos = new ArrayList<AuthorityInfo>(N);
+            for (int i=0; i<N; i++) {
+                // Make deep copy because AuthorityInfo syncs are liable to change.
+                infos.add(new AuthorityInfo(mAuthorities.valueAt(i)));
+            }
+            return infos;
+        }
+    }
+
+    /**
+     * 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
+     */
+    public SyncStatusInfo getStatusByAccountAndAuthority(Account account, int userId,
+            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)
+                        && ainfo.userId == userId
+                        && 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, int userId, 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 (userId != ainfo.userId) {
+                    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;
+        }
+    }
+
+    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, int userId, String authorityName,
+            String tag) {
+        AccountAndUser au = new AccountAndUser(accountName, userId);
+        AccountInfo accountInfo = mAccounts.get(au);
+        if (accountInfo == null) {
+            if (tag != null) {
+                if (DEBUG) {
+                    Log.v(TAG, tag + ": unknown account " + au);
+                }
+            }
+            return null;
+        }
+        AuthorityInfo authority = accountInfo.authorities.get(authorityName);
+        if (authority == null) {
+            if (tag != null) {
+                if (DEBUG) {
+                    Log.v(TAG, tag + ": unknown authority " + authorityName);
+                }
+            }
+            return null;
+        }
+
+        return authority;
+    }
+
+    private AuthorityInfo getOrCreateAuthorityLocked(Account accountName, int userId,
+            String authorityName, int ident, boolean doWrite) {
+        AccountAndUser au = new AccountAndUser(accountName, userId);
+        AccountInfo account = mAccounts.get(au);
+        if (account == null) {
+            account = new AccountInfo(au);
+            mAccounts.put(au, account);
+        }
+        AuthorityInfo authority = account.authorities.get(authorityName);
+        if (authority == null) {
+            if (ident < 0) {
+                ident = mNextAuthorityId;
+                mNextAuthorityId++;
+                doWrite = true;
+            }
+            if (DEBUG) {
+                Log.v(TAG, "created a new AuthorityInfo for " + accountName
+                        + ", user " + userId
+                        + ", provider " + authorityName);
+            }
+            authority = new AuthorityInfo(accountName, userId, authorityName, ident);
+            account.authorities.put(authorityName, authority);
+            mAuthorities.put(ident, authority);
+            if (doWrite) {
+                writeAccountInfoLocked();
+            }
+        }
+
+        return authority;
+    }
+
+    private void removeAuthorityLocked(Account account, int userId, String authorityName,
+            boolean doWrite) {
+        AccountInfo accountInfo = mAccounts.get(new AccountAndUser(account, userId));
+        if (accountInfo != null) {
+            final AuthorityInfo authorityInfo = accountInfo.authorities.remove(authorityName);
+            if (authorityInfo != null) {
+                mAuthorities.remove(authorityInfo.ident);
+                if (doWrite) {
+                    writeAccountInfoLocked();
+                }
+            }
+        }
+    }
+
+    public SyncStatusInfo getOrCreateSyncStatus(AuthorityInfo authority) {
+        synchronized (mAuthorities) {
+            return getOrCreateSyncStatusLocked(authority.ident);
+        }
+    }
+
+    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();
+        }
+    }
+
+    /**
+     * public for testing
+     */
+    public void clearAndReadState() {
+        synchronized (mAuthorities) {
+            mAuthorities.clear();
+            mAccounts.clear();
+            mPendingOperations.clear();
+            mSyncStatus.clear();
+            mSyncHistory.clear();
+
+            readAccountInfoLocked();
+            readStatusLocked();
+            readPendingOperationsLocked();
+            readStatisticsLocked();
+            readAndDeleteLegacyAccountInfoLocked();
+            writeAccountInfoLocked();
+            writeStatusLocked();
+            writePendingOperationsLocked();
+            writeStatisticsLocked();
+        }
+    }
+
+    /**
+     * Read all account information back in to the initial engine state.
+     */
+    private void readAccountInfoLocked() {
+        int highestAuthorityId = -1;
+        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, XML_ATTR_LISTEN_FOR_TICKLES);
+                String versionString = parser.getAttributeValue(null, "version");
+                int version;
+                try {
+                    version = (versionString == null) ? 0 : Integer.parseInt(versionString);
+                } catch (NumberFormatException e) {
+                    version = 0;
+                }
+                String nextIdString = parser.getAttributeValue(null, XML_ATTR_NEXT_AUTHORITY_ID);
+                try {
+                    int id = (nextIdString == null) ? 0 : Integer.parseInt(nextIdString);
+                    mNextAuthorityId = Math.max(mNextAuthorityId, id);
+                } catch (NumberFormatException e) {
+                    // don't care
+                }
+                String offsetString = parser.getAttributeValue(null, XML_ATTR_SYNC_RANDOM_OFFSET);
+                try {
+                    mSyncRandomOffset = (offsetString == null) ? 0 : Integer.parseInt(offsetString);
+                } catch (NumberFormatException e) {
+                    mSyncRandomOffset = 0;
+                }
+                if (mSyncRandomOffset == 0) {
+                    Random random = new Random(System.currentTimeMillis());
+                    mSyncRandomOffset = random.nextInt(86400);
+                }
+                mMasterSyncAutomatically.put(0, listen == null || Boolean.parseBoolean(listen));
+                eventType = parser.next();
+                AuthorityInfo authority = null;
+                Pair<Bundle, Long> periodicSync = null;
+                do {
+                    if (eventType == XmlPullParser.START_TAG) {
+                        tagName = parser.getName();
+                        if (parser.getDepth() == 2) {
+                            if ("authority".equals(tagName)) {
+                                authority = parseAuthority(parser, version);
+                                periodicSync = null;
+                                if (authority.ident > highestAuthorityId) {
+                                    highestAuthorityId = authority.ident;
+                                }
+                            } else if (XML_TAG_LISTEN_FOR_TICKLES.equals(tagName)) {
+                                parseListenForTickles(parser);
+                            }
+                        } else if (parser.getDepth() == 3) {
+                            if ("periodicSync".equals(tagName) && authority != null) {
+                                periodicSync = parsePeriodicSync(parser, authority);
+                            }
+                        } else if (parser.getDepth() == 4 && periodicSync != null) {
+                            if ("extra".equals(tagName)) {
+                                parseExtra(parser, periodicSync);
+                            }
+                        }
+                    }
+                    eventType = parser.next();
+                } while (eventType != XmlPullParser.END_DOCUMENT);
+            }
+        } catch (XmlPullParserException e) {
+            Log.w(TAG, "Error reading accounts", e);
+            return;
+        } catch (java.io.IOException e) {
+            if (fis == null) Log.i(TAG, "No initial accounts");
+            else Log.w(TAG, "Error reading accounts", e);
+            return;
+        } finally {
+            mNextAuthorityId = Math.max(highestAuthorityId + 1, mNextAuthorityId);
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (java.io.IOException e1) {
+                }
+            }
+        }
+
+        maybeMigrateSettingsForRenamedAuthorities();
+    }
+
+    /**
+     * some authority names have changed. copy over their settings and delete the old ones
+     * @return true if a change was made
+     */
+    private boolean maybeMigrateSettingsForRenamedAuthorities() {
+        boolean writeNeeded = false;
+
+        ArrayList<AuthorityInfo> authoritiesToRemove = new ArrayList<AuthorityInfo>();
+        final int N = mAuthorities.size();
+        for (int i=0; i<N; i++) {
+            AuthorityInfo authority = mAuthorities.valueAt(i);
+            // skip this authority if it isn't one of the renamed ones
+            final String newAuthorityName = sAuthorityRenames.get(authority.authority);
+            if (newAuthorityName == null) {
+                continue;
+            }
+
+            // remember this authority so we can remove it later. we can't remove it
+            // now without messing up this loop iteration
+            authoritiesToRemove.add(authority);
+
+            // this authority isn't enabled, no need to copy it to the new authority name since
+            // the default is "disabled"
+            if (!authority.enabled) {
+                continue;
+            }
+
+            // if we already have a record of this new authority then don't copy over the settings
+            if (getAuthorityLocked(authority.account, authority.userId, newAuthorityName, "cleanup")
+                    != null) {
+                continue;
+            }
+
+            AuthorityInfo newAuthority = getOrCreateAuthorityLocked(authority.account,
+                    authority.userId, newAuthorityName, -1 /* ident */, false /* doWrite */);
+            newAuthority.enabled = true;
+            writeNeeded = true;
+        }
+
+        for (AuthorityInfo authorityInfo : authoritiesToRemove) {
+            removeAuthorityLocked(authorityInfo.account, authorityInfo.userId,
+                    authorityInfo.authority, false /* doWrite */);
+            writeNeeded = true;
+        }
+
+        return writeNeeded;
+    }
+
+    private void parseListenForTickles(XmlPullParser parser) {
+        String user = parser.getAttributeValue(null, XML_ATTR_USER);
+        int userId = 0;
+        try {
+            userId = Integer.parseInt(user);
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "error parsing the user for listen-for-tickles", e);
+        } catch (NullPointerException e) {
+            Log.e(TAG, "the user in listen-for-tickles is null", e);
+        }
+        String enabled = parser.getAttributeValue(null, XML_ATTR_ENABLED);
+        boolean listen = enabled == null || Boolean.parseBoolean(enabled);
+        mMasterSyncAutomatically.put(userId, listen);
+    }
+
+    private AuthorityInfo parseAuthority(XmlPullParser parser, int version) {
+        AuthorityInfo authority = null;
+        int id = -1;
+        try {
+            id = Integer.parseInt(parser.getAttributeValue(
+                    null, "id"));
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "error parsing the id of the authority", e);
+        } catch (NullPointerException e) {
+            Log.e(TAG, "the id of the authority is null", e);
+        }
+        if (id >= 0) {
+            String authorityName = parser.getAttributeValue(null, "authority");
+            String enabled = parser.getAttributeValue(null, XML_ATTR_ENABLED);
+            String syncable = parser.getAttributeValue(null, "syncable");
+            String accountName = parser.getAttributeValue(null, "account");
+            String accountType = parser.getAttributeValue(null, "type");
+            String user = parser.getAttributeValue(null, XML_ATTR_USER);
+            int userId = user == null ? 0 : Integer.parseInt(user);
+            if (accountType == null) {
+                accountType = "com.google";
+                syncable = "unknown";
+            }
+            authority = mAuthorities.get(id);
+            if (DEBUG_FILE) Log.v(TAG, "Adding authority: account="
+                    + accountName + " auth=" + authorityName
+                    + " user=" + userId
+                    + " enabled=" + enabled
+                    + " syncable=" + syncable);
+            if (authority == null) {
+                if (DEBUG_FILE) Log.v(TAG, "Creating entry");
+                authority = getOrCreateAuthorityLocked(
+                        new Account(accountName, accountType), userId, authorityName, id, false);
+                // If the version is 0 then we are upgrading from a file format that did not
+                // know about periodic syncs. In that case don't clear the list since we
+                // want the default, which is a daily periodioc sync.
+                // Otherwise clear out this default list since we will populate it later with
+                // the periodic sync descriptions that are read from the configuration file.
+                if (version > 0) {
+                    authority.periodicSyncs.clear();
+                }
+            }
+            if (authority != null) {
+                authority.enabled = enabled == null || Boolean.parseBoolean(enabled);
+                if ("unknown".equals(syncable)) {
+                    authority.syncable = -1;
+                } else {
+                    authority.syncable =
+                            (syncable == null || Boolean.parseBoolean(syncable)) ? 1 : 0;
+                }
+            } else {
+                Log.w(TAG, "Failure adding authority: account="
+                        + accountName + " auth=" + authorityName
+                        + " enabled=" + enabled
+                        + " syncable=" + syncable);
+            }
+        }
+
+        return authority;
+    }
+
+    private Pair<Bundle, Long> parsePeriodicSync(XmlPullParser parser, AuthorityInfo authority) {
+        Bundle extras = new Bundle();
+        String periodValue = parser.getAttributeValue(null, "period");
+        final long period;
+        try {
+            period = Long.parseLong(periodValue);
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "error parsing the period of a periodic sync", e);
+            return null;
+        } catch (NullPointerException e) {
+            Log.e(TAG, "the period of a periodic sync is null", e);
+            return null;
+        }
+        final Pair<Bundle, Long> periodicSync = Pair.create(extras, period);
+        authority.periodicSyncs.add(periodicSync);
+
+        return periodicSync;
+    }
+
+    private void parseExtra(XmlPullParser parser, Pair<Bundle, Long> periodicSync) {
+        final Bundle extras = periodicSync.first;
+        String name = parser.getAttributeValue(null, "name");
+        String type = parser.getAttributeValue(null, "type");
+        String value1 = parser.getAttributeValue(null, "value1");
+        String value2 = parser.getAttributeValue(null, "value2");
+
+        try {
+            if ("long".equals(type)) {
+                extras.putLong(name, Long.parseLong(value1));
+            } else if ("integer".equals(type)) {
+                extras.putInt(name, Integer.parseInt(value1));
+            } else if ("double".equals(type)) {
+                extras.putDouble(name, Double.parseDouble(value1));
+            } else if ("float".equals(type)) {
+                extras.putFloat(name, Float.parseFloat(value1));
+            } else if ("boolean".equals(type)) {
+                extras.putBoolean(name, Boolean.parseBoolean(value1));
+            } else if ("string".equals(type)) {
+                extras.putString(name, value1);
+            } else if ("account".equals(type)) {
+                extras.putParcelable(name, new Account(value1, value2));
+            }
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "error parsing bundle value", e);
+        } catch (NullPointerException e) {
+            Log.e(TAG, "error parsing bundle value", e);
+        }
+    }
+
+    /**
+     * 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");
+            out.attribute(null, "version", Integer.toString(ACCOUNTS_VERSION));
+            out.attribute(null, XML_ATTR_NEXT_AUTHORITY_ID, Integer.toString(mNextAuthorityId));
+            out.attribute(null, XML_ATTR_SYNC_RANDOM_OFFSET, Integer.toString(mSyncRandomOffset));
+
+            // Write the Sync Automatically flags for each user
+            final int M = mMasterSyncAutomatically.size();
+            for (int m = 0; m < M; m++) {
+                int userId = mMasterSyncAutomatically.keyAt(m);
+                Boolean listen = mMasterSyncAutomatically.valueAt(m);
+                out.startTag(null, XML_TAG_LISTEN_FOR_TICKLES);
+                out.attribute(null, XML_ATTR_USER, Integer.toString(userId));
+                out.attribute(null, XML_ATTR_ENABLED, Boolean.toString(listen));
+                out.endTag(null, XML_TAG_LISTEN_FOR_TICKLES);
+            }
+
+            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, XML_ATTR_USER, Integer.toString(authority.userId));
+                out.attribute(null, "type", authority.account.type);
+                out.attribute(null, "authority", authority.authority);
+                out.attribute(null, XML_ATTR_ENABLED, Boolean.toString(authority.enabled));
+                if (authority.syncable < 0) {
+                    out.attribute(null, "syncable", "unknown");
+                } else {
+                    out.attribute(null, "syncable", Boolean.toString(authority.syncable != 0));
+                }
+                for (Pair<Bundle, Long> periodicSync : authority.periodicSyncs) {
+                    out.startTag(null, "periodicSync");
+                    out.attribute(null, "period", Long.toString(periodicSync.second));
+                    final Bundle extras = periodicSync.first;
+                    for (String key : extras.keySet()) {
+                        out.startTag(null, "extra");
+                        out.attribute(null, "name", key);
+                        final Object value = extras.get(key);
+                        if (value instanceof Long) {
+                            out.attribute(null, "type", "long");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof Integer) {
+                            out.attribute(null, "type", "integer");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof Boolean) {
+                            out.attribute(null, "type", "boolean");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof Float) {
+                            out.attribute(null, "type", "float");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof Double) {
+                            out.attribute(null, "type", "double");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof String) {
+                            out.attribute(null, "type", "string");
+                            out.attribute(null, "value1", value.toString());
+                        } else if (value instanceof Account) {
+                            out.attribute(null, "type", "account");
+                            out.attribute(null, "value1", ((Account)value).name);
+                            out.attribute(null, "value2", ((Account)value).type);
+                        }
+                        out.endTag(null, "extra");
+                    }
+                    out.endTag(null, "periodicSync");
+                }
+                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 readAndDeleteLegacyAccountInfoLocked() {
+        // 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), 0 /* legacy is single-user */,
+                        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.numSourcePeriodic = 0;
+                    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), 0);
+                } 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();
+
+            (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 = 3;
+
+    /**
+     * 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 && version != 1) {
+                    Log.w(TAG, "Unknown pending operation version "
+                            + version + "; dropping all ops");
+                    break;
+                }
+                int authorityId = in.readInt();
+                int syncSource = in.readInt();
+                byte[] flatExtras = in.createByteArray();
+                boolean expedited;
+                if (version == PENDING_OPERATION_VERSION) {
+                    expedited = in.readInt() != 0;
+                } else {
+                    expedited = false;
+                }
+                int reason = in.readInt();
+                AuthorityInfo authority = mAuthorities.get(authorityId);
+                if (authority != null) {
+                    Bundle extras;
+                    if (flatExtras != null) {
+                        extras = unflattenBundle(flatExtras);
+                    } else {
+                        // if we are unable to parse the extras for whatever reason convert this
+                        // to a regular sync by creating an empty extras
+                        extras = new Bundle();
+                    }
+                    PendingOperation op = new PendingOperation(
+                            authority.account, authority.userId, reason, syncSource,
+                            authority.authority, extras, expedited);
+                    op.authorityId = authorityId;
+                    op.flatExtras = flatExtras;
+                    if (DEBUG_FILE) Log.v(TAG, "Adding pending op: account=" + op.account
+                            + " auth=" + op.authority
+                            + " src=" + op.syncSource
+                            + " reason=" + op.reason
+                            + " expedited=" + op.expedited
+                            + " 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);
+        out.writeInt(op.expedited ? 1 : 0);
+        out.writeInt(op.reason);
+    }
+
+    /**
+     * 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;
+    }
+
+    private void requestSync(Account account, int userId, int reason, String authority,
+            Bundle extras) {
+        // If this is happening in the system process, then call the syncrequest listener
+        // to make a request back to the SyncManager directly.
+        // If this is probably a test instance, then call back through the ContentResolver
+        // which will know which userId to apply based on the Binder id.
+        if (android.os.Process.myUid() == android.os.Process.SYSTEM_UID
+                && mSyncRequestListener != null) {
+            mSyncRequestListener.onSyncRequest(account, userId, reason, authority, extras);
+        } else {
+            ContentResolver.requestSync(account, authority, extras);
+        }
+    }
+
+    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);
+            }
+        }
+    }
+}
diff --git a/services/java/com/android/server/os/SchedulingPolicyService.java b/services/java/com/android/server/os/SchedulingPolicyService.java
new file mode 100644
index 0000000..c0123bf
--- /dev/null
+++ b/services/java/com/android/server/os/SchedulingPolicyService.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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 com.android.server.os;
+
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.ISchedulingPolicyService;
+import android.os.Process;
+
+/**
+ * The implementation of the scheduling policy service interface.
+ *
+ * @hide
+ */
+public class SchedulingPolicyService extends ISchedulingPolicyService.Stub {
+
+    private static final String TAG = "SchedulingPolicyService";
+
+    // Minimum and maximum values allowed for requestPriority parameter prio
+    private static final int PRIORITY_MIN = 1;
+    private static final int PRIORITY_MAX = 3;
+
+    public SchedulingPolicyService() {
+    }
+
+    public int requestPriority(int pid, int tid, int prio) {
+        //Log.i(TAG, "requestPriority(pid=" + pid + ", tid=" + tid + ", prio=" + prio + ")");
+
+        // Verify that caller is mediaserver, priority is in range, and that the
+        // callback thread specified by app belongs to the app that called mediaserver.
+        // Once we've verified that the caller is mediaserver, we can trust the pid but
+        // we can't trust the tid.  No need to explicitly check for pid == 0 || tid == 0,
+        // since if not the case then the getThreadGroupLeader() test will also fail.
+        if (Binder.getCallingUid() != Process.MEDIA_UID || prio < PRIORITY_MIN ||
+                prio > PRIORITY_MAX || Process.getThreadGroupLeader(tid) != pid) {
+            return PackageManager.PERMISSION_DENIED;
+        }
+        try {
+            // make good use of our CAP_SYS_NICE capability
+            Process.setThreadGroup(tid, Binder.getCallingPid() == pid ?
+                    Process.THREAD_GROUP_AUDIO_SYS : Process.THREAD_GROUP_AUDIO_APP);
+            // must be in this order or it fails the schedulability constraint
+            Process.setThreadScheduler(tid, Process.SCHED_FIFO, prio);
+        } catch (RuntimeException e) {
+            return PackageManager.PERMISSION_DENIED;
+        }
+        return PackageManager.PERMISSION_GRANTED;
+    }
+
+}
diff --git a/services/java/com/android/server/search/SearchManagerService.java b/services/java/com/android/server/search/SearchManagerService.java
new file mode 100644
index 0000000..132ae79
--- /dev/null
+++ b/services/java/com/android/server/search/SearchManagerService.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2007 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 com.android.server.search;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerNative;
+import android.app.AppGlobals;
+import android.app.ISearchManager;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.ContentObserver;
+import android.os.Binder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * The search manager service handles the search UI, and maintains a registry of searchable
+ * activities.
+ */
+public class SearchManagerService extends ISearchManager.Stub {
+
+    // general debugging support
+    private static final String TAG = "SearchManagerService";
+
+    // Context that the service is running in.
+    private final Context mContext;
+
+    // This field is initialized lazily in getSearchables(), and then never modified.
+    private final SparseArray<Searchables> mSearchables = new SparseArray<Searchables>();
+
+    /**
+     * Initializes the Search Manager service in the provided system context.
+     * Only one instance of this object should be created!
+     *
+     * @param context to use for accessing DB, window manager, etc.
+     */
+    public SearchManagerService(Context context)  {
+        mContext = context;
+        mContext.registerReceiver(new BootCompletedReceiver(),
+                new IntentFilter(Intent.ACTION_BOOT_COMPLETED));
+        mContext.registerReceiver(new UserReceiver(),
+                new IntentFilter(Intent.ACTION_USER_REMOVED));
+        new MyPackageMonitor().register(context, null, UserHandle.ALL, true);
+    }
+
+    private Searchables getSearchables(int userId) {
+        long origId = Binder.clearCallingIdentity();
+        try {
+            boolean userExists = ((UserManager) mContext.getSystemService(Context.USER_SERVICE))
+                    .getUserInfo(userId) != null;
+            if (!userExists) return null;
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+        synchronized (mSearchables) {
+            Searchables searchables = mSearchables.get(userId);
+
+            if (searchables == null) {
+                //Log.i(TAG, "Building list of searchable activities for userId=" + userId);
+                searchables = new Searchables(mContext, userId);
+                searchables.buildSearchableList();
+                mSearchables.append(userId, searchables);
+            }
+            return searchables;
+        }
+    }
+
+    private void onUserRemoved(int userId) {
+        if (userId != UserHandle.USER_OWNER) {
+            synchronized (mSearchables) {
+                mSearchables.remove(userId);
+            }
+        }
+    }
+
+    /**
+     * Creates the initial searchables list after boot.
+     */
+    private final class BootCompletedReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            new Thread() {
+                @Override
+                public void run() {
+                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                    mContext.unregisterReceiver(BootCompletedReceiver.this);
+                    getSearchables(0);
+                }
+            }.start();
+        }
+    }
+
+    private final class UserReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            onUserRemoved(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_OWNER));
+        }
+    }
+
+    /**
+     * Refreshes the "searchables" list when packages are added/removed.
+     */
+    class MyPackageMonitor extends PackageMonitor {
+
+        @Override
+        public void onSomePackagesChanged() {
+            updateSearchables();
+        }
+
+        @Override
+        public void onPackageModified(String pkg) {
+            updateSearchables();
+        }
+
+        private void updateSearchables() {
+            final int changingUserId = getChangingUserId();
+            synchronized (mSearchables) {
+                // Update list of searchable activities
+                for (int i = 0; i < mSearchables.size(); i++) {
+                    if (changingUserId == mSearchables.keyAt(i)) {
+                        getSearchables(mSearchables.keyAt(i)).buildSearchableList();
+                        break;
+                    }
+                }
+            }
+            // Inform all listeners that the list of searchables has been updated.
+            Intent intent = new Intent(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED);
+            intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+                    | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+            mContext.sendBroadcastAsUser(intent, new UserHandle(changingUserId));
+        }
+    }
+
+    class GlobalSearchProviderObserver extends ContentObserver {
+        private final ContentResolver mResolver;
+
+        public GlobalSearchProviderObserver(ContentResolver resolver) {
+            super(null);
+            mResolver = resolver;
+            mResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.SEARCH_GLOBAL_SEARCH_ACTIVITY),
+                    false /* notifyDescendants */,
+                    this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            synchronized (mSearchables) {
+                for (int i = 0; i < mSearchables.size(); i++) {
+                    getSearchables(mSearchables.keyAt(i)).buildSearchableList();
+                }
+            }
+            Intent intent = new Intent(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
+            intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+        }
+
+    }
+
+    //
+    // Searchable activities API
+    //
+
+    /**
+     * Returns the SearchableInfo for a given activity.
+     *
+     * @param launchActivity The activity from which we're launching this search.
+     * @return Returns a SearchableInfo record describing the parameters of the search,
+     * or null if no searchable metadata was available.
+     */
+    public SearchableInfo getSearchableInfo(final ComponentName launchActivity) {
+        if (launchActivity == null) {
+            Log.e(TAG, "getSearchableInfo(), activity == null");
+            return null;
+        }
+        return getSearchables(UserHandle.getCallingUserId()).getSearchableInfo(launchActivity);
+    }
+
+    /**
+     * Returns a list of the searchable activities that can be included in global search.
+     */
+    public List<SearchableInfo> getSearchablesInGlobalSearch() {
+        return getSearchables(UserHandle.getCallingUserId()).getSearchablesInGlobalSearchList();
+    }
+
+    public List<ResolveInfo> getGlobalSearchActivities() {
+        return getSearchables(UserHandle.getCallingUserId()).getGlobalSearchActivities();
+    }
+
+    /**
+     * Gets the name of the global search activity.
+     */
+    public ComponentName getGlobalSearchActivity() {
+        return getSearchables(UserHandle.getCallingUserId()).getGlobalSearchActivity();
+    }
+
+    /**
+     * Gets the name of the web search activity.
+     */
+    public ComponentName getWebSearchActivity() {
+        return getSearchables(UserHandle.getCallingUserId()).getWebSearchActivity();
+    }
+
+    @Override
+    public ComponentName getAssistIntent(int userHandle) {
+        try {
+            if (userHandle != UserHandle.getCallingUserId()) {
+                // Requesting a different user, make sure that they have the permission
+                if (ActivityManager.checkComponentPermission(
+                        android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                        Binder.getCallingUid(), -1, true)
+                        == PackageManager.PERMISSION_GRANTED) {
+                    // Translate to the current user id, if caller wasn't aware
+                    if (userHandle == UserHandle.USER_CURRENT) {
+                        long identity = Binder.clearCallingIdentity();
+                        userHandle = ActivityManagerNative.getDefault().getCurrentUser().id;
+                        Binder.restoreCallingIdentity(identity);
+                    }
+                } else {
+                    String msg = "Permission Denial: "
+                            + "Request to getAssistIntent for " + userHandle
+                            + " but is calling from user " + UserHandle.getCallingUserId()
+                            + "; this requires "
+                            + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+                    Slog.w(TAG, msg);
+                    return null;
+                }
+            }
+            IPackageManager pm = AppGlobals.getPackageManager();
+            Intent assistIntent = new Intent(Intent.ACTION_ASSIST);
+            ResolveInfo info =
+                    pm.resolveIntent(assistIntent,
+                    assistIntent.resolveTypeIfNeeded(mContext.getContentResolver()),
+                    PackageManager.MATCH_DEFAULT_ONLY, userHandle);
+            if (info != null) {
+                return new ComponentName(
+                        info.activityInfo.applicationInfo.packageName,
+                        info.activityInfo.name);
+            }
+        } catch (RemoteException re) {
+            // Local call
+            Log.e(TAG, "RemoteException in getAssistIntent: " + re);
+        } catch (Exception e) {
+            Log.e(TAG, "Exception in getAssistIntent: " + e);
+        }
+        return null;
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
+
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        synchronized (mSearchables) {
+            for (int i = 0; i < mSearchables.size(); i++) {
+                ipw.print("\nUser: "); ipw.println(mSearchables.keyAt(i));
+                ipw.increaseIndent();
+                mSearchables.valueAt(i).dump(fd, ipw, args);
+                ipw.decreaseIndent();
+            }
+        }
+    }
+}
diff --git a/services/java/com/android/server/search/Searchables.java b/services/java/com/android/server/search/Searchables.java
new file mode 100644
index 0000000..0ffbb7d
--- /dev/null
+++ b/services/java/com/android/server/search/Searchables.java
@@ -0,0 +1,464 @@
+/*
+ * 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 com.android.server.search;
+
+import android.app.AppGlobals;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This class maintains the information about all searchable activities.
+ * This is a hidden class.
+ */
+public class Searchables {
+
+    private static final String LOG_TAG = "Searchables";
+
+    // static strings used for XML lookups, etc.
+    // TODO how should these be documented for the developer, in a more structured way than
+    // the current long wordy javadoc in SearchManager.java ?
+    private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable";
+    private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*";
+
+    private Context mContext;
+
+    private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null;
+    private ArrayList<SearchableInfo> mSearchablesList = null;
+    private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null;
+    // Contains all installed activities that handle the global search
+    // intent.
+    private List<ResolveInfo> mGlobalSearchActivities;
+    private ComponentName mCurrentGlobalSearchActivity = null;
+    private ComponentName mWebSearchActivity = null;
+
+    public static String GOOGLE_SEARCH_COMPONENT_NAME =
+            "com.android.googlesearch/.GoogleSearch";
+    public static String ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME =
+            "com.google.android.providers.enhancedgooglesearch/.Launcher";
+
+    // Cache the package manager instance
+    final private IPackageManager mPm;
+    // User for which this Searchables caches information
+    private int mUserId;
+
+    /**
+     *
+     * @param context Context to use for looking up activities etc.
+     */
+    public Searchables (Context context, int userId) {
+        mContext = context;
+        mUserId = userId;
+        mPm = AppGlobals.getPackageManager();
+    }
+
+    /**
+     * Look up, or construct, based on the activity.
+     *
+     * The activities fall into three cases, based on meta-data found in
+     * the manifest entry:
+     * <ol>
+     * <li>The activity itself implements search.  This is indicated by the
+     * presence of a "android.app.searchable" meta-data attribute.
+     * The value is a reference to an XML file containing search information.</li>
+     * <li>A related activity implements search.  This is indicated by the
+     * presence of a "android.app.default_searchable" meta-data attribute.
+     * The value is a string naming the activity implementing search.  In this
+     * case the factory will "redirect" and return the searchable data.</li>
+     * <li>No searchability data is provided.  We return null here and other
+     * code will insert the "default" (e.g. contacts) search.
+     *
+     * TODO: cache the result in the map, and check the map first.
+     * TODO: it might make sense to implement the searchable reference as
+     * an application meta-data entry.  This way we don't have to pepper each
+     * and every activity.
+     * TODO: can we skip the constructor step if it's a non-searchable?
+     * TODO: does it make sense to plug the default into a slot here for
+     * automatic return?  Probably not, but it's one way to do it.
+     *
+     * @param activity The name of the current activity, or null if the
+     * activity does not define any explicit searchable metadata.
+     */
+    public SearchableInfo getSearchableInfo(ComponentName activity) {
+        // Step 1.  Is the result already hashed?  (case 1)
+        SearchableInfo result;
+        synchronized (this) {
+            result = mSearchablesMap.get(activity);
+            if (result != null) return result;
+        }
+
+        // Step 2.  See if the current activity references a searchable.
+        // Note:  Conceptually, this could be a while(true) loop, but there's
+        // no point in implementing reference chaining here and risking a loop.
+        // References must point directly to searchable activities.
+
+        ActivityInfo ai = null;
+        try {
+            ai = mPm.getActivityInfo(activity, PackageManager.GET_META_DATA, mUserId);
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error getting activity info " + re);
+            return null;
+        }
+        String refActivityName = null;
+
+        // First look for activity-specific reference
+        Bundle md = ai.metaData;
+        if (md != null) {
+            refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
+        }
+        // If not found, try for app-wide reference
+        if (refActivityName == null) {
+            md = ai.applicationInfo.metaData;
+            if (md != null) {
+                refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
+            }
+        }
+
+        // Irrespective of source, if a reference was found, follow it.
+        if (refActivityName != null)
+        {
+            // This value is deprecated, return null
+            if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) {
+                return null;
+            }
+            String pkg = activity.getPackageName();
+            ComponentName referredActivity;
+            if (refActivityName.charAt(0) == '.') {
+                referredActivity = new ComponentName(pkg, pkg + refActivityName);
+            } else {
+                referredActivity = new ComponentName(pkg, refActivityName);
+            }
+
+            // Now try the referred activity, and if found, cache
+            // it against the original name so we can skip the check
+            synchronized (this) {
+                result = mSearchablesMap.get(referredActivity);
+                if (result != null) {
+                    mSearchablesMap.put(activity, result);
+                    return result;
+                }
+            }
+        }
+
+        // Step 3.  None found. Return null.
+        return null;
+
+    }
+
+    /**
+     * Builds an entire list (suitable for display) of
+     * activities that are searchable, by iterating the entire set of
+     * ACTION_SEARCH & ACTION_WEB_SEARCH intents.
+     *
+     * Also clears the hash of all activities -> searches which will
+     * refill as the user clicks "search".
+     *
+     * This should only be done at startup and again if we know that the
+     * list has changed.
+     *
+     * TODO: every activity that provides a ACTION_SEARCH intent should
+     * also provide searchability meta-data.  There are a bunch of checks here
+     * that, if data is not found, silently skip to the next activity.  This
+     * won't help a developer trying to figure out why their activity isn't
+     * showing up in the list, but an exception here is too rough.  I would
+     * like to find a better notification mechanism.
+     *
+     * TODO: sort the list somehow?  UI choice.
+     */
+    public void buildSearchableList() {
+        // These will become the new values at the end of the method
+        HashMap<ComponentName, SearchableInfo> newSearchablesMap
+                                = new HashMap<ComponentName, SearchableInfo>();
+        ArrayList<SearchableInfo> newSearchablesList
+                                = new ArrayList<SearchableInfo>();
+        ArrayList<SearchableInfo> newSearchablesInGlobalSearchList
+                                = new ArrayList<SearchableInfo>();
+
+        // Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers.
+        List<ResolveInfo> searchList;
+        final Intent intent = new Intent(Intent.ACTION_SEARCH);
+
+        long ident = Binder.clearCallingIdentity();
+        try {
+            searchList = queryIntentActivities(intent, PackageManager.GET_META_DATA);
+
+            List<ResolveInfo> webSearchInfoList;
+            final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH);
+            webSearchInfoList = queryIntentActivities(webSearchIntent, PackageManager.GET_META_DATA);
+
+            // analyze each one, generate a Searchables record, and record
+            if (searchList != null || webSearchInfoList != null) {
+                int search_count = (searchList == null ? 0 : searchList.size());
+                int web_search_count = (webSearchInfoList == null ? 0 : webSearchInfoList.size());
+                int count = search_count + web_search_count;
+                for (int ii = 0; ii < count; ii++) {
+                    // for each component, try to find metadata
+                    ResolveInfo info = (ii < search_count)
+                            ? searchList.get(ii)
+                            : webSearchInfoList.get(ii - search_count);
+                    ActivityInfo ai = info.activityInfo;
+                    // Check first to avoid duplicate entries.
+                    if (newSearchablesMap.get(new ComponentName(ai.packageName, ai.name)) == null) {
+                        SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai,
+                                mUserId);
+                        if (searchable != null) {
+                            newSearchablesList.add(searchable);
+                            newSearchablesMap.put(searchable.getSearchActivity(), searchable);
+                            if (searchable.shouldIncludeInGlobalSearch()) {
+                                newSearchablesInGlobalSearchList.add(searchable);
+                            }
+                        }
+                    }
+                }
+            }
+
+            List<ResolveInfo> newGlobalSearchActivities = findGlobalSearchActivities();
+
+            // Find the global search activity
+            ComponentName newGlobalSearchActivity = findGlobalSearchActivity(
+                    newGlobalSearchActivities);
+
+            // Find the web search activity
+            ComponentName newWebSearchActivity = findWebSearchActivity(newGlobalSearchActivity);
+
+            // Store a consistent set of new values
+            synchronized (this) {
+                mSearchablesMap = newSearchablesMap;
+                mSearchablesList = newSearchablesList;
+                mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList;
+                mGlobalSearchActivities = newGlobalSearchActivities;
+                mCurrentGlobalSearchActivity = newGlobalSearchActivity;
+                mWebSearchActivity = newWebSearchActivity;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    /**
+     * Returns a sorted list of installed search providers as per
+     * the following heuristics:
+     *
+     * (a) System apps are given priority over non system apps.
+     * (b) Among system apps and non system apps, the relative ordering
+     * is defined by their declared priority.
+     */
+    private List<ResolveInfo> findGlobalSearchActivities() {
+        // Step 1 : Query the package manager for a list
+        // of activities that can handle the GLOBAL_SEARCH intent.
+        Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
+        List<ResolveInfo> activities =
+                    queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+        if (activities != null && !activities.isEmpty()) {
+            // Step 2: Rank matching activities according to our heuristics.
+            Collections.sort(activities, GLOBAL_SEARCH_RANKER);
+        }
+
+        return activities;
+    }
+
+    /**
+     * Finds the global search activity.
+     */
+    private ComponentName findGlobalSearchActivity(List<ResolveInfo> installed) {
+        // Fetch the global search provider from the system settings,
+        // and if it's still installed, return it.
+        final String searchProviderSetting = getGlobalSearchProviderSetting();
+        if (!TextUtils.isEmpty(searchProviderSetting)) {
+            final ComponentName globalSearchComponent = ComponentName.unflattenFromString(
+                    searchProviderSetting);
+            if (globalSearchComponent != null && isInstalled(globalSearchComponent)) {
+                return globalSearchComponent;
+            }
+        }
+
+        return getDefaultGlobalSearchProvider(installed);
+    }
+
+    /**
+     * Checks whether the global search provider with a given
+     * component name is installed on the system or not. This deals with
+     * cases such as the removal of an installed provider.
+     */
+    private boolean isInstalled(ComponentName globalSearch) {
+        Intent intent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
+        intent.setComponent(globalSearch);
+
+        List<ResolveInfo> activities = queryIntentActivities(intent,
+                PackageManager.MATCH_DEFAULT_ONLY);
+        if (activities != null && !activities.isEmpty()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private static final Comparator<ResolveInfo> GLOBAL_SEARCH_RANKER =
+            new Comparator<ResolveInfo>() {
+        @Override
+        public int compare(ResolveInfo lhs, ResolveInfo rhs) {
+            if (lhs == rhs) {
+                return 0;
+            }
+            boolean lhsSystem = isSystemApp(lhs);
+            boolean rhsSystem = isSystemApp(rhs);
+
+            if (lhsSystem && !rhsSystem) {
+                return -1;
+            } else if (rhsSystem && !lhsSystem) {
+                return 1;
+            } else {
+                // Either both system engines, or both non system
+                // engines.
+                //
+                // Note, this isn't a typo. Higher priority numbers imply
+                // higher priority, but are "lower" in the sort order.
+                return rhs.priority - lhs.priority;
+            }
+        }
+    };
+
+    /**
+     * @return true iff. the resolve info corresponds to a system application.
+     */
+    private static final boolean isSystemApp(ResolveInfo res) {
+        return (res.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+    }
+
+    /**
+     * Returns the highest ranked search provider as per the
+     * ranking defined in {@link #getGlobalSearchActivities()}.
+     */
+    private ComponentName getDefaultGlobalSearchProvider(List<ResolveInfo> providerList) {
+        if (providerList != null && !providerList.isEmpty()) {
+            ActivityInfo ai = providerList.get(0).activityInfo;
+            return new ComponentName(ai.packageName, ai.name);
+        }
+
+        Log.w(LOG_TAG, "No global search activity found");
+        return null;
+    }
+
+    private String getGlobalSearchProviderSetting() {
+        return Settings.Secure.getString(mContext.getContentResolver(),
+                Settings.Secure.SEARCH_GLOBAL_SEARCH_ACTIVITY);
+    }
+
+    /**
+     * Finds the web search activity.
+     *
+     * Only looks in the package of the global search activity.
+     */
+    private ComponentName findWebSearchActivity(ComponentName globalSearchActivity) {
+        if (globalSearchActivity == null) {
+            return null;
+        }
+        Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
+        intent.setPackage(globalSearchActivity.getPackageName());
+        List<ResolveInfo> activities =
+                queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+
+        if (activities != null && !activities.isEmpty()) {
+            ActivityInfo ai = activities.get(0).activityInfo;
+            // TODO: do some sanity checks here?
+            return new ComponentName(ai.packageName, ai.name);
+        }
+        Log.w(LOG_TAG, "No web search activity found");
+        return null;
+    }
+
+    private List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
+        List<ResolveInfo> activities = null;
+        try {
+            activities =
+                    mPm.queryIntentActivities(intent,
+                    intent.resolveTypeIfNeeded(mContext.getContentResolver()),
+                    flags, mUserId);
+        } catch (RemoteException re) {
+            // Local call
+        }
+        return activities;
+    }
+
+    /**
+     * Returns the list of searchable activities.
+     */
+    public synchronized ArrayList<SearchableInfo> getSearchablesList() {
+        ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList);
+        return result;
+    }
+
+    /**
+     * Returns a list of the searchable activities that can be included in global search.
+     */
+    public synchronized ArrayList<SearchableInfo> getSearchablesInGlobalSearchList() {
+        return new ArrayList<SearchableInfo>(mSearchablesInGlobalSearchList);
+    }
+
+    /**
+     * Returns a list of activities that handle the global search intent.
+     */
+    public synchronized ArrayList<ResolveInfo> getGlobalSearchActivities() {
+        return new ArrayList<ResolveInfo>(mGlobalSearchActivities);
+    }
+
+    /**
+     * Gets the name of the global search activity.
+     */
+    public synchronized ComponentName getGlobalSearchActivity() {
+        return mCurrentGlobalSearchActivity;
+    }
+
+    /**
+     * Gets the name of the web search activity.
+     */
+    public synchronized ComponentName getWebSearchActivity() {
+        return mWebSearchActivity;
+    }
+
+    void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("Searchable authorities:");
+        synchronized (this) {
+            if (mSearchablesList != null) {
+                for (SearchableInfo info: mSearchablesList) {
+                    pw.print("  "); pw.println(info.getSuggestAuthority());
+                }
+            }
+        }
+    }
+}