Merge "Add RawContactsEntity.CORP_CONTENT_URI"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index aae425c..347abb3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3,29 +3,33 @@
         android:sharedUserId="android.uid.shared"
         android:sharedUserLabel="@string/sharedUserLabel">
 
-    <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
-    <uses-permission android:name="android.permission.READ_PROFILE" />
-    <uses-permission android:name="android.permission.WRITE_PROFILE" />
-    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.BIND_DIRECTORY_SEARCH" />
-    <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
-    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
     <uses-permission android:name="android.permission.MANAGE_USERS" />
+    <uses-permission android:name="android.permission.PROCESS_PHONE_ACCOUNT_REGISTRATION" />
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_PROFILE" />
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.SEND_CALL_LOG_CHANGE" />
+    <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_PROFILE" />
     <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
     <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL" />
     <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL" />
 
+    <permission
+            android:name="android.permission.SEND_CALL_LOG_CHANGE"
+            android:label="Broadcast that a change happened to the call log."
+            android:protectionLevel="signature|system"/>
+
     <application android:process="android.process.acore"
         android:label="@string/app_label"
         android:icon="@drawable/app_icon"
-        android:backupAgent="CallLogBackupAgent">
-
-        <meta-data android:name="com.google.android.backup.api_key"
-                android:value="AEdPqrEAAAAI0N64ZsWbwY3WiVlfYvjLWRVpOrAOl9xKHkraxA" />
+        android:allowBackup="false">
 
         <provider android:name="ContactsProvider2"
             android:authorities="contacts;com.android.contacts"
@@ -77,6 +81,14 @@
             </intent-filter>
         </receiver>
 
+        <receiver android:name="PhoneAccountRegistrationReceiver"
+                android:permission="android.permission.BROADCAST_PHONE_ACCOUNT_REGISTRATION">
+            <!-- Broadcast sent after a phone account is registered in telecom. -->
+            <intent-filter>
+                <action android:name="android.telecom.action.PHONE_ACCOUNT_REGISTERED"/>
+            </intent-filter>
+        </receiver>
+
         <receiver android:name="PackageIntentReceiver">
             <intent-filter>
                 <action android:name="android.intent.action.PACKAGE_ADDED" />
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index ce03b98..e01744e 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -26,7 +26,7 @@
     <string name="local_invisible_directory" msgid="705244318477396120">"Altul"</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Mesaj vocal de la "</string>
     <string name="debug_dump_title" msgid="4916885724165570279">"Copiaţi baza de date a agendei"</string>
-    <string name="debug_dump_database_message" msgid="406438635002392290">"Sunteţi pe cale 1) să faceţi o copie, pe stocarea internă, a bazei dvs. de date care include toate informaţiile referitoare la agendă şi întregul jurnal de apeluri şi 2) să trimiteţi această copie prin e-mail. Nu uitaţi să ştergeţi această copie după ce aţi copiat-o de pe dispozitiv sau după ce a fost primit e-mailul."</string>
+    <string name="debug_dump_database_message" msgid="406438635002392290">"Sunteţi pe cale 1) să faceţi o copie, pe stocarea internă, a bazei dvs. de date care include toate informaţiile referitoare la agendă și întregul jurnal de apeluri și 2) să trimiteţi această copie prin e-mail. Nu uitaţi să ştergeţi această copie după ce aţi copiat-o de pe dispozitiv sau după ce a fost primit e-mailul."</string>
     <string name="debug_dump_delete_button" msgid="7832879421132026435">"Ștergeţi acum"</string>
     <string name="debug_dump_start_button" msgid="2837506913757600001">"Porniţi"</string>
     <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Alegeţi un program pentru a trimite fişierul"</string>
