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