am 1317d37c: am bcaff37b: am 27174a97: am 8da96d94: am 07b5ddba: Fix CallLogProviderTest.testAddCall

* commit '1317d37cf02dfcff12f586bf1c84db3a36c5888d':
  Fix CallLogProviderTest.testAddCall
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4eb2bdf..8017966 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -59,6 +59,13 @@
             android:permission="com.android.voicemail.permission.ADD_VOICEMAIL">
         </provider>
 
+        <provider android:name="ContactMetadataProvider"
+                  android:authorities="com.android.contacts.metadata"
+                  android:multiprocess="false"
+                  android:exported="true"
+                  android:permission="android.permission.READ_WRITE_CONTACT_METADATA">
+        </provider>
+
         <!-- Handles database upgrades after OTAs, then disables itself -->
         <receiver android:name="ContactsUpgradeReceiver">
             <!-- This broadcast is sent after the core system has finished
diff --git a/res/values-az-rAZ/strings.xml b/res/values-az-rAZ/strings.xml
index d11f636..23c0885 100644
--- a/res/values-az-rAZ/strings.xml
+++ b/res/values-az-rAZ/strings.xml
@@ -19,14 +19,11 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"Kontakt Yaddaşı"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontaktlar"</string>
-    <string name="upgrade_msg" msgid="8640807392794309950">"Kontakt data bazası təkmilləşdirilir."</string>
     <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontakt təkmilləşdirməsi əlavə yaddaş tələb edir."</string>
     <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Kontakt üçün yaddaş təkmilləşdirilir."</string>
     <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Təkmilləşdirməni tamamlamaq üçün toxunun."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontaktlar"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Digər"</string>
-    <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Bütün səsli məktublara giriş"</string>
-    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Tətbiqə bu cihazın giriş əldə edə biləcəyi bütün səsli məktubları saxlamağa və əldə etməyə imkan verir."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Səsli mesaj göndərən: "</string>
     <string name="debug_dump_title" msgid="4916885724165570279">"Kontakt data bazasını kopyalayın"</string>
     <string name="debug_dump_database_message" msgid="406438635002392290">"Siz 1) informasiyaya və daxili yaddaş ehtiyatındakı zəng jurnalına bağlı data bazanızın nüsxəsini hazırlamaq 2) və onu e-poçt ilə göndərmək üzrəsiniz. Onu cihazdan kənarda və ya alınmış e-məktubda uğurla kopyalayandan sonra nüsxəsini silməyi unutmayın."</string>
diff --git a/res/values-gu-rIN/strings.xml b/res/values-gu-rIN/strings.xml
index 7f58e0c..16cd70b 100644
--- a/res/values-gu-rIN/strings.xml
+++ b/res/values-gu-rIN/strings.xml
@@ -16,7 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core એપ્લિકેશન્સ"</string>
+    <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core એપ્લિકેશનો"</string>
     <string name="app_label" msgid="3389954322874982620">"સંપર્કો સ્ટોરેજ"</string>
     <string name="provider_label" msgid="6012150850819899907">"સંપર્કો"</string>
     <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"સંપર્કો અપગ્રેડને વધુ મેમરીની જરૂર છે."</string>
diff --git a/res/values-ml-rIN/strings.xml b/res/values-ml-rIN/strings.xml
index 5493e4b..a3aa6d1 100644
--- a/res/values-ml-rIN/strings.xml
+++ b/res/values-ml-rIN/strings.xml
@@ -17,10 +17,10 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android കോർ അപ്ലിക്കേഷനുകൾ"</string>
-    <string name="app_label" msgid="3389954322874982620">"കോൺടാക്റ്റുകളുടെ സംഭരണം"</string>
+    <string name="app_label" msgid="3389954322874982620">"കോൺടാക്റ്റുകളുടെ സ്റ്റോറേജ്"</string>
     <string name="provider_label" msgid="6012150850819899907">"വിലാസങ്ങൾ"</string>
     <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"കോൺ‌ടാക്റ്റുകൾ അപ്‌ഗ്രേഡുചെയ്യാൻ കൂടുതൽ മെമ്മറി ആവശ്യമാണ്."</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"കോൺ‌ടാക്റ്റുകൾക്കായുള്ള സംഭരണം അപ്‌ഗ്രേഡുചെയ്യുന്നു"</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"കോൺ‌ടാക്റ്റുകൾക്കായുള്ള സ്റ്റോറേജ്  അപ്‌ഗ്രേഡുചെയ്യുന്നു"</string>
     <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"അപ്‌ഗ്രേഡ് പൂർത്തിയാക്കാൻ സ്‌പർശിക്കുക."</string>
     <string name="default_directory" msgid="93961630309570294">"വിലാസങ്ങൾ"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"മറ്റുള്ളവ"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 6669a21..1cf97fd 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -23,7 +23,7 @@
     <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Oppgraderer lagring for kontakter"</string>
     <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Trykk for å fullføre oppgraderingen."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontakter"</string>
-    <string name="local_invisible_directory" msgid="705244318477396120">"Andre"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Annet"</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Talemelding fra "</string>
     <string name="debug_dump_title" msgid="4916885724165570279">"Kopiér kontaktdatabasen"</string>
     <string name="debug_dump_database_message" msgid="406438635002392290">"Du er i ferd med å 1) lage en kopi av databasen som omfatter all kontaktrelatert informasjon og alle anropslogger til den interne lagringsplassen, og 2) sende kopien med e-post. Husk å slette kopien så snart du har kopiert den fra enheten eller når e-posten er mottatt."</string>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..6906957
--- /dev/null
+++ b/res/values-pt-rBR/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="sharedUserLabel" msgid="8024311725474286801">"Principais apps do Android"</string>
+    <string name="app_label" msgid="3389954322874982620">"Armazenamento de contatos"</string>
+    <string name="provider_label" msgid="6012150850819899907">"Contatos"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"A atualização de contatos precisa de mais memória."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Atualização do armazenamento para contatos"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toque para concluir a atualização."</string>
+    <string name="default_directory" msgid="93961630309570294">"Contatos"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Outros"</string>
+    <string name="voicemail_from_column" msgid="435732568832121444">"Correio de voz de "</string>
+    <string name="debug_dump_title" msgid="4916885724165570279">"Copiar banco de dados de contatos"</string>
+    <string name="debug_dump_database_message" msgid="406438635002392290">"Você está prestes a 1) fazer uma cópia de seu banco de dados no armazenamento interno, com todas as informações relacionadas aos contatos e todo o histórico de chamadas e 2) enviar essa cópia por e-mail. Lembre-se de excluir a cópia, logo que você a tiver copiado do dispositivo ou que o e-mail for recebido."</string>
+    <string name="debug_dump_delete_button" msgid="7832879421132026435">"Excluir agora"</string>
+    <string name="debug_dump_start_button" msgid="2837506913757600001">"Iniciar"</string>
+    <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Escolha um programa para enviar o arquivo"</string>
+    <string name="debug_dump_email_subject" msgid="108188398416385976">"BD de contatos anexado"</string>
+    <string name="debug_dump_email_body" msgid="4577749800871444318">"Meu banco de dados de contatos está anexado, Lá estão todas as informações de meus contatos. Use-o com cuidado."</string>
+</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 89d5140..97b65d5 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -23,7 +23,7 @@
     <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Надограђивање меморије за контакте"</string>
     <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Додирните да бисте довршили надоградњу."</string>
     <string name="default_directory" msgid="93961630309570294">"Контакти"</string>
-    <string name="local_invisible_directory" msgid="705244318477396120">"Други"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Другo"</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Говорна пошта од "</string>
     <string name="debug_dump_title" msgid="4916885724165570279">"Копирање базе података са контактима"</string>
     <string name="debug_dump_database_message" msgid="406438635002392290">"Управо ћете 1) направити копију базе података која садржи све информације у вези са контактима и целокупну евиденцију позива у интерној меморији и 2) послати је имејлом. Не заборавите да избришете копију чим је будете копирали са уређаја или чим будете примили имејл."</string>
diff --git a/src/com/android/providers/contacts/ContactDirectoryManager.java b/src/com/android/providers/contacts/ContactDirectoryManager.java
index 530a31b..b7039a2 100644
--- a/src/com/android/providers/contacts/ContactDirectoryManager.java
+++ b/src/com/android/providers/contacts/ContactDirectoryManager.java
@@ -194,7 +194,8 @@
         Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms");
 
         // Announce the change to listeners of the contacts authority
-        mContactsProvider.notifyChange(false);
+        mContactsProvider.notifyChange(/* syncToNetwork =*/false,
+                /* syncToMetadataNetwork =*/false);
     }
 
     @VisibleForTesting
diff --git a/src/com/android/providers/contacts/ContactMetadataProvider.java b/src/com/android/providers/contacts/ContactMetadataProvider.java
new file mode 100644
index 0000000..eff13fc
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactMetadataProvider.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2015 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.providers.contacts;
+
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.OperationApplicationException;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Binder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.MetadataSync;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.common.content.ProjectionMap;
+import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.ContactsDatabaseHelper.Views;
+import com.android.providers.contacts.MetadataEntryParser.MetadataEntry;
+import com.android.providers.contacts.util.SelectionBuilder;
+import com.android.providers.contacts.util.UserUtils;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
+
+import static com.android.providers.contacts.ContactsProvider2.getLimit;
+import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
+
+/**
+ * Simple content provider to handle directing contact metadata specific calls.
+ */
+public class ContactMetadataProvider extends ContentProvider {
+    private static final String TAG = "ContactMetadata";
+    private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
+    private static final int METADATA_SYNC = 1;
+    private static final int METADATA_SYNC_ID = 2;
+
+    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    static {
+        sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync", METADATA_SYNC);
+        sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync/#", METADATA_SYNC_ID);
+    }
+
+    private static final Map<String, String> sMetadataProjectionMap = ProjectionMap.builder()
+            .add(MetadataSync._ID)
+            .add(MetadataSync.RAW_CONTACT_BACKUP_ID)
+            .add(MetadataSync.ACCOUNT_TYPE)
+            .add(MetadataSync.ACCOUNT_NAME)
+            .add(MetadataSync.DATA_SET)
+            .add(MetadataSync.DATA)
+            .add(MetadataSync.DELETED)
+            .build();
+
+    private ContactsDatabaseHelper mDbHelper;
+    private ContactsProvider2 mContactsProvider;
+
+    @Override
+    public boolean onCreate() {
+        final Context context = getContext();
+        mDbHelper = getDatabaseHelper(context);
+        final IContentProvider iContentProvider = context.getContentResolver().acquireProvider(
+                ContactsContract.AUTHORITY);
+        final ContentProvider provider = ContentProvider.coerceToLocalContentProvider(
+                iContentProvider);
+        mContactsProvider = (ContactsProvider2) provider;
+        return true;
+    }
+
+    protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
+        return ContactsDatabaseHelper.getInstance(context);
+    }
+
+    @VisibleForTesting
+    protected void setDatabaseHelper(final ContactsDatabaseHelper helper) {
+        mDbHelper = helper;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "query: uri=" + uri + "  projection=" + Arrays.toString(projection) +
+                    "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
+                    "  order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
+                    " User=" + UserUtils.getCurrentUserHandle(getContext()));
+        }
+
+        final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        String limit = getLimit(uri);
+        qb.setTables(Views.METADATA_SYNC);
+        qb.setProjectionMap(sMetadataProjectionMap);
+        qb.setStrict(true);
+
+        final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
+
+        final int match = sURIMatcher.match(uri);
+        switch (match) {
+            case METADATA_SYNC:
+                break;
+
+            case METADATA_SYNC_ID: {
+                selectionBuilder.addClause(getEqualityClause(MetadataSync._ID,
+                        ContentUris.parseId(uri)));
+                break;
+            }
+            default:
+                throw new IllegalArgumentException("Unknown URL " + uri);
+        }
+
+        final SQLiteDatabase db = mDbHelper.getReadableDatabase();
+        return qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
+                null, sortOrder, limit);
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        int match = sURIMatcher.match(uri);
+        switch (match) {
+            case METADATA_SYNC:
+                return MetadataSync.CONTENT_TYPE;
+            case METADATA_SYNC_ID:
+                return MetadataSync.CONTENT_ITEM_TYPE;
+            default:
+                throw new IllegalArgumentException("Unknown URI: " + uri);
+        }
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            // Insert the new entry, and also parse the data column to update related tables.
+            final long metadataSyncId = updateOrInsertDataToMetadataSync(
+                    db, uri, values, /* isInsert = */ true);
+            db.setTransactionSuccessful();
+            return ContentUris.withAppendedId(uri, metadataSyncId);
+        } finally {
+            db.endTransaction();
+        }
+}
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            final int matchedUriId = sURIMatcher.match(uri);
+            int numDeletes = 0;
+            switch (matchedUriId) {
+                case METADATA_SYNC:
+                    Cursor c = db.query(Views.METADATA_SYNC, new String[]{MetadataSync._ID},
+                            selection, selectionArgs, null, null, null);
+                    try {
+                        while (c.moveToNext()) {
+                            final long contactMetadataId = c.getLong(0);
+                            numDeletes += db.delete(Tables.METADATA_SYNC,
+                                    MetadataSync._ID + "=" + contactMetadataId, null);
+                        }
+                    } finally {
+                        c.close();
+                    }
+                    db.setTransactionSuccessful();
+                    return numDeletes;
+                default:
+                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+                            "Calling contact metadata delete on an unknown/invalid URI", uri));
+            }
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            // Update the metadata entry and parse the data column to update related tables.
+            updateOrInsertDataToMetadataSync(db, uri, values, /* isInsert = */ false);
+            db.setTransactionSuccessful();
+            return 1;
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    @Override
+    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+            throws OperationApplicationException {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "applyBatch: " + operations.size() + " ops");
+        }
+        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            ContentProviderResult[] results = super.applyBatch(operations);
+            db.setTransactionSuccessful();
+            return results;
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "bulkInsert: " + values.length + " inserts");
+        }
+        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        db.beginTransaction();
+        try {
+            final int numValues = super.bulkInsert(uri, values);
+            db.setTransactionSuccessful();
+            return numValues;
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    /**
+     * Insert or update a non-deleted entry to MetadataSync table, and also parse the data column
+     * to update related tables for the raw contact.
+     * Set 'isInsert' as true if it's insert and false if update.
+     * Returns new inserted metadataSyncId if it's insert, and returns 1 if it's update.
+     */
+    private long updateOrInsertDataToMetadataSync(SQLiteDatabase db, Uri uri, ContentValues values,
+            boolean isInsert) {
+        final int matchUri = sURIMatcher.match(uri);
+        if ((isInsert && matchUri != METADATA_SYNC) ||
+                (!isInsert && matchUri != METADATA_SYNC && matchUri != METADATA_SYNC_ID)) {
+            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+                    "Calling contact metadata insert or update on an unknown/invalid URI", uri));
+        }
+
+        // Don't insert or update a deleted metadata.
+        Integer deleted = values.getAsInteger(MetadataSync.DELETED);
+        if (deleted != null && deleted != 0) {
+            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+                    "Cannot insert or update deleted metadata:" + values.toString(), uri));
+        }
+
+        // Check if data column is empty or null.
+        final String data = values.getAsString(MetadataSync.DATA);
+        if (TextUtils.isEmpty(data)) {
+            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+                    "Data column cannot be empty.", uri));
+        }
+
+        long result = 0;
+        if (matchUri == METADATA_SYNC_ID) {
+            // Update for the metadataSyncId.
+            final long metadataSyncId = ContentUris.parseId(uri);
+            final String selection = MetadataSync._ID + "=?";
+            final String[] selectionArgs = new String[1];
+            selectionArgs[0] = String.valueOf(metadataSyncId);
+            db.update(Tables.METADATA_SYNC, values, selection, selectionArgs);
+            result = 1;
+        } else {
+            // Update or insert for backupId and account info.
+            final Long accountId = replaceAccountInfoByAccountId(uri, values);
+            final String rawContactBackupId = values.getAsString(MetadataSync.RAW_CONTACT_BACKUP_ID);
+            deleted = 0; //Only insert or update non-deleted metadata
+            if (accountId == null || rawContactBackupId == null) {
+                throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+                        "Invalid identifier is found: accountId=" + accountId + "; " +
+                                "rawContactBackupId=" + rawContactBackupId, uri));
+            }
+
+            if (isInsert) {
+                result = mDbHelper.insertMetadataSync(rawContactBackupId, accountId, data, deleted);
+                if (result <= 0) {
+                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+                            "Metadata insertion failed. Values= " + values.toString(), uri));
+                }
+            } else {
+                mDbHelper.updateMetadataSync(rawContactBackupId, accountId, data, deleted);
+                result = 1;
+            }
+        }
+
+        // Parse the data column and update other tables.
+        // Data field will never be empty or null, since contacts prefs and usage stats
+        // have default values.
+        final MetadataEntry metadataEntry = MetadataEntryParser.parseDataToMetaDataEntry(data);
+        mContactsProvider.updateFromMetaDataEntry(db, metadataEntry);
+
+        return result;
+    }
+
+    /**
+     *  Replace account_type, account_name and data_set with account_id. If a valid account_id
+     *  cannot be found for this combination, return null.
+     */
+    private Long replaceAccountInfoByAccountId(Uri uri, ContentValues values) {
+        String accountName = values.getAsString(MetadataSync.ACCOUNT_NAME);
+        String accountType = values.getAsString(MetadataSync.ACCOUNT_TYPE);
+        String dataSet = values.getAsString(MetadataSync.DATA_SET);
+        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
+        if (partialUri) {
+            // Throw when either account is incomplete.
+            throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
+        }
+
+        final AccountWithDataSet account = AccountWithDataSet.get(
+                accountName, accountType, dataSet);
+
+        final Long id = mDbHelper.getAccountIdOrNull(account);
+        if (id == null) {
+            return null;
+        }
+
+        values.put(MetadataSyncColumns.ACCOUNT_ID, id);
+        // Only remove the account information once the account ID is extracted (since these
+        // fields are actually used by resolveAccountWithDataSet to extract the relevant ID).
+        values.remove(MetadataSync.ACCOUNT_NAME);
+        values.remove(MetadataSync.ACCOUNT_TYPE);
+        values.remove(MetadataSync.DATA_SET);
+
+        return id;
+    }
+}
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index aac37ba..bcf0b75 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -63,6 +63,7 @@
 import android.provider.ContactsContract.DisplayPhoto;
 import android.provider.ContactsContract.FullNameStyle;
 import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.MetadataSync;
 import android.provider.ContactsContract.PhoneticNameStyle;
 import android.provider.ContactsContract.PhotoFiles;
 import android.provider.ContactsContract.PinnedPositions;
@@ -79,6 +80,7 @@
 import android.text.TextUtils;
 import android.text.util.Rfc822Token;
 import android.text.util.Rfc822Tokenizer;
+import android.util.Base64;
 import android.util.Log;
 
 import com.android.common.content.SyncStateContentProviderHelper;
@@ -92,6 +94,9 @@
 
 import libcore.icu.ICU;
 
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.Locale;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -119,9 +124,10 @@
      *   800-899 Kitkat
      *   900-999 Lollipop
      *   1000-1099 M
+     *   1100-1199 N
      * </pre>
      */