diff --git a/src/com/android/providers/contacts/CallLogBackupAgent.java b/src/com/android/providers/contacts/CallLogBackupAgent.java
deleted file mode 100644
index e5c77e6..0000000
--- a/src/com/android/providers/contacts/CallLogBackupAgent.java
+++ /dev/null
@@ -1,357 +0,0 @@
-/*
- * 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.app.backup.BackupAgent;
-import android.app.backup.BackupDataInput;
-import android.app.backup.BackupDataOutput;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.database.Cursor;
-import android.os.ParcelFileDescriptor;
-import android.provider.CallLog;
-import android.provider.CallLog.Calls;
-import android.telecom.PhoneAccountHandle;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.DataInput;
-import java.io.DataInputStream;
-import java.io.DataOutput;
-import java.io.DataOutputStream;
-import java.io.EOFException;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-/**
- * Call log backup agent.
- */
-public class CallLogBackupAgent extends BackupAgent {
-
-    @VisibleForTesting
-    static class CallLogBackupState {
-        int version;
-        SortedSet<Integer> callIds;
-    }
-
-    private static class Call {
-        int id;
-        long date;
-        long duration;
-        String number;
-        int type;
-        int numberPresentation;
-        String accountComponentName;
-        String accountId;
-        String accountAddress;
-        Long dataUsage;
-        int features;
-
-        @Override
-        public String toString() {
-            if (isDebug()) {
-                return  "[" + id + ", account: [" + accountComponentName + " : " + accountId +
-                    "]," + number + ", " + date + "]";
-            } else {
-                return "[" + id + "]";
-            }
-        }
-    }
-
-    private static final String TAG = "CallLogBackupAgent";
-
-    /** Current version of CallLogBackup. Used to track the backup format. */
-    private static final int VERSION = 1;
-    /** Version indicating that there exists no previous backup entry. */
-    @VisibleForTesting
-    static final int VERSION_NO_PREVIOUS_STATE = 0;
-
-    private static final String[] CALL_LOG_PROJECTION = new String[] {
-        CallLog.Calls._ID,
-        CallLog.Calls.DATE,
-        CallLog.Calls.DURATION,
-        CallLog.Calls.NUMBER,
-        CallLog.Calls.TYPE,
-        CallLog.Calls.COUNTRY_ISO,
-        CallLog.Calls.GEOCODED_LOCATION,
-        CallLog.Calls.NUMBER_PRESENTATION,
-        CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME,
-        CallLog.Calls.PHONE_ACCOUNT_ID,
-        CallLog.Calls.PHONE_ACCOUNT_ADDRESS,
-        CallLog.Calls.DATA_USAGE,
-        CallLog.Calls.FEATURES
-    };
-
-    /** ${inheritDoc} */
-    @Override
-    public void onBackup(ParcelFileDescriptor oldStateDescriptor, BackupDataOutput data,
-            ParcelFileDescriptor newStateDescriptor) throws IOException {
-
-        // Get the list of the previous calls IDs which were backed up.
-        DataInputStream dataInput = new DataInputStream(
-                new FileInputStream(oldStateDescriptor.getFileDescriptor()));
-        final CallLogBackupState state;
-        try {
-            state = readState(dataInput);
-        } finally {
-            dataInput.close();
-        }
-
-        // Run the actual backup of data
-        runBackup(state, data);
-
-        // Rewrite the backup state.
-        DataOutputStream dataOutput = new DataOutputStream(new BufferedOutputStream(
-                new FileOutputStream(newStateDescriptor.getFileDescriptor())));
-        try {
-            writeState(dataOutput, state);
-        } finally {
-            dataOutput.close();
-        }
-    }
-
-    /** ${inheritDoc} */
-    @Override
-    public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
-            throws IOException {
-        if (isDebug()) {
-            Log.d(TAG, "Performing Restore");
-        }
-
-        while (data.readNextHeader()) {
-            Call call = readCallFromData(data);
-            if (call != null) {
-                writeCallToProvider(call);
-                if (isDebug()) {
-                    Log.d(TAG, "Restored call: " + call);
-                }
-            }
-        }
-    }
-
-    @VisibleForTesting
-    void runBackup(CallLogBackupState state, BackupDataOutput data) {
-        SortedSet<Integer> callsToRemove = new TreeSet<>(state.callIds);
-
-        // Get all the existing call log entries.
-        Cursor cursor = getAllCallLogEntries();
-        if (cursor == null) {
-            return;
-        }
-
-        try {
-            // Loop through all the call log entries to identify:
-            // (1) new calls
-            // (2) calls which have been deleted.
-            while (cursor.moveToNext()) {
-                Call call = readCallFromCursor(cursor);
-
-                if (!state.callIds.contains(call.id)) {
-
-                    if (isDebug()) {
-                        Log.d(TAG, "Adding call to backup: " + call);
-                    }
-
-                    // This call new (not in our list from the last backup), lets back it up.
-                    addCallToBackup(data, call);
-                    state.callIds.add(call.id);
-                } else {
-                    // This call still exists in the current call log so delete it from the
-                    // "callsToRemove" set since we want to keep it.
-                    callsToRemove.remove(call.id);
-                }
-            }
-
-            // Remove calls which no longer exist in the set.
-            for (Integer i : callsToRemove) {
-                if (isDebug()) {
-                    Log.d(TAG, "Removing call from backup: " + i);
-                }
-
-                removeCallFromBackup(data, i);
-                state.callIds.remove(i);
-            }
-
-        } finally {
-            cursor.close();
-        }
-    }
-
-    private Cursor getAllCallLogEntries() {
-        // We use the API here instead of querying ContactsDatabaseHelper directly because
-        // CallLogProvider has special locks in place for sychronizing when to read.  Using the APIs
-        // gives us that for free.
-        ContentResolver resolver = getContentResolver();
-        return resolver.query(CallLog.Calls.CONTENT_URI, CALL_LOG_PROJECTION, null, null, null);
-    }
-
-    private void writeCallToProvider(Call call) {
-        Long dataUsage = call.dataUsage == 0 ? null : call.dataUsage;
-
-        PhoneAccountHandle handle = new PhoneAccountHandle(
-                ComponentName.unflattenFromString(call.accountComponentName), call.accountId);
-        Calls.addCall(null /* CallerInfo */, this, call.number, call.numberPresentation, call.type,
-                call.features, handle, call.date, (int) call.duration,
-                dataUsage, true /* addForAllUsers */);
-    }
-
-    @VisibleForTesting
-    CallLogBackupState readState(DataInput dataInput) throws IOException {
-        CallLogBackupState state = new CallLogBackupState();
-        state.callIds = new TreeSet<>();
-
-        try {
-            // Read the version.
-            state.version = dataInput.readInt();
-
-            if (state.version >= 1) {
-                // Read the size.
-                int size = dataInput.readInt();
-
-                // Read all of the call IDs.
-                for (int i = 0; i < size; i++) {
-                    state.callIds.add(dataInput.readInt());
-                }
-            }
-        } catch (EOFException e) {
-            state.version = VERSION_NO_PREVIOUS_STATE;
-        }
-
-        return state;
-    }
-
-    @VisibleForTesting
-    void writeState(DataOutput dataOutput, CallLogBackupState state)
-            throws IOException {
-        // Write version first of all
-        dataOutput.writeInt(VERSION);
-
-        // [Version 1]
-        // size + callIds
-        dataOutput.writeInt(state.callIds.size());
-        for (Integer i : state.callIds) {
-            dataOutput.writeInt(i);
-        }
-    }
-
-    @VisibleForTesting
-    Call readCallFromData(BackupDataInput data) {
-        final int callId;
-        try {
-            callId = Integer.parseInt(data.getKey());
-        } catch (NumberFormatException e) {
-            Log.e(TAG, "Unexpected key found in restore: " + data.getKey());
-            return null;
-        }
-
-        try {
-            byte [] byteArray = new byte[data.getDataSize()];
-            data.readEntityData(byteArray, 0, byteArray.length);
-            DataInputStream dataInput = new DataInputStream(new ByteArrayInputStream(byteArray));
-
-            Call call = new Call();
-            call.id = callId;
-
-            int version = dataInput.readInt();
-            if (version >= 1) {
-                call.date = dataInput.readLong();
-                call.duration = dataInput.readLong();
-                call.number = dataInput.readUTF();
-                call.type = dataInput.readInt();
-                call.numberPresentation = dataInput.readInt();
-                call.accountComponentName = dataInput.readUTF();
-                call.accountId = dataInput.readUTF();
-                call.accountAddress = dataInput.readUTF();
-                call.dataUsage = dataInput.readLong();
-                call.features = dataInput.readInt();
-            }
-
-            return call;
-        } catch (IOException e) {
-            Log.e(TAG, "Error reading call data for " + callId, e);
-            return null;
-        }
-    }
-
-    private Call readCallFromCursor(Cursor cursor) {
-        Call call = new Call();
-        call.id = cursor.getInt(cursor.getColumnIndex(CallLog.Calls._ID));
-        call.date = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
-        call.duration = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DURATION));
-        call.number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
-        call.type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
-        call.numberPresentation =
-                cursor.getInt(cursor.getColumnIndex(CallLog.Calls.NUMBER_PRESENTATION));
-        call.accountComponentName =
-                cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME));
-        call.accountId =
-                cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ID));
-        call.accountAddress =
-                cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ADDRESS));
-        call.dataUsage = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATA_USAGE));
-        call.features = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.FEATURES));
-        return call;
-    }
-
-    private void addCallToBackup(BackupDataOutput output, Call call) {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        DataOutputStream data = new DataOutputStream(baos);
-
-        try {
-            data.writeInt(VERSION);
-            data.writeLong(call.date);
-            data.writeLong(call.duration);
-            data.writeUTF(call.number);
-            data.writeInt(call.type);
-            data.writeInt(call.numberPresentation);
-            data.writeUTF(call.accountComponentName);
-            data.writeUTF(call.accountId);
-            data.writeUTF(call.accountAddress);
-            data.writeLong(call.dataUsage);
-            data.writeInt(call.features);
-            data.flush();
-
-            output.writeEntityHeader(Integer.toString(call.id), baos.size());
-            output.writeEntityData(baos.toByteArray(), baos.size());
-
-            if (isDebug()) {
-                Log.d(TAG, "Wrote call to backup: " + call + " with byte array: " + baos);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to backup call: " + call, e);
-        }
-    }
-
-    private void removeCallFromBackup(BackupDataOutput output, int callId) {
-        try {
-            output.writeEntityHeader(Integer.toString(callId), -1);
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to remove call: " + callId, e);
-        }
-    }
-
-    private static boolean isDebug() {
-        return Log.isLoggable(TAG, Log.DEBUG);
-    }
-}
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index 6bf913e..547e4d3 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -39,6 +39,9 @@
 import android.os.UserManager;
 import android.provider.CallLog;
 import android.provider.CallLog.Calls;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -46,7 +49,6 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 import com.android.providers.contacts.util.SelectionBuilder;
 import com.android.providers.contacts.util.UserUtils;
-
 import com.google.common.annotations.VisibleForTesting;
 
 import java.util.HashMap;
@@ -60,12 +62,16 @@
     private static final String TAG = CallLogProvider.class.getSimpleName();
 
     private static final int BACKGROUND_TASK_INITIALIZE = 0;
+    private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1;
 
     /** Selection clause for selecting all calls that were made after a certain time */
     private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?";
     /** Selection clause to use to exclude voicemail records.  */
     private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause(
             Calls.TYPE, Calls.VOICEMAIL_TYPE);
+    /** Selection clause to exclude hidden records. */
+    private static final String EXCLUDE_HIDDEN_SELECTION = getEqualityClause(
+            Calls.PHONE_ACCOUNT_HIDDEN, 0);
 
     @VisibleForTesting
     static final String[] CALL_LOG_SYNC_PROJECTION = new String[] {
@@ -80,12 +86,22 @@
         Calls.PHONE_ACCOUNT_ID
     };
 
+    static final String[] MINIMAL_PROJECTION = new String[] { Calls._ID };
+
     private static final int CALLS = 1;
 
     private static final int CALLS_ID = 2;
 
     private static final int CALLS_FILTER = 3;
 
+    private static final String UNHIDE_BY_PHONE_ACCOUNT_QUERY =
+            "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " +
+            Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " + Calls.PHONE_ACCOUNT_ID + "=?;";
+
+    private static final String UNHIDE_BY_ADDRESS_QUERY =
+            "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " +
+            Calls.PHONE_ACCOUNT_ADDRESS + "=?;";
+
     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     static {
         sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
@@ -109,6 +125,7 @@
         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME);
         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID);
         sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS);
+        sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_HIDDEN, Calls.PHONE_ACCOUNT_HIDDEN);
         sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
         sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
         sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION);
@@ -122,6 +139,7 @@
         sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
         sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
         sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
+        sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI);
         sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER);
     }
 
@@ -155,13 +173,13 @@
         mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
             @Override
             public void handleMessage(Message msg) {
-                performBackgroundTask(msg.what);
+                performBackgroundTask(msg.what, msg.obj);
             }
         };
 
         mReadAccessLatch = new CountDownLatch(1);
 
-        scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE);
+        scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE, null);
 
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
             Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate finish");
@@ -190,6 +208,7 @@
 
         final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
         checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, true /*isQuery*/);
+        selectionBuilder.addClause(EXCLUDE_HIDDEN_SELECTION);
 
         final int match = sURIMatcher.match(uri);
         switch (match) {
@@ -357,6 +376,10 @@
         return getContext();
     }
 