-    static final int DATABASE_VERSION = 1011;
+    static final int DATABASE_VERSION = 1104;
 
     public interface Tables {
         public static final String CONTACTS = "contacts";
@@ -150,6 +156,7 @@
         public static final String DEFAULT_DIRECTORY = "default_directory";
         public static final String SEARCH_INDEX = "search_index";
         public static final String VOICEMAIL_STATUS = "voicemail_status";
+        public static final String METADATA_SYNC = "metadata_sync";
         public static final String PRE_AUTHORIZED_URIS = "pre_authorized_uris";
 
         // This list of tables contains auto-incremented sequences.
@@ -273,6 +280,15 @@
                 + " JOIN " + Tables.ACCOUNTS + " ON ("
                 + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
                 + ")";
+
+        public static final String RAW_CONTACTS_JOIN_METADATA_SYNC = Tables.RAW_CONTACTS
+                + " JOIN " + Tables.METADATA_SYNC + " ON ("
+                + RawContactsColumns.CONCRETE_BACKUP_ID + "="
+                + MetadataSyncColumns.CONCRETE_BACKUP_ID
+                + " AND "
+                + RawContactsColumns.CONCRETE_ACCOUNT_ID + "="
+                + MetadataSyncColumns.CONCRETE_ACCOUNT_ID
+                + ")";
     }
 
     public interface Joins {
@@ -304,6 +320,7 @@
         public static final String GROUPS = "view_groups";
         public static final String DATA_USAGE_STAT = "view_data_usage_stat";
         public static final String STREAM_ITEMS = "view_stream_items";
+        public static final String METADATA_SYNC = "view_metadata_sync";
     }
 
     public interface Projections {
@@ -441,6 +458,8 @@
         public static final String CONCRETE_PINNED =
                 Tables.RAW_CONTACTS + "." + RawContacts.PINNED;
 
+        public static final String CONCRETE_METADATA_DIRTY =
+                Tables.RAW_CONTACTS + "." + RawContacts.METADATA_DIRTY;
         public static final String DISPLAY_NAME = RawContacts.DISPLAY_NAME_PRIMARY;
         public static final String DISPLAY_NAME_SOURCE = RawContacts.DISPLAY_NAME_SOURCE;
         public static final String AGGREGATION_NEEDED = "aggregation_needed";
@@ -729,6 +748,16 @@
         public static final int USAGE_TYPE_INT_SHORT_TEXT = 2;
     }
 
+    public interface MetadataSyncColumns {
+        static final String CONCRETE_ID = Tables.METADATA_SYNC + "._id";
+        static final String ACCOUNT_ID = "account_id";
+        static final String CONCRETE_BACKUP_ID = Tables.METADATA_SYNC + "." +
+                MetadataSync.RAW_CONTACT_BACKUP_ID;
+        static final String CONCRETE_ACCOUNT_ID = Tables.METADATA_SYNC + "." + ACCOUNT_ID;
+        static final String CONCRETE_DELETED = Tables.METADATA_SYNC + "." +
+                MetadataSync.DELETED;
+    }
+
     private  interface EmailQuery {
         public static final String TABLE = Tables.DATA;
 
@@ -984,9 +1013,20 @@
     private SQLiteStatement mStatusUpdateDelete;
     private SQLiteStatement mResetNameVerifiedForOtherRawContacts;
     private SQLiteStatement mContactInDefaultDirectoryQuery;
+    private SQLiteStatement mMetadataSyncInsert;
+    private SQLiteStatement mMetadataSyncUpdate;
 
     private StringBuilder mSb = new StringBuilder();
 
+    private MessageDigest mMessageDigest;
+    {
+        try {
+            mMessageDigest = MessageDigest.getInstance("SHA-1");
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("No such algorithm.", e);
+        }
+    }
+
     private boolean mUseStrictPhoneNumberComparison;
 
     private String[] mSelectionArgs1 = new String[1];
@@ -1217,6 +1257,7 @@
                 RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," +
                 RawContacts.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.DELETED + " INTEGER NOT NULL DEFAULT 0," +
+                RawContacts.METADATA_DIRTY + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," +
                 RawContacts.AGGREGATION_MODE + " INTEGER NOT NULL DEFAULT " +
                         RawContacts.AGGREGATION_MODE_DEFAULT + "," +
@@ -1581,6 +1622,19 @@
                 DataUsageStatColumns.USAGE_TYPE_INT +
         ");");
 
+        db.execSQL("CREATE TABLE IF NOT EXISTS "
+                + Tables.METADATA_SYNC + " (" +
+                MetadataSync._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                MetadataSync.RAW_CONTACT_BACKUP_ID + " TEXT NOT NULL," +
+                MetadataSyncColumns.ACCOUNT_ID + " INTEGER NOT NULL," +
+                MetadataSync.DATA + " TEXT," +
+                MetadataSync.DELETED + " INTEGER NOT NULL DEFAULT 0);");
+
+        db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS metadata_sync_index ON " +
+                Tables.METADATA_SYNC + " (" +
+                MetadataSync.RAW_CONTACT_BACKUP_ID + ", " +
+                MetadataSyncColumns.ACCOUNT_ID +");");
+
         db.execSQL("CREATE TABLE " + Tables.PRE_AUTHORIZED_URIS + " ("+
                 PreAuthorizedUris._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                 PreAuthorizedUris.URI + " STRING NOT NULL, " +
@@ -1830,6 +1884,7 @@
         db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES + ";");
         db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_STAT + ";");
         db.execSQL("DROP VIEW IF EXISTS " + Views.STREAM_ITEMS + ";");
+        db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC + ";");
 
         String dataColumns =
                 Data.IS_PRIMARY + ", "
@@ -1982,6 +2037,7 @@
                 + RawContacts.AGGREGATION_MODE + ", "
                 + RawContacts.RAW_CONTACT_IS_READ_ONLY + ", "
                 + RawContacts.DELETED + ", "
+                + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
                 + RawContacts.DISPLAY_NAME_SOURCE  + ", "
                 + RawContacts.DISPLAY_NAME_PRIMARY  + ", "
                 + RawContacts.DISPLAY_NAME_ALTERNATIVE  + ", "
@@ -2035,6 +2091,7 @@
         String rawEntitiesSelect = "SELECT "
                 + RawContacts.CONTACT_ID + ", "
                 + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
+                + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
                 + dataColumns + ", "
                 + syncColumns + ", "
                 + Data.SYNC1 + ", "
@@ -2068,6 +2125,7 @@
                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + Contacts._ID + ", "
                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
                 + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
+                + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
                 + dataColumns + ", "
                 + syncColumns + ", "
                 + contactsColumns + ", "
@@ -2157,6 +2215,21 @@
                 + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")";
 
         db.execSQL("CREATE VIEW " + Views.STREAM_ITEMS + " AS " + streamItemSelect);
+
+        String metadataSyncSelect = "SELECT " +
+                MetadataSyncColumns.CONCRETE_ID + ", " +
+                MetadataSync.RAW_CONTACT_BACKUP_ID + ", " +
+                AccountsColumns.ACCOUNT_NAME + ", " +
+                AccountsColumns.ACCOUNT_TYPE + ", " +
+                AccountsColumns.DATA_SET + ", " +
+                MetadataSync.DATA + ", " +
+                MetadataSync.DELETED +
+                " FROM " + Tables.METADATA_SYNC
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                +   MetadataSyncColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
+                + ")";
+
+        db.execSQL("CREATE VIEW " + Views.METADATA_SYNC + " AS " + metadataSyncSelect);
     }
 
     private static String buildDisplayPhotoUriAlias(String contactIdColumn, String alias) {
@@ -2895,19 +2968,33 @@
             oldVersion = 1009;
         }
 
-        if (oldVersion < 1010) {
-            upgradeToVersion1010(db);
-            rebuildSqliteStats = true;
-            oldVersion = 1010;
+        if (oldVersion < 1100) {
+            upgradeToVersion1100(db);
+            upgradeViewsAndTriggers = true;
+            oldVersion = 1100;
         }
 
-        if (oldVersion < 1011) {
-            // There was a merge error, so do step1010 again, and also re-crete views.
-            // upgradeToVersion1010() is safe to re-run.
-            upgradeToVersion1010(db);
-            rebuildSqliteStats = true;
+        if (oldVersion < 1101) {
+            upgradeToVersion1101(db);
+            oldVersion = 1101;
+        }
+
+        if (oldVersion < 1102) {
+            // Version 1009 was added *after* 1100/1101.  For master devices
+            // that have already been updated to 1101, we do it again.
+            upgradeToVersion1009(db);
+            oldVersion = 1102;
+        }
+
+        if (oldVersion < 1103) {
             upgradeViewsAndTriggers = true;
-            oldVersion = 1011;
+            oldVersion = 1103;
+        }
+
+        if (oldVersion < 1104) {
+            upgradeToVersion1104(db);
+            upgradeViewsAndTriggers = true;
+            oldVersion = 1104;
         }
 
         if (upgradeViewsAndTriggers) {
@@ -3646,7 +3733,7 @@
     private void upgradeToVersion401(SQLiteDatabase db) {
         db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" +
                 Contacts._ID + " INTEGER PRIMARY KEY" +
-        ");");
+                ");");
         db.execSQL("INSERT INTO " + Tables.VISIBLE_CONTACTS +
                 " SELECT " + Contacts._ID +
                 " FROM " + Tables.CONTACTS +
@@ -3801,8 +3888,8 @@
                 " ADD " + Groups.GROUP_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0");
         db.execSQL(
                 "UPDATE " + Tables.GROUPS +
-                "   SET " + Groups.GROUP_IS_READ_ONLY + "=1" +
-                " WHERE " + Groups.SYSTEM_ID + " NOT NULL");
+                        "   SET " + Groups.GROUP_IS_READ_ONLY + "=1" +
+                        " WHERE " + Groups.SYSTEM_ID + " NOT NULL");
     }
 
     private void upgradeToVersion416(SQLiteDatabase db) {
@@ -4419,14 +4506,97 @@
             // Log verbose because this should be the majority case.
             Log.v(TAG, "Version 1007: Columns already exist, skipping upgrade steps.");
         }
-    }
+  }
+
 
     public void upgradeToVersion1009(SQLiteDatabase db) {
-        db.execSQL("ALTER TABLE data ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
+        try {
+            db.execSQL("ALTER TABLE data ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
+        } catch (SQLiteException ignore) {
+        }
     }
 
-    public void upgradeToVersion1010(SQLiteDatabase db) {
-        db.execSQL("DROP TABLE IF EXISTS metadata_sync");
+    private void upgradeToVersion1100(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE raw_contacts ADD metadata_dirty INTEGER NOT NULL DEFAULT 0;");
+    }
+
+    // Data.hash_id column is used for metadata backup, and this upgrade is to generate
+    // hash_id column. Usually data1 and data2 are two main columns to store data info.
+    // But for photo, we don't use data1 and data2, instead, use data15 to store photo blob.
+    // So this upgrade generates hash_id from (data1 + data2) or (data15) using sha-1.
+    private void upgradeToVersion1101(SQLiteDatabase db) {
+        final SQLiteStatement update = db.compileStatement(
+                "UPDATE " + Tables.DATA +
+                " SET " + Data.HASH_ID + "=?" +
+                " WHERE " + Data._ID + "=?"
+        );
+        final String selection = Data.HASH_ID + " IS NULL";
+        final Cursor c = db.query(Tables.DATA, new String[] {Data._ID, Data.DATA1, Data.DATA2,
+                        Data.DATA15},
+                selection, null, null, null, null);
+        try {
+            while (c.moveToNext()) {
+                final long dataId = c.getLong(0);
+                final String data1 = c.getString(1);
+                final String data2 = c.getString(2);
+                final byte[] data15 = c.getBlob(3);
+                final String hashId = generateHashId(data1, data2, data15);
+                if (!TextUtils.isEmpty(hashId)) {
+                    update.bindString(1, hashId);
+                    update.bindLong(2, dataId);
+                    update.execute();
+                }
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Add new metadata_sync table to cache the meta data on raw contacts level from server before
+     * they are merged into other CP2 tables. The data column is the blob column containing all
+     * the backed up metadata for this raw_contact. This table should only be used by metadata
+     * sync adapter.
+     */
+    public void upgradeToVersion1104(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS metadata_sync;");
+        db.execSQL("CREATE TABLE metadata_sync (" +
+                "_id INTEGER PRIMARY KEY AUTOINCREMENT, raw_contact_backup_id TEXT NOT NULL, " +
+                "account_id INTEGER NOT NULL, data TEXT, deleted INTEGER NOT NULL DEFAULT 0);");
+        db.execSQL("CREATE UNIQUE INDEX metadata_sync_index ON metadata_sync (" +
+                "raw_contact_backup_id, account_id);");
+    }
+
+    /**
+     * Generate hash_id from data1, data2 and data15 columns.
+     * If one of data1 and data2 is not null, using data1 and data2 to get hash_id,
+     * otherwise, using data15 to generate.
+     */
+    public String generateHashId(String data1, String data2, byte[] data15) {
+        final StringBuilder sb = new StringBuilder();
+        byte[] hashInput = null;
+        if (!TextUtils.isEmpty(data1) || !TextUtils.isEmpty(data2)) {
+            sb.append(data1);
+            sb.append(data2);
+            hashInput = sb.toString().getBytes();
+        } else if (data15 != null) {
+            hashInput = data15;
+        }
+        if (hashInput != null) {
+            final String hashId = generateHashIdForData(hashInput);
+            return hashId;
+        } else {
+            return null;
+        }
+    }
+
+    // Use SHA-1 hash method to generate hash string for the input.
+    @VisibleForTesting
+    String generateHashIdForData(byte[] input) {
+        synchronized (mMessageDigest) {
+            final byte[] hashResult = mMessageDigest.digest(input);
+            return Base64.encodeToString(hashResult, Base64.DEFAULT);
+        }
     }
 
     public String extractHandleFromEmailAddress(String email) {
@@ -4617,6 +4787,9 @@
             updateIndexStats(db, Tables.DATA_USAGE_STAT,
                     "data_usage_stat_index", "20 2 1");
 
+            updateIndexStats(db, Tables.METADATA_SYNC,
+                    "metadata_sync_index", "10000 1 1");
+
             // Tiny tables
             updateIndexStats(db, Tables.AGGREGATION_EXCEPTIONS,
                     null, "10");
@@ -4952,14 +5125,14 @@
         }
         final SQLiteStatement select = getWritableDatabase().compileStatement(
                 "SELECT " + AccountsColumns._ID +
-                " FROM " + Tables.ACCOUNTS +
-                " WHERE " +
-                "((?1 IS NULL AND " + AccountsColumns.ACCOUNT_NAME + " IS NULL) OR " +
-                "(" + AccountsColumns.ACCOUNT_NAME + "=?1)) AND " +
-                "((?2 IS NULL AND " + AccountsColumns.ACCOUNT_TYPE + " IS NULL) OR " +
-                "(" + AccountsColumns.ACCOUNT_TYPE + "=?2)) AND " +
-                "((?3 IS NULL AND " + AccountsColumns.DATA_SET + " IS NULL) OR " +
-                "(" + AccountsColumns.DATA_SET + "=?3))");
+                        " FROM " + Tables.ACCOUNTS +
+                        " WHERE " +
+                        "((?1 IS NULL AND " + AccountsColumns.ACCOUNT_NAME + " IS NULL) OR " +
+                        "(" + AccountsColumns.ACCOUNT_NAME + "=?1)) AND " +
+                        "((?2 IS NULL AND " + AccountsColumns.ACCOUNT_TYPE + " IS NULL) OR " +
+                        "(" + AccountsColumns.ACCOUNT_TYPE + "=?2)) AND " +
+                        "((?3 IS NULL AND " + AccountsColumns.DATA_SET + " IS NULL) OR " +
+                        "(" + AccountsColumns.DATA_SET + "=?3))");
         try {
             DatabaseUtils.bindObjectToProgram(select, 1, accountWithDataSet.getAccountName());
             DatabaseUtils.bindObjectToProgram(select, 2, accountWithDataSet.getAccountType());
@@ -5987,4 +6160,40 @@
                 " WHERE " + SearchIndexColumns.CONTACT_ID + "=CAST(? AS int)",
                 new String[] {String.valueOf(contactId)});
     }
+
+    public long insertMetadataSync(String backupId, Long accountId, String data, Integer deleted) {
+        if (mMetadataSyncInsert == null) {
+            mMetadataSyncInsert = getWritableDatabase().compileStatement(
+                    "INSERT INTO " + Tables.METADATA_SYNC + "("
+                            + MetadataSync.RAW_CONTACT_BACKUP_ID + ", "
+                            + MetadataSyncColumns.ACCOUNT_ID + ", "
+                            + MetadataSync.DATA + ","
+                            + MetadataSync.DELETED + ")" +
+                            " VALUES (?,?,?,?)");
+        }
+        mMetadataSyncInsert.bindString(1, backupId);
+        mMetadataSyncInsert.bindLong(2, accountId);
+        data = (data == null) ? "" : data;
+        mMetadataSyncInsert.bindString(3, data);
+        mMetadataSyncInsert.bindLong(4, deleted);
+        return mMetadataSyncInsert.executeInsert();
+    }
+
+    public void updateMetadataSync(String backupId, Long accountId, String data, Integer deleted) {
+        if (mMetadataSyncUpdate == null) {
+            mMetadataSyncUpdate = getWritableDatabase().compileStatement(
+                    "UPDATE " + Tables.METADATA_SYNC
+                            + " SET " + MetadataSync.DATA + "=?,"
+                            + MetadataSync.DELETED + "=?"
+                            + " WHERE " + MetadataSync.RAW_CONTACT_BACKUP_ID + "=? AND "
+                            + MetadataSyncColumns.ACCOUNT_ID + "=?");
+        }
+
+        data = (data == null) ? "" : data;
+        mMetadataSyncUpdate.bindString(1, data);
+        mMetadataSyncUpdate.bindLong(2, deleted);
+        mMetadataSyncUpdate.bindString(3, backupId);
+        mMetadataSyncUpdate.bindLong(4, accountId);
+        mMetadataSyncUpdate.execute();
+    }
 }
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 7dc7e7e..abe54e9 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -92,6 +92,7 @@
 import android.provider.ContactsContract.Directory;
 import android.provider.ContactsContract.DisplayPhoto;
 import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.MetadataSync;
 import android.provider.ContactsContract.PhoneLookup;
 import android.provider.ContactsContract.PhotoFiles;
 import android.provider.ContactsContract.PinnedPositions;
@@ -112,6 +113,7 @@
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
+import android.util.Base64;
 import android.util.Log;
 import com.android.common.content.ProjectionMap;
 import com.android.common.content.SyncStateContentProviderHelper;
@@ -129,6 +131,7 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
 import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Joins;
+import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
@@ -155,6 +158,11 @@
 import com.android.providers.contacts.database.ContactsTableUtil;
 import com.android.providers.contacts.database.DeletedContactsTableUtil;
 import com.android.providers.contacts.database.MoreDatabaseUtils;
+import com.android.providers.contacts.MetadataEntryParser.AggregationData;
+import com.android.providers.contacts.MetadataEntryParser.FieldData;
+import com.android.providers.contacts.MetadataEntryParser.MetadataEntry;
+import com.android.providers.contacts.MetadataEntryParser.RawContactInfo;
+import com.android.providers.contacts.MetadataEntryParser.UsageStats;
 import com.android.providers.contacts.util.Clock;
 import com.android.providers.contacts.util.ContactsPermissions;
 import com.android.providers.contacts.util.DbQueryUtils;
@@ -178,7 +186,10 @@
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
 import java.io.Writer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -538,6 +549,23 @@
             " SET " + RawContacts.DIRTY + "=1" +
             " WHERE " + RawContacts._ID + " IN (";
 
+    /** Sql for updating METADATA_DIRTY flag on multiple raw contacts */
+    private static final String UPDATE_RAW_CONTACT_SET_METADATA_DIRTY_SQL =
+            "UPDATE " + Tables.RAW_CONTACTS +
+                    " SET " + RawContacts.METADATA_DIRTY + "=1" +
+                    " WHERE " + RawContacts._ID + " IN (";
+
+    // Sql for updating MetadataSync.DELETED flag on multiple raw contacts.
+    // When using this sql, add comma separated raw contacts ids and "))".
+    private static final String UPDATE_METADATASYNC_SET_DELETED_SQL =
+            "UPDATE " + Tables.METADATA_SYNC
+                    + " SET " + MetadataSync.DELETED + "=1"
+                    + " WHERE " + MetadataSync._ID + " IN "
+                            + "(SELECT " + MetadataSyncColumns.CONCRETE_ID
+                            + " FROM " + Tables.RAW_CONTACTS_JOIN_METADATA_SYNC
+                            + " WHERE " + RawContactsColumns.CONCRETE_DELETED + "=1 AND "
+                            + RawContactsColumns.CONCRETE_ID + " IN (";
+
     /** Sql for updating VERSION on multiple raw contacts */
     private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
             "UPDATE " + Tables.RAW_CONTACTS +
@@ -844,6 +872,7 @@
             .add(RawContacts.PINNED)
             .add(RawContacts.AGGREGATION_MODE)
             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
+            .add(RawContacts.METADATA_DIRTY)
             .addAll(sRawContactColumns)
             .addAll(sRawContactSyncColumns)
             .build();
@@ -914,6 +943,7 @@
             .add(Data._ID, "MIN(" + Data._ID + ")")
             .add(RawContacts.CONTACT_ID)
             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
+            .add(Data.HASH_ID)
             .addAll(sDataColumns)
             .addAll(sDataPresenceColumns)
             .addAll(sContactsColumns)
@@ -1443,6 +1473,7 @@
     private boolean mVisibleTouched = false;
 
     private boolean mSyncToNetwork;
+    private boolean mSyncToMetadataNetWork;
 
     private LocaleSet mCurrentLocales;
     private int mContactsAccountCount;
@@ -1459,6 +1490,9 @@
     private int mFastScrollingIndexCacheMissCount;
     private long mTotalTimeFastScrollingIndexGenerate;
 
+    // MetadataSync flag.
+    private boolean mMetadataSyncEnabled;
+
     @Override
     public boolean onCreate() {
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
@@ -1494,6 +1528,9 @@
 
         mFastScrollingIndexCache = FastScrollingIndexCache.getInstance(getContext());
 
+        mMetadataSyncEnabled = android.provider.Settings.Global.getInt(
+                getContext().getContentResolver(), Global.CONTACT_METADATA_SYNC, 0) == 1;
+
         mContactsHelper = getDatabaseHelper(getContext());
         mDbHelper.set(mContactsHelper);
 
@@ -1977,7 +2014,8 @@
                         ContentValues updateValues = new ContentValues();
                         updateValues.putNull(Photo.PHOTO_FILE_ID);
                         updateData(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
-                                updateValues, null, null, false);
+                                updateValues, null, null, /* callerIsSyncAdapter =*/false,
+                                /* callerIsMetadataSyncAdapter =*/false);
                     }
                     if (photoFileIdToStreamItemPhotoId.containsKey(missingPhotoId)) {
                         // For missing photos that were in stream item photos, just delete the
@@ -1998,7 +2036,7 @@
     }
 
     @Override
-    protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
+    public ContactsDatabaseHelper getDatabaseHelper(final Context context) {
         return ContactsDatabaseHelper.getInstance(context);
     }
 
@@ -2419,9 +2457,12 @@
         for (long rawContactId : mTransactionContext.get().getInsertedRawContactIds()) {
             mDbHelper.get().updateRawContactDisplayName(db, rawContactId);
             mAggregator.get().onRawContactInsert(mTransactionContext.get(), db, rawContactId);
+            if (mMetadataSyncEnabled) {
+                updateMetadataOnRawContactInsert(db, rawContactId);
+            }
         }
 
-        Set<Long> dirtyRawContacts = mTransactionContext.get().getDirtyRawContactIds();
+        final Set<Long> dirtyRawContacts = mTransactionContext.get().getDirtyRawContactIds();
         if (!dirtyRawContacts.isEmpty()) {
             mSb.setLength(0);
             mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
@@ -2430,7 +2471,7 @@
             db.execSQL(mSb.toString());
         }
 
-        Set<Long> updatedRawContacts = mTransactionContext.get().getUpdatedRawContactIds();
+        final Set<Long> updatedRawContacts = mTransactionContext.get().getUpdatedRawContactIds();
         if (!updatedRawContacts.isEmpty()) {
             mSb.setLength(0);
             mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
@@ -2439,8 +2480,29 @@
             db.execSQL(mSb.toString());
         }
 
+        final Set<Long> metadataDirtyRawContacts =
+                mTransactionContext.get().getMetadataDirtyRawContactIds();
+        if (!metadataDirtyRawContacts.isEmpty() && mMetadataSyncEnabled) {
+            mSb.setLength(0);
+            mSb.append(UPDATE_RAW_CONTACT_SET_METADATA_DIRTY_SQL);
+            appendIds(mSb, metadataDirtyRawContacts);
+            mSb.append(")");
+            db.execSQL(mSb.toString());
+            mSyncToMetadataNetWork = true;
+        }
+
         final Set<Long> changedRawContacts = mTransactionContext.get().getChangedRawContactIds();
         ContactsTableUtil.updateContactLastUpdateByRawContactId(db, changedRawContacts);
+        if (!changedRawContacts.isEmpty() && mMetadataSyncEnabled) {
+            // For the deleted raw contact, set related metadata as deleted
+            // if metadata flag is enabled.
+            mSb.setLength(0);
+            mSb.append(UPDATE_METADATASYNC_SET_DELETED_SQL);
+            appendIds(mSb, changedRawContacts);
+            mSb.append("))");
+            db.execSQL(mSb.toString());
+            mSyncToMetadataNetWork = true;
+        }
 
         // Update sync states.
         for (Map.Entry<Long, Object> entry : mTransactionContext.get().getUpdatedSyncStates()) {
@@ -2454,6 +2516,54 @@
         mTransactionContext.get().clearExceptSearchIndexUpdates();
     }
 
+    @VisibleForTesting
+    void setMetadataSyncForTest(boolean enabled) {
+        mMetadataSyncEnabled = enabled;
+    }
+
+    interface MetadataSyncQuery {
+        String TABLE = Tables.RAW_CONTACTS_JOIN_METADATA_SYNC;
+        String[] COLUMNS = new String[] {
+                MetadataSyncColumns.CONCRETE_ID,
+                MetadataSync.DATA
+        };
+        int METADATA_SYNC_ID = 0;
+        int METADATA_SYNC_DATA = 1;
+        String SELECTION = MetadataSyncColumns.CONCRETE_DELETED + "=0 AND " +
+                RawContactsColumns.CONCRETE_ID + "=?";
+    }
+
+    /**
+     * Fetch the related metadataSync data column for the raw contact id.
+     * Returns null if there's no metadata for the raw contact.
+     */
+    private String queryMetadataSyncData(SQLiteDatabase db, long rawContactId) {
+        String metadataSyncData = null;
+        mSelectionArgs1[0] = String.valueOf(rawContactId);
+        final Cursor cursor = db.query(MetadataSyncQuery.TABLE,
+                MetadataSyncQuery.COLUMNS, MetadataSyncQuery.SELECTION,
+                mSelectionArgs1, null, null, null);
+        try {
+            if (cursor.moveToFirst()) {
+                metadataSyncData = cursor.getString(MetadataSyncQuery.METADATA_SYNC_DATA);
+            }
+        } finally {
+            cursor.close();
+        }
+        return metadataSyncData;
+    }
+
+    private void updateMetadataOnRawContactInsert(SQLiteDatabase db, long rawContactId) {
+        // Read metadata from MetadataSync table for the raw contact, and update.
+        final String metadataSyncData = queryMetadataSyncData(db, rawContactId);
+        if (TextUtils.isEmpty(metadataSyncData)) {
+            return;
+        }
+        final MetadataEntry metadataEntry = MetadataEntryParser.parseDataToMetaDataEntry(
+                metadataSyncData);
+        updateFromMetaDataEntry(db, metadataEntry);
+    }
+
     /**
      * Appends comma separated IDs.
      * @param ids Should not be empty
@@ -2468,13 +2578,17 @@
 
     @Override
     protected void notifyChange() {
-        notifyChange(mSyncToNetwork);
+        notifyChange(mSyncToNetwork, mMetadataSyncEnabled);
         mSyncToNetwork = false;
+        mSyncToMetadataNetWork = false;
     }
 
-    protected void notifyChange(boolean syncToNetwork) {
+    protected void notifyChange(boolean syncToNetwork, boolean syncToMetadataNetwork) {
         getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
                 syncToNetwork);
+
+        getContext().getContentResolver().notifyChange(MetadataSync.METADATA_AUTHORITY_URI,
+                null, syncToMetadataNetwork);
     }
 
     protected void setProviderStatus(int status) {
@@ -2739,6 +2853,7 @@
             values.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
         }
 
+        final boolean needToUpdateMetadata = shouldMarkMetadataDirtyForRawContact(values);
         // Databases that were created prior to the 906 upgrade have a default of Int.MAX_VALUE
         // for RawContacts.PINNED. Manually set the value to the correct default (0) if it is not
         // set.
@@ -2750,6 +2865,11 @@
         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
         final long rawContactId = db.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, values);
 
+        if (needToUpdateMetadata) {
+            mTransactionContext.get().markRawContactMetadataDirty(rawContactId,
+                    /* isMetadataSyncAdapter =*/false);
+        }
+
         final int aggregationMode = getIntValue(values, RawContacts.AGGREGATION_MODE,
                 RawContacts.AGGREGATION_MODE_DEFAULT);
         mAggregator.get().markNewForAggregation(rawContactId, aggregationMode);