+    void adjustForNewPhoneAccount(PhoneAccountHandle handle) {
+        scheduleBackgroundTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle);
+    }
+
     /**
      * Returns a {@link DatabaseModifier} that takes care of sending necessary notifications
      * after the operation is performed.
@@ -467,6 +490,48 @@
     }
 
     /**
+     * Un-hides any hidden call log entries that are associated with the specified handle.
+     *
+     * @param handle The handle to the newly registered {@link android.telecom.PhoneAccount}.
+     */
+    private void adjustForNewPhoneAccountInternal(PhoneAccountHandle handle) {
+        String[] handleArgs =
+                new String[] { handle.getComponentName().flattenToString(), handle.getId() };
+
+        // Check to see if any entries exist for this handle. If so (not empty), run the un-hiding
+        // update. If not, then try to identify the call from the phone number.
+        Cursor cursor = query(Calls.CONTENT_URI, MINIMAL_PROJECTION,
+                Calls.PHONE_ACCOUNT_COMPONENT_NAME + " =? AND " + Calls.PHONE_ACCOUNT_ID + " =?",
+                handleArgs, null);
+
+        if (cursor != null) {
+            try {
+                if (cursor.getCount() >= 1) {
+                    // run un-hiding process based on phone account
+                    mDbHelper.getWritableDatabase().execSQL(
+                            UNHIDE_BY_PHONE_ACCOUNT_QUERY, handleArgs);
+                } else {
+                    TelecomManager tm = TelecomManager.from(getContext());
+                    if (tm != null) {
+
+                        PhoneAccount account = tm.getPhoneAccount(handle);
+                        if (account != null) {
+                            // We did not find any items for the specific phone account, so run the
+                            // query based on the phone number instead.
+                            mDbHelper.getWritableDatabase().execSQL(UNHIDE_BY_ADDRESS_QUERY,
+                                    new String[] { account.getAddress().toString() });
+                        }
+
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+    }
+
+    /**
      * @param cursor to copy call log entries from
      *
      * @return the timestamp of the last synced entry.
@@ -544,11 +609,11 @@
         }
     }
 
-    private void scheduleBackgroundTask(int task) {
-        mBackgroundHandler.sendEmptyMessage(task);
+    private void scheduleBackgroundTask(int task, Object arg) {
+        mBackgroundHandler.obtainMessage(task, arg).sendToTarget();
     }
 
-    private void performBackgroundTask(int task) {
+    private void performBackgroundTask(int task, Object arg) {
         if (task == BACKGROUND_TASK_INITIALIZE) {
             try {
                 final Context context = getContext();
@@ -563,6 +628,8 @@
                 mReadAccessLatch.countDown();
                 mReadAccessLatch = null;
             }
+        } else if (task == BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT) {
+            adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg);
         }
 
     }
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index fc4d343..0012728 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -23,8 +23,8 @@
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.database.CharArrayBuffer;
 import android.database.Cursor;
@@ -90,12 +90,12 @@
 import com.google.android.collect.Sets;
 import com.google.common.annotations.VisibleForTesting;
 
+import libcore.icu.ICU;
+
 import java.util.Locale;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
-import libcore.icu.ICU;
-
 /**
  * Database helper for contacts. Designed as a singleton to make sure that all
  * {@link android.content.ContentProvider} users get the same reference.
@@ -118,10 +118,10 @@
      *   700-799 Jelly Bean
      *   800-899 Kitkat
      *   900-999 Lollipop
-     *   1000-1100 M
+     *   1000-1099 M
      * </pre>
      */
-    static final int DATABASE_VERSION = 1003;
+    static final int DATABASE_VERSION = 1007;
 
     public interface Tables {
         public static final String CONTACTS = "contacts";
@@ -413,6 +413,8 @@
         public static final String CONCRETE_ACCOUNT_ID = Tables.RAW_CONTACTS + "." + ACCOUNT_ID;
         public static final String CONCRETE_SOURCE_ID =
                 Tables.RAW_CONTACTS + "." + RawContacts.SOURCE_ID;
+        public static final String CONCRETE_BACKUP_ID =
+                Tables.RAW_CONTACTS + "." + RawContacts.BACKUP_ID;
         public static final String CONCRETE_VERSION =
                 Tables.RAW_CONTACTS + "." + RawContacts.VERSION;
         public static final String CONCRETE_DIRTY =
@@ -1519,6 +1521,7 @@
                 Calls.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," +
                 Calls.PHONE_ACCOUNT_ID + " TEXT," +
                 Calls.PHONE_ACCOUNT_ADDRESS + " TEXT," +
+                Calls.PHONE_ACCOUNT_HIDDEN + " INTEGER NOT NULL DEFAULT 0," +
                 Calls.SUB_ID + " INTEGER DEFAULT -1," +
                 Calls.NEW + " INTEGER," +
                 Calls.CACHED_NAME + " TEXT," +
@@ -1532,6 +1535,7 @@
                 Calls.CACHED_MATCHED_NUMBER + " TEXT," +
                 Calls.CACHED_NORMALIZED_NUMBER + " TEXT," +
                 Calls.CACHED_PHOTO_ID + " INTEGER NOT NULL DEFAULT 0," +
+                Calls.CACHED_PHOTO_URI + " TEXT," +
                 Calls.CACHED_FORMATTED_NUMBER + " TEXT," +
                 Voicemails._DATA + " TEXT," +
                 Voicemails.HAS_CONTENT + " INTEGER," +
@@ -1539,13 +1543,17 @@
                 Voicemails.SOURCE_DATA + " TEXT," +
                 Voicemails.SOURCE_PACKAGE + " TEXT," +
                 Voicemails.TRANSCRIPTION + " TEXT," +
-                Voicemails.STATE + " INTEGER" +
+                Voicemails.STATE + " INTEGER," +
+                Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
+                Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0" +
         ");");
 
         // Voicemail source status table.
         db.execSQL("CREATE TABLE " + Tables.VOICEMAIL_STATUS + " (" +
                 VoicemailContract.Status._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                 VoicemailContract.Status.SOURCE_PACKAGE + " TEXT UNIQUE NOT NULL," +
+                VoicemailContract.Status.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," +
+                VoicemailContract.Status.PHONE_ACCOUNT_ID + " TEXT," +
                 VoicemailContract.Status.SETTINGS_URI + " TEXT," +
                 VoicemailContract.Status.VOICEMAIL_ACCESS_URI + " TEXT," +
                 VoicemailContract.Status.CONFIGURATION_STATE + " INTEGER," +
@@ -1878,6 +1886,7 @@
                             + AccountsColumns.CONCRETE_DATA_SET + " END) AS "
                                 + RawContacts.ACCOUNT_TYPE_AND_DATA_SET + ","
                 + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
+                + RawContactsColumns.CONCRETE_BACKUP_ID + " AS " + RawContacts.BACKUP_ID + ","
                 + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
                 + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
                 + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ","
@@ -2000,7 +2009,6 @@
                 + RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE  + ", "
                 + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ", "
                 + rawContactOptionColumns + ", "
-                + RawContacts.BACKUP_ID + ", "
                 + syncColumns
                 + " FROM " + Tables.RAW_CONTACTS
                 + " JOIN " + Tables.ACCOUNTS + " ON ("
@@ -2881,6 +2889,26 @@
             oldVersion = 1003;
         }
 
+        if (oldVersion < 1004) {
+            upgradeToVersion1004(db);
+            oldVersion = 1004;
+        }
+
+        if (oldVersion < 1005) {
+            upgradeToVersion1005(db);
+            oldVersion = 1005;
+        }
+
+        if (oldVersion < 1006) {
+            upgradeViewsAndTriggers = true;
+            oldVersion = 1006;
+        }
+
+        if (oldVersion < 1007) {
+            upgradeToVersion1007(db);
+            oldVersion = 1007;
+        }
+
         if (upgradeViewsAndTriggers) {
             createContactsViews(db);
             createGroupsView(db);
@@ -4374,6 +4402,39 @@
         }
     }
 
+    /**
+     * Add a "hidden" column for call log entries we want to hide after an upgrade until the user
+     * adds the right phone account to the device.
+     */
+    public void upgradeToVersion1004(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE calls ADD phone_account_hidden INTEGER NOT NULL DEFAULT 0;");
+    }
+
+    public void upgradeToVersion1005(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE calls ADD photo_uri TEXT;");
+    }
+
+    /**
+     * The try/catch pattern exists because some devices have the upgrade and some do not. This is
+     * because the below updates were merged into version 1005 after some devices had already
+     * upgraded to version 1005 and hence did not receive the below upgrades.
+     */
+    public void upgradeToVersion1007(SQLiteDatabase db) {
+        try {
+            // Add multi-sim fields
+            db.execSQL("ALTER TABLE voicemail_status ADD phone_account_component_name TEXT;");
+            db.execSQL("ALTER TABLE voicemail_status ADD phone_account_id TEXT;");
+
+            // For use by the sync adapter
+            db.execSQL("ALTER TABLE calls ADD dirty INTEGER NOT NULL DEFAULT 0;");
+            db.execSQL("ALTER TABLE calls ADD deleted INTEGER NOT NULL DEFAULT 0;");
+        } catch (SQLiteException e) {
+            // These columns already exist. Do nothing.
+            // Log verbose because this should be the majority case.
+            Log.v(TAG, "Version 1007: Columns already exist, skipping upgrade steps.");
+        }
+  }
+
     public String extractHandleFromEmailAddress(String email) {
         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
         if (tokens.length == 0) {
@@ -4818,7 +4879,14 @@
         return mMimeTypeIdSip;
     }
 
-    public int getDisplayNameSourceForMimeTypeId(int mimeTypeId) {
+    /**
+     * Returns a {@link ContactsContract.DisplayNameSources} value based on {@param mimeTypeId}.
+     * This does not return {@link ContactsContract.DisplayNameSources#STRUCTURED_PHONETIC_NAME}.
+     * The calling client needs to inspect the structured name itself to distinguish between
+     * {@link ContactsContract.DisplayNameSources#STRUCTURED_NAME} and
+     * {@code STRUCTURED_PHONETIC_NAME}.
+     */
+    private int getDisplayNameSourceForMimeTypeId(int mimeTypeId) {
         if (mimeTypeId == mMimeTypeIdStructuredName) {
             return DisplayNameSources.STRUCTURED_NAME;
         }
@@ -5540,6 +5608,22 @@
             while (c.moveToNext()) {
                 int mimeType = c.getInt(RawContactNameQuery.MIMETYPE);
                 int source = getDisplayNameSourceForMimeTypeId(mimeType);
+
+                if (source == DisplayNameSources.STRUCTURED_NAME) {
+                    final String given = c.getString(RawContactNameQuery.GIVEN_NAME);
+                    final String middle = c.getString(RawContactNameQuery.MIDDLE_NAME);
+                    final String family = c.getString(RawContactNameQuery.FAMILY_NAME);
+                    final String suffix = c.getString(RawContactNameQuery.SUFFIX);
+                    final String prefix = c.getString(RawContactNameQuery.PREFIX);
+                    if (TextUtils.isEmpty(given) && TextUtils.isEmpty(middle)
+                            && TextUtils.isEmpty(family) && TextUtils.isEmpty(suffix)
+                            && TextUtils.isEmpty(prefix)) {
+                        // Every non-phonetic name component is empty. Therefore, lets lower the
+                        // source score to STRUCTURED_PHONETIC_NAME.
+                        source = DisplayNameSources.STRUCTURED_PHONETIC_NAME;
+                    }
+                }
+
                 if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) {
                     continue;
                 }
@@ -5626,7 +5710,8 @@
         String sortKeyAlternative = null;
         int displayNameStyle = FullNameStyle.UNDEFINED;
 
-        if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) {
+        if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME
+                || bestDisplayNameSource == DisplayNameSources.STRUCTURED_PHONETIC_NAME) {
             displayNameStyle = bestName.fullNameStyle;
             if (displayNameStyle == FullNameStyle.CJK
                     || displayNameStyle == FullNameStyle.UNDEFINED) {
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 4abf9ae..58d55bf 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -670,6 +670,7 @@
             .add(RawContacts.ACCOUNT_TYPE_AND_DATA_SET)
             .add(RawContacts.DIRTY)
             .add(RawContacts.SOURCE_ID)
+            .add(RawContacts.BACKUP_ID)
             .add(RawContacts.VERSION)
             .build();
 
@@ -873,6 +874,7 @@
     private static final ProjectionMap sDataProjectionMap = ProjectionMap.builder()
             .add(Data._ID)
             .add(Data.RAW_CONTACT_ID)
+            .add(Data.HASH_ID)
             .add(Data.CONTACT_ID)
             .add(Data.NAME_RAW_CONTACT_ID)
             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
diff --git a/src/com/android/providers/contacts/DbModifierWithNotification.java b/src/com/android/providers/contacts/DbModifierWithNotification.java
index 5ce41c4..3576849 100644
--- a/src/com/android/providers/contacts/DbModifierWithNotification.java
+++ b/src/com/android/providers/contacts/DbModifierWithNotification.java
@@ -20,8 +20,8 @@
 import static android.Manifest.permission.ADD_VOICEMAIL;
 import static android.Manifest.permission.READ_VOICEMAIL;
 
-import android.app.backup.BackupManager;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
@@ -33,6 +33,7 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Bundle;
 import android.provider.CallLog.Calls;
 import android.provider.VoicemailContract;
 import android.provider.VoicemailContract.Status;
@@ -43,6 +44,7 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 import com.android.providers.contacts.util.DbQueryUtils;
 import com.google.android.collect.Lists;
+import com.google.common.collect.Iterables;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -74,7 +76,6 @@
     private final boolean mIsCallsTable;
     private final VoicemailPermissions mVoicemailPermissions;
 
-    private BackupManager mBackupManager;
 
     public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) {
         this(tableName, db, null, context);
@@ -91,7 +92,6 @@
         mDb = db;
         mInsertHelper = insertHelper;
         mContext = context;
-        mBackupManager = new BackupManager(context);
         mBaseUri = mTableName.equals(Tables.VOICEMAIL_STATUS) ?
                 Status.CONTENT_URI : Voicemails.CONTENT_URI;
         mIsCallsTable = mTableName.equals(Tables.CALLS);
@@ -128,7 +128,14 @@
 
     private void notifyCallLogChange() {
         mContext.getContentResolver().notifyChange(Calls.CONTENT_URI, null, false);
-        mBackupManager.dataChanged();
+
+        Intent intent = new Intent("android.intent.action.CALL_LOG_CHANGE");
+        intent.setComponent(new ComponentName("com.android.providers.calllogbackup",
+                "com.android.providers.calllogbackup.CallLogChangeReceiver"));
+
+        if (!mContext.getPackageManager().queryBroadcastReceivers(intent, 0).isEmpty()) {
+            mContext.sendBroadcast(intent);
+        }
     }
 
     private void notifyVoicemailChangeOnInsert(Uri notificationUri, Set<String> packagesModified) {
@@ -145,8 +152,20 @@
     public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
         Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
         packagesModified.addAll(getModifiedPackages(values));
+
+        boolean isVoicemail = packagesModified.size() != 0;
+
+        if (mIsCallsTable && isVoicemail) {
+            // If a calling package is modifying its own entries, it means that the change came from
+            // the server and thus is synced or "clean". Otherwise, it means that a local change
+            // is being made to the database, so the entries should be marked as "dirty" so that
+            // the corresponding sync adapter knows they need to be synced.
+            final int isDirty = isSelfModifying(packagesModified) ? 0 : 1;
+            values.put(VoicemailContract.Voicemails.DIRTY, isDirty);
+        }
+
         int count = mDb.update(table, values, whereClause, whereArgs);
-        if (count > 0 && packagesModified.size() != 0) {
+        if (count > 0 && isVoicemail) {
             notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
         }
         if (count > 0 && mIsCallsTable) {
@@ -158,8 +177,25 @@
     @Override
     public int delete(String table, String whereClause, String[] whereArgs) {
         Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
-        int count = mDb.delete(table, whereClause, whereArgs);
-        if (count > 0 && packagesModified.size() != 0) {
+        boolean isVoicemail = packagesModified.size() != 0;
+
+        // If a deletion is made by a package that is not the package that inserted the voicemail,
+        // this means that the user deleted the voicemail. However, we do not want to delete it from
+        // the database until after the server has been notified of the deletion. To ensure this,
+        // mark the entry as "deleted"--deleted entries should be hidden from the user.
+        // Once the changes are synced to the server, delete will be called again, this time
+        // removing the rows from the table.
+        final int count;
+        if (mIsCallsTable && isVoicemail && !isSelfModifying(packagesModified)) {
+            ContentValues values = new ContentValues();
+            values.put(VoicemailContract.Voicemails.DIRTY, 1);
+            values.put(VoicemailContract.Voicemails.DELETED, 1);
+            count = mDb.update(table, values, whereClause, whereArgs);
+        } else {
+            count = mDb.delete(table, whereClause, whereArgs);
+        }
+
+        if (count > 0 && isVoicemail) {
             notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
         }
         if (count > 0 && mIsCallsTable) {
@@ -200,6 +236,11 @@
         return impactedPackages;
     }
 
+    private boolean isSelfModifying(Set<String> packagesModified) {
+        return packagesModified.size() == 1 && getCallingPackages().contains(
+                Iterables.getOnlyElement(packagesModified));
+    }
+
     private void notifyVoicemailChange(Uri notificationUri, Set<String> modifiedPackages,
             String... intentActions) {
         // Notify the observers.
diff --git a/src/com/android/providers/contacts/PhoneAccountRegistrationReceiver.java b/src/com/android/providers/contacts/PhoneAccountRegistrationReceiver.java
new file mode 100644
index 0000000..8a68889
--- /dev/null
+++ b/src/com/android/providers/contacts/PhoneAccountRegistrationReceiver.java
@@ -0,0 +1,56 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.provider.CallLog;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+/**
+ * This will be launched when a new phone account is registered in telecom. It is used by the call
+ * log to un-hide any entries which were previously hidden after a backup-restore until it's
+ * associated phone-account is registered with telecom.
+ *
+ * IOW, after a restore, we hide call log entries until the user inserts the corresponding SIM,
+ * registers the corresponding SIP account, or registers a corresponding alternative phone-account.
+ */
+public class PhoneAccountRegistrationReceiver extends BroadcastReceiver {
+    static final String TAG = "PhoneAccountReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        // We are now running with the system up, but no apps started,
+        // so can do whatever cleanup after an upgrade that we want.
+        if (TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED.equals(intent.getAction())) {
+
+            PhoneAccountHandle handle = (PhoneAccountHandle) intent.getParcelableExtra(
+                    TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+
+            IContentProvider iprovider =
+                    context.getContentResolver().acquireProvider(CallLog.AUTHORITY);
+            ContentProvider provider = ContentProvider.coerceToLocalContentProvider(iprovider);
+            if (provider instanceof CallLogProvider) {
+                ((CallLogProvider) provider).adjustForNewPhoneAccount(handle);
+            }
+        }
+    }
+}
diff --git a/src/com/android/providers/contacts/VoicemailContentTable.java b/src/com/android/providers/contacts/VoicemailContentTable.java
index 16b3df7..9813eea 100644
--- a/src/com/android/providers/contacts/VoicemailContentTable.java
+++ b/src/com/android/providers/contacts/VoicemailContentTable.java
@@ -69,6 +69,8 @@
             .add(Voicemails.SOURCE_PACKAGE)
             .add(Voicemails.HAS_CONTENT)
             .add(Voicemails.MIME_TYPE)
+            .add(Voicemails.DIRTY)
+            .add(Voicemails.DELETED)
             .add(OpenableColumns.DISPLAY_NAME)
             .add(OpenableColumns.SIZE)
             .build();
@@ -99,6 +101,8 @@
                 .add(Voicemails.HAS_CONTENT)
                 .add(Voicemails.MIME_TYPE)
                 .add(Voicemails._DATA)
+                .add(Voicemails.DIRTY)
+                .add(Voicemails.DELETED)
                 .add(OpenableColumns.DISPLAY_NAME, createDisplayName(context))
                 .add(OpenableColumns.SIZE, "NULL")
                 .build();
diff --git a/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java b/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java
new file mode 100644
index 0000000..5540a24
--- /dev/null
+++ b/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java
@@ -0,0 +1,456 @@
+/*
+ * 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.aggregation.util;
+
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
+import com.android.providers.contacts.util.Hex;
+
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Logic for matching raw contacts' data.
+ */
+public class RawContactMatcher {
+    private static final String TAG = "ContactMatcher";
+
+    // Best possible match score
+    public static final int MAX_SCORE = 100;
+
+    // Suggest to aggregate contacts if their match score is equal or greater than this threshold
+    public static final int SCORE_THRESHOLD_SUGGEST = 50;
+
+    // Automatically aggregate contacts if their match score is equal or greater than this threshold
+    public static final int SCORE_THRESHOLD_PRIMARY = 70;
+
+    // Automatically aggregate contacts if the match score is equal or greater than this threshold
+    // and there is a secondary match (phone number, email etc).
+    public static final int SCORE_THRESHOLD_SECONDARY = 50;
+
+    // Score for missing data (as opposed to present data but a bad match)
+    private static final int NO_DATA_SCORE = -1;
+
+    // Score for matching phone numbers
+    private static final int PHONE_MATCH_SCORE = 71;
+
+    // Score for matching email addresses
+    private static final int EMAIL_MATCH_SCORE = 71;
+
+    // Score for matching nickname
+    private static final int NICKNAME_MATCH_SCORE = 71;
+
+    // Maximum number of characters in a name to be considered by the matching algorithm.
+    private static final int MAX_MATCHED_NAME_LENGTH = 30;
+
+    // Scores a multiplied by this number to allow room for "fractional" scores
+    private static final int SCORE_SCALE = 1000;
+
+    public static final int MATCHING_ALGORITHM_EXACT = 0;
+    public static final int MATCHING_ALGORITHM_CONSERVATIVE = 1;
+    public static final int MATCHING_ALGORITHM_APPROXIMATE = 2;
+
+    // Minimum edit distance between two names to be considered an approximate match
+    public static final float APPROXIMATE_MATCH_THRESHOLD = 0.82f;
+
+    // Minimum edit distance between two email ids to be considered an approximate match
+    public static final float APPROXIMATE_MATCH_THRESHOLD_FOR_EMAIL = 0.95f;
+
+    // Returned value when we found multiple matches and that was not allowed
+    public static final long MULTIPLE_MATCHES = -2;
+
+    /**
+     * Name matching scores: a matrix by name type vs. candidate lookup type.
+     * For example, if the name type is "full name" while we are looking for a
+     * "full name", the score may be 99. If we are looking for a "nickname" but
+     * find "first name", the score may be 50 (see specific scores defined
+     * below.)
+     * <p>
+     * For approximate matching, we have a range of scores, let's say 40-70.  Depending one how
+     * similar the two strings are, the score will be somewhere between 40 and 70, with the exact
+     * match producing the score of 70.  The score may also be 0 if the similarity (distance)
+     * between the strings is below the threshold.
+     * <p>
+     * We use a string matching algorithm, which is particularly suited for
+     * name matching. See {@link NameDistance}.
+     */
+    private static int[] sMinScore =
+            new int[NameLookupType.TYPE_COUNT * NameLookupType.TYPE_COUNT];
+    private static int[] sMaxScore =
+            new int[NameLookupType.TYPE_COUNT * NameLookupType.TYPE_COUNT];
+
+    /*
+     * Note: the reverse names ({@link NameLookupType#FULL_NAME_REVERSE},
+     * {@link NameLookupType#FULL_NAME_REVERSE_CONCATENATED} may appear to be redundant. They are
+     * not!  They are useful in three-way aggregation cases when we have, for example, both
+     * John Smith and Smith John.  A third contact with the name John Smith should be aggregated
+     * with the former rather than the latter.  This is why "reverse" matches have slightly lower
+     * scores than direct matches.
+     */
+    static {
+        setScoreRange(NameLookupType.NAME_EXACT,
+                NameLookupType.NAME_EXACT, 99, 99);
+        setScoreRange(NameLookupType.NAME_VARIANT,
+                NameLookupType.NAME_VARIANT, 90, 90);
+        setScoreRange(NameLookupType.NAME_COLLATION_KEY,
+                NameLookupType.NAME_COLLATION_KEY, 50, 80);
+
+        setScoreRange(NameLookupType.NAME_COLLATION_KEY,
+                NameLookupType.EMAIL_BASED_NICKNAME, 30, 60);
+        setScoreRange(NameLookupType.NAME_COLLATION_KEY,
+                NameLookupType.NICKNAME, 50, 60);
+
+        setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+                NameLookupType.EMAIL_BASED_NICKNAME, 50, 60);
+        setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+                NameLookupType.NAME_COLLATION_KEY, 50, 60);
+        setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+                NameLookupType.NICKNAME, 50, 60);
+
+        setScoreRange(NameLookupType.NICKNAME,
+                NameLookupType.NICKNAME, 50, 60);
+        setScoreRange(NameLookupType.NICKNAME,
+                NameLookupType.NAME_COLLATION_KEY, 50, 60);
+        setScoreRange(NameLookupType.NICKNAME,
+                NameLookupType.EMAIL_BASED_NICKNAME, 50, 60);
+    }
+
+    /**
+     * Populates the cells of the score matrix and score span matrix
+     * corresponding to the {@code candidateNameType} and {@code nameType}.
+     */
+    private static void setScoreRange(int candidateNameType, int nameType, int scoreFrom, int scoreTo) {
+        int index = nameType * NameLookupType.TYPE_COUNT + candidateNameType;
+        sMinScore[index] = scoreFrom;
+        sMaxScore[index] = scoreTo;
+    }
+
+    /**
+     * Returns the lower range for the match score for the given {@code candidateNameType} and
+     * {@code nameType}.
+     */
+    private static int getMinScore(int candidateNameType, int nameType) {
+        int index = nameType * NameLookupType.TYPE_COUNT + candidateNameType;
+        return sMinScore[index];
+    }
+
+    /**
+     * Returns the upper range for the match score for the given {@code candidateNameType} and
+     * {@code nameType}.
+     */
+    private static int getMaxScore(int candidateNameType, int nameType) {
+        int index = nameType * NameLookupType.TYPE_COUNT + candidateNameType;
+        return sMaxScore[index];
+    }
+
+    /**
+     * Captures the max score and match count for a specific contact.  Used in an
+     * contactId - MatchScore map.
+     */
+    public static class MatchScore implements Comparable<MatchScore> {
+        private long mContactId;
+        private boolean mKeepIn;
+        private boolean mKeepOut;
+        private int mPrimaryScore;
+        private int mSecondaryScore;
+        private int mMatchCount;
+
+        public MatchScore(long contactId) {
+            this.mContactId = contactId;
+        }
+
+        public void reset(long contactId) {
+            this.mContactId = contactId;
+            mKeepIn = false;
+            mKeepOut = false;
+            mPrimaryScore = 0;
+            mSecondaryScore = 0;
+            mMatchCount = 0;
+        }
+
+        public long getContactId() {
+            return mContactId;
+        }
+
+        public void updatePrimaryScore(int score) {
+            if (score > mPrimaryScore) {
+                mPrimaryScore = score;
+            }
+            mMatchCount++;
+        }
+
+        public void updateSecondaryScore(int score) {
+            if (score > mSecondaryScore) {
+                mSecondaryScore = score;
+            }
+            mMatchCount++;
+        }
+
+        public void keepIn() {
+            mKeepIn = true;
+        }
+
+        public void keepOut() {
+            mKeepOut = true;
+        }
+
+        public int getScore() {
+            if (mKeepOut) {
+                return 0;
+            }
+
+            if (mKeepIn) {
+                return MAX_SCORE;
+            }
+
+            int score = (mPrimaryScore > mSecondaryScore ? mPrimaryScore : mSecondaryScore);
+
+            // Ensure that of two contacts with the same match score the one with more matching
+            // data elements wins.
+            return score * SCORE_SCALE + mMatchCount;
+        }
+
+        /**
+         * Descending order of match score.
+         */
+        @Override
+        public int compareTo(MatchScore another) {
+            return another.getScore() - getScore();
+        }
+
+        @Override
+        public String toString() {
+            return mContactId + ": " + mPrimaryScore + "/" + mSecondaryScore + "(" + mMatchCount
+                    + ")";
+        }
+    }
+
+    private final HashMap<Long, MatchScore> mScores = new HashMap<Long, MatchScore>();
+    private final ArrayList<MatchScore> mScoreList = new ArrayList<MatchScore>();
+    private int mScoreCount = 0;
+
+    private final NameDistance mNameDistanceConservative = new NameDistance();
+    private final NameDistance mNameDistanceApproximate = new NameDistance(MAX_MATCHED_NAME_LENGTH);
+
+    private MatchScore getMatchingScore(long contactId) {
+        MatchScore matchingScore = mScores.get(contactId);
+        if (matchingScore == null) {
+            if (mScoreList.size() > mScoreCount) {
+                matchingScore = mScoreList.get(mScoreCount);
+                matchingScore.reset(contactId);
+            } else {
+                matchingScore = new MatchScore(contactId);
+                mScoreList.add(matchingScore);
+            }
+            mScoreCount++;
+            mScores.put(contactId, matchingScore);
+        }
+        return matchingScore;
+    }
+
+    /**
+     * Marks the contact as a full match, because we found an Identity match
+     */
+    public void matchIdentity(long contactId) {
+        updatePrimaryScore(contactId, MAX_SCORE);
+    }
+
+    /**
+     * Checks if there is a match and updates the overall score for the
+     * specified contact for a discovered match. The new score is determined
+     * by the prior score, by the type of name we were looking for, the type
+     * of name we found and, if the match is approximate, the distance between the candidate and
+     * actual name.
+     */
+    public void matchName(long contactId, int candidateNameType, String candidateName,
+            int nameType, String name, int algorithm) {
+        int maxScore = getMaxScore(candidateNameType, nameType);
+        if (maxScore == 0) {
+            return;
+        }
+
+        if (candidateName.equals(name)) {
+            updatePrimaryScore(contactId, maxScore);
+            return;
+        }
+
+        if (algorithm == MATCHING_ALGORITHM_EXACT) {
+            return;
+        }
+
+        int minScore = getMinScore(candidateNameType, nameType);
+        if (minScore == maxScore) {
+            return;
+        }
+
+        final byte[] decodedCandidateName;
+        final byte[] decodedName;
+        try {
+            decodedCandidateName = Hex.decodeHex(candidateName);
+            decodedName = Hex.decodeHex(name);
+        } catch (RuntimeException e) {
+            // How could this happen??  See bug 6827136
+            Log.e(TAG, "Failed to decode normalized name.  Skipping.", e);
+            return;
+        }
+
+        NameDistance nameDistance = algorithm == MATCHING_ALGORITHM_CONSERVATIVE ?
+                mNameDistanceConservative : mNameDistanceApproximate;
+
+        int score;
+        float distance = nameDistance.getDistance(decodedCandidateName, decodedName);
+        boolean emailBased = candidateNameType == NameLookupType.EMAIL_BASED_NICKNAME
+                || nameType == NameLookupType.EMAIL_BASED_NICKNAME;
+        float threshold = emailBased
+                ? APPROXIMATE_MATCH_THRESHOLD_FOR_EMAIL
+                : APPROXIMATE_MATCH_THRESHOLD;
+        if (distance > threshold) {
+            score = (int)(minScore +  (maxScore - minScore) * (1.0f - distance));
+        } else {
+            score = 0;
+        }
+
+        updatePrimaryScore(contactId, score);
+    }
+
+    public void updateScoreWithPhoneNumberMatch(long contactId) {
+        updateSecondaryScore(contactId, PHONE_MATCH_SCORE);
+    }
+
+    public void updateScoreWithEmailMatch(long contactId) {
+        updateSecondaryScore(contactId, EMAIL_MATCH_SCORE);
+    }
+
+    public void updateScoreWithNicknameMatch(long contactId) {
+        updateSecondaryScore(contactId, NICKNAME_MATCH_SCORE);
+    }
+
+    private void updatePrimaryScore(long contactId, int score) {
+        getMatchingScore(contactId).updatePrimaryScore(score);
+    }
+
+    private void updateSecondaryScore(long contactId, int score) {
+        getMatchingScore(contactId).updateSecondaryScore(score);
+    }
+
+    public void keepIn(long contactId) {
+        getMatchingScore(contactId).keepIn();
+    }
+
+    public void keepOut(long contactId) {
+        getMatchingScore(contactId).keepOut();
+    }
+
+    public void clear() {
+        mScores.clear();
+        mScoreCount = 0;
+    }
+
+    /**
+     * Returns a list of IDs for contacts that are matched on secondary data elements
+     * (phone number, email address, nickname). We still need to obtain the approximate
+     * primary score for those contacts to determine if any of them should be aggregated.
+     * <p>
+     * May return null.
+     */
+    public List<Long> prepareSecondaryMatchCandidates(int threshold) {
+        ArrayList<Long> contactIds = null;
+
+        for (int i = 0; i < mScoreCount; i++) {
+            MatchScore score = mScoreList.get(i);
+            if (score.mKeepOut) {
+                continue;
+            }
+
+            int s = score.mSecondaryScore;
+            if (s >= threshold) {
+                if (contactIds == null) {
+                    contactIds = new ArrayList<Long>();
+                }
+                contactIds.add(score.mContactId);
+            }
+            score.mPrimaryScore = NO_DATA_SCORE;
+        }
+        return contactIds;
+    }
+
+    /**
+     * Returns the contactId with the best match score over the specified threshold or -1
+     * if no such contact is found.  If multiple contacts are found, and
+     * {@code allowMultipleMatches} is {@code true}, it returns the first one found, but if
+     * {@code allowMultipleMatches} is {@code false} it'll return {@link #MULTIPLE_MATCHES}.
+     */
+    public long pickBestMatch(int threshold, boolean allowMultipleMatches) {
+        long contactId = -1;
+        int maxScore = 0;
+        for (int i = 0; i < mScoreCount; i++) {
+            MatchScore score = mScoreList.get(i);
+            if (score.mKeepOut) {
+                continue;
+            }
+
+            if (score.mKeepIn) {
+                return score.mContactId;
+            }
+
+            int s = score.mPrimaryScore;
+            if (s == NO_DATA_SCORE) {
+                s = score.mSecondaryScore;
+            }
+
+            if (s >= threshold) {
+                if (contactId != -1 && !allowMultipleMatches) {
+                    return MULTIPLE_MATCHES;
+                }
+                // In order to make it stable, let's jut pick the one with the lowest ID
+                // if multiple candidates are found.
+                if ((s > maxScore) || ((s == maxScore) && (contactId > score.mContactId))) {
+                    contactId = score.mContactId;
+                    maxScore = s;
+                }
+            }
+        }
+        return contactId;
+    }
+
+    /**
+     * Returns matches in the order of descending score.
+     */
+    public List<MatchScore> pickBestMatches(int threshold) {
+        int scaledThreshold = threshold * SCORE_SCALE;
+        List<MatchScore> matches = mScoreList.subList(0, mScoreCount);
+        Collections.sort(matches);
+        int count = 0;
+        for (int i = 0; i < mScoreCount; i++) {
+            MatchScore matchScore = matches.get(i);
+            if (matchScore.getScore() >= scaledThreshold) {
+                count++;
+            } else {
+                break;
+            }
+        }
+
+        return matches.subList(0, count);
+    }
+
+    @Override
+    public String toString() {
+        return mScoreList.subList(0, mScoreCount).toString();
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java b/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
index 547eafa..8e4121d 100644
--- a/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
@@ -98,6 +98,11 @@
             public File getDir(String name, int mode) {
                 return getTestDirectory();
             }
+
+            @Override
+            public PackageManager getPackageManager() {
+                return new MockPackageManager(mActor.getProviderContext().getPackageName());
+            }
         };
     }
 
@@ -145,6 +150,7 @@
     private interface VvmProviderCalls {
         public void sendOrderedBroadcast(Intent intent, String receiverPermission);
         public File getDir(String name, int mode);
+        public PackageManager getPackageManager();
     }
 
     public static class TestVoicemailProvider extends VoicemailContentProvider {
@@ -171,7 +177,7 @@
                 }
                 @Override
                 public PackageManager getPackageManager() {
-                    return new MockPackageManager("com.test.package1", "com.test.package2");
+                    return mDelegate.getPackageManager();
                 }
             };
         }
diff --git a/tests/src/com/android/providers/contacts/CallLogBackupAgentTest.java b/tests/src/com/android/providers/contacts/CallLogBackupAgentTest.java
deleted file mode 100644
index 2699f65..0000000
--- a/tests/src/com/android/providers/contacts/CallLogBackupAgentTest.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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 static org.mockito.Mockito.when;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.doReturn;
-
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import com.android.providers.contacts.CallLogBackupAgent.CallLogBackupState;
-
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.io.DataInput;
-import java.io.DataOutput;
-import java.io.EOFException;
-import java.util.TreeSet;
-
-/**
- * Test cases for {@link com.android.providers.contacts.CallLogBackupAgent}
- */
-@SmallTest
-public class CallLogBackupAgentTest extends AndroidTestCase {
-
-    @Mock DataInput mDataInput;
-    @Mock DataOutput mDataOutput;
-
-    CallLogBackupAgent mCallLogBackupAgent;
-
-    MockitoHelper mMockitoHelper = new MockitoHelper();
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-
-        mMockitoHelper.setUp(getClass());
-        // Since we're testing a system app, AppDataDirGuesser doesn't find our
-        // cache dir, so set it explicitly.
-        System.setProperty("dexmaker.dexcache", getContext().getCacheDir().toString());
-
-        MockitoAnnotations.initMocks(this);
-
-        mCallLogBackupAgent = new CallLogBackupAgent();
-    }
-
-    @Override
-    public void tearDown() throws Exception {
-        mMockitoHelper.tearDown();
-    }
-
-    public void testReadState_NoCall() throws Exception {
-        when(mDataInput.readInt()).thenThrow(new EOFException());
-
-        CallLogBackupState state = mCallLogBackupAgent.readState(mDataInput);
-
-        assertEquals(state.version, CallLogBackupAgent.VERSION_NO_PREVIOUS_STATE);
-        assertEquals(state.callIds.size(), 0);
-    }
-
-    public void testReadState_OneCall() throws Exception {
-        when(mDataInput.readInt()).thenReturn(
-                1 /* version */,
-                1 /* size */,
-                101 /* call-ID */ );
-
-        CallLogBackupState state = mCallLogBackupAgent.readState(mDataInput);
-
-        assertEquals(1, state.version);
-        assertEquals(1, state.callIds.size());
-        assertTrue(state.callIds.contains(101));
-    }
-
-    public void testReadState_MultipleCalls() throws Exception {
-        when(mDataInput.readInt()).thenReturn(
-                1 /* version */,
-                2 /* size */,
-                101 /* call-ID */,
-                102 /* call-ID */);
-
-        CallLogBackupState state = mCallLogBackupAgent.readState(mDataInput);
-
-        assertEquals(1, state.version);
-        assertEquals(2, state.callIds.size());
-        assertTrue(state.callIds.contains(101));
-        assertTrue(state.callIds.contains(102));
-    }
-
-    public void testWriteState_NoCalls() throws Exception {
-        CallLogBackupState state = new CallLogBackupState();
-        state.version = 1;
-        state.callIds = new TreeSet<>();
-
-        mCallLogBackupAgent.writeState(mDataOutput, state);
-
-        InOrder inOrder = Mockito.inOrder(mDataOutput);
-        inOrder.verify(mDataOutput).writeInt(1 /* version */);
-        inOrder.verify(mDataOutput).writeInt(0 /* size */);
-    }
-
-    public void testWriteState_OneCall() throws Exception {
-        CallLogBackupState state = new CallLogBackupState();
-        state.version = 1;
-        state.callIds = new TreeSet<>();
-        state.callIds.add(101);
-
-        mCallLogBackupAgent.writeState(mDataOutput, state);
-
-        InOrder inOrder = Mockito.inOrder(mDataOutput);
-        inOrder.verify(mDataOutput, times(2)).writeInt(1);
-        inOrder.verify(mDataOutput).writeInt(101 /* call-ID */);
-    }
-
-    public void testWriteState_MultipleCalls() throws Exception {
-        CallLogBackupState state = new CallLogBackupState();
-        state.version = 1;
-        state.callIds = new TreeSet<>();
-        state.callIds.add(101);
-        state.callIds.add(102);
-        state.callIds.add(103);
-
-        mCallLogBackupAgent.writeState(mDataOutput, state);
-
-        InOrder inOrder = Mockito.inOrder(mDataOutput);
-        inOrder.verify(mDataOutput).writeInt(1 /* version */);
-        inOrder.verify(mDataOutput).writeInt(3 /* size */);
-        inOrder.verify(mDataOutput).writeInt(101 /* call-ID */);
-        inOrder.verify(mDataOutput).writeInt(102 /* call-ID */);
-        inOrder.verify(mDataOutput).writeInt(103 /* call-ID */);
-    }
-}
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
index b8233f6..ea436d8 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -60,7 +60,9 @@
             Voicemails.MIME_TYPE,
             Voicemails.SOURCE_PACKAGE,
             Voicemails.SOURCE_DATA,
-            Voicemails.STATE};
+            Voicemails.STATE,
+            Voicemails.DIRTY,
+            Voicemails.DELETED};
     /** Total number of columns exposed by call_log provider. */
     private static final int NUM_CALLLOG_FIELDS = 25;
 
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index b0fb3e9..501f04e 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -307,6 +307,7 @@
                 RawContacts.DATA_SET,
                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
                 RawContacts.SOURCE_ID,
+                RawContacts.BACKUP_ID,
                 RawContacts.VERSION,
                 RawContacts.RAW_CONTACT_IS_USER_PROFILE,
                 RawContacts.DIRTY,
@@ -340,6 +341,7 @@
         assertProjection(Data.CONTENT_URI, new String[]{
                 Data._ID,
                 Data.RAW_CONTACT_ID,
+                Data.HASH_ID,
                 Data.DATA_VERSION,
                 Data.IS_PRIMARY,
                 Data.IS_SUPER_PRIMARY,
@@ -379,6 +381,7 @@
                 RawContacts.DATA_SET,
                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
                 RawContacts.SOURCE_ID,
+                RawContacts.BACKUP_ID,
                 RawContacts.VERSION,
                 RawContacts.DIRTY,
                 RawContacts.RAW_CONTACT_IS_USER_PROFILE,
@@ -543,6 +546,7 @@
                 RawContacts.DATA_SET,
                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
                 RawContacts.SOURCE_ID,
+                RawContacts.BACKUP_ID,
                 RawContacts.VERSION,
                 RawContacts.DELETED,
                 RawContacts.DIRTY,
@@ -602,6 +606,7 @@
                 RawContacts.DATA_SET,
                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
                 RawContacts.SOURCE_ID,
+                RawContacts.BACKUP_ID,
                 RawContacts.VERSION,
                 RawContacts.DIRTY,
                 RawContacts.DELETED,
diff --git a/tests/src/com/android/providers/contacts/MockitoHelper.java b/tests/src/com/android/providers/contacts/MockitoHelper.java
deleted file mode 100644
index 3f3f7f6..0000000
--- a/tests/src/com/android/providers/contacts/MockitoHelper.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.util.Log;
-
-/**
- * Helper for Mockito-based test cases.
- */
-public final class MockitoHelper {
-    private static final String TAG = "MockitoHelper";
-
-    private ClassLoader mOriginalClassLoader;
-    private Thread mContextThread;
-
-    /**
-     * Creates a new helper, which in turn will set the context classloader so
-     * it can load Mockito resources.
-     *
-     * @param packageClass test case class
-     */
-    public void setUp(Class<?> packageClass) throws Exception {
-        // makes a copy of the context classloader
-        mContextThread = Thread.currentThread();
-        mOriginalClassLoader = mContextThread.getContextClassLoader();
-        ClassLoader newClassLoader = packageClass.getClassLoader();
-        Log.v(TAG, "Changing context classloader from " + mOriginalClassLoader
-                + " to " + newClassLoader);
-        mContextThread.setContextClassLoader(newClassLoader);
-    }
-
-    /**
-     * Restores the context classloader to the previous value.
-     */
-    public void tearDown() throws Exception {
-        Log.v(TAG, "Restoring context classloader to " + mOriginalClassLoader);
-        mContextThread.setContextClassLoader(mOriginalClassLoader);
-    }
-}
diff --git a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
index 4fe1907..1d3ac8a 100644
--- a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
@@ -58,7 +58,7 @@
             Calls.COUNTRY_ISO
     };
     /** Total number of columns exposed by voicemail provider. */
-    private static final int NUM_VOICEMAIL_FIELDS = 14;
+    private static final int NUM_VOICEMAIL_FIELDS = 16;
 
     @Override
     protected void setUp() throws Exception {
@@ -120,6 +120,28 @@
         assertStoredValues(uri, values);
     }
 
+    public void testUpdateOwnPackageVoicemail_NotDirty() {
+        final Uri uri = mResolver.insert(voicemailUri(), getTestVoicemailValues());
+        mResolver.update(uri, new ContentValues(), null, null);
+
+        // Updating a package's own voicemail should not make the voicemail dirty.
+        ContentValues values = getTestVoicemailValues();
+        values.put(Voicemails.DIRTY, "0");
+        assertStoredValues(uri, values);
+    }
+
+    public void testUpdateOwnPackageVoicemail_RemovesDirtyStatus() {
+        ContentValues values = getTestVoicemailValues();
+        values.put(Voicemails.DIRTY, "1");
+        final Uri uri = mResolver.insert(voicemailUri(), getTestVoicemailValues());
+
+        mResolver.update(uri, new ContentValues(), null, null);
+        // At this point, the voicemail should be set back to not dirty.
+        ContentValues newValues = getTestVoicemailValues();
+        newValues.put(Voicemails.DIRTY, "0");
+        assertStoredValues(uri, newValues);
+    }
+
     public void testDelete() {
         Uri uri = insertVoicemail();
         int count = mResolver.delete(voicemailUri(), Voicemails._ID + "="
@@ -240,8 +262,10 @@
             }
         });
 
-        // If we have the manage voicemail permission, we should be able to both update and delete
-        // voicemails from all packages
+        // If we have the manage voicemail permission, we should be able to both update voicemails
+        // from all packages. However, when updating or deleting a voicemail from a different
+        // package, the "dirty" flag must be set on updates and "dirty" and "delete" flags must be
+        // set on deletion.
         setUpForNoPermission();
         mActor.addPermissions(WRITE_VOICEMAIL_PERMISSION);
         mResolver.update(anotherVoicemail, getTestVoicemailValues(), null, null);
@@ -254,10 +278,15 @@
 
         mResolver.delete(anotherVoicemail, null, null);
 
-        // Now add the read voicemail permission temporarily to verify that the delete actually
-        // worked
+        // Now add the read voicemail permission temporarily to verify that the delete flag is set.
         mActor.addPermissions(READ_VOICEMAIL_PERMISSION);
-        assertEquals(0, getCount(anotherVoicemail, null, null));
+
+        ContentValues values = getTestVoicemailValues();
+        values.put(Voicemails.DIRTY, "1");
+        values.put(Voicemails.DELETED, "1");
+
+        assertEquals(1, getCount(anotherVoicemail, null, null));
+        assertStoredValues(anotherVoicemail, values);
     }
 
     private Uri withSourcePackageParam(Uri uri) {
diff --git a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
index 204875b..43fc488 100644
--- a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
@@ -1618,6 +1618,74 @@
         cursor.close();
     }
 
+    public void testAggregation_phoneticNamePriority1() {
+        // Setup: one raw contact has a complex phonetic name and the other a simple given name
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertPhoneticName(mResolver, rawContactId1, "name phonetic");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name", ACCOUNT_1);
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: given name is used instead of phonetic, contrary to results of
+        // testAggregation_nameComplexity
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name", queryDisplayName(contactId));
+    }
+
+    // Same as testAggregation_phoneticNamePriority1, but with setup order reversed
+    public void testAggregation_phoneticNamePriority2() {
+        // Setup: one raw contact has a complex phonetic name and the other a simple given name
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertPhoneticName(mResolver, rawContactId1, "name phonetic");
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: given name is used instead of phonetic, contrary to results of
+        // testAggregation_nameComplexity
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name", queryDisplayName(contactId));
+    }
+
+    public void testAggregation_nameComplexity1() {
+        // Setup: two names, one of which is unambiguously more complex
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name phonetic", ACCOUNT_1);
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: more complex name is used
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name phonetic", queryDisplayName(contactId));
+    }
+
+    // Same as testAggregation_nameComplexity1, but with setup order reversed
+    public void testAggregation_nameComplexity2() {
+        // Setup: two names, one of which is unambiguously more complex
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name phonetic", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, null,
+                "name", ACCOUNT_1);
+
+        // Action: aggregate
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+                rawContactId2);
+
+        // Verify: more complex name is used
+        long contactId = queryContactId(rawContactId1);
+        assertEquals("name phonetic", queryDisplayName(contactId));
+    }
+
     public void testAggregation_clearSuperPrimary() {
         // Three types of mime-type super primary merging are tested here
         // 1. both raw contacts have super primary phone numbers
diff --git a/tests/src/com/android/providers/contacts/testutil/DataUtil.java b/tests/src/com/android/providers/contacts/testutil/DataUtil.java
index 1f4f35a..2afd567 100644
--- a/tests/src/com/android/providers/contacts/testutil/DataUtil.java
+++ b/tests/src/com/android/providers/contacts/testutil/DataUtil.java
@@ -59,14 +59,14 @@
 
     public static Uri insertStructuredName(
             ContentResolver resolver, long rawContactId, String givenName, String familyName,
-            String phoneticGiven) {
-        return insertStructuredName(resolver, rawContactId, givenName, familyName, phoneticGiven,
+            String phoneticFamily) {
+        return insertStructuredName(resolver, rawContactId, givenName, familyName, phoneticFamily,
                 /* isSuperPrimary = true */ false);
     }
 
     public static Uri insertStructuredName(
             ContentResolver resolver, long rawContactId, String givenName, String familyName,
-            String phoneticGiven, boolean isSuperPrimary) {
+            String phoneticFamily, boolean isSuperPrimary) {
         ContentValues values = new ContentValues();
         StringBuilder sb = new StringBuilder();
         if (givenName != null) {
@@ -78,14 +78,16 @@
         if (familyName != null) {
             sb.append(familyName);
         }
-        if (sb.length() == 0 && phoneticGiven != null) {
-            sb.append(phoneticGiven);
+        if (sb.length() == 0 && phoneticFamily != null) {
+            sb.append(phoneticFamily);
         }
         values.put(StructuredName.DISPLAY_NAME, sb.toString());
         values.put(StructuredName.GIVEN_NAME, givenName);
         values.put(StructuredName.FAMILY_NAME, familyName);
-        if (phoneticGiven != null) {
-            values.put(StructuredName.PHONETIC_GIVEN_NAME, phoneticGiven);
+        if (phoneticFamily != null) {
+            // When creating phonetic names, be careful to use PHONETIC_FAMILY_NAME instead of
+            // PHONETIC_GIVEN_NAME, to work around b/19612393.
+            values.put(StructuredName.PHONETIC_FAMILY_NAME, phoneticFamily);
         }
         if (isSuperPrimary) {
             values.put(Data.IS_PRIMARY, 1);
@@ -94,4 +96,13 @@
 
         return insertStructuredName(resolver, rawContactId, values);
     }
+
+    public static Uri insertPhoneticName(ContentResolver resolver, long rawContactId,
+            String phoneticFamilyName) {
+        ContentValues values = new ContentValues();
+        // When creating phonetic names, be careful to use PHONETIC_FAMILY_NAME instead of
+        // PHONETIC_GIVEN_NAME, to work around b/19612393.
+        values.put(StructuredName.PHONETIC_FAMILY_NAME, phoneticFamilyName);
+        return insertStructuredName(resolver, rawContactId, values);
+    }
 }
\ No newline at end of file