@@ -2812,6 +2932,9 @@
                 mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
 
         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
+        // Generate hash_id from data1 and data2 column, since group data stores in data1 field.
+        getDataRowHandler(GroupMembership.CONTENT_ITEM_TYPE).handleHashIdForInsert(
+                groupMembershipValues);
         db.insert(Tables.DATA, null, groupMembershipValues);
     }
 
@@ -3830,7 +3953,8 @@
         values.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
         values.putNull(RawContacts.CONTACT_ID);
         values.put(RawContacts.DIRTY, 1);
-        return updateRawContact(db, rawContactId, values, callerIsSyncAdapter);
+        return updateRawContact(db, rawContactId, values, callerIsSyncAdapter,
+                /* callerIsMetadataSyncAdapter =*/false);
     }
 
     private int deleteDataUsage() {
@@ -3932,7 +4056,8 @@
                 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
                     + (selection == null ? "" : " AND " + selection);
 
-                count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
+                count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter,
+                        /* callerIsMetadataSyncAdapter =*/false);
                 break;
             }
 
@@ -3940,7 +4065,8 @@
             case PROFILE_DATA: {
                 invalidateFastScrollingIndexCache();
                 count = updateData(uri, values, appendAccountToSelection(uri, selection),
-                        selectionArgs, callerIsSyncAdapter);
+                        selectionArgs, callerIsSyncAdapter,
+                        /* callerIsMetadataSyncAdapter =*/false);
                 if (count > 0) {
                     mSyncToNetwork |= !callerIsSyncAdapter;
                 }
@@ -3953,7 +4079,8 @@
             case CALLABLES_ID:
             case POSTALS_ID: {
                 invalidateFastScrollingIndexCache();
-                count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
+                count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter,
+                        /* callerIsMetadataSyncAdapter =*/false);
                 if (count > 0) {
                     mSyncToNetwork |= !callerIsSyncAdapter;
                 }
@@ -4006,7 +4133,8 @@
             }
 
             case AGGREGATION_EXCEPTIONS: {
-                count = updateAggregationException(db, values);
+                count = updateAggregationException(db, values,
+                        /* callerIsMetadataSyncAdapter =*/false);
                 invalidateFastScrollingIndexCache();
                 break;
             }
@@ -4317,7 +4445,8 @@
         try {
             while (cursor.moveToNext()) {
                 long rawContactId = cursor.getLong(0);
-                updateRawContact(db, rawContactId, values, callerIsSyncAdapter);
+                updateRawContact(db, rawContactId, values, callerIsSyncAdapter,
+                        /* callerIsMetadataSyncAdapter =*/false);
                 count++;
             }
         } finally {
@@ -4328,7 +4457,7 @@
     }
 
     private int updateRawContact(SQLiteDatabase db, long rawContactId, ContentValues values,
-            boolean callerIsSyncAdapter) {
+            boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
         final String selection = RawContactsColumns.CONCRETE_ID + " = ?";
         mSelectionArgs1[0] = Long.toString(rawContactId);
 
@@ -4403,6 +4532,10 @@
             if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) {
                 aggregator.markForAggregation(rawContactId, aggregationMode, false);
             }
+            if (shouldMarkMetadataDirtyForRawContact(values)) {
+                mTransactionContext.get().markRawContactMetadataDirty(
+                        rawContactId, callerIsMetadataSyncAdapter);
+            }
             if (flagExists(values, RawContacts.STARRED)) {
                 if (!callerIsSyncAdapter) {
                     updateFavoritesMembership(rawContactId, flagIsSet(values, RawContacts.STARRED));
@@ -4444,7 +4577,8 @@
     }
 
     private int updateData(Uri uri, ContentValues inputValues, String selection,
-            String[] selectionArgs, boolean callerIsSyncAdapter) {
+            String[] selectionArgs, boolean callerIsSyncAdapter,
+            boolean callerIsMetadataSyncAdapter) {
 
         final ContentValues values = new ContentValues(inputValues);
         values.remove(Data._ID);
@@ -4470,7 +4604,7 @@
                 selection, selectionArgs, null, -1 /* directory ID */, null);
         try {
             while(c.moveToNext()) {
-                count += updateData(values, c, callerIsSyncAdapter);
+                count += updateData(values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter);
             }
         } finally {
             c.close();
@@ -4486,7 +4620,8 @@
         }
     }
 
-    private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
+    private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter,
+            boolean callerIsMetadataSyncAdapter) {
         if (values.size() == 0) {
             return 0;
         }
@@ -4501,7 +4636,7 @@
         DataRowHandler rowHandler = getDataRowHandler(mimeType);
         boolean updated =
                 rowHandler.update(db, mTransactionContext.get(), values, c,
-                        callerIsSyncAdapter);
+                        callerIsSyncAdapter, callerIsMetadataSyncAdapter);
         if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
             scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
         }
@@ -4556,11 +4691,17 @@
             return 0;  // Nothing to update, bail out.
         }
 
-        boolean hasStarredValue = flagExists(values, RawContacts.STARRED);
+        final boolean hasStarredValue = flagExists(values, RawContacts.STARRED);
+        final boolean hasPinnedValue = flagExists(values, RawContacts.PINNED);
+        final boolean hasVoiceMailValue = flagExists(values, RawContacts.SEND_TO_VOICEMAIL);
         if (hasStarredValue) {
             // Mark dirty when changing starred to trigger sync.
             values.put(RawContacts.DIRTY, 1);
         }
+        if (hasStarredValue || hasPinnedValue || hasVoiceMailValue) {
+            // Mark dirty to trigger metadata syncing.
+            values.put(RawContacts.METADATA_DIRTY, 1);
+        }
 
         mSelectionArgs1[0] = String.valueOf(contactId);
         db.update(Tables.RAW_CONTACTS, values, RawContacts.CONTACT_ID + "=?"
@@ -4617,7 +4758,8 @@
         return rslt;
     }
 
-    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
+    private int updateAggregationException(SQLiteDatabase db, ContentValues values,
+            boolean callerIsMetadataSyncAdapter) {
         Integer exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
         Long rcId1 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID1);
         Long rcId2 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID2);
@@ -4656,17 +4798,185 @@
 
         aggregator.aggregateContact(mTransactionContext.get(), db, rawContactId1);
         aggregator.aggregateContact(mTransactionContext.get(), db, rawContactId2);
+        mTransactionContext.get().markRawContactMetadataDirty(rawContactId1,
+                callerIsMetadataSyncAdapter);
+        mTransactionContext.get().markRawContactMetadataDirty(rawContactId2,
+                callerIsMetadataSyncAdapter);
 
         // The return value is fake - we just confirm that we made a change, not count actual
         // rows changed.
         return 1;
     }
 
+    private boolean shouldMarkMetadataDirtyForRawContact(ContentValues values) {
+        return (flagExists(values, RawContacts.STARRED) || flagExists(values, RawContacts.PINNED)
+                || flagExists(values, RawContacts.SEND_TO_VOICEMAIL));
+    }
+
     @Override
     public void onAccountsUpdated(Account[] accounts) {
         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
     }
 
+    interface RawContactsBackupQuery {
+        String TABLE = Tables.RAW_CONTACTS;
+        String[] COLUMNS = new String[] {
+                RawContacts._ID,
+        };
+        int RAW_CONTACT_ID = 0;
+        String SELECTION = RawContacts.DELETED + "=0 AND " +
+                RawContacts.BACKUP_ID + "=? AND " +
+                RawContactsColumns.ACCOUNT_ID + "=?";
+    }
+
+    /**
+     * Fetch rawContactId related to the given backupId.
+     * Return 0 if there's no such rawContact or it's deleted.
+     */
+    private long queryRawContactId(SQLiteDatabase db, String backupId, long accountId) {
+        if (TextUtils.isEmpty(backupId)) {
+            return 0;
+        }
+        mSelectionArgs2[0] = backupId;
+        mSelectionArgs2[1] = String.valueOf(accountId);
+        long rawContactId = 0;
+        final Cursor cursor = db.query(RawContactsBackupQuery.TABLE,
+                RawContactsBackupQuery.COLUMNS, RawContactsBackupQuery.SELECTION,
+                mSelectionArgs2, null, null, null);
+        try {
+            if (cursor.moveToFirst()) {
+                rawContactId = cursor.getLong(RawContactsBackupQuery.RAW_CONTACT_ID);
+            }
+        } finally {
+            cursor.close();
+        }
+        return rawContactId;
+    }
+
+    interface DataHashQuery {
+        String TABLE = Tables.DATA;
+        String[] COLUMNS = new String[] {
+                Data._ID,
+        };
+        int DATA_ID = 0;
+        String SELECTION = Data.RAW_CONTACT_ID + "=? AND " + Data.HASH_ID + "=?";
+    }
+
+    /**
+     * Fetch a list of dataId related to the given hashId.
+     * Return empty list if there's no such data.
+     */
+    private ArrayList<Long> queryDataId(SQLiteDatabase db, long rawContactId, String hashId) {
+        if (rawContactId == 0 || TextUtils.isEmpty(hashId)) {
+            return new ArrayList<>();
+        }
+        mSelectionArgs2[0] = String.valueOf(rawContactId);
+        mSelectionArgs2[1] = hashId;
+        ArrayList<Long> result = new ArrayList<>();
+        long dataId = 0;
+        final Cursor c = db.query(DataHashQuery.TABLE, DataHashQuery.COLUMNS,
+                DataHashQuery.SELECTION, mSelectionArgs2, null, null, null);
+        try {
+            while (c.moveToNext()) {
+                dataId = c.getLong(DataHashQuery.DATA_ID);
+                result.add(dataId);
+            }
+        } finally {
+            c.close();
+        }
+        return result;
+    }
+
+    private long searchRawContactIdForRawContactInfo(SQLiteDatabase db,
+            RawContactInfo rawContactInfo) {
+        if (rawContactInfo == null) {
+            return 0;
+        }
+        final String backupId = rawContactInfo.mBackupId;
+        final String accountType = rawContactInfo.mAccountType;
+        final String accountName = rawContactInfo.mAccountName;
+        final String dataSet = rawContactInfo.mDataSet;
+        ContentValues values = new ContentValues();
+        values.put(AccountsColumns.ACCOUNT_TYPE, accountType);
+        values.put(AccountsColumns.ACCOUNT_NAME, accountName);
+        if (dataSet != null) {
+            values.put(AccountsColumns.DATA_SET, dataSet);
+        }
+
+        final long accountId = replaceAccountInfoByAccountId(RawContacts.CONTENT_URI, values);
+        final long rawContactId = queryRawContactId(db, backupId, accountId);
+        return rawContactId;
+    }
+
+    /**
+     * Update RawContact, Data, DataUsageStats, AggregationException tables from MetadataEntry.
+     */
+    @NeededForTesting
+    void updateFromMetaDataEntry(SQLiteDatabase db, MetadataEntry metadataEntry) {
+        final RawContactInfo rawContactInfo =  metadataEntry.mRawContactInfo;
+        final long rawContactId = searchRawContactIdForRawContactInfo(db, rawContactInfo);
+        if (rawContactId == 0) {
+            return;
+        }
+
+        ContentValues rawContactValues = new ContentValues();
+        rawContactValues.put(RawContacts.SEND_TO_VOICEMAIL, metadataEntry.mSendToVoicemail);
+        rawContactValues.put(RawContacts.STARRED, metadataEntry.mStarred);
+        rawContactValues.put(RawContacts.PINNED, metadataEntry.mPinned);
+        updateRawContact(db, rawContactId, rawContactValues, /* callerIsSyncAdapter =*/true,
+                /* callerIsMetadataSyncAdapter =*/true);
+
+        // Update Data and DataUsageStats table.
+        for (int i = 0; i < metadataEntry.mFieldDatas.size(); i++) {
+            final FieldData fieldData = metadataEntry.mFieldDatas.get(i);
+            final String dataHashId = fieldData.mDataHashId;
+            final ArrayList<Long> dataIds = queryDataId(db, rawContactId, dataHashId);
+
+            for (long dataId : dataIds) {
+                // Update is_primary and is_super_primary.
+                ContentValues dataValues = new ContentValues();
+                dataValues.put(Data.IS_PRIMARY, fieldData.mIsPrimary ? 1 : 0);
+                dataValues.put(Data.IS_SUPER_PRIMARY, fieldData.mIsSuperPrimary ? 1 : 0);
+                updateData(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
+                        dataValues, null, null, /* callerIsSyncAdapter =*/true,
+                        /* callerIsMetadataSyncAdapter =*/true);
+
+                // Update UsageStats.
+                for (int j = 0; j < fieldData.mUsageStatsList.size(); j++) {
+                    final UsageStats usageStats = fieldData.mUsageStatsList.get(j);
+                    final String usageType = usageStats.mUsageType;
+                    final int typeInt = getDataUsageFeedbackType(usageType.toLowerCase(), null);
+                    final long lastTimeUsed = usageStats.mLastTimeUsed;
+                    final int timesUsed = usageStats.mTimesUsed;
+                    ContentValues usageStatsValues = new ContentValues();
+                    usageStatsValues.put(DataUsageStatColumns.DATA_ID, dataId);
+                    usageStatsValues.put(DataUsageStatColumns.USAGE_TYPE_INT, typeInt);
+                    usageStatsValues.put(DataUsageStatColumns.LAST_TIME_USED, lastTimeUsed);
+                    usageStatsValues.put(DataUsageStatColumns.TIMES_USED, timesUsed);
+                    updateDataUsageStats(db, usageStatsValues);
+                }
+            }
+        }
+
+        // Update AggregationException table.
+        for (int i = 0; i < metadataEntry.mAggregationDatas.size(); i++) {
+            final AggregationData aggregationData = metadataEntry.mAggregationDatas.get(i);
+            final int typeInt = getAggregationType(aggregationData.mType, null);
+            final RawContactInfo aggregationContact1 = aggregationData.mRawContactInfo1;
+            final RawContactInfo aggregationContact2 = aggregationData.mRawContactInfo2;
+            final long rawContactId1 = searchRawContactIdForRawContactInfo(db, aggregationContact1);
+            final long rawContactId2 = searchRawContactIdForRawContactInfo(db, aggregationContact2);
+            if (rawContactId1 == 0 || rawContactId2 == 0) {
+                continue;
+            }
+            ContentValues values = new ContentValues();
+            values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
+            values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
+            values.put(AggregationExceptions.TYPE, typeInt);
+            updateAggregationException(db, values, /* callerIsMetadataSyncAdapter =*/true);
+        }
+    }
+
     /** return serialized version of {@code accounts} */
     @VisibleForTesting
     static String accountsToString(Set<Account> accounts) {
@@ -7891,7 +8201,7 @@
      * @return A string containing a non-negative integer, or <code>null</code> if
      *         the parameter is not set, or is set to an invalid value.
      */
-    private String getLimit(Uri uri) {
+     static String getLimit(Uri uri) {
         String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY);
         if (limitParam == null) {
             return null;
@@ -8994,13 +9304,28 @@
 
         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
 
+        final Set<Long> rawContactIds = new HashSet<>();
+        final Cursor cursor = db.rawQuery(rawContactIdSelect.toString(), null);
+        try {
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                final long rid =   cursor.getLong(0);
+                mTransactionContext.get().markRawContactMetadataDirty(rid,
+                        /* isMetadataSyncAdapter =*/false);
+                rawContactIds.add(rid);
+            }
+        } finally {
+            cursor.close();
+        }
+
         mSelectionArgs1[0] = String.valueOf(currentTimeMillis);
+        final String rids = TextUtils.join(",", rawContactIds);
 
         db.execSQL("UPDATE " + Tables.RAW_CONTACTS +
                 " SET " + RawContacts.LAST_TIME_CONTACTED + "=?" +
                 "," + RawContacts.TIMES_CONTACTED + "=" +
                     "ifnull(" + RawContacts.TIMES_CONTACTED + ",0) + 1" +
-                " WHERE " + RawContacts._ID + " IN (" + rawContactIdSelect.toString() + ")"
+                " WHERE " + RawContacts._ID + " IN (" + rids + ")"
                 , mSelectionArgs1);
         db.execSQL("UPDATE " + Tables.CONTACTS +
                 " SET " + Contacts.LAST_TIME_CONTACTED + "=?1" +
@@ -9009,7 +9334,7 @@
                 "," + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=?1" +
                 " WHERE " + Contacts._ID + " IN (SELECT " + RawContacts.CONTACT_ID +
                     " FROM " + Tables.RAW_CONTACTS +
-                    " WHERE " + RawContacts._ID + " IN (" + rawContactIdSelect.toString() + "))"
+                    " WHERE " + RawContacts._ID + " IN (" + rids + "))"
                 , mSelectionArgs1);
 
         return successful;
@@ -9079,6 +9404,52 @@
     }
 
     /**
+     * Update {@link Tables#DATA_USAGE_STAT}.
+     * Update or insert usageType, lastTimeUsed, and timesUsed for specific dataId.
+     */
+    private void updateDataUsageStats(SQLiteDatabase db, ContentValues values) {
+        final String dataId = values.getAsString(DataUsageStatColumns.DATA_ID);
+        final String type = values.getAsString(DataUsageStatColumns.USAGE_TYPE_INT);
+        final String lastTimeUsed = values.getAsString(DataUsageStatColumns.LAST_TIME_USED);
+        final String timesUsed = values.getAsString(DataUsageStatColumns.TIMES_USED);
+
+        mSelectionArgs2[0] = dataId;
+        mSelectionArgs2[1] = type;
+        final Cursor cursor = db.query(DataUsageStatQuery.TABLE,
+                DataUsageStatQuery.COLUMNS, DataUsageStatQuery.SELECTION,
+                mSelectionArgs2, null, null, null);
+
+        try {
+            if (cursor.moveToFirst()) {
+                final long id = cursor.getLong(DataUsageStatQuery.ID);
+
+                mSelectionArgs3[0] = lastTimeUsed;
+                mSelectionArgs3[1] = timesUsed;
+                mSelectionArgs3[2] = String.valueOf(id);
+                db.execSQL("UPDATE " + Tables.DATA_USAGE_STAT +
+                        " SET " + DataUsageStatColumns.LAST_TIME_USED + "=?" +
+                        "," + DataUsageStatColumns.TIMES_USED + "=?" +
+                        " WHERE " + DataUsageStatColumns._ID + "=?",
+                        mSelectionArgs3);
+            } else {
+                mSelectionArgs4[0] = dataId;
+                mSelectionArgs4[1] = type;
+                mSelectionArgs4[2] = timesUsed;
+                mSelectionArgs4[3] = lastTimeUsed;
+                db.execSQL("INSERT INTO " + Tables.DATA_USAGE_STAT +
+                        "(" + DataUsageStatColumns.DATA_ID +
+                        "," + DataUsageStatColumns.USAGE_TYPE_INT +
+                        "," + DataUsageStatColumns.TIMES_USED +
+                        "," + DataUsageStatColumns.LAST_TIME_USED +
+                        ") VALUES (?,?,?,?)",
+                        mSelectionArgs4);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
      * Returns a sort order String for promoting data rows (email addresses, phone numbers, etc.)
      * associated with a primary account. The primary account should be supplied from applications
      * with {@link ContactsContract#PRIMARY_ACCOUNT_NAME} and
@@ -9252,6 +9623,22 @@
         throw new IllegalArgumentException("Invalid usage type " + type);
     }
 
+    private static final int getAggregationType(String type, Integer defaultType) {
+        if ("TOGETHER".equalsIgnoreCase(type)) {
+            return AggregationExceptions.TYPE_KEEP_TOGETHER; // 1
+        }
+        if ("SEPARATE".equalsIgnoreCase(type)) {
+            return AggregationExceptions.TYPE_KEEP_SEPARATE; // 2
+        }
+        if ("UNSET".equalsIgnoreCase(type)) {
+            return AggregationExceptions.TYPE_AUTOMATIC; // 0
+        }
+        if (defaultType != null) {
+            return defaultType;
+        }
+        throw new IllegalArgumentException("Invalid aggregation type " + type);
+    }
+
     /** Use only for debug logging */
     @Override
     public String toString() {
diff --git a/src/com/android/providers/contacts/DataRowHandler.java b/src/com/android/providers/contacts/DataRowHandler.java
index dbe8cbc..0c338a6 100644
--- a/src/com/android/providers/contacts/DataRowHandler.java
+++ b/src/com/android/providers/contacts/DataRowHandler.java
@@ -37,6 +37,9 @@
  */
 public abstract class DataRowHandler {
 
+    private static final String[] HASH_INPUT_COLUMNS = new String[] {
+            Data.DATA1, Data.DATA2, Data.DATA15 };
+
     public interface DataDeleteQuery {
         public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
 
@@ -107,6 +110,9 @@
      */
     public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId,
             ContentValues values) {
+        // Generate hash_id from data1 and data2 columns.
+        // For photo, use data15 column instead of data1 and data2 to generate hash_id.
+        handleHashIdForInsert(values);
         final long dataId = db.insert(Tables.DATA, null, values);
 
         final Integer primary = values.getAsInteger(Data.IS_PRIMARY);
@@ -114,6 +120,7 @@
         if ((primary != null && primary != 0) || (superPrimary != null && superPrimary != 0)) {
             final long mimeTypeId = getMimeTypeId();
             mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
+            txContext.markRawContactMetadataDirty(rawContactId, /* isMetadataSyncAdapter =*/false);
 
             // We also have to make sure that no other data item on this raw_contact is
             // configured super primary
@@ -146,11 +153,14 @@
      * @return true if update changed something
      */
     public boolean update(SQLiteDatabase db, TransactionContext txContext,
-            ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
+            ContentValues values, Cursor c, boolean callerIsSyncAdapter,
+            boolean callerIsMetadataSyncAdapter) {
         long dataId = c.getLong(DataUpdateQuery._ID);
         long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
 
-        handlePrimaryAndSuperPrimary(values, dataId, rawContactId);
+        handlePrimaryAndSuperPrimary(txContext, values, dataId, rawContactId,
+                callerIsMetadataSyncAdapter);
+        handleHashIdForUpdate(values, dataId);
 
         if (values.size() > 0) {
             mSelectionArgs1[0] = String.valueOf(dataId);
@@ -178,17 +188,70 @@
     }
 
     /**
+     * Fetch data1, data2, and data15 from values if they exist, and generate hash_id
+     * if one of data1 and data2 columns is set, otherwise using data15 instead.
+     * hash_id is null if all of these three field is null.
+     * Add hash_id key to values.
+     */
+    public void handleHashIdForInsert(ContentValues values) {
+        final String data1 = values.getAsString(Data.DATA1);
+        final String data2 = values.getAsString(Data.DATA2);
+        final byte[] data15 = values.getAsByteArray(Data.DATA15);
+        final String hashId = mDbHelper.generateHashId(data1, data2, data15);
+        if (TextUtils.isEmpty(hashId)) {
+            values.putNull(Data.HASH_ID);
+        } else {
+            values.put(Data.HASH_ID, hashId);
+        }
+    }
+
+    /**
+     * Compute hash_id column and add it to values.
+     * If both of data1 and data2 changed, using new values to compute hash_id.
+     * If one of data1 and data2 changed, read another one from DB and compute hash_id.
+     * If both data1 and data2 are null, use data15 to compute hash_id.
+     */
+    private void handleHashIdForUpdate(ContentValues values, long dataId) {
+        if (values.containsKey(Data.DATA1) || values.containsKey(Data.DATA2)
+                || values.containsKey(Data.DATA15)) {
+            String data1 = values.getAsString(Data.DATA1);
+            String data2 = values.getAsString(Data.DATA2);
+            byte[] data15 = values.getAsByteArray(Data.DATA15);
+            mSelectionArgs1[0] = String.valueOf(dataId);
+            final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA,
+                    HASH_INPUT_COLUMNS, Data._ID + "=?", mSelectionArgs1, null, null, null);
+            try {
+                if (c.moveToFirst()) {
+                    data1 = values.containsKey(Data.DATA1) ? data1 : c.getString(0);
+                    data2 = values.containsKey(Data.DATA2) ? data2 : c.getString(1);
+                    data15 = values.containsKey(Data.DATA15) ? data15 : c.getBlob(2);
+                }
+            } finally {
+                c.close();
+            }
+            final String hashId = mDbHelper.generateHashId(data1, data2, data15);
+            if (TextUtils.isEmpty(hashId)) {
+                values.putNull(Data.HASH_ID);
+            } else {
+                values.put(Data.HASH_ID, hashId);
+            }
+        }
+    }
+
+    /**
      * Ensures that all super-primary and primary flags of this raw_contact are
      * configured correctly
      */
-    private void handlePrimaryAndSuperPrimary(ContentValues values, long dataId,
-            long rawContactId) {
+    private void handlePrimaryAndSuperPrimary(TransactionContext txContext, ContentValues values,
+            long dataId, long rawContactId, boolean callerIsMetadataSyncAdapter) {
         final boolean hasPrimary = values.getAsInteger(Data.IS_PRIMARY) != null;
         final boolean hasSuperPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY) != null;
 
         // Nothing to do? Bail out early
         if (!hasPrimary && !hasSuperPrimary) return;
 
+        txContext.markRawContactMetadataDirty(rawContactId, callerIsMetadataSyncAdapter);
+
         final long mimeTypeId = getMimeTypeId();
 
         // Check if we want to clear values
@@ -254,6 +317,7 @@
         db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
         if (count != 0 && primary) {
             fixPrimary(db, rawContactId);
+            txContext.markRawContactMetadataDirty(rawContactId, /* isMetadataSyncAdapter =*/false);
         }
 
         if (hasSearchableData()) {
diff --git a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
index 063fcdb..5ae3a01 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
@@ -49,14 +49,15 @@
 
     @Override
     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
-            Cursor c, boolean callerIsSyncAdapter) {
+            Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
         final long dataId = c.getLong(DataUpdateQuery._ID);
         final ContentValues augmented = getAugmentedValues(db, dataId, values);
         if (augmented == null) {        // No change
             return false;
         }
         enforceTypeAndLabel(augmented);
-        return super.update(db, txContext, values, c, callerIsSyncAdapter);
+        return super.update(db, txContext, values, c, callerIsSyncAdapter,
+                callerIsMetadataSyncAdapter);
     }
 
     /**
diff --git a/src/com/android/providers/contacts/DataRowHandlerForEmail.java b/src/com/android/providers/contacts/DataRowHandlerForEmail.java
index 539c959..3c7311f 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForEmail.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForEmail.java
@@ -50,8 +50,8 @@
 
     @Override
     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
-            Cursor c, boolean callerIsSyncAdapter) {
-        if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+            Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
+        if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
             return false;
         }
 
diff --git a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
index e291986..4e9d5d4 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
@@ -85,11 +85,11 @@
 
     @Override
     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
-            Cursor c, boolean callerIsSyncAdapter) {
+            Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
         long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
         boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId);
         resolveGroupSourceIdInValues(txContext, rawContactId, db, values, false);
-        if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+        if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
             return false;
         }
         boolean isStarred = hasFavoritesGroupMembership(db, rawContactId);
diff --git a/src/com/android/providers/contacts/DataRowHandlerForIdentity.java b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java
index 32e9757..4d4a33f 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForIdentity.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java
@@ -46,9 +46,9 @@
 
     @Override
     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
-            Cursor c, boolean callerIsSyncAdapter) {
+            Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
 
-        super.update(db, txContext, values, c, callerIsSyncAdapter);
+        super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter);
 
         // Identity affects aggregation.
         final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
diff --git a/src/com/android/providers/contacts/DataRowHandlerForNickname.java b/src/com/android/providers/contacts/DataRowHandlerForNickname.java
index 03b96a3..cc85c2b 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForNickname.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForNickname.java
@@ -52,11 +52,11 @@
 
     @Override
     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
-            Cursor c, boolean callerIsSyncAdapter) {
+            Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
         long dataId = c.getLong(DataUpdateQuery._ID);
         long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
 
-        if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+        if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
             return false;
         }
 
diff --git a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
index 66a3b1b..5b69fe3 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
@@ -51,8 +51,8 @@
 
     @Override
     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
-            Cursor c, boolean callerIsSyncAdapter) {
-        if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+            Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
+        if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
             return false;
         }
 
diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
index 052252e..7bbac68 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
@@ -58,10 +58,10 @@
 
     @Override
     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
-            Cursor c, boolean callerIsSyncAdapter) {
+            Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
         fillNormalizedNumber(values);
 
-        if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+        if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
             return false;
         }
 
diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
index 532a852..3d28b05 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
@@ -76,7 +76,7 @@
 
     @Override
     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
-            Cursor c, boolean callerIsSyncAdapter) {
+            Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
         long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
 
         if (values.containsKey(SKIP_PROCESSING_KEY)) {
@@ -89,7 +89,7 @@
         }
 
         // Do the actual update.
-        if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
+        if (!super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter)) {
             return false;
         }
 
diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
index 044e972..b80f759 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
@@ -63,7 +63,7 @@
 
     @Override
     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
-            Cursor c, boolean callerIsSyncAdapter) {
+            Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
         final long dataId = c.getLong(DataUpdateQuery._ID);
         final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
 
@@ -74,7 +74,7 @@
 
         fixStructuredNameComponents(augmented, values);
 
-        super.update(db, txContext, values, c, callerIsSyncAdapter);
+        super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter);
         if (values.containsKey(StructuredName.DISPLAY_NAME)) {
             augmented.putAll(values);
             String name = augmented.getAsString(StructuredName.DISPLAY_NAME);
diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
index 7fc97b7..235bbd7 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
@@ -60,7 +60,7 @@
 
     @Override
     public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
-            Cursor c, boolean callerIsSyncAdapter) {
+            Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
         final long dataId = c.getLong(DataUpdateQuery._ID);
         final ContentValues augmented = getAugmentedValues(db, dataId, values);
         if (augmented == null) {    // No change
@@ -68,7 +68,7 @@
         }
 
         fixStructuredPostalComponents(augmented, values);
-        super.update(db, txContext, values, c, callerIsSyncAdapter);
+        super.update(db, txContext, values, c, callerIsSyncAdapter, callerIsMetadataSyncAdapter);
         return true;
     }
 
diff --git a/src/com/android/providers/contacts/MetadataEntryParser.java b/src/com/android/providers/contacts/MetadataEntryParser.java
new file mode 100644
index 0000000..b90f938
--- /dev/null
+++ b/src/com/android/providers/contacts/MetadataEntryParser.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2015 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.providers.contacts;
+
+import android.text.TextUtils;
+import com.android.providers.contacts.util.NeededForTesting;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+@NeededForTesting
+public class MetadataEntryParser {
+
+    private final static String UNIQUE_CONTACT_ID = "unique_contact_id";
+    private final static String ACCOUNT_TYPE = "account_type";
+    private final static String CUSTOM_ACCOUNT_TYPE = "custom_account_type";
+    private final static String ENUM_VALUE_FOR_GOOGLE_ACCOUNT = "GOOGLE_ACCOUNT";
+    private final static String GOOGLE_ACCOUNT_TYPE = "com.google";
+    private final static String ENUM_VALUE_FOR_CUSTOM_ACCOUNT = "CUSTOM_ACCOUNT";
+    private final static String ACCOUNT_NAME = "account_name";
+    private final static String DATA_SET = "data_set";
+    private final static String ENUM_FOR_PLUS_DATA_SET = "GOOGLE_PLUS";
+    private final static String ENUM_FOR_CUSTOM_DATA_SET = "CUSTOM";
+    private final static String PLUS_DATA_SET_TYPE = "plus";
+    private final static String CUSTOM_DATA_SET = "custom_data_set";
+    private final static String CONTACT_ID = "contact_id";
+    private final static String CONTACT_PREFS = "contact_prefs";
+    private final static String SEND_TO_VOICEMAIL = "send_to_voicemail";
+    private final static String STARRED = "starred";
+    private final static String PINNED = "pinned";
+    private final static String AGGREGATION_DATA = "aggregation_data";
+    private final static String CONTACT_IDS = "contact_ids";
+    private final static String TYPE = "type";
+    private final static String FIELD_DATA = "field_data";
+    private final static String FIELD_DATA_ID = "field_data_id";
+    private final static String FIELD_DATA_PREFS = "field_data_prefs";
+    private final static String IS_PRIMARY = "is_primary";
+    private final static String IS_SUPER_PRIMARY = "is_super_primary";
+    private final static String USAGE_STATS = "usage_stats";
+    private final static String USAGE_TYPE = "usage_type";
+    private final static String LAST_TIME_USED = "last_time_used";
+    private final static String USAGE_COUNT = "usage_count";
+
+    @NeededForTesting
+    public static class UsageStats {
+        final String mUsageType;
+        final long mLastTimeUsed;
+        final int mTimesUsed;
+
+        @NeededForTesting
+        public UsageStats(String usageType, long lastTimeUsed, int timesUsed) {
+            this.mUsageType = usageType;
+            this.mLastTimeUsed = lastTimeUsed;
+            this.mTimesUsed = timesUsed;
+        }
+    }
+
+    @NeededForTesting
+    public static class FieldData {
+        final String mDataHashId;
+        final boolean mIsPrimary;
+        final boolean mIsSuperPrimary;
+        final ArrayList<UsageStats> mUsageStatsList;
+
+        @NeededForTesting
+        public FieldData(String dataHashId, boolean isPrimary, boolean isSuperPrimary,
+                ArrayList<UsageStats> usageStatsList) {
+            this.mDataHashId = dataHashId;
+            this.mIsPrimary = isPrimary;
+            this.mIsSuperPrimary = isSuperPrimary;
+            this.mUsageStatsList = usageStatsList;
+        }
+    }
+
+    @NeededForTesting
+    public static class RawContactInfo {
+        final String mBackupId;
+        final String mAccountType;
+        final String mAccountName;
+        final String mDataSet;
+
+        @NeededForTesting
+        public RawContactInfo(String backupId, String accountType, String accountName,
+                String dataSet) {
+            this.mBackupId = backupId;
+            this.mAccountType = accountType;
+            this.mAccountName = accountName;
+            mDataSet = dataSet;
+        }
+    }
+
+    @NeededForTesting
+    public static class AggregationData {
+        final RawContactInfo mRawContactInfo1;
+        final RawContactInfo mRawContactInfo2;
+        final String mType;
+
+        @NeededForTesting
+        public AggregationData(RawContactInfo rawContactInfo1, RawContactInfo rawContactInfo2,
+                String type) {
+            this.mRawContactInfo1 = rawContactInfo1;
+            this.mRawContactInfo2 = rawContactInfo2;
+            this.mType = type;
+        }
+    }
+
+    @NeededForTesting
+    public static class MetadataEntry {
+        final RawContactInfo mRawContactInfo;
+        final int mSendToVoicemail;
+        final int mStarred;
+        final int mPinned;
+        final ArrayList<FieldData> mFieldDatas;
+        final ArrayList<AggregationData> mAggregationDatas;
+
+        @NeededForTesting
+        public MetadataEntry(RawContactInfo rawContactInfo,
+                int sendToVoicemail, int starred, int pinned,
+                ArrayList<FieldData> fieldDatas,
+                ArrayList<AggregationData> aggregationDatas) {
+            this.mRawContactInfo = rawContactInfo;
+            this.mSendToVoicemail = sendToVoicemail;
+            this.mStarred = starred;
+            this.mPinned = pinned;
+            this.mFieldDatas = fieldDatas;
+            this.mAggregationDatas = aggregationDatas;
+        }
+    }
+
+    @NeededForTesting
+    static MetadataEntry parseDataToMetaDataEntry(String inputData) {
+        if (TextUtils.isEmpty(inputData)) {
+            throw new IllegalArgumentException("Input cannot be empty.");
+        }
+
+        try {
+            final JSONObject root = new JSONObject(inputData);
+            // Parse to get rawContactId and account info.
+            final JSONObject uniqueContactJSON = root.getJSONObject(UNIQUE_CONTACT_ID);
+            final RawContactInfo rawContactInfo = parseUniqueContact(uniqueContactJSON);
+
+            // Parse contactPrefs to get sendToVoicemail, starred, pinned.
+            final JSONObject contactPrefs = root.getJSONObject(CONTACT_PREFS);
+            final boolean sendToVoicemail = contactPrefs.getBoolean(SEND_TO_VOICEMAIL);
+            final boolean starred = contactPrefs.getBoolean(STARRED);
+            final int pinned = contactPrefs.getInt(PINNED);
+
+            // Parse aggregationDatas
+            final ArrayList<AggregationData> aggregationsList = new ArrayList<AggregationData>();
+            if (root.has(AGGREGATION_DATA)) {
+                final JSONArray aggregationDatas = root.getJSONArray(AGGREGATION_DATA);
+
+                for (int i = 0; i < aggregationDatas.length(); i++) {
+                    final JSONObject aggregationData = aggregationDatas.getJSONObject(i);
+                    final JSONArray contacts = aggregationData.getJSONArray(CONTACT_IDS);
+
+                    if (contacts.length() != 2) {
+                        throw new IllegalArgumentException(
+                                "There should be two contacts for each aggregation.");
+                    }
+                    final JSONObject rawContact1 = contacts.getJSONObject(0);
+                    final RawContactInfo aggregationContact1 = parseUniqueContact(rawContact1);
+                    final JSONObject rawContact2 = contacts.getJSONObject(1);
+                    final RawContactInfo aggregationContact2 = parseUniqueContact(rawContact2);
+                    final String type = aggregationData.getString(TYPE);
+                    if (TextUtils.isEmpty(type)) {
+                        throw new IllegalArgumentException("Aggregation type cannot be empty.");
+                    }
+
+                    final AggregationData aggregation = new AggregationData(
+                            aggregationContact1, aggregationContact2, type);
+                    aggregationsList.add(aggregation);
+                }
+            }
+
+            // Parse fieldDatas
+            final ArrayList<FieldData> fieldDatasList = new ArrayList<FieldData>();
+            if (root.has(FIELD_DATA)) {
+                final JSONArray fieldDatas = root.getJSONArray(FIELD_DATA);
+
+                for (int i = 0; i < fieldDatas.length(); i++) {
+                    final JSONObject fieldData = fieldDatas.getJSONObject(i);
+                    final String dataHashId = fieldData.getString(FIELD_DATA_ID);
+                    if (TextUtils.isEmpty(dataHashId)) {
+                        throw new IllegalArgumentException("Field data hash id cannot be empty.");
+                    }
+                    final JSONObject fieldDataPrefs = fieldData.getJSONObject(FIELD_DATA_PREFS);
+                    final boolean isPrimary = fieldDataPrefs.getBoolean(IS_PRIMARY);
+                    final boolean isSuperPrimary = fieldDataPrefs.getBoolean(IS_SUPER_PRIMARY);
+
+                    final ArrayList<UsageStats> usageStatsList = new ArrayList<UsageStats>();
+                    if (fieldData.has(USAGE_STATS)) {
+                        final JSONArray usageStats = fieldData.getJSONArray(USAGE_STATS);
+                        for (int j = 0; j < usageStats.length(); j++) {
+                            final JSONObject usageStat = usageStats.getJSONObject(j);
+                            final String usageType = usageStat.getString(USAGE_TYPE);
+                            if (TextUtils.isEmpty(usageType)) {
+                                throw new IllegalArgumentException("Usage type cannot be empty.");
+                            }
+                            final long lastTimeUsed = usageStat.getLong(LAST_TIME_USED);
+                            final int usageCount = usageStat.getInt(USAGE_COUNT);
+
+                            final UsageStats usageStatsParsed = new UsageStats(
+                                    usageType, lastTimeUsed, usageCount);
+                            usageStatsList.add(usageStatsParsed);
+                        }
+                    }
+
+                    final FieldData fieldDataParse = new FieldData(dataHashId, isPrimary,
+                            isSuperPrimary, usageStatsList);
+                    fieldDatasList.add(fieldDataParse);
+                }
+            }
+            final MetadataEntry metaDataEntry = new MetadataEntry(rawContactInfo,
+                    sendToVoicemail ? 1 : 0, starred ? 1 : 0, pinned,
+                    fieldDatasList, aggregationsList);
+            return metaDataEntry;
+        } catch (JSONException e) {
+            throw new IllegalArgumentException("JSON Exception.", e);
+        }
+    }
+
+    private static RawContactInfo parseUniqueContact(JSONObject uniqueContactJSON) {
+        try {
+            final String backupId = uniqueContactJSON.getString(CONTACT_ID);
+            final String accountName = uniqueContactJSON.getString(ACCOUNT_NAME);
+            String accountType = uniqueContactJSON.getString(ACCOUNT_TYPE);
+            if (ENUM_VALUE_FOR_GOOGLE_ACCOUNT.equals(accountType)) {
+                accountType = GOOGLE_ACCOUNT_TYPE;
+            } else if (ENUM_VALUE_FOR_CUSTOM_ACCOUNT.equals(accountType)) {
+                accountType = uniqueContactJSON.getString(CUSTOM_ACCOUNT_TYPE);
+            } else {
+                throw new IllegalArgumentException("Unknown account type.");
+            }
+
+            String dataSet = null;
+            switch (uniqueContactJSON.getString(DATA_SET)) {
+                case ENUM_FOR_PLUS_DATA_SET:
+                    dataSet = PLUS_DATA_SET_TYPE;
+                    break;
+                case ENUM_FOR_CUSTOM_DATA_SET:
+                    dataSet = uniqueContactJSON.getString(CUSTOM_DATA_SET);
+                    break;
+            }
+            if (TextUtils.isEmpty(backupId) || TextUtils.isEmpty(accountType)
+                    || TextUtils.isEmpty(accountName)) {
+                throw new IllegalArgumentException(
+                        "Contact backup id, account type, account name cannot be empty.");
+            }
+            final RawContactInfo rawContactInfo = new RawContactInfo(
+                    backupId, accountType, accountName, dataSet);
+            return rawContactInfo;
+        } catch (JSONException e) {
+            throw new IllegalArgumentException("JSON Exception.", e);
+        }
+    }
+}
diff --git a/src/com/android/providers/contacts/ProfileProvider.java b/src/com/android/providers/contacts/ProfileProvider.java
index dfb8748..fe274a0 100644
--- a/src/com/android/providers/contacts/ProfileProvider.java
+++ b/src/com/android/providers/contacts/ProfileProvider.java
@@ -103,8 +103,8 @@
         mDelegate.notifyChange();
     }
 
-    protected void notifyChange(boolean syncToNetwork) {
-        mDelegate.notifyChange(syncToNetwork);
+    protected void notifyChange(boolean syncToNetwork, boolean syncToMetadataNetWork) {
+        mDelegate.notifyChange(syncToNetwork, syncToMetadataNetWork);
     }
 
     protected Locale getLocale() {
diff --git a/src/com/android/providers/contacts/TransactionContext.java b/src/com/android/providers/contacts/TransactionContext.java
index d8c93ef..fb23dfc 100644
--- a/src/com/android/providers/contacts/TransactionContext.java
+++ b/src/com/android/providers/contacts/TransactionContext.java
@@ -34,6 +34,7 @@
     /** Map from raw contact id to account Id */
     private HashMap<Long, Long> mInsertedRawContactsAccounts;
     private HashSet<Long> mUpdatedRawContacts;
+    private HashSet<Long> mMetadataDirtyRawContacts;
     private HashSet<Long> mDirtyRawContacts;
     // Set used to track what has been changed and deleted. This is needed so we can update the
     // contact last touch timestamp.  Dirty set above is only set when sync adapter is false.
@@ -75,6 +76,15 @@
         markRawContactChangedOrDeletedOrInserted(rawContactId);
     }
 
+    public void markRawContactMetadataDirty(long rawContactId, boolean isMetadataSyncAdapter) {
+        if (!isMetadataSyncAdapter) {
+            if (mMetadataDirtyRawContacts == null) {
+                mMetadataDirtyRawContacts = Sets.newHashSet();
+            }
+            mMetadataDirtyRawContacts.add(rawContactId);
+        }
+    }
+
     public void markRawContactChangedOrDeletedOrInserted(long rawContactId) {
         if (mChangedRawContacts == null) {
             mChangedRawContacts = Sets.newHashSet();
@@ -112,6 +122,11 @@
         return mDirtyRawContacts;
     }
 
+    public Set<Long> getMetadataDirtyRawContactIds() {
+        if (mMetadataDirtyRawContacts == null) mMetadataDirtyRawContacts = Sets.newHashSet();
+        return mMetadataDirtyRawContacts;
+    }
+
     public Set<Long> getChangedRawContactIds() {
         if (mChangedRawContacts == null) mChangedRawContacts = Sets.newHashSet();
         return mChangedRawContacts;
@@ -147,6 +162,7 @@
         mUpdatedRawContacts = null;
         mUpdatedSyncStates = null;
         mDirtyRawContacts = null;
+        mMetadataDirtyRawContacts = null;
         mChangedRawContacts = null;
     }
 
diff --git a/tests/assets/test1/testFileDeviceContactMetadataJSON.txt b/tests/assets/test1/testFileDeviceContactMetadataJSON.txt
new file mode 100644
index 0000000..65e624d
--- /dev/null
+++ b/tests/assets/test1/testFileDeviceContactMetadataJSON.txt
@@ -0,0 +1,69 @@
+{
+  "unique_contact_id": {
+    "account_type": "CUSTOM_ACCOUNT",
+    "custom_account_type": "facebook",
+    "account_name": "android-test",
+    "contact_id": "1111111",
+    "data_set": "FOCUS"
+  },
+  "contact_prefs": {
+    "send_to_voicemail": true,
+    "starred": false,
+    "pinned": 2
+  },
+  "aggregation_data": [
+    {
+      "type": "TOGETHER",
+      "contact_ids": [
+        {
+          "account_type": "GOOGLE_ACCOUNT",
+          "account_name": "android-test2",
+          "contact_id": "2222222",
+          "data_set": "GOOGLE_PLUS"
+        },
+        {
+          "account_type": "GOOGLE_ACCOUNT",
+          "account_name": "android-test3",
+          "contact_id": "3333333",
+          "data_set": "CUSTOM",
+          "custom_data_set": "custom type"
+        }
+      ]
+    }
+  ],
+  "field_data": [
+    {
+      "field_data_id": "1001",
+      "field_data_prefs": {
+        "is_primary": true,
+        "is_super_primary": true
+      },
+      "usage_stats": [
+        {
+          "usage_type": "CALL",
+          "last_time_used": 10000001,
+          "usage_count": 10
+        },
+        {
+          "usage_type": "SHORT_TEXT",
+          "last_time_used": 20000002,
+          "usage_count": 20
+        }
+      ]
+    },
+    {
+      "field_data_id": "1002",
+      "field_data_prefs": {
+        "is_primary": false,
+        "is_super_primary": false
+      },
+      "usage_stats": [
+        {
+          "usage_type": "LONG_TEXT",
+          "last_time_used": 30000003,
+          "usage_count": 30
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 6e389b7..9233361 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -55,7 +55,7 @@
 import android.test.MoreAsserts;
 import android.test.mock.MockContentResolver;
 import android.util.Log;
-
+import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 import com.android.providers.contacts.testutil.CommonDatabaseUtils;
 import com.android.providers.contacts.testutil.DataUtil;
@@ -133,6 +133,7 @@
         mActor.addPermissions(
                 "android.permission.READ_CONTACTS",
                 "android.permission.WRITE_CONTACTS",
+                "android.permission.READ_WRITE_CONTACT_METADATA",
                 "android.permission.READ_SOCIAL_STREAM",
                 "android.permission.WRITE_SOCIAL_STREAM");
     }
@@ -557,6 +558,18 @@
                 " WHERE " + BaseColumns._ID + "=" + contactId);
     }
 
+    protected long createAccount(String accountName, String accountType, String dataSet) {
+        // There's no api for this, so we just tweak the DB directly.
+        SQLiteDatabase db = ((ContactsProvider2) getProvider()).getDatabaseHelper()
+                .getWritableDatabase();
+
+        ContentValues values = new ContentValues();
+        values.put(AccountsColumns.ACCOUNT_NAME, accountName);
+        values.put(AccountsColumns.ACCOUNT_TYPE, accountType);
+        values.put(AccountsColumns.DATA_SET, dataSet);
+        return db.insert(Tables.ACCOUNTS, null, values);
+    }
+
     protected Cursor queryRawContact(long rawContactId) {
         return mResolver.query(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
                 null, null, null, null);
@@ -817,6 +830,14 @@
         c.close();
     }
 
+    protected void assertMetadataDirty(Uri uri, boolean state) {
+        Cursor c = mResolver.query(uri, new String[]{"metadata_dirty"}, null, null, null);
+        assertTrue(c.moveToNext());
+        assertEquals(state, c.getLong(0) != 0);
+        assertFalse(c.moveToNext());
+        c.close();
+    }
+
     protected long getVersion(Uri uri) {
         Cursor c = mResolver.query(uri, new String[]{"version"}, null, null, null);
         assertTrue(c.moveToNext());
@@ -832,6 +853,12 @@
         mResolver.update(uri, values, null, null);
     }
 
+    protected void clearMetadataDirty(Uri uri) {
+        ContentValues values = new ContentValues();
+        values.put("metadata_dirty", 0);
+        mResolver.update(uri, values, null, null);
+    }
+
     protected void storeValue(Uri contentUri, long id, String column, String value) {
         storeValue(ContentUris.withAppendedId(contentUri, id), column, value);
     }
@@ -1290,6 +1317,10 @@
         assertEquals(expected, (getContactsProvider()).isNetworkNotified());
     }
 
+    protected void assertMetadataNetworkNotified(boolean expected) {
+        assertEquals(expected, (getContactsProvider()).isMetadataNetworkNotified());
+    }
+
     protected void assertProjection(Uri uri, String[] expectedProjection) {
         Cursor cursor = mResolver.query(uri, null, "0", null, null);
         String[] actualProjection = cursor.getColumnNames();
diff --git a/tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java b/tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java
new file mode 100644
index 0000000..e48e2a6
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2015 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.providers.contacts;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.MetadataSync;
+import android.provider.ContactsContract.RawContacts;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.MediumTest;
+import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncColumns;
+import com.android.providers.contacts.testutil.RawContactUtil;
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link com.android.providers.contacts.ContactMetadataProvider}.
+ * <p/>
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -e class com.android.providers.contacts.ContactMetadataProviderTest -w \
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@MediumTest
+public class ContactMetadataProviderTest extends BaseContactsProvider2Test {
+    private static String TEST_ACCOUNT_TYPE1 = "test_account_type1";
+    private static String TEST_ACCOUNT_NAME1 = "test_account_name1";
+    private static String TEST_DATA_SET1 = "plus";
+    private static String TEST_BACKUP_ID1 = "1001";
+    private static String TEST_DATA1 = "{\n" +
+            "  \"unique_contact_id\": {\n" +
+            "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+            "    \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+            "    \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+            "    \"contact_id\": " + TEST_BACKUP_ID1 + ",\n" +
+            "    \"data_set\": \"GOOGLE_PLUS\"\n" +
+            "  },\n" +
+            "  \"contact_prefs\": {\n" +
+            "    \"send_to_voicemail\": true,\n" +
+            "    \"starred\": true,\n" +
+            "    \"pinned\": 2\n" +
+            "  }\n" +
+            "  }";
+
+    private static String TEST_ACCOUNT_TYPE2 = "test_account_type2";
+    private static String TEST_ACCOUNT_NAME2 = "test_account_name2";
+    private static String TEST_DATA_SET2 = null;
+    private static String TEST_BACKUP_ID2 = "1002";
+    private static String TEST_DATA2 =  "{\n" +
+            "  \"unique_contact_id\": {\n" +
+            "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+            "    \"custom_account_type\": " + TEST_ACCOUNT_TYPE2 + ",\n" +
+            "    \"account_name\": " + TEST_ACCOUNT_NAME2 + ",\n" +
+            "    \"contact_id\": " + TEST_BACKUP_ID2 + ",\n" +
+            "    \"data_set\": \"GOOGLE_PLUS\"\n" +
+            "  },\n" +
+            "  \"contact_prefs\": {\n" +
+            "    \"send_to_voicemail\": true,\n" +
+            "    \"starred\": true,\n" +
+            "    \"pinned\": 2\n" +
+            "  }\n" +
+            "  }";
+
+    private static String SELECTION_BY_TEST_ACCOUNT1 = MetadataSync.ACCOUNT_NAME + "='" +
+            TEST_ACCOUNT_NAME1 + "' AND " + MetadataSync.ACCOUNT_TYPE + "='" + TEST_ACCOUNT_TYPE1 +
+            "' AND " + MetadataSync.DATA_SET + "='" + TEST_DATA_SET1 + "'";
+
+    private static String SELECTION_BY_TEST_ACCOUNT2 = MetadataSync.ACCOUNT_NAME + "='" +
+            TEST_ACCOUNT_NAME2 + "' AND " + MetadataSync.ACCOUNT_TYPE + "='" + TEST_ACCOUNT_TYPE2 +
+            "' AND " + MetadataSync.DATA_SET + "='" + TEST_DATA_SET2 + "'";
+
+    private ContactMetadataProvider mContactMetadataProvider;
+    private AccountWithDataSet mTestAccount;
+    private ContentValues defaultValues;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mContactMetadataProvider = (ContactMetadataProvider) addProvider(
+                ContactMetadataProvider.class, MetadataSync.METADATA_AUTHORITY);
+        // Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
+        // are using different dbHelpers.
+        mContactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
+                mActor.provider).getDatabaseHelper(getContext()));
+        setupData();
+    }
+
+    public void testInsertWithInvalidUri() {
+        try {
+            mResolver.insert(Uri.withAppendedPath(MetadataSync.METADATA_AUTHORITY_URI,
+                    "metadata"), getDefaultValues());
+            fail("the insert was expected to fail, but it succeeded");
+        } catch (IllegalArgumentException e) {
+            // this was expected
+        }
+    }
+
+    public void testUpdateWithInvalidUri() {
+        try {
+            mResolver.update(Uri.withAppendedPath(MetadataSync.METADATA_AUTHORITY_URI,
+                    "metadata"), getDefaultValues(), null, null);
+            fail("the update was expected to fail, but it succeeded");
+        } catch (IllegalArgumentException e) {
+            // this was expected
+        }
+    }
+
+    public void testGetMetadataByAccount() {
+        Cursor c = mResolver.query(MetadataSync.CONTENT_URI, null, SELECTION_BY_TEST_ACCOUNT1,
+                null, null);
+        assertEquals(1, c.getCount());
+
+        ContentValues expectedValues = defaultValues;
+        expectedValues.remove(MetadataSyncColumns.ACCOUNT_ID);
+        c.moveToFirst();
+        assertCursorValues(c, expectedValues);
+        c.close();
+    }
+
+    public void testFailOnInsertMetadataForSameAccountIdAndBackupId() {
+        // Insert a new metadata with same account and backupId as defaultValues should fail.
+        String newData = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+                "    \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+                "    \"contact_id\": " + TEST_BACKUP_ID1 + ",\n" +
+                "    \"data_set\": \"GOOGLE_PLUS\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": false,\n" +
+                "    \"starred\": false,\n" +
+                "    \"pinned\": 1\n" +
+                "  }\n" +
+                "  }";
+
+        ContentValues  newValues =  new ContentValues();
+        newValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        newValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        newValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+        newValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, TEST_BACKUP_ID1);
+        newValues.put(MetadataSync.DATA, newData);
+        newValues.put(MetadataSync.DELETED, 0);
+        try {
+            mResolver.insert(MetadataSync.CONTENT_URI, newValues);
+        } catch (Exception e) {
+            // Expected.
+        }
+    }
+
+    public void testInsertAndUpdateMetadataSync() {
+        // Create a raw contact with backupId.
+        String backupId = "backupId10001";
+        long rawContactId = RawContactUtil.createRawContactWithAccountDataSet(
+                mResolver, mTestAccount);
+        Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+        ContentValues values = new ContentValues();
+        values.put(RawContacts.BACKUP_ID, backupId);
+        assertEquals(1, mResolver.update(rawContactUri, values, null, null));
+
+        assertStoredValue(rawContactUri, RawContacts._ID, rawContactId);
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        assertStoredValue(rawContactUri, RawContacts.BACKUP_ID, backupId);
+        assertStoredValue(rawContactUri, RawContacts.DATA_SET, TEST_DATA_SET1);
+
+        String deleted = "0";
+        String insertJson = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+                "    \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+                "    \"contact_id\": " + backupId + ",\n" +
+                "    \"data_set\": \"GOOGLE_PLUS\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": true,\n" +
+                "    \"starred\": true,\n" +
+                "    \"pinned\": 2\n" +
+                "  }\n" +
+                "  }";
+
+        // Insert to MetadataSync table.
+        ContentValues insertedValues = new ContentValues();
+        insertedValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+        insertedValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        insertedValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        insertedValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+        insertedValues.put(MetadataSync.DATA, insertJson);
+        insertedValues.put(MetadataSync.DELETED, deleted);
+        Uri metadataUri = mResolver.insert(MetadataSync.CONTENT_URI, insertedValues);
+
+        long metadataId = ContentUris.parseId(metadataUri);
+        assertEquals(true, metadataId > 0);
+
+        // Check if RawContact table is updated  after inserting metadata.
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        assertStoredValue(rawContactUri, RawContacts.BACKUP_ID, backupId);
+        assertStoredValue(rawContactUri, RawContacts.DATA_SET, TEST_DATA_SET1);
+        assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "1");
+        assertStoredValue(rawContactUri, RawContacts.STARRED, "1");
+        assertStoredValue(rawContactUri, RawContacts.PINNED, "2");
+
+        // Update the MetadataSync table.
+        String updatedJson = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+                "    \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+                "    \"contact_id\": " + backupId + ",\n" +
+                "    \"data_set\": \"GOOGLE_PLUS\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": false,\n" +
+                "    \"starred\": false,\n" +
+                "    \"pinned\": 1\n" +
+                "  }\n" +
+                "  }";
+        ContentValues updatedValues = new ContentValues();
+        updatedValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+        updatedValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        updatedValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        updatedValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+        updatedValues.put(MetadataSync.DATA, updatedJson);
+        updatedValues.put(MetadataSync.DELETED, deleted);
+        assertEquals(1, mResolver.update(MetadataSync.CONTENT_URI, updatedValues, null, null));
+
+        // Check if the update is correct.
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        assertStoredValue(rawContactUri, RawContacts.DATA_SET, TEST_DATA_SET1);
+        assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "0");
+        assertStoredValue(rawContactUri, RawContacts.STARRED, "0");
+        assertStoredValue(rawContactUri, RawContacts.PINNED, "1");
+    }
+
+    public void testInsertMetadataWithoutUpdateTables() {
+        // If raw contact doesn't exist, don't update raw contact tables.
+        String backupId = "newBackupId";
+        String deleted = "0";
+        String insertJson = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+                "    \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+                "    \"contact_id\": " + backupId + ",\n" +
+                "    \"data_set\": \"GOOGLE_PLUS\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": true,\n" +
+                "    \"starred\": true,\n" +
+                "    \"pinned\": 2\n" +
+                "  }\n" +
+                "  }";
+
+        // Insert to MetadataSync table.
+        ContentValues insertedValues = new ContentValues();
+        insertedValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+        insertedValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        insertedValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        insertedValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+        insertedValues.put(MetadataSync.DATA, insertJson);
+        insertedValues.put(MetadataSync.DELETED, deleted);
+        Uri metadataUri = mResolver.insert(MetadataSync.CONTENT_URI, insertedValues);
+
+        long metadataId = ContentUris.parseId(metadataUri);
+        assertEquals(true, metadataId > 0);
+    }
+
+    public void testFailUpdateDeletedMetadata() {
+        String backupId = "backupId001";
+        String newData = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+                "    \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+                "    \"contact_id\": " + backupId + ",\n" +
+                "    \"data_set\": \"GOOGLE_PLUS\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": false,\n" +
+                "    \"starred\": false,\n" +
+                "    \"pinned\": 1\n" +
+                "  }\n" +
+                "  }";
+
+        ContentValues  newValues =  new ContentValues();
+        newValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        newValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        newValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+        newValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+        newValues.put(MetadataSync.DATA, newData);
+        newValues.put(MetadataSync.DELETED, 1);
+
+        try {
+            mResolver.insert(MetadataSync.CONTENT_URI, newValues);
+            fail("the update was expected to fail, but it succeeded");
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
+    public void testInsertWithNullData() {
+        ContentValues  newValues =  new ContentValues();
+        String data = null;
+        String backupId = "backupId002";
+        newValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        newValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        newValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+        newValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+        newValues.put(MetadataSync.DATA, data);
+        newValues.put(MetadataSync.DELETED, 0);
+
+        try {
+            mResolver.insert(MetadataSync.CONTENT_URI, newValues);
+        } catch (IllegalArgumentException e) {
+            // Expected.
+        }
+    }
+
+    public void testUpdateWithNullData() {
+        ContentValues  newValues =  new ContentValues();
+        String data = null;
+        newValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        newValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        newValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+        newValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, TEST_BACKUP_ID1);
+        newValues.put(MetadataSync.DATA, data);
+        newValues.put(MetadataSync.DELETED, 0);
+
+        try {
+            mResolver.update(MetadataSync.CONTENT_URI, newValues, null, null);
+        } catch (IllegalArgumentException e) {
+            // Expected.
+        }
+    }
+
+    public void testUpdateForMetadataSyncId() {
+        Cursor c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync._ID},
+                SELECTION_BY_TEST_ACCOUNT1, null, null);
+        assertEquals(1, c.getCount());
+        c.moveToNext();
+        long metadataSyncId = c.getLong(0);
+        c.close();
+
+        Uri metadataUri = ContentUris.withAppendedId(MetadataSync.CONTENT_URI, metadataSyncId);
+        ContentValues  newValues =  new ContentValues();
+        String newData = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+                "    \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+                "    \"contact_id\": " + TEST_BACKUP_ID1 + ",\n" +
+                "    \"data_set\": \"GOOGLE_PLUS\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": false,\n" +
+                "    \"starred\": false,\n" +
+                "    \"pinned\": 1\n" +
+                "  }\n" +
+                "  }";
+        newValues.put(MetadataSync.DATA, newData);
+        newValues.put(MetadataSync.DELETED, 0);
+        assertEquals(1, mResolver.update(metadataUri, newValues, null, null));
+        assertStoredValue(metadataUri, MetadataSync.DATA, newData);
+    }
+
+    public void testDeleteMetadata() {
+        //insert another metadata for TEST_ACCOUNT
+        insertMetadata(TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1, "2", TEST_DATA1, 0);
+        Cursor c = mResolver.query(MetadataSync.CONTENT_URI, null, SELECTION_BY_TEST_ACCOUNT1,
+                null, null);
+        assertEquals(2, c.getCount());
+        int numOfDeletion = mResolver.delete(MetadataSync.CONTENT_URI, SELECTION_BY_TEST_ACCOUNT1,
+                null);
+        assertEquals(2, numOfDeletion);
+        c = mResolver.query(MetadataSync.CONTENT_URI, null, SELECTION_BY_TEST_ACCOUNT1,
+                null, null);
+        assertEquals(0, c.getCount());
+    }
+
+    public void testBulkInsert() {
+        Cursor c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync._ID},
+                SELECTION_BY_TEST_ACCOUNT1, null, null);
+        assertEquals(1, c.getCount());
+
+        ContentValues values1 = getMetadataContentValues(
+                TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1, "123", TEST_DATA1, 0);
+        ContentValues values2 = getMetadataContentValues(
+                TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1, "456", TEST_DATA1, 0);
+        ContentValues[] values = new ContentValues[] {values1, values2};
+
+        mResolver.bulkInsert(MetadataSync.CONTENT_URI, values);
+        c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync._ID},
+                SELECTION_BY_TEST_ACCOUNT1, null, null);
+        assertEquals(3, c.getCount());
+    }
+
+    public void testBatchOperations() throws Exception {
+        // Two mentadata_sync entries in the beginning, one for TEST_ACCOUNT1 and another for
+        // TEST_ACCOUNT2
+        Cursor c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync._ID},
+                null, null, null);
+        assertEquals(2, c.getCount());
+
+        String updatedData = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+                "    \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+                "    \"contact_id\": " + TEST_BACKUP_ID1 + ",\n" +
+                "    \"data_set\": \"GOOGLE_PLUS\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": true,\n" +
+                "    \"starred\": false,\n" +
+                "    \"pinned\": 5\n" +
+                "  }\n" +
+                "  }";
+
+        String newBackupId = "2222";
+        String newData = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": " + TEST_ACCOUNT_TYPE1 + ",\n" +
+                "    \"account_name\": " + TEST_ACCOUNT_NAME1 + ",\n" +
+                "    \"contact_id\": " + newBackupId + ",\n" +
+                "    \"data_set\": \"GOOGLE_PLUS\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": true,\n" +
+                "    \"starred\": false,\n" +
+                "    \"pinned\": 5\n" +
+                "  }\n" +
+                "  }";
+
+        ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
+        ops.add(ContentProviderOperation.newUpdate(MetadataSync.CONTENT_URI)
+                .withSelection(SELECTION_BY_TEST_ACCOUNT1, null)
+                .withValue(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1)
+                .withValue(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1)
+                .withValue(MetadataSync.DATA_SET, TEST_DATA_SET1)
+                .withValue(MetadataSync.RAW_CONTACT_BACKUP_ID, TEST_BACKUP_ID1)
+                .withValue(MetadataSync.DATA, updatedData)
+                .withValue(MetadataSync.DELETED, 0)
+                .build());
+
+        ops.add(ContentProviderOperation.newInsert(MetadataSync.CONTENT_URI)
+                .withValue(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1)
+                .withValue(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1)
+                .withValue(MetadataSync.DATA_SET, TEST_DATA_SET1)
+                .withValue(MetadataSync.RAW_CONTACT_BACKUP_ID, newBackupId)
+                .withValue(MetadataSync.DATA, newData)
+                .withValue(MetadataSync.DELETED, 0)
+                .build());
+
+        ops.add(ContentProviderOperation.newDelete(MetadataSync.CONTENT_URI)
+                .withSelection(SELECTION_BY_TEST_ACCOUNT2, null)
+                .build());
+
+        // Batch three operations: update the metadata_entry of TEST_ACCOUNT1; insert one new
+        // metadata_entry for TEST_ACCOUNT1; delete metadata_entry of TEST_ACCOUNT2
+        mResolver.applyBatch(MetadataSync.METADATA_AUTHORITY, ops);
+
+        // After the batch operations, there should be two metadata_entry for TEST_ACCOUNT1 with
+        // new data value and no metadata_entry for TEST_ACCOUNT2.
+        c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync.DATA},
+                SELECTION_BY_TEST_ACCOUNT1, null, null);
+        assertEquals(2, c.getCount());
+        Set<String> actualData = new HashSet<>();
+        while (c.moveToNext()) {
+            actualData.add(c.getString(0));
+        }
+        c.close();
+        MoreAsserts.assertContentsInAnyOrder(actualData, updatedData, newData);
+
+        c = mResolver.query(MetadataSync.CONTENT_URI, new String[] {MetadataSync._ID},
+                SELECTION_BY_TEST_ACCOUNT2, null, null);
+        assertEquals(0, c.getCount());
+    }
+
+    private void setupData() {
+        mTestAccount = new AccountWithDataSet(TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1,
+                TEST_DATA_SET1);
+        long rawContactId1 = RawContactUtil.createRawContactWithAccountDataSet(
+                mResolver, mTestAccount);
+        createAccount(TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1);
+        insertMetadata(getDefaultValues());
+
+        // Insert another entry for another account
+        createAccount(TEST_ACCOUNT_NAME2, TEST_ACCOUNT_TYPE2, TEST_DATA_SET2);
+        insertMetadata(TEST_ACCOUNT_NAME2, TEST_ACCOUNT_TYPE2, TEST_DATA_SET2, TEST_BACKUP_ID2,
+                TEST_DATA2, 0);
+    }
+
+    private ContentValues getDefaultValues() {
+        defaultValues = new ContentValues();
+        defaultValues.put(MetadataSync.ACCOUNT_NAME, TEST_ACCOUNT_NAME1);
+        defaultValues.put(MetadataSync.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE1);
+        defaultValues.put(MetadataSync.DATA_SET, TEST_DATA_SET1);
+        defaultValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, TEST_BACKUP_ID1);
+        defaultValues.put(MetadataSync.DATA, TEST_DATA1);
+        defaultValues.put(MetadataSync.DELETED, 0);
+        return defaultValues;
+    }
+
+    private long insertMetadata(String accountName, String accountType, String dataSet,
+            String backupId, String data, int deleted) {
+        return insertMetadata(getMetadataContentValues(
+                accountName, accountType, dataSet, backupId, data, deleted));
+    }
+
+    private ContentValues getMetadataContentValues(String accountName, String accountType,
+            String dataSet, String backupId, String data, int deleted) {
+        ContentValues values = new ContentValues();
+        values.put(MetadataSync.ACCOUNT_NAME, accountName);
+        values.put(MetadataSync.ACCOUNT_TYPE, accountType);
+        values.put(MetadataSync.DATA_SET, dataSet);
+        values.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+        values.put(MetadataSync.DATA, data);
+        values.put(MetadataSync.DELETED, deleted);
+        return values;
+    }
+
+    private long insertMetadata(ContentValues values) {
+        return ContentUris.parseId(mResolver.insert(MetadataSync.CONTENT_URI, values));
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index 7072763..c9320a5 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -56,6 +56,7 @@
 import android.provider.ContactsContract.DisplayPhoto;
 import android.provider.ContactsContract.FullNameStyle;
 import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.MetadataSync;
 import android.provider.ContactsContract.PhoneLookup;
 import android.provider.ContactsContract.PhoneticNameStyle;
 import android.provider.ContactsContract.PinnedPositions;
@@ -84,6 +85,11 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.MetadataEntryParser.AggregationData;
+import com.android.providers.contacts.MetadataEntryParser.FieldData;
+import com.android.providers.contacts.MetadataEntryParser.MetadataEntry;
+import com.android.providers.contacts.MetadataEntryParser.RawContactInfo;
+import com.android.providers.contacts.MetadataEntryParser.UsageStats;
 import com.android.providers.contacts.testutil.CommonDatabaseUtils;
 import com.android.providers.contacts.testutil.ContactUtil;
 import com.android.providers.contacts.testutil.DataUtil;
@@ -312,6 +318,7 @@
                 RawContacts.VERSION,
                 RawContacts.RAW_CONTACT_IS_USER_PROFILE,
                 RawContacts.DIRTY,
+                RawContacts.METADATA_DIRTY,
                 RawContacts.DELETED,
                 RawContacts.DISPLAY_NAME_PRIMARY,
                 RawContacts.DISPLAY_NAME_ALTERNATIVE,
@@ -430,6 +437,7 @@
         assertProjection(Phone.CONTENT_FILTER_URI.buildUpon().appendPath("123").build(),
             new String[]{
                 Data._ID,
+                Data.HASH_ID,
                 Data.DATA_VERSION,
                 Data.IS_PRIMARY,
                 Data.IS_SUPER_PRIMARY,
@@ -1017,6 +1025,7 @@
         values.put(RawContacts.CONTACT_ID, contactId);
         assertStoredValues(dataUri, values);
 
+        values.remove(Photo.PHOTO);// Remove byte[] value.
         assertSelection(Data.CONTENT_URI, values, Data._ID, dataId);
 
         // Access the same data through the directory under RawContacts
@@ -1032,6 +1041,73 @@
         assertNetworkNotified(true);
     }
 
+    public void testDataInsertAndUpdate_WithHashId() {
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
+
+        // Insert a data.
+        ContentValues values = new ContentValues();
+        putDataValues(values, rawContactId);
+        Uri dataUri = mResolver.insert(Data.CONTENT_URI, values);
+
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+        final ContactsDatabaseHelper helper = cp.getDatabaseHelper(mContext);
+        String data1 = values.getAsString(Data.DATA1);
+        String data2 = values.getAsString(Data.DATA2);
+        String combineString = data1+data2;
+        String hashId = helper.generateHashIdForData(combineString.getBytes());
+        assertStoredValue(dataUri, Data.HASH_ID, hashId);
+
+        // Update the data with primary, and check if hash_id is not changed.
+        values.remove(Data.DATA1);
+        values.remove(Data.DATA2);
+        values.remove(Data.DATA15);
+        values.put(Data.IS_PRIMARY, "1");
+        mResolver.update(dataUri, values, null, null);
+        assertStoredValue(dataUri, Data.IS_PRIMARY, "1");
+        assertStoredValue(dataUri, Data.HASH_ID, hashId);
+
+        // Update the data with new data1.
+        values = new ContentValues();
+        putDataValues(values, rawContactId);
+        String newData1 = "Newone";
+        values.put(Data.DATA1, newData1);
+        mResolver.update(dataUri, values, null, null);
+        combineString = newData1+data2;
+        String newHashId = helper.generateHashIdForData(combineString.getBytes());
+        assertStoredValue(dataUri, Data.HASH_ID, newHashId);
+
+        // Update the data with a new Data2.
+        values.remove(Data.DATA1);
+        values.put(Data.DATA2, "Newtwo");
+        combineString = "NewoneNewtwo";
+        String testHashId = helper.generateHashIdForData(combineString.getBytes());
+        mResolver.update(dataUri, values, null, null);
+        assertStoredValue(dataUri, Data.HASH_ID, testHashId);
+
+        // Update the data with a new data1 + data2.
+        values.put(Data.DATA1, "one");
+        combineString = "oneNewtwo";
+        testHashId = helper.generateHashIdForData(combineString.getBytes());
+        mResolver.update(dataUri, values, null, null);
+        assertStoredValue(dataUri, Data.HASH_ID, testHashId);
+
+        // Update the data with null data1 and null data2.
+        values.putNull(Data.DATA1);
+        values.putNull(Data.DATA2);
+        byte[] data15 = values.getAsByteArray(Data.DATA15);
+        testHashId = helper.generateHashIdForData(data15);
+        mResolver.update(dataUri, values, null, null);
+        assertStoredValue(dataUri, Data.HASH_ID, testHashId);
+
+        // Insert a data with null data1, null data2 and null data15, should insert null hash_id.
+        putDataValues(values, rawContactId);
+        values.remove(Data.DATA1);
+        values.remove(Data.DATA2);
+        values.remove(Data.DATA15);
+        Uri dataUri2 = mResolver.insert(Data.CONTENT_URI, values);
+        assertStoredValue(dataUri2, Data.HASH_ID, null);
+    }
+
     public void testDataInsertPhoneNumberTooLongIsTrimmed() {
         long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
 
@@ -2733,6 +2809,241 @@
         assertStoredValuesOrderly(filterUri1, new ContentValues[] { v2, v1, v4, v3 });
     }
 
+    public void testUpdateFromMetadataEntry() {
+        String accountType1 = "accountType1";
+        String accountName1 = "accountName1";
+        String dataSet1 = "plus";
+        Account account1 = new Account(accountName1, accountType1);
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, account1);
+        Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+        // Add backup_id for the raw contact.
+        String backupId = "backupId100001";
+        ContentValues values = new ContentValues();
+        values.put(RawContacts.BACKUP_ID, backupId);
+        assertEquals(1, mResolver.update(rawContactUri, values, null, null));
+
+        String emailAddress = "address@email.com";
+        Uri dataUri = insertEmail(rawContactId, emailAddress);
+        String hashId = getStoredValue(dataUri, Data.HASH_ID);
+
+        // Another data that should not be updated.
+        String phoneNumber = "111-111-1111";
+        Uri dataUri2 = insertPhoneNumber(rawContactId, phoneNumber);
+
+        String accountType2 = "accountType2";
+        String accountName2 = "accountName2";
+        Account account2 = new Account(accountName2, accountType2);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, account2);
+        Uri rawContactUri2 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId2);
+        String backupId2 = "backupId100003";
+        ContentValues values2 = new ContentValues();
+        values2.put(RawContacts.BACKUP_ID, backupId2);
+        assertEquals(1, mResolver.update(rawContactUri2, values2, null, null));
+
+        String usageTypeString = "CALL";
+        int lastTimeUsed = 1111111;
+        int timesUsed = 5;
+        String aggregationTypeString = "SEPARATE";
+        int aggregationType = AggregationExceptions.TYPE_KEEP_SEPARATE;
+
+        RawContactInfo rawContactInfo = new RawContactInfo(
+                backupId, accountType1, accountName1, null);
+        UsageStats usageStats = new UsageStats(usageTypeString, lastTimeUsed, timesUsed);
+        ArrayList<UsageStats> usageStatsList = new ArrayList<>();
+        usageStatsList.add(usageStats);
+        FieldData fieldData = new FieldData(hashId, true, true, usageStatsList);
+        ArrayList<FieldData> fieldDataList = new ArrayList<>();
+        fieldDataList.add(fieldData);
+        ArrayList<AggregationData> aggregationDataList = new ArrayList<>();
+        MetadataEntry metadataEntry = new MetadataEntry(rawContactInfo,
+                1, 1, 1, fieldDataList, aggregationDataList);
+
+        ContactsProvider2 provider = (ContactsProvider2) getProvider();
+        final ContactsDatabaseHelper helper =
+                ((ContactsDatabaseHelper) provider.getDatabaseHelper());
+        SQLiteDatabase db = helper.getWritableDatabase();
+
+        // Before updating tables from MetadataEntry.
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, accountType1);
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, accountName1);
+        assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "0");
+        assertStoredValue(rawContactUri, RawContacts.STARRED, "0");
+        assertStoredValue(rawContactUri, RawContacts.PINNED, "0");
+        assertStoredValue(dataUri, Data.IS_PRIMARY, 0);
+        assertStoredValue(dataUri, Data.IS_SUPER_PRIMARY, 0);
+
+        // Update tables without aggregation first, since aggregator will affect pinned value.
+        provider.updateFromMetaDataEntry(db, metadataEntry);
+
+        // After updating tables from MetadataEntry.
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, accountType1);
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, accountName1);
+        assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "1");
+        assertStoredValue(rawContactUri, RawContacts.STARRED, "1");
+        assertStoredValue(rawContactUri, RawContacts.PINNED, "1");
+        assertStoredValue(dataUri, Data.IS_PRIMARY, 1);
+        assertStoredValue(dataUri, Data.IS_SUPER_PRIMARY, 1);
+        assertStoredValue(dataUri2, Data.IS_PRIMARY, 0);
+        assertStoredValue(dataUri2, Data.IS_SUPER_PRIMARY, 0);
+        final Uri dataUriWithUsageType = Data.CONTENT_URI.buildUpon().appendQueryParameter(
+                DataUsageFeedback.USAGE_TYPE, usageTypeString).build();
+        assertDataUsageCursorContains(dataUriWithUsageType, emailAddress, timesUsed, lastTimeUsed);
+
+        // Update AggregationException table.
+        RawContactInfo aggregationContact = new RawContactInfo(
+                backupId2, accountType2, accountName2, null);
+        AggregationData aggregationData = new AggregationData(
+                rawContactInfo, aggregationContact, aggregationTypeString);
+        aggregationDataList.add(aggregationData);
+        metadataEntry = new MetadataEntry(rawContactInfo,
+                1, 1, 1, fieldDataList, aggregationDataList);
+        provider.updateFromMetaDataEntry(db, metadataEntry);
+
+        // Check if AggregationException table is updated.
+        assertStoredValue(AggregationExceptions.CONTENT_URI, AggregationExceptions.RAW_CONTACT_ID1,
+                rawContactId);
+        assertStoredValue(AggregationExceptions.CONTENT_URI, AggregationExceptions.RAW_CONTACT_ID2,
+                rawContactId2);
+        assertStoredValue(AggregationExceptions.CONTENT_URI, AggregationExceptions.TYPE,
+                aggregationType);
+    }
+
+    public void testUpdateMetadataOnRawContactInsert() throws Exception {
+        ContactMetadataProvider contactMetadataProvider = (ContactMetadataProvider) addProvider(
+                ContactMetadataProvider.class, MetadataSync.METADATA_AUTHORITY);
+        // Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
+        // are using different dbHelpers.
+        contactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
+                mActor.provider).getDatabaseHelper(getContext()));
+        // Create an account first.
+        String backupId = "backupId001";
+        String accountType = "accountType";
+        String accountName = "accountName";
+        Account account = new Account(accountName, accountType);
+        createAccount(accountName, accountType, null);
+
+        // Insert a metadata to MetadataSync table.
+        String data = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": " + accountType + ",\n" +
+                "    \"account_name\": " + accountName + ",\n" +
+                "    \"contact_id\": " + backupId + ",\n" +
+                "    \"data_set\": \"FOCUS\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": true,\n" +
+                "    \"starred\": true,\n" +
+                "    \"pinned\": 1\n" +
+                "  }\n" +
+                "  }";
+
+        ContentValues insertedValues = new ContentValues();
+        insertedValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+        insertedValues.put(MetadataSync.ACCOUNT_TYPE, accountType);
+        insertedValues.put(MetadataSync.ACCOUNT_NAME, accountName);
+        insertedValues.put(MetadataSync.DATA, data);
+        mResolver.insert(MetadataSync.CONTENT_URI, insertedValues);
+
+        // Enable metadataSync flag.
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+        cp.setMetadataSyncForTest(true);
+        // Insert a raw contact.
+        long rawContactId = RawContactUtil.createRawContactWithBackupId(mResolver, backupId,
+                account);
+        Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+        // Check if the raw contact is updated.
+        assertStoredValue(rawContactUri, RawContacts._ID, rawContactId);
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, accountType);
+        assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, accountName);
+        assertStoredValue(rawContactUri, RawContacts.BACKUP_ID, backupId);
+        assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "1");
+        assertStoredValue(rawContactUri, RawContacts.STARRED, "1");
+        assertStoredValue(rawContactUri, RawContacts.PINNED, "1");
+    }
+
+    public void testDeleteMetadataOnRawContactDelete() throws Exception {
+        ContactMetadataProvider contactMetadataProvider = (ContactMetadataProvider) addProvider(
+                ContactMetadataProvider.class, MetadataSync.METADATA_AUTHORITY);
+        // Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
+        // are using different dbHelpers.
+        contactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
+                mActor.provider).getDatabaseHelper(getContext()));
+        // Enable metadataSync flag.
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+        cp.setMetadataSyncForTest(true);
+        // Create an account first.
+        String backupId = "backupId001";
+        String accountType = "accountType";
+        String accountName = "accountName";
+        Account account = new Account(accountName, accountType);
+        createAccount(accountName, accountType, null);
+
+        // Insert a raw contact.
+        long rawContactId = RawContactUtil.createRawContactWithBackupId(mResolver, backupId,
+                account);
+        Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+
+        // Insert a metadata to MetadataSync table.
+        String data = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": " + accountType + ",\n" +
+                "    \"account_name\": " + accountName + ",\n" +
+                "    \"contact_id\": " + backupId + ",\n" +
+                "    \"data_set\": \"FOCUS\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": true,\n" +
+                "    \"starred\": true,\n" +
+                "    \"pinned\": 1\n" +
+                "  }\n" +
+                "  }";
+
+        ContentValues insertedValues = new ContentValues();
+        insertedValues.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId);
+        insertedValues.put(MetadataSync.ACCOUNT_TYPE, accountType);
+        insertedValues.put(MetadataSync.ACCOUNT_NAME, accountName);
+        insertedValues.put(MetadataSync.DATA, data);
+        Uri metadataUri = mResolver.insert(MetadataSync.CONTENT_URI, insertedValues);
+
+        // Delete raw contact.
+        mResolver.delete(rawContactUri, null, null);
+        // Check if the metadata is deleted.
+        assertStoredValue(metadataUri, MetadataSync.DELETED, "1");
+        // check raw contact metadata_dirty column is not changed on raw contact deletion
+        assertMetadataDirty(rawContactUri, false);
+        // Notify metadata network on raw contact deletion
+        assertMetadataNetworkNotified(true);
+
+        // Add another rawcontact and metadata, and don't delete them.
+        // Insert a raw contact.
+        String backupId2 = "newBackupId";
+        long rawContactId2 = RawContactUtil.createRawContactWithBackupId(mResolver, backupId2,
+                account);
+        Uri rawContactUri2 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+
+        // Insert a metadata to MetadataSync table.
+        ContentValues insertedValues2 = new ContentValues();
+        insertedValues2.put(MetadataSync.RAW_CONTACT_BACKUP_ID, backupId2);
+        insertedValues2.put(MetadataSync.ACCOUNT_TYPE, accountType);
+        insertedValues2.put(MetadataSync.ACCOUNT_NAME, accountName);
+        insertedValues2.put(MetadataSync.DATA, data);
+        Uri metadataUri2 = mResolver.insert(MetadataSync.CONTENT_URI, insertedValues2);
+
+        // Update raw contact but not delete.
+        ContentValues values = new ContentValues();
+        values.put(RawContacts.STARRED, "1");
+        mResolver.update(rawContactUri2, values, null, null);
+
+        // Check if the metadata is not marked as deleted.
+        assertStoredValue(metadataUri2, MetadataSync.DELETED, "0");
+        // check raw contact metadata_dirty column is changed on raw contact update
+        assertMetadataDirty(rawContactUri2, true);
+        // Notify metadata network on raw contact update
+        assertMetadataNetworkNotified(true);
+    }
+
     public void testPostalsQuery() {
         long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Alice", "Nextore");
         Uri dataUri = insertPostalAddress(rawContactId, "1600 Amphiteatre Ave, Mountain View");
@@ -6516,6 +6827,107 @@
         assertEquals(version, getVersion(uri));
     }
 
+    public void testMarkAsMetadataDirtyForRawContactMetadataChange() {
+        // Enable metadataSync flag.
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+        cp.setMetadataSyncForTest(true);
+
+        long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
+        Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+
+        Uri uri = DataUtil.insertStructuredName(mResolver, rawContactId, "John", "Doe");
+        clearMetadataDirty(rawContactUri);
+
+        ContentValues values = new ContentValues();
+        values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+        mResolver.update(rawContactUri, values, null, null);
+        assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, 1);
+        assertMetadataDirty(rawContactUri, true);
+        assertMetadataNetworkNotified(true);
+    }
+
+    public void testMarkAsMetadataDirtyForAggregationExceptionChange() {
+        // Enable metadataSync flag.
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+        cp.setMetadataSyncForTest(true);
+
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, new Account("b", "b"));
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+
+        assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1),
+                true);
+        assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId2),
+                true);
+        assertMetadataNetworkNotified(true);
+    }
+
+    public void testMarkAsMetadataDirtyForUsageStatsChange() {
+        // Enable metadataSync flag.
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+        cp.setMetadataSyncForTest(true);
+
+        final long rid1 = RawContactUtil.createRawContactWithName(mResolver, "contact", "a");
+        final long did1a = ContentUris.parseId(insertEmail(rid1, "email_1_a@email.com"));
+        updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1a);
+
+        assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rid1),
+                true);
+        assertMetadataNetworkNotified(true);
+    }
+
+    public void testMarkAsMetadataDirtyForDataPrimarySettingInsert() {
+        // Enable metadataSync flag.
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+        cp.setMetadataSyncForTest(true);
+
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
+        Uri mailUri11 = insertEmail(rawContactId1, "test1@domain1.com", true, true);
+
+        assertStoredValue(mailUri11, Data.IS_PRIMARY, 1);
+        assertStoredValue(mailUri11, Data.IS_SUPER_PRIMARY, 1);
+        assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1),
+                true);
+        assertMetadataNetworkNotified(true);
+    }
+
+    public void testMarkAsMetadataDirtyForDataPrimarySettingUpdate() {
+        // Enable metadataSync flag.
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+        cp.setMetadataSyncForTest(true);
+
+        long rawContactId = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
+        Uri mailUri1 = insertEmail(rawContactId, "test1@domain1.com");
+
+        assertStoredValue(mailUri1, Data.IS_PRIMARY, 0);
+        assertStoredValue(mailUri1, Data.IS_SUPER_PRIMARY, 0);
+
+        ContentValues values = new ContentValues();
+        values.put(Data.IS_SUPER_PRIMARY, 1);
+        mResolver.update(mailUri1, values, null, null);
+
+        assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                true);
+        assertMetadataNetworkNotified(true);
+    }
+
+    public void testMarkAsMetadataDirtyForDataDelete() {
+        // Enable metadataSync flag.
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+        cp.setMetadataSyncForTest(true);
+
+        long rawContactId = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
+        Uri mailUri1 = insertEmail(rawContactId, "test1@domain1.com", true, true);
+
+        mResolver.delete(mailUri1, null, null);
+
+        assertMetadataDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                true);
+        assertMetadataNetworkNotified(true);
+    }
+
     public void testDeleteContactWithoutName() {
         Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, new ContentValues());
         long rawContactId = ContentUris.parseId(rawContactUri);
@@ -9103,7 +9515,7 @@
         values.put(Data.DATA12, "twelve");
         values.put(Data.DATA13, "thirteen");
         values.put(Data.DATA14, "fourteen");
-        values.put(Data.DATA15, "fifteen");
+        values.put(Data.DATA15, "fifteen".getBytes());
         values.put(Data.CARRIER_PRESENCE, Data.CARRIER_PRESENCE_VT_CAPABLE);
         values.put(Data.SYNC1, "sync1");
         values.put(Data.SYNC2, "sync2");
diff --git a/tests/src/com/android/providers/contacts/MetadataEntryParserTest.java b/tests/src/com/android/providers/contacts/MetadataEntryParserTest.java
new file mode 100644
index 0000000..be4df57
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/MetadataEntryParserTest.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2015 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.providers.contacts;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import com.android.providers.contacts.MetadataEntryParser.AggregationData;
+import com.android.providers.contacts.MetadataEntryParser.FieldData;
+import com.android.providers.contacts.MetadataEntryParser.MetadataEntry;
+import com.android.providers.contacts.MetadataEntryParser.RawContactInfo;
+import com.android.providers.contacts.MetadataEntryParser.UsageStats;
+import org.json.JSONException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Unit tests for {@link MetadataEntryParser}.
+ *
+ * Run the test like this:
+ * <code>
+ adb shell am instrument -e class com.android.providers.contacts.MetadataEntryParserTest -w \
+         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@SmallTest
+public class MetadataEntryParserTest extends AndroidTestCase {
+
+    public void testErrorForEmptyInput() {
+        try {
+            MetadataEntryParser.parseDataToMetaDataEntry("");
+        } catch (IllegalArgumentException e) {
+            // Expected.
+        }
+    }
+
+    public void testParseDataToMetadataEntry() throws IOException {
+        String contactBackupId = "1111111";
+        String accountType = "facebook";
+        String accountName = "android-test";
+        String dataSet = null;
+        int sendToVoicemail = 1;
+        int starred = 0;
+        int pinned = 2;
+        String dataHashId1 = "1001";
+        String usageType1_1 = "CALL";
+        long lastTimeUsed1_1 = 10000001;
+        int timesUsed1_1 = 10;
+        String usageType1_2 = "SHORT_TEXT";
+        long lastTimeUsed1_2 = 20000002;
+        int timesUsed1_2 = 20;
+        String dataHashId2 = "1002";
+        String usageType2 = "LONG_TEXT";
+        long lastTimeUsed2 = 30000003;
+        int timesUsed2 = 30;
+        String aggregationContactBackupId1 = "2222222";
+        String aggregationAccountType1 = "com.google";
+        String aggregationAccountName1 = "android-test2";
+        String aggregationDataSet1 = "plus";
+        String aggregationContactBackupId2 = "3333333";
+        String aggregationAccountType2 = "com.google";
+        String aggregationAccountName2 = "android-test3";
+        String aggregationDataSet2 = "custom type";
+        String type = "TOGETHER";
+        String inputFile = "test1/testFileDeviceContactMetadataJSON.txt";
+
+        RawContactInfo rawContactInfo = new RawContactInfo(
+                contactBackupId, accountType, accountName, dataSet);
+        RawContactInfo aggregationContact1 = new RawContactInfo(aggregationContactBackupId1,
+                aggregationAccountType1, aggregationAccountName1, aggregationDataSet1);
+        RawContactInfo aggregationContact2 = new RawContactInfo(aggregationContactBackupId2,
+                aggregationAccountType2, aggregationAccountName2, aggregationDataSet2);
+        AggregationData aggregationData = new AggregationData(
+                aggregationContact1, aggregationContact2, type);
+        ArrayList<AggregationData> aggregationDataList = new ArrayList<>();
+        aggregationDataList.add(aggregationData);
+
+        UsageStats usageStats1_1 = new UsageStats(usageType1_1, lastTimeUsed1_1, timesUsed1_1);
+        UsageStats usageStats1_2 = new UsageStats(usageType1_2, lastTimeUsed1_2, timesUsed1_2);
+        UsageStats usageStats2 = new UsageStats(usageType2, lastTimeUsed2, timesUsed2);
+
+        ArrayList<UsageStats> usageStats1List = new ArrayList<>();
+        usageStats1List.add(usageStats1_1);
+        usageStats1List.add(usageStats1_2);
+        FieldData fieldData1 = new FieldData(dataHashId1, true, true, usageStats1List);
+
+        ArrayList<UsageStats> usageStats2List = new ArrayList<>();
+        usageStats2List.add(usageStats2);
+        FieldData fieldData2 = new FieldData(dataHashId2, false, false, usageStats2List);
+
+        ArrayList<FieldData> fieldDataList = new ArrayList<>();
+        fieldDataList.add(fieldData1);
+        fieldDataList.add(fieldData2);
+
+        MetadataEntry expectedResult = new MetadataEntry(rawContactInfo,
+                sendToVoicemail, starred, pinned, fieldDataList, aggregationDataList);
+
+        String inputJson = readAssetAsString(inputFile);
+        MetadataEntry metadataEntry = MetadataEntryParser.parseDataToMetaDataEntry(
+                inputJson.toString());
+        assertMetaDataEntry(expectedResult, metadataEntry);
+    }
+
+    public void testErrorForMissingContactId() {
+        String input = "{\"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": \"facebook\",\n" +
+                "    \"account_name\": \"android-test\"\n" +
+                "  }}";
+        try {
+            MetadataEntryParser.parseDataToMetaDataEntry(input);
+        } catch (IllegalArgumentException e) {
+            // Expected.
+        }
+    }
+
+    public void testErrorForNullContactId() throws JSONException {
+        String input = "{\"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": \"facebook\",\n" +
+                "    \"account_name\": \"android-test\",\n" +
+                "    \"contact_id\": \"\"\n" +
+                "  }}";
+        try {
+            MetadataEntryParser.parseDataToMetaDataEntry(input);
+        } catch (IllegalArgumentException e) {
+            // Expected.
+        }
+    }
+
+    public void testErrorForNullAccountType() throws JSONException {
+        String input = "{\"unique_contact_id\": {\n" +
+                "    \"account_type\": \"\",\n" +
+                "    \"custom_account_type\": \"facebook\",\n" +
+                "    \"account_name\": \"android-test\",\n" +
+                "    \"contact_id\": \"\"\n" +
+                "  }}";
+        try {
+            MetadataEntryParser.parseDataToMetaDataEntry(input);
+        } catch (IllegalArgumentException e) {
+            // Expected.
+        }
+    }
+
+    public void testErrorForNullAccountName() throws JSONException {
+        String input = "{\"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": \"facebook\",\n" +
+                "    \"account_name\": \"\",\n" +
+                "    \"contact_id\": \"1111111\"\n" +
+                "  }}";
+        try {
+            MetadataEntryParser.parseDataToMetaDataEntry(input);
+        } catch (IllegalArgumentException e) {
+            // Expected.
+        }
+    }
+
+    public void testErrorForNullFieldDataId() throws JSONException {
+        String input = "{\"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": \"facebook\",\n" +
+                "    \"account_name\": \"android-test\",\n" +
+                "    \"contact_id\": \"1111111\"\n" +
+                "  },\n" +
+                "    \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": true,\n" +
+                "    \"starred\": false,\n" +
+                "    \"pinned\": 2\n" +
+                "  }," +
+                "    \"field_data\": [{\n" +
+                "      \"field_data_id\": \"\"}]" +
+                "}";
+        try {
+            MetadataEntryParser.parseDataToMetaDataEntry(input);
+        } catch (IllegalArgumentException e) {
+            // Expected.
+        }
+    }
+
+    public void testErrorForNullAggregationType() throws JSONException {
+        String input = "{\n" +
+                "  \"unique_contact_id\": {\n" +
+                "    \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+                "    \"custom_account_type\": \"facebook\",\n" +
+                "    \"account_name\": \"android-test\",\n" +
+                "    \"contact_id\": \"1111111\"\n" +
+                "  },\n" +
+                "  \"contact_prefs\": {\n" +
+                "    \"send_to_voicemail\": true,\n" +
+                "    \"starred\": false,\n" +
+                "    \"pinned\": 2\n" +
+                "  },\n" +
+                "  \"aggregation_data\": [\n" +
+                "    {\n" +
+                "      \"type\": \"\",\n" +
+                "      \"contact_ids\": [\n" +
+                "        {\n" +
+                "          \"contact_id\": \"2222222\"\n" +
+                "        },\n" +
+                "        {\n" +
+                "          \"contact_id\": \"3333333\"\n" +
+                "        }\n" +
+                "      ]\n" +
+                "    }\n" +
+                "  ]}";
+        try {
+            MetadataEntryParser.parseDataToMetaDataEntry(input);
+        } catch (IllegalArgumentException e) {
+            // Expected.
+        }
+    }
+
+    private String readAssetAsString(String fileName) throws IOException {
+        Context context = getTestContext();
+        InputStream input = context.getAssets().open(fileName);
+        ByteArrayOutputStream contents = new ByteArrayOutputStream();
+        int len;
+        byte[] data = new byte[1024];
+        do {
+            len = input.read(data);
+            if (len > 0) contents.write(data, 0, len);
+        } while (len == data.length);
+        return contents.toString();
+    }
+
+    private void assertMetaDataEntry(MetadataEntry entry1, MetadataEntry entry2) {
+        assertRawContactInfoEquals(entry1.mRawContactInfo, entry2.mRawContactInfo);
+        assertEquals(entry1.mSendToVoicemail, entry2.mSendToVoicemail);
+        assertEquals(entry1.mStarred, entry2.mStarred);
+        assertEquals(entry1.mPinned, entry2.mPinned);
+        assertAggregationDataListEquals(entry1.mAggregationDatas, entry2.mAggregationDatas);
+        assertFieldDataListEquals(entry1.mFieldDatas, entry2.mFieldDatas);
+    }
+
+    private void assertRawContactInfoEquals(RawContactInfo contact1, RawContactInfo contact2) {
+        assertEquals(contact1.mBackupId, contact2.mBackupId);
+        assertEquals(contact1.mAccountType, contact2.mAccountType);
+        assertEquals(contact1.mAccountName, contact2.mAccountName);
+        assertEquals(contact1.mDataSet, contact2.mDataSet);
+    }
+
+    private void assertAggregationDataListEquals(ArrayList<AggregationData> aggregationList1,
+            ArrayList<AggregationData> aggregationList2) {
+        assertEquals(aggregationList1.size(), aggregationList2.size());
+        for (int i = 0; i < aggregationList1.size(); i++) {
+            assertAggregationDataEquals(aggregationList1.get(i), aggregationList2.get(i));
+        }
+    }
+
+    private void assertAggregationDataEquals(AggregationData aggregationData1,
+            AggregationData aggregationData2) {
+        assertRawContactInfoEquals(aggregationData1.mRawContactInfo1,
+                aggregationData2.mRawContactInfo1);
+        assertRawContactInfoEquals(aggregationData1.mRawContactInfo2,
+                aggregationData2.mRawContactInfo2);
+        assertEquals(aggregationData1.mType, aggregationData2.mType);
+    }
+
+    private void assertFieldDataListEquals(ArrayList<FieldData> fieldDataList1,
+            ArrayList<FieldData> fieldDataList2) {
+        assertEquals(fieldDataList1.size(), fieldDataList2.size());
+        for (int i = 0; i < fieldDataList1.size(); i++) {
+            assertFieldDataEquals(fieldDataList1.get(i), fieldDataList2.get(i));
+        }
+    }
+
+    private void assertFieldDataEquals(FieldData fieldData1, FieldData fieldData2) {
+        assertEquals(fieldData1.mDataHashId, fieldData2.mDataHashId);
+        assertEquals(fieldData1.mIsPrimary, fieldData2.mIsPrimary);
+        assertEquals(fieldData1.mIsSuperPrimary, fieldData2.mIsSuperPrimary);
+        assertUsageStatsListEquals(fieldData1.mUsageStatsList, fieldData2.mUsageStatsList);
+    }
+
+    private void assertUsageStatsListEquals(ArrayList<UsageStats> usageStatsList1,
+            ArrayList<UsageStats> usageStatsList2) {
+        assertEquals(usageStatsList1.size(), usageStatsList2.size());
+        for (int i = 0; i < usageStatsList1.size(); i++) {
+            assertUsageStatsEquals(usageStatsList1.get(i), usageStatsList2.get(i));
+        }
+    }
+
+    private void assertUsageStatsEquals(UsageStats usageStats1, UsageStats usageStats2) {
+        assertEquals(usageStats1.mUsageType, usageStats2.mUsageType);
+        assertEquals(usageStats1.mLastTimeUsed, usageStats2.mLastTimeUsed);
+        assertEquals(usageStats1.mTimesUsed, usageStats2.mTimesUsed);
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java b/tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java
index a55072c..8dd09bf 100644
--- a/tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java
@@ -29,7 +29,7 @@
     }
 
     @Override
-    protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
+    public ContactsDatabaseHelper getDatabaseHelper(final Context context) {
         if (mDbHelper == null) {
             mDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context);
         }
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index 1d127c7..19878f8 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -37,11 +37,12 @@
     private boolean mDataWipeEnabled = true;
     private Account mAccount;
     private boolean mNetworkNotified;
+    private boolean mMetadataNetworkNotified;
     private boolean mIsPhone = true;
     private boolean mIsVoiceCapable = true;
 
     @Override
-    protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
+    public ContactsDatabaseHelper getDatabaseHelper(final Context context) {
         if (sDbHelper == null) {
             sDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context);
         }
@@ -61,17 +62,23 @@
     public void onBegin() {
         super.onBegin();
         mNetworkNotified = false;
+        mMetadataNetworkNotified = false;
     }
 
     @Override
-    protected void notifyChange(boolean syncToNetwork) {
+    protected void notifyChange(boolean syncToNetwork, boolean syncToMetadataNetwork) {
         mNetworkNotified |= syncToNetwork;
+        mMetadataNetworkNotified |= syncToMetadataNetwork;
     }
 
     public boolean isNetworkNotified() {
         return mNetworkNotified;
     }
 
+    public boolean isMetadataNetworkNotified() {
+        return mMetadataNetworkNotified;
+    }
+
     public void setIsPhone(boolean flag) {
         mIsPhone = flag;
     }
diff --git a/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java b/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java
index e9cd3b5..f24875b 100644
--- a/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java
+++ b/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java
@@ -24,6 +24,7 @@
 import android.net.Uri;
 import android.provider.ContactsContract;
 import android.test.mock.MockContentResolver;
+import com.android.providers.contacts.AccountWithDataSet;
 
 import java.util.List;
 
@@ -90,7 +91,18 @@
             String... extras) {
         ContentValues values = new ContentValues();
         CommonDatabaseUtils.extrasVarArgsToValues(values, extras);
-        final Uri uri = TestUtil.maybeAddAccountQueryParameters(ContactsContract.RawContacts.CONTENT_URI, account);
+        final Uri uri = TestUtil.maybeAddAccountQueryParameters(
+                ContactsContract.RawContacts.CONTENT_URI, account);
+        Uri contactUri = resolver.insert(uri, values);
+        return ContentUris.parseId(contactUri);
+    }
+
+    public static long createRawContactWithAccountDataSet(ContentResolver resolver,
+            AccountWithDataSet accountWithDataSet, String... extras) {
+        ContentValues values = new ContentValues();
+        CommonDatabaseUtils.extrasVarArgsToValues(values, extras);
+        final Uri uri = TestUtil.maybeAddAccountWithDataSetQueryParameters(
+                ContactsContract.RawContacts.CONTENT_URI, accountWithDataSet);
         Uri contactUri = resolver.insert(uri, values);
         return ContentUris.parseId(contactUri);
     }
@@ -118,4 +130,19 @@
     public static long createRawContact(ContentResolver resolver) {
         return createRawContact(resolver, null);
     }
+
+    public static long createRawContactWithBackupId(ContentResolver resolver, String backupId,
+            Account account) {
+        ContentValues values = new ContentValues();
+        values.put(ContactsContract.RawContacts.BACKUP_ID, backupId);
+        final Uri uri = ContactsContract.RawContacts.CONTENT_URI
+                .buildUpon()
+                .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME,
+                        account.name)
+                .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE,
+                        account.type)
+                .build();
+        Uri contactUri = resolver.insert(uri, values);
+        return ContentUris.parseId(contactUri);
+    }
 }
diff --git a/tests/src/com/android/providers/contacts/testutil/TestUtil.java b/tests/src/com/android/providers/contacts/testutil/TestUtil.java
index 2020f6d..05ff61d 100644
--- a/tests/src/com/android/providers/contacts/testutil/TestUtil.java
+++ b/tests/src/com/android/providers/contacts/testutil/TestUtil.java
@@ -18,8 +18,9 @@
 
 import android.accounts.Account;
 import android.net.Uri;
-import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
+import com.android.providers.contacts.AccountWithDataSet;
 
 /**
  * Common methods used for testing.
@@ -46,8 +47,20 @@
             return uri;
         }
         return uri.buildUpon()
-                .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
-                .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
+                .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
+                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
+                .build();
+    }
+
+    public static Uri maybeAddAccountWithDataSetQueryParameters(Uri uri,
+            AccountWithDataSet account) {
+        if (account == null) {
+            return uri;
+        }
+        return uri.buildUpon()
+                .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.getAccountName())
+                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.getAccountType())
+                .appendQueryParameter(RawContacts.DATA_SET, account.getDataSet())
                 .build();
     }
 }