am e21d4ae9: (-s ours) Import revised translations. DO NOT MERGE
Merge commit 'e21d4ae92d89e1d3999fbf0edb71f99ce71c7740'
* commit 'e21d4ae92d89e1d3999fbf0edb71f99ce71c7740':
Import revised translations. DO NOT MERGE
diff --git a/Android.mk b/Android.mk
index b13e757..e963f99 100644
--- a/Android.mk
+++ b/Android.mk
@@ -3,7 +3,8 @@
LOCAL_MODULE_TAGS := user
-LOCAL_SRC_FILES := $(call all-subdir-java-files)
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JAVA_LIBRARIES := ext
@@ -11,3 +12,6 @@
LOCAL_CERTIFICATE := shared
include $(BUILD_PACKAGE)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8f967d2..d7de1fb 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -7,20 +7,50 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.cp" />
<uses-permission android:name="android.permission.SUBSCRIBED_FEEDS_READ" />
<uses-permission android:name="android.permission.SUBSCRIBED_FEEDS_WRITE" />
<application android:process="android.process.acore"
- android:label="@string/app_label"
- android:icon="@drawable/app_icon">
- <provider android:name="ContactsProvider" android:authorities="contacts;call_log"
- android:syncable="false" android:multiprocess="false"
- android:readPermission="android.permission.READ_CONTACTS"
- android:writePermission="android.permission.WRITE_CONTACTS">
- <path-permission android:path="/people/search_suggest_query"
+ android:label="@string/app_label"
+ android:icon="@drawable/app_icon">
+
+ <provider android:name="ContactsProvider"
+ android:authorities="contacts"
+ android:syncable="false" android:multiprocess="false"
+ android:readPermission="android.permission.READ_CONTACTS"
+ android:writePermission="android.permission.WRITE_CONTACTS">
+ <path-permission
+ android:path="/people/search_suggest_query"
android:readPermission="android.permission.GLOBAL_SEARCH" />
</provider>
+
+ <provider android:name="ContactsProvider2"
+ android:authorities="com.android.contacts"
+ android:syncable="false"
+ android:multiprocess="false"
+ android:readPermission="android.permission.READ_CONTACTS"
+ android:writePermission="android.permission.WRITE_CONTACTS">
+ <path-permission
+ android:path="/contacts/search_suggest_query"
+ android:readPermission="android.permission.GLOBAL_SEARCH" />
+ </provider>
+
+ <provider android:name="CallLogProvider"
+ android:authorities="call_log"
+ android:syncable="false" android:multiprocess="false"
+ android:readPermission="android.permission.READ_CONTACTS"
+ android:writePermission="android.permission.WRITE_CONTACTS">
+ </provider>
+
+ <!-- TODO: create permissions for social data -->
+ <provider android:name="SocialProvider"
+ android:authorities="com.android.social"
+ android:syncable="false"
+ android:multiprocess="false"
+ android:readPermission="android.permission.READ_CONTACTS"
+ android:writePermission="android.permission.WRITE_CONTACTS" />
</application>
</manifest>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index b46efd6..3602cb5 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Úložiště kontaktů"</string>
+ <string name="app_label">"Úložiště kontaktů"</string>
</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 2bc0da7..ed45745 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Lagring af kontakter"</string>
+ <string name="app_label">"Lagring af kontaktpersoner"</string>
</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 23c39c5..1543aaa 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Kontakte-Speicher"</string>
+ <string name="app_label">"Kontakte-Speicher"</string>
</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 65f8e7a..f948f23 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Χώρος αποθ.επαφ."</string>
+ <string name="app_label">"Χώρος αποθήκευσης επαφών"</string>
</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 79d73b0..e76c6a1 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Espacio de almacenamiento para Contactos"</string>
+ <string name="app_label">"Espacio de almacenamiento para Contactos"</string>
</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index f57b236..d228322 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Información de los contactos"</string>
+ <string name="app_label">"Información de los contactos"</string>
</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index ccca354..8417247 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Liste des contacts"</string>
+ <string name="app_label">"Liste des contacts"</string>
</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 844bc7c..07e72ae 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Archiviazione contatti"</string>
+ <string name="app_label">"Archiviazione contatti"</string>
</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 8036c32..b7c79a8 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"アドレス帳"</string>
+ <string name="app_label">"アドレス帳"</string>
</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index a35960c..f5f37d6 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"주소록 저장소"</string>
+ <string name="app_label">"주소록 저장소"</string>
</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index ce6608c..3c3f3d6 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Opslag contacten"</string>
+ <string name="app_label">"Opslag contacten"</string>
</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 6310ac1..fc4e7d8 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Spis kontaktów"</string>
+ <string name="app_label">"Spis kontaktów"</string>
</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index a7b33a3..412fc5b 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Armazenamento de contactos"</string>
+ <string name="app_label">"Armazenamento de contactos"</string>
</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 78f4294..16c4fd8 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Armazenamento de contatos"</string>
+ <string name="app_label">"Armazenamento de contatos"</string>
</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 277674d..f57220f 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Хранилище контактов"</string>
+ <string name="app_label">"Хранилище контактов"</string>
</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 166f5bb..ded539c 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Kontakter"</string>
+ <string name="app_label">"Kontakter"</string>
</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 55ac9fd..37f559e 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"Kişi Deposu"</string>
+ <string name="app_label">"Kişi Deposu"</string>
</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index f896f7b..205731f 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"联系人存储"</string>
+ <string name="app_label">"联系人存储"</string>
</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index d0216b6..82b0155 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_label" msgid="3389954322874982620">"聯絡人儲存空間"</string>
+ <string name="app_label">"聯絡人儲存空間"</string>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 45637dd..ac119ed 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 The Android Open Source Project
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +14,8 @@
limitations under the License.
-->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- This is the label for the application that stores contacts data -->
<string name="app_label">Contacts Storage</string>
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
new file mode 100644
index 0000000..fbda970
--- /dev/null
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts;
+
+import com.android.providers.contacts.OpenHelper.Tables;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+
+import java.util.HashMap;
+
+/**
+ * Call log content provider.
+ */
+public class CallLogProvider extends ContentProvider {
+
+ private static final int CALLS = 1;
+
+ private static final int CALLS_ID = 2;
+
+ private static final int CALLS_FILTER = 3;
+
+ private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ static {
+ sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
+ sURIMatcher.addURI(CallLog.AUTHORITY, "calls/#", CALLS_ID);
+ sURIMatcher.addURI(CallLog.AUTHORITY, "calls/filter/*", CALLS_FILTER);
+ }
+
+ private static final HashMap<String, String> sCallsProjectionMap;
+ static {
+
+ // Calls projection map
+ sCallsProjectionMap = new HashMap<String, String>();
+ sCallsProjectionMap.put(Calls._ID, Calls._ID);
+ sCallsProjectionMap.put(Calls.NUMBER, Calls.NUMBER);
+ sCallsProjectionMap.put(Calls.DATE, Calls.DATE);
+ sCallsProjectionMap.put(Calls.DURATION, Calls.DURATION);
+ sCallsProjectionMap.put(Calls.TYPE, Calls.TYPE);
+ sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
+ sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
+ sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
+ sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
+ }
+
+ private OpenHelper mOpenHelper;
+ private DatabaseUtils.InsertHelper mCallsInserter;
+
+ @Override
+ public boolean onCreate() {
+ final Context context = getContext();
+
+ mOpenHelper = getOpenHelper(context);
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ mCallsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALLS);
+
+ return true;
+ }
+
+ /* Visible for testing */
+ protected OpenHelper getOpenHelper(final Context context) {
+ return OpenHelper.getInstance(context);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ int match = sURIMatcher.match(uri);
+ switch (match) {
+ case CALLS: {
+ qb.setTables("calls");
+ qb.setProjectionMap(sCallsProjectionMap);
+ break;
+ }
+
+ case CALLS_ID: {
+ qb.setTables("calls");
+ qb.setProjectionMap(sCallsProjectionMap);
+ qb.appendWhere("calls._id=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+ }
+
+ case CALLS_FILTER: {
+ qb.setTables("calls");
+ qb.setProjectionMap(sCallsProjectionMap);
+ String phoneNumber = uri.getPathSegments().get(2);
+ qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
+ qb.appendWhereEscapeString(phoneNumber);
+ qb.appendWhere(")");
+ break;
+ }
+
+ default:
+ throw new IllegalArgumentException("Unknown URL " + uri);
+ }
+
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, null);
+ if (c != null) {
+ c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI);
+ }
+ return c;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ int match = sURIMatcher.match(uri);
+ switch (match) {
+ case CALLS:
+ return Calls.CONTENT_TYPE;
+ case CALLS_ID:
+ return Calls.CONTENT_ITEM_TYPE;
+ case CALLS_FILTER:
+ return Calls.CONTENT_TYPE;
+ default:
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ long rowId = mCallsInserter.insert(values);
+ if (rowId > 0) {
+ return ContentUris.withAppendedId(uri, rowId);
+ }
+ return null;
+ }
+
+ @Override
+ public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ String where;
+ final int matchedUriId = sURIMatcher.match(url);
+ switch (matchedUriId) {
+ case CALLS:
+ where = selection;
+ break;
+
+ case CALLS_ID:
+ where = DatabaseUtils.concatenateWhere(selection, Calls._ID + "="
+ + url.getPathSegments().get(1));
+ break;
+
+ default:
+ throw new UnsupportedOperationException("Cannot update URL: " + url);
+ }
+
+ return db.update(Tables.CALLS, values, where, selectionArgs);
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ final int matchedUriId = sURIMatcher.match(uri);
+ switch (matchedUriId) {
+ case CALLS:
+ return db.delete(Tables.CALLS, selection, selectionArgs);
+
+ default:
+ throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
+ }
+ }
+}
diff --git a/src/com/android/providers/contacts/ContactAggregationScheduler.java b/src/com/android/providers/contacts/ContactAggregationScheduler.java
new file mode 100644
index 0000000..59bb8cd
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactAggregationScheduler.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+
+/**
+ * A scheduler for asynchronous aggregation of contacts. Aggregation will start after
+ * a short delay after it is scheduled, unless it is scheduled again, in which case the
+ * aggregation pass is delayed. There is an upper boundary on how long aggregation can
+ * be delayed.
+ */
+public class ContactAggregationScheduler {
+
+ public interface Aggregator {
+
+ /**
+ * Performs an aggregation run.
+ */
+ void run();
+
+ /**
+ * Interrupts aggregation.
+ */
+ void interrupt();
+ }
+
+ // Message ID used for communication with the aggregator
+ private static final int START_AGGREGATION_MESSAGE_ID = 1;
+
+ // Aggregation is delayed by this many milliseconds to allow changes to accumulate
+ public static final int AGGREGATION_DELAY = 500;
+
+ // Maximum delay of aggregation from the initial aggregation request
+ public static final int MAX_AGGREGATION_DELAY = 5000;
+
+ public static final int STATUS_STAND_BY = 0;
+ public static final int STATUS_SCHEDULED = 1;
+ public static final int STATUS_RUNNING = 2;
+
+ private Aggregator mAggregator;
+
+ // Aggregation status
+ private int mStatus = STATUS_STAND_BY;
+
+ // If true, we need to automatically reschedule aggregation after the current pass is done
+ private boolean mRescheduleWhenComplete;
+
+ // The time when aggregation was request for the first time. Reset when aggregation is completed
+ private long mInitialRequestTimestamp;
+
+ private HandlerThread mHandlerThread;
+ private Handler mMessageHandler;
+
+ public void setAggregator(Aggregator aggregator) {
+ mAggregator = aggregator;
+ }
+
+ public void start() {
+ mHandlerThread = new HandlerThread("ContactAggregator", Process.THREAD_PRIORITY_BACKGROUND);
+ mHandlerThread.start();
+ mMessageHandler = new Handler(mHandlerThread.getLooper()) {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case START_AGGREGATION_MESSAGE_ID:
+ run();
+ break;
+
+ default:
+ throw new IllegalStateException("Unhandled message: " + msg.what);
+ }
+ }
+ };
+ }
+
+ public void stop() {
+ mAggregator.interrupt();
+ Looper looper = mHandlerThread.getLooper();
+ if (looper != null) {
+ looper.quit();
+ }
+ }
+
+ /**
+ * Schedules an aggregation pass after a short delay.
+ */
+ public synchronized void schedule() {
+
+ switch (mStatus) {
+ case STATUS_STAND_BY: {
+
+ mInitialRequestTimestamp = currentTime();
+ mStatus = STATUS_SCHEDULED;
+ runDelayed();
+ break;
+ }
+
+ case STATUS_SCHEDULED: {
+
+ // If it has been less than MAX_AGGREGATION_DELAY millis since the initial request,
+ // reschedule the request.
+ if (currentTime() - mInitialRequestTimestamp < MAX_AGGREGATION_DELAY) {
+ runDelayed();
+ }
+ break;
+ }
+
+ case STATUS_RUNNING: {
+
+ // If it has been less than MAX_AGGREGATION_DELAY millis since the initial request,
+ // interrupt the current pass and reschedule the request.
+ if (currentTime() - mInitialRequestTimestamp < MAX_AGGREGATION_DELAY) {
+ mAggregator.interrupt();
+ }
+
+ mRescheduleWhenComplete = true;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Called just before an aggregation pass begins.
+ */
+ public void run() {
+ synchronized (this) {
+ mStatus = STATUS_RUNNING;
+ mRescheduleWhenComplete = false;
+ }
+ try {
+ mAggregator.run();
+ } finally {
+ synchronized (this) {
+ mStatus = STATUS_STAND_BY;
+ mInitialRequestTimestamp = 0;
+ if (mRescheduleWhenComplete) {
+ mRescheduleWhenComplete = false;
+ schedule();
+ }
+ }
+ }
+ }
+
+ /* package */ void runDelayed() {
+
+ // If aggregation has already been requested, cancel the previous request
+ mMessageHandler.removeMessages(START_AGGREGATION_MESSAGE_ID);
+
+ // Schedule aggregation for AGGREGATION_DELAY milliseconds from now
+ mMessageHandler.sendEmptyMessageDelayed(
+ START_AGGREGATION_MESSAGE_ID, AGGREGATION_DELAY);
+ }
+
+ /* package */ long currentTime() {
+ return System.currentTimeMillis();
+ }
+}
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java
new file mode 100644
index 0000000..ad70640
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -0,0 +1,1226 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts;
+
+import com.android.providers.contacts.ContactMatcher.MatchScore;
+import com.android.providers.contacts.OpenHelper.AggregationExceptionColumns;
+import com.android.providers.contacts.OpenHelper.Clauses;
+import com.android.providers.contacts.OpenHelper.ContactsColumns;
+import com.android.providers.contacts.OpenHelper.MimetypesColumns;
+import com.android.providers.contacts.OpenHelper.NameLookupColumns;
+import com.android.providers.contacts.OpenHelper.NameLookupType;
+import com.android.providers.contacts.OpenHelper.RawContactsColumns;
+import com.android.providers.contacts.OpenHelper.Tables;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+
+/**
+ * ContactAggregator deals with aggregating contact information coming from different sources.
+ * Two John Doe contacts from two disjoint sources are presumed to be the same
+ * person unless the user declares otherwise.
+ * <p>
+ * ContactAggregator runs on a separate thread.
+ */
+public class ContactAggregator implements ContactAggregationScheduler.Aggregator {
+
+ private static final String TAG = "ContactAggregator";
+
+ // Data mime types used in the contact matching algorithm
+ private static final String MIMETYPE_SELECTION_IN_CLAUSE = MimetypesColumns.MIMETYPE + " IN ('"
+ + Email.CONTENT_ITEM_TYPE + "','"
+ + Nickname.CONTENT_ITEM_TYPE + "','"
+ + Phone.CONTENT_ITEM_TYPE + "','"
+ + StructuredName.CONTENT_ITEM_TYPE + "')";
+
+ private static final String[] DATA_JOIN_MIMETYPE_COLUMNS = new String[] {
+ MimetypesColumns.MIMETYPE,
+ Data.DATA1,
+ Data.DATA2
+ };
+
+ private static final int COL_MIMETYPE = 0;
+ private static final int COL_DATA1 = 1;
+ private static final int COL_DATA2 = 2;
+
+ private static final String[] DATA_JOIN_MIMETYPE_AND_CONTACT_COLUMNS = new String[] {
+ Data.DATA1, Data.DATA2, RawContacts.CONTACT_ID
+ };
+
+ private static final int COL_DATA_CONTACT_DATA1 = 0;
+ private static final int COL_DATA_CONTACT_DATA2 = 1;
+ private static final int COL_DATA_CONTACT_CONTACT_ID = 2;
+
+ private static final String[] NAME_LOOKUP_COLUMNS = new String[] {
+ RawContacts.CONTACT_ID, NameLookupColumns.NORMALIZED_NAME, NameLookupColumns.NAME_TYPE
+ };
+
+ private static final int COL_NAME_LOOKUP_CONTACT_ID = 0;
+ private static final int COL_NORMALIZED_NAME = 1;
+ private static final int COL_NAME_TYPE = 2;
+
+ private static final String[] AGGREGATE_EXCEPTION_JOIN_CONTACT_TWICE_COLUMNS = new String[]{
+ AggregationExceptions.TYPE,
+ AggregationExceptionColumns.RAW_CONTACT_ID1,
+ "raw_contacts1." + RawContacts.CONTACT_ID,
+ "raw_contacts2." + RawContacts.CONTACT_ID
+ };
+
+ private static final int COL_TYPE = 0;
+ private static final int COL_RAW_CONTACT_ID1 = 1;
+ private static final int COL_CONTACT_ID1 = 2;
+ private static final int COL_CONTACT_ID2 = 3;
+
+ private static final String[] CONTACT_ID_COLUMN = new String[] { RawContacts._ID };
+
+ private static final String[] CONTACT_OPTIONS_COLUMNS = new String[] {
+ RawContacts.CUSTOM_RINGTONE,
+ RawContacts.SEND_TO_VOICEMAIL,
+ RawContacts.LAST_TIME_CONTACTED,
+ RawContacts.TIMES_CONTACTED,
+ RawContacts.STARRED,
+ };
+
+ private static final int COL_CUSTOM_RINGTONE = 0;
+ private static final int COL_SEND_TO_VOICEMAIL = 1;
+ private static final int COL_LAST_TIME_CONTACTED = 2;
+ private static final int COL_TIMES_CONTACTED = 3;
+ private static final int COL_STARRED = 4;
+
+ private static final String[] CONTACT_ID_COLUMNS = new String[]{ RawContacts.CONTACT_ID };
+ private static final int COL_CONTACT_ID = 0;
+
+ private static final int MODE_INSERT_LOOKUP_DATA = 0;
+ private static final int MODE_AGGREGATION = 1;
+ private static final int MODE_SUGGESTIONS = 2;
+
+ private final OpenHelper mOpenHelper;
+ private final ContactAggregationScheduler mScheduler;
+
+ // Set if the current aggregation pass should be interrupted
+ private volatile boolean mCancel;
+
+ /**
+ * Captures a potential match for a given name. The matching algorithm
+ * constructs a bunch of NameMatchCandidate objects for various potential matches
+ * and then executes the search in bulk.
+ */
+ private static class NameMatchCandidate {
+ String mName;
+ int mLookupType;
+
+ public NameMatchCandidate(String name, int nameLookupType) {
+ mName = name;
+ mLookupType = nameLookupType;
+ }
+ }
+
+ /**
+ * A list of {@link NameMatchCandidate} that keeps its elements even when the list is
+ * truncated. This is done for optimization purposes to avoid excessive object allocation.
+ */
+ private static class MatchCandidateList {
+ private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>();
+ private int mCount;
+
+ /**
+ * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists.
+ */
+ public void add(String name, int nameLookupType) {
+ if (mCount >= mList.size()) {
+ mList.add(new NameMatchCandidate(name, nameLookupType));
+ } else {
+ NameMatchCandidate candidate = mList.get(mCount);
+ candidate.mName = name;
+ candidate.mLookupType = nameLookupType;
+ }
+ mCount++;
+ }
+
+ public void clear() {
+ mCount = 0;
+ }
+ }
+
+ /**
+ * Constructor. Starts a contact aggregation thread. Call {@link #quit} to kill the
+ * aggregation thread. Call {@link #schedule} to kick off the aggregation process after
+ * a delay of {@link #AGGREGATION_DELAY} milliseconds.
+ */
+ public ContactAggregator(Context context, OpenHelper openHelper,
+ ContactAggregationScheduler scheduler) {
+ mOpenHelper = openHelper;
+ mScheduler = scheduler;
+ mScheduler.setAggregator(this);
+ mScheduler.start();
+
+ // Perform an aggregation pass in the beginning, which will most of the time
+ // do nothing. It will only be useful if the content provider has been killed
+ // before completing aggregation.
+ mScheduler.schedule();
+ }
+
+ /**
+ * Schedules aggregation pass after a short delay. This method should be called every time
+ * the {@link RawContacts#CONTACT_ID} field is reset on any record.
+ */
+ public void schedule() {
+ mScheduler.schedule();
+ }
+
+ /**
+ * Kills the contact aggregation thread.
+ */
+ public void quit() {
+ mScheduler.stop();
+ }
+
+ /**
+ * Invoked by the scheduler to cancel aggregation.
+ */
+ public void interrupt() {
+ mCancel = true;
+ }
+
+ /**
+ * Find all contacts that require aggregation and pass them through aggregation one by one.
+ * Do not call directly. It is invoked by the scheduler.
+ */
+ public void run() {
+ mCancel = false;
+ Log.i(TAG, "Contact aggregation");
+
+ MatchCandidateList candidates = new MatchCandidateList();
+ ContactMatcher matcher = new ContactMatcher();
+ ContentValues values = new ContentValues();
+
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ final Cursor c = db.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
+ RawContacts.CONTACT_ID + " IS NULL AND "
+ + RawContacts.AGGREGATION_MODE + "=" + RawContacts.AGGREGATION_MODE_DEFAULT,
+ null, null, null, null);
+
+ int totalCount = c.getCount();
+ int count = 0;
+ try {
+ if (c.moveToFirst()) {
+ db.beginTransaction();
+ try {
+ do {
+ if (mCancel) {
+ break;
+ }
+ aggregateContact(db, c.getInt(0), candidates, matcher, values);
+ count++;
+ db.yieldIfContendedSafely();
+ } while (c.moveToNext());
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+ } finally {
+ c.close();
+
+ // Unless the aggregation pass was not interrupted, reset the last request timestamp
+ if (count == totalCount) {
+ Log.i(TAG, "Contact aggregation complete: " + totalCount);
+ } else {
+ Log.i(TAG, "Contact aggregation interrupted: " + count + "/" + totalCount);
+ }
+ }
+ }
+
+ /**
+ * Synchronously aggregate the specified contact.
+ */
+ public void aggregateContact(long rawContactId) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ aggregateContact(db, rawContactId);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Synchronously aggregate the specified contact assuming an open transaction.
+ */
+ public void aggregateContact(SQLiteDatabase db, long rawContactId) {
+ MatchCandidateList candidates = new MatchCandidateList();
+ ContactMatcher matcher = new ContactMatcher();
+ ContentValues values = new ContentValues();
+ aggregateContact(db, rawContactId, candidates, matcher, values);
+ }
+
+ /**
+ * Marks the specified contact for (re)aggregation.
+ *
+ * @param rawContactId contact ID that needs to be (re)aggregated
+ * @return The contact aggregation mode:
+ * {@link RawContacts#AGGREGATION_MODE_DEFAULT},
+ * {@link RawContacts#AGGREGATION_MODE_IMMEDIATE} or
+ * {@link RawContacts#AGGREGATION_MODE_DISABLED}.
+ */
+ public int markContactForAggregation(long rawContactId) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ int aggregationMode = mOpenHelper.getAggregationMode(rawContactId);
+ if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
+ return aggregationMode;
+ }
+
+ long contactId = mOpenHelper.getContactId(rawContactId);
+ if (contactId == 0) {
+ // Not aggregated
+ return aggregationMode;
+ }
+
+ mOpenHelper.removeContactIfSingleton(rawContactId);
+
+ // TODO compiled statements
+
+ // Clear out data used for aggregation - we will recreate it during aggregation
+ db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE "
+ + NameLookupColumns.RAW_CONTACT_ID + "=" + rawContactId);
+
+ // Clear out the contact ID field on the contact
+ ContentValues values = new ContentValues();
+ values.putNull(RawContacts.CONTACT_ID);
+ db.update(Tables.RAW_CONTACTS, values, RawContacts._ID + "=" + rawContactId, null);
+
+ return aggregationMode;
+ }
+
+ public void updateAggregateData(long contactId) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ final ContentValues values = new ContentValues();
+ updateAggregateData(db, contactId, values);
+ }
+
+ /**
+ * Given a specific raw contact, finds all matching aggregate contacts and chooses the one
+ * with the highest match score. If no such contact is found, creates a new contact.
+ */
+ /* package */ synchronized void aggregateContact(SQLiteDatabase db, long rawContactId,
+ MatchCandidateList candidates, ContactMatcher matcher, ContentValues values) {
+ candidates.clear();
+ matcher.clear();
+
+ long contactId = pickBestMatchBasedOnExceptions(db, rawContactId, matcher);
+ if (contactId == -1) {
+ contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher);
+ }
+
+ boolean newAgg = false;
+
+ if (contactId == -1) {
+ newAgg = true;
+ ContentValues contactValues = new ContentValues();
+ contactValues.put(Contacts.DISPLAY_NAME, "");
+ contactId = db.insert(Tables.CONTACTS, Contacts.DISPLAY_NAME, contactValues);
+ }
+
+ updateContactAggregationData(db, rawContactId, candidates, values);
+ mOpenHelper.setContactId(rawContactId, contactId);
+
+ updateAggregateData(db, contactId, values);
+ updatePrimaries(db, contactId, rawContactId, newAgg);
+ mOpenHelper.updateContactVisible(contactId);
+
+ }
+
+ /**
+ * Computes match scores based on exceptions entered by the user: always match and never match.
+ * Returns the aggregate contact with the always match exception if any.
+ */
+ private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId,
+ ContactMatcher matcher) {
+ final Cursor c = db.query(Tables.AGGREGATION_EXCEPTIONS_JOIN_RAW_CONTACTS_TWICE,
+ AGGREGATE_EXCEPTION_JOIN_CONTACT_TWICE_COLUMNS,
+ AggregationExceptionColumns.RAW_CONTACT_ID1 + "=" + rawContactId
+ + " OR " + AggregationExceptionColumns.RAW_CONTACT_ID2 + "=" + rawContactId,
+ null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ int type = c.getInt(COL_TYPE);
+ long rawContactId1 = c.getLong(COL_RAW_CONTACT_ID1);
+ long contactId = -1;
+ if (rawContactId == rawContactId1) {
+ if (!c.isNull(COL_CONTACT_ID2)) {
+ contactId = c.getLong(COL_CONTACT_ID2);
+ }
+ } else {
+ if (!c.isNull(COL_CONTACT_ID1)) {
+ contactId = c.getLong(COL_CONTACT_ID1);
+ }
+ }
+ if (contactId != -1) {
+ if (type == AggregationExceptions.TYPE_KEEP_IN) {
+ return contactId;
+ } else {
+ matcher.keepOut(contactId);
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ return -1;
+ }
+
+ /**
+ * Picks the best matching contact based on matches between data elements. It considers
+ * name match to be primary and phone, email etc matches to be secondary. A good primary
+ * match triggers aggregation, while a good secondary match only triggers aggregation in
+ * the absence of a strong primary mismatch.
+ * <p>
+ * Consider these examples:
+ * <p>
+ * John Doe with phone number 111-111-1111 and Jon Doe with phone number 111-111-1111 should
+ * be aggregated (same number, similar names).
+ * <p>
+ * John Doe with phone number 111-111-1111 and Deborah Doe with phone number 111-111-1111 should
+ * not be aggregated (same number, different names).
+ */
+ private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId,
+ MatchCandidateList candidates, ContactMatcher matcher) {
+
+ updateMatchScoresBasedOnDataMatches(db, rawContactId, MODE_AGGREGATION, candidates, matcher);
+
+ // See if we have already found a good match based on name matches alone
+ long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+ if (bestMatch == -1) {
+ // We haven't found a good match on name, see if we have any matches on phone, email etc
+ bestMatch = pickBestMatchBasedOnSecondaryData(db, candidates, matcher);
+ }
+
+ return bestMatch;
+ }
+
+ /**
+ * Picks the best matching contact based on secondary data matches. The method loads
+ * structured names for all candidate contacts and recomputes match scores using approximate
+ * matching.
+ */
+ private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db,
+ MatchCandidateList candidates, ContactMatcher matcher) {
+ List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates(
+ ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+ if (secondaryContactIds == null) {
+ return -1;
+ }
+
+ StringBuilder selection = new StringBuilder();
+ selection.append(RawContacts.CONTACT_ID).append(" IN (");
+ for (int i = 0; i < secondaryContactIds.size(); i++) {
+ if (i != 0) {
+ selection.append(',');
+ }
+ selection.append(secondaryContactIds.get(i));
+ }
+ selection.append(") AND " + MimetypesColumns.MIMETYPE + "='"
+ + StructuredName.CONTENT_ITEM_TYPE + "'");
+
+ final Cursor c = db.query(Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS,
+ DATA_JOIN_MIMETYPE_AND_CONTACT_COLUMNS,
+ selection.toString(), null, null, null, null);
+
+ MatchCandidateList nameCandidates = new MatchCandidateList();
+ try {
+ while (c.moveToNext()) {
+ String givenName = c.getString(COL_DATA_CONTACT_DATA1);
+ String familyName = c.getString(COL_DATA_CONTACT_DATA2);
+ long contactId = c.getLong(COL_DATA_CONTACT_CONTACT_ID);
+
+ nameCandidates.clear();
+ addMatchCandidatesStructuredName(givenName, familyName, MODE_INSERT_LOOKUP_DATA,
+ nameCandidates);
+
+ // Note the N^2 complexity of the following fragment. This is not a huge concern
+ // since the number of candidates is very small and in general secondary hits
+ // in the absence of primary hits are rare.
+ for (int i = 0; i < candidates.mCount; i++) {
+ NameMatchCandidate candidate = candidates.mList.get(i);
+
+ // We only want to compare structured names to structured names
+ // at this stage, we need to ignore all other sources of name lookup data.
+ if (NameLookupType.isBasedOnStructuredName(candidate.mLookupType)) {
+ for (int j = 0; j < nameCandidates.mCount; j++) {
+ NameMatchCandidate nameCandidate = nameCandidates.mList.get(j);
+ matcher.matchName(contactId,
+ nameCandidate.mLookupType, nameCandidate.mName,
+ candidate.mLookupType, candidate.mName, true);
+ }
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
+ }
+
+ /**
+ * Computes scores for contacts that have matching data rows.
+ */
+ private void updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
+ int mode, MatchCandidateList candidates, ContactMatcher matcher) {
+
+ final Cursor c = db.query(Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS,
+ DATA_JOIN_MIMETYPE_COLUMNS,
+ Data.RAW_CONTACT_ID + "=" + rawContactId + " AND ("
+ + MIMETYPE_SELECTION_IN_CLAUSE + ")",
+ null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ String mimeType = c.getString(COL_MIMETYPE);
+ String data1 = c.getString(COL_DATA1);
+ String data2 = c.getString(COL_DATA2);
+ if (mimeType.equals(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) {
+ addMatchCandidatesStructuredName(data1, data2, mode, candidates);
+ } else if (mimeType.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
+ addMatchCandidatesEmail(data2, mode, candidates);
+ lookupEmailMatches(db, data2, matcher);
+ } else if (mimeType.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
+ lookupPhoneMatches(db, data2, matcher);
+ } else if (mimeType.equals(CommonDataKinds.Nickname.CONTENT_ITEM_TYPE)) {
+ addMatchCandidatesNickname(data2, mode, candidates);
+ lookupNicknameMatches(db, data2, matcher);
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ lookupNameMatches(db, candidates, matcher);
+
+ if (mode == MODE_SUGGESTIONS) {
+ lookupApproximateNameMatches(db, candidates, matcher);
+ }
+ }
+
+ /**
+ * Looks for matches based on the full name (first + last).
+ */
+ private void addMatchCandidatesStructuredName(String givenName, String familyName, int mode,
+ MatchCandidateList candidates) {
+ if (TextUtils.isEmpty(givenName)) {
+
+ // If neither the first nor last name are specified, we won't aggregate
+ if (TextUtils.isEmpty(familyName)) {
+ return;
+ }
+
+ addMatchCandidatesFamilyNameOnly(familyName, candidates);
+ } else if (TextUtils.isEmpty(familyName)) {
+ addMatchCandidatesGivenNameOnly(givenName, candidates);
+ } else {
+ addMatchCandidatesFullName(givenName, familyName, mode, candidates);
+ }
+ }
+
+ private void addMatchCandidatesGivenNameOnly(String givenName,
+ MatchCandidateList candidates) {
+ String givenNameN = NameNormalizer.normalize(givenName);
+ candidates.add(givenNameN, NameLookupType.GIVEN_NAME_ONLY);
+
+ String[] clusters = mOpenHelper.getCommonNicknameClusters(givenNameN);
+ if (clusters != null) {
+ for (int i = 0; i < clusters.length; i++) {
+ candidates.add(clusters[i], NameLookupType.GIVEN_NAME_ONLY_AS_NICKNAME);
+ }
+ }
+ }
+
+ private void addMatchCandidatesFamilyNameOnly(String familyName,
+ MatchCandidateList candidates) {
+ String familyNameN = NameNormalizer.normalize(familyName);
+ candidates.add(familyNameN, NameLookupType.FAMILY_NAME_ONLY);
+
+ // Take care of first and last names swapped
+ String[] clusters = mOpenHelper.getCommonNicknameClusters(familyNameN);
+ if (clusters != null) {
+ for (int i = 0; i < clusters.length; i++) {
+ candidates.add(clusters[i], NameLookupType.FAMILY_NAME_ONLY_AS_NICKNAME);
+ }
+ }
+ }
+
+ private void addMatchCandidatesFullName(String givenName, String familyName, int mode,
+ MatchCandidateList candidates) {
+ final String givenNameN = NameNormalizer.normalize(givenName);
+ final String[] givenNameNicknames = mOpenHelper.getCommonNicknameClusters(givenNameN);
+ final String familyNameN = NameNormalizer.normalize(familyName);
+ final String[] familyNameNicknames = mOpenHelper.getCommonNicknameClusters(familyNameN);
+ candidates.add(givenNameN + "." + familyNameN, NameLookupType.FULL_NAME);
+ if (givenNameNicknames != null) {
+ for (int i = 0; i < givenNameNicknames.length; i++) {
+ candidates.add(givenNameNicknames[i] + "." + familyNameN,
+ NameLookupType.FULL_NAME_WITH_NICKNAME);
+ }
+ }
+ candidates.add(familyNameN + "." + givenNameN, NameLookupType.FULL_NAME_REVERSE);
+ if (familyNameNicknames != null) {
+ for (int i = 0; i < familyNameNicknames.length; i++) {
+ candidates.add(familyNameNicknames[i] + "." + givenNameN,
+ NameLookupType.FULL_NAME_WITH_NICKNAME_REVERSE);
+ }
+ }
+ candidates.add(givenNameN + familyNameN, NameLookupType.FULL_NAME_CONCATENATED);
+ candidates.add(familyNameN + givenNameN, NameLookupType.FULL_NAME_REVERSE_CONCATENATED);
+
+ if (mode == MODE_AGGREGATION || mode == MODE_SUGGESTIONS) {
+ candidates.add(givenNameN, NameLookupType.GIVEN_NAME_ONLY);
+ if (givenNameNicknames != null) {
+ for (int i = 0; i < givenNameNicknames.length; i++) {
+ candidates.add(givenNameNicknames[i],
+ NameLookupType.GIVEN_NAME_ONLY_AS_NICKNAME);
+ }
+ }
+
+ candidates.add(familyNameN, NameLookupType.FAMILY_NAME_ONLY);
+ if (familyNameNicknames != null) {
+ for (int i = 0; i < familyNameNicknames.length; i++) {
+ candidates.add(familyNameNicknames[i],
+ NameLookupType.FAMILY_NAME_ONLY_AS_NICKNAME);
+ }
+ }
+ }
+ }
+
+ /**
+ * Extracts the user name portion from an email address and normalizes it so that it
+ * can be matched against names and nicknames.
+ */
+ private void addMatchCandidatesEmail(String email, int mode, MatchCandidateList candidates) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
+ if (tokens.length == 0) {
+ return;
+ }
+
+ String address = tokens[0].getAddress();
+ int at = address.indexOf('@');
+ if (at != -1) {
+ address = address.substring(0, at);
+ }
+
+ candidates.add(NameNormalizer.normalize(address), NameLookupType.EMAIL_BASED_NICKNAME);
+ }
+
+
+ /**
+ * Normalizes the nickname and adds it to the list of candidates.
+ */
+ private void addMatchCandidatesNickname(String nickname, int mode,
+ MatchCandidateList candidates) {
+ candidates.add(NameNormalizer.normalize(nickname), NameLookupType.NICKNAME);
+ }
+
+ /**
+ * Given a list of {@link NameMatchCandidate}'s, finds all matches and computes their scores.
+ */
+ private void lookupNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
+ ContactMatcher matcher) {
+
+ if (candidates.mCount == 0) {
+ return;
+ }
+
+ StringBuilder selection = new StringBuilder();
+ selection.append(NameLookupColumns.NORMALIZED_NAME);
+ selection.append(" IN (");
+ for (int i = 0; i < candidates.mCount; i++) {
+ DatabaseUtils.appendEscapedSQLString(selection, candidates.mList.get(i).mName);
+ selection.append(",");
+ }
+
+ // Yank the last comma
+ selection.setLength(selection.length() - 1);
+ selection.append(") AND ");
+ selection.append(RawContacts.CONTACT_ID);
+ selection.append(" NOT NULL");
+
+ matchAllCandidates(db, selection.toString(), candidates, matcher, false);
+ }
+
+ /**
+ * Loads name lookup rows for approximate name matching and updates match scores based on that
+ * data.
+ */
+ private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
+ ContactMatcher matcher) {
+ HashSet<String> firstLetters = new HashSet<String>();
+ for (int i = 0; i < candidates.mCount; i++) {
+ final NameMatchCandidate candidate = candidates.mList.get(i);
+ if (candidate.mName.length() >= 2) {
+ String firstLetter = candidate.mName.substring(0, 2);
+ if (!firstLetters.contains(firstLetter)) {
+ firstLetters.add(firstLetter);
+ final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '"
+ + firstLetter + "*') AND " + RawContacts.CONTACT_ID + " NOT NULL";
+ matchAllCandidates(db, selection, candidates, matcher, true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads all candidate rows from the name lookup table and updates match scores based
+ * on that data.
+ */
+ private void matchAllCandidates(SQLiteDatabase db, String selection,
+ MatchCandidateList candidates, ContactMatcher matcher, boolean approximate) {
+ final Cursor c = db.query(Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS, NAME_LOOKUP_COLUMNS,
+ selection, null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ Long contactId = c.getLong(COL_NAME_LOOKUP_CONTACT_ID);
+ String name = c.getString(COL_NORMALIZED_NAME);
+ int nameType = c.getInt(COL_NAME_TYPE);
+
+ // Determine which candidate produced this match
+ for (int i = 0; i < candidates.mCount; i++) {
+ NameMatchCandidate candidate = candidates.mList.get(i);
+ matcher.matchName(contactId, candidate.mLookupType, candidate.mName,
+ nameType, name, approximate);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ private void lookupPhoneMatches(SQLiteDatabase db, String phoneNumber, ContactMatcher matcher) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ OpenHelper.buildPhoneLookupQuery(qb, phoneNumber);
+ Cursor c = qb.query(db, CONTACT_ID_COLUMNS,
+ RawContacts.CONTACT_ID + " NOT NULL", null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long contactId = c.getLong(COL_CONTACT_ID);
+ matcher.updateScoreWithPhoneNumberMatch(contactId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Finds exact email matches and updates their match scores.
+ */
+ private void lookupEmailMatches(SQLiteDatabase db, String address, ContactMatcher matcher) {
+ Cursor c = db.query(Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS, CONTACT_ID_COLUMNS,
+ Clauses.WHERE_EMAIL_MATCHES + " AND " + RawContacts.CONTACT_ID + " NOT NULL",
+ new String[]{address}, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long contactId = c.getLong(COL_CONTACT_ID);
+ matcher.updateScoreWithEmailMatch(contactId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Finds exact nickname matches in the name lookup table and updates their match scores.
+ */
+ private void lookupNicknameMatches(SQLiteDatabase db, String nickname, ContactMatcher matcher) {
+ String normalized = NameNormalizer.normalize(nickname);
+ Cursor c = db.query(true, Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS, CONTACT_ID_COLUMNS,
+ NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NICKNAME + " AND "
+ + NameLookupColumns.NORMALIZED_NAME + "='" + normalized + "' AND "
+ + RawContacts.CONTACT_ID + " NOT NULL",
+ null, null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long contactId = c.getLong(COL_CONTACT_ID);
+ matcher.updateScoreWithNicknameMatch(contactId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Prepares the supplied contact for aggregation with other contacts by (re)computing
+ * match lookup keys.
+ */
+ private void updateContactAggregationData(SQLiteDatabase db, long rawContactId,
+ MatchCandidateList candidates, ContentValues values) {
+ candidates.clear();
+
+ final Cursor c = db.query(Tables.DATA_JOIN_MIMETYPES,
+ DATA_JOIN_MIMETYPE_COLUMNS,
+ DatabaseUtils.concatenateWhere(Data.RAW_CONTACT_ID + "=" + rawContactId,
+ MIMETYPE_SELECTION_IN_CLAUSE),
+ null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ String mimeType = c.getString(COL_MIMETYPE);
+ String data1 = c.getString(COL_DATA1);
+ String data2 = c.getString(COL_DATA2);
+ if (mimeType.equals(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) {
+ addMatchCandidatesStructuredName(data1, data2, MODE_INSERT_LOOKUP_DATA,
+ candidates);
+ } else if (mimeType.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
+ addMatchCandidatesEmail(data2, MODE_INSERT_LOOKUP_DATA, candidates);
+ } else if (mimeType.equals(CommonDataKinds.Nickname.CONTENT_ITEM_TYPE)) {
+ addMatchCandidatesNickname(data2, MODE_INSERT_LOOKUP_DATA, candidates);
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ for (int i = 0; i < candidates.mCount; i++) {
+ NameMatchCandidate candidate = candidates.mList.get(i);
+ mOpenHelper.insertNameLookup(rawContactId, candidate.mLookupType, candidate.mName);
+ }
+ }
+
+ /**
+ * Updates aggregate-level data from constituent contacts.
+ */
+ private void updateAggregateData(final SQLiteDatabase db, long contactId,
+ final ContentValues values) {
+ updateDisplayName(db, contactId, values);
+ updateSendToVoicemailAndRingtone(db, contactId);
+ updatePhotoId(db, contactId, values);
+ }
+
+ /**
+ * Updates the contact record's {@link Contacts#DISPLAY_NAME} field. If none of the
+ * constituent raw contacts has a suitable name, leaves the aggregate contact record unchanged.
+ */
+ private void updateDisplayName(SQLiteDatabase db, long contactId, ContentValues values) {
+ String displayName = getBestDisplayName(db, contactId);
+
+ // If don't have anything to base the display name on, let's just leave what was in
+ // that field hoping that there was something there before and it is still valid.
+ if (displayName == null) {
+ return;
+ }
+
+ values.clear();
+ values.put(Contacts.DISPLAY_NAME, displayName);
+ db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+ }
+
+ private void updatePhotoId(SQLiteDatabase db, long contactId, ContentValues values) {
+ int photoId = choosePhotoId(db, contactId);
+
+ if (photoId == -1) {
+ return;
+ }
+
+ values.clear();
+ values.put(Contacts.PHOTO_ID, photoId);
+ db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+ }
+
+ /**
+ * Updates the various {@link ContactsColumns} primary values based on the
+ * newly joined {@link RawContacts} entry. If some aggregate primary values are
+ * unassigned, primary values from this contact will be promoted as the new
+ * super-primaries.
+ */
+ private void updatePrimaries(SQLiteDatabase db, long aggId, long rawContactId, boolean newAgg) {
+ Cursor cursor = null;
+
+ boolean hasOptimalPhone = false;
+ boolean hasFallbackPhone = false;
+ boolean hasOptimalEmail = false;
+ boolean hasFallbackEmail = false;
+
+ // Read currently recorded aggregate primary values
+ try {
+ cursor = db.query(Tables.CONTACTS, Projections.PROJ_CONTACT_PRIMARIES,
+ Contacts._ID + "=" + aggId, null, null, null, null);
+ if (cursor.moveToNext()) {
+ hasOptimalPhone = (cursor.getLong(Projections.COL_OPTIMAL_PRIMARY_PHONE_ID) != 0);
+ hasFallbackPhone = (cursor.getLong(Projections.COL_FALLBACK_PRIMARY_PHONE_ID) != 0);
+ hasOptimalEmail = (cursor.getLong(Projections.COL_OPTIMAL_PRIMARY_EMAIL_ID) != 0);
+ hasFallbackEmail = (cursor.getLong(Projections.COL_FALLBACK_PRIMARY_EMAIL_ID) != 0);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ long candidatePhone = 0;
+ long candidateEmail = 0;
+ boolean candidateIsRestricted = false;
+
+ // Find primary data items from newly-joined contact, returning one
+ // candidate for each mimetype.
+ try {
+ cursor = db.query(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS,
+ Projections.PROJ_DATA,
+ Data.RAW_CONTACT_ID + "=" + rawContactId + " AND " + Data.IS_PRIMARY + "=1 AND "
+ + Projections.PRIMARY_MIME_CLAUSE, null, Data.MIMETYPE, null, null);
+ while (cursor.moveToNext()) {
+ final long dataId = cursor.getLong(Projections.COL_DATA_ID);
+ final String mimeType = cursor.getString(Projections.COL_DATA_MIMETYPE);
+
+ candidateIsRestricted = (cursor.getInt(Projections.COL_IS_RESTRICTED) == 1);
+
+ if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ candidatePhone = dataId;
+ } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ candidateEmail = dataId;
+ }
+
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ final ContentValues values = new ContentValues();
+
+ // If a new contact, and single child is restricted, then mark
+ // contact as being protected by package. Otherwise set as null if
+ // multiple under contact or not restricted.
+ values.put(ContactsColumns.SINGLE_IS_RESTRICTED, (newAgg && candidateIsRestricted) ? 1
+ : 0);
+
+ // If newly joined contact has a primary phone number, consider
+ // promoting it up into aggregate as super-primary.
+ if (candidatePhone != 0) {
+ if (!hasOptimalPhone) {
+ values.put(ContactsColumns.OPTIMAL_PRIMARY_PHONE_ID, candidatePhone);
+ values.put(ContactsColumns.OPTIMAL_PRIMARY_PHONE_IS_RESTRICTED,
+ candidateIsRestricted ? 1 : 0);
+ }
+
+ // Also promote to unrestricted value, if none provided yet.
+ if (!hasFallbackPhone && !candidateIsRestricted) {
+ values.put(ContactsColumns.FALLBACK_PRIMARY_PHONE_ID, candidatePhone);
+ }
+ }
+
+ // If newly joined contact has a primary email, consider promoting it up
+ // into aggregate as super-primary.
+ if (candidateEmail != 0) {
+ if (!hasOptimalEmail) {
+ values.put(ContactsColumns.OPTIMAL_PRIMARY_EMAIL_ID, candidateEmail);
+ values.put(ContactsColumns.OPTIMAL_PRIMARY_EMAIL_IS_RESTRICTED,
+ candidateIsRestricted ? 1 : 0);
+ }
+
+ // Also promote to unrestricted value, if none provided yet.
+ if (!hasFallbackEmail && !candidateIsRestricted) {
+ values.put(ContactsColumns.FALLBACK_PRIMARY_EMAIL_ID, candidateEmail);
+ }
+ }
+
+ // Only write updated contact values if we made changes.
+ if (values.size() > 0) {
+ Log.d(TAG, "some sort of promotion is going on: " + values.toString());
+ db.update(Tables.CONTACTS, values, Contacts._ID + "=" + aggId, null);
+ }
+
+ }
+
+ /**
+ * Computes display name for the given contact. Chooses a longer name over a shorter name
+ * and a mixed-case name over an all lowercase or uppercase name.
+ */
+ private String getBestDisplayName(SQLiteDatabase db, long contactId) {
+ String bestDisplayName = null;
+
+ final Cursor c = db.query(Tables.RAW_CONTACTS, new String[] {RawContactsColumns.DISPLAY_NAME},
+ RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ String displayName = c.getString(0);
+ if (!TextUtils.isEmpty(displayName)) {
+ if (bestDisplayName == null) {
+ bestDisplayName = displayName;
+ } else {
+ if (NameNormalizer.compareComplexity(displayName, bestDisplayName) > 0) {
+ bestDisplayName = displayName;
+ }
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ return bestDisplayName;
+ }
+
+ /**
+ * Iterates over the photos associated with contact defined by contactId, and chooses one
+ * to be associated with the contact. Initially this just chooses the first photo in a list
+ * sorted by account name.
+ */
+ private int choosePhotoId(SQLiteDatabase db, long contactId) {
+ int chosenPhotoId = -1;
+ String chosenAccount = null;
+
+ final Cursor c = db.query(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS,
+ new String[] {"data._id AS _id", RawContacts.ACCOUNT_NAME},
+ DatabaseUtils.concatenateWhere(RawContacts.CONTACT_ID + "=" + contactId,
+ Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'"),
+ null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ int photoId = c.getInt(0);
+ String account = c.getString(1);
+ if (chosenAccount == null) {
+ chosenAccount = account;
+ chosenPhotoId = photoId;
+ } else {
+ if (account.compareToIgnoreCase(chosenAccount) < 0 ) {
+ chosenAccount = account;
+ chosenPhotoId = photoId;
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ return chosenPhotoId;
+ }
+
+ /**
+ * Updates the contact's send-to-voicemail and custom-ringtone options based on
+ * constituent contacts' options.
+ */
+ private void updateSendToVoicemailAndRingtone(SQLiteDatabase db, long contactId) {
+ int totalContactCount = 0;
+ int contactSendToVoicemail = 0;
+ String contactCustomRingtone = null;
+ long contactLastTimeContacted = 0;
+ int contactTimesContacted = 0;
+ boolean contactStarred = false;
+
+ final Cursor c = db.query(Tables.RAW_CONTACTS, CONTACT_OPTIONS_COLUMNS,
+ RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ totalContactCount++;
+ if (!c.isNull(COL_SEND_TO_VOICEMAIL)) {
+ boolean sendToVoicemail = (c.getInt(COL_SEND_TO_VOICEMAIL) != 0);
+ if (sendToVoicemail) {
+ contactSendToVoicemail++;
+ }
+ }
+
+ if (contactCustomRingtone == null && !c.isNull(COL_CUSTOM_RINGTONE)) {
+ contactCustomRingtone = c.getString(COL_CUSTOM_RINGTONE);
+ }
+
+ long lastTimeContacted = c.getLong(COL_LAST_TIME_CONTACTED);
+ if (lastTimeContacted > contactLastTimeContacted) {
+ contactLastTimeContacted = lastTimeContacted;
+ }
+
+ int timesContacted = c.getInt(COL_TIMES_CONTACTED);
+ if (timesContacted > contactTimesContacted) {
+ contactTimesContacted = timesContacted;
+ }
+
+ contactStarred |= (c.getInt(COL_STARRED) != 0);
+ }
+ } finally {
+ c.close();
+ }
+
+ ContentValues values = new ContentValues(2);
+ values.put(Contacts.SEND_TO_VOICEMAIL, totalContactCount == contactSendToVoicemail);
+ values.put(Contacts.CUSTOM_RINGTONE, contactCustomRingtone);
+ values.put(Contacts.LAST_TIME_CONTACTED, contactLastTimeContacted);
+ values.put(Contacts.TIMES_CONTACTED, contactTimesContacted);
+ values.put(Contacts.STARRED, contactStarred);
+
+ db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+ }
+
+ /**
+ * Finds matching contacts and returns a cursor on those.
+ */
+ public Cursor queryAggregationSuggestions(long contactId, String[] projection,
+ HashMap<String, String> projectionMap, int maxSuggestions) {
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+
+ Cursor c;
+
+ // If this method is called in the middle of aggregation pass, we want to pause the
+ // aggregation, but not kill it.
+ db.beginTransaction();
+ try {
+ List<MatchScore> bestMatches = findMatchingContacts(db, contactId, maxSuggestions);
+ c = queryMatchingContacts(db, contactId, projection, projectionMap, bestMatches);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ return c;
+ }
+
+ /**
+ * Loads contacts with specified IDs and returns them in the order of IDs in the
+ * supplied list.
+ */
+ private Cursor queryMatchingContacts(final SQLiteDatabase db, long contactId,
+ String[] projection, HashMap<String, String> projectionMap,
+ List<MatchScore> bestMatches) {
+
+ StringBuilder selection = new StringBuilder();
+ selection.append(Contacts._ID);
+ selection.append(" IN (");
+ for (int i = 0; i < bestMatches.size(); i++) {
+ MatchScore matchScore = bestMatches.get(i);
+ if (i != 0) {
+ selection.append(",");
+ }
+ selection.append(matchScore.getContactId());
+ }
+ selection.append(")");
+
+ final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(Tables.CONTACTS);
+ qb.setProjectionMap(projectionMap);
+
+ final Cursor cursor = qb.query(db, projection, selection.toString(), null, null, null,
+ Contacts._ID);
+
+ ArrayList<Long> sortedContactIds = new ArrayList<Long>(bestMatches.size());
+ for (MatchScore matchScore : bestMatches) {
+ sortedContactIds.add(matchScore.getContactId());
+ }
+
+ Collections.sort(sortedContactIds);
+
+ int[] positionMap = new int[bestMatches.size()];
+ for (int i = 0; i < positionMap.length; i++) {
+ long id = bestMatches.get(i).getContactId();
+ positionMap[i] = sortedContactIds.indexOf(id);
+ }
+
+ return new ReorderingCursorWrapper(cursor, positionMap);
+ }
+
+ /**
+ * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
+ * descending order of match score.
+ */
+ private List<MatchScore> findMatchingContacts(final SQLiteDatabase db,
+ long contactId, int maxSuggestions) {
+
+ MatchCandidateList candidates = new MatchCandidateList();
+ ContactMatcher matcher = new ContactMatcher();
+
+ // Don't aggregate a contact with itself
+ matcher.keepOut(contactId);
+
+ final Cursor c = db.query(Tables.RAW_CONTACTS, CONTACT_ID_COLUMN,
+ RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long rawContactId = c.getLong(0);
+ updateMatchScoresBasedOnDataMatches(db, rawContactId, MODE_SUGGESTIONS, candidates,
+ matcher);
+ }
+ } finally {
+ c.close();
+ }
+
+ List<MatchScore> matches = matcher.pickBestMatches(maxSuggestions,
+ ContactMatcher.SCORE_THRESHOLD_SUGGEST);
+
+ // TODO: remove the debug logging
+ Log.i(TAG, "MATCHES: " + matches);
+ return matches;
+ }
+
+ /**
+ * Various database projections used internally.
+ */
+ private interface Projections {
+ static final String[] PROJ_CONTACT_PRIMARIES = new String[] {
+ ContactsColumns.OPTIMAL_PRIMARY_PHONE_ID,
+ ContactsColumns.FALLBACK_PRIMARY_PHONE_ID,
+ ContactsColumns.OPTIMAL_PRIMARY_EMAIL_ID,
+ ContactsColumns.FALLBACK_PRIMARY_EMAIL_ID,
+ ContactsColumns.SINGLE_IS_RESTRICTED,
+ };
+
+ static final int COL_OPTIMAL_PRIMARY_PHONE_ID = 0;
+ static final int COL_FALLBACK_PRIMARY_PHONE_ID = 1;
+ static final int COL_OPTIMAL_PRIMARY_EMAIL_ID = 2;
+ static final int COL_FALLBACK_PRIMARY_EMAIL_ID = 3;
+ static final int COL_SINGLE_IS_RESTRICTED = 4;
+
+ static final String[] PROJ_DATA = new String[] {
+ Tables.DATA + "." + Data._ID,
+ Data.MIMETYPE,
+ RawContacts.IS_RESTRICTED,
+ };
+
+ static final int COL_DATA_ID = 0;
+ static final int COL_DATA_MIMETYPE = 1;
+ static final int COL_IS_RESTRICTED = 2;
+
+ static final String PRIMARY_MIME_CLAUSE = "(" + Data.MIMETYPE + "=\""
+ + CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "\" OR " + Data.MIMETYPE + "=\""
+ + CommonDataKinds.Email.CONTENT_ITEM_TYPE + "\")";
+ }
+}
diff --git a/src/com/android/providers/contacts/ContactMatcher.java b/src/com/android/providers/contacts/ContactMatcher.java
new file mode 100644
index 0000000..6ab98dd
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactMatcher.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import com.android.providers.contacts.OpenHelper.NameLookupType;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Logic for matching contacts' data and accumulating match scores.
+ */
+public class 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;
+
+ // Minimum edit distance between two names to be considered an approximate match
+ public static final float APPROXIMATE_MATCH_THRESHOLD = 0.7f;
+
+ // Maximum number of characters in a name to be considered by the matching algorithm.
+ private static final int MAX_MATCHED_NAME_LENGTH = 12;
+
+ // Scores a multiplied by this number to allow room for "fractional" scores
+ private static final int SCORE_SCALE = 1000;
+
+
+ /**
+ * 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 the Jaro-Winkler algorithm, which is particularly suited for
+ * name matching. See {@link JaroWinklerDistance}.
+ */
+ 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.FULL_NAME,
+ NameLookupType.FULL_NAME, 99, 99);
+ setScoreRange(NameLookupType.FULL_NAME,
+ NameLookupType.FULL_NAME_REVERSE, 90, 90);
+
+ setScoreRange(NameLookupType.FULL_NAME_REVERSE,
+ NameLookupType.FULL_NAME, 90, 90);
+ setScoreRange(NameLookupType.FULL_NAME_REVERSE,
+ NameLookupType.FULL_NAME_REVERSE, 99, 99);
+
+ setScoreRange(NameLookupType.FULL_NAME_CONCATENATED,
+ NameLookupType.FULL_NAME_CONCATENATED, 40, 80);
+ setScoreRange(NameLookupType.FULL_NAME_CONCATENATED,
+ NameLookupType.FULL_NAME_REVERSE_CONCATENATED, 30, 70);
+ setScoreRange(NameLookupType.FULL_NAME_CONCATENATED,
+ NameLookupType.EMAIL_BASED_NICKNAME, 30, 60);
+ setScoreRange(NameLookupType.FULL_NAME_CONCATENATED,
+ NameLookupType.NICKNAME, 30, 60);
+
+ setScoreRange(NameLookupType.FULL_NAME_REVERSE_CONCATENATED,
+ NameLookupType.FULL_NAME_CONCATENATED, 30, 70);
+ setScoreRange(NameLookupType.FULL_NAME_REVERSE_CONCATENATED,
+ NameLookupType.FULL_NAME_REVERSE_CONCATENATED, 40, 80);
+
+ setScoreRange(NameLookupType.FULL_NAME_WITH_NICKNAME,
+ NameLookupType.FULL_NAME_WITH_NICKNAME, 75, 75);
+ setScoreRange(NameLookupType.FULL_NAME_WITH_NICKNAME_REVERSE,
+ NameLookupType.FULL_NAME_WITH_NICKNAME_REVERSE, 73, 73);
+
+ setScoreRange(NameLookupType.FAMILY_NAME_ONLY,
+ NameLookupType.FAMILY_NAME_ONLY, 45, 75);
+ setScoreRange(NameLookupType.FAMILY_NAME_ONLY,
+ NameLookupType.FULL_NAME_CONCATENATED, 32, 72);
+ setScoreRange(NameLookupType.FAMILY_NAME_ONLY,
+ NameLookupType.FULL_NAME_REVERSE_CONCATENATED, 30, 70);
+ setScoreRange(NameLookupType.FAMILY_NAME_ONLY,
+ NameLookupType.EMAIL_BASED_NICKNAME, 30, 60);
+ setScoreRange(NameLookupType.FAMILY_NAME_ONLY,
+ NameLookupType.NICKNAME, 30, 60);
+
+ setScoreRange(NameLookupType.FAMILY_NAME_ONLY_AS_NICKNAME,
+ NameLookupType.FAMILY_NAME_ONLY_AS_NICKNAME, 71, 71);
+ setScoreRange(NameLookupType.FAMILY_NAME_ONLY_AS_NICKNAME,
+ NameLookupType.GIVEN_NAME_ONLY_AS_NICKNAME, 70, 70);
+
+ setScoreRange(NameLookupType.GIVEN_NAME_ONLY,
+ NameLookupType.GIVEN_NAME_ONLY, 40, 70);
+ setScoreRange(NameLookupType.GIVEN_NAME_ONLY,
+ NameLookupType.FULL_NAME_CONCATENATED, 32, 72);
+ setScoreRange(NameLookupType.GIVEN_NAME_ONLY,
+ NameLookupType.FULL_NAME_REVERSE_CONCATENATED, 30, 70);
+ setScoreRange(NameLookupType.GIVEN_NAME_ONLY,
+ NameLookupType.EMAIL_BASED_NICKNAME, 30, 60);
+ setScoreRange(NameLookupType.GIVEN_NAME_ONLY,
+ NameLookupType.NICKNAME, 30, 60);
+
+ setScoreRange(NameLookupType.GIVEN_NAME_ONLY_AS_NICKNAME,
+ NameLookupType.GIVEN_NAME_ONLY_AS_NICKNAME, 73, 73);
+ setScoreRange(NameLookupType.GIVEN_NAME_ONLY_AS_NICKNAME,
+ NameLookupType.FAMILY_NAME_ONLY_AS_NICKNAME, 70, 70);
+
+ setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+ NameLookupType.EMAIL_BASED_NICKNAME, 30, 60);
+ setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+ NameLookupType.GIVEN_NAME_ONLY, 30, 60);
+ setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+ NameLookupType.FAMILY_NAME_ONLY, 30, 60);
+ setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+ NameLookupType.FULL_NAME_CONCATENATED, 30, 60);
+ setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+ NameLookupType.FULL_NAME_REVERSE_CONCATENATED, 30, 60);
+ setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+ NameLookupType.NICKNAME, 30, 60);
+
+ setScoreRange(NameLookupType.NICKNAME,
+ NameLookupType.NICKNAME, 30, 60);
+ setScoreRange(NameLookupType.NICKNAME,
+ NameLookupType.GIVEN_NAME_ONLY, 30, 60);
+ setScoreRange(NameLookupType.NICKNAME,
+ NameLookupType.FAMILY_NAME_ONLY, 30, 60);
+ setScoreRange(NameLookupType.NICKNAME,
+ NameLookupType.FULL_NAME_CONCATENATED, 30, 60);
+ setScoreRange(NameLookupType.NICKNAME,
+ NameLookupType.FULL_NAME_REVERSE_CONCATENATED, 30, 60);
+ setScoreRange(NameLookupType.NICKNAME,
+ NameLookupType.EMAIL_BASED_NICKNAME, 30, 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.
+ */
+ 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 JaroWinklerDistance mJaroWinklerDistance =
+ new JaroWinklerDistance(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;
+ }
+
+ /**
+ * 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, boolean approximate) {
+ int maxScore = getMaxScore(candidateNameType, nameType);
+ if (maxScore == 0) {
+ return;
+ }
+
+ if (candidateName.equals(name)) {
+ updatePrimaryScore(contactId, maxScore);
+ return;
+ }
+
+ if (!approximate) {
+ return;
+ }
+
+ int minScore = getMinScore(candidateNameType, nameType);
+ if (minScore == maxScore) {
+ return;
+ }
+
+ float distance = mJaroWinklerDistance.getDistance(
+ Hex.decodeHex(candidateName), Hex.decodeHex(name));
+
+ int score;
+ if (distance > APPROXIMATE_MATCH_THRESHOLD) {
+ float adjustedDistance = (distance - APPROXIMATE_MATCH_THRESHOLD)
+ / (1f - APPROXIMATE_MATCH_THRESHOLD);
+ score = (int)(minScore + (maxScore - minScore) * adjustedDistance);
+ } 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 contactd.
+ * <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.
+ */
+ public long pickBestMatch(int threshold) {
+ long contactId = -1;
+ int maxScore = 0;
+ for (int i = 0; i < mScoreCount; i++) {
+ MatchScore score = mScoreList.get(i);
+ if (score.mKeepIn) {
+ return score.mContactId;
+ }
+
+ if (score.mKeepOut) {
+ continue;
+ }
+
+ int s = score.mPrimaryScore;
+ if (s == NO_DATA_SCORE) {
+ s = score.mSecondaryScore;
+ }
+
+ if (s >= threshold && s > maxScore) {
+ contactId = score.mContactId;
+ maxScore = s;
+ }
+ }
+ return contactId;
+ }
+
+ /**
+ * Returns up to {@code maxSuggestions} best scoring matches.
+ */
+ public List<MatchScore> pickBestMatches(int maxSuggestions, 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;
+ }
+ }
+
+ if (count > maxSuggestions) {
+ count = maxSuggestions;
+ }
+
+ return matches.subList(0, count);
+ }
+}
diff --git a/src/com/android/providers/contacts/ContactsProvider.java b/src/com/android/providers/contacts/ContactsProvider.java
index 777aa61..a89b6db 100644
--- a/src/com/android/providers/contacts/ContactsProvider.java
+++ b/src/com/android/providers/contacts/ContactsProvider.java
@@ -44,10 +44,8 @@
import android.os.MemoryFile;
import android.os.ParcelFileDescriptor;
import android.provider.CallLog;
-import android.provider.Contacts;
-import android.provider.LiveFolders;
-import android.provider.SyncConstValue;
import android.provider.CallLog.Calls;
+import android.provider.Contacts;
import android.provider.Contacts.ContactMethods;
import android.provider.Contacts.Extensions;
import android.provider.Contacts.GroupMembership;
@@ -61,22 +59,24 @@
import android.provider.Contacts.Photos;
import android.provider.Contacts.Presence;
import android.provider.Contacts.PresenceColumns;
+import android.provider.LiveFolders;
+import android.provider.SyncConstValue;
+import android.provider.Calendar;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Config;
import android.util.Log;
-
-import com.google.android.collect.Maps;
-import com.google.android.collect.Sets;
+import android.accounts.Account;
import com.android.internal.database.ArrayListCursor;
+import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -125,7 +125,7 @@
* other methods (other than the DB layer) */
private final ContentValues mValuesLocal = new ContentValues();
- private String[] mAccounts = new String[0];
+ private Account[] mAccounts = new Account[0];
private final Object mAccountsLock = new Object();
private DatabaseUtils.InsertHelper mDeletedPeopleInserter;
@@ -134,7 +134,8 @@
private int mIndexPeopleSyncTime;
private int mIndexPeopleSyncVersion;
private int mIndexPeopleSyncDirty;
- private int mIndexPeopleSyncAccount;
+ private int mIndexPeopleSyncAccountName;
+ private int mIndexPeopleSyncAccountType;
private int mIndexPeopleName;
private int mIndexPeoplePhoneticName;
private int mIndexPeopleNotes;
@@ -145,7 +146,8 @@
private int mIndexPhotosSyncTime;
private int mIndexPhotosSyncVersion;
private int mIndexPhotosSyncDirty;
- private int mIndexPhotosSyncAccount;
+ private int mIndexPhotosSyncAccountName;
+ private int mIndexPhotosSyncAccountType;
private int mIndexPhotosExistsOnServer;
private int mIndexPhotosSyncError;
private DatabaseUtils.InsertHelper mContactMethodsInserter;
@@ -169,7 +171,8 @@
private int mIndexExtensionsValue;
private DatabaseUtils.InsertHelper mGroupMembershipInserter;
private int mIndexGroupMembershipPersonId;
- private int mIndexGroupMembershipGroupSyncAccount;
+ private int mIndexGroupMembershipGroupSyncAccountName;
+ private int mIndexGroupMembershipGroupSyncAccountType;
private int mIndexGroupMembershipGroupSyncId;
private DatabaseUtils.InsertHelper mCallsInserter;
private DatabaseUtils.InsertHelper mPhonesInserter;
@@ -180,28 +183,8 @@
private int mIndexPhonesNumberKey;
private int mIndexPhonesIsPrimary;
- private static HashMap<String, String> mSearchSuggestionsProjectionMap;
- private static String mSearchSuggestionLanguage;
-
public ContactsProvider() {
super(DATABASE_NAME, DATABASE_VERSION, Contacts.CONTENT_URI);
- mSearchSuggestionLanguage = Locale.getDefault().getLanguage();
- // Search suggestions projection map
- mSearchSuggestionsProjectionMap = new HashMap<String, String>();
- updateSuggestColumnTexts();
- mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_ICON_1,
- "(CASE WHEN " + Photos.DATA + " IS NOT NULL"
- + " THEN '" + People.CONTENT_URI + "/' || people._id ||"
- + " '/" + Photos.CONTENT_DIRECTORY + "/data'"
- + " ELSE " + com.android.internal.R.drawable.ic_contact_picture
- + " END) AS " + SearchManager.SUGGEST_COLUMN_ICON_1);
- mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_ICON_2,
- PRESENCE_ICON_SQL + " AS " + SearchManager.SUGGEST_COLUMN_ICON_2);
- mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
- "people._id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
- mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
- "people._id AS " + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
- mSearchSuggestionsProjectionMap.put(People._ID, "people._id AS " + People._ID);
}
@Override
@@ -223,7 +206,8 @@
mIndexPeopleSyncTime = mPeopleInserter.getColumnIndex(People._SYNC_TIME);
mIndexPeopleSyncVersion = mPeopleInserter.getColumnIndex(People._SYNC_VERSION);
mIndexPeopleSyncDirty = mPeopleInserter.getColumnIndex(People._SYNC_DIRTY);
- mIndexPeopleSyncAccount = mPeopleInserter.getColumnIndex(People._SYNC_ACCOUNT);
+ mIndexPeopleSyncAccountName = mPeopleInserter.getColumnIndex(People._SYNC_ACCOUNT);
+ mIndexPeopleSyncAccountType = mPeopleInserter.getColumnIndex(People._SYNC_ACCOUNT_TYPE);
mIndexPeopleName = mPeopleInserter.getColumnIndex(People.NAME);
mIndexPeoplePhoneticName = mPeopleInserter.getColumnIndex(People.PHONETIC_NAME);
mIndexPeopleNotes = mPeopleInserter.getColumnIndex(People.NOTES);
@@ -236,26 +220,32 @@
mIndexPhotosSyncTime = mPhotosInserter.getColumnIndex(Photos._SYNC_TIME);
mIndexPhotosSyncVersion = mPhotosInserter.getColumnIndex(Photos._SYNC_VERSION);
mIndexPhotosSyncDirty = mPhotosInserter.getColumnIndex(Photos._SYNC_DIRTY);
- mIndexPhotosSyncAccount = mPhotosInserter.getColumnIndex(Photos._SYNC_ACCOUNT);
+ mIndexPhotosSyncAccountName = mPhotosInserter.getColumnIndex(Photos._SYNC_ACCOUNT);
+ mIndexPhotosSyncAccountType = mPhotosInserter.getColumnIndex(Photos._SYNC_ACCOUNT_TYPE);
mIndexPhotosSyncError = mPhotosInserter.getColumnIndex(Photos.SYNC_ERROR);
mIndexPhotosExistsOnServer = mPhotosInserter.getColumnIndex(Photos.EXISTS_ON_SERVER);
mContactMethodsInserter = new DatabaseUtils.InsertHelper(db, sContactMethodsTable);
- mIndexContactMethodsPersonId = mContactMethodsInserter.getColumnIndex(ContactMethods.PERSON_ID);
+ mIndexContactMethodsPersonId =
+ mContactMethodsInserter.getColumnIndex(ContactMethods.PERSON_ID);
mIndexContactMethodsLabel = mContactMethodsInserter.getColumnIndex(ContactMethods.LABEL);
mIndexContactMethodsKind = mContactMethodsInserter.getColumnIndex(ContactMethods.KIND);
mIndexContactMethodsType = mContactMethodsInserter.getColumnIndex(ContactMethods.TYPE);
mIndexContactMethodsData = mContactMethodsInserter.getColumnIndex(ContactMethods.DATA);
- mIndexContactMethodsAuxData = mContactMethodsInserter.getColumnIndex(ContactMethods.AUX_DATA);
- mIndexContactMethodsIsPrimary = mContactMethodsInserter.getColumnIndex(ContactMethods.ISPRIMARY);
+ mIndexContactMethodsAuxData =
+ mContactMethodsInserter.getColumnIndex(ContactMethods.AUX_DATA);
+ mIndexContactMethodsIsPrimary =
+ mContactMethodsInserter.getColumnIndex(ContactMethods.ISPRIMARY);
mOrganizationsInserter = new DatabaseUtils.InsertHelper(db, sOrganizationsTable);
- mIndexOrganizationsPersonId = mOrganizationsInserter.getColumnIndex(Organizations.PERSON_ID);
+ mIndexOrganizationsPersonId =
+ mOrganizationsInserter.getColumnIndex(Organizations.PERSON_ID);
mIndexOrganizationsLabel = mOrganizationsInserter.getColumnIndex(Organizations.LABEL);
mIndexOrganizationsType = mOrganizationsInserter.getColumnIndex(Organizations.TYPE);
mIndexOrganizationsCompany = mOrganizationsInserter.getColumnIndex(Organizations.COMPANY);
mIndexOrganizationsTitle = mOrganizationsInserter.getColumnIndex(Organizations.TITLE);
- mIndexOrganizationsIsPrimary = mOrganizationsInserter.getColumnIndex(Organizations.ISPRIMARY);
+ mIndexOrganizationsIsPrimary =
+ mOrganizationsInserter.getColumnIndex(Organizations.ISPRIMARY);
mExtensionsInserter = new DatabaseUtils.InsertHelper(db, sExtensionsTable);
mIndexExtensionsPersonId = mExtensionsInserter.getColumnIndex(Extensions.PERSON_ID);
@@ -263,9 +253,14 @@
mIndexExtensionsValue = mExtensionsInserter.getColumnIndex(Extensions.VALUE);
mGroupMembershipInserter = new DatabaseUtils.InsertHelper(db, sGroupmembershipTable);
- mIndexGroupMembershipPersonId = mGroupMembershipInserter.getColumnIndex(GroupMembership.PERSON_ID);
- mIndexGroupMembershipGroupSyncAccount = mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ACCOUNT);
- mIndexGroupMembershipGroupSyncId = mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ID);
+ mIndexGroupMembershipPersonId =
+ mGroupMembershipInserter.getColumnIndex(GroupMembership.PERSON_ID);
+ mIndexGroupMembershipGroupSyncAccountName =
+ mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ACCOUNT);
+ mIndexGroupMembershipGroupSyncAccountType =
+ mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
+ mIndexGroupMembershipGroupSyncId =
+ mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ID);
mCallsInserter = new DatabaseUtils.InsertHelper(db, sCallsTable);
@@ -307,23 +302,9 @@
// use new token format from 73
db.execSQL("delete from peopleLookup");
try {
- // With longForQuery(), _TOKERNIZE() is called just once, toward the first entry
- // in "people" table. This may be a bug. Instead, we use rawQuery() for now.
- // DatabaseUtils.longForQuery(db, query, null);
-
- // Cursors objects are lazily executed. So we have to call some method which forces
- // the cursor to run the query.
- Cursor cursor =
- db.rawQuery("SELECT _TOKENIZE('peopleLookup', _id, name, ' ') from people",
- null);
- try {
- int rows = cursor.getCount();
- Log.i(TAG, "Processed " + rows + " contacts.");
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
+ DatabaseUtils.longForQuery(db,
+ "SELECT _TOKENIZE('peopleLookup', _id, name, ' ') from people;",
+ null);
} catch (SQLiteDoneException ex) {
// it is ok to throw this,
// it just means you don't have data in people table
@@ -385,52 +366,71 @@
oldVersion = 80;
}
- // Because of historical reason, version 81 have two types.
- // 1) One type already has "peopleLookupWithPhoneticName" table but does not have
- // "peopleLookup" table with token_index.
- // 2) Another type has "peopleLookup" table with token_index but does not have
- // "peopleLookupWithPhoneticName" table.
- // For simplicity, both databases are once dropped here.
- // This is slow but should be done just once anyway...
- if (oldVersion == 80 || oldVersion == 81) {
+ if (oldVersion == 80) {
+ db.execSQL("ALTER TABLE people ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("ALTER TABLE _deleted_people ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("ALTER TABLE groups ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("ALTER TABLE _deleted_groups ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("ALTER TABLE settings ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("ALTER TABLE photos ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("ALTER TABLE groupmembership ADD COLUMN _sync_account_type TEXT;");
+
+ db.execSQL("UPDATE people"
+ + " SET _sync_account_type='com.google.GAIA'"
+ + " WHERE _sync_account IS NOT NULL");
+ db.execSQL("UPDATE _deleted_people"
+ + " SET _sync_account_type='com.google.GAIA'"
+ + " WHERE _sync_account IS NOT NULL");
+ db.execSQL("UPDATE groups"
+ + " SET _sync_account_type='com.google.GAIA'"
+ + " WHERE _sync_account IS NOT NULL");
+ db.execSQL("UPDATE _deleted_groups"
+ + " SET _sync_account_type='com.google.GAIA'"
+ + " WHERE _sync_account IS NOT NULL");
+ db.execSQL("UPDATE settings"
+ + " SET _sync_account_type='com.google.GAIA'"
+ + " WHERE _sync_account IS NOT NULL");
+ db.execSQL("UPDATE photos"
+ + " SET _sync_account_type='com.google.GAIA'"
+ + " WHERE _sync_account IS NOT NULL");
+ db.execSQL("UPDATE groupmembership"
+ + " SET _sync_account_type='com.google.GAIA'"
+ + " WHERE _sync_account IS NOT NULL");
+
+ db.execSQL("CREATE INDEX groupTempIndex ON groups ("
+ + Groups.NAME + "," + Groups.SYSTEM_ID + "," + Groups._SYNC_ACCOUNT + ","
+ + Groups._SYNC_ACCOUNT_TYPE + ");");
+
+ db.execSQL("DROP INDEX groupmembershipIndex3");
+ db.execSQL("CREATE INDEX groupmembershipIndex3 ON groupmembership "
+ + "(group_sync_account_type, group_sync_account, group_sync_id);");
+
+ // Trigger to move an account_people row to _deleted_account_people when it is deleted
+ db.execSQL("DROP TRIGGER groups_to_deleted");
+ db.execSQL("CREATE TRIGGER groups_to_deleted DELETE ON groups " +
+ "WHEN old._sync_id is not null " +
+ "BEGIN " +
+ "INSERT INTO _deleted_groups " +
+ "(_sync_id, _sync_account, _sync_account_type, _sync_version) " +
+ "VALUES (old._sync_id, old._sync_account, old._sync_account_type," +
+ "old._sync_version);" +
+ "END");
+
+ oldVersion++;
+ }
+
+ if (oldVersion == 81) {
Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
newVersion + ", which will preserve existing data");
-
- recreatePeopleLookupTable(db);
- try {
- String query = "SELECT _TOKENIZE('peopleLookup', _id, name, ' ', 1) FROM people";
- Cursor cursor = db.rawQuery(query, null);
- try {
- int rows = cursor.getCount();
- Log.i(TAG, "Processed " + rows + " contacts.");
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- } catch (SQLiteException e) {
- Log.e(TAG, e.toString() + ": " + e.getMessage());
- }
-
- recreatePeopleLookupWithPhoneticNameTable(db);
- try {
- String query = "SELECT _TOKENIZE('peopleLookupWithPhoneticName', _id, "
- + PHONETIC_LOOKUP_SQL_SIMPLE +
- ", ' ', 1) FROM people";
- Cursor cursor = db.rawQuery(query, null);
- try {
- int rows = cursor.getCount();
- Log.i(TAG, "Processed " + rows + " contacts.");
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- } catch (SQLiteException e) {
- Log.e(TAG, e.toString() + ": " + e.getMessage());
- }
-
- oldVersion = 82;
+ // 81 adds the token_index column
+ db.execSQL("DELETE FROM peopleLookup");
+ db.execSQL("ALTER TABLE peopleLookup ADD token_index INTEGER;");
+ String[] tokenize = {"_TOKENIZE('peopleLookup', _id, name, ' ', 1)"};
+ Cursor cursor = db.query("people", tokenize, null, null, null, null, null);
+ int rows = cursor.getCount();
+ cursor.close();
+ Log.i(TAG, "Processed " + rows + " contacts.");
+ oldVersion = 81;
}
return upgradeWasLossless;
@@ -439,7 +439,6 @@
protected void dropTables(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS people");
db.execSQL("DROP TABLE IF EXISTS peopleLookup");
- db.execSQL("DROP TABLE IF EXISTS peopleLookupWithPhoneticName");
db.execSQL("DROP TABLE IF EXISTS _deleted_people");
db.execSQL("DROP TABLE IF EXISTS phones");
db.execSQL("DROP TABLE IF EXISTS contact_methods");
@@ -454,74 +453,13 @@
db.execSQL("DROP TABLE IF EXISTS settings");
}
- private void recreatePeopleLookupTable(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS peopleLookup");
- db.execSQL("DROP INDEX IF EXISTS peopleLookupIndex");
- db.execSQL("DROP TRIGGER IF EXISTS peopleLookup_update");
- db.execSQL("DROP TRIGGER IF EXISTS peopleLookup_insert");
-
- db.execSQL("CREATE TABLE peopleLookup (" +
- "token TEXT," +
- "source INTEGER REFERENCES people(_id)," +
- "token_index INTEGER" +
- ");");
- db.execSQL("CREATE INDEX peopleLookupIndex ON peopleLookup (" +
- "token," +
- "source" +
- ");");
-
- // Triggers to keep the peopleLookup table up to date
- db.execSQL("CREATE TRIGGER peopleLookup_update UPDATE OF name ON people " +
- "BEGIN " +
- "DELETE FROM peopleLookup WHERE source = new._id;" +
- "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" +
- "END");
- db.execSQL("CREATE TRIGGER peopleLookup_insert AFTER INSERT ON people " +
- "BEGIN " +
- "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" +
- "END");
- }
-
- private void recreatePeopleLookupWithPhoneticNameTable(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS peopleLookupWithPhoneticName");
- db.execSQL("DROP INDEX IF EXISTS peopleLookupwithPhoneticNameIndex");
- db.execSQL("DROP TRIGGER IF EXISTS peopleLookupWithPhoneticName_update");
- db.execSQL("DROP TRIGGER IF EXISTS peopleLookupWithPhoneticName_insert");
-
- db.execSQL("CREATE TABLE peopleLookupWithPhoneticName (" +
- "token TEXT," +
- "source INTEGER REFERENCES people(_id)," +
- "token_index INTEGER" +
- ");");
- db.execSQL("CREATE INDEX peopleLookupWithPhoneticNameIndex ON " +
- "peopleLookupWithPhoneticName (" +
- "token," +
- "source" +
- ");");
-
- // Triggers to keep the peopleLookupWithPhoneticName table up to date
- db.execSQL("CREATE TRIGGER peopleLookupWithPhoneticName_update UPDATE OF " +
- "name, phonetic_name ON people " +
- "BEGIN " +
- "DELETE FROM peopleLookupWithPhoneticName WHERE source = new._id;" +
- "SELECT _TOKENIZE('peopleLookupWithPhoneticName', new._id, " +
- PHONETIC_LOOKUP_SQL_SIMPLE_WITH_NEW +
- ", ' ', 1);" +
- "END");
- db.execSQL("CREATE TRIGGER peopleLookupWithPhoneticName_insert AFTER INSERT ON people " +
- "BEGIN " +
- "SELECT _TOKENIZE('peopleLookupWithPhoneticName', new._id, " +
- PHONETIC_LOOKUP_SQL_SIMPLE_WITH_NEW +
- ", ' ', 1);" +
- "END");
- }
-
@Override
protected void bootstrapDatabase(SQLiteDatabase db) {
super.bootstrapDatabase(db);
db.execSQL("CREATE TABLE people (" +
People._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
People._SYNC_ACCOUNT + " TEXT," + // From the sync source
+ People._SYNC_ACCOUNT_TYPE + " TEXT," + // From the sync source
People._SYNC_ID + " TEXT," + // From the sync source
People._SYNC_TIME + " TEXT," + // From the sync source
People._SYNC_VERSION + " TEXT," + // From the sync source
@@ -560,6 +498,7 @@
db.execSQL("CREATE TABLE groups (" +
Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Groups._SYNC_ACCOUNT + " TEXT," + // From the sync source
+ Groups._SYNC_ACCOUNT_TYPE + " TEXT," + // From the sync source
Groups._SYNC_ID + " TEXT," + // From the sync source
Groups._SYNC_TIME + " TEXT," + // From the sync source
Groups._SYNC_VERSION + " TEXT," + // From the sync source
@@ -574,7 +513,8 @@
Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 0," +
Groups.SYSTEM_ID + " TEXT," +
"UNIQUE(" +
- Groups.NAME + "," + Groups.SYSTEM_ID + "," + Groups._SYNC_ACCOUNT + ")" +
+ Groups.NAME + "," + Groups.SYSTEM_ID + "," + Groups._SYNC_ACCOUNT + "," +
+ Groups._SYNC_ACCOUNT_TYPE + ")" +
");");
db.execSQL("CREATE INDEX groupsSyncDirtyIndex ON groups (" + Groups._SYNC_DIRTY + ");");
@@ -603,6 +543,7 @@
+ Photos.DATA + " BLOB,"
+ Photos.SYNC_ERROR + " TEXT,"
+ Photos._SYNC_ACCOUNT + " TEXT,"
+ + Photos._SYNC_ACCOUNT_TYPE + " TEXT,"
+ Photos._SYNC_ID + " TEXT,"
+ Photos._SYNC_TIME + " TEXT,"
+ Photos._SYNC_VERSION + " TEXT,"
@@ -627,6 +568,7 @@
"_sync_id TEXT," +
(isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
"_sync_account TEXT," +
+ "_sync_account_type TEXT," +
"_sync_mark INTEGER)"); // Used to filter out new rows
db.execSQL("CREATE TABLE _deleted_groups (" +
@@ -634,6 +576,7 @@
"_sync_id TEXT," +
(isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
"_sync_account TEXT," +
+ "_sync_account_type TEXT," +
"_sync_mark INTEGER)"); // Used to filter out new rows
db.execSQL("CREATE TABLE phones (" +
@@ -680,6 +623,7 @@
db.execSQL("CREATE TABLE settings (" +
"_id INTEGER PRIMARY KEY," +
"_sync_account TEXT," +
+ "_sync_account_type TEXT," +
"key STRING NOT NULL," +
"value STRING " +
");");
@@ -712,18 +656,18 @@
"person INTEGER REFERENCES people(_id)," +
"group_id INTEGER REFERENCES groups(_id)," +
"group_sync_account STRING," +
+ "group_sync_account_type STRING," +
"group_sync_id STRING" +
");");
db.execSQL("CREATE INDEX groupmembershipIndex1 ON groupmembership (person, group_id);");
db.execSQL("CREATE INDEX groupmembershipIndex2 ON groupmembership (group_id, person);");
db.execSQL("CREATE INDEX groupmembershipIndex3 ON groupmembership "
- + "(group_sync_account, group_sync_id);");
+ + "(group_sync_account, group_sync_account_type, group_sync_id);");
// Trigger to completely remove a contacts data when they're deleted
db.execSQL("CREATE TRIGGER contact_cleanup DELETE ON people " +
"BEGIN " +
"DELETE FROM peopleLookup WHERE source = old._id;" +
- "DELETE FROM peopleLookupWithPhoneticName WHERE source = old._id;" +
"DELETE FROM phones WHERE person = old._id;" +
"DELETE FROM contact_methods WHERE person = old._id;" +
"DELETE FROM organizations WHERE person = old._id;" +
@@ -743,14 +687,22 @@
"WHEN old._sync_id is not null " +
"BEGIN " +
"INSERT INTO _deleted_groups " +
- "(_sync_id, _sync_account, _sync_version) " +
- "VALUES (old._sync_id, old._sync_account, " +
+ "(_sync_id, _sync_account, _sync_account_type, _sync_version) " +
+ "VALUES (old._sync_id, old._sync_account, old._sync_account_type, " +
"old._sync_version);" +
"END");
- recreatePeopleLookupTable(db);
- recreatePeopleLookupWithPhoneticNameTable(db);
-
+ // Triggers to keep the peopleLookup table up to date
+ db.execSQL("CREATE TRIGGER peopleLookup_update UPDATE OF name ON people " +
+ "BEGIN " +
+ "DELETE FROM peopleLookup WHERE source = new._id;" +
+ "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" +
+ "END");
+ db.execSQL("CREATE TRIGGER peopleLookup_insert AFTER INSERT ON people " +
+ "BEGIN " +
+ "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" +
+ "END");
+
// Triggers to set the _sync_dirty flag when a phone is changed,
// inserted or deleted
db.execSQL("CREATE TRIGGER phones_update UPDATE ON phones " +
@@ -887,10 +839,10 @@
+ Presence.PERSON_ID + ");");
}
- private String buildPeopleLookupWhereClauseCommon(String filterParam, String tableName) {
- StringBuilder filter = new StringBuilder("people._id IN (SELECT source FROM ");
- filter.append(tableName);
- filter.append(" WHERE token GLOB ");
+ @SuppressWarnings("deprecation")
+ private String buildPeopleLookupWhereClause(String filterParam) {
+ StringBuilder filter = new StringBuilder(
+ "people._id IN (SELECT source FROM peopleLookup WHERE token GLOB ");
// NOTE: Query parameters won't work here since the SQL compiler
// needs to parse the actual string to know that it can use the
// index to do a prefix scan.
@@ -899,18 +851,7 @@
filter.append(')');
return filter.toString();
}
-
- private String buildPeopleLookupWhereClause(String filterParam) {
- return buildPeopleLookupWhereClauseCommon(filterParam, "peopleLookup");
- }
- private String buildPeopleLookupWhereClauseForSuggestion(String filterParam) {
- return buildPeopleLookupWhereClauseCommon(filterParam,
- usePhoneticNameForPeopleLookup()
- ? "peopleLookupWithPhoneticName"
- : "peopleLookup");
- }
-
@Override
public Cursor queryInternal(Uri url, String[] projectionIn,
String selection, String[] selectionArgs, String sort) {
@@ -1079,7 +1020,7 @@
}
case SEARCH_SHORTCUT: {
qb.setTables(PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN);
- qb.setProjectionMap(getCurrentSearchSuggestionsProjectionMap());
+ qb.setProjectionMap(sSearchSuggestionsProjectionMap);
qb.appendWhere(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID + "=");
qb.appendWhere(url.getPathSegments().get(1));
break;
@@ -1472,7 +1413,8 @@
return "people._id IN (SELECT person FROM groupmembership JOIN groups " +
"ON (group_id=groups._id OR " +
"(group_sync_id = groups._sync_id AND " +
- "group_sync_account = groups._sync_account)) "+
+ "group_sync_account = groups._sync_account AND " +
+ "group_sync_account_type = groups._sync_account_type)) "+
"WHERE " + Groups.NAME + "="
+ DatabaseUtils.sqlEscapeString(groupName) + ")";
}
@@ -1511,17 +1453,6 @@
}
}
- private HashMap<String, String> getCurrentSearchSuggestionsProjectionMap() {
- String currentLanguage = Locale.getDefault().getLanguage();
- synchronized (this) {
- if (!currentLanguage.equals(mSearchSuggestionLanguage)) {
- mSearchSuggestionLanguage = currentLanguage;
- updateSuggestColumnTexts();
- }
- }
- return mSearchSuggestionsProjectionMap;
- }
-
/**
* Either sets up the query builder so we can run the proper query against the database
* and returns null, or returns a cursor with the results already in it.
@@ -1532,7 +1463,7 @@
*/
private Cursor handleSearchSuggestionsQuery(Uri url, SQLiteQueryBuilder qb) {
qb.setTables(PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN);
- qb.setProjectionMap(getCurrentSearchSuggestionsProjectionMap());
+ qb.setProjectionMap(sSearchSuggestionsProjectionMap);
if (url.getPathSegments().size() > 1) {
// A search term was entered, use it to filter
@@ -1546,7 +1477,7 @@
// match the query
final String searchClause = url.getLastPathSegment();
if (!TextUtils.isDigitsOnly(searchClause)) {
- qb.appendWhere(buildPeopleLookupWhereClauseForSuggestion(searchClause));
+ qb.appendWhere(buildPeopleLookupWhereClause(searchClause));
} else {
final String[] columnNames = new String[] {
"_id",
@@ -1699,18 +1630,19 @@
return mime;
}
- private ContentValues queryAndroidStarredGroupId(String account) {
+ private ContentValues queryAndroidStarredGroupId(Account account) {
String whereString;
String[] whereArgs;
- if (!TextUtils.isEmpty(account)) {
- whereString = "_sync_account=? AND name=?";
- whereArgs = new String[]{account, Groups.GROUP_ANDROID_STARRED};
+ if (account != null) {
+ whereString = "_sync_account=? AND _sync_account_type=? AND name=?";
+ whereArgs = new String[]{account.mName, account.mType, Groups.GROUP_ANDROID_STARRED};
} else {
whereString = "_sync_account is null AND name=?";
whereArgs = new String[]{Groups.GROUP_ANDROID_STARRED};
}
Cursor cursor = getDatabase().query(sGroupsTable,
- new String[]{Groups._ID, Groups._SYNC_ID, Groups._SYNC_ACCOUNT},
+ new String[]{Groups._ID, Groups._SYNC_ID, Groups._SYNC_ACCOUNT,
+ Groups._SYNC_ACCOUNT_TYPE},
whereString, whereArgs, null, null, null);
try {
if (cursor.moveToNext()) {
@@ -1718,6 +1650,7 @@
result.put(Groups._ID, cursor.getLong(0));
result.put(Groups._SYNC_ID, cursor.getString(1));
result.put(Groups._SYNC_ACCOUNT, cursor.getString(2));
+ result.put(Groups._SYNC_ACCOUNT_TYPE, cursor.getString(3));
return result;
}
return null;
@@ -1783,9 +1716,11 @@
if (rowID > 0) {
resultUri = ContentUris.withAppendedId(Groups.CONTENT_URI, rowID);
if (!isTemporary() && newMap.containsKey(Groups.SHOULD_SYNC)) {
- final String account = newMap.getAsString(Groups._SYNC_ACCOUNT);
- if (!TextUtils.isEmpty(account)) {
+ final String accountName = newMap.getAsString(Groups._SYNC_ACCOUNT);
+ final String accountType = newMap.getAsString(Groups._SYNC_ACCOUNT_TYPE);
+ if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
final ContentResolver cr = getContext().getContentResolver();
+ final Account account = new Account(accountName, accountType);
onLocalChangesForAccount(cr, account, false);
}
}
@@ -1804,7 +1739,12 @@
if (rowID > 0) {
resultUri = ContentUris.withAppendedId(People.CONTENT_URI, rowID);
if (!isTemporary()) {
- String account = mValues.getAsString(People._SYNC_ACCOUNT);
+ String accountName = mValues.getAsString(People._SYNC_ACCOUNT);
+ String accountType = mValues.getAsString(People._SYNC_ACCOUNT_TYPE);
+ Account account = null;
+ if (accountName != null || accountType != null) {
+ account = new Account(accountName, accountType);
+ }
Long starredValue = mValues.getAsLong(People.STARRED);
final String syncId = mValues.getAsString(People._SYNC_ID);
boolean isStarred = starredValue != null && starredValue != 0;
@@ -1813,7 +1753,8 @@
mDb.delete(sPhotosTable, "person=" + rowID, null);
mValues.clear();
mValues.put(Photos.PERSON_ID, rowID);
- mValues.put(Photos._SYNC_ACCOUNT, account);
+ mValues.put(Photos._SYNC_ACCOUNT, accountName);
+ mValues.put(Photos._SYNC_ACCOUNT_TYPE, accountType);
mValues.put(Photos._SYNC_ID, syncId);
mValues.put(Photos._SYNC_DIRTY, 0);
mPhotosInserter.insert(mValues);
@@ -1973,19 +1914,26 @@
}
@Override
- protected void onAccountsChanged(String[] accountsArray) {
+ protected void onAccountsChanged(Account[] accountsArray) {
super.onAccountsChanged(accountsArray);
synchronized (mAccountsLock) {
- mAccounts = new String[accountsArray.length];
+ mAccounts = new Account[accountsArray.length];
System.arraycopy(accountsArray, 0, mAccounts, 0, mAccounts.length);
}
}
private void ensureSyncAccountIsSet(ContentValues values) {
synchronized (mAccountsLock) {
- String account = values.getAsString(SyncConstValue._SYNC_ACCOUNT);
+ final String accountName = values.getAsString(SyncConstValue._SYNC_ACCOUNT);
+ final String accountType = values.getAsString(SyncConstValue._SYNC_ACCOUNT_TYPE);
+ Account account = null;
+ if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
+ account = new Account(accountName, accountType);
+ }
if (account == null && mAccounts.length > 0) {
- values.put(SyncConstValue._SYNC_ACCOUNT, mAccounts[0]);
+ // TODO(fredq) change this to pick the account that is syncable for contacts
+ values.put(SyncConstValue._SYNC_ACCOUNT, mAccounts[0].mName);
+ values.put(SyncConstValue._SYNC_ACCOUNT_TYPE, mAccounts[0].mType);
}
}
}
@@ -2008,21 +1956,28 @@
}
private Uri insertIntoGroupmembership(ContentValues values) {
- String groupSyncAccount = values.getAsString(GroupMembership.GROUP_SYNC_ACCOUNT);
+ String groupSyncAccountName = values.getAsString(GroupMembership.GROUP_SYNC_ACCOUNT);
+ String groupSyncAccountType = values.getAsString(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
String groupSyncId = values.getAsString(GroupMembership.GROUP_SYNC_ID);
final Long personId = values.getAsLong(GroupMembership.PERSON_ID);
if (!values.containsKey(GroupMembership.GROUP_ID)) {
- if (TextUtils.isEmpty(groupSyncAccount) || TextUtils.isEmpty(groupSyncId)) {
+ if (TextUtils.isEmpty(groupSyncAccountName) || TextUtils.isEmpty(groupSyncAccountType)
+ || TextUtils.isEmpty(groupSyncId)) {
throw new IllegalArgumentException(
"insertIntoGroupmembership: no GROUP_ID wasn't specified and non-empty "
- + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT fields weren't specifid, "
+ + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT and GROUP_SYNC_ACCOUNT_TYPE fields "
+ + "weren't specifid, "
+ values);
}
if (0 != DatabaseUtils.longForQuery(getDatabase(), ""
+ "SELECT COUNT(*) "
+ "FROM groupmembership "
- + "WHERE group_sync_id=? AND person=?",
- new String[]{groupSyncId, String.valueOf(personId)})) {
+ + "WHERE group_sync_id=? "
+ + " AND group_sync_account=? "
+ + " AND group_sync_account_type=? "
+ + " AND person=?",
+ new String[]{groupSyncId, groupSyncAccountName, groupSyncAccountType,
+ String.valueOf(personId)})) {
final String errorMessage =
"insertIntoGroupmembership: a row with this server key already exists, "
+ values;
@@ -2031,10 +1986,12 @@
}
} else {
long groupId = values.getAsLong(GroupMembership.GROUP_ID);
- if (!TextUtils.isEmpty(groupSyncAccount) || !TextUtils.isEmpty(groupSyncId)) {
+ if (!TextUtils.isEmpty(groupSyncAccountName) || !TextUtils.isEmpty(groupSyncAccountType)
+ || !TextUtils.isEmpty(groupSyncId)) {
throw new IllegalArgumentException(
"insertIntoGroupmembership: GROUP_ID was specified but "
- + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT fields were also specifid, "
+ + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT and GROUP_SYNC_ACCOUNT_TYPE fields "
+ + "were also specifid, "
+ values);
}
if (0 != DatabaseUtils.longForQuery(getDatabase(),
@@ -2064,7 +2021,7 @@
return ContentUris.withAppendedId(GroupMembership.CONTENT_URI, rowId);
}
- private void fixupGroupMembershipAfterPeopleUpdate(String account, long personId,
+ private void fixupGroupMembershipAfterPeopleUpdate(Account account, long personId,
boolean makeStarred) {
ContentValues starredGroupInfo = queryAndroidStarredGroupId(account);
if (makeStarred) {
@@ -2073,17 +2030,22 @@
mValuesLocal.clear();
mValuesLocal.put(Groups.NAME, Groups.GROUP_ANDROID_STARRED);
mValuesLocal.put(Groups._SYNC_DIRTY, 1);
- mValuesLocal.put(Groups._SYNC_ACCOUNT, account);
+ mValuesLocal.put(Groups._SYNC_ACCOUNT, account == null ? null : account.mName);
+ mValuesLocal.put(Groups._SYNC_ACCOUNT_TYPE, account == null ? null : account.mType);
long groupId = mGroupsInserter.insert(mValuesLocal);
starredGroupInfo = new ContentValues();
starredGroupInfo.put(Groups._ID, groupId);
- starredGroupInfo.put(Groups._SYNC_ACCOUNT, account);
+ starredGroupInfo.put(Groups._SYNC_ACCOUNT,
+ mValuesLocal.getAsString(Groups._SYNC_ACCOUNT));
+ starredGroupInfo.put(Groups._SYNC_ACCOUNT_TYPE,
+ mValuesLocal.getAsString(Groups._SYNC_ACCOUNT_TYPE));
// don't put the _SYNC_ID in here since we don't know it yet
}
final Long groupId = starredGroupInfo.getAsLong(Groups._ID);
final String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID);
- final String syncAccount = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
+ final String syncAccountName = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
+ final String syncAccountType = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT_TYPE);
// check that either groupId is set or the syncId/Account is set
final boolean hasSyncId = !TextUtils.isEmpty(syncId);
@@ -2098,17 +2060,21 @@
mValuesLocal.put(GroupMembership.PERSON_ID, personId);
mValuesLocal.put(GroupMembership.GROUP_ID, groupId);
mValuesLocal.put(GroupMembership.GROUP_SYNC_ID, syncId);
- mValuesLocal.put(GroupMembership.GROUP_SYNC_ACCOUNT, syncAccount);
+ mValuesLocal.put(GroupMembership.GROUP_SYNC_ACCOUNT, syncAccountName);
+ mValuesLocal.put(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE, syncAccountType);
mGroupMembershipInserter.insert(mValuesLocal);
} else {
if (starredGroupInfo != null) {
// delete the groupmembership rows for this person that match the starred group id
- String syncAccount = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
+ String syncAccountName = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
+ String syncAccountType = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT_TYPE);
String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID);
if (!TextUtils.isEmpty(syncId)) {
mDb.delete(sGroupmembershipTable,
- "person=? AND group_sync_id=? AND group_sync_account=?",
- new String[]{String.valueOf(personId), syncId, syncAccount});
+ "person=? AND group_sync_id=? AND group_sync_account=?"
+ + " AND group_sync_account_type=?",
+ new String[]{String.valueOf(personId), syncId,
+ syncAccountName, syncAccountType});
} else {
mDb.delete(sGroupmembershipTable, "person=? AND group_id=?",
new String[]{
@@ -2346,11 +2312,14 @@
Cursor cursor = db.query(sPeopleTable, null, where, whereArgs, null, null, null);
try {
final int idxSyncId = cursor.getColumnIndexOrThrow(People._SYNC_ID);
- final int idxSyncAccount = cursor.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
+ final int idxSyncAccountName = cursor.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
+ final int idxSyncAccountType = cursor.getColumnIndexOrThrow(People._SYNC_ACCOUNT_TYPE);
final int idxSyncVersion = cursor.getColumnIndexOrThrow(People._SYNC_VERSION);
final int dstIdxSyncId = mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ID);
- final int dstIdxSyncAccount =
+ final int dstIdxSyncAccountName =
mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ACCOUNT);
+ final int dstIdxSyncAccountType =
+ mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ACCOUNT_TYPE);
final int dstIdxSyncVersion =
mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_VERSION);
while (cursor.moveToNext()) {
@@ -2359,7 +2328,10 @@
// insert into deleted table
mDeletedPeopleInserter.prepareForInsert();
mDeletedPeopleInserter.bind(dstIdxSyncId, syncId);
- mDeletedPeopleInserter.bind(dstIdxSyncAccount, cursor.getString(idxSyncAccount));
+ mDeletedPeopleInserter.bind(dstIdxSyncAccountName,
+ cursor.getString(idxSyncAccountName));
+ mDeletedPeopleInserter.bind(dstIdxSyncAccountType,
+ cursor.getString(idxSyncAccountType));
mDeletedPeopleInserter.bind(dstIdxSyncVersion, cursor.getString(idxSyncVersion));
mDeletedPeopleInserter.execute();
}
@@ -2372,27 +2344,31 @@
}
private int deleteFromGroups(String where, String[] whereArgs) {
- HashSet<String> modifiedAccounts = Sets.newHashSet();
+ HashSet<Account> modifiedAccounts = Sets.newHashSet();
Cursor cursor = getDatabase().query(sGroupsTable, null, where, whereArgs,
null, null, null);
try {
final int indexName = cursor.getColumnIndexOrThrow(Groups.NAME);
final int indexSyncAccount = cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT);
+ final int indexSyncAccountType =
+ cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE);
final int indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID);
final int indexId = cursor.getColumnIndexOrThrow(Groups._ID);
final int indexShouldSync = cursor.getColumnIndexOrThrow(Groups.SHOULD_SYNC);
while (cursor.moveToNext()) {
String oldName = cursor.getString(indexName);
- String syncAccount = cursor.getString(indexSyncAccount);
+ String syncAccountName = cursor.getString(indexSyncAccount);
+ String syncAccountType = cursor.getString(indexSyncAccountType);
String syncId = cursor.getString(indexSyncId);
boolean shouldSync = cursor.getLong(indexShouldSync) != 0;
long id = cursor.getLong(indexId);
fixupPeopleStarredOnGroupRename(oldName, null, id);
- if (!TextUtils.isEmpty(syncAccount) && !TextUtils.isEmpty(syncId)) {
- fixupPeopleStarredOnGroupRename(oldName, null, syncAccount, syncId);
+ if (!TextUtils.isEmpty(syncAccountName) && !TextUtils.isEmpty(syncId)) {
+ fixupPeopleStarredOnGroupRename(oldName, null,
+ new Account(syncAccountName, syncAccountType), syncId);
}
- if (!TextUtils.isEmpty(syncAccount) && shouldSync) {
- modifiedAccounts.add(syncAccount);
+ if (!TextUtils.isEmpty(syncAccountName) && shouldSync) {
+ modifiedAccounts.add(new Account(syncAccountName, syncAccountType));
}
}
} finally {
@@ -2403,7 +2379,7 @@
if (numRows > 0) {
if (!isTemporary()) {
final ContentResolver cr = getContext().getContentResolver();
- for (String account : modifiedAccounts) {
+ for (Account account : modifiedAccounts) {
onLocalChangesForAccount(cr, account, true);
}
}
@@ -2418,7 +2394,7 @@
* @param resolver the content resolver to use
* @param account the account the changes are tied to
*/
- protected void onLocalChangesForAccount(final ContentResolver resolver, String account,
+ protected void onLocalChangesForAccount(final ContentResolver resolver, Account account,
boolean groupsModified) {
// Do nothing
}
@@ -2603,7 +2579,9 @@
Cursor c = mDb.query(sPeopleTable, null,
where, whereArgs, null, null, null);
try {
- int indexAccount = c.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
+ int indexAccountName = c.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
+ int indexAccountType =
+ c.getColumnIndexOrThrow(People._SYNC_ACCOUNT_TYPE);
int indexId = c.getColumnIndexOrThrow(People._ID);
Long starredValue = values.getAsLong(People.STARRED);
Long primaryPhone = values.getAsLong(People.PRIMARY_PHONE_ID);
@@ -2613,7 +2591,13 @@
while (c.moveToNext()) {
final long personId = c.getLong(indexId);
if (hasStarred) {
- fixupGroupMembershipAfterPeopleUpdate(c.getString(indexAccount),
+ final String accountName = c.getString(indexAccountName);
+ final String accountType = c.getString(indexAccountType);
+ final Account account =
+ (accountName != null || accountType != null)
+ ? new Account(accountName, accountType)
+ : null;
+ fixupGroupMembershipAfterPeopleUpdate(account,
personId, starredValue != null && starredValue != 0);
}
@@ -2654,18 +2638,21 @@
private int updateSettings(ContentValues values) {
final SQLiteDatabase db = getDatabase();
- final String account = values.getAsString(Contacts.Settings._SYNC_ACCOUNT);
+ final String accountName = values.getAsString(Contacts.Settings._SYNC_ACCOUNT);
+ final String accountType = values.getAsString(Contacts.Settings._SYNC_ACCOUNT_TYPE);
final String key = values.getAsString(Contacts.Settings.KEY);
if (key == null) {
throw new IllegalArgumentException("you must specify the key when updating settings");
}
+ Account account = null;
+ if (accountName != null || accountType != null) {
+ account = new Account(accountName, accountType);
+ }
if (account == null) {
db.delete(sSettingsTable, "_sync_account IS NULL AND key=?", new String[]{key});
} else {
- if (TextUtils.isEmpty(account)) {
- throw new IllegalArgumentException("account cannot be the empty string, " + values);
- }
- db.delete(sSettingsTable, "_sync_account=? AND key=?", new String[]{account, key});
+ db.delete(sSettingsTable, "_sync_account=? AND _sync_account_type=? AND key=?",
+ new String[]{account.mName, account.mType, key});
}
long rowId = db.insert(sSettingsTable, Contacts.Settings.KEY, values);
if (rowId < 0) {
@@ -2684,7 +2671,7 @@
}
}
- Set<String> modifiedAccounts = Sets.newHashSet();
+ Set<Account> modifiedAccounts = Sets.newHashSet();
final SQLiteDatabase db = getDatabase();
if (values.containsKey(Groups.NAME) || values.containsKey(Groups.SHOULD_SYNC)) {
String newName = values.getAsString(Groups.NAME);
@@ -2692,21 +2679,25 @@
try {
final int indexName = cursor.getColumnIndexOrThrow(Groups.NAME);
final int indexSyncAccount = cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT);
+ final int indexSyncAccountType =
+ cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE);
final int indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID);
final int indexId = cursor.getColumnIndexOrThrow(Groups._ID);
while (cursor.moveToNext()) {
- String syncAccount = cursor.getString(indexSyncAccount);
+ String accountName = cursor.getString(indexSyncAccount);
+ String accountType = cursor.getString(indexSyncAccountType);
if (values.containsKey(Groups.NAME)) {
String oldName = cursor.getString(indexName);
String syncId = cursor.getString(indexSyncId);
long id = cursor.getLong(indexId);
fixupPeopleStarredOnGroupRename(oldName, newName, id);
- if (!TextUtils.isEmpty(syncAccount) && !TextUtils.isEmpty(syncId)) {
- fixupPeopleStarredOnGroupRename(oldName, newName, syncAccount, syncId);
+ if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(syncId)) {
+ fixupPeopleStarredOnGroupRename(oldName, newName,
+ new Account(accountName, accountType), syncId);
}
}
- if (!TextUtils.isEmpty(syncAccount) && values.containsKey(Groups.SHOULD_SYNC)) {
- modifiedAccounts.add(syncAccount);
+ if (!TextUtils.isEmpty(accountName) && values.containsKey(Groups.SHOULD_SYNC)) {
+ modifiedAccounts.add(new Account(accountName, accountType));
}
}
} finally {
@@ -2718,7 +2709,7 @@
if (numRows > 0) {
if (!isTemporary()) {
final ContentResolver cr = getContext().getContentResolver();
- for (String account : modifiedAccounts) {
+ for (Account account : modifiedAccounts) {
onLocalChangesForAccount(cr, account, true);
}
}
@@ -2747,9 +2738,10 @@
}
void fixupPeopleStarredOnGroupRename(String oldName, String newName,
- String syncAccount, String syncId) {
- fixupPeopleStarredOnGroupRename(oldName, newName, "_sync_account=? AND _sync_id=?",
- new String[]{syncAccount, syncId});
+ Account syncAccount, String syncId) {
+ fixupPeopleStarredOnGroupRename(oldName, newName,
+ "_sync_account=? AND _sync_account_type=? AND _sync_id=?",
+ new String[]{syncAccount.mName, syncAccount.mType, syncId});
}
void fixupPeopleStarredOnGroupRename(String oldName, String newName, long groupId) {
@@ -3106,14 +3098,24 @@
// Copy the person
mPeopleInserter.prepareForInsert();
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_ID, mPeopleInserter, mIndexPeopleSyncId);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_TIME, mPeopleInserter, mIndexPeopleSyncTime);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_VERSION, mPeopleInserter, mIndexPeopleSyncVersion);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_DIRTY, mPeopleInserter, mIndexPeopleSyncDirty);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_ACCOUNT, mPeopleInserter, mIndexPeopleSyncAccount);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People.NAME, mPeopleInserter, mIndexPeopleName);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People.PHONETIC_NAME, mPeopleInserter, mIndexPeoplePhoneticName);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People.NOTES, mPeopleInserter, mIndexPeopleNotes);
+ DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
+ People._SYNC_ID, mPeopleInserter, mIndexPeopleSyncId);
+ DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
+ People._SYNC_TIME, mPeopleInserter, mIndexPeopleSyncTime);
+ DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
+ People._SYNC_VERSION, mPeopleInserter, mIndexPeopleSyncVersion);
+ DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
+ People._SYNC_DIRTY, mPeopleInserter, mIndexPeopleSyncDirty);
+ DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
+ People._SYNC_ACCOUNT, mPeopleInserter, mIndexPeopleSyncAccountName);
+ DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
+ People._SYNC_ACCOUNT_TYPE, mPeopleInserter, mIndexPeopleSyncAccountType);
+ DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
+ People.NAME, mPeopleInserter, mIndexPeopleName);
+ DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
+ People.PHONETIC_NAME, mPeopleInserter, mIndexPeoplePhoneticName);
+ DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
+ People.NOTES, mPeopleInserter, mIndexPeopleNotes);
long localPersonID = mPeopleInserter.execute();
Cursor c;
@@ -3133,7 +3135,9 @@
DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_VERSION,
mPhotosInserter, mIndexPhotosSyncVersion);
DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ACCOUNT,
- mPhotosInserter, mIndexPhotosSyncAccount);
+ mPhotosInserter, mIndexPhotosSyncAccountName);
+ DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ACCOUNT_TYPE,
+ mPhotosInserter, mIndexPhotosSyncAccountType);
DatabaseUtils.cursorStringToInsertHelper(c, Photos.EXISTS_ON_SERVER,
mPhotosInserter, mIndexPhotosExistsOnServer);
mPhotosInserter.bind(mIndexPhotosSyncError, (String)null);
@@ -3209,11 +3213,13 @@
final int isPrimaryValue = c.getInt(primaryIndex);
mContactMethodsInserter.prepareForInsert();
mContactMethodsInserter.bind(mIndexContactMethodsPersonId, localPersonID);
- mContactMethodsInserter.bind(mIndexContactMethodsLabel, c.getString(labelIndex));
+ mContactMethodsInserter.bind(mIndexContactMethodsLabel,
+ c.getString(labelIndex));
mContactMethodsInserter.bind(mIndexContactMethodsKind, kind);
mContactMethodsInserter.bind(mIndexContactMethodsType, type);
mContactMethodsInserter.bind(mIndexContactMethodsData, c.getString(dataIndex));
- mContactMethodsInserter.bind(mIndexContactMethodsAuxData, c.getString(auxDataIndex));
+ mContactMethodsInserter.bind(mIndexContactMethodsAuxData,
+ c.getString(auxDataIndex));
mContactMethodsInserter.bind(mIndexContactMethodsIsPrimary, isPrimaryValue);
long rowId = mContactMethodsInserter.execute();
if ((kind == Contacts.KIND_EMAIL) && (isPrimaryValue != 0)) {
@@ -3258,7 +3264,8 @@
mOrganizationsInserter.bind(mIndexOrganizationsPersonId, localPersonID);
mOrganizationsInserter.bind(mIndexOrganizationsLabel, c.getString(labelIndex));
mOrganizationsInserter.bind(mIndexOrganizationsType, type);
- mOrganizationsInserter.bind(mIndexOrganizationsCompany, c.getString(companyIndex));
+ mOrganizationsInserter.bind(mIndexOrganizationsCompany,
+ c.getString(companyIndex));
mOrganizationsInserter.bind(mIndexOrganizationsTitle, c.getString(titleIndex));
mOrganizationsInserter.bind(mIndexOrganizationsIsPrimary, isPrimaryValue);
long rowId = mOrganizationsInserter.execute();
@@ -3291,14 +3298,20 @@
c = doSubQuery(diffsDb, sGroupmembershipTable, null, diffsPersonID,
sGroupmembershipTable + "._id");
try {
- final int accountIndex =
+ final int accountNameIndex =
c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ACCOUNT);
+ final int accountTypeIndex =
+ c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
final int idIndex = c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ID);
while(c.moveToNext()) {
mGroupMembershipInserter.prepareForInsert();
mGroupMembershipInserter.bind(mIndexGroupMembershipPersonId, localPersonID);
- mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncAccount, c.getString(accountIndex));
- mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncId, c.getString(idIndex));
+ mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncAccountName,
+ c.getString(accountNameIndex));
+ mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncAccountType,
+ c.getString(accountTypeIndex));
+ mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncId,
+ c.getString(idIndex));
mGroupMembershipInserter.execute();
}
} finally {
@@ -3306,7 +3319,8 @@
}
// Copy all extensions rows
- c = doSubQuery(diffsDb, sExtensionsTable, null, diffsPersonID, sExtensionsTable + "._id");
+ c = doSubQuery(diffsDb, sExtensionsTable, null, diffsPersonID,
+ sExtensionsTable + "._id");
try {
final int nameIndex = c.getColumnIndexOrThrow(Extensions.NAME);
final int valueIndex = c.getColumnIndexOrThrow(Extensions.VALUE);
@@ -3595,6 +3609,8 @@
DatabaseUtils.cursorStringToContentValues(cRemote,
GroupMembership.GROUP_SYNC_ACCOUNT, mValues);
DatabaseUtils.cursorStringToContentValues(cRemote,
+ GroupMembership.GROUP_SYNC_ACCOUNT_TYPE, mValues);
+ DatabaseUtils.cursorStringToContentValues(cRemote,
GroupMembership.GROUP_SYNC_ID, mValues);
if (joinResult == CursorJoiner.Result.RIGHT) {
mValues.put(GroupMembership.PERSON_ID, localPersonID);
@@ -3660,7 +3676,10 @@
if(cRemote.moveToNext()) {
mValues.clear();
DatabaseUtils.cursorStringToContentValues(cRemote, Photos._SYNC_ID, mValues);
- DatabaseUtils.cursorStringToContentValues(cRemote, Photos._SYNC_ACCOUNT, mValues);
+ DatabaseUtils.cursorStringToContentValues(cRemote,
+ Photos._SYNC_ACCOUNT, mValues);
+ DatabaseUtils.cursorStringToContentValues(cRemote,
+ Photos._SYNC_ACCOUNT_TYPE, mValues);
db.update(sPhotosTable, mValues, Photos.PERSON_ID + '=' + localPersonID, null);
}
} finally {
@@ -3687,6 +3706,8 @@
DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_TIME, mValues);
DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_VERSION, mValues);
DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_ACCOUNT, mValues);
+ DatabaseUtils.cursorStringToContentValues(diffsCursor,
+ People._SYNC_ACCOUNT_TYPE, mValues);
DatabaseUtils.cursorStringToContentValues(diffsCursor, People.NAME, mValues);
DatabaseUtils.cursorStringToContentValues(diffsCursor, People.PHONETIC_NAME, mValues);
DatabaseUtils.cursorStringToContentValues(diffsCursor, People.NOTES, mValues);
@@ -3794,6 +3815,7 @@
DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_TIME, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_VERSION, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT, mValues);
+ DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT_TYPE, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups.NAME, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups.NOTES, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups.SYSTEM_ID, mValues);
@@ -3816,16 +3838,21 @@
// We may have just synced the metadata for a groups we previously marked for
// syncing.
final ContentResolver cr = getContext().getContentResolver();
- final String account = mValues.getAsString(Groups._SYNC_ACCOUNT);
- onLocalChangesForAccount(cr, account, false);
+ final String accountName = mValues.getAsString(Groups._SYNC_ACCOUNT);
+ final String accountType = mValues.getAsString(Groups._SYNC_ACCOUNT_TYPE);
+ onLocalChangesForAccount(cr, new Account(accountName, accountType), false);
}
String oldName = null;
String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME));
- String account = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
+ String accountName = cursor.getString(
+ cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
+ String accountType = cursor.getString(
+ cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE));
String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID));
// this must come after the insert, otherwise the join won't work
- fixupPeopleStarredOnGroupRename(oldName, newName, account, syncId);
+ fixupPeopleStarredOnGroupRename(oldName, newName, new Account(accountName, accountType),
+ syncId);
}
@Override
@@ -3846,16 +3873,21 @@
String oldName = DatabaseUtils.stringForQuery(db,
"select name from groups where _id=" + localRowId, null);
String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME));
- String account = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
+ String accountName = cursor.getString(
+ cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
+ String accountType = cursor.getString(
+ cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE));
String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID));
// this can come before or after the delete
- fixupPeopleStarredOnGroupRename(oldName, newName, account, syncId);
+ fixupPeopleStarredOnGroupRename(oldName, newName,
+ new Account(accountName, accountType), syncId);
mValues.clear();
DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ID, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_TIME, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_VERSION, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT, mValues);
+ DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT_TYPE, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups.NAME, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups.NOTES, mValues);
DatabaseUtils.cursorStringToContentValues(cursor, Groups.SYSTEM_ID, mValues);
@@ -3874,7 +3906,9 @@
c.moveToNext();
String oldName = c.getString(c.getColumnIndexOrThrow(Groups.NAME));
String newName = null;
- String account = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
+ String accountName = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
+ String accountType = c.getString(
+ c.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE));
String syncId = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ID));
String systemId = c.getString(c.getColumnIndexOrThrow(Groups.SYSTEM_ID));
if (!TextUtils.isEmpty(systemId)) {
@@ -3887,7 +3921,8 @@
}
// this must come before the delete, since the join won't work once this row is gone
- fixupPeopleStarredOnGroupRename(oldName, newName, account, syncId);
+ fixupPeopleStarredOnGroupRename(oldName, newName,
+ new Account(accountName, accountType), syncId);
} finally {
c.close();
}
@@ -4096,6 +4131,7 @@
private static final HashMap<String, String> sPresenceProjectionMap;
private static final HashMap<String, String> sEmailSearchProjectionMap;
private static final HashMap<String, String> sOrganizationsProjectionMap;
+ private static final HashMap<String, String> sSearchSuggestionsProjectionMap;
private static final HashMap<String, String> sGroupMembershipProjectionMap;
private static final HashMap<String, String> sPhotosProjectionMap;
private static final HashMap<String, String> sExtensionsProjectionMap;
@@ -4132,7 +4168,7 @@
+ "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') "
+ "THEN phonetic_name "
+ "ELSE "
- + "(CASE WHEN (name is NOT NULL AND name != '') "
+ + "(CASE WHEN (name is NOT NULL AND name != '')"
+ "THEN name "
+ "ELSE "
+ "(CASE WHEN primary_email IS NOT NULL THEN "
@@ -4149,9 +4185,6 @@
+ "END"
+ ")";
- private static final String NAME_WHEN_SQL
- = " WHEN name is NOT NULL ANd name != '' THEN name";
-
private static final String PRIMARY_ORGANIZATION_WHEN_SQL
= " WHEN primary_organization is NOT NULL THEN "
+ "(SELECT company FROM organizations WHERE organizations._id = primary_organization)";
@@ -4205,92 +4238,6 @@
+ " THEN " + Presence.getPresenceIconResourceId(status);
}
-
- // This is similar to DISPLAY_NAME_SQL. Only difference is that this prioritize
- // phonetic_name.
- private static final String PHONETIC_LOOKUP_STRING_SQL =
- "GET_NORMALIZED_STRING("
- + "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') "
- + "THEN phonetic_name "
- + "ELSE "
- + "(CASE WHEN (name is NOT NULL AND name != '') "
- + "THEN name "
- + "ELSE "
- + "(CASE WHEN primary_organization is NOT NULL THEN "
- + "(SELECT company FROM organizations WHERE "
- + "organizations._id = primary_organization) "
- + "ELSE "
- + "(CASE WHEN primary_phone IS NOT NULL THEN "
- +"(SELECT number FROM phones WHERE phones._id = primary_phone) "
- + "ELSE "
- + "(CASE WHEN primary_email IS NOT NULL THEN "
- + "(SELECT data FROM contact_methods WHERE "
- + "contact_methods._id = primary_email) "
- + "ELSE "
- + "null "
- + "END) "
- + "END) "
- + "END) "
- + "END) "
- + "END)";
-
- private static final String PHONETIC_SUGGEST_DESCRIPTION_SQL =
- "(CASE"
- + " WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') THEN "
- // PHONETIC_LOOKUP_STRING_SQL returns phonetic_name. try name, org, phone, email
- + "(CASE"
- + NAME_WHEN_SQL
- + PRIMARY_ORGANIZATION_WHEN_SQL
- + PRIMARY_PHONE_WHEN_SQL
- + PRIMARY_EMAIL_WHEN_SQL
- + " ELSE null END)"
- // PHONETIC_LOOKUP_STRING_SQL returns name, try org, phone, email
- + " WHEN (name IS NOT NULL AND name != '') THEN "
- + "(CASE"
- + PRIMARY_ORGANIZATION_WHEN_SQL
- + PRIMARY_PHONE_WHEN_SQL
- + PRIMARY_EMAIL_WHEN_SQL
- + " ELSE null END)"
- // PHONETIC_LOOKUP_STRING_SQL returns org, try phone, email
- + " WHEN primary_organization is NOT NULL THEN "
- + "(CASE"
- + PRIMARY_PHONE_WHEN_SQL
- + PRIMARY_EMAIL_WHEN_SQL
- + " ELSE null END)"
- // PHONETIC_LOOKUP_STRING_SQL returns phone, try email
- + " WHEN primary_phone IS NOT NULL THEN "
- + "(CASE"
- + PRIMARY_EMAIL_WHEN_SQL
- + " ELSE null END)"
- // PHONETIC_LOOKUP_STRING_SQL returns email or NULL, return NULL
- + " ELSE null END)";
-
- // "primary_organization" etc. are not considered here, since peopleLookup does not
- // consider them either.
- private static final String PHONETIC_LOOKUP_SQL_SIMPLE =
- "GET_NORMALIZED_STRING("
- + "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') "
- + "THEN phonetic_name "
- + "ELSE "
- + "(CASE WHEN (name is NOT NULL AND name != '') "
- + "THEN name "
- + "ELSE "
- + "'' "
- + "END) "
- + "END)";
-
- private static final String PHONETIC_LOOKUP_SQL_SIMPLE_WITH_NEW =
- "GET_NORMALIZED_STRING("
- + "CASE WHEN (new.phonetic_name IS NOT NULL AND new.phonetic_name != '') "
- + "THEN new.phonetic_name "
- + "ELSE "
- + "(CASE WHEN (new.name is NOT NULL AND new.name != '') "
- + "THEN new.name "
- + "ELSE "
- + "'' "
- + "END) "
- + "END)";
-
private static final String[] sPhonesKeyColumns;
private static final String[] sContactMethodsKeyColumns;
private static final String[] sOrganizationsKeyColumns;
@@ -4312,28 +4259,6 @@
return (sb == null) ? "" : sb.toString();
}
- /**
- * @return true when phonetic_name should be considered when looking up people's names.
- */
- private synchronized boolean usePhoneticNameForPeopleLookup() {
- return mSearchSuggestionLanguage.equals(Locale.JAPAN.getLanguage());
- }
-
- private void updateSuggestColumnTexts() {
- if (usePhoneticNameForPeopleLookup()) {
- mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
- PHONETIC_LOOKUP_STRING_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
- mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
- PHONETIC_SUGGEST_DESCRIPTION_SQL + " AS " +
- SearchManager.SUGGEST_COLUMN_TEXT_2);
- } else {
- mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
- DISPLAY_NAME_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
- mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
- SUGGEST_DESCRIPTION_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2);
- }
- }
-
static {
// Contacts URI matching table
UriMatcher matcher = sURIMatcher;
@@ -4463,6 +4388,7 @@
syncColumns.put(SyncConstValue._SYNC_LOCAL_ID, SyncConstValue._SYNC_LOCAL_ID);
syncColumns.put(SyncConstValue._SYNC_DIRTY, SyncConstValue._SYNC_DIRTY);
syncColumns.put(SyncConstValue._SYNC_ACCOUNT, SyncConstValue._SYNC_ACCOUNT);
+ syncColumns.put(SyncConstValue._SYNC_ACCOUNT_TYPE, SyncConstValue._SYNC_ACCOUNT_TYPE);
// Phones columns
HashMap<String, String> phonesColumns = new HashMap<String, String>();
@@ -4491,8 +4417,10 @@
// People with E-mail or IM projection map
map = new HashMap<String, String>();
map.put(People._ID, "people._id AS " + People._ID);
- map.put(ContactMethods.DATA, "contact_methods." + ContactMethods.DATA + " AS " + ContactMethods.DATA);
- map.put(ContactMethods.KIND, "contact_methods." + ContactMethods.KIND + " AS " + ContactMethods.KIND);
+ map.put(ContactMethods.DATA,
+ "contact_methods." + ContactMethods.DATA + " AS " + ContactMethods.DATA);
+ map.put(ContactMethods.KIND,
+ "contact_methods." + ContactMethods.KIND + " AS " + ContactMethods.KIND);
map.putAll(peopleColumns);
sPeopleWithEmailOrImProjectionMap = map;
@@ -4509,6 +4437,7 @@
map.put(GroupMembership.PERSON_ID, GroupMembership.PERSON_ID);
map.put(GroupMembership.GROUP_ID, "groups._id AS " + GroupMembership.GROUP_ID);
map.put(GroupMembership.GROUP_SYNC_ACCOUNT, GroupMembership.GROUP_SYNC_ACCOUNT);
+ map.put(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE, GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
map.put(GroupMembership.GROUP_SYNC_ID, GroupMembership.GROUP_SYNC_ID);
map.putAll(groupsColumns);
sGroupMembershipProjectionMap = map;
@@ -4597,6 +4526,27 @@
map.putAll(peopleColumns);
sPresenceProjectionMap = map;
+ // Search suggestions projection map
+ map = new HashMap<String, String>();
+ map.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
+ DISPLAY_NAME_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
+ map.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SUGGEST_DESCRIPTION_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2);
+ map.put(SearchManager.SUGGEST_COLUMN_ICON_1,
+ "(CASE WHEN " + Photos.DATA + " IS NOT NULL"
+ + " THEN '" + People.CONTENT_URI + "/' || people._id ||"
+ + " '/" + Photos.CONTENT_DIRECTORY + "/data'"
+ + " ELSE " + com.android.internal.R.drawable.ic_contact_picture
+ + " END) AS " + SearchManager.SUGGEST_COLUMN_ICON_1);
+ map.put(SearchManager.SUGGEST_COLUMN_ICON_2,
+ PRESENCE_ICON_SQL + " AS " + SearchManager.SUGGEST_COLUMN_ICON_2);
+ map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
+ "people._id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
+ map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
+ "people._id AS " + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
+ map.put(People._ID, "people._id AS " + People._ID);
+ sSearchSuggestionsProjectionMap = map;
+
// Photos projection map
map = new HashMap<String, String>();
map.put(Photos._ID, Photos._ID);
@@ -4628,18 +4578,22 @@
ContactMethods.DATA, ContactMethods.KIND);
sOrganizationsKeyOrderBy = buildOrderBy(sOrganizationsTable, Organizations.COMPANY);
sGroupmembershipKeyOrderBy =
- buildOrderBy(sGroupmembershipTable, GroupMembership.GROUP_SYNC_ACCOUNT);
+ buildOrderBy(sGroupmembershipTable, GroupMembership.GROUP_SYNC_ACCOUNT_TYPE,
+ GroupMembership.GROUP_SYNC_ACCOUNT);
sPhonesKeyColumns = new String[]{Phones.NUMBER};
sContactMethodsKeyColumns = new String[]{ContactMethods.DATA, ContactMethods.KIND};
sOrganizationsKeyColumns = new String[]{Organizations.COMPANY};
- sGroupmembershipKeyColumns = new String[]{GroupMembership.GROUP_SYNC_ACCOUNT};
+ sGroupmembershipKeyColumns = new String[]{GroupMembership.GROUP_SYNC_ACCOUNT,
+ GroupMembership.GROUP_SYNC_ACCOUNT_TYPE};
sExtensionsKeyColumns = new String[]{Extensions.NAME};
String groupJoinByLocalId = "groups._id=groupmembership.group_id";
String groupJoinByServerId = "("
+ "groups._sync_account=groupmembership.group_sync_account"
+ " AND "
+ + "groups._sync_account_type=groupmembership.group_sync_account_type"
+ + " AND "
+ "groups._sync_id=groupmembership.group_sync_id"
+ ")";
sGroupsJoinString = "(" + groupJoinByLocalId + " OR " + groupJoinByServerId + ")";
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
new file mode 100644
index 0000000..48cdac8
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -0,0 +1,3182 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts;
+
+import com.android.internal.content.SyncStateContentProviderHelper;
+import com.android.internal.database.ArrayListCursor;
+import com.android.providers.contacts.OpenHelper.AggregationExceptionColumns;
+import com.android.providers.contacts.OpenHelper.Clauses;
+import com.android.providers.contacts.OpenHelper.ContactsColumns;
+import com.android.providers.contacts.OpenHelper.DataColumns;
+import com.android.providers.contacts.OpenHelper.GroupsColumns;
+import com.android.providers.contacts.OpenHelper.MimetypesColumns;
+import com.android.providers.contacts.OpenHelper.PackagesColumns;
+import com.android.providers.contacts.OpenHelper.PhoneColumns;
+import com.android.providers.contacts.OpenHelper.PhoneLookupColumns;
+import com.android.providers.contacts.OpenHelper.RawContactsColumns;
+import com.android.providers.contacts.OpenHelper.Tables;
+import com.google.android.collect.Lists;
+
+import android.accounts.Account;
+import android.app.SearchManager;
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.OperationApplicationException;
+import android.content.UriMatcher;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteCursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.database.sqlite.SQLiteStatement;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.Contacts.Intents;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+
+/**
+ * Contacts content provider. The contract between this provider and applications
+ * is defined in {@link ContactsContract}.
+ */
+public class ContactsProvider2 extends ContentProvider {
+ // TODO: clean up debug tag and rename this class
+ private static final String TAG = "ContactsProvider ~~~~";
+
+ // TODO: carefully prevent all incoming nested queries; they can be gaping security holes
+ // TODO: check for restricted flag during insert(), update(), and delete() calls
+
+ /** Default for the maximum number of returned aggregation suggestions. */
+ private static final int DEFAULT_MAX_SUGGESTIONS = 5;
+
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, "
+ + Contacts.TIMES_CONTACTED + " DESC, "
+ + Contacts.DISPLAY_NAME + " ASC";
+ private static final String STREQUENT_LIMIT =
+ "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
+ + Contacts.STARRED + "=1) + 25";
+
+ private static final int CONTACTS = 1000;
+ private static final int CONTACTS_ID = 1001;
+ private static final int CONTACTS_DATA = 1002;
+ private static final int CONTACTS_SUMMARY = 1003;
+ private static final int CONTACTS_RAW_CONTACTS = 1004;
+ private static final int CONTACTS_SUMMARY_ID = 1005;
+ private static final int CONTACTS_SUMMARY_FILTER = 1006;
+ private static final int CONTACTS_SUMMARY_STREQUENT = 1007;
+ private static final int CONTACTS_SUMMARY_STREQUENT_FILTER = 1008;
+ private static final int CONTACTS_SUMMARY_GROUP = 1009;
+
+ private static final int RAW_CONTACTS = 2002;
+ private static final int RAW_CONTACTS_ID = 2003;
+ private static final int RAW_CONTACTS_DATA = 2004;
+ private static final int CONTACTS_FILTER_EMAIL = 2005;
+
+ private static final int DATA = 3000;
+ private static final int DATA_ID = 3001;
+ private static final int PHONES = 3002;
+ private static final int PHONES_FILTER = 3003;
+ private static final int POSTALS = 3004;
+
+ private static final int PHONE_LOOKUP = 4000;
+
+ private static final int AGGREGATION_EXCEPTIONS = 6000;
+ private static final int AGGREGATION_EXCEPTION_ID = 6001;
+
+ private static final int PRESENCE = 7000;
+ private static final int PRESENCE_ID = 7001;
+
+ private static final int AGGREGATION_SUGGESTIONS = 8000;
+
+ private static final int GROUPS = 10000;
+ private static final int GROUPS_ID = 10001;
+ private static final int GROUPS_SUMMARY = 10003;
+
+ private static final int SYNCSTATE = 11000;
+
+ private static final int SEARCH_SUGGESTIONS = 12001;
+ private static final int SEARCH_SHORTCUT = 12002;
+
+ private interface ContactsQuery {
+ public static final String TABLE = Tables.RAW_CONTACTS;
+
+ public static final String[] PROJECTION = new String[] {
+ RawContactsColumns.CONCRETE_ID,
+ RawContacts.ACCOUNT_NAME,
+ RawContacts.ACCOUNT_TYPE,
+ };
+
+ public static final int RAW_CONTACT_ID = 0;
+ public static final int ACCOUNT_NAME = 1;
+ public static final int ACCOUNT_TYPE = 2;
+ }
+
+ private interface DataRawContactsQuery {
+ public static final String TABLE = Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS;
+
+ public static final String[] PROJECTION = new String[] {
+ RawContactsColumns.CONCRETE_ID,
+ DataColumns.CONCRETE_ID,
+ RawContacts.CONTACT_ID,
+ RawContacts.IS_RESTRICTED,
+ Data.MIMETYPE,
+ };
+
+ public static final int RAW_CONTACT_ID = 0;
+ public static final int DATA_ID = 1;
+ public static final int CONTACT_ID = 2;
+ public static final int IS_RESTRICTED = 3;
+ public static final int MIMETYPE = 4;
+ }
+
+ private interface DataContactsQuery {
+ public static final String TABLE = Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS;
+
+ public static final String[] PROJECTION = new String[] {
+ RawContactsColumns.CONCRETE_ID,
+ DataColumns.CONCRETE_ID,
+ ContactsColumns.CONCRETE_ID,
+ MimetypesColumns.CONCRETE_ID,
+ Phone.NUMBER,
+ Email.DATA,
+ ContactsColumns.OPTIMAL_PRIMARY_PHONE_ID,
+ ContactsColumns.FALLBACK_PRIMARY_PHONE_ID,
+ ContactsColumns.OPTIMAL_PRIMARY_EMAIL_ID,
+ ContactsColumns.FALLBACK_PRIMARY_EMAIL_ID,
+ };
+
+ public static final int RAW_CONTACT_ID = 0;
+ public static final int DATA_ID = 1;
+ public static final int CONTACT_ID = 2;
+ public static final int MIMETYPE_ID = 3;
+ public static final int PHONE_NUMBER = 4;
+ public static final int EMAIL_DATA = 5;
+ public static final int OPTIMAL_PHONE_ID = 6;
+ public static final int FALLBACK_PHONE_ID = 7;
+ public static final int OPTIMAL_EMAIL_ID = 8;
+ public static final int FALLBACK_EMAIL_ID = 9;
+
+ }
+
+ private interface DisplayNameQuery {
+ public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
+
+ public static final String[] COLUMNS = new String[] {
+ MimetypesColumns.MIMETYPE,
+ Data.IS_PRIMARY,
+ Data.DATA2,
+ StructuredName.DISPLAY_NAME,
+ };
+
+ public static final int MIMETYPE = 0;
+ public static final int IS_PRIMARY = 1;
+ public static final int DATA2 = 2;
+ public static final int DISPLAY_NAME = 3;
+ }
+
+ private interface DataQuery {
+ public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
+
+ public static final String[] COLUMNS = new String[] {
+ DataColumns.CONCRETE_ID,
+ MimetypesColumns.MIMETYPE,
+ Data.RAW_CONTACT_ID,
+ Data.IS_PRIMARY,
+ Data.DATA1,
+ Data.DATA2,
+ Data.DATA3,
+ Data.DATA4,
+ Data.DATA5,
+ Data.DATA6,
+ Data.DATA7,
+ Data.DATA8,
+ Data.DATA9,
+ Data.DATA10,
+ Data.DATA11,
+ Data.DATA12,
+ Data.DATA13,
+ Data.DATA14,
+ Data.DATA15,
+ };
+
+ public static final int ID = 0;
+ public static final int MIMETYPE = 1;
+ public static final int RAW_CONTACT_ID = 2;
+ public static final int IS_PRIMARY = 3;
+ public static final int DATA1 = 4;
+ public static final int DATA2 = 5;
+ public static final int DATA3 = 6;
+ public static final int DATA4 = 7;
+ public static final int DATA5 = 8;
+ public static final int DATA6 = 9;
+ public static final int DATA7 = 10;
+ public static final int DATA8 = 11;
+ public static final int DATA9 = 12;
+ public static final int DATA10 = 13;
+ public static final int DATA11 = 14;
+ public static final int DATA12 = 15;
+ public static final int DATA13 = 16;
+ public static final int DATA14 = 17;
+ public static final int DATA15 = 18;
+ }
+
+ private interface DataIdQuery {
+ String[] COLUMNS = { Data._ID };
+
+ int _ID = 0;
+ }
+
+ // Higher number represents higher priority in choosing what data to use for the display name
+ private static final int DISPLAY_NAME_PRIORITY_EMAIL = 1;
+ private static final int DISPLAY_NAME_PRIORITY_PHONE = 2;
+ private static final int DISPLAY_NAME_PRIORITY_ORGANIZATION = 3;
+ private static final int DISPLAY_NAME_PRIORITY_STRUCTURED_NAME = 4;
+
+ private static final HashMap<String, Integer> sDisplayNamePriorities;
+ static {
+ sDisplayNamePriorities = new HashMap<String, Integer>();
+ sDisplayNamePriorities.put(StructuredName.CONTENT_ITEM_TYPE,
+ DISPLAY_NAME_PRIORITY_STRUCTURED_NAME);
+ sDisplayNamePriorities.put(Organization.CONTENT_ITEM_TYPE,
+ DISPLAY_NAME_PRIORITY_ORGANIZATION);
+ sDisplayNamePriorities.put(Phone.CONTENT_ITEM_TYPE,
+ DISPLAY_NAME_PRIORITY_PHONE);
+ sDisplayNamePriorities.put(Email.CONTENT_ITEM_TYPE,
+ DISPLAY_NAME_PRIORITY_EMAIL);
+ }
+
+ /** Contains just the contacts columns */
+ private static final HashMap<String, String> sContactsProjectionMap;
+ /** Contains the contact columns along with primary phone */
+ private static final HashMap<String, String> sContactsSummaryProjectionMap;
+ /** Contains the data, contacts, and contact columns, for joined tables. */
+ private static final HashMap<String, String> sDataRawContactsContactProjectionMap;
+ /** Contains the data, contacts, group sourceid and contact columns, for joined tables. */
+ private static final HashMap<String, String> sDataRawContactsGroupsContactProjectionMap;
+ /** Contains the contacts, and raw contact columns, for joined tables. */
+ private static final HashMap<String, String> sRawContactsContactsProjectionMap;
+ /** Contains just the contacts columns */
+ private static final HashMap<String, String> sRawContactsProjectionMap;
+ /** Contains just the data columns */
+ private static final HashMap<String, String> sDataGroupsProjectionMap;
+ /** Contains the data and contacts columns, for joined tables */
+ private static final HashMap<String, String> sDataRawContactsGroupsProjectionMap;
+ /** Contains the data and contacts columns, for joined tables */
+ private static final HashMap<String, String> sDataRawContactsProjectionMap;
+ /** Contains the just the {@link Groups} columns */
+ private static final HashMap<String, String> sGroupsProjectionMap;
+ /** Contains {@link Groups} columns along with summary details */
+ private static final HashMap<String, String> sGroupsSummaryProjectionMap;
+ /** Contains the agg_exceptions columns */
+ private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
+ /** Contains Presence columns */
+ private static final HashMap<String, String> sPresenceProjectionMap;
+
+ /** Sql select statement that returns the contact id associated with a data record. */
+ private static final String sNestedRawContactIdSelect;
+ /** Sql select statement that returns the mimetype id associated with a data record. */
+ private static final String sNestedMimetypeSelect;
+ /** Sql select statement that returns the contact id associated with a contact record. */
+ private static final String sNestedContactIdSelect;
+ /** Sql select statement that returns a list of contact ids associated with an contact record. */
+ private static final String sNestedContactIdListSelect;
+ /** Sql where statement used to match all the data records that need to be updated when a new
+ * "primary" is selected.*/
+ private static final String sSetPrimaryWhere;
+ /** Sql where statement used to match all the data records that need to be updated when a new
+ * "super primary" is selected.*/
+ private static final String sSetSuperPrimaryWhere;
+ /** Sql where statement for filtering on groups. */
+ private static final String sContactsInGroupSelect;
+ /** Precompiled sql statement for setting a data record to the primary. */
+ private SQLiteStatement mSetPrimaryStatement;
+ /** Precompiled sql statement for setting a data record to the super primary. */
+ private SQLiteStatement mSetSuperPrimaryStatement;
+ /** Precompiled sql statement for incrementing times contacted for an contact */
+ private SQLiteStatement mLastTimeContactedUpdate;
+ /** Precompiled sql statement for updating a contact display name */
+ private SQLiteStatement mContactDisplayNameUpdate;
+
+ static {
+ // Contacts URI matching table
+ final UriMatcher matcher = sUriMatcher;
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/raw_contacts", CONTACTS_RAW_CONTACTS);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary", CONTACTS_SUMMARY);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/#", CONTACTS_SUMMARY_ID);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/filter/*",
+ CONTACTS_SUMMARY_FILTER);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/strequent/",
+ CONTACTS_SUMMARY_STREQUENT);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/strequent/filter/*",
+ CONTACTS_SUMMARY_STREQUENT_FILTER);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts_summary/group/*",
+ CONTACTS_SUMMARY_GROUP);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
+ AGGREGATION_SUGGESTIONS);
+ matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
+ matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
+ matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
+ matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/filter_email/*",
+ CONTACTS_FILTER_EMAIL);
+
+ matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
+ matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
+ matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
+ matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
+ matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
+
+ matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
+ matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
+ matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
+
+ matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
+
+ matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
+ matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
+ AGGREGATION_EXCEPTIONS);
+ matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
+ AGGREGATION_EXCEPTION_ID);
+
+ matcher.addURI(ContactsContract.AUTHORITY, "presence", PRESENCE);
+ matcher.addURI(ContactsContract.AUTHORITY, "presence/#", PRESENCE_ID);
+
+ matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
+ SEARCH_SUGGESTIONS);
+ matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
+ SEARCH_SUGGESTIONS);
+ matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
+ SEARCH_SHORTCUT);
+
+ HashMap<String, String> columns;
+
+ // Contacts projection map
+ columns = new HashMap<String, String>();
+ columns.put(Contacts._ID, "contacts._id AS _id");
+ columns.put(Contacts.DISPLAY_NAME, ContactsColumns.CONCRETE_DISPLAY_NAME + " AS "
+ + Contacts.DISPLAY_NAME);
+ columns.put(Contacts.LAST_TIME_CONTACTED, ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
+ + " AS " + Contacts.LAST_TIME_CONTACTED);
+ columns.put(Contacts.TIMES_CONTACTED, ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS "
+ + Contacts.TIMES_CONTACTED);
+ columns.put(Contacts.STARRED, ContactsColumns.CONCRETE_STARRED + " AS "
+ + Contacts.STARRED);
+ columns.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
+ columns.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
+ columns.put(Contacts.PRIMARY_PHONE_ID, Contacts.PRIMARY_PHONE_ID);
+ columns.put(Contacts.PRIMARY_EMAIL_ID, Contacts.PRIMARY_EMAIL_ID);
+ columns.put(Contacts.CUSTOM_RINGTONE, ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS "
+ + Contacts.CUSTOM_RINGTONE);
+ columns.put(Contacts.SEND_TO_VOICEMAIL, ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
+ + " AS " + Contacts.SEND_TO_VOICEMAIL);
+ columns.put(ContactsColumns.FALLBACK_PRIMARY_PHONE_ID,
+ ContactsColumns.FALLBACK_PRIMARY_PHONE_ID);
+ columns.put(ContactsColumns.FALLBACK_PRIMARY_EMAIL_ID,
+ ContactsColumns.FALLBACK_PRIMARY_EMAIL_ID);
+ sContactsProjectionMap = columns;
+
+ columns = new HashMap<String, String>();
+ columns.putAll(sContactsProjectionMap);
+
+ // Contacts primaries projection map. The overall presence status is
+ // the most-present value, as indicated by the largest value.
+ columns.put(Contacts.PRESENCE_STATUS, "MAX(" + Presence.PRESENCE_STATUS + ")");
+ columns.put(Contacts.PRIMARY_PHONE_TYPE, CommonDataKinds.Phone.TYPE);
+ columns.put(Contacts.PRIMARY_PHONE_LABEL, CommonDataKinds.Phone.LABEL);
+ columns.put(Contacts.PRIMARY_PHONE_NUMBER, CommonDataKinds.Phone.NUMBER);
+ sContactsSummaryProjectionMap = columns;
+
+ // RawContacts projection map
+ columns = new HashMap<String, String>();
+ columns.put(RawContacts._ID, Tables.RAW_CONTACTS + "." + RawContacts._ID + " AS _id");
+ columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
+ columns.put(RawContacts.ACCOUNT_NAME,
+ OpenHelper.RawContactsColumns.CONCRETE_ACCOUNT_NAME
+ + " AS " + RawContacts.ACCOUNT_NAME);
+ columns.put(RawContacts.ACCOUNT_TYPE,
+ OpenHelper.RawContactsColumns.CONCRETE_ACCOUNT_TYPE
+ + " AS " + RawContacts.ACCOUNT_TYPE);
+ columns.put(RawContacts.SOURCE_ID,
+ OpenHelper.RawContactsColumns.CONCRETE_SOURCE_ID
+ + " AS " + RawContacts.SOURCE_ID);
+ columns.put(RawContacts.VERSION,
+ OpenHelper.RawContactsColumns.CONCRETE_VERSION
+ + " AS " + RawContacts.VERSION);
+ columns.put(RawContacts.DIRTY,
+ OpenHelper.RawContactsColumns.CONCRETE_DIRTY
+ + " AS " + RawContacts.DIRTY);
+ columns.put(RawContacts.DELETED,
+ OpenHelper.RawContactsColumns.CONCRETE_DELETED
+ + " AS " + RawContacts.DELETED);
+ sRawContactsProjectionMap = columns;
+
+ columns = new HashMap<String, String>();
+ columns.putAll(sContactsProjectionMap);
+ columns.putAll(sRawContactsProjectionMap);
+ sRawContactsContactsProjectionMap = columns;
+
+ // Data projection map
+ columns = new HashMap<String, String>();
+ columns.put(Data._ID, Tables.DATA + "." + Data._ID + " AS _id");
+ columns.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID);
+ columns.put(Data.RES_PACKAGE, PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE);
+ columns.put(Data.MIMETYPE, Data.MIMETYPE);
+ columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
+ columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
+ columns.put(Data.DATA_VERSION, Data.DATA_VERSION);
+ columns.put(Data.DATA1, "data.data1 as data1");
+ columns.put(Data.DATA2, "data.data2 as data2");
+ columns.put(Data.DATA3, "data.data3 as data3");
+ columns.put(Data.DATA4, "data.data4 as data4");
+ columns.put(Data.DATA5, "data.data5 as data5");
+ columns.put(Data.DATA6, "data.data6 as data6");
+ columns.put(Data.DATA7, "data.data7 as data7");
+ columns.put(Data.DATA8, "data.data8 as data8");
+ columns.put(Data.DATA9, "data.data9 as data9");
+ columns.put(Data.DATA10, "data.data10 as data10");
+ columns.put(Data.DATA11, "data.data11 as data11");
+ columns.put(Data.DATA12, "data.data12 as data12");
+ columns.put(Data.DATA13, "data.data13 as data13");
+ columns.put(Data.DATA14, "data.data14 as data14");
+ columns.put(Data.DATA15, "data.data15 as data15");
+ columns.put(GroupMembership.GROUP_SOURCE_ID, GroupsColumns.CONCRETE_SOURCE_ID + " AS "
+ + GroupMembership.GROUP_SOURCE_ID);
+
+ // TODO: remove this projection
+ // Mappings used for backwards compatibility.
+ columns.put("number", Phone.NUMBER);
+ sDataGroupsProjectionMap = columns;
+
+ // Data, groups and contacts projection map for joins. _id comes from the data table
+ columns = new HashMap<String, String>();
+ columns.putAll(sRawContactsProjectionMap);
+ columns.putAll(sDataGroupsProjectionMap); // _id will be replaced with the one from data
+ columns.put(Data.RAW_CONTACT_ID, DataColumns.CONCRETE_RAW_CONTACT_ID);
+ sDataRawContactsGroupsProjectionMap = columns;
+
+ // Data and contacts projection map for joins. _id comes from the data table
+ columns = new HashMap<String, String>();
+ columns.putAll(sDataRawContactsGroupsProjectionMap);
+ columns.remove(GroupMembership.GROUP_SOURCE_ID);
+ sDataRawContactsProjectionMap = columns;
+
+ // Data and contacts projection map for joins. _id comes from the data table
+ columns = new HashMap<String, String>();
+ columns.putAll(sContactsProjectionMap);
+ columns.putAll(sRawContactsProjectionMap); //
+ columns.putAll(sDataGroupsProjectionMap); // _id will be replaced with the one from data
+ columns.put(Data.RAW_CONTACT_ID, DataColumns.CONCRETE_RAW_CONTACT_ID);
+ sDataRawContactsGroupsContactProjectionMap = columns;
+
+ // Data and contacts projection map for joins. _id comes from the data table
+ columns = new HashMap<String, String>();
+ columns.putAll(sDataRawContactsGroupsContactProjectionMap);
+ columns.remove(GroupMembership.GROUP_SOURCE_ID);
+ sDataRawContactsContactProjectionMap = columns;
+
+ // Groups projection map
+ columns = new HashMap<String, String>();
+ columns.put(Groups._ID, "groups._id AS _id");
+ columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
+ columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
+ columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID);
+ columns.put(Groups.DIRTY, Groups.DIRTY);
+ columns.put(Groups.VERSION, Groups.VERSION);
+ columns.put(Groups.RES_PACKAGE, PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE);
+ columns.put(Groups.TITLE, Groups.TITLE);
+ columns.put(Groups.TITLE_RES, Groups.TITLE_RES);
+ columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
+ sGroupsProjectionMap = columns;
+
+ // RawContacts and groups projection map
+ columns = new HashMap<String, String>();
+ columns.putAll(sGroupsProjectionMap);
+
+ columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
+ + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
+ + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
+ + ") AS " + Groups.SUMMARY_COUNT);
+
+ columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
+ + ContactsColumns.CONCRETE_ID + ") FROM "
+ + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
+ + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
+ + " AND " + Clauses.HAS_PRIMARY_PHONE + ") AS " + Groups.SUMMARY_WITH_PHONES);
+
+ sGroupsSummaryProjectionMap = columns;
+
+ // Aggregate exception projection map
+ columns = new HashMap<String, String>();
+ columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
+ columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
+ columns.put(AggregationExceptions.CONTACT_ID,
+ "raw_contacts1." + RawContacts.CONTACT_ID
+ + " AS " + AggregationExceptions.CONTACT_ID);
+ columns.put(AggregationExceptions.RAW_CONTACT_ID, AggregationExceptionColumns.RAW_CONTACT_ID2);
+ sAggregationExceptionsProjectionMap = columns;
+
+
+ columns = new HashMap<String, String>();
+ columns.put(Presence._ID, Presence._ID);
+ columns.put(Presence.RAW_CONTACT_ID, Presence.RAW_CONTACT_ID);
+ columns.put(Presence.DATA_ID, Presence.DATA_ID);
+ columns.put(Presence.IM_ACCOUNT, Presence.IM_ACCOUNT);
+ columns.put(Presence.IM_HANDLE, Presence.IM_HANDLE);
+ columns.put(Presence.IM_PROTOCOL, Presence.IM_PROTOCOL);
+ columns.put(Presence.PRESENCE_STATUS, Presence.PRESENCE_STATUS);
+ columns.put(Presence.PRESENCE_CUSTOM_STATUS, Presence.PRESENCE_CUSTOM_STATUS);
+ sPresenceProjectionMap = columns;
+
+ sNestedRawContactIdSelect = "SELECT " + Data.RAW_CONTACT_ID + " FROM " + Tables.DATA + " WHERE "
+ + Data._ID + "=?";
+ sNestedMimetypeSelect = "SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA
+ + " WHERE " + Data._ID + "=?";
+ sNestedContactIdSelect = "SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS
+ + " WHERE " + RawContacts._ID + "=(" + sNestedRawContactIdSelect + ")";
+ sNestedContactIdListSelect = "SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS
+ + " WHERE " + RawContacts.CONTACT_ID + "=(" + sNestedContactIdSelect + ")";
+ sSetPrimaryWhere = Data.RAW_CONTACT_ID + "=(" + sNestedRawContactIdSelect + ") AND "
+ + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")";
+ sSetSuperPrimaryWhere = Data.RAW_CONTACT_ID + " IN (" + sNestedContactIdListSelect + ") AND "
+ + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")";
+ sContactsInGroupSelect = ContactsColumns.CONCRETE_ID + " IN (SELECT "
+ + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS + " WHERE ("
+ + RawContactsColumns.CONCRETE_ID + " IN (SELECT " + Tables.DATA + "."
+ + Data.RAW_CONTACT_ID + " FROM " + Tables.DATA_JOIN_MIMETYPES + " WHERE ("
+ + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND "
+ + GroupMembership.GROUP_ROW_ID + "=(SELECT " + Tables.GROUPS + "."
+ + Groups._ID + " FROM " + Tables.GROUPS + " WHERE " + Groups.TITLE + "=?)))))";
+ }
+
+ /**
+ * Handles inserts and update for a specific Data type.
+ */
+ private abstract class DataRowHandler {
+
+ protected final String mMimetype;
+
+ public DataRowHandler(String mimetype) {
+ mMimetype = mimetype;
+ }
+
+ /**
+ * Inserts a row into the {@link Data} table.
+ */
+ public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
+ final long dataId = db.insert(Tables.DATA, null, values);
+
+ Integer primary = values.getAsInteger(Data.IS_PRIMARY);
+ if (primary != null && primary != 0) {
+ setIsPrimary(dataId);
+ }
+
+ fixContactDisplayName(db, rawContactId);
+ return dataId;
+ }
+
+ /**
+ * Validates data and updates a {@link Data} row using the cursor, which contains
+ * the current data.
+ */
+ public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
+ throw new UnsupportedOperationException();
+ }
+
+ public int delete(SQLiteDatabase db, Cursor c) {
+ long dataId = c.getLong(DataQuery.ID);
+ long rawContactId = c.getLong(DataQuery.RAW_CONTACT_ID);
+ boolean primary = c.getInt(DataQuery.IS_PRIMARY) != 0;
+ int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
+ if (count != 0 && primary) {
+ fixPrimary(db, rawContactId);
+ fixContactDisplayName(db, rawContactId);
+ }
+ return count;
+ }
+
+ private void fixPrimary(SQLiteDatabase db, long rawContactId) {
+ long newPrimaryId = findNewPrimaryDataId(db, rawContactId);
+ if (newPrimaryId != -1) {
+ ContactsProvider2.this.setIsPrimary(newPrimaryId);
+ }
+ }
+
+ protected long findNewPrimaryDataId(SQLiteDatabase db, long rawContactId) {
+ long primaryId = -1;
+ int primaryType = -1;
+ Cursor c = queryData(db, rawContactId);
+ try {
+ while (c.moveToNext()) {
+ long dataId = c.getLong(DataQuery.ID);
+ int type = c.getInt(DataQuery.DATA2);
+ if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
+ primaryId = dataId;
+ primaryType = type;
+ }
+ }
+ } finally {
+ c.close();
+ }
+ return primaryId;
+ }
+
+ /**
+ * Returns the rank of a specific record type to be used in determining the primary
+ * row. Lower number represents higher priority.
+ */
+ protected int getTypeRank(int type) {
+ return 0;
+ }
+
+ protected Cursor queryData(SQLiteDatabase db, long rawContactId) {
+ // TODO Lookup integer mimetype IDs' instead of joining for speed
+ return db.query(DataQuery.TABLE, DataQuery.COLUMNS, Data.RAW_CONTACT_ID + "="
+ + rawContactId + " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'",
+ null, null, null, null);
+ }
+
+ protected void fixContactDisplayName(SQLiteDatabase db, long rawContactId) {
+ if (!sDisplayNamePriorities.containsKey(mMimetype)) {
+ return;
+ }
+
+ String bestDisplayName = null;
+ Cursor c = db.query(DisplayNameQuery.TABLE, DisplayNameQuery.COLUMNS,
+ Data.RAW_CONTACT_ID + "=" + rawContactId, null, null, null, null);
+ try {
+ int maxPriority = -1;
+ while (c.moveToNext()) {
+ String mimeType = c.getString(DisplayNameQuery.MIMETYPE);
+ boolean primary;
+ String name;
+
+ if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ name = c.getString(DisplayNameQuery.DISPLAY_NAME);
+ primary = true;
+ } else {
+ name = c.getString(DisplayNameQuery.DATA2);
+ primary = (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0);
+ }
+
+ if (primary && name != null) {
+ Integer priority = sDisplayNamePriorities.get(mimeType);
+ if (priority != null && priority > maxPriority) {
+ maxPriority = priority;
+ bestDisplayName = name;
+ }
+ }
+ }
+
+ } finally {
+ c.close();
+ }
+
+ ContactsProvider2.this.setDisplayName(rawContactId, bestDisplayName);
+ }
+ }
+
+ public class CustomDataRowHandler extends DataRowHandler {
+
+ public CustomDataRowHandler(String mimetype) {
+ super(mimetype);
+ }
+ }
+
+ public class StructuredNameRowHandler extends DataRowHandler {
+
+ private final NameSplitter mNameSplitter;
+
+ public StructuredNameRowHandler(NameSplitter nameSplitter) {
+ super(StructuredName.CONTENT_ITEM_TYPE);
+ mNameSplitter = nameSplitter;
+ }
+
+ @Override
+ public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
+ fixStructuredNameComponents(values);
+ return super.insert(db, rawContactId, values);
+ }
+
+ @Override
+ public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
+ // TODO Parse the full name if it has changed and replace pre-existing piece parts.
+ }
+
+ /**
+ * Parses the supplied display name, but only if the incoming values do not already contain
+ * structured name parts. Also, if the display name is not provided, generate one by
+ * concatenating first name and last name
+ *
+ * TODO see if the order of first and last names needs to be conditionally reversed for
+ * some locales, e.g. China.
+ */
+ private void fixStructuredNameComponents(ContentValues values) {
+ String fullName = values.getAsString(StructuredName.DISPLAY_NAME);
+ if (!TextUtils.isEmpty(fullName)
+ && TextUtils.isEmpty(values.getAsString(StructuredName.PREFIX))
+ && TextUtils.isEmpty(values.getAsString(StructuredName.GIVEN_NAME))
+ && TextUtils.isEmpty(values.getAsString(StructuredName.MIDDLE_NAME))
+ && TextUtils.isEmpty(values.getAsString(StructuredName.FAMILY_NAME))
+ && TextUtils.isEmpty(values.getAsString(StructuredName.SUFFIX))) {
+ NameSplitter.Name name = new NameSplitter.Name();
+ mNameSplitter.split(name, fullName);
+
+ values.put(StructuredName.PREFIX, name.getPrefix());
+ values.put(StructuredName.GIVEN_NAME, name.getGivenNames());
+ values.put(StructuredName.MIDDLE_NAME, name.getMiddleName());
+ values.put(StructuredName.FAMILY_NAME, name.getFamilyName());
+ values.put(StructuredName.SUFFIX, name.getSuffix());
+ }
+
+ if (TextUtils.isEmpty(fullName)) {
+ String givenName = values.getAsString(StructuredName.GIVEN_NAME);
+ String familyName = values.getAsString(StructuredName.FAMILY_NAME);
+ if (TextUtils.isEmpty(givenName)) {
+ fullName = familyName;
+ } else if (TextUtils.isEmpty(familyName)) {
+ fullName = givenName;
+ } else {
+ fullName = givenName + " " + familyName;
+ }
+
+ if (!TextUtils.isEmpty(fullName)) {
+ values.put(StructuredName.DISPLAY_NAME, fullName);
+ }
+ }
+ }
+ }
+
+ public class CommonDataRowHandler extends DataRowHandler {
+
+ private final String mTypeColumn;
+ private final String mLabelColumn;
+
+ public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) {
+ super(mimetype);
+ mTypeColumn = typeColumn;
+ mLabelColumn = labelColumn;
+ }
+
+ @Override
+ public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
+ int type;
+ String label;
+ if (values.containsKey(mTypeColumn)) {
+ type = values.getAsInteger(mTypeColumn);
+ } else {
+ type = BaseTypes.TYPE_CUSTOM;
+ }
+ if (values.containsKey(mLabelColumn)) {
+ label = values.getAsString(mLabelColumn);
+ } else {
+ label = null;
+ }
+
+ if (type != BaseTypes.TYPE_CUSTOM && label != null) {
+ throw new RuntimeException(mLabelColumn + " value can only be specified with "
+ + mTypeColumn + "=" + BaseTypes.TYPE_CUSTOM + "(custom)");
+ }
+
+ if (type == BaseTypes.TYPE_CUSTOM && label == null) {
+ throw new RuntimeException(mLabelColumn + " value must be specified when "
+ + mTypeColumn + "=" + BaseTypes.TYPE_CUSTOM + "(custom)");
+ }
+
+ return super.insert(db, rawContactId, values);
+ }
+
+ @Override
+ public void update(SQLiteDatabase db, ContentValues values, Cursor cursor) {
+ // TODO read the data and check the constraint
+ }
+ }
+
+ public class OrganizationDataRowHandler extends CommonDataRowHandler {
+
+ public OrganizationDataRowHandler() {
+ super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL);
+ }
+
+ @Override
+ public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
+ long id = super.insert(db, rawContactId, values);
+ fixContactDisplayName(db, rawContactId);
+ return id;
+ }
+
+ @Override
+ protected int getTypeRank(int type) {
+ switch (type) {
+ case Organization.TYPE_WORK: return 0;
+ case Organization.TYPE_CUSTOM: return 1;
+ case Organization.TYPE_OTHER: return 2;
+ default: return 1000;
+ }
+ }
+ }
+
+ public class EmailDataRowHandler extends CommonDataRowHandler {
+
+ public EmailDataRowHandler() {
+ super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL);
+ }
+
+ @Override
+ public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
+ long id = super.insert(db, rawContactId, values);
+ fixContactDisplayName(db, rawContactId);
+ return id;
+ }
+
+ @Override
+ protected int getTypeRank(int type) {
+ switch (type) {
+ case Email.TYPE_HOME: return 0;
+ case Email.TYPE_WORK: return 1;
+ case Email.TYPE_CUSTOM: return 2;
+ case Email.TYPE_OTHER: return 3;
+ default: return 1000;
+ }
+ }
+ }
+
+ public class PhoneDataRowHandler extends CommonDataRowHandler {
+
+ public PhoneDataRowHandler() {
+ super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL);
+ }
+
+ @Override
+ public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
+ ContentValues phoneValues = new ContentValues();
+ String number = values.getAsString(Phone.NUMBER);
+ String normalizedNumber = null;
+ if (number != null) {
+ normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
+ values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
+ }
+
+ long id = super.insert(db, rawContactId, values);
+
+ if (number != null) {
+ phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
+ phoneValues.put(PhoneLookupColumns.DATA_ID, id);
+ phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
+ db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
+ }
+
+ return id;
+ }
+
+ @Override
+ protected int getTypeRank(int type) {
+ switch (type) {
+ case Phone.TYPE_MOBILE: return 0;
+ case Phone.TYPE_WORK: return 1;
+ case Phone.TYPE_HOME: return 2;
+ case Phone.TYPE_PAGER: return 3;
+ case Phone.TYPE_CUSTOM: return 4;
+ case Phone.TYPE_OTHER: return 5;
+ case Phone.TYPE_FAX_WORK: return 6;
+ case Phone.TYPE_FAX_HOME: return 7;
+ default: return 1000;
+ }
+ }
+ }
+
+ private HashMap<String, DataRowHandler> mDataRowHandlers;
+ private final ContactAggregationScheduler mAggregationScheduler;
+ private OpenHelper mOpenHelper;
+
+ private ContactAggregator mContactAggregator;
+ private NameSplitter mNameSplitter;
+ private LegacyApiSupport mLegacyApiSupport;
+
+ private ContentValues mValues = new ContentValues();
+
+ public ContactsProvider2() {
+ this(new ContactAggregationScheduler());
+ }
+
+ /**
+ * Constructor for testing.
+ */
+ /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) {
+ mAggregationScheduler = scheduler;
+ }
+
+ @Override
+ public boolean onCreate() {
+ final Context context = getContext();
+
+ mOpenHelper = getOpenHelper(context);
+ mLegacyApiSupport = new LegacyApiSupport(context, mOpenHelper, this);
+ mContactAggregator = new ContactAggregator(context, mOpenHelper, mAggregationScheduler);
+
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ mSetPrimaryStatement = db.compileStatement(
+ "UPDATE " + Tables.DATA + " SET " + Data.IS_PRIMARY
+ + "=(_id=?) WHERE " + sSetPrimaryWhere);
+ mSetSuperPrimaryStatement = db.compileStatement(
+ "UPDATE " + Tables.DATA + " SET " + Data.IS_SUPER_PRIMARY
+ + "=(_id=?) WHERE " + sSetSuperPrimaryWhere);
+ mLastTimeContactedUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
+ + RawContacts.TIMES_CONTACTED + "=" + RawContacts.TIMES_CONTACTED + "+1,"
+ + RawContacts.LAST_TIME_CONTACTED + "=? WHERE " + RawContacts.CONTACT_ID + "=?");
+
+ mContactDisplayNameUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
+ + RawContactsColumns.DISPLAY_NAME + "=? WHERE " + RawContacts._ID + "=?");
+
+ mNameSplitter = new NameSplitter(
+ context.getString(com.android.internal.R.string.common_name_prefixes),
+ context.getString(com.android.internal.R.string.common_last_name_prefixes),
+ context.getString(com.android.internal.R.string.common_name_suffixes),
+ context.getString(com.android.internal.R.string.common_name_conjunctions));
+
+ mDataRowHandlers = new HashMap<String, DataRowHandler>();
+
+ mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
+ mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
+ new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
+ mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
+ StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL));
+ mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
+ mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
+ mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
+ Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL));
+ mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
+ new StructuredNameRowHandler(mNameSplitter));
+
+ return (db != null);
+ }
+
+ /* Visible for testing */
+ protected OpenHelper getOpenHelper(final Context context) {
+ return OpenHelper.getInstance(context);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (mContactAggregator != null) {
+ mContactAggregator.quit();
+ }
+
+ super.finalize();
+ }
+
+ /**
+ * Wipes all data from the contacts database.
+ */
+ /* package */ void wipeData() {
+ mOpenHelper.wipeData();
+ }
+
+ /**
+ * Called when a change has been made.
+ *
+ * @param uri the uri that the change was made to
+ */
+ private void onChange(Uri uri) {
+ getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
+ }
+
+ @Override
+ public boolean isTemporary() {
+ return false;
+ }
+
+ private DataRowHandler getDataRowHandler(final String mimeType) {
+ DataRowHandler handler = mDataRowHandlers.get(mimeType);
+ if (handler == null) {
+ handler = new CustomDataRowHandler(mimeType);
+ mDataRowHandlers.put(mimeType, handler);
+ }
+ return handler;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ final int match = sUriMatcher.match(uri);
+ long id = 0;
+
+ switch (match) {
+ case SYNCSTATE:
+ id = mOpenHelper.getSyncState().insert(mOpenHelper.getWritableDatabase(), values);
+ break;
+
+ case CONTACTS: {
+ insertContact(values);
+ break;
+ }
+
+ case RAW_CONTACTS: {
+ final Account account = readAccountFromQueryParams(uri);
+ id = insertRawContact(values, account);
+ break;
+ }
+
+ case RAW_CONTACTS_DATA: {
+ values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
+ id = insertData(values);
+ break;
+ }
+
+ case DATA: {
+ id = insertData(values);
+ break;
+ }
+
+ case GROUPS: {
+ final Account account = readAccountFromQueryParams(uri);
+ id = insertGroup(values, account);
+ break;
+ }
+
+ case PRESENCE: {
+ id = insertPresence(values);
+ break;
+ }
+
+ default:
+ return mLegacyApiSupport.insert(uri, values);
+ }
+
+ if (id < 0) {
+ return null;
+ }
+
+ final Uri result = ContentUris.withAppendedId(uri, id);
+ onChange(result);
+ return result;
+ }
+
+ /**
+ * If account is non-null then store it in the values. If the account is already
+ * specified in the values then it must be consistent with the account, if it is non-null.
+ * @param values the ContentValues to read from and update
+ * @param account the explicitly provided Account
+ * @return false if the accounts are inconsistent
+ */
+ private boolean resolveAccount(ContentValues values, Account account) {
+ // If either is specified then both must be specified.
+ final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+ final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+ if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
+ final Account valuesAccount = new Account(accountName, accountType);
+ if (account != null && !valuesAccount.equals(account)) {
+ return false;
+ }
+ account = valuesAccount;
+ }
+ if (account != null) {
+ values.put(RawContacts.ACCOUNT_NAME, account.mName);
+ values.put(RawContacts.ACCOUNT_TYPE, account.mType);
+ }
+ return true;
+ }
+
+ /**
+ * Inserts an item in the contacts table
+ *
+ * @param values the values for the new row
+ * @return the row ID of the newly created row
+ */
+ private long insertContact(ContentValues values) {
+ throw new UnsupportedOperationException("Aggregates are created automatically");
+ }
+
+ /**
+ * Inserts an item in the contacts table
+ *
+ * @param values the values for the new row
+ * @param account the account this contact should be associated with. may be null.
+ * @return the row ID of the newly created row
+ */
+ private long insertRawContact(ContentValues values, Account account) {
+ /*
+ * The contact record is inserted in the contacts table, but it needs to
+ * be processed by the aggregator before it will be returned by the
+ * "aggregates" queries.
+ */
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ ContentValues overriddenValues = new ContentValues(values);
+ overriddenValues.putNull(RawContacts.CONTACT_ID);
+ if (!resolveAccount(overriddenValues, account)) {
+ return -1;
+ }
+
+ return db.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
+ }
+
+ /**
+ * Inserts an item in the data table
+ *
+ * @param values the values for the new row
+ * @return the row ID of the newly created row
+ */
+ private long insertData(ContentValues values) {
+ int aggregationMode = RawContacts.AGGREGATION_MODE_DISABLED;
+
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ long id = 0;
+ db.beginTransaction();
+ try {
+ mValues.clear();
+ mValues.putAll(values);
+
+ long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
+
+ // Replace package with internal mapping
+ final String packageName = mValues.getAsString(Data.RES_PACKAGE);
+ if (packageName != null) {
+ mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
+ }
+ mValues.remove(Data.RES_PACKAGE);
+
+ // Replace mimetype with internal mapping
+ final String mimeType = mValues.getAsString(Data.MIMETYPE);
+ if (TextUtils.isEmpty(mimeType)) {
+ throw new RuntimeException(Data.MIMETYPE + " is required");
+ }
+
+ mValues.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType));
+ mValues.remove(Data.MIMETYPE);
+
+ if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ boolean containsGroupSourceId = mValues.containsKey(GroupMembership.GROUP_SOURCE_ID);
+ boolean containsGroupId = mValues.containsKey(GroupMembership.GROUP_ROW_ID);
+ if (containsGroupSourceId && containsGroupId) {
+ throw new IllegalArgumentException(
+ "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
+ + "and GroupMembership.GROUP_ROW_ID");
+ }
+
+ if (!containsGroupSourceId && !containsGroupId) {
+ throw new IllegalArgumentException(
+ "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
+ + "and GroupMembership.GROUP_ROW_ID");
+ }
+
+ if (containsGroupSourceId) {
+ final String sourceId = mValues.getAsString(GroupMembership.GROUP_SOURCE_ID);
+ final long groupId = getOrMakeGroup(db, rawContactId, sourceId);
+ mValues.remove(GroupMembership.GROUP_SOURCE_ID);
+ mValues.put(GroupMembership.GROUP_ROW_ID, groupId);
+ }
+ }
+
+ id = getDataRowHandler(mimeType).insert(db, rawContactId, mValues);
+
+ aggregationMode = mContactAggregator.markContactForAggregation(rawContactId);
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ triggerAggregation(id, aggregationMode);
+ return id;
+ }
+
+ private void triggerAggregation(long rawContactId, int aggregationMode) {
+ switch (aggregationMode) {
+ case RawContacts.AGGREGATION_MODE_DEFAULT:
+ mContactAggregator.schedule();
+ break;
+
+ case RawContacts.AGGREGATION_MODE_IMMEDITATE:
+ mContactAggregator.aggregateContact(rawContactId);
+ break;
+
+ case RawContacts.AGGREGATION_MODE_DISABLED:
+ // Do nothing
+ break;
+ }
+ }
+
+ /**
+ * Returns the group id of the group with sourceId and the same account as rawContactId.
+ * If the group doesn't already exist then it is first created,
+ * @param db SQLiteDatabase to use for this operation
+ * @param rawContactId the contact this group is associated with
+ * @param sourceId the sourceIf of the group to query or create
+ * @return the group id of the existing or created group
+ * @throws IllegalArgumentException if the contact is not associated with an account
+ * @throws IllegalStateException if a group needs to be created but the creation failed
+ */
+ private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) {
+ Account account = null;
+ Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "="
+ + rawContactId, null, null, null, null);
+ try {
+ if (c.moveToNext()) {
+ final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME);
+ final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE);
+ if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+ account = new Account(accountName, accountType);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ if (account == null) {
+ throw new IllegalArgumentException("if the groupmembership only "
+ + "has a sourceid the the contact must be associate with "
+ + "an account");
+ }
+
+ // look up the group that contains this sourceId and has the same account name and type
+ // as the contact refered to by rawContactId
+ c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
+ Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
+ new String[]{sourceId, account.mName, account.mType}, null, null, null);
+ try {
+ if (c.moveToNext()) {
+ return c.getLong(0);
+ } else {
+ ContentValues groupValues = new ContentValues();
+ groupValues.put(Groups.ACCOUNT_NAME, account.mName);
+ groupValues.put(Groups.ACCOUNT_TYPE, account.mType);
+ groupValues.put(Groups.SOURCE_ID, sourceId);
+ long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
+ if (groupId < 0) {
+ throw new IllegalStateException("unable to create a new group with "
+ + "this sourceid: " + groupValues);
+ }
+ return groupId;
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Delete data row by row so that fixing of primaries etc work correctly.
+ */
+ private int deleteData(String selection, String[] selectionArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int count = 0;
+ db.beginTransaction();
+ try {
+
+ // Note that the query will return data according to the access restrictions,
+ // so we don't need to worry about deleting data we don't have permission to read.
+ Cursor c = query(Data.CONTENT_URI, DataIdQuery.COLUMNS, selection, selectionArgs, null);
+ try {
+ while(c.moveToNext()) {
+ long dataId = c.getLong(DataIdQuery._ID);
+ count += deleteData(dataId);
+ }
+ } finally {
+ c.close();
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ return count;
+ }
+
+ public int deleteData(long dataId, String[] allowedMimeTypes) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ Cursor c = db.query(DataQuery.TABLE, DataQuery.COLUMNS,
+ DataColumns.CONCRETE_ID + "=" + dataId, null, null, null, null);
+ // TODO apply restrictions
+ try {
+ if (!c.moveToFirst()) {
+ return 0;
+ }
+
+ String mimeType = c.getString(DataQuery.MIMETYPE);
+ boolean valid = false;
+ for (int i = 0; i < allowedMimeTypes.length; i++) {
+ if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
+ valid = true;
+ break;
+ }
+ }
+
+ if (!valid) {
+ throw new RuntimeException("Data type mismatch: expected "
+ + Lists.newArrayList(allowedMimeTypes));
+ }
+
+ return getDataRowHandler(mimeType).delete(db, c);
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Delete the given {@link Data} row, fixing up any {@link Contacts}
+ * primaries that reference it.
+ */
+ private int deleteData(long dataId) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ final long mimePhone = mOpenHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+ final long mimeEmail = mOpenHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
+
+ // Check to see if the data about to be deleted was a super-primary on
+ // the parent aggregate, and set flags to fix-up once deleted.
+ long aggId = -1;
+ long mimeId = -1;
+ String dataRaw = null;
+ boolean fixOptimal = false;
+ boolean fixFallback = false;
+
+ Cursor cursor = null;
+ try {
+ cursor = db.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
+ DataColumns.CONCRETE_ID + "=" + dataId, null, null, null, null);
+ if (cursor.moveToFirst()) {
+ aggId = cursor.getLong(DataContactsQuery.CONTACT_ID);
+ mimeId = cursor.getLong(DataContactsQuery.MIMETYPE_ID);
+ if (mimeId == mimePhone) {
+ dataRaw = cursor.getString(DataContactsQuery.PHONE_NUMBER);
+ fixOptimal = (cursor.getLong(DataContactsQuery.OPTIMAL_PHONE_ID) == dataId);
+ fixFallback = (cursor.getLong(DataContactsQuery.FALLBACK_PHONE_ID) == dataId);
+ } else if (mimeId == mimeEmail) {
+ dataRaw = cursor.getString(DataContactsQuery.EMAIL_DATA);
+ fixOptimal = (cursor.getLong(DataContactsQuery.OPTIMAL_EMAIL_ID) == dataId);
+ fixFallback = (cursor.getLong(DataContactsQuery.FALLBACK_EMAIL_ID) == dataId);
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ cursor = null;
+ }
+ }
+
+ // Delete the requested data item.
+ int dataDeleted = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
+
+ // Fix-up any super-primary values that are now invalid.
+ if (fixOptimal || fixFallback) {
+ final ContentValues values = new ContentValues();
+ final StringBuilder scoreClause = new StringBuilder();
+
+ final String SCORE = "score";
+
+ // Build scoring clause that will first pick data items under the
+ // same aggregate that have identical values, otherwise fall back to
+ // normal primary scoring from the member contacts.
+ scoreClause.append("(CASE WHEN ");
+ if (mimeId == mimePhone) {
+ scoreClause.append(Phone.NUMBER);
+ } else if (mimeId == mimeEmail) {
+ scoreClause.append(Email.DATA);
+ }
+ scoreClause.append("=");
+ DatabaseUtils.appendEscapedSQLString(scoreClause, dataRaw);
+ scoreClause.append(" THEN 2 ELSE " + Data.IS_PRIMARY + " END) AS " + SCORE);
+
+ final String[] PROJ_PRIMARY = new String[] {
+ DataColumns.CONCRETE_ID,
+ RawContacts.IS_RESTRICTED,
+ scoreClause.toString(),
+ };
+
+ final int COL_DATA_ID = 0;
+ final int COL_IS_RESTRICTED = 1;
+ final int COL_SCORE = 2;
+
+ cursor = db.query(Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS, PROJ_PRIMARY,
+ ContactsColumns.CONCRETE_ID + "=" + aggId + " AND " + DataColumns.MIMETYPE_ID
+ + "=" + mimeId, null, null, null, SCORE);
+
+ if (fixOptimal) {
+ String colId = null;
+ String colIsRestricted = null;
+ if (mimeId == mimePhone) {
+ colId = ContactsColumns.OPTIMAL_PRIMARY_PHONE_ID;
+ colIsRestricted = ContactsColumns.OPTIMAL_PRIMARY_PHONE_IS_RESTRICTED;
+ } else if (mimeId == mimeEmail) {
+ colId = ContactsColumns.OPTIMAL_PRIMARY_EMAIL_ID;
+ colIsRestricted = ContactsColumns.OPTIMAL_PRIMARY_EMAIL_IS_RESTRICTED;
+ }
+
+ // Start by replacing with null, since fixOptimal told us that
+ // the previous aggregate values are bad.
+ values.putNull(colId);
+ values.putNull(colIsRestricted);
+
+ // When finding a new optimal primary, we only care about the
+ // highest scoring value, regardless of source.
+ if (cursor.moveToFirst()) {
+ final long newOptimal = cursor.getLong(COL_DATA_ID);
+ final long newIsRestricted = cursor.getLong(COL_IS_RESTRICTED);
+
+ if (newOptimal != 0) {
+ values.put(colId, newOptimal);
+ }
+ if (newIsRestricted != 0) {
+ values.put(colIsRestricted, newIsRestricted);
+ }
+ }
+ }
+
+ if (fixFallback) {
+ String colId = null;
+ if (mimeId == mimePhone) {
+ colId = ContactsColumns.FALLBACK_PRIMARY_PHONE_ID;
+ } else if (mimeId == mimeEmail) {
+ colId = ContactsColumns.FALLBACK_PRIMARY_EMAIL_ID;
+ }
+
+ // Start by replacing with null, since fixFallback told us that
+ // the previous aggregate values are bad.
+ values.putNull(colId);
+
+ // The best fallback value is the highest scoring data item that
+ // hasn't been restricted.
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ final boolean isRestricted = (cursor.getInt(COL_IS_RESTRICTED) == 1);
+ if (!isRestricted) {
+ values.put(colId, cursor.getLong(COL_DATA_ID));
+ break;
+ }
+ }
+ }
+
+ // Push through any contact updates we have
+ if (values.size() > 0) {
+ db.update(Tables.CONTACTS, values, ContactsColumns.CONCRETE_ID + "=" + aggId,
+ null);
+ }
+ }
+
+ return dataDeleted;
+ }
+
+ /**
+ * Inserts an item in the groups table
+ */
+ private long insertGroup(ContentValues values, Account account) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ ContentValues overriddenValues = new ContentValues(values);
+ if (!resolveAccount(overriddenValues, account)) {
+ return -1;
+ }
+
+ // Replace package with internal mapping
+ final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE);
+ if (packageName != null) {
+ overriddenValues.put(GroupsColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
+ }
+ overriddenValues.remove(Groups.RES_PACKAGE);
+
+ return db.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
+ }
+
+ /**
+ * Inserts a presence update.
+ */
+ public long insertPresence(ContentValues values) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ final String handle = values.getAsString(Presence.IM_HANDLE);
+ final String protocol = values.getAsString(Presence.IM_PROTOCOL);
+ if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) {
+ throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required");
+ }
+
+ // TODO: generalize to allow other providers to match against email
+ boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == Integer.parseInt(protocol);
+
+ StringBuilder selection = new StringBuilder();
+ String[] selectionArgs;
+ if (matchEmail) {
+ selection.append("(" + Clauses.WHERE_IM_MATCHES + ") OR ("
+ + Clauses.WHERE_EMAIL_MATCHES + ")");
+ selectionArgs = new String[] { protocol, handle, handle };
+ } else {
+ selection.append(Clauses.WHERE_IM_MATCHES);
+ selectionArgs = new String[] { protocol, handle };
+ }
+
+ if (values.containsKey(Presence.DATA_ID)) {
+ selection.append(" AND " + DataColumns.CONCRETE_ID + "=")
+ .append(values.getAsLong(Presence.DATA_ID));
+ }
+
+ if (values.containsKey(Presence.RAW_CONTACT_ID)) {
+ selection.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + "=")
+ .append(values.getAsLong(Presence.RAW_CONTACT_ID));
+ }
+
+ selection.append(" AND ").append(getContactsRestrictionExceptions());
+
+ long dataId = -1;
+ long rawContactId = -1;
+
+ Cursor cursor = null;
+ try {
+ cursor = db.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
+ selection.toString(), selectionArgs, null, null, null);
+ if (cursor.moveToFirst()) {
+ dataId = cursor.getLong(DataContactsQuery.DATA_ID);
+ rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
+ } else {
+ // No contact found, return a null URI
+ return -1;
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ values.put(Presence.DATA_ID, dataId);
+ values.put(Presence.RAW_CONTACT_ID, rawContactId);
+
+ // Insert the presence update
+ long presenceId = db.replace(Tables.PRESENCE, null, values);
+ return presenceId;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case SYNCSTATE:
+ return mOpenHelper.getSyncState().delete(db, selection, selectionArgs);
+
+ case CONTACTS_ID: {
+ long contactId = ContentUris.parseId(uri);
+
+ // Remove references to the contact first
+ ContentValues values = new ContentValues();
+ values.putNull(RawContacts.CONTACT_ID);
+ db.update(Tables.RAW_CONTACTS, values,
+ RawContacts.CONTACT_ID + "=" + contactId, null);
+
+ return db.delete(Tables.CONTACTS, BaseColumns._ID + "=" + contactId, null);
+ }
+
+ case RAW_CONTACTS_ID: {
+ return deleteRawContact(uri);
+ }
+
+ case DATA: {
+ return deleteData(selection, selectionArgs);
+ }
+
+ case DATA_ID: {
+ long dataId = ContentUris.parseId(uri);
+ return deleteData(dataId);
+ }
+
+ case GROUPS_ID: {
+ long groupId = ContentUris.parseId(uri);
+ final long groupMembershipMimetypeId = mOpenHelper
+ .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
+ int groupsDeleted = db.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
+ int dataDeleted = db.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
+ + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
+ + groupId, null);
+ mOpenHelper.updateAllVisible();
+ return groupsDeleted + dataDeleted;
+ }
+
+ case PRESENCE: {
+ return db.delete(Tables.PRESENCE, null, null);
+ }
+
+ default:
+ return mLegacyApiSupport.delete(uri, selection, selectionArgs);
+ }
+ }
+
+ private int deleteRawContact(Uri uri) {
+ boolean permanentDeletion = false;
+ String permanent = uri.getQueryParameter(RawContacts.DELETE_PERMANENTLY);
+ if (permanent != null && !"false".equals(permanent.toLowerCase())) {
+ permanentDeletion = true;
+ }
+
+ long rawContactId = ContentUris.parseId(uri);
+ return deleteRawContact(rawContactId, permanentDeletion);
+ }
+
+ public int deleteRawContact(long rawContactId, boolean permanently) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ // TODO delete aggregation exceptions
+ mOpenHelper.removeContactIfSingleton(rawContactId);
+ if (permanently) {
+ db.delete(Tables.PRESENCE, Presence.RAW_CONTACT_ID + "=" + rawContactId, null);
+ return db.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
+ } else {
+ mValues.clear();
+ mValues.put(RawContacts.DELETED, true);
+ mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
+ mValues.putNull(RawContacts.CONTACT_ID);
+ return updateRawContact(rawContactId, mValues, null, null);
+ }
+ }
+
+ private static Account readAccountFromQueryParams(Uri uri) {
+ final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+ final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
+ if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
+ return null;
+ }
+ return new Account(name, type);
+ }
+
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int count = 0;
+
+ final int match = sUriMatcher.match(uri);
+ switch(match) {
+ case SYNCSTATE:
+ return mOpenHelper.getSyncState().update(db, values, selection, selectionArgs);
+
+ // TODO(emillar): We will want to disallow editing the contacts table at some point.
+ case CONTACTS: {
+ count = db.update(Tables.CONTACTS, values, selection, selectionArgs);
+ break;
+ }
+
+ case CONTACTS_ID: {
+ count = updateContactData(db, ContentUris.parseId(uri), values);
+ break;
+ }
+
+ case DATA: {
+ count = updateData(values, selection, selectionArgs);
+ break;
+ }
+
+ case DATA_ID: {
+ count = updateData(ContentUris.parseId(uri), values);
+ break;
+ }
+
+ case RAW_CONTACTS: {
+ count = db.update(Tables.RAW_CONTACTS, values, selection, selectionArgs);
+ break;
+ }
+
+ case RAW_CONTACTS_ID: {
+ long rawContactId = ContentUris.parseId(uri);
+ count = updateRawContact(rawContactId, values, selection, selectionArgs);
+ break;
+ }
+
+ case GROUPS: {
+ count = db.update(Tables.GROUPS, values, selection, selectionArgs);
+ mOpenHelper.updateAllVisible();
+ break;
+ }
+
+ case GROUPS_ID: {
+ long groupId = ContentUris.parseId(uri);
+ String selectionWithId = (Groups._ID + "=" + groupId + " ")
+ + (selection == null ? "" : " AND " + selection);
+ count = db.update(Tables.GROUPS, values, selectionWithId, selectionArgs);
+
+ // If changing visibility, then update contacts
+ if (values.containsKey(Groups.GROUP_VISIBLE)) {
+ mOpenHelper.updateAllVisible();
+ }
+
+ break;
+ }
+
+ case AGGREGATION_EXCEPTIONS: {
+ count = updateAggregationException(db, values);
+ break;
+ }
+
+ default:
+ return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
+ }
+
+ if (count > 0) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ }
+ return count;
+ }
+
+ private int updateRawContact(long rawContactId, ContentValues values, String selection,
+ String[] selectionArgs) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ String selectionWithId = (RawContacts._ID + " = " + rawContactId + " ")
+ + (selection == null ? "" : " AND " + selection);
+ return db.update(Tables.RAW_CONTACTS, values, selectionWithId, selectionArgs);
+ }
+
+ private int updateData(ContentValues values, String selection,
+ String[] selectionArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int count = 0;
+ db.beginTransaction();
+ try {
+
+ // Note that the query will return data according to the access restrictions,
+ // so we don't need to worry about deleting data we don't have permission to read.
+ Cursor c = query(Data.CONTENT_URI, DataIdQuery.COLUMNS, selection, selectionArgs, null);
+ try {
+ while(c.moveToNext()) {
+ long dataId = c.getLong(DataIdQuery._ID);
+ count += updateData(dataId, values);
+ }
+ } finally {
+ c.close();
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ return count;
+ }
+
+ private int updateData(long dataId, ContentValues values) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ mValues.clear();
+ mValues.putAll(values);
+ mValues.remove(Data._ID);
+ mValues.remove(Data.RAW_CONTACT_ID);
+ mValues.remove(Data.MIMETYPE);
+
+ String packageName = values.getAsString(Data.RES_PACKAGE);
+ if (packageName != null) {
+ mValues.remove(Data.RES_PACKAGE);
+ mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
+ }
+
+ boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
+ boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);
+
+ // Remove primary or super primary values being set to 0. This is disallowed by the
+ // content provider.
+ if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
+ containsIsSuperPrimary = false;
+ mValues.remove(Data.IS_SUPER_PRIMARY);
+ }
+ if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
+ containsIsPrimary = false;
+ mValues.remove(Data.IS_PRIMARY);
+ }
+
+ if (containsIsSuperPrimary) {
+ setIsSuperPrimary(dataId);
+ setIsPrimary(dataId);
+
+ // Now that we've taken care of setting these, remove them from "values".
+ mValues.remove(Data.IS_SUPER_PRIMARY);
+ if (containsIsPrimary) {
+ mValues.remove(Data.IS_PRIMARY);
+ }
+ } else if (containsIsPrimary) {
+ setIsPrimary(dataId);
+
+ // Now that we've taken care of setting this, remove it from "values".
+ mValues.remove(Data.IS_PRIMARY);
+ }
+
+ if (mValues.size() > 0) {
+ return db.update(Tables.DATA, mValues, Data._ID + " = " + dataId, null);
+ }
+ return 0;
+ }
+
+ private int updateContactData(SQLiteDatabase db, long contactId, ContentValues values) {
+
+ // First update all constituent contacts
+ ContentValues optionValues = new ContentValues(5);
+ OpenHelper.copyStringValue(optionValues, RawContacts.CUSTOM_RINGTONE,
+ values, Contacts.CUSTOM_RINGTONE);
+ OpenHelper.copyLongValue(optionValues, RawContacts.SEND_TO_VOICEMAIL,
+ values, Contacts.SEND_TO_VOICEMAIL);
+ OpenHelper.copyLongValue(optionValues, RawContacts.LAST_TIME_CONTACTED,
+ values, Contacts.LAST_TIME_CONTACTED);
+ OpenHelper.copyLongValue(optionValues, RawContacts.TIMES_CONTACTED,
+ values, Contacts.TIMES_CONTACTED);
+ OpenHelper.copyLongValue(optionValues, RawContacts.STARRED,
+ values, Contacts.STARRED);
+
+ // Nothing to update - just return
+ if (optionValues.size() == 0) {
+ return 0;
+ }
+
+ db.update(Tables.RAW_CONTACTS, optionValues,
+ RawContacts.CONTACT_ID + "=" + contactId, null);
+ return db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+ }
+
+ public void updateContactTime(long contactId, long lastTimeContacted) {
+ mLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
+ mLastTimeContactedUpdate.bindLong(2, contactId);
+ mLastTimeContactedUpdate.execute();
+ }
+
+ private static class RawContactPair {
+ final long rawContactId1;
+ final long rawContactId2;
+
+ /**
+ * Constructor that ensures that this.rawContactId1 < this.rawContactId2
+ */
+ public RawContactPair(long rawContactId1, long rawContactId2) {
+ if (rawContactId1 < rawContactId2) {
+ this.rawContactId1 = rawContactId1;
+ this.rawContactId2 = rawContactId2;
+ } else {
+ this.rawContactId2 = rawContactId1;
+ this.rawContactId1 = rawContactId2;
+ }
+ }
+ }
+
+ private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
+ int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
+ long contactId = values.getAsInteger(AggregationExceptions.CONTACT_ID);
+ long rawContactId = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID);
+
+ // First, we build a list of contactID-contactID pairs for the given contact and contact.
+ ArrayList<RawContactPair> pairs = new ArrayList<RawContactPair>();
+ Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts.CONTACT_ID
+ + "=" + contactId, null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long aggregatedContactId = c.getLong(ContactsQuery.RAW_CONTACT_ID);
+ if (aggregatedContactId != rawContactId) {
+ pairs.add(new RawContactPair(aggregatedContactId, rawContactId));
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ // Now we iterate through all contact pairs to see if we need to insert/delete/update
+ // the corresponding exception
+ ContentValues exceptionValues = new ContentValues(3);
+ exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
+ for (RawContactPair pair : pairs) {
+ final String whereClause =
+ AggregationExceptionColumns.RAW_CONTACT_ID1 + "=" + pair.rawContactId1 + " AND "
+ + AggregationExceptionColumns.RAW_CONTACT_ID2 + "=" + pair.rawContactId2;
+ if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
+ db.delete(Tables.AGGREGATION_EXCEPTIONS, whereClause, null);
+ } else {
+ exceptionValues.put(AggregationExceptionColumns.RAW_CONTACT_ID1, pair.rawContactId1);
+ exceptionValues.put(AggregationExceptionColumns.RAW_CONTACT_ID2, pair.rawContactId2);
+ db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
+ exceptionValues);
+ }
+ }
+
+ int aggregationMode = mContactAggregator.markContactForAggregation(rawContactId);
+ if (aggregationMode != RawContacts.AGGREGATION_MODE_DISABLED) {
+ mContactAggregator.aggregateContact(db, rawContactId);
+ if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC
+ || exceptionType == AggregationExceptions.TYPE_KEEP_OUT) {
+ mContactAggregator.updateAggregateData(contactId);
+ }
+ }
+
+ // The return value is fake - we just confirm that we made a change, not count actual
+ // rows changed.
+ return 1;
+ }
+
+ /**
+ * Test if a {@link String} value appears in the given list.
+ */
+ private boolean isContained(String[] array, String value) {
+ if (array != null) {
+ for (String test : array) {
+ if (value.equals(test)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Test if a {@link String} value appears in the given list, and add to the
+ * array if the value doesn't already appear.
+ */
+ private String[] assertContained(String[] array, String value) {
+ if (array == null) {
+ array = new String[] {value};
+ } else if (!isContained(array, value)) {
+ String[] newArray = new String[array.length + 1];
+ System.arraycopy(array, 0, newArray, 0, array.length);
+ newArray[array.length] = value;
+ array = newArray;
+ }
+ return array;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ String groupBy = null;
+ String limit = getLimit(uri);
+
+ String contactIdColName = Tables.CONTACTS + "." + Contacts._ID;
+
+ // TODO: Consider writing a test case for RestrictionExceptions when you
+ // write a new query() block to make sure it protects restricted data.
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case SYNCSTATE:
+ return mOpenHelper.getSyncState().query(db, projection, selection, selectionArgs,
+ sortOrder);
+
+ case CONTACTS: {
+ qb.setTables(Tables.CONTACTS);
+ applyAggregateRestrictionExceptions(qb);
+ applyAggregatePrimaryRestrictionExceptions(sContactsProjectionMap);
+ qb.setProjectionMap(sContactsProjectionMap);
+ break;
+ }
+
+ case CONTACTS_ID: {
+ long aggId = ContentUris.parseId(uri);
+ qb.setTables(Tables.CONTACTS);
+ qb.appendWhere(ContactsColumns.CONCRETE_ID + "=" + aggId + " AND ");
+ applyAggregateRestrictionExceptions(qb);
+ applyAggregatePrimaryRestrictionExceptions(sContactsProjectionMap);
+ qb.setProjectionMap(sContactsProjectionMap);
+ break;
+ }
+
+ case CONTACTS_SUMMARY: {
+ // TODO: join into social status tables
+ qb.setTables(Tables.CONTACTS_JOIN_PRESENCE_PRIMARY_PHONE);
+ applyAggregateRestrictionExceptions(qb);
+ applyAggregatePrimaryRestrictionExceptions(sContactsSummaryProjectionMap);
+ projection = assertContained(projection, Contacts.PRIMARY_PHONE_ID);
+ qb.setProjectionMap(sContactsSummaryProjectionMap);
+ groupBy = contactIdColName;
+ break;
+ }
+
+ case CONTACTS_SUMMARY_ID: {
+ // TODO: join into social status tables
+ long aggId = ContentUris.parseId(uri);
+ qb.setTables(Tables.CONTACTS_JOIN_PRESENCE_PRIMARY_PHONE);
+ qb.appendWhere(ContactsColumns.CONCRETE_ID + "=" + aggId + " AND ");
+ applyAggregateRestrictionExceptions(qb);
+ applyAggregatePrimaryRestrictionExceptions(sContactsSummaryProjectionMap);
+ projection = assertContained(projection, Contacts.PRIMARY_PHONE_ID);
+ qb.setProjectionMap(sContactsSummaryProjectionMap);
+ groupBy = contactIdColName;
+ break;
+ }
+
+ case CONTACTS_SUMMARY_FILTER: {
+ // TODO: filter query based on callingUid
+ qb.setTables(Tables.CONTACTS_JOIN_PRESENCE_PRIMARY_PHONE);
+ qb.setProjectionMap(sContactsSummaryProjectionMap);
+ if (uri.getPathSegments().size() > 2) {
+ qb.appendWhere(buildContactLookupWhereClause(uri.getLastPathSegment()));
+ }
+ groupBy = contactIdColName;
+ break;
+ }
+
+ case CONTACTS_SUMMARY_STREQUENT_FILTER:
+ case CONTACTS_SUMMARY_STREQUENT: {
+ // Build the first query for starred
+ qb.setTables(Tables.CONTACTS_JOIN_PRESENCE_PRIMARY_PHONE);
+ qb.setProjectionMap(sContactsSummaryProjectionMap);
+ if (match == CONTACTS_SUMMARY_STREQUENT_FILTER
+ && uri.getPathSegments().size() > 3) {
+ qb.appendWhere(buildContactLookupWhereClause(uri.getLastPathSegment()));
+ }
+ final String starredQuery = qb.buildQuery(projection, Contacts.STARRED + "=1",
+ null, contactIdColName, null, null,
+ null /* limit */);
+
+ // Build the second query for frequent
+ qb = new SQLiteQueryBuilder();
+ qb.setTables(Tables.CONTACTS_JOIN_PRESENCE_PRIMARY_PHONE);
+ qb.setProjectionMap(sContactsSummaryProjectionMap);
+ if (match == CONTACTS_SUMMARY_STREQUENT_FILTER
+ && uri.getPathSegments().size() > 3) {
+ qb.appendWhere(buildContactLookupWhereClause(uri.getLastPathSegment()));
+ }
+ final String frequentQuery = qb.buildQuery(projection,
+ Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
+ + " = 0 OR " + Contacts.STARRED + " IS NULL)",
+ null, contactIdColName, null, null, null);
+
+ // Put them together
+ final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
+ STREQUENT_ORDER_BY, STREQUENT_LIMIT);
+ Cursor c = db.rawQueryWithFactory(null, query, null,
+ Tables.CONTACTS_JOIN_PRESENCE_PRIMARY_PHONE);
+
+ if ((c != null) && !isTemporary()) {
+ c.setNotificationUri(getContext().getContentResolver(),
+ ContactsContract.AUTHORITY_URI);
+ }
+ return c;
+ }
+
+ case CONTACTS_SUMMARY_GROUP: {
+ qb.setTables(Tables.CONTACTS_JOIN_PRESENCE_PRIMARY_PHONE);
+ applyAggregateRestrictionExceptions(qb);
+ applyAggregatePrimaryRestrictionExceptions(sContactsSummaryProjectionMap);
+ projection = assertContained(projection, Contacts.PRIMARY_PHONE_ID);
+ qb.setProjectionMap(sContactsSummaryProjectionMap);
+ if (uri.getPathSegments().size() > 2) {
+ qb.appendWhere(" AND " + sContactsInGroupSelect);
+ selectionArgs = appendGroupArg(selectionArgs, uri.getLastPathSegment());
+ }
+ groupBy = contactIdColName;
+ break;
+ }
+
+ case CONTACTS_DATA: {
+ long aggId = Long.parseLong(uri.getPathSegments().get(1));
+ qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS_GROUPS);
+ qb.setProjectionMap(sDataRawContactsGroupsContactProjectionMap);
+ qb.appendWhere(RawContacts.CONTACT_ID + "=" + aggId + " AND ");
+ applyDataRestrictionExceptions(qb);
+ break;
+ }
+
+ case CONTACTS_RAW_CONTACTS: {
+ long contactId = Long.parseLong(uri.getPathSegments().get(1));
+ qb.setTables(Tables.RAW_CONTACTS_JOIN_CONTACTS);
+ qb.setProjectionMap(sRawContactsContactsProjectionMap);
+ qb.appendWhere(RawContacts.CONTACT_ID + "=" + contactId + " AND ");
+ applyDataRestrictionExceptions(qb);
+ break;
+ }
+
+ case PHONES_FILTER: {
+ qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
+ qb.setProjectionMap(sDataRawContactsContactProjectionMap);
+ qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
+ if (uri.getPathSegments().size() > 2) {
+ qb.appendWhere(" AND " + buildContactLookupWhereClause(
+ uri.getLastPathSegment()));
+ }
+ break;
+ }
+
+ case PHONES: {
+ qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
+ qb.setProjectionMap(sDataRawContactsContactProjectionMap);
+ qb.appendWhere(Data.MIMETYPE + " = \"" + Phone.CONTENT_ITEM_TYPE + "\"");
+ break;
+ }
+
+ case POSTALS: {
+ qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
+ qb.setProjectionMap(sDataRawContactsContactProjectionMap);
+ qb.appendWhere(Data.MIMETYPE + " = \"" + StructuredPostal.CONTENT_ITEM_TYPE + "\"");
+ break;
+ }
+
+ case RAW_CONTACTS: {
+ qb.setTables(Tables.RAW_CONTACTS);
+ qb.setProjectionMap(sRawContactsProjectionMap);
+ applyContactsRestrictionExceptions(qb);
+ break;
+ }
+
+ case RAW_CONTACTS_ID: {
+ long rawContactId = ContentUris.parseId(uri);
+ qb.setTables(Tables.RAW_CONTACTS);
+ qb.setProjectionMap(sRawContactsProjectionMap);
+ qb.appendWhere(RawContactsColumns.CONCRETE_ID + "=" + rawContactId + " AND ");
+ applyContactsRestrictionExceptions(qb);
+ break;
+ }
+
+ case RAW_CONTACTS_DATA: {
+ long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
+ qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS);
+ qb.setProjectionMap(sDataRawContactsGroupsProjectionMap);
+ qb.appendWhere(Data.RAW_CONTACT_ID + "=" + rawContactId + " AND ");
+ applyDataRestrictionExceptions(qb);
+ break;
+ }
+
+ case CONTACTS_FILTER_EMAIL: {
+ // TODO: filter query based on callingUid
+ qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
+ qb.setProjectionMap(sDataRawContactsProjectionMap);
+ qb.appendWhere(Data.MIMETYPE + "='" + CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'");
+ qb.appendWhere(" AND " + CommonDataKinds.Email.DATA + "=");
+ qb.appendWhereEscapeString(uri.getPathSegments().get(2));
+ break;
+ }
+
+ case DATA: {
+ final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+ final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
+ if (!TextUtils.isEmpty(accountName)) {
+ qb.appendWhere(RawContactsColumns.CONCRETE_ACCOUNT_NAME + "="
+ + DatabaseUtils.sqlEscapeString(accountName) + " AND "
+ + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + "="
+ + DatabaseUtils.sqlEscapeString(accountType) + " AND ");
+ }
+ qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS);
+ qb.setProjectionMap(sDataGroupsProjectionMap);
+ applyDataRestrictionExceptions(qb);
+ break;
+ }
+
+ case DATA_ID: {
+ qb.setTables(Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS);
+ qb.setProjectionMap(sDataGroupsProjectionMap);
+ qb.appendWhere(DataColumns.CONCRETE_ID + "=" + ContentUris.parseId(uri) + " AND ");
+ applyDataRestrictionExceptions(qb);
+ break;
+ }
+
+ case PHONE_LOOKUP: {
+ // TODO: filter query based on callingUid
+ if (TextUtils.isEmpty(sortOrder)) {
+ // Default the sort order to something reasonable so we get consistent
+ // results when callers don't request an ordering
+ sortOrder = Data.RAW_CONTACT_ID;
+ }
+
+ final String number = uri.getLastPathSegment();
+ OpenHelper.buildPhoneLookupQuery(qb, number);
+ qb.setProjectionMap(sDataRawContactsProjectionMap);
+ break;
+ }
+
+ case GROUPS: {
+ qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
+ qb.setProjectionMap(sGroupsProjectionMap);
+ break;
+ }
+
+ case GROUPS_ID: {
+ long groupId = ContentUris.parseId(uri);
+ qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
+ qb.setProjectionMap(sGroupsProjectionMap);
+ qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId);
+ break;
+ }
+
+ case GROUPS_SUMMARY: {
+ qb.setTables(Tables.GROUPS_JOIN_PACKAGES_DATA_RAW_CONTACTS_CONTACTS);
+ qb.setProjectionMap(sGroupsSummaryProjectionMap);
+ groupBy = GroupsColumns.CONCRETE_ID;
+ break;
+ }
+
+ case AGGREGATION_EXCEPTIONS: {
+ qb.setTables(Tables.AGGREGATION_EXCEPTIONS_JOIN_RAW_CONTACTS);
+ qb.setProjectionMap(sAggregationExceptionsProjectionMap);
+ break;
+ }
+
+ case AGGREGATION_SUGGESTIONS: {
+ long contactId = Long.parseLong(uri.getPathSegments().get(1));
+
+ // TODO drop MAX_SUGGESTIONS in favor of LIMIT
+ final String maxSuggestionsParam =
+ uri.getQueryParameter(AggregationSuggestions.MAX_SUGGESTIONS);
+
+ final int maxSuggestions;
+ if (maxSuggestionsParam != null) {
+ maxSuggestions = Integer.parseInt(maxSuggestionsParam);
+ } else {
+ maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
+ }
+
+ return mContactAggregator.queryAggregationSuggestions(contactId, projection,
+ sContactsProjectionMap, maxSuggestions);
+ }
+
+ case PRESENCE: {
+ qb.setTables(Tables.PRESENCE);
+ qb.setProjectionMap(sPresenceProjectionMap);
+ break;
+ }
+
+ case PRESENCE_ID: {
+ qb.setTables(Tables.PRESENCE);
+ qb.setProjectionMap(sPresenceProjectionMap);
+ qb.appendWhere(Presence._ID + "=" + ContentUris.parseId(uri));
+ break;
+ }
+
+ case SEARCH_SUGGESTIONS: {
+ return handleSearchSuggestionsQuery(uri, limit);
+ }
+
+ case SEARCH_SHORTCUT: {
+ // TODO
+ break;
+ }
+
+ default:
+ return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
+ sortOrder, limit);
+ }
+
+ // Perform the query and set the notification uri
+ final Cursor c = qb.query(db, projection, selection, selectionArgs,
+ groupBy, null, sortOrder, limit);
+ if (c != null) {
+ c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
+ }
+ return c;
+ }
+
+ /**
+ * Gets the value of the "limit" URI query parameter.
+ *
+ * @return A string containing a non-negative integer, or <code>null</code> if
+ * the parameter is not set, or is set to an invalid value.
+ */
+ private String getLimit(Uri url) {
+ String limitParam = url.getQueryParameter("limit");
+ if (limitParam == null) {
+ return null;
+ }
+ // make sure that the limit is a non-negative integer
+ try {
+ int l = Integer.parseInt(limitParam);
+ if (l < 0) {
+ Log.w(TAG, "Invalid limit parameter: " + limitParam);
+ return null;
+ }
+ return String.valueOf(l);
+ } catch (NumberFormatException ex) {
+ Log.w(TAG, "Invalid limit parameter: " + limitParam);
+ return null;
+ }
+ }
+
+
+ public Cursor handleSearchSuggestionsQuery(Uri url, String limit) {
+ if (url.getPathSegments().size() <= 1) {
+ return null;
+ }
+
+ final String searchClause = url.getLastPathSegment();
+ if (TextUtils.isDigitsOnly(searchClause)) {
+ return buildCursorForSearchSuggestionsBasedOnPhoneNumber(searchClause);
+ } else {
+ return buildCursorForSearchSuggestionsBasedOnName(searchClause, limit);
+ }
+ }
+
+ private static final String[] SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS = {
+ "_id",
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+ SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
+ };
+
+ private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) {
+ Resources r = getContext().getResources();
+ String s;
+ int i;
+
+ ArrayList<Object> dialNumber = new ArrayList<Object>();
+ dialNumber.add(0); // _id
+ s = r.getString(com.android.internal.R.string.dial_number_using, searchClause);
+ i = s.indexOf('\n');
+ if (i < 0) {
+ dialNumber.add(s);
+ dialNumber.add("");
+ } else {
+ dialNumber.add(s.substring(0, i));
+ dialNumber.add(s.substring(i + 1));
+ }
+ dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact));
+ dialNumber.add("tel:" + searchClause);
+ dialNumber.add(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
+ dialNumber.add(null);
+
+ ArrayList<Object> createContact = new ArrayList<Object>();
+ createContact.add(1); // _id
+ s = r.getString(com.android.internal.R.string.create_contact_using, searchClause);
+ i = s.indexOf('\n');
+ if (i < 0) {
+ createContact.add(s);
+ createContact.add("");
+ } else {
+ createContact.add(s.substring(0, i));
+ createContact.add(s.substring(i + 1));
+ }
+ createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact));
+ createContact.add("tel:" + searchClause);
+ createContact.add(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
+ createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
+
+ @SuppressWarnings({"unchecked"}) ArrayList<ArrayList> rows = new ArrayList<ArrayList>();
+ rows.add(dialNumber);
+ rows.add(createContact);
+
+ return new ArrayListCursor(SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS, rows);
+ }
+
+ private interface SearchSuggestionQuery {
+ public static final String JOIN_RAW_CONTACTS =
+ " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) ";
+
+ public static final String JOIN_CONTACTS =
+ " JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+
+ public static final String JOIN_MIMETYPES =
+ " JOIN mimetypes ON (data.mimetype_id = mimetypes._id AND mimetypes.mimetype IN ('"
+ + StructuredName.CONTENT_ITEM_TYPE + "','" + Email.CONTENT_ITEM_TYPE + "','"
+ + Phone.CONTENT_ITEM_TYPE + "','" + Organization.CONTENT_ITEM_TYPE + "','"
+ + Photo.CONTENT_ITEM_TYPE + "','" + GroupMembership.CONTENT_ITEM_TYPE + "')) ";
+
+ // TODO join with groups and ensure that suggestions are from the My Contacts group
+ public static final String JOIN_GROUPS = " JOIN groups ON (mimetypes.mimetype='"
+ + GroupMembership.CONTENT_ITEM_TYPE + "' " + " AND groups._id = data."
+ + GroupMembership.GROUP_ROW_ID + ") ";
+
+ public static final String TABLE = "data " + JOIN_RAW_CONTACTS + JOIN_MIMETYPES
+ + JOIN_CONTACTS;
+
+ public static final String PRESENCE_SQL = "(SELECT MAX(" + Presence.PRESENCE_STATUS
+ + ") FROM " + Tables.PRESENCE + " WHERE " + Tables.PRESENCE + "."
+ + Presence.RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")";
+
+ public static final String[] COLUMNS = {
+ ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID,
+ ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME,
+ PRESENCE_SQL + " AS " + Contacts.PRESENCE_STATUS,
+ DataColumns.CONCRETE_ID + " AS data_id",
+ MimetypesColumns.MIMETYPE,
+ Data.IS_SUPER_PRIMARY,
+ Data.DATA2,
+ };
+
+ public static final int CONTACT_ID = 0;
+ public static final int DISPLAY_NAME = 1;
+ public static final int PRESENCE_STATUS = 2;
+ public static final int DATA_ID = 3;
+ public static final int MIMETYPE = 4;
+ public static final int IS_SUPER_PRIMARY = 5;
+ public static final int DATA2 = 6;
+ }
+
+ private static class SearchSuggestion {
+ String contactId;
+ boolean titleIsName;
+ String organization;
+ String email;
+ String phoneNumber;
+ String photoUri;
+ String normalizedName;
+ int presence = -1;
+ boolean processed;
+ String text1;
+ String text2;
+ String icon1;
+ String icon2;
+
+ public SearchSuggestion(long contactId) {
+ this.contactId = String.valueOf(contactId);
+ }
+
+ private void process() {
+ if (processed) {
+ return;
+ }
+
+ boolean hasOrganization = !TextUtils.isEmpty(organization);
+ boolean hasEmail = !TextUtils.isEmpty(email);
+ boolean hasPhone = !TextUtils.isEmpty(phoneNumber);
+
+ boolean titleIsOrganization = !titleIsName && hasOrganization;
+ boolean titleIsEmail = !titleIsName && !titleIsOrganization && hasEmail;
+ boolean titleIsPhone = !titleIsName && !titleIsOrganization && !titleIsEmail
+ && hasPhone;
+
+ if (!titleIsOrganization && hasOrganization) {
+ text2 = organization;
+ } else if (!titleIsEmail && hasEmail) {
+ text2 = email;
+ } else if (!titleIsPhone && hasPhone) {
+ text2 = phoneNumber;
+ }
+
+ if (photoUri != null) {
+ icon1 = photoUri;
+ } else {
+ icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture);
+ }
+
+ if (presence != -1) {
+ icon2 = String.valueOf(Presence.getPresenceIconResourceId(presence));
+ }
+
+ processed = true;
+ }
+
+ public String getSortKey() {
+ if (normalizedName == null) {
+ process();
+ normalizedName = text1 == null ? "" : NameNormalizer.normalize(text1);
+ }
+ return normalizedName;
+ }
+
+ @SuppressWarnings({"unchecked"})
+ public ArrayList asList() {
+ process();
+
+ ArrayList<Object> list = new ArrayList<Object>();
+ list.add(contactId);
+ list.add(text1);
+ list.add(text2);
+ list.add(icon1);
+ list.add(icon2);
+ list.add(contactId);
+ list.add(contactId);
+ return list;
+ }
+ }
+
+ private static final String[] SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS = {
+ "_id",
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_ICON_2,
+ SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
+ SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
+ };
+
+ private Cursor buildCursorForSearchSuggestionsBasedOnName(String searchClause, String limit) {
+ ArrayList<SearchSuggestion> suggestionList = new ArrayList<SearchSuggestion>();
+ HashMap<Long, SearchSuggestion> suggestionMap = new HashMap<Long, SearchSuggestion>();
+
+ StringBuilder selection = new StringBuilder();
+ selection.append(getContactsRestrictionExceptions());
+ selection.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN ");
+ appendRawContactsByFilterAsNestedQuery(selection, searchClause, limit);
+
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ Cursor c = db.query(true, SearchSuggestionQuery.TABLE,
+ SearchSuggestionQuery.COLUMNS, selection.toString(), null, null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+
+ long contactId = c.getLong(SearchSuggestionQuery.CONTACT_ID);
+ SearchSuggestion suggestion = suggestionMap.get(contactId);
+ if (suggestion == null) {
+ suggestion = new SearchSuggestion(contactId);
+ suggestionList.add(suggestion);
+ suggestionMap.put(contactId, suggestion);
+ }
+
+ boolean isSuperPrimary = c.getInt(SearchSuggestionQuery.IS_SUPER_PRIMARY) != 0;
+ suggestion.text1 = c.getString(SearchSuggestionQuery.DISPLAY_NAME);
+
+ if (!c.isNull(SearchSuggestionQuery.PRESENCE_STATUS)) {
+ suggestion.presence = c.getInt(SearchSuggestionQuery.PRESENCE_STATUS);
+ }
+
+ String mimetype = c.getString(SearchSuggestionQuery.MIMETYPE);
+ if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ suggestion.titleIsName = true;
+ } else if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ if (isSuperPrimary || suggestion.photoUri == null) {
+
+ // TODO introduce a dedicate URI for contact photo: /contact/#/photo
+ long dataId = c.getLong(SearchSuggestionQuery.DATA_ID);
+ suggestion.photoUri =
+ ContentUris.withAppendedId(Data.CONTENT_URI, dataId).toString();
+ }
+ } else if (Organization.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ if (isSuperPrimary || suggestion.organization == null) {
+ suggestion.organization = c.getString(SearchSuggestionQuery.DATA2);
+ }
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ if (isSuperPrimary || suggestion.email == null) {
+ suggestion.email = c.getString(SearchSuggestionQuery.DATA2);
+ }
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ if (isSuperPrimary || suggestion.phoneNumber == null) {
+ suggestion.phoneNumber = c.getString(SearchSuggestionQuery.DATA2);
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ Collections.sort(suggestionList, new Comparator<SearchSuggestion>() {
+ public int compare(SearchSuggestion row1, SearchSuggestion row2) {
+ return row1.getSortKey().compareTo(row2.getSortKey());
+ }
+ });
+
+ @SuppressWarnings({"unchecked"}) ArrayList<ArrayList> rows = new ArrayList<ArrayList>();
+ for (int i = 0; i < suggestionList.size(); i++) {
+ rows.add(suggestionList.get(i).asList());
+ }
+
+ return new ArrayListCursor(SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS, rows);
+ }
+
+ /**
+ * List of package names with access to {@link RawContacts#IS_RESTRICTED} data.
+ */
+ private static final String[] sAllowedPackages = new String[] {
+ "com.android.contacts",
+ "com.facebook",
+ };
+
+ /**
+ * Check if {@link Binder#getCallingUid()} should be allowed access to
+ * {@link RawContacts#IS_RESTRICTED} data.
+ */
+ private boolean hasRestrictedAccess() {
+ final PackageManager pm = getContext().getPackageManager();
+ final String[] callerPackages = pm.getPackagesForUid(Binder.getCallingUid());
+
+ // Has restricted access if caller matches any packages
+ for (String callerPackage : callerPackages) {
+ for (String allowedPackage : sAllowedPackages) {
+ if (allowedPackage.equals(callerPackage)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Restrict selection of {@link Contacts} to only public ones, or those
+ * the caller has been granted an exception to.
+ */
+ private void applyAggregateRestrictionExceptions(SQLiteQueryBuilder qb) {
+ if (hasRestrictedAccess()) {
+ qb.appendWhere("1");
+ } else {
+ qb.appendWhere(ContactsColumns.SINGLE_IS_RESTRICTED + "=0");
+ }
+ }
+
+ /**
+ * Find any exceptions that have been granted to the calling process, and
+ * add projections to correctly select {@link Contacts#PRIMARY_PHONE_ID}
+ * and {@link Contacts#PRIMARY_EMAIL_ID}.
+ */
+ private void applyAggregatePrimaryRestrictionExceptions(HashMap<String, String> projection) {
+ String projectionPhone;
+ String projectionEmail;
+
+ if (hasRestrictedAccess()) {
+ // With restricted access, always give optimal values
+ projectionPhone = ContactsColumns.OPTIMAL_PRIMARY_PHONE_ID + " AS "
+ + Contacts.PRIMARY_PHONE_ID;
+ projectionEmail = ContactsColumns.OPTIMAL_PRIMARY_EMAIL_ID + " AS "
+ + Contacts.PRIMARY_EMAIL_ID;
+ } else {
+ // With general access, always give fallback values
+ projectionPhone = ContactsColumns.FALLBACK_PRIMARY_PHONE_ID + " AS "
+ + Contacts.PRIMARY_PHONE_ID;
+ projectionEmail = ContactsColumns.FALLBACK_PRIMARY_EMAIL_ID + " AS "
+ + Contacts.PRIMARY_EMAIL_ID;
+ }
+
+ projection.remove(Contacts.PRIMARY_PHONE_ID);
+ projection.put(Contacts.PRIMARY_PHONE_ID, projectionPhone);
+
+ projection.remove(Contacts.PRIMARY_EMAIL_ID);
+ projection.put(Contacts.PRIMARY_EMAIL_ID, projectionEmail);
+ }
+
+ /**
+ * Find any exceptions that have been granted to the
+ * {@link Binder#getCallingUid()}, and add a limiting clause to the given
+ * {@link SQLiteQueryBuilder} to hide restricted data.
+ */
+ private void applyContactsRestrictionExceptions(SQLiteQueryBuilder qb) {
+ qb.appendWhere(getContactsRestrictionExceptions());
+ }
+
+ private String getContactsRestrictionExceptions() {
+ if (hasRestrictedAccess()) {
+ return "1";
+ } else {
+ return RawContacts.IS_RESTRICTED + "=0";
+ }
+ }
+
+ public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
+ if (hasRestrictedAccess()) {
+ return "1";
+ } else {
+ return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
+ + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
+ }
+ }
+
+ /**
+ * Find any exceptions that have been granted to the
+ * {@link Binder#getCallingUid()}, and add a limiting clause to the given
+ * {@link SQLiteQueryBuilder} to hide restricted data.
+ */
+ void applyDataRestrictionExceptions(SQLiteQueryBuilder qb) {
+ applyContactsRestrictionExceptions(qb);
+ }
+
+ /**
+ * An implementation of EntityIterator that joins the contacts and data tables
+ * and consumes all the data rows for a contact in order to build the Entity for a contact.
+ */
+ private static class ContactsEntityIterator implements EntityIterator {
+ private final Cursor mEntityCursor;
+ private volatile boolean mIsClosed;
+
+ private static final String[] DATA_KEYS = new String[]{
+ "data1",
+ "data2",
+ "data3",
+ "data4",
+ "data5",
+ "data6",
+ "data7",
+ "data8",
+ "data9",
+ "data10",
+ "data11",
+ "data12",
+ "data13",
+ "data14",
+ "data15"};
+
+ private static final String[] PROJECTION = new String[]{
+ RawContacts.ACCOUNT_NAME,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.SOURCE_ID,
+ RawContacts.VERSION,
+ RawContacts.DIRTY,
+ RawContacts.Data._ID,
+ RawContacts.Data.RES_PACKAGE,
+ RawContacts.Data.MIMETYPE,
+ RawContacts.Data.DATA1,
+ RawContacts.Data.DATA2,
+ RawContacts.Data.DATA3,
+ RawContacts.Data.DATA4,
+ RawContacts.Data.DATA5,
+ RawContacts.Data.DATA6,
+ RawContacts.Data.DATA7,
+ RawContacts.Data.DATA8,
+ RawContacts.Data.DATA9,
+ RawContacts.Data.DATA10,
+ RawContacts.Data.DATA11,
+ RawContacts.Data.DATA12,
+ RawContacts.Data.DATA13,
+ RawContacts.Data.DATA14,
+ RawContacts.Data.DATA15,
+ RawContacts.Data.RAW_CONTACT_ID,
+ RawContacts.Data.IS_PRIMARY,
+ RawContacts.Data.DATA_VERSION,
+ GroupMembership.GROUP_SOURCE_ID};
+
+ private static final int COLUMN_ACCOUNT_NAME = 0;
+ private static final int COLUMN_ACCOUNT_TYPE = 1;
+ private static final int COLUMN_SOURCE_ID = 2;
+ private static final int COLUMN_VERSION = 3;
+ private static final int COLUMN_DIRTY = 4;
+ private static final int COLUMN_DATA_ID = 5;
+ private static final int COLUMN_RES_PACKAGE = 6;
+ private static final int COLUMN_MIMETYPE = 7;
+ private static final int COLUMN_DATA1 = 8;
+ private static final int COLUMN_CONTACT_ID = 23;
+ private static final int COLUMN_IS_PRIMARY = 24;
+ private static final int COLUMN_DATA_VERSION = 25;
+ private static final int COLUMN_GROUP_SOURCE_ID = 26;
+
+ public ContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri,
+ String selection, String[] selectionArgs, String sortOrder) {
+ mIsClosed = false;
+
+ final String updatedSortOrder = (sortOrder == null)
+ ? RawContacts.Data.RAW_CONTACT_ID
+ : (RawContacts.Data.RAW_CONTACT_ID + "," + sortOrder);
+
+ final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
+ final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(Tables.CONTACT_ENTITIES);
+ if (contactsIdString != null) {
+ qb.appendWhere(Data.RAW_CONTACT_ID + "=" + contactsIdString);
+ }
+ final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+ final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
+ if (!TextUtils.isEmpty(accountName)) {
+ qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
+ + DatabaseUtils.sqlEscapeString(accountName) + " AND "
+ + RawContacts.ACCOUNT_TYPE + "="
+ + DatabaseUtils.sqlEscapeString(accountType));
+ }
+ mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
+ null, null, updatedSortOrder);
+ mEntityCursor.moveToFirst();
+ }
+
+ public void close() {
+ if (mIsClosed) {
+ throw new IllegalStateException("closing when already closed");
+ }
+ mIsClosed = true;
+ mEntityCursor.close();
+ }
+
+ public boolean hasNext() throws RemoteException {
+ if (mIsClosed) {
+ throw new IllegalStateException("calling hasNext() when the iterator is closed");
+ }
+
+ return !mEntityCursor.isAfterLast();
+ }
+
+ public Entity next() throws RemoteException {
+ if (mIsClosed) {
+ throw new IllegalStateException("calling next() when the iterator is closed");
+ }
+ if (!hasNext()) {
+ throw new IllegalStateException("you may only call next() if hasNext() is true");
+ }
+
+ final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
+
+ final long rawContactId = c.getLong(COLUMN_CONTACT_ID);
+
+ // we expect the cursor is already at the row we need to read from
+ ContentValues contactValues = new ContentValues();
+ contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
+ contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
+ contactValues.put(RawContacts._ID, rawContactId);
+ contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY));
+ contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION));
+ contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
+ Entity contact = new Entity(contactValues);
+
+ // read data rows until the contact id changes
+ do {
+ if (rawContactId != c.getLong(COLUMN_CONTACT_ID)) {
+ break;
+ }
+ // add the data to to the contact
+ ContentValues dataValues = new ContentValues();
+ dataValues.put(RawContacts.Data._ID, c.getString(COLUMN_DATA_ID));
+ dataValues.put(RawContacts.Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
+ dataValues.put(RawContacts.Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
+ dataValues.put(RawContacts.Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY));
+ dataValues.put(RawContacts.Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
+ if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
+ dataValues.put(GroupMembership.GROUP_SOURCE_ID,
+ c.getString(COLUMN_GROUP_SOURCE_ID));
+ }
+ dataValues.put(RawContacts.Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
+ for (int i = 0; i < 10; i++) {
+ final int columnIndex = i + COLUMN_DATA1;
+ String key = DATA_KEYS[i];
+ if (c.isNull(columnIndex)) {
+ // don't put anything
+ } else if (c.isLong(columnIndex)) {
+ dataValues.put(key, c.getLong(columnIndex));
+ } else if (c.isFloat(columnIndex)) {
+ dataValues.put(key, c.getFloat(columnIndex));
+ } else if (c.isString(columnIndex)) {
+ dataValues.put(key, c.getString(columnIndex));
+ } else if (c.isBlob(columnIndex)) {
+ dataValues.put(key, c.getBlob(columnIndex));
+ }
+ }
+ contact.addSubValue(Data.CONTENT_URI, dataValues);
+ } while (mEntityCursor.moveToNext());
+
+ return contact;
+ }
+ }
+
+ /**
+ * An implementation of EntityIterator that joins the contacts and data tables
+ * and consumes all the data rows for a contact in order to build the Entity for a contact.
+ */
+ private static class GroupsEntityIterator implements EntityIterator {
+ private final Cursor mEntityCursor;
+ private volatile boolean mIsClosed;
+
+ private static final String[] PROJECTION = new String[]{
+ Groups._ID,
+ Groups.ACCOUNT_NAME,
+ Groups.ACCOUNT_TYPE,
+ Groups.SOURCE_ID,
+ Groups.DIRTY,
+ Groups.VERSION,
+ Groups.RES_PACKAGE,
+ Groups.TITLE,
+ Groups.TITLE_RES,
+ Groups.GROUP_VISIBLE};
+
+ private static final int COLUMN_ID = 0;
+ private static final int COLUMN_ACCOUNT_NAME = 1;
+ private static final int COLUMN_ACCOUNT_TYPE = 2;
+ private static final int COLUMN_SOURCE_ID = 3;
+ private static final int COLUMN_DIRTY = 4;
+ private static final int COLUMN_VERSION = 5;
+ private static final int COLUMN_RES_PACKAGE = 6;
+ private static final int COLUMN_TITLE = 7;
+ private static final int COLUMN_TITLE_RES = 8;
+ private static final int COLUMN_GROUP_VISIBLE = 9;
+
+ public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri,
+ String selection, String[] selectionArgs, String sortOrder) {
+ mIsClosed = false;
+
+ final String updatedSortOrder = (sortOrder == null)
+ ? Groups._ID
+ : (Groups._ID + "," + sortOrder);
+
+ final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase();
+ final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(Tables.GROUPS_JOIN_PACKAGES);
+ qb.setProjectionMap(sGroupsProjectionMap);
+ if (groupIdString != null) {
+ qb.appendWhere(Groups._ID + "=" + groupIdString);
+ }
+ final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
+ final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
+ if (!TextUtils.isEmpty(accountName)) {
+ qb.appendWhere(Groups.ACCOUNT_NAME + "="
+ + DatabaseUtils.sqlEscapeString(accountName) + " AND "
+ + Groups.ACCOUNT_TYPE + "="
+ + DatabaseUtils.sqlEscapeString(accountType));
+ }
+ mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
+ null, null, updatedSortOrder);
+ mEntityCursor.moveToFirst();
+ }
+
+ public void close() {
+ if (mIsClosed) {
+ throw new IllegalStateException("closing when already closed");
+ }
+ mIsClosed = true;
+ mEntityCursor.close();
+ }
+
+ public boolean hasNext() throws RemoteException {
+ if (mIsClosed) {
+ throw new IllegalStateException("calling hasNext() when the iterator is closed");
+ }
+
+ return !mEntityCursor.isAfterLast();
+ }
+
+ public Entity next() throws RemoteException {
+ if (mIsClosed) {
+ throw new IllegalStateException("calling next() when the iterator is closed");
+ }
+ if (!hasNext()) {
+ throw new IllegalStateException("you may only call next() if hasNext() is true");
+ }
+
+ final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
+
+ final long groupId = c.getLong(COLUMN_ID);
+
+ // we expect the cursor is already at the row we need to read from
+ ContentValues groupValues = new ContentValues();
+ groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
+ groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
+ groupValues.put(Groups._ID, groupId);
+ groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY));
+ groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION));
+ groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
+ groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
+ groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE));
+ groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES));
+ groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE));
+ Entity group = new Entity(groupValues);
+
+ mEntityCursor.moveToNext();
+
+ return group;
+ }
+ }
+
+ @Override
+ public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
+ String sortOrder) {
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case RAW_CONTACTS:
+ case RAW_CONTACTS_ID:
+ String contactsIdString = null;
+ if (match == RAW_CONTACTS_ID) {
+ contactsIdString = uri.getPathSegments().get(1);
+ }
+
+ return new ContactsEntityIterator(this, contactsIdString,
+ uri, selection, selectionArgs, sortOrder);
+ case GROUPS:
+ case GROUPS_ID:
+ String idString = null;
+ if (match == GROUPS_ID) {
+ idString = uri.getPathSegments().get(1);
+ }
+
+ return new GroupsEntityIterator(this, idString,
+ uri, selection, selectionArgs, sortOrder);
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case CONTACTS: return Contacts.CONTENT_TYPE;
+ case CONTACTS_ID: return Contacts.CONTENT_ITEM_TYPE;
+ case RAW_CONTACTS: return RawContacts.CONTENT_TYPE;
+ case RAW_CONTACTS_ID: return RawContacts.CONTENT_ITEM_TYPE;
+ case DATA_ID:
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ long dataId = ContentUris.parseId(uri);
+ return mOpenHelper.getDataMimeType(dataId);
+ case AGGREGATION_EXCEPTIONS: return AggregationExceptions.CONTENT_TYPE;
+ case AGGREGATION_EXCEPTION_ID: return AggregationExceptions.CONTENT_ITEM_TYPE;
+ case AGGREGATION_SUGGESTIONS: return Contacts.CONTENT_TYPE;
+ case SEARCH_SUGGESTIONS:
+ return SearchManager.SUGGEST_MIME_TYPE;
+ case SEARCH_SHORTCUT:
+ return SearchManager.SHORTCUT_MIME_TYPE;
+ }
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+
+ @Override
+ public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ ContentProviderResult[] results = super.applyBatch(operations);
+ db.setTransactionSuccessful();
+ return results;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private void setDisplayName(long rawContactId, String displayName) {
+ if (displayName != null) {
+ mContactDisplayNameUpdate.bindString(1, displayName);
+ } else {
+ mContactDisplayNameUpdate.bindNull(1);
+ }
+ mContactDisplayNameUpdate.bindLong(2, rawContactId);
+ mContactDisplayNameUpdate.execute();
+ }
+
+ /*
+ * Sets the given dataId record in the "data" table to primary, and resets all data records of
+ * the same mimetype and under the same contact to not be primary.
+ *
+ * @param dataId the id of the data record to be set to primary.
+ */
+ private void setIsPrimary(long dataId) {
+ mSetPrimaryStatement.bindLong(1, dataId);
+ mSetPrimaryStatement.bindLong(2, dataId);
+ mSetPrimaryStatement.bindLong(3, dataId);
+ mSetPrimaryStatement.execute();
+ }
+
+ /*
+ * Sets the given dataId record in the "data" table to "super primary", and resets all data
+ * records of the same mimetype and under the same aggregate to not be "super primary".
+ *
+ * @param dataId the id of the data record to be set to primary.
+ */
+ private void setIsSuperPrimary(long dataId) {
+ mSetSuperPrimaryStatement.bindLong(1, dataId);
+ mSetSuperPrimaryStatement.bindLong(2, dataId);
+ mSetSuperPrimaryStatement.bindLong(3, dataId);
+ mSetSuperPrimaryStatement.execute();
+
+ // Find the parent aggregate and package for this new primary
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ long aggId = -1;
+ boolean isRestricted = false;
+ String mimeType = null;
+
+ Cursor cursor = null;
+ try {
+ cursor = db.query(DataRawContactsQuery.TABLE, DataRawContactsQuery.PROJECTION,
+ DataColumns.CONCRETE_ID + "=" + dataId, null, null, null, null);
+ if (cursor.moveToFirst()) {
+ aggId = cursor.getLong(DataRawContactsQuery.CONTACT_ID);
+ isRestricted = (cursor.getInt(DataRawContactsQuery.IS_RESTRICTED) == 1);
+ mimeType = cursor.getString(DataRawContactsQuery.MIMETYPE);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ // Bypass aggregate update if no parent found, or if we don't keep track
+ // of super-primary for this mimetype.
+ if (aggId == -1) {
+ return;
+ }
+
+ boolean isPhone = CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType);
+ boolean isEmail = CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType);
+
+ // Record this value as the new primary for the parent aggregate
+ final ContentValues values = new ContentValues();
+ if (isPhone) {
+ values.put(ContactsColumns.OPTIMAL_PRIMARY_PHONE_ID, dataId);
+ values.put(ContactsColumns.OPTIMAL_PRIMARY_PHONE_IS_RESTRICTED, isRestricted);
+ } else if (isEmail) {
+ values.put(ContactsColumns.OPTIMAL_PRIMARY_EMAIL_ID, dataId);
+ values.put(ContactsColumns.OPTIMAL_PRIMARY_EMAIL_IS_RESTRICTED, isRestricted);
+ }
+
+ // If this data is unrestricted, then also set as fallback
+ if (!isRestricted && isPhone) {
+ values.put(ContactsColumns.FALLBACK_PRIMARY_PHONE_ID, dataId);
+ } else if (!isRestricted && isEmail) {
+ values.put(ContactsColumns.FALLBACK_PRIMARY_EMAIL_ID, dataId);
+ }
+
+ // Push update into contacts table, if needed
+ if (values.size() > 0) {
+ db.update(Tables.CONTACTS, values, Contacts._ID + "=" + aggId, null);
+ }
+ }
+
+ private String buildContactLookupWhereClause(String filterParam) {
+ StringBuilder filter = new StringBuilder();
+ filter.append(Tables.CONTACTS);
+ filter.append(".");
+ filter.append(Contacts._ID);
+ filter.append(" IN (SELECT ");
+ filter.append(RawContacts.CONTACT_ID);
+ filter.append(" FROM ");
+ filter.append(Tables.RAW_CONTACTS);
+ filter.append(" WHERE ");
+ filter.append(RawContacts._ID);
+ filter.append(" IN ");
+ appendRawContactsByFilterAsNestedQuery(filter, filterParam, null);
+ filter.append(")");
+ return filter.toString();
+ }
+
+ public String getRawContactsByFilterAsNestedQuery(String filterParam) {
+ StringBuilder sb = new StringBuilder();
+ appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
+ return sb.toString();
+ }
+
+ private void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam,
+ String limit) {
+ sb.append("(SELECT DISTINCT raw_contact_id FROM name_lookup WHERE normalized_name GLOB '");
+ sb.append(NameNormalizer.normalize(filterParam));
+ sb.append("*'");
+ if (limit != null) {
+ sb.append(" LIMIT ").append(limit);
+ }
+ sb.append(")");
+ }
+
+ private String[] appendGroupArg(String[] selectionArgs, String arg) {
+ if (selectionArgs == null) {
+ return new String[] {arg};
+ } else {
+ int newLength = selectionArgs.length + 1;
+ String[] newSelectionArgs = new String[newLength];
+ System.arraycopy(selectionArgs, 0, newSelectionArgs, 0, selectionArgs.length);
+ newSelectionArgs[newLength - 1] = arg;
+ return newSelectionArgs;
+ }
+ }
+}
diff --git a/src/com/android/providers/contacts/Hex.java b/src/com/android/providers/contacts/Hex.java
new file mode 100644
index 0000000..991f095
--- /dev/null
+++ b/src/com/android/providers/contacts/Hex.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+/**
+ * Basic hex operations: from byte array to string and vice versa.
+ *
+ * TODO: move to the framework and consider implementing as native code.
+ */
+public class Hex {
+
+ private static final char[] HEX_DIGITS = new char[]{
+ '0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+ };
+
+ private static final char[] FIRST_CHAR = new char[256];
+ private static final char[] SECOND_CHAR = new char[256];
+ static {
+ for (int i = 0; i < 256; i++) {
+ FIRST_CHAR[i] = HEX_DIGITS[(i >> 4) & 0xF];
+ SECOND_CHAR[i] = HEX_DIGITS[i & 0xF];
+ }
+ }
+
+ private static final byte[] DIGITS = new byte['f'+1];
+ static {
+ for (int i = 0; i <= 'F'; i++) {
+ DIGITS[i] = -1;
+ }
+ for (byte i = 0; i < 10; i++) {
+ DIGITS['0' + i] = i;
+ }
+ for (byte i = 0; i < 6; i++) {
+ DIGITS['A' + i] = (byte)(10 + i);
+ DIGITS['a' + i] = (byte)(10 + i);
+ }
+ }
+
+ /**
+ * Quickly converts a byte array to a hexadecimal string representation.
+ *
+ * @param array byte array, possibly zero-terminated.
+ */
+ public static String encodeHex(byte[] array, boolean zeroTerminated) {
+ char[] cArray = new char[array.length * 2];
+
+ int j = 0;
+ for (int i = 0; i < array.length; i++) {
+ int index = array[i] & 0xFF;
+ if (index == 0 && zeroTerminated) {
+ break;
+ }
+
+ cArray[j++] = FIRST_CHAR[index];
+ cArray[j++] = SECOND_CHAR[index];
+ }
+
+ return new String(cArray, 0, j);
+ }
+
+ /**
+ * Quickly converts a hexadecimal string to a byte array.
+ */
+ public static byte[] decodeHex(String hexString) {
+ int length = hexString.length();
+
+ if ((length & 0x01) != 0) {
+ throw new IllegalArgumentException("Odd number of characters.");
+ }
+
+ boolean badHex = false;
+ byte[] out = new byte[length >> 1];
+ for (int i = 0, j = 0; j < length; i++) {
+ int c1 = hexString.charAt(j++);
+ if (c1 > 'f') {
+ badHex = true;
+ break;
+ }
+
+ final byte d1 = DIGITS[c1];
+ if (d1 == -1) {
+ badHex = true;
+ break;
+ }
+
+ int c2 = hexString.charAt(j++);
+ if (c2 > 'f') {
+ badHex = true;
+ break;
+ }
+
+ final byte d2 = DIGITS[c2];
+ if (d2 == -1) {
+ badHex = true;
+ break;
+ }
+
+ out[i] = (byte) (d1 << 4 | d2);
+ }
+
+ if (badHex) {
+ throw new IllegalArgumentException("Invalid hexadecimal digit: " + hexString);
+ }
+
+ return out;
+ }
+}
diff --git a/src/com/android/providers/contacts/JaroWinklerDistance.java b/src/com/android/providers/contacts/JaroWinklerDistance.java
new file mode 100644
index 0000000..730059d
--- /dev/null
+++ b/src/com/android/providers/contacts/JaroWinklerDistance.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import java.util.Arrays;
+
+/**
+ * A string distance calculator, particularly suited for name matching.
+ * <p>
+ * A detailed discussion of the topic of record linkage in general and name matching
+ * in particular can be found in this article:
+ * <blockquote>
+ * Winkler, W. E. (2006). "Overview of Record Linkage and Current Research Directions".
+ * Research Report Series, RRS.
+ * </blockquote>
+ */
+public class JaroWinklerDistance {
+
+ private static final float WINKLER_BONUS_THRESHOLD = 0.7f;
+
+ private final int mMaxLength;
+ private final boolean[] mMatchFlags1;
+ private final boolean[] mMatchFlags2;
+
+ /**
+ * Constructor.
+ *
+ * @param maxLength byte arrays are truncate if longer than this number
+ */
+ public JaroWinklerDistance(int maxLength) {
+ mMaxLength = maxLength;
+ mMatchFlags1 = new boolean[maxLength];
+ mMatchFlags2 = new boolean[maxLength];
+ }
+
+ /**
+ * Computes a string distance between two normalized strings passed as byte arrays.
+ */
+ public float getDistance(byte bytes1[], byte bytes2[]) {
+ byte[] array1, array2;
+
+ if (bytes1.length > bytes2.length) {
+ array2 = bytes1;
+ array1 = bytes2;
+ } else {
+ array2 = bytes2;
+ array1 = bytes1;
+ }
+
+ int length1 = array1.length;
+ if (length1 > mMaxLength) {
+ length1 = mMaxLength;
+ }
+
+ int length2 = array2.length;
+ if (length2 > mMaxLength) {
+ length2 = mMaxLength;
+ }
+
+ Arrays.fill(mMatchFlags1, 0, length1, false);
+ Arrays.fill(mMatchFlags2, 0, length2, false);
+
+ int range = length2 / 2 - 1;
+ if (range < 0) {
+ range = 0;
+ }
+
+ int matches = 0;
+ for (int i = 0; i < length1; i++) {
+ byte c1 = array1[i];
+
+ int from = i - range;
+ if (from < 0) {
+ from = 0;
+ }
+
+ int to = i + range + 1;
+ if (to > length2) {
+ to = length2;
+ }
+
+ for (int j = from; j < to; j++) {
+ if (!mMatchFlags2[j] && c1 == array2[j]) {
+ mMatchFlags1[i] = mMatchFlags2[j] = true;
+ matches++;
+ break;
+ }
+ }
+ }
+
+ if (matches == 0) {
+ return 0f;
+ }
+
+ int transpositions = 0;
+ int j = 0;
+ for (int i = 0; i < length1; i++) {
+ if (mMatchFlags1[i]) {
+ while (!mMatchFlags2[j]) {
+ j++;
+ }
+ if (array1[i] != array2[j]) {
+ transpositions++;
+ }
+ j++;
+ }
+ }
+
+ float m = matches;
+ float jaro = ((m / length1 + m / length2 + (m - (transpositions / 2)) / m)) / 3;
+
+ if (jaro < WINKLER_BONUS_THRESHOLD) {
+ return jaro;
+ }
+
+ // Add Winkler bonus
+ int prefix = 0;
+ for (int i = 0; i < length1; i++) {
+ if (bytes1[i] != bytes2[i]) {
+ break;
+ }
+ prefix++;
+ }
+
+ return jaro + Math.min(0.1f, 1f / length2) * prefix * (1 - jaro);
+ }
+}
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
new file mode 100644
index 0000000..d04bf63
--- /dev/null
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -0,0 +1,1333 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import com.android.providers.contacts.OpenHelper.DataColumns;
+import com.android.providers.contacts.OpenHelper.ExtensionsColumns;
+import com.android.providers.contacts.OpenHelper.GroupsColumns;
+import com.android.providers.contacts.OpenHelper.MimetypesColumns;
+import com.android.providers.contacts.OpenHelper.PhoneColumns;
+import com.android.providers.contacts.OpenHelper.RawContactsColumns;
+import com.android.providers.contacts.OpenHelper.Tables;
+
+import android.app.SearchManager;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.database.sqlite.SQLiteStatement;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.People;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.util.Log;
+
+import java.util.HashMap;
+
+public class LegacyApiSupport implements OpenHelper.Delegate {
+
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ private static final int PEOPLE = 1;
+ private static final int PEOPLE_ID = 2;
+ private static final int PEOPLE_UPDATE_CONTACT_TIME = 3;
+ private static final int ORGANIZATIONS = 4;
+ private static final int ORGANIZATIONS_ID = 5;
+ private static final int PEOPLE_CONTACTMETHODS = 6;
+ private static final int PEOPLE_CONTACTMETHODS_ID = 7;
+ private static final int CONTACTMETHODS = 8;
+ private static final int CONTACTMETHODS_ID = 9;
+ private static final int PEOPLE_PHONES = 10;
+ private static final int PEOPLE_PHONES_ID = 11;
+ private static final int PHONES = 12;
+ private static final int PHONES_ID = 13;
+ private static final int EXTENSIONS = 14;
+ private static final int EXTENSIONS_ID = 15;
+ private static final int PEOPLE_EXTENSIONS = 16;
+ private static final int PEOPLE_EXTENSIONS_ID = 17;
+ private static final int GROUPS = 18;
+ private static final int GROUPS_ID = 19;
+ private static final int GROUPMEMBERSHIP = 20;
+ private static final int GROUPMEMBERSHIP_ID = 21;
+ private static final int PEOPLE_GROUPMEMBERSHIP = 22;
+ private static final int PEOPLE_GROUPMEMBERSHIP_ID = 23;
+ private static final int PEOPLE_PHOTO = 24;
+ private static final int PHOTOS = 25;
+ private static final int PHOTOS_ID = 26;
+ private static final int PRESENCE = 27;
+ private static final int PRESENCE_ID = 28;
+ private static final int PEOPLE_FILTER = 29;
+ private static final int DELETED_PEOPLE = 30;
+ private static final int DELETED_GROUPS = 31;
+ private static final int SEARCH_SUGGESTIONS = 32;
+
+
+
+ private static final String PEOPLE_JOINS =
+ " LEFT OUTER JOIN data name ON (raw_contacts._id = name.raw_contact_id"
+ + " AND (SELECT mimetype FROM mimetypes WHERE mimetypes._id = name.mimetype_id)"
+ + "='" + StructuredName.CONTENT_ITEM_TYPE + "')"
+ + " LEFT OUTER JOIN data organization ON (raw_contacts._id = organization.raw_contact_id"
+ + " AND (SELECT mimetype FROM mimetypes WHERE mimetypes._id = organization.mimetype_id)"
+ + "='" + Organization.CONTENT_ITEM_TYPE + "' AND organization.is_primary)"
+ + " LEFT OUTER JOIN data email ON (raw_contacts._id = email.raw_contact_id"
+ + " AND (SELECT mimetype FROM mimetypes WHERE mimetypes._id = email.mimetype_id)"
+ + "='" + Email.CONTENT_ITEM_TYPE + "' AND email.is_primary)"
+ + " LEFT OUTER JOIN data note ON (raw_contacts._id = note.raw_contact_id"
+ + " AND (SELECT mimetype FROM mimetypes WHERE mimetypes._id = note.mimetype_id)"
+ + "='" + Note.CONTENT_ITEM_TYPE + "')"
+ + " LEFT OUTER JOIN data phone ON (raw_contacts._id = phone.raw_contact_id"
+ + " AND (SELECT mimetype FROM mimetypes WHERE mimetypes._id = phone.mimetype_id)"
+ + "='" + Phone.CONTENT_ITEM_TYPE + "' AND phone.is_primary)";
+
+ public static final String DATA_JOINS =
+ " JOIN mimetypes ON (mimetypes._id = data.mimetype_id)"
+ + " JOIN raw_contacts ON (raw_contacts._id = data.raw_contact_id)"
+ + PEOPLE_JOINS;
+
+ public static final String PRESENCE_JOINS =
+ " LEFT OUTER JOIN presence ON ("
+ + " presence.presence_id = (SELECT max(presence_id) FROM presence"
+ + " WHERE view_v1_people._id = presence.raw_contact_id))";
+
+ private static final String PHONETIC_NAME_SQL = "trim(trim("
+ + "ifnull(name." + StructuredName.PHONETIC_GIVEN_NAME + ",' ')||' '||"
+ + "ifnull(name." + StructuredName.PHONETIC_MIDDLE_NAME + ",' '))||' '||"
+ + "ifnull(name." + StructuredName.PHONETIC_FAMILY_NAME + ",' ')) ";
+
+ private static final String CONTACT_METHOD_KIND_SQL =
+ "CAST ((CASE WHEN mimetype='" + Email.CONTENT_ITEM_TYPE + "'"
+ + " THEN " + android.provider.Contacts.KIND_EMAIL
+ + " ELSE"
+ + " (CASE WHEN mimetype='" + Im.CONTENT_ITEM_TYPE +"'"
+ + " THEN " + android.provider.Contacts.KIND_IM
+ + " ELSE"
+ + " (CASE WHEN mimetype='" + StructuredPostal.CONTENT_ITEM_TYPE + "'"
+ + " THEN " + android.provider.Contacts.KIND_POSTAL
+ + " ELSE"
+ + " NULL"
+ + " END)"
+ + " END)"
+ + " END) AS INTEGER)";
+
+ public interface LegacyTables {
+ public static final String PEOPLE = "view_v1_people";
+ public static final String PEOPLE_JOIN_PRESENCE = "view_v1_people" + PRESENCE_JOINS;
+ public static final String GROUPS = "view_v1_groups";
+ public static final String ORGANIZATIONS = "view_v1_organizations";
+ public static final String CONTACT_METHODS = "view_v1_contact_methods";
+ public static final String PHONES = "view_v1_phones";
+ public static final String EXTENSIONS = "view_v1_extensions";
+ public static final String GROUP_MEMBERSHIP = "view_v1_group_membership";
+ public static final String PHOTOS = "view_v1_photos";
+ public static final String PRESENCE_JOIN_CONTACTS = Tables.PRESENCE +
+ " LEFT OUTER JOIN " + Tables.RAW_CONTACTS
+ + " ON (" + Tables.PRESENCE + "." + Presence.RAW_CONTACT_ID + "="
+ + RawContactsColumns.CONCRETE_ID + ")";
+ }
+
+ private static final String[] ORGANIZATION_MIME_TYPES = new String[] {
+ Organization.CONTENT_ITEM_TYPE
+ };
+
+ private static final String[] CONTACT_METHOD_MIME_TYPES = new String[] {
+ Email.CONTENT_ITEM_TYPE,
+ Im.CONTENT_ITEM_TYPE,
+ StructuredPostal.CONTENT_ITEM_TYPE,
+ };
+
+ private static final String[] PHONE_MIME_TYPES = new String[] {
+ Phone.CONTENT_ITEM_TYPE
+ };
+
+ private interface PhotoQuery {
+ String[] COLUMNS = { Data._ID };
+
+ int _ID = 0;
+ }
+
+ /**
+ * A custom data row that is used to store legacy photo data fields no
+ * longer directly supported by the API.
+ */
+ private interface LegacyPhotoData {
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/photo_v1_extras";
+
+ public static final String PHOTO_DATA_ID = Data.DATA1;
+ public static final String LOCAL_VERSION = Data.DATA2;
+ public static final String DOWNLOAD_REQUIRED = Data.DATA3;
+ public static final String EXISTS_ON_SERVER = Data.DATA4;
+ public static final String SYNC_ERROR = Data.DATA5;
+ }
+
+ public static final String LEGACY_PHOTO_JOIN =
+ " LEFT OUTER JOIN data legacy_photo ON (raw_contacts._id = legacy_photo.raw_contact_id"
+ + " AND (SELECT mimetype FROM mimetypes WHERE mimetypes._id = legacy_photo.mimetype_id)"
+ + "='" + LegacyPhotoData.CONTENT_ITEM_TYPE + "'"
+ + " AND " + DataColumns.CONCRETE_ID + " = legacy_photo." + LegacyPhotoData.PHOTO_DATA_ID
+ + ")";
+
+ private static final HashMap<String, String> sPeopleProjectionMap;
+ private static final HashMap<String, String> sOrganizationProjectionMap;
+ private static final HashMap<String, String> sContactMethodProjectionMap;
+ private static final HashMap<String, String> sPhoneProjectionMap;
+ private static final HashMap<String, String> sExtensionProjectionMap;
+ private static final HashMap<String, String> sGroupProjectionMap;
+ private static final HashMap<String, String> sGroupMembershipProjectionMap;
+ private static final HashMap<String, String> sPhotoProjectionMap;
+ private static final HashMap<String, String> sPresenceProjectionMap;
+
+ static {
+
+ // Contacts URI matching table
+ UriMatcher matcher = sUriMatcher;
+
+ String authority = android.provider.Contacts.AUTHORITY;
+ matcher.addURI(authority, "extensions", EXTENSIONS);
+ matcher.addURI(authority, "extensions/#", EXTENSIONS_ID);
+ matcher.addURI(authority, "groups", GROUPS);
+ matcher.addURI(authority, "groups/#", GROUPS_ID);
+// matcher.addURI(authority, "groups/name/*/members", GROUP_NAME_MEMBERS);
+// matcher.addURI(authority, "groups/name/*/members/filter/*",
+// GROUP_NAME_MEMBERS_FILTER);
+// matcher.addURI(authority, "groups/system_id/*/members", GROUP_SYSTEM_ID_MEMBERS);
+// matcher.addURI(authority, "groups/system_id/*/members/filter/*",
+// GROUP_SYSTEM_ID_MEMBERS_FILTER);
+ matcher.addURI(authority, "groupmembership", GROUPMEMBERSHIP);
+ matcher.addURI(authority, "groupmembership/#", GROUPMEMBERSHIP_ID);
+// matcher.addURI(authority, "groupmembershipraw", GROUPMEMBERSHIP_RAW);
+ matcher.addURI(authority, "people", PEOPLE);
+// matcher.addURI(authority, "people/strequent", PEOPLE_STREQUENT);
+// matcher.addURI(authority, "people/strequent/filter/*", PEOPLE_STREQUENT_FILTER);
+ matcher.addURI(authority, "people/filter/*", PEOPLE_FILTER);
+// matcher.addURI(authority, "people/with_phones_filter/*",
+// PEOPLE_WITH_PHONES_FILTER);
+// matcher.addURI(authority, "people/with_email_or_im_filter/*",
+// PEOPLE_WITH_EMAIL_OR_IM_FILTER);
+ matcher.addURI(authority, "people/#", PEOPLE_ID);
+ matcher.addURI(authority, "people/#/extensions", PEOPLE_EXTENSIONS);
+ matcher.addURI(authority, "people/#/extensions/#", PEOPLE_EXTENSIONS_ID);
+ matcher.addURI(authority, "people/#/phones", PEOPLE_PHONES);
+ matcher.addURI(authority, "people/#/phones/#", PEOPLE_PHONES_ID);
+// matcher.addURI(authority, "people/#/phones_with_presence",
+// PEOPLE_PHONES_WITH_PRESENCE);
+ matcher.addURI(authority, "people/#/photo", PEOPLE_PHOTO);
+// matcher.addURI(authority, "people/#/photo/data", PEOPLE_PHOTO_DATA);
+ matcher.addURI(authority, "people/#/contact_methods", PEOPLE_CONTACTMETHODS);
+// matcher.addURI(authority, "people/#/contact_methods_with_presence",
+// PEOPLE_CONTACTMETHODS_WITH_PRESENCE);
+ matcher.addURI(authority, "people/#/contact_methods/#", PEOPLE_CONTACTMETHODS_ID);
+// matcher.addURI(authority, "people/#/organizations", PEOPLE_ORGANIZATIONS);
+// matcher.addURI(authority, "people/#/organizations/#", PEOPLE_ORGANIZATIONS_ID);
+ matcher.addURI(authority, "people/#/groupmembership", PEOPLE_GROUPMEMBERSHIP);
+ matcher.addURI(authority, "people/#/groupmembership/#", PEOPLE_GROUPMEMBERSHIP_ID);
+// matcher.addURI(authority, "people/raw", PEOPLE_RAW);
+// matcher.addURI(authority, "people/owner", PEOPLE_OWNER);
+ matcher.addURI(authority, "people/#/update_contact_time",
+ PEOPLE_UPDATE_CONTACT_TIME);
+ matcher.addURI(authority, "deleted_people", DELETED_PEOPLE);
+ matcher.addURI(authority, "deleted_groups", DELETED_GROUPS);
+ matcher.addURI(authority, "phones", PHONES);
+// matcher.addURI(authority, "phones_with_presence", PHONES_WITH_PRESENCE);
+// matcher.addURI(authority, "phones/filter/*", PHONES_FILTER);
+// matcher.addURI(authority, "phones/filter_name/*", PHONES_FILTER_NAME);
+// matcher.addURI(authority, "phones/mobile_filter_name/*",
+// PHONES_MOBILE_FILTER_NAME);
+ matcher.addURI(authority, "phones/#", PHONES_ID);
+ matcher.addURI(authority, "photos", PHOTOS);
+ matcher.addURI(authority, "photos/#", PHOTOS_ID);
+ matcher.addURI(authority, "contact_methods", CONTACTMETHODS);
+// matcher.addURI(authority, "contact_methods/email", CONTACTMETHODS_EMAIL);
+// matcher.addURI(authority, "contact_methods/email/*", CONTACTMETHODS_EMAIL_FILTER);
+ matcher.addURI(authority, "contact_methods/#", CONTACTMETHODS_ID);
+// matcher.addURI(authority, "contact_methods/with_presence",
+// CONTACTMETHODS_WITH_PRESENCE);
+ matcher.addURI(authority, "presence", PRESENCE);
+ matcher.addURI(authority, "presence/#", PRESENCE_ID);
+ matcher.addURI(authority, "organizations", ORGANIZATIONS);
+ matcher.addURI(authority, "organizations/#", ORGANIZATIONS_ID);
+// matcher.addURI(authority, "voice_dialer_timestamp", VOICE_DIALER_TIMESTAMP);
+ matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY,
+ SEARCH_SUGGESTIONS);
+ matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
+ SEARCH_SUGGESTIONS);
+// matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
+// SEARCH_SHORTCUT);
+// matcher.addURI(authority, "settings", SETTINGS);
+//
+// matcher.addURI(authority, "live_folders/people", LIVE_FOLDERS_PEOPLE);
+// matcher.addURI(authority, "live_folders/people/*",
+// LIVE_FOLDERS_PEOPLE_GROUP_NAME);
+// matcher.addURI(authority, "live_folders/people_with_phones",
+// LIVE_FOLDERS_PEOPLE_WITH_PHONES);
+// matcher.addURI(authority, "live_folders/favorites",
+// LIVE_FOLDERS_PEOPLE_FAVORITES);
+
+
+ HashMap<String, String> peopleProjectionMap = new HashMap<String, String>();
+ peopleProjectionMap.put(People.NAME, People.NAME);
+ peopleProjectionMap.put(People.DISPLAY_NAME, People.DISPLAY_NAME);
+ peopleProjectionMap.put(People.PHONETIC_NAME, People.PHONETIC_NAME);
+ peopleProjectionMap.put(People.NOTES, People.NOTES);
+ peopleProjectionMap.put(People.TIMES_CONTACTED, People.TIMES_CONTACTED);
+ peopleProjectionMap.put(People.LAST_TIME_CONTACTED, People.LAST_TIME_CONTACTED);
+ peopleProjectionMap.put(People.CUSTOM_RINGTONE, People.CUSTOM_RINGTONE);
+ peopleProjectionMap.put(People.SEND_TO_VOICEMAIL, People.SEND_TO_VOICEMAIL);
+ peopleProjectionMap.put(People.STARRED, People.STARRED);
+
+ sPeopleProjectionMap = new HashMap<String, String>(peopleProjectionMap);
+ sPeopleProjectionMap.put(People.PRIMARY_ORGANIZATION_ID, People.PRIMARY_ORGANIZATION_ID);
+ sPeopleProjectionMap.put(People.PRIMARY_EMAIL_ID, People.PRIMARY_EMAIL_ID);
+ sPeopleProjectionMap.put(People.PRIMARY_PHONE_ID, People.PRIMARY_PHONE_ID);
+ sPeopleProjectionMap.put(People.NUMBER, People.NUMBER);
+ sPeopleProjectionMap.put(People.TYPE, People.TYPE);
+ sPeopleProjectionMap.put(People.LABEL, People.LABEL);
+ sPeopleProjectionMap.put(People.NUMBER_KEY, People.NUMBER_KEY);
+ sPeopleProjectionMap.put(People.IM_PROTOCOL, People.IM_PROTOCOL);
+ sPeopleProjectionMap.put(People.IM_HANDLE, People.IM_HANDLE);
+ sPeopleProjectionMap.put(People.IM_ACCOUNT, People.IM_ACCOUNT);
+ sPeopleProjectionMap.put(People.PRESENCE_STATUS, People.PRESENCE_STATUS);
+ sPeopleProjectionMap.put(People.PRESENCE_CUSTOM_STATUS, People.PRESENCE_CUSTOM_STATUS);
+
+ sOrganizationProjectionMap = new HashMap<String, String>();
+ sOrganizationProjectionMap.put(android.provider.Contacts.Organizations.PERSON_ID,
+ android.provider.Contacts.Organizations.PERSON_ID);
+ sOrganizationProjectionMap.put(android.provider.Contacts.Organizations.ISPRIMARY,
+ android.provider.Contacts.Organizations.ISPRIMARY);
+ sOrganizationProjectionMap.put(android.provider.Contacts.Organizations.COMPANY,
+ android.provider.Contacts.Organizations.COMPANY);
+ sOrganizationProjectionMap.put(android.provider.Contacts.Organizations.TYPE,
+ android.provider.Contacts.Organizations.TYPE);
+ sOrganizationProjectionMap.put(android.provider.Contacts.Organizations.LABEL,
+ android.provider.Contacts.Organizations.LABEL);
+ sOrganizationProjectionMap.put(android.provider.Contacts.Organizations.TITLE,
+ android.provider.Contacts.Organizations.TITLE);
+
+ sContactMethodProjectionMap = new HashMap<String, String>(peopleProjectionMap);
+ sContactMethodProjectionMap.put(ContactMethods.PERSON_ID, ContactMethods.PERSON_ID);
+ sContactMethodProjectionMap.put(ContactMethods.KIND, ContactMethods.KIND);
+ sContactMethodProjectionMap.put(ContactMethods.ISPRIMARY, ContactMethods.ISPRIMARY);
+ sContactMethodProjectionMap.put(ContactMethods.TYPE, ContactMethods.TYPE);
+ sContactMethodProjectionMap.put(ContactMethods.DATA, ContactMethods.DATA);
+ sContactMethodProjectionMap.put(ContactMethods.LABEL, ContactMethods.LABEL);
+ sContactMethodProjectionMap.put(ContactMethods.AUX_DATA, ContactMethods.AUX_DATA);
+
+ sPhoneProjectionMap = new HashMap<String, String>(peopleProjectionMap);
+ sPhoneProjectionMap.put(android.provider.Contacts.Phones.PERSON_ID,
+ android.provider.Contacts.Phones.PERSON_ID);
+ sPhoneProjectionMap.put(android.provider.Contacts.Phones.ISPRIMARY,
+ android.provider.Contacts.Phones.ISPRIMARY);
+ sPhoneProjectionMap.put(android.provider.Contacts.Phones.NUMBER,
+ android.provider.Contacts.Phones.NUMBER);
+ sPhoneProjectionMap.put(android.provider.Contacts.Phones.TYPE,
+ android.provider.Contacts.Phones.TYPE);
+ sPhoneProjectionMap.put(android.provider.Contacts.Phones.LABEL,
+ android.provider.Contacts.Phones.LABEL);
+ sPhoneProjectionMap.put(android.provider.Contacts.Phones.NUMBER_KEY,
+ android.provider.Contacts.Phones.NUMBER_KEY);
+
+ sExtensionProjectionMap = new HashMap<String, String>();
+ sExtensionProjectionMap.put(android.provider.Contacts.Extensions.PERSON_ID,
+ android.provider.Contacts.Extensions.PERSON_ID);
+ sExtensionProjectionMap.put(android.provider.Contacts.Extensions.NAME,
+ android.provider.Contacts.Extensions.NAME);
+ sExtensionProjectionMap.put(android.provider.Contacts.Extensions.VALUE,
+ android.provider.Contacts.Extensions.VALUE);
+
+ sGroupProjectionMap = new HashMap<String, String>();
+ sGroupProjectionMap.put(android.provider.Contacts.Groups._ID,
+ android.provider.Contacts.Groups._ID);
+ sGroupProjectionMap.put(android.provider.Contacts.Groups.NAME,
+ android.provider.Contacts.Groups.NAME);
+ sGroupProjectionMap.put(android.provider.Contacts.Groups.NOTES,
+ android.provider.Contacts.Groups.NOTES);
+ sGroupProjectionMap.put(android.provider.Contacts.Groups.SYSTEM_ID,
+ android.provider.Contacts.Groups.SYSTEM_ID);
+
+ sGroupMembershipProjectionMap = new HashMap<String, String>();
+ sGroupMembershipProjectionMap.put(android.provider.Contacts.GroupMembership.PERSON_ID,
+ android.provider.Contacts.GroupMembership.PERSON_ID);
+ sGroupMembershipProjectionMap.put(android.provider.Contacts.GroupMembership.GROUP_ID,
+ android.provider.Contacts.GroupMembership.GROUP_ID);
+
+ sPhotoProjectionMap = new HashMap<String, String>();
+ sPhotoProjectionMap.put(android.provider.Contacts.Photos.PERSON_ID,
+ android.provider.Contacts.Photos.PERSON_ID);
+ sPhotoProjectionMap.put(android.provider.Contacts.Photos.DATA,
+ android.provider.Contacts.Photos.DATA);
+ sPhotoProjectionMap.put(android.provider.Contacts.Photos.LOCAL_VERSION,
+ android.provider.Contacts.Photos.LOCAL_VERSION);
+ sPhotoProjectionMap.put(android.provider.Contacts.Photos.DOWNLOAD_REQUIRED,
+ android.provider.Contacts.Photos.DOWNLOAD_REQUIRED);
+ sPhotoProjectionMap.put(android.provider.Contacts.Photos.EXISTS_ON_SERVER,
+ android.provider.Contacts.Photos.EXISTS_ON_SERVER);
+ sPhotoProjectionMap.put(android.provider.Contacts.Photos.SYNC_ERROR,
+ android.provider.Contacts.Photos.SYNC_ERROR);
+
+ sPresenceProjectionMap = new HashMap<String, String>();
+ sPresenceProjectionMap.put(android.provider.Contacts.Presence._ID,
+ Tables.PRESENCE + "." + Presence._ID
+ + " AS " + android.provider.Contacts.Presence._ID);
+ sPresenceProjectionMap.put(android.provider.Contacts.Presence.PERSON_ID,
+ Tables.PRESENCE + "." + Presence.RAW_CONTACT_ID
+ + " AS " + android.provider.Contacts.Presence.PERSON_ID);
+ sPresenceProjectionMap.put(android.provider.Contacts.Presence.IM_PROTOCOL,
+ Presence.IM_PROTOCOL
+ + " AS " + android.provider.Contacts.Presence.IM_PROTOCOL);
+ sPresenceProjectionMap.put(android.provider.Contacts.Presence.IM_HANDLE,
+ Presence.IM_HANDLE
+ + " AS " + android.provider.Contacts.Presence.IM_HANDLE);
+ sPresenceProjectionMap.put(android.provider.Contacts.Presence.IM_ACCOUNT,
+ Presence.IM_ACCOUNT
+ + " AS " + android.provider.Contacts.Presence.IM_ACCOUNT);
+ sPresenceProjectionMap.put(android.provider.Contacts.Presence.PRESENCE_STATUS,
+ Presence.PRESENCE_STATUS
+ + " AS " + android.provider.Contacts.Presence.PRESENCE_STATUS);
+ sPresenceProjectionMap.put(android.provider.Contacts.Presence.PRESENCE_CUSTOM_STATUS,
+ Presence.PRESENCE_CUSTOM_STATUS
+ + " AS " + android.provider.Contacts.Presence.PRESENCE_CUSTOM_STATUS);
+ }
+
+ private final Context mContext;
+ private final OpenHelper mOpenHelper;
+ private final ContactsProvider2 mContactsProvider;
+ private final NameSplitter mPhoneticNameSplitter;
+
+ /** Precompiled sql statement for incrementing times contacted for a contact */
+ private final SQLiteStatement mLastTimeContactedUpdate;
+
+ private final ContentValues mValues = new ContentValues();
+
+ public LegacyApiSupport(Context context, OpenHelper openHelper,
+ ContactsProvider2 contactsProvider) {
+ mContext = context;
+ mContactsProvider = contactsProvider;
+ mOpenHelper = openHelper;
+ mOpenHelper.setDelegate(this);
+
+ mPhoneticNameSplitter = new NameSplitter("", "", "",
+ context.getString(com.android.internal.R.string.common_name_conjunctions));
+
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ mLastTimeContactedUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
+ + RawContacts.TIMES_CONTACTED + "="
+ + RawContacts.TIMES_CONTACTED + "+1,"
+ + RawContacts.LAST_TIME_CONTACTED + "=? WHERE "
+ + RawContacts._ID + "=?");
+ }
+
+
+ public void createDatabase(SQLiteDatabase db) {
+
+ db.execSQL("DROP VIEW IF EXISTS " + LegacyTables.PEOPLE + ";");
+ db.execSQL("CREATE VIEW " + LegacyTables.PEOPLE + " AS SELECT " +
+ RawContactsColumns.CONCRETE_ID
+ + " AS " + android.provider.Contacts.People._ID + ", " +
+ "name." + StructuredName.DISPLAY_NAME
+ + " AS " + People.NAME + ", " +
+ Tables.RAW_CONTACTS + "." + RawContactsColumns.DISPLAY_NAME
+ + " AS " + People.DISPLAY_NAME + ", " +
+ PHONETIC_NAME_SQL
+ + " AS " + People.PHONETIC_NAME + " , " +
+ "note." + Note.NOTE
+ + " AS " + People.NOTES + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.TIMES_CONTACTED
+ + " AS " + People.TIMES_CONTACTED + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.LAST_TIME_CONTACTED
+ + " AS " + People.LAST_TIME_CONTACTED + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.CUSTOM_RINGTONE
+ + " AS " + People.CUSTOM_RINGTONE + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.SEND_TO_VOICEMAIL
+ + " AS " + People.SEND_TO_VOICEMAIL + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.STARRED
+ + " AS " + People.STARRED + ", " +
+ "organization." + Data._ID
+ + " AS " + People.PRIMARY_ORGANIZATION_ID + ", " +
+ "email." + Data._ID
+ + " AS " + People.PRIMARY_EMAIL_ID + ", " +
+ "phone." + Data._ID
+ + " AS " + People.PRIMARY_PHONE_ID + ", " +
+ "phone." + Phone.NUMBER
+ + " AS " + People.NUMBER + ", " +
+ "phone." + Phone.TYPE
+ + " AS " + People.TYPE + ", " +
+ "phone." + Phone.LABEL
+ + " AS " + People.LABEL + ", " +
+ "phone." + PhoneColumns.NORMALIZED_NUMBER
+ + " AS " + People.NUMBER_KEY + ", " +
+ RawContacts.IS_RESTRICTED +
+ " FROM " + Tables.RAW_CONTACTS + PEOPLE_JOINS +
+ " WHERE " + Tables.RAW_CONTACTS + "." + RawContacts.DELETED + "=0" +
+ ";");
+
+ db.execSQL("DROP VIEW IF EXISTS " + LegacyTables.ORGANIZATIONS + ";");
+ db.execSQL("CREATE VIEW " + LegacyTables.ORGANIZATIONS + " AS SELECT " +
+ DataColumns.CONCRETE_ID
+ + " AS " + android.provider.Contacts.Organizations._ID + ", " +
+ Data.RAW_CONTACT_ID
+ + " AS " + android.provider.Contacts.Organizations.PERSON_ID + ", " +
+ Data.IS_PRIMARY
+ + " AS " + android.provider.Contacts.Organizations.ISPRIMARY + ", " +
+ Organization.COMPANY
+ + " AS " + android.provider.Contacts.Organizations.COMPANY + ", " +
+ Organization.TYPE
+ + " AS " + android.provider.Contacts.Organizations.TYPE + ", " +
+ Organization.LABEL
+ + " AS " + android.provider.Contacts.Organizations.LABEL + ", " +
+ Organization.TITLE
+ + " AS " + android.provider.Contacts.Organizations.TITLE + ", " +
+ RawContacts.IS_RESTRICTED +
+ " FROM " + Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS +
+ " WHERE " + MimetypesColumns.CONCRETE_MIMETYPE + "='"
+ + Organization.CONTENT_ITEM_TYPE + "'"
+ + " AND " + Tables.RAW_CONTACTS + "." + RawContacts.DELETED + "=0" +
+ ";");
+
+ db.execSQL("DROP VIEW IF EXISTS " + LegacyTables.CONTACT_METHODS + ";");
+ db.execSQL("CREATE VIEW " + LegacyTables.CONTACT_METHODS + " AS SELECT " +
+ DataColumns.CONCRETE_ID
+ + " AS " + ContactMethods._ID + ", " +
+ DataColumns.CONCRETE_RAW_CONTACT_ID
+ + " AS " + ContactMethods.PERSON_ID + ", " +
+ CONTACT_METHOD_KIND_SQL
+ + " AS " + ContactMethods.KIND + ", " +
+ DataColumns.CONCRETE_IS_PRIMARY
+ + " AS " + ContactMethods.ISPRIMARY + ", " +
+ DataColumns.CONCRETE_DATA1
+ + " AS " + ContactMethods.TYPE + ", " +
+ DataColumns.CONCRETE_DATA2
+ + " AS " + ContactMethods.DATA + ", " +
+ DataColumns.CONCRETE_DATA3
+ + " AS " + ContactMethods.LABEL + ", " +
+ DataColumns.CONCRETE_DATA14
+ + " AS " + ContactMethods.AUX_DATA + ", " +
+ "name." + StructuredName.DISPLAY_NAME
+ + " AS " + ContactMethods.NAME + ", " +
+ Tables.RAW_CONTACTS + "." + RawContactsColumns.DISPLAY_NAME
+ + " AS " + ContactMethods.DISPLAY_NAME + ", " +
+ PHONETIC_NAME_SQL
+ + " AS " + ContactMethods.PHONETIC_NAME + " , " +
+ "note." + Note.NOTE
+ + " AS " + ContactMethods.NOTES + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.TIMES_CONTACTED
+ + " AS " + ContactMethods.TIMES_CONTACTED + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.LAST_TIME_CONTACTED
+ + " AS " + ContactMethods.LAST_TIME_CONTACTED + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.CUSTOM_RINGTONE
+ + " AS " + ContactMethods.CUSTOM_RINGTONE + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.SEND_TO_VOICEMAIL
+ + " AS " + ContactMethods.SEND_TO_VOICEMAIL + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.STARRED
+ + " AS " + ContactMethods.STARRED + ", " +
+ RawContacts.IS_RESTRICTED +
+ " FROM " + Tables.DATA + DATA_JOINS +
+ " WHERE " + ContactMethods.KIND + " IS NOT NULL"
+ + " AND " + Tables.RAW_CONTACTS + "." + RawContacts.DELETED + "=0" +
+ ";");
+
+
+ db.execSQL("DROP VIEW IF EXISTS " + LegacyTables.PHONES + ";");
+ db.execSQL("CREATE VIEW " + LegacyTables.PHONES + " AS SELECT " +
+ DataColumns.CONCRETE_ID
+ + " AS " + android.provider.Contacts.Phones._ID + ", " +
+ DataColumns.CONCRETE_RAW_CONTACT_ID
+ + " AS " + android.provider.Contacts.Phones.PERSON_ID + ", " +
+ DataColumns.CONCRETE_IS_PRIMARY
+ + " AS " + android.provider.Contacts.Phones.ISPRIMARY + ", " +
+ Tables.DATA + "." + Phone.NUMBER
+ + " AS " + android.provider.Contacts.Phones.NUMBER + ", " +
+ Tables.DATA + "." + Phone.TYPE
+ + " AS " + android.provider.Contacts.Phones.TYPE + ", " +
+ Tables.DATA + "." + Phone.LABEL
+ + " AS " + android.provider.Contacts.Phones.LABEL + ", " +
+ PhoneColumns.CONCRETE_NORMALIZED_NUMBER
+ + " AS " + android.provider.Contacts.Phones.NUMBER_KEY + ", " +
+ "name." + StructuredName.DISPLAY_NAME
+ + " AS " + android.provider.Contacts.Phones.NAME + ", " +
+ Tables.RAW_CONTACTS + "." + RawContactsColumns.DISPLAY_NAME
+ + " AS " + android.provider.Contacts.Phones.DISPLAY_NAME + ", " +
+ PHONETIC_NAME_SQL
+ + " AS " + android.provider.Contacts.Phones.PHONETIC_NAME + " , " +
+ "note." + Note.NOTE
+ + " AS " + android.provider.Contacts.Phones.NOTES + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.TIMES_CONTACTED
+ + " AS " + android.provider.Contacts.Phones.TIMES_CONTACTED + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.LAST_TIME_CONTACTED
+ + " AS " + android.provider.Contacts.Phones.LAST_TIME_CONTACTED + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.CUSTOM_RINGTONE
+ + " AS " + android.provider.Contacts.Phones.CUSTOM_RINGTONE + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.SEND_TO_VOICEMAIL
+ + " AS " + android.provider.Contacts.Phones.SEND_TO_VOICEMAIL + ", " +
+ Tables.RAW_CONTACTS + "." + RawContacts.STARRED
+ + " AS " + android.provider.Contacts.Phones.STARRED + ", " +
+ RawContacts.IS_RESTRICTED +
+ " FROM " + Tables.DATA + DATA_JOINS +
+ " WHERE " + MimetypesColumns.CONCRETE_MIMETYPE + "='"
+ + Phone.CONTENT_ITEM_TYPE + "'"
+ + " AND " + Tables.RAW_CONTACTS + "." + RawContacts.DELETED + "=0" +
+ ";");
+
+ db.execSQL("DROP VIEW IF EXISTS " + LegacyTables.EXTENSIONS + ";");
+ db.execSQL("CREATE VIEW " + LegacyTables.EXTENSIONS + " AS SELECT " +
+ DataColumns.CONCRETE_ID
+ + " AS " + android.provider.Contacts.Extensions._ID + ", " +
+ DataColumns.CONCRETE_RAW_CONTACT_ID
+ + " AS " + android.provider.Contacts.Extensions.PERSON_ID + ", " +
+ ExtensionsColumns.NAME
+ + " AS " + android.provider.Contacts.Extensions.NAME + ", " +
+ ExtensionsColumns.VALUE
+ + " AS " + android.provider.Contacts.Extensions.VALUE + ", " +
+ RawContacts.IS_RESTRICTED +
+ " FROM " + Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS +
+ " WHERE " + MimetypesColumns.CONCRETE_MIMETYPE + "='"
+ + android.provider.Contacts.Extensions.CONTENT_ITEM_TYPE + "'"
+ + " AND " + Tables.RAW_CONTACTS + "." + RawContacts.DELETED + "=0" +
+ ";");
+
+ db.execSQL("DROP VIEW IF EXISTS " + LegacyTables.GROUPS + ";");
+ db.execSQL("CREATE VIEW " + LegacyTables.GROUPS + " AS SELECT " +
+ GroupsColumns.CONCRETE_ID + " AS " + android.provider.Contacts.Groups._ID + ", " +
+ Groups.TITLE + " AS " + android.provider.Contacts.Groups.NAME + ", " +
+ Groups.NOTES + " AS " + android.provider.Contacts.Groups.NOTES + " , " +
+ Groups.SYSTEM_ID + " AS " + android.provider.Contacts.Groups.SYSTEM_ID +
+ " FROM " + Tables.GROUPS +
+ ";");
+
+ db.execSQL("DROP VIEW IF EXISTS " + LegacyTables.GROUP_MEMBERSHIP + ";");
+ db.execSQL("CREATE VIEW " + LegacyTables.GROUP_MEMBERSHIP + " AS SELECT " +
+ DataColumns.CONCRETE_ID
+ + " AS " + android.provider.Contacts.GroupMembership._ID + ", " +
+ DataColumns.CONCRETE_RAW_CONTACT_ID
+ + " AS " + android.provider.Contacts.GroupMembership.PERSON_ID + ", " +
+ GroupMembership.GROUP_ROW_ID
+ + " AS " + android.provider.Contacts.GroupMembership.GROUP_ID + ", " +
+ Groups.TITLE
+ + " AS " + android.provider.Contacts.GroupMembership.NAME + ", " +
+ Groups.NOTES
+ + " AS " + android.provider.Contacts.GroupMembership.NOTES + " , " +
+ Groups.SYSTEM_ID
+ + " AS " + android.provider.Contacts.GroupMembership.SYSTEM_ID + ", " +
+ RawContacts.IS_RESTRICTED +
+ " FROM " + Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS +
+ " WHERE " + MimetypesColumns.CONCRETE_MIMETYPE + "='"
+ + GroupMembership.CONTENT_ITEM_TYPE + "'"
+ + " AND " + Tables.RAW_CONTACTS + "." + RawContacts.DELETED + "=0" +
+ ";");
+
+ db.execSQL("DROP VIEW IF EXISTS " + LegacyTables.PHOTOS + ";");
+ db.execSQL("CREATE VIEW " + LegacyTables.PHOTOS + " AS SELECT " +
+ DataColumns.CONCRETE_ID
+ + " AS " + android.provider.Contacts.Photos._ID + ", " +
+ DataColumns.CONCRETE_RAW_CONTACT_ID
+ + " AS " + android.provider.Contacts.Photos.PERSON_ID + ", " +
+ Tables.DATA + "." + Photo.PHOTO
+ + " AS " + android.provider.Contacts.Photos.DATA + ", " +
+ "legacy_photo." + LegacyPhotoData.EXISTS_ON_SERVER
+ + " AS " + android.provider.Contacts.Photos.EXISTS_ON_SERVER + ", " +
+ "legacy_photo." + LegacyPhotoData.DOWNLOAD_REQUIRED
+ + " AS " + android.provider.Contacts.Photos.DOWNLOAD_REQUIRED + ", " +
+ "legacy_photo." + LegacyPhotoData.LOCAL_VERSION
+ + " AS " + android.provider.Contacts.Photos.LOCAL_VERSION + ", " +
+ "legacy_photo." + LegacyPhotoData.SYNC_ERROR
+ + " AS " + android.provider.Contacts.Photos.SYNC_ERROR + ", " +
+ RawContacts.IS_RESTRICTED +
+ " FROM " + Tables.DATA + DATA_JOINS + LEGACY_PHOTO_JOIN +
+ " WHERE " + MimetypesColumns.CONCRETE_MIMETYPE + "='"
+ + Photo.CONTENT_ITEM_TYPE + "'"
+ + " AND " + Tables.RAW_CONTACTS + "." + RawContacts.DELETED + "=0" +
+ ";");
+ }
+
+ public Uri insert(Uri uri, ContentValues values) {
+ final int match = sUriMatcher.match(uri);
+ long id = 0;
+ switch (match) {
+ case PEOPLE:
+ id = insertPeople(values);
+ break;
+
+ case ORGANIZATIONS:
+ id = insertOrganization(values);
+ break;
+
+ case PEOPLE_CONTACTMETHODS: {
+ long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
+ id = insertContactMethod(rawContactId, values);
+ break;
+ }
+
+ case CONTACTMETHODS: {
+ long rawContactId = getRequiredValue(values, ContactMethods.PERSON_ID);
+ id = insertContactMethod(rawContactId, values);
+ break;
+ }
+
+ case PHONES: {
+ long rawContactId = getRequiredValue(values,
+ android.provider.Contacts.Phones.PERSON_ID);
+ id = insertPhone(rawContactId, values);
+ break;
+ }
+
+ case EXTENSIONS: {
+ long rawContactId = getRequiredValue(values,
+ android.provider.Contacts.Extensions.PERSON_ID);
+ id = insertExtension(rawContactId, values);
+ break;
+ }
+
+ case GROUPS:
+ id = insertGroup(values);
+ break;
+
+ case GROUPMEMBERSHIP: {
+ long rawContactId = getRequiredValue(values,
+ android.provider.Contacts.GroupMembership.PERSON_ID);
+ long groupId = getRequiredValue(values,
+ android.provider.Contacts.GroupMembership.GROUP_ID);
+ id = insertGroupMembership(rawContactId, groupId);
+ break;
+ }
+
+ case PRESENCE: {
+ id = insertPresence(values);
+ break;
+ }
+
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+
+ if (id < 0) {
+ return null;
+ }
+
+ final Uri result = ContentUris.withAppendedId(uri, id);
+ onChange(result);
+ return result;
+ }
+
+ private long getRequiredValue(ContentValues values, String column) {
+ if (!values.containsKey(column)) {
+ throw new RuntimeException("Required value: " + column);
+ }
+
+ return values.getAsLong(column);
+ }
+
+ private long insertPeople(ContentValues values) {
+ mValues.clear();
+
+ OpenHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
+ values, People.CUSTOM_RINGTONE);
+ OpenHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
+ values, People.SEND_TO_VOICEMAIL);
+ OpenHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
+ values, People.LAST_TIME_CONTACTED);
+ OpenHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
+ values, People.TIMES_CONTACTED);
+ OpenHelper.copyLongValue(mValues, RawContacts.STARRED,
+ values, People.STARRED);
+ Uri contactUri = mContactsProvider.insert(RawContacts.CONTENT_URI, mValues);
+ long rawContactId = ContentUris.parseId(contactUri);
+
+ if (values.containsKey(People.NAME) || values.containsKey(People.PHONETIC_NAME)) {
+ mValues.clear();
+ mValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
+ mValues.put(ContactsContract.Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ OpenHelper.copyStringValue(mValues, StructuredName.DISPLAY_NAME,
+ values, People.NAME);
+ if (values.containsKey(People.PHONETIC_NAME)) {
+ String phoneticName = values.getAsString(People.PHONETIC_NAME);
+ NameSplitter.Name parsedName = new NameSplitter.Name();
+ mPhoneticNameSplitter.split(parsedName, phoneticName);
+ mValues.put(StructuredName.PHONETIC_GIVEN_NAME, parsedName.getGivenNames());
+ mValues.put(StructuredName.PHONETIC_MIDDLE_NAME, parsedName.getMiddleName());
+ mValues.put(StructuredName.PHONETIC_FAMILY_NAME, parsedName.getFamilyName());
+ }
+
+ mContactsProvider.insert(ContactsContract.Data.CONTENT_URI, mValues);
+ }
+
+ if (values.containsKey(People.NOTES)) {
+ mValues.clear();
+ mValues.put(Data.RAW_CONTACT_ID, rawContactId);
+ mValues.put(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
+ OpenHelper.copyStringValue(mValues, Note.NOTE, values, People.NOTES);
+ mContactsProvider.insert(Data.CONTENT_URI, mValues);
+ }
+
+ // TODO instant aggregation
+ return rawContactId;
+ }
+
+ private long insertOrganization(ContentValues values) {
+ mValues.clear();
+
+ OpenHelper.copyLongValue(mValues, Data.RAW_CONTACT_ID,
+ values, android.provider.Contacts.Organizations.PERSON_ID);
+ mValues.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+
+ OpenHelper.copyLongValue(mValues, Data.IS_PRIMARY,
+ values, android.provider.Contacts.Organizations.ISPRIMARY);
+
+ OpenHelper.copyStringValue(mValues, Organization.COMPANY,
+ values, android.provider.Contacts.Organizations.COMPANY);
+
+ // TYPE values happen to remain the same between V1 and V2 - can just copy the value
+ OpenHelper.copyLongValue(mValues, Organization.TYPE,
+ values, android.provider.Contacts.Organizations.TYPE);
+
+ OpenHelper.copyStringValue(mValues, Organization.LABEL,
+ values, android.provider.Contacts.Organizations.LABEL);
+ OpenHelper.copyStringValue(mValues, Organization.TITLE,
+ values, android.provider.Contacts.Organizations.TITLE);
+
+ Uri uri = mContactsProvider.insert(Data.CONTENT_URI, mValues);
+
+ return ContentUris.parseId(uri);
+ }
+
+ private long insertPhone(long rawContactId, ContentValues values) {
+ mValues.clear();
+
+ mValues.put(Data.RAW_CONTACT_ID, rawContactId);
+ mValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+
+ OpenHelper.copyLongValue(mValues, Data.IS_PRIMARY,
+ values, android.provider.Contacts.Phones.ISPRIMARY);
+
+ OpenHelper.copyStringValue(mValues, Phone.NUMBER,
+ values, android.provider.Contacts.Phones.NUMBER);
+
+ // TYPE values happen to remain the same between V1 and V2 - can just copy the value
+ OpenHelper.copyLongValue(mValues, Phone.TYPE,
+ values, android.provider.Contacts.Phones.TYPE);
+
+ OpenHelper.copyStringValue(mValues, Phone.LABEL,
+ values, android.provider.Contacts.Phones.LABEL);
+
+ Uri uri = mContactsProvider.insert(Data.CONTENT_URI, mValues);
+
+ return ContentUris.parseId(uri);
+ }
+
+ private long insertContactMethod(long rawContactId, ContentValues values) {
+ Integer kind = values.getAsInteger(ContactMethods.KIND);
+ if (kind == null) {
+ throw new RuntimeException("Required value: " + ContactMethods.KIND);
+ }
+
+ mValues.clear();
+ mValues.put(Data.RAW_CONTACT_ID, rawContactId);
+
+ OpenHelper.copyLongValue(mValues, Data.IS_PRIMARY, values, ContactMethods.ISPRIMARY);
+
+ switch (kind) {
+ case android.provider.Contacts.KIND_EMAIL:
+ copyCommonFields(values, Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL,
+ Email.DATA, Data.DATA14);
+ break;
+
+ case android.provider.Contacts.KIND_IM:
+ copyCommonFields(values, Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL,
+ Email.DATA, Data.DATA14);
+ break;
+
+ case android.provider.Contacts.KIND_POSTAL:
+ copyCommonFields(values, StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE,
+ StructuredPostal.LABEL, StructuredPostal.FORMATTED_ADDRESS, Data.DATA14);
+ break;
+ }
+
+ Uri uri = mContactsProvider.insert(Data.CONTENT_URI, mValues);
+ return ContentUris.parseId(uri);
+ }
+
+ private void copyCommonFields(ContentValues values, String mimeType, String typeColumn,
+ String labelColumn, String dataColumn, String auxDataColumn) {
+ mValues.put(Data.MIMETYPE, mimeType);
+ OpenHelper.copyLongValue(mValues, typeColumn, values, ContactMethods.TYPE);
+ OpenHelper.copyStringValue(mValues, labelColumn, values, ContactMethods.LABEL);
+ OpenHelper.copyStringValue(mValues, dataColumn, values, ContactMethods.DATA);
+ OpenHelper.copyStringValue(mValues, auxDataColumn, values, ContactMethods.AUX_DATA);
+ }
+
+ private long insertExtension(long rawContactId, ContentValues values) {
+ mValues.clear();
+
+ mValues.put(Data.RAW_CONTACT_ID, rawContactId);
+ mValues.put(Data.MIMETYPE, android.provider.Contacts.Extensions.CONTENT_ITEM_TYPE);
+
+ OpenHelper.copyStringValue(mValues, ExtensionsColumns.NAME,
+ values, android.provider.Contacts.People.Extensions.NAME);
+ OpenHelper.copyStringValue(mValues, ExtensionsColumns.VALUE,
+ values, android.provider.Contacts.People.Extensions.VALUE);
+
+ Uri uri = mContactsProvider.insert(Data.CONTENT_URI, mValues);
+ return ContentUris.parseId(uri);
+ }
+
+ private long insertGroup(ContentValues values) {
+ mValues.clear();
+
+ OpenHelper.copyStringValue(mValues, Groups.TITLE,
+ values, android.provider.Contacts.Groups.NAME);
+ OpenHelper.copyStringValue(mValues, Groups.NOTES,
+ values, android.provider.Contacts.Groups.NOTES);
+ OpenHelper.copyStringValue(mValues, Groups.SYSTEM_ID,
+ values, android.provider.Contacts.Groups.SYSTEM_ID);
+
+ Uri uri = mContactsProvider.insert(Groups.CONTENT_URI, mValues);
+ return ContentUris.parseId(uri);
+ }
+
+ private long insertGroupMembership(long rawContactId, long groupId) {
+ mValues.clear();
+
+ mValues.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
+ mValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId);
+ mValues.put(GroupMembership.GROUP_ROW_ID, groupId);
+
+ Uri uri = mContactsProvider.insert(Data.CONTENT_URI, mValues);
+ return ContentUris.parseId(uri);
+ }
+
+ private long insertPresence(ContentValues values) {
+ mValues.clear();
+
+ OpenHelper.copyLongValue(mValues, Presence._ID,
+ values, android.provider.Contacts.Presence._ID);
+ OpenHelper.copyLongValue(mValues, Presence.RAW_CONTACT_ID,
+ values, android.provider.Contacts.Presence.PERSON_ID);
+ OpenHelper.copyStringValue(mValues, Presence.IM_PROTOCOL,
+ values, android.provider.Contacts.Presence.IM_PROTOCOL);
+ OpenHelper.copyStringValue(mValues, Presence.IM_HANDLE,
+ values, android.provider.Contacts.Presence.IM_HANDLE);
+ OpenHelper.copyStringValue(mValues, Presence.IM_ACCOUNT,
+ values, android.provider.Contacts.Presence.IM_ACCOUNT);
+ OpenHelper.copyLongValue(mValues, Presence.PRESENCE_STATUS,
+ values, android.provider.Contacts.Presence.PRESENCE_STATUS);
+ OpenHelper.copyStringValue(mValues, Presence.PRESENCE_CUSTOM_STATUS,
+ values, android.provider.Contacts.Presence.PRESENCE_CUSTOM_STATUS);
+
+ return mContactsProvider.insertPresence(mValues);
+ }
+
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ final int match = sUriMatcher.match(uri);
+ int count = 0;
+ switch(match) {
+ case PEOPLE_UPDATE_CONTACT_TIME:
+ count = updateContactTime(uri, values);
+ break;
+
+ case PEOPLE_PHOTO: {
+ long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
+ return updatePhoto(rawContactId, values);
+ }
+
+ case PHOTOS:
+ // TODO
+ break;
+
+ case PHOTOS_ID:
+ // TODO
+ break;
+
+
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+
+ if (count > 0) {
+ mContext.getContentResolver().notifyChange(uri, null);
+ }
+ return count;
+ }
+
+
+ private int updateContactTime(Uri uri, ContentValues values) {
+
+ // TODO check sanctions
+
+ long lastTimeContacted;
+ if (values.containsKey(People.LAST_TIME_CONTACTED)) {
+ lastTimeContacted = values.getAsLong(People.LAST_TIME_CONTACTED);
+ } else {
+ lastTimeContacted = System.currentTimeMillis();
+ }
+
+ long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
+ long contactId = mOpenHelper.getContactId(rawContactId);
+ if (contactId != 0) {
+ mContactsProvider.updateContactTime(contactId, lastTimeContacted);
+ } else {
+ mLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
+ mLastTimeContactedUpdate.bindLong(2, rawContactId);
+ mLastTimeContactedUpdate.execute();
+ }
+ return 1;
+ }
+
+ private int updatePhoto(long rawContactId, ContentValues values) {
+
+ // TODO check sanctions
+
+ int count;
+
+ long dataId = -1;
+ Cursor c = mContactsProvider.query(Data.CONTENT_URI, PhotoQuery.COLUMNS,
+ Data.RAW_CONTACT_ID + "=" + rawContactId + " AND "
+ + Data.MIMETYPE + "=" + mOpenHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE),
+ null, null);
+ try {
+ if (c.moveToFirst()) {
+ dataId = c.getLong(PhotoQuery._ID);
+ }
+ } finally {
+ c.close();
+ }
+
+ mValues.clear();
+ byte[] bytes = values.getAsByteArray(android.provider.Contacts.Photos.DATA);
+ mValues.put(Photo.PHOTO, bytes);
+
+ if (dataId == -1) {
+ mValues.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+ mValues.put(Data.RAW_CONTACT_ID, rawContactId);
+ Uri dataUri = mContactsProvider.insert(Data.CONTENT_URI, mValues);
+ dataId = ContentUris.parseId(dataUri);
+ count = 1;
+ } else {
+ Uri dataUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
+ count = mContactsProvider.update(dataUri, mValues, null, null);
+ }
+
+ mValues.clear();
+ OpenHelper.copyStringValue(mValues, LegacyPhotoData.LOCAL_VERSION,
+ values, android.provider.Contacts.Photos.LOCAL_VERSION);
+ OpenHelper.copyStringValue(mValues, LegacyPhotoData.DOWNLOAD_REQUIRED,
+ values, android.provider.Contacts.Photos.DOWNLOAD_REQUIRED);
+ OpenHelper.copyStringValue(mValues, LegacyPhotoData.EXISTS_ON_SERVER,
+ values, android.provider.Contacts.Photos.EXISTS_ON_SERVER);
+ OpenHelper.copyStringValue(mValues, LegacyPhotoData.SYNC_ERROR,
+ values, android.provider.Contacts.Photos.SYNC_ERROR);
+
+ int updated = mContactsProvider.update(Data.CONTENT_URI, mValues,
+ Data.MIMETYPE + "='" + LegacyPhotoData.CONTENT_ITEM_TYPE + "'"
+ + " AND " + Data.RAW_CONTACT_ID + "=" + rawContactId
+ + " AND " + LegacyPhotoData.PHOTO_DATA_ID + "=" + dataId, null);
+ if (updated == 0) {
+ mValues.put(Data.RAW_CONTACT_ID, rawContactId);
+ mValues.put(Data.MIMETYPE, LegacyPhotoData.CONTENT_ITEM_TYPE);
+ mValues.put(LegacyPhotoData.PHOTO_DATA_ID, dataId);
+ mContactsProvider.insert(Data.CONTENT_URI, mValues);
+ }
+
+ return count;
+ }
+
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ final int match = sUriMatcher.match(uri);
+ int count = 0;
+ switch (match) {
+ case PEOPLE_ID:
+ count = mContactsProvider.deleteRawContact(ContentUris.parseId(uri), false);
+ break;
+
+ case ORGANIZATIONS_ID:
+ count = mContactsProvider.deleteData(ContentUris.parseId(uri),
+ ORGANIZATION_MIME_TYPES);
+ break;
+
+ case CONTACTMETHODS_ID:
+ count = mContactsProvider.deleteData(ContentUris.parseId(uri),
+ CONTACT_METHOD_MIME_TYPES);
+ break;
+
+ case PHONES_ID:
+ count = mContactsProvider.deleteData(ContentUris.parseId(uri),
+ PHONE_MIME_TYPES);
+ break;
+
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+
+ return count;
+ }
+
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder, String limit) {
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ String groupBy = null;
+
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case PEOPLE: {
+ qb.setTables(LegacyTables.PEOPLE_JOIN_PRESENCE);
+ qb.setProjectionMap(sPeopleProjectionMap);
+ break;
+ }
+
+ case PEOPLE_ID:
+ qb.setTables(LegacyTables.PEOPLE_JOIN_PRESENCE);
+ qb.setProjectionMap(sPeopleProjectionMap);
+ qb.appendWhere(People._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case PEOPLE_FILTER: {
+ qb.setTables(LegacyTables.PEOPLE_JOIN_PRESENCE);
+ qb.setProjectionMap(sPeopleProjectionMap);
+ String filterParam = uri.getPathSegments().get(2);
+ qb.appendWhere(People._ID + " IN "
+ + mContactsProvider.getRawContactsByFilterAsNestedQuery(filterParam));
+ break;
+ }
+
+ case ORGANIZATIONS:
+ qb.setTables(LegacyTables.ORGANIZATIONS);
+ qb.setProjectionMap(sOrganizationProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ break;
+
+ case ORGANIZATIONS_ID:
+ qb.setTables(LegacyTables.ORGANIZATIONS);
+ qb.setProjectionMap(sOrganizationProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.Organizations._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case CONTACTMETHODS:
+ qb.setTables(LegacyTables.CONTACT_METHODS);
+ qb.setProjectionMap(sContactMethodProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ break;
+
+ case CONTACTMETHODS_ID:
+ qb.setTables(LegacyTables.CONTACT_METHODS);
+ qb.setProjectionMap(sContactMethodProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + ContactMethods._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case PEOPLE_CONTACTMETHODS:
+ qb.setTables(LegacyTables.CONTACT_METHODS);
+ qb.setProjectionMap(sContactMethodProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + ContactMethods.PERSON_ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ qb.appendWhere(" AND " + ContactMethods.KIND + " IS NOT NULL");
+ break;
+
+ case PEOPLE_CONTACTMETHODS_ID:
+ qb.setTables(LegacyTables.CONTACT_METHODS);
+ qb.setProjectionMap(sContactMethodProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + ContactMethods.PERSON_ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ qb.appendWhere(" AND " + ContactMethods._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(3));
+ qb.appendWhere(" AND " + ContactMethods.KIND + " IS NOT NULL");
+ break;
+
+ case PHONES:
+ qb.setTables(LegacyTables.PHONES);
+ qb.setProjectionMap(sPhoneProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ break;
+
+ case PHONES_ID:
+ qb.setTables(LegacyTables.PHONES);
+ qb.setProjectionMap(sPhoneProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.Phones._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case PEOPLE_PHONES:
+ qb.setTables(LegacyTables.PHONES);
+ qb.setProjectionMap(sPhoneProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.Phones.PERSON_ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case PEOPLE_PHONES_ID:
+ qb.setTables(LegacyTables.PHONES);
+ qb.setProjectionMap(sPhoneProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.Phones.PERSON_ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ qb.appendWhere(" AND " + android.provider.Contacts.Phones._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(3));
+ break;
+
+ case EXTENSIONS:
+ qb.setTables(LegacyTables.EXTENSIONS);
+ qb.setProjectionMap(sExtensionProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ break;
+
+ case EXTENSIONS_ID:
+ qb.setTables(LegacyTables.EXTENSIONS);
+ qb.setProjectionMap(sExtensionProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.Extensions._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case PEOPLE_EXTENSIONS:
+ qb.setTables(LegacyTables.EXTENSIONS);
+ qb.setProjectionMap(sExtensionProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.Extensions.PERSON_ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case PEOPLE_EXTENSIONS_ID:
+ qb.setTables(LegacyTables.EXTENSIONS);
+ qb.setProjectionMap(sExtensionProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.Extensions.PERSON_ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ qb.appendWhere(" AND " + android.provider.Contacts.Extensions._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(3));
+ break;
+
+ case GROUPS:
+ qb.setTables(LegacyTables.GROUPS);
+ qb.setProjectionMap(sGroupProjectionMap);
+ break;
+
+ case GROUPS_ID:
+ qb.setTables(LegacyTables.GROUPS);
+ qb.setProjectionMap(sGroupProjectionMap);
+ qb.appendWhere(android.provider.Contacts.Groups._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case GROUPMEMBERSHIP:
+ qb.setTables(LegacyTables.GROUP_MEMBERSHIP);
+ qb.setProjectionMap(sGroupMembershipProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ break;
+
+ case GROUPMEMBERSHIP_ID:
+ qb.setTables(LegacyTables.GROUP_MEMBERSHIP);
+ qb.setProjectionMap(sGroupMembershipProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.GroupMembership._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case PEOPLE_GROUPMEMBERSHIP:
+ qb.setTables(LegacyTables.GROUP_MEMBERSHIP);
+ qb.setProjectionMap(sGroupMembershipProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.GroupMembership.PERSON_ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case PEOPLE_GROUPMEMBERSHIP_ID:
+ qb.setTables(LegacyTables.GROUP_MEMBERSHIP);
+ qb.setProjectionMap(sGroupMembershipProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.GroupMembership.PERSON_ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ qb.appendWhere(" AND " + android.provider.Contacts.GroupMembership._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(3));
+ break;
+
+ case PEOPLE_PHOTO:
+ qb.setTables(LegacyTables.PHOTOS);
+ qb.setProjectionMap(sPhotoProjectionMap);
+ mContactsProvider.applyDataRestrictionExceptions(qb);
+ qb.appendWhere(" AND " + android.provider.Contacts.Photos.PERSON_ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ limit = "1";
+ break;
+
+ case PRESENCE:
+ qb.setTables(Tables.PRESENCE);
+ qb.setProjectionMap(sPresenceProjectionMap);
+ qb.appendWhere(mContactsProvider.getContactsRestrictionExceptionAsNestedQuery(
+ android.provider.Contacts.Presence.PERSON_ID));
+ break;
+
+ case PRESENCE_ID:
+ qb.setTables(Tables.PRESENCE);
+ qb.setProjectionMap(sPresenceProjectionMap);
+ qb.appendWhere(mContactsProvider.getContactsRestrictionExceptionAsNestedQuery(
+ android.provider.Contacts.Presence.PERSON_ID));
+ qb.appendWhere(" AND " + android.provider.Contacts.Presence._ID + "=");
+ qb.appendWhere(uri.getPathSegments().get(1));
+ break;
+
+ case SEARCH_SUGGESTIONS:
+
+ // No legacy compatibility for search suggestions
+ return mContactsProvider.handleSearchSuggestionsQuery(uri, limit);
+
+ case DELETED_PEOPLE:
+ case DELETED_GROUPS:
+ throw new UnsupportedOperationException();
+
+ default:
+ throw new IllegalArgumentException("Unknown URL " + uri);
+ }
+
+ // Perform the query and set the notification uri
+ final Cursor c = qb.query(db, projection, selection, selectionArgs,
+ groupBy, null, sortOrder, limit);
+ if (c != null) {
+ c.setNotificationUri(mContext.getContentResolver(), RawContacts.CONTENT_URI);
+ }
+ DatabaseUtils.dumpCursor(c);
+ return c;
+ }
+
+ /**
+ * Called when a change has been made.
+ *
+ * @param uri the uri that the change was made to
+ */
+ private void onChange(Uri uri) {
+ mContext.getContentResolver().notifyChange(android.provider.Contacts.CONTENT_URI, null);
+ }
+}
diff --git a/src/com/android/providers/contacts/NameNormalizer.java b/src/com/android/providers/contacts/NameNormalizer.java
new file mode 100644
index 0000000..eca40fc
--- /dev/null
+++ b/src/com/android/providers/contacts/NameNormalizer.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import com.ibm.icu4jni.text.CollationAttribute;
+import com.ibm.icu4jni.text.Collator;
+import com.ibm.icu4jni.text.RuleBasedCollator;
+
+/**
+ * Converts a name to a normalized form by removing all non-letter characters and normalizing
+ * UNICODE according to http://unicode.org/unicode/reports/tr15
+ */
+public class NameNormalizer {
+
+ private static final RuleBasedCollator sCompressingCollator;
+ static {
+ sCompressingCollator = (RuleBasedCollator)Collator.getInstance(null);
+ sCompressingCollator.setStrength(Collator.PRIMARY);
+ sCompressingCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
+ }
+
+ private static final RuleBasedCollator sComplexityCollator;
+ static {
+ sComplexityCollator = (RuleBasedCollator)Collator.getInstance(null);
+ sComplexityCollator.setStrength(Collator.TERTIARY);
+ sComplexityCollator.setAttribute(CollationAttribute.CASE_FIRST,
+ CollationAttribute.VALUE_LOWER_FIRST);
+ }
+
+ /**
+ * Converts the supplied name to a string that can be used to perform approximate matching
+ * of names. It ignores non-letter characters and removes accents.
+ */
+ public static String normalize(String name) {
+ return Hex.encodeHex(sCompressingCollator.getSortKey(lettersOnly(name)), true);
+ }
+
+ /**
+ * Compares "complexity" of two names, which is determined by the presence
+ * of mixed case characters, accents and, if all else is equal, length.
+ */
+ public static int compareComplexity(String name1, String name2) {
+ int diff = sComplexityCollator.compare(lettersOnly(name1), lettersOnly(name2));
+ if (diff != 0) {
+ return diff;
+ }
+
+ return name1.length() - name2.length();
+ }
+
+ /**
+ * Returns a string containing just the letters from the original string.
+ */
+ private static String lettersOnly(String name) {
+ char[] letters = name.toCharArray();
+ int length = 0;
+ for (int i = 0; i < letters.length; i++) {
+ final char c = letters[i];
+ if (Character.isLetter(c)) {
+ letters[length++] = c;
+ }
+ }
+
+ if (length != letters.length) {
+ return new String(letters, 0, length);
+ }
+
+ return name;
+ }
+}
diff --git a/src/com/android/providers/contacts/NameSplitter.java b/src/com/android/providers/contacts/NameSplitter.java
new file mode 100644
index 0000000..aad3bc5
--- /dev/null
+++ b/src/com/android/providers/contacts/NameSplitter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import java.util.HashSet;
+import java.util.StringTokenizer;
+
+/**
+ * The purpose of this class is to split a full name into given names and last
+ * name. The logic only supports having a single last name. If the full name has
+ * multiple last names the output will be incorrect.
+ * <p>
+ * Core algorithm:
+ * <ol>
+ * <li>Remove the suffixes (III, Ph.D., M.D.).</li>
+ * <li>Remove the prefixes (Mr., Pastor, Reverend, Sir).</li>
+ * <li>Assign the last remaining token as the last name.</li>
+ * <li>If the previous word to the last name is one from LASTNAME_PREFIXES, use
+ * this word also as the last name.</li>
+ * <li>Assign the rest of the words as the "given names".</li>
+ * </ol>
+ */
+public class NameSplitter {
+
+ private final HashSet<String> mPrefixesSet;
+ private final HashSet<String> mSuffixesSet;
+ private final int mMaxSuffixLength;
+ private final HashSet<String> mLastNamePrefixesSet;
+ private final HashSet<String> mConjuctions;
+
+ public static class Name {
+ private String prefix;
+ private String givenNames;
+ private String middleName;
+ private String familyName;
+ private String suffix;
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public String getGivenNames() {
+ return givenNames;
+ }
+
+ public String getMiddleName() {
+ return middleName;
+ }
+
+ public String getFamilyName() {
+ return familyName;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+ }
+
+ private static class NameTokenizer extends StringTokenizer {
+ private static final int MAX_TOKENS = 10;
+ private final String[] mTokens;
+ private int mDotBitmask;
+ private int mStartPointer;
+ private int mEndPointer;
+
+ public NameTokenizer(String fullName) {
+ super(fullName, " .,", true);
+
+ mTokens = new String[MAX_TOKENS];
+
+ // Iterate over tokens, skipping over empty ones and marking tokens that
+ // are followed by dots.
+ while (hasMoreTokens() && mEndPointer < MAX_TOKENS) {
+ final String token = nextToken();
+ if (token.length() > 0) {
+ final char c = token.charAt(0);
+ if (c == ' ' || c == ',') {
+ continue;
+ }
+ }
+
+ if (mEndPointer > 0 && token.charAt(0) == '.') {
+ mDotBitmask |= (1 << (mEndPointer - 1));
+ } else {
+ mTokens[mEndPointer] = token;
+ mEndPointer++;
+ }
+ }
+ }
+
+ /**
+ * Returns true if the token is followed by a dot in the original full name.
+ */
+ public boolean hasDot(int index) {
+ return (mDotBitmask & (1 << index)) != 0;
+ }
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param commonPrefixes comma-separated list of common prefixes,
+ * e.g. "Mr, Ms, Mrs"
+ * @param commonLastNamePrefixes comma-separated list of common last name prefixes,
+ * e.g. "d', st, st., von"
+ * @param commonSuffixes comma-separated list of common suffixes,
+ * e.g. "Jr, M.D., MD, D.D.S."
+ * @param commonConjunctions comma-separated list of common conjuctions,
+ * e.g. "AND, Or"
+ */
+ public NameSplitter(String commonPrefixes, String commonLastNamePrefixes,
+ String commonSuffixes, String commonConjunctions) {
+ mPrefixesSet = convertToSet(commonPrefixes);
+ mLastNamePrefixesSet = convertToSet(commonLastNamePrefixes);
+ mSuffixesSet = convertToSet(commonSuffixes);
+ mConjuctions = convertToSet(commonConjunctions);
+
+ int maxLength = 0;
+ for (String suffix : mSuffixesSet) {
+ if (suffix.length() > maxLength) {
+ maxLength = suffix.length();
+ }
+ }
+
+ mMaxSuffixLength = maxLength;
+ }
+
+ /**
+ * Converts a comma-separated list of Strings to a set of Strings. Trims strings
+ * and converts them to upper case.
+ */
+ private static HashSet<String> convertToSet(String strings) {
+ HashSet<String> set = new HashSet<String>();
+ if (strings != null) {
+ String[] split = strings.split(",");
+ for (int i = 0; i < split.length; i++) {
+ set.add(split[i].trim().toUpperCase());
+ }
+ }
+ return set;
+ }
+
+ /**
+ * Parses a full name and returns parsed components in the Name object.
+ */
+ public void split(Name name, String fullName) {
+ if (fullName == null) {
+ return;
+ }
+
+ NameTokenizer tokens = new NameTokenizer(fullName);
+ parsePrefix(name, tokens);
+ parseSuffix(name, tokens);
+ parseLastName(name, tokens);
+ parseMiddleName(name, tokens);
+ parseGivenNames(name, tokens);
+ }
+
+ /**
+ * Parses the first word from the name if it is a prefix.
+ */
+ private void parsePrefix(Name name, NameTokenizer tokens) {
+ if (tokens.mStartPointer == tokens.mEndPointer) {
+ return;
+ }
+
+ String firstToken = tokens.mTokens[tokens.mStartPointer];
+ if (mPrefixesSet.contains(firstToken.toUpperCase())) {
+ name.prefix = firstToken;
+ tokens.mStartPointer++;
+ }
+ }
+
+ /**
+ * Parses the last word(s) from the name if it is a suffix.
+ */
+ private void parseSuffix(Name name, NameTokenizer tokens) {
+ if (tokens.mStartPointer == tokens.mEndPointer) {
+ return;
+ }
+
+ String lastToken = tokens.mTokens[tokens.mEndPointer - 1];
+ if (lastToken.length() > mMaxSuffixLength) {
+ return;
+ }
+
+ String normalized = lastToken.toUpperCase();
+ if (mSuffixesSet.contains(normalized)) {
+ name.suffix = lastToken;
+ tokens.mEndPointer--;
+ return;
+ }
+
+ if (tokens.hasDot(tokens.mEndPointer - 1)) {
+ lastToken += '.';
+ }
+ normalized += ".";
+
+ // Take care of suffixes like M.D. and D.D.S.
+ int pos = tokens.mEndPointer - 1;
+ while (normalized.length() <= mMaxSuffixLength) {
+
+ if (mSuffixesSet.contains(normalized)) {
+ name.suffix = lastToken;
+ tokens.mEndPointer = pos;
+ return;
+ }
+
+ if (pos == tokens.mStartPointer) {
+ break;
+ }
+
+ pos--;
+ if (tokens.hasDot(pos)) {
+ lastToken = tokens.mTokens[pos] + "." + lastToken;
+ } else {
+ lastToken = tokens.mTokens[pos] + " " + lastToken;
+ }
+
+ normalized = tokens.mTokens[pos].toUpperCase() + "." + normalized;
+ }
+ }
+
+ private void parseLastName(Name name, NameTokenizer tokens) {
+ if (tokens.mStartPointer == tokens.mEndPointer) {
+ return;
+ }
+
+ name.familyName = tokens.mTokens[tokens.mEndPointer - 1];
+ tokens.mEndPointer--;
+
+ // Take care of last names like "D'Onofrio" and "von Cliburn"
+ if ((tokens.mEndPointer - tokens.mStartPointer) > 0) {
+ String lastNamePrefix = tokens.mTokens[tokens.mEndPointer - 1];
+ final String normalized = lastNamePrefix.toUpperCase();
+ if (mLastNamePrefixesSet.contains(normalized)
+ || mLastNamePrefixesSet.contains(normalized + ".")) {
+ if (tokens.hasDot(tokens.mEndPointer - 1)) {
+ lastNamePrefix += '.';
+ }
+ name.familyName = lastNamePrefix + " " + name.familyName;
+ tokens.mEndPointer--;
+ }
+ }
+ }
+
+
+ private void parseMiddleName(Name name, NameTokenizer tokens) {
+ if (tokens.mStartPointer == tokens.mEndPointer) {
+ return;
+ }
+
+ if ((tokens.mEndPointer - tokens.mStartPointer) > 1) {
+ if ((tokens.mEndPointer - tokens.mStartPointer) == 2
+ || !mConjuctions.contains(tokens.mTokens[tokens.mEndPointer - 2].
+ toUpperCase())) {
+ name.middleName = tokens.mTokens[tokens.mEndPointer - 1];
+ tokens.mEndPointer--;
+ }
+ }
+ }
+
+ private void parseGivenNames(Name name, NameTokenizer tokens) {
+ if (tokens.mStartPointer == tokens.mEndPointer) {
+ return;
+ }
+
+ if ((tokens.mEndPointer - tokens.mStartPointer) == 1) {
+ name.givenNames = tokens.mTokens[tokens.mStartPointer];
+ } else {
+ StringBuilder sb = new StringBuilder();
+ for (int i = tokens.mStartPointer; i < tokens.mEndPointer; i++) {
+ if (i != tokens.mStartPointer) {
+ sb.append(' ');
+ }
+ sb.append(tokens.mTokens[i]);
+ if (tokens.hasDot(i)) {
+ sb.append('.');
+ }
+ }
+ name.givenNames = sb.toString();
+ }
+ }
+}
diff --git a/src/com/android/providers/contacts/OpenHelper.java b/src/com/android/providers/contacts/OpenHelper.java
new file mode 100644
index 0000000..01c41d4
--- /dev/null
+++ b/src/com/android/providers/contacts/OpenHelper.java
@@ -0,0 +1,1178 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts;
+
+import com.android.internal.content.SyncStateContentProviderHelper;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDoneException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.database.sqlite.SQLiteStatement;
+import android.provider.BaseColumns;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.SocialContract.Activities;
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+
+import java.util.HashMap;
+
+/**
+ * Database open helper for contacts and social activity data. Designed as a
+ * singleton to make sure that all {@link android.content.ContentProvider} users get the same
+ * reference. Provides handy methods for maintaining package and mime-type
+ * lookup tables.
+ */
+/* package */ class OpenHelper extends SQLiteOpenHelper {
+ private static final String TAG = "OpenHelper";
+
+ private static final int DATABASE_VERSION = 58;
+ private static final String DATABASE_NAME = "contacts2.db";
+ private static final String DATABASE_PRESENCE = "presence_db";
+
+ public interface Delegate {
+ void createDatabase(SQLiteDatabase db);
+ }
+
+ public interface Tables {
+ public static final String ACCOUNTS = "accounts";
+ public static final String CONTACTS = "contacts";
+ public static final String RAW_CONTACTS = "raw_contacts";
+ public static final String PACKAGES = "packages";
+ public static final String MIMETYPES = "mimetypes";
+ public static final String PHONE_LOOKUP = "phone_lookup";
+ public static final String NAME_LOOKUP = "name_lookup";
+ public static final String AGGREGATION_EXCEPTIONS = "agg_exceptions";
+ public static final String DATA = "data";
+ public static final String GROUPS = "groups";
+ public static final String PRESENCE = "presence";
+ public static final String NICKNAME_LOOKUP = "nickname_lookup";
+ public static final String CALLS = "calls";
+ public static final String CONTACT_ENTITIES = "contact_entities_view";
+
+ public static final String CONTACTS_JOIN_PRESENCE_PRIMARY_PHONE = "contacts "
+ + "LEFT OUTER JOIN raw_contacts ON (contacts._id = raw_contacts.contact_id) "
+ + "LEFT OUTER JOIN presence ON (raw_contacts._id = presence.raw_contact_id) "
+ + "LEFT OUTER JOIN data ON (primary_phone_id = data._id)";
+
+ public static final String DATA_JOIN_MIMETYPES = "data "
+ + "LEFT OUTER JOIN mimetypes ON (data.mimetype_id = mimetypes._id)";
+
+ public static final String DATA_JOIN_MIMETYPE_RAW_CONTACTS = "data "
+ + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
+
+ public static final String DATA_JOIN_RAW_CONTACTS_GROUPS = "data "
+ + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)"
+ + "LEFT OUTER JOIN groups ON (groups._id = data." + GroupMembership.GROUP_ROW_ID
+ + ")";
+
+ public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS = "data "
+ + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
+ + "LEFT OUTER JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
+
+ public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
+ + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
+ + "LEFT OUTER JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+
+ public static final String RAW_CONTACTS_JOIN_CONTACTS = "raw_contacts "
+ + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+
+ public static final String DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
+ + "LEFT OUTER JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+
+ public static final String DATA_INNER_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
+ + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+
+ public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS_GROUPS =
+ "data "
+ + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
+ + "LEFT OUTER JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ + "LEFT OUTER JOIN groups "
+ + " ON (mimetypes.mimetype='" + GroupMembership.CONTENT_ITEM_TYPE + "' "
+ + " AND groups._id = data." + GroupMembership.GROUP_ROW_ID + ") "
+ + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+
+ public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS = "data "
+ + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
+ + "LEFT OUTER JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ + "LEFT OUTER JOIN groups "
+ + " ON (mimetypes.mimetype='" + GroupMembership.CONTENT_ITEM_TYPE + "' "
+ + " AND groups._id = data." + GroupMembership.GROUP_ROW_ID + ") ";
+
+ public static final String GROUPS_JOIN_PACKAGES = "groups "
+ + "LEFT OUTER JOIN packages ON (groups.package_id = packages._id)";
+
+ public static final String GROUPS_JOIN_PACKAGES_DATA_RAW_CONTACTS_CONTACTS = "groups "
+ + "LEFT OUTER JOIN packages ON (groups.package_id = packages._id) "
+ + "LEFT OUTER JOIN data "
+ + " ON (groups._id = data." + GroupMembership.GROUP_ROW_ID + ") "
+ + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+
+ public static final String ACTIVITIES = "activities";
+
+ public static final String ACTIVITIES_JOIN_MIMETYPES = "activities "
+ + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id)";
+
+ public static final String ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS =
+ "activities "
+ + "LEFT OUTER JOIN packages ON (activities.package_id = packages._id) "
+ + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id) "
+ + "LEFT OUTER JOIN raw_contacts ON (activities.author_contact_id = " +
+ "raw_contacts._id) "
+ + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
+
+ public static final String NAME_LOOKUP_JOIN_RAW_CONTACTS = "name_lookup "
+ + "INNER JOIN raw_contacts ON (name_lookup.raw_contact_id = raw_contacts._id)";
+
+ public static final String AGGREGATION_EXCEPTIONS_JOIN_RAW_CONTACTS = "agg_exceptions "
+ + "INNER JOIN raw_contacts raw_contacts1 "
+ + "ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) ";
+
+ public static final String AGGREGATION_EXCEPTIONS_JOIN_RAW_CONTACTS_TWICE =
+ "agg_exceptions "
+ + "INNER JOIN raw_contacts raw_contacts1 "
+ + "ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) "
+ + "INNER JOIN raw_contacts raw_contacts2 "
+ + "ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) ";
+ }
+
+ public interface Clauses {
+ public static final String WHERE_IM_MATCHES = MimetypesColumns.MIMETYPE + "=" + Im.MIMETYPE
+ + " AND " + Im.PROTOCOL + "=? AND " + Im.DATA + "=?";
+
+ public static final String WHERE_EMAIL_MATCHES = MimetypesColumns.MIMETYPE + "="
+ + Email.MIMETYPE + " AND " + Email.DATA + "=?";
+
+ public static final String MIMETYPE_IS_GROUP_MEMBERSHIP = MimetypesColumns.CONCRETE_MIMETYPE
+ + "='" + GroupMembership.CONTENT_ITEM_TYPE + "'";
+
+ public static final String BELONGS_TO_GROUP = DataColumns.CONCRETE_GROUP_ID + "="
+ + GroupsColumns.CONCRETE_ID;
+
+ public static final String HAS_PRIMARY_PHONE = "("
+ + ContactsColumns.OPTIMAL_PRIMARY_PHONE_ID + " IS NOT NULL OR "
+ + ContactsColumns.FALLBACK_PRIMARY_PHONE_ID + " IS NOT NULL)";
+
+ // TODO: add in check against package_visible
+ public static final String IN_VISIBLE_GROUP = "SELECT MIN(COUNT(" + DataColumns.CONCRETE_ID
+ + "),1) FROM " + Tables.DATA_JOIN_RAW_CONTACTS_GROUPS + " WHERE "
+ + DataColumns.MIMETYPE_ID + "=? AND " + RawContacts.CONTACT_ID + "="
+ + ContactsColumns.CONCRETE_ID + " AND " + Groups.GROUP_VISIBLE + "=1";
+
+ public static final String GROUP_HAS_ACCOUNT_AND_SOURCE_ID =
+ Groups.SOURCE_ID + "=? AND "
+ + Groups.ACCOUNT_NAME + "=? AND "
+ + Groups.ACCOUNT_TYPE + "=?";
+ }
+
+ public interface ContactsColumns {
+ public static final String OPTIMAL_PRIMARY_PHONE_ID = "optimal_phone_id";
+ public static final String OPTIMAL_PRIMARY_PHONE_IS_RESTRICTED =
+ "optimal_phone_is_restricted";
+ public static final String FALLBACK_PRIMARY_PHONE_ID = "fallback_phone_id";
+
+ public static final String OPTIMAL_PRIMARY_EMAIL_ID = "optimal_email_id";
+ public static final String OPTIMAL_PRIMARY_EMAIL_IS_RESTRICTED =
+ "optimal_email_is_restricted";
+ public static final String FALLBACK_PRIMARY_EMAIL_ID = "fallback_email_id";
+
+ public static final String SINGLE_IS_RESTRICTED = "single_is_restricted";
+
+ public static final String CONCRETE_ID = Tables.CONTACTS + "." + BaseColumns._ID;
+ public static final String CONCRETE_DISPLAY_NAME = Tables.CONTACTS + "."
+ + Contacts.DISPLAY_NAME;
+
+ public static final String CONCRETE_TIMES_CONTACTED = Tables.CONTACTS + "."
+ + Contacts.TIMES_CONTACTED;
+ public static final String CONCRETE_LAST_TIME_CONTACTED = Tables.CONTACTS + "."
+ + Contacts.LAST_TIME_CONTACTED;
+ public static final String CONCRETE_STARRED = Tables.CONTACTS + "." + Contacts.STARRED;
+ public static final String CONCRETE_CUSTOM_RINGTONE = Tables.CONTACTS + "."
+ + Contacts.CUSTOM_RINGTONE;
+ public static final String CONCRETE_SEND_TO_VOICEMAIL = Tables.CONTACTS + "."
+ + Contacts.SEND_TO_VOICEMAIL;
+ }
+
+ public interface RawContactsColumns {
+ public static final String CONCRETE_ID =
+ Tables.RAW_CONTACTS + "." + BaseColumns._ID;
+ public static final String CONCRETE_ACCOUNT_NAME =
+ Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_NAME;
+ public static final String CONCRETE_ACCOUNT_TYPE =
+ Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_TYPE;
+ public static final String CONCRETE_SOURCE_ID =
+ Tables.RAW_CONTACTS + "." + RawContacts.SOURCE_ID;
+ public static final String CONCRETE_VERSION =
+ Tables.RAW_CONTACTS + "." + RawContacts.VERSION;
+ public static final String CONCRETE_DIRTY =
+ Tables.RAW_CONTACTS + "." + RawContacts.DIRTY;
+ public static final String CONCRETE_DELETED =
+ Tables.RAW_CONTACTS + "." + RawContacts.DELETED;
+ public static final String DISPLAY_NAME = "display_name";
+ }
+
+ public interface DataColumns {
+ public static final String PACKAGE_ID = "package_id";
+ public static final String MIMETYPE_ID = "mimetype_id";
+
+ public static final String CONCRETE_ID = Tables.DATA + "." + BaseColumns._ID;
+ public static final String CONCRETE_MIMETYPE_ID = Tables.DATA + "." + MIMETYPE_ID;
+ public static final String CONCRETE_RAW_CONTACT_ID = Tables.DATA + "."
+ + Data.RAW_CONTACT_ID;
+ public static final String CONCRETE_GROUP_ID = Tables.DATA + "."
+ + GroupMembership.GROUP_ROW_ID;
+
+ public static final String CONCRETE_DATA1 = Tables.DATA + "." + Data.DATA1;
+ public static final String CONCRETE_DATA2 = Tables.DATA + "." + Data.DATA2;
+ public static final String CONCRETE_DATA3 = Tables.DATA + "." + Data.DATA3;
+ public static final String CONCRETE_DATA4 = Tables.DATA + "." + Data.DATA4;
+ public static final String CONCRETE_DATA5 = Tables.DATA + "." + Data.DATA5;
+ public static final String CONCRETE_DATA6 = Tables.DATA + "." + Data.DATA6;
+ public static final String CONCRETE_DATA7 = Tables.DATA + "." + Data.DATA7;
+ public static final String CONCRETE_DATA8 = Tables.DATA + "." + Data.DATA8;
+ public static final String CONCRETE_DATA9 = Tables.DATA + "." + Data.DATA9;
+ public static final String CONCRETE_DATA10 = Tables.DATA + "." + Data.DATA10;
+ public static final String CONCRETE_DATA11 = Tables.DATA + "." + Data.DATA11;
+ public static final String CONCRETE_DATA12 = Tables.DATA + "." + Data.DATA12;
+ public static final String CONCRETE_DATA13 = Tables.DATA + "." + Data.DATA13;
+ public static final String CONCRETE_DATA14 = Tables.DATA + "." + Data.DATA14;
+ public static final String CONCRETE_DATA15 = Tables.DATA + "." + Data.DATA15;
+ public static final String CONCRETE_IS_PRIMARY = Tables.DATA + "." + Data.IS_PRIMARY;
+ public static final String CONCRETE_PACKAGE_ID = Tables.DATA + "." + PACKAGE_ID;
+ }
+
+ // Used only for legacy API support
+ public interface ExtensionsColumns {
+ public static final String NAME = Data.DATA1;
+ public static final String VALUE = Data.DATA2;
+ }
+
+ public interface GroupMembershipColumns {
+ public static final String RAW_CONTACT_ID = Data.RAW_CONTACT_ID;
+ public static final String GROUP_ROW_ID = GroupMembership.GROUP_ROW_ID;
+ }
+
+ public interface PhoneColumns {
+ public static final String NORMALIZED_NUMBER = Data.DATA4;
+ public static final String CONCRETE_NORMALIZED_NUMBER = DataColumns.CONCRETE_DATA4;
+ }
+
+ public interface GroupsColumns {
+ public static final String PACKAGE_ID = "package_id";
+
+ public static final String CONCRETE_ID = Tables.GROUPS + "." + BaseColumns._ID;
+ public static final String CONCRETE_SOURCE_ID = Tables.GROUPS + "." + Groups.SOURCE_ID;
+}
+
+ public interface ActivitiesColumns {
+ public static final String PACKAGE_ID = "package_id";
+ public static final String MIMETYPE_ID = "mimetype_id";
+ }
+
+ public interface PhoneLookupColumns {
+ public static final String _ID = BaseColumns._ID;
+ public static final String DATA_ID = "data_id";
+ public static final String RAW_CONTACT_ID = "raw_contact_id";
+ public static final String NORMALIZED_NUMBER = "normalized_number";
+ }
+
+ public interface NameLookupColumns {
+ public static final String _ID = BaseColumns._ID;
+ public static final String RAW_CONTACT_ID = "raw_contact_id";
+ public static final String NORMALIZED_NAME = "normalized_name";
+ public static final String NAME_TYPE = "name_type";
+ }
+
+ public final static class NameLookupType {
+ public static final int FULL_NAME = 0;
+ public static final int FULL_NAME_CONCATENATED = 1;
+ public static final int FULL_NAME_REVERSE = 2;
+ public static final int FULL_NAME_REVERSE_CONCATENATED = 3;
+ public static final int FULL_NAME_WITH_NICKNAME = 4;
+ public static final int FULL_NAME_WITH_NICKNAME_REVERSE = 5;
+ public static final int GIVEN_NAME_ONLY = 6;
+ public static final int GIVEN_NAME_ONLY_AS_NICKNAME = 7;
+ public static final int FAMILY_NAME_ONLY = 8;
+ public static final int FAMILY_NAME_ONLY_AS_NICKNAME = 9;
+ public static final int NICKNAME = 10;
+ public static final int EMAIL_BASED_NICKNAME = 11;
+
+ // This is the highest name lookup type code plus one
+ public static final int TYPE_COUNT = 12;
+
+ public static boolean isBasedOnStructuredName(int nameLookupType) {
+ return nameLookupType != NameLookupType.EMAIL_BASED_NICKNAME
+ && nameLookupType != NameLookupType.NICKNAME;
+ }
+ }
+
+ public interface PackagesColumns {
+ public static final String _ID = BaseColumns._ID;
+ public static final String PACKAGE = "package";
+
+ public static final String CONCRETE_ID = Tables.PACKAGES + "." + _ID;
+ }
+
+ public interface MimetypesColumns {
+ public static final String _ID = BaseColumns._ID;
+ public static final String MIMETYPE = "mimetype";
+
+ public static final String CONCRETE_ID = Tables.MIMETYPES + "." + BaseColumns._ID;
+ public static final String CONCRETE_MIMETYPE = Tables.MIMETYPES + "." + MIMETYPE;
+ }
+
+ public interface AggregationExceptionColumns {
+ public static final String _ID = BaseColumns._ID;
+ public static final String RAW_CONTACT_ID1 = "raw_contact_id1";
+ public static final String RAW_CONTACT_ID2 = "raw_contact_id2";
+ }
+
+ public interface NicknameLookupColumns {
+ public static final String NAME = "name";
+ public static final String CLUSTER = "cluster";
+ }
+
+ private static final String[] NICKNAME_LOOKUP_COLUMNS = new String[] {
+ NicknameLookupColumns.CLUSTER
+ };
+
+ private static final int COL_NICKNAME_LOOKUP_CLUSTER = 0;
+
+ /** In-memory cache of previously found mimetype mappings */
+ private final HashMap<String, Long> mMimetypeCache = new HashMap<String, Long>();
+ /** In-memory cache of previously found package name mappings */
+ private final HashMap<String, Long> mPackageCache = new HashMap<String, Long>();
+
+
+ /** Compiled statements for querying and inserting mappings */
+ private SQLiteStatement mMimetypeQuery;
+ private SQLiteStatement mPackageQuery;
+ private SQLiteStatement mContactIdQuery;
+ private SQLiteStatement mAggregationModeQuery;
+ private SQLiteStatement mContactIdUpdate;
+ private SQLiteStatement mMimetypeInsert;
+ private SQLiteStatement mPackageInsert;
+ private SQLiteStatement mNameLookupInsert;
+
+ private SQLiteStatement mDataMimetypeQuery;
+ private SQLiteStatement mActivitiesMimetypeQuery;
+
+ private final Context mContext;
+ private final SyncStateContentProviderHelper mSyncState;
+ private HashMap<String, String[]> mNicknameClusterCache;
+
+ /** Compiled statements for updating {@link Contacts#IN_VISIBLE_GROUP}. */
+ private SQLiteStatement mVisibleAllUpdate;
+ private SQLiteStatement mVisibleSpecificUpdate;
+
+ private Delegate mDelegate;
+
+ private static OpenHelper sSingleton = null;
+
+ public static synchronized OpenHelper getInstance(Context context) {
+ if (sSingleton == null) {
+ sSingleton = new OpenHelper(context);
+ }
+ return sSingleton;
+ }
+
+ /**
+ * Private constructor, callers except unit tests should obtain an instance through
+ * {@link #getInstance(android.content.Context)} instead.
+ */
+ /* package */ OpenHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ Log.i(TAG, "Creating OpenHelper");
+
+ mContext = context;
+ mSyncState = new SyncStateContentProviderHelper();
+ }
+
+ public Delegate getDelegate() {
+ return mDelegate;
+ }
+
+ public void setDelegate(Delegate delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ mSyncState.onDatabaseOpened(db);
+
+ // Create compiled statements for package and mimetype lookups
+ mMimetypeQuery = db.compileStatement("SELECT " + MimetypesColumns._ID + " FROM "
+ + Tables.MIMETYPES + " WHERE " + MimetypesColumns.MIMETYPE + "=?");
+ mPackageQuery = db.compileStatement("SELECT " + PackagesColumns._ID + " FROM "
+ + Tables.PACKAGES + " WHERE " + PackagesColumns.PACKAGE + "=?");
+ mContactIdQuery = db.compileStatement("SELECT " + RawContacts.CONTACT_ID + " FROM "
+ + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?");
+ mContactIdUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
+ + RawContacts.CONTACT_ID + "=?" + " WHERE " + RawContacts._ID + "=?");
+ mAggregationModeQuery = db.compileStatement("SELECT " + RawContacts.AGGREGATION_MODE
+ + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?");
+ mMimetypeInsert = db.compileStatement("INSERT INTO " + Tables.MIMETYPES + "("
+ + MimetypesColumns.MIMETYPE + ") VALUES (?)");
+ mPackageInsert = db.compileStatement("INSERT INTO " + Tables.PACKAGES + "("
+ + PackagesColumns.PACKAGE + ") VALUES (?)");
+
+ mDataMimetypeQuery = db.compileStatement("SELECT " + MimetypesColumns.MIMETYPE + " FROM "
+ + Tables.DATA_JOIN_MIMETYPES + " WHERE " + Tables.DATA + "." + Data._ID + "=?");
+ mActivitiesMimetypeQuery = db.compileStatement("SELECT " + MimetypesColumns.MIMETYPE
+ + " FROM " + Tables.ACTIVITIES_JOIN_MIMETYPES + " WHERE " + Tables.ACTIVITIES + "."
+ + Activities._ID + "=?");
+ mNameLookupInsert = db.compileStatement("INSERT INTO " + Tables.NAME_LOOKUP + "("
+ + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.NAME_TYPE + ","
+ + NameLookupColumns.NORMALIZED_NAME + ") VALUES (?,?,?)");
+
+ final String visibleUpdate = "UPDATE " + Tables.CONTACTS + " SET "
+ + Contacts.IN_VISIBLE_GROUP + "= (" + Clauses.IN_VISIBLE_GROUP + ")";
+
+ mVisibleAllUpdate = db.compileStatement(visibleUpdate);
+ mVisibleSpecificUpdate = db.compileStatement(visibleUpdate + " WHERE "
+ + ContactsColumns.CONCRETE_ID + "=?");
+
+ // Make sure we have an in-memory presence table
+ final String tableName = DATABASE_PRESENCE + "." + Tables.PRESENCE;
+ final String indexName = DATABASE_PRESENCE + ".presenceIndex";
+
+ db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + tableName + " ("+
+ Presence._ID + " INTEGER PRIMARY KEY," +
+ Presence.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
+ Presence.DATA_ID + " INTEGER REFERENCES data(_id)," +
+ Presence.IM_PROTOCOL + " TEXT," +
+ Presence.IM_HANDLE + " TEXT," +
+ Presence.IM_ACCOUNT + " TEXT," +
+ Presence.PRESENCE_STATUS + " INTEGER," +
+ Presence.PRESENCE_CUSTOM_STATUS + " TEXT," +
+ "UNIQUE(" + Presence.IM_PROTOCOL + ", " + Presence.IM_HANDLE + ", "
+ + Presence.IM_ACCOUNT + ")" +
+ ");");
+
+ db.execSQL("CREATE INDEX IF NOT EXISTS " + indexName + " ON " + Tables.PRESENCE + " ("
+ + Presence.RAW_CONTACT_ID + ");");
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Log.i(TAG, "Bootstrapping database");
+
+ mSyncState.createDatabase(db);
+
+ // One row per group of contacts corresponding to the same person
+ db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Contacts.DISPLAY_NAME + " TEXT," +
+ Contacts.PHOTO_ID + " INTEGER REFERENCES data(_id)," +
+ Contacts.CUSTOM_RINGTONE + " TEXT," +
+ Contacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
+ Contacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
+ Contacts.LAST_TIME_CONTACTED + " INTEGER," +
+ Contacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
+ Contacts.IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 1," +
+ ContactsColumns.OPTIMAL_PRIMARY_PHONE_ID + " INTEGER REFERENCES data(_id)," +
+ ContactsColumns.OPTIMAL_PRIMARY_PHONE_IS_RESTRICTED + " INTEGER DEFAULT 0," +
+ ContactsColumns.FALLBACK_PRIMARY_PHONE_ID + " INTEGER REFERENCES data(_id)," +
+ ContactsColumns.OPTIMAL_PRIMARY_EMAIL_ID + " INTEGER REFERENCES data(_id)," +
+ ContactsColumns.OPTIMAL_PRIMARY_EMAIL_IS_RESTRICTED + " INTEGER DEFAULT 0," +
+ ContactsColumns.FALLBACK_PRIMARY_EMAIL_ID + " INTEGER REFERENCES data(_id)," +
+ ContactsColumns.SINGLE_IS_RESTRICTED + " INTEGER REFERENCES package(_id)" +
+ ");");
+
+ // Contacts table
+ db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" +
+ RawContacts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ RawContacts.IS_RESTRICTED + " INTEGER DEFAULT 0," +
+ RawContacts.ACCOUNT_NAME + " STRING DEFAULT NULL, " +
+ RawContacts.ACCOUNT_TYPE + " STRING DEFAULT NULL, " +
+ RawContacts.SOURCE_ID + " TEXT," +
+ RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," +
+ RawContacts.DIRTY + " INTEGER NOT NULL DEFAULT 1," +
+ RawContacts.DELETED + " INTEGER NOT NULL DEFAULT 0," +
+ RawContacts.CONTACT_ID + " INTEGER," +
+ RawContacts.AGGREGATION_MODE + " INTEGER NOT NULL DEFAULT " +
+ RawContacts.AGGREGATION_MODE_DEFAULT + "," +
+ RawContacts.CUSTOM_RINGTONE + " TEXT," +
+ RawContacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
+ RawContacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
+ RawContacts.LAST_TIME_CONTACTED + " INTEGER," +
+ RawContacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
+ RawContactsColumns.DISPLAY_NAME + " TEXT" +
+ ");");
+
+ // Package name mapping table
+ db.execSQL("CREATE TABLE " + Tables.PACKAGES + " (" +
+ PackagesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ PackagesColumns.PACKAGE + " TEXT NOT NULL" +
+ ");");
+
+ // Mimetype mapping table
+ db.execSQL("CREATE TABLE " + Tables.MIMETYPES + " (" +
+ MimetypesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ MimetypesColumns.MIMETYPE + " TEXT NOT NULL" +
+ ");");
+
+ // Public generic data table
+ db.execSQL("CREATE TABLE " + Tables.DATA + " (" +
+ Data._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ DataColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
+ DataColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," +
+ Data.RAW_CONTACT_ID + " INTEGER NOT NULL," +
+ Data.IS_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
+ Data.IS_SUPER_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
+ Data.DATA_VERSION + " INTEGER NOT NULL DEFAULT 0," +
+ Data.DATA1 + " TEXT," +
+ Data.DATA2 + " TEXT," +
+ Data.DATA3 + " TEXT," +
+ Data.DATA4 + " TEXT," +
+ Data.DATA5 + " TEXT," +
+ Data.DATA6 + " TEXT," +
+ Data.DATA7 + " TEXT," +
+ Data.DATA8 + " TEXT," +
+ Data.DATA9 + " TEXT," +
+ Data.DATA10 + " TEXT," +
+ Data.DATA11 + " TEXT," +
+ Data.DATA12 + " TEXT," +
+ Data.DATA13 + " TEXT," +
+ Data.DATA14 + " TEXT," +
+ Data.DATA15 + " TEXT" +
+ ");");
+
+ /**
+ * Automatically delete Data rows when a raw contact is deleted.
+ */
+ db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_deleted "
+ + " BEFORE DELETE ON " + Tables.RAW_CONTACTS
+ + " BEGIN "
+ + " DELETE FROM " + Tables.DATA
+ + " WHERE " + Data.RAW_CONTACT_ID + "=OLD." + RawContacts._ID + ";"
+ + " DELETE FROM " + Tables.PHONE_LOOKUP
+ + " WHERE " + PhoneLookupColumns.RAW_CONTACT_ID + "=OLD." + RawContacts._ID + ";"
+ + " END");
+
+ /**
+ * Triggers that set {@link RawContacts#DIRTY} and update {@link RawContacts#VERSION}
+ * when the contact is marked for deletion or any time a data row is inserted, updated
+ * or deleted.
+ */
+ db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_marked_deleted "
+ + " BEFORE UPDATE ON " + Tables.RAW_CONTACTS
+ + " BEGIN "
+ + " UPDATE " + Tables.RAW_CONTACTS
+ + " SET "
+ + RawContacts.VERSION + "=OLD." + RawContacts.VERSION + "+1, "
+ + RawContacts.DIRTY + "=1"
+ + " WHERE " + RawContacts._ID + "=OLD." + RawContacts._ID
+ + " AND NEW." + RawContacts.DELETED + "!= OLD." + RawContacts.DELETED + ";"
+ + " END");
+
+ db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated AFTER UPDATE ON " + Tables.DATA
+ + " BEGIN "
+ + " UPDATE " + Tables.DATA
+ + " SET " + Data.DATA_VERSION + "=OLD." + Data.DATA_VERSION + "+1 "
+ + " WHERE " + Data._ID + "=OLD." + Data._ID + ";"
+ + " UPDATE " + Tables.RAW_CONTACTS
+ + " SET " + RawContacts.DIRTY + "=1, "
+ + " " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 "
+ + " WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";"
+ + " END");
+
+ db.execSQL("CREATE TRIGGER " + Tables.DATA + "_inserted BEFORE INSERT ON " + Tables.DATA
+ + " BEGIN "
+ + " UPDATE " + Tables.RAW_CONTACTS
+ + " SET " + RawContacts.DIRTY + "=1, "
+ + " " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 "
+ + " WHERE " + RawContacts._ID + "=NEW." + Data.RAW_CONTACT_ID + ";"
+ + " END");
+
+ db.execSQL("CREATE TRIGGER " + Tables.DATA + "_deleted BEFORE DELETE ON " + Tables.DATA
+ + " BEGIN "
+ + " UPDATE " + Tables.RAW_CONTACTS
+ + " SET " + RawContacts.DIRTY + "=1,"
+ + " " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 "
+ + " WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";"
+ + " DELETE FROM " + Tables.PHONE_LOOKUP
+ + " WHERE " + PhoneLookupColumns.DATA_ID + "=OLD." + Data._ID + ";"
+ + " END");
+
+ // Private phone numbers table used for lookup
+ db.execSQL("CREATE TABLE " + Tables.PHONE_LOOKUP + " (" +
+ PhoneLookupColumns._ID + " INTEGER PRIMARY KEY," +
+ PhoneLookupColumns.DATA_ID + " INTEGER REFERENCES data(_id) NOT NULL," +
+ PhoneLookupColumns.RAW_CONTACT_ID
+ + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
+ PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL" +
+ ");");
+
+ db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" +
+ PhoneLookupColumns.NORMALIZED_NUMBER + " ASC, " +
+ PhoneLookupColumns.RAW_CONTACT_ID + ", " +
+ PhoneLookupColumns.DATA_ID +
+ ");");
+
+ // Private name/nickname table used for lookup
+ db.execSQL("CREATE TABLE " + Tables.NAME_LOOKUP + " (" +
+ NameLookupColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ NameLookupColumns.RAW_CONTACT_ID
+ + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
+ NameLookupColumns.NORMALIZED_NAME + " TEXT," +
+ NameLookupColumns.NAME_TYPE + " INTEGER" +
+ ");");
+
+ db.execSQL("CREATE INDEX name_lookup_index ON " + Tables.NAME_LOOKUP + " (" +
+ NameLookupColumns.NORMALIZED_NAME + " ASC, " +
+ NameLookupColumns.NAME_TYPE + " ASC, " +
+ NameLookupColumns.RAW_CONTACT_ID +
+ ");");
+
+ db.execSQL("CREATE TABLE " + Tables.NICKNAME_LOOKUP + " (" +
+ NicknameLookupColumns.NAME + " TEXT," +
+ NicknameLookupColumns.CLUSTER + " TEXT" +
+ ");");
+
+ db.execSQL("CREATE UNIQUE INDEX nickname_lookup_index ON " + Tables.NICKNAME_LOOKUP + " (" +
+ NicknameLookupColumns.NAME + ", " +
+ NicknameLookupColumns.CLUSTER +
+ ");");
+
+ // Groups table
+ db.execSQL("CREATE TABLE " + Tables.GROUPS + " (" +
+ Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ GroupsColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
+ Groups.ACCOUNT_NAME + " STRING DEFAULT NULL, " +
+ Groups.ACCOUNT_TYPE + " STRING DEFAULT NULL, " +
+ Groups.SOURCE_ID + " TEXT," +
+ Groups.VERSION + " INTEGER NOT NULL DEFAULT 1," +
+ Groups.DIRTY + " INTEGER NOT NULL DEFAULT 1," +
+ Groups.TITLE + " TEXT," +
+ Groups.TITLE_RES + " INTEGER," +
+ Groups.NOTES + " TEXT," +
+ Groups.SYSTEM_ID + " TEXT," +
+ Groups.GROUP_VISIBLE + " INTEGER" +
+ ");");
+
+ db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_updated1 "
+ + " BEFORE UPDATE ON " + Tables.GROUPS
+ + " BEGIN "
+ + " UPDATE " + Tables.GROUPS
+ + " SET "
+ + Groups.VERSION + "=OLD." + Groups.VERSION + "+1, "
+ + Groups.DIRTY + "=1"
+ + " WHERE " + Groups._ID + "=OLD." + Groups._ID + ";"
+ + " END");
+
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.AGGREGATION_EXCEPTIONS + " (" +
+ AggregationExceptionColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ AggregationExceptions.TYPE + " INTEGER NOT NULL, " +
+ AggregationExceptionColumns.RAW_CONTACT_ID1
+ + " INTEGER REFERENCES raw_contacts(_id), " +
+ AggregationExceptionColumns.RAW_CONTACT_ID2
+ + " INTEGER REFERENCES raw_contacts(_id)" +
+ ");");
+
+ db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index1 ON " +
+ Tables.AGGREGATION_EXCEPTIONS + " (" +
+ AggregationExceptionColumns.RAW_CONTACT_ID1 + ", " +
+ AggregationExceptionColumns.RAW_CONTACT_ID2 +
+ ");");
+
+ db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index2 ON " +
+ Tables.AGGREGATION_EXCEPTIONS + " (" +
+ AggregationExceptionColumns.RAW_CONTACT_ID2 + ", " +
+ AggregationExceptionColumns.RAW_CONTACT_ID1 +
+ ");");
+
+ // The table for recent calls is here so we can do table joins
+ // on people, phones, and calls all in one place.
+ db.execSQL("CREATE TABLE " + Tables.CALLS + " (" +
+ Calls._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Calls.NUMBER + " TEXT," +
+ Calls.DATE + " INTEGER," +
+ Calls.DURATION + " INTEGER," +
+ Calls.TYPE + " INTEGER," +
+ Calls.NEW + " INTEGER," +
+ Calls.CACHED_NAME + " TEXT," +
+ Calls.CACHED_NUMBER_TYPE + " INTEGER," +
+ Calls.CACHED_NUMBER_LABEL + " TEXT" +
+ ");");
+
+ // Activities table
+ db.execSQL("CREATE TABLE " + Tables.ACTIVITIES + " (" +
+ Activities._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ ActivitiesColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
+ ActivitiesColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," +
+ Activities.RAW_ID + " TEXT," +
+ Activities.IN_REPLY_TO + " TEXT," +
+ Activities.AUTHOR_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
+ Activities.TARGET_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
+ Activities.PUBLISHED + " INTEGER NOT NULL," +
+ Activities.THREAD_PUBLISHED + " INTEGER NOT NULL," +
+ Activities.TITLE + " TEXT NOT NULL," +
+ Activities.SUMMARY + " TEXT," +
+ Activities.LINK + " TEXT, " +
+ Activities.THUMBNAIL + " BLOB" +
+ ");");
+
+ db.execSQL("CREATE VIEW " + Tables.CONTACT_ENTITIES + " AS SELECT "
+ + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + ","
+ + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + ","
+ + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
+ + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
+ + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
+ + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + ","
+ + RawContacts.CONTACT_ID + ", "
+ + Data.MIMETYPE + ", "
+ + Data.DATA1 + ", "
+ + Data.DATA2 + ", "
+ + Data.DATA3 + ", "
+ + Data.DATA4 + ", "
+ + Data.DATA5 + ", "
+ + Data.DATA6 + ", "
+ + Data.DATA7 + ", "
+ + Data.DATA8 + ", "
+ + Data.DATA9 + ", "
+ + Data.DATA10 + ", "
+ + Data.DATA11 + ", "
+ + Data.DATA12 + ", "
+ + Data.DATA13 + ", "
+ + Data.DATA14 + ", "
+ + Data.DATA15 + ", "
+ + Data.RAW_CONTACT_ID + ", "
+ + Data.IS_PRIMARY + ", "
+ + Data.DATA_VERSION + ", "
+ + DataColumns.CONCRETE_ID + " AS " + RawContacts._ID + ","
+ + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
+ + " FROM " + Tables.DATA
+ + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
+ + DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
+ + " LEFT OUTER JOIN " + Tables.MIMETYPES + " ON ("
+ + DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
+ + " LEFT OUTER JOIN " + Tables.RAW_CONTACTS + " ON ("
+ + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
+ + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
+ + MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
+ + "' AND " + GroupsColumns.CONCRETE_ID + "=" + DataColumns.CONCRETE_DATA1 + ")");
+
+ loadNicknameLookupTable(db);
+ if (mDelegate != null) {
+ mDelegate.createDatabase(db);
+ }
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion
+ + ", data will be lost!");
+
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.ACCOUNTS + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.CONTACTS + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.RAW_CONTACTS + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.PACKAGES + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.MIMETYPES + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.DATA + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.PHONE_LOOKUP + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.NAME_LOOKUP + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.NICKNAME_LOOKUP + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.GROUPS + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.ACTIVITIES + ";");
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.CALLS);
+
+ db.execSQL("DROP VIEW IF EXISTS " + Tables.CONTACT_ENTITIES + ";");
+
+ // TODO: we should not be dropping agg_exceptions and contact_options. In case that table's
+ // schema changes, we should try to preserve the data, because it was entered by the user
+ // and has never been synched to the server.
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.AGGREGATION_EXCEPTIONS + ";");
+
+ onCreate(db);
+
+ // TODO: eventually when this supports upgrades we should do something like the following:
+// if (!upgradeDatabase(db, oldVersion, newVersion)) {
+// mSyncState.discardSyncData(db, null /* all accounts */);
+// ContentResolver.requestSync(null /* all accounts */,
+// mContentUri.getAuthority(), new Bundle());
+// }
+ }
+
+ /**
+ * Wipes all data except mime type and package lookup tables.
+ */
+ public void wipeData() {
+ SQLiteDatabase db = getWritableDatabase();
+ db.execSQL("DELETE FROM " + Tables.CONTACTS + ";");
+ db.execSQL("DELETE FROM " + Tables.RAW_CONTACTS + ";");
+ db.execSQL("DELETE FROM " + Tables.DATA + ";");
+ db.execSQL("DELETE FROM " + Tables.PHONE_LOOKUP + ";");
+ db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + ";");
+ db.execSQL("DELETE FROM " + Tables.GROUPS + ";");
+ db.execSQL("DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS + ";");
+ db.execSQL("DELETE FROM " + Tables.ACTIVITIES + ";");
+ db.execSQL("DELETE FROM " + Tables.CALLS);
+
+ // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
+
+ db.execSQL("VACUUM;");
+ }
+
+ /**
+ * Return the {@link ApplicationInfo#uid} for the given package name.
+ */
+ public static int getUidForPackageName(PackageManager pm, String packageName) {
+ try {
+ ApplicationInfo clientInfo = pm.getApplicationInfo(packageName, 0 /* no flags */);
+ return clientInfo.uid;
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Perform an internal string-to-integer lookup using the compiled
+ * {@link SQLiteStatement} provided, using the in-memory cache to speed up
+ * lookups. If a mapping isn't found in cache or database, it will be
+ * created. All new, uncached answers are added to the cache automatically.
+ *
+ * @param query Compiled statement used to query for the mapping.
+ * @param insert Compiled statement used to insert a new mapping when no
+ * existing one is found in cache or from query.
+ * @param value Value to find mapping for.
+ * @param cache In-memory cache of previous answers.
+ * @return An unique integer mapping for the given value.
+ */
+ private synchronized long getCachedId(SQLiteStatement query, SQLiteStatement insert,
+ String value, HashMap<String, Long> cache) {
+ // Try an in-memory cache lookup
+ if (cache.containsKey(value)) {
+ return cache.get(value);
+ }
+
+ long id = -1;
+ try {
+ // Try searching database for mapping
+ DatabaseUtils.bindObjectToProgram(query, 1, value);
+ id = query.simpleQueryForLong();
+ } catch (SQLiteDoneException e) {
+ // Nothing found, so try inserting new mapping
+ DatabaseUtils.bindObjectToProgram(insert, 1, value);
+ id = insert.executeInsert();
+ }
+
+ if (id != -1) {
+ // Cache and return the new answer
+ cache.put(value, id);
+ return id;
+ } else {
+ // Otherwise throw if no mapping found or created
+ throw new IllegalStateException("Couldn't find or create internal "
+ + "lookup table entry for value " + value);
+ }
+ }
+
+ /**
+ * Convert a package name into an integer, using {@link Tables#PACKAGES} for
+ * lookups and possible allocation of new IDs as needed.
+ */
+ public long getPackageId(String packageName) {
+ // Make sure compiled statements are ready by opening database
+ getReadableDatabase();
+ return getCachedId(mPackageQuery, mPackageInsert, packageName, mPackageCache);
+ }
+
+ /**
+ * Convert a mimetype into an integer, using {@link Tables#MIMETYPES} for
+ * lookups and possible allocation of new IDs as needed.
+ */
+ public long getMimeTypeId(String mimetype) {
+ // Make sure compiled statements are ready by opening database
+ getReadableDatabase();
+ return getCachedId(mMimetypeQuery, mMimetypeInsert, mimetype, mMimetypeCache);
+ }
+
+ /**
+ * Find the mimetype for the given {@link Data#_ID}.
+ */
+ public String getDataMimeType(long dataId) {
+ // Make sure compiled statements are ready by opening database
+ getReadableDatabase();
+ try {
+ // Try database query to find mimetype
+ DatabaseUtils.bindObjectToProgram(mDataMimetypeQuery, 1, dataId);
+ String mimetype = mDataMimetypeQuery.simpleQueryForString();
+ return mimetype;
+ } catch (SQLiteDoneException e) {
+ // No valid mapping found, so return null
+ return null;
+ }
+ }
+
+ /**
+ * Find the mime-type for the given {@link Activities#_ID}.
+ */
+ public String getActivityMimeType(long activityId) {
+ // Make sure compiled statements are ready by opening database
+ getReadableDatabase();
+ try {
+ // Try database query to find mimetype
+ DatabaseUtils.bindObjectToProgram(mActivitiesMimetypeQuery, 1, activityId);
+ String mimetype = mActivitiesMimetypeQuery.simpleQueryForString();
+ return mimetype;
+ } catch (SQLiteDoneException e) {
+ // No valid mapping found, so return null
+ return null;
+ }
+ }
+
+ /**
+ * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts.
+ */
+ public void updateAllVisible() {
+ final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
+ mVisibleAllUpdate.bindLong(1, groupMembershipMimetypeId);
+ mVisibleAllUpdate.execute();
+ }
+
+ /**
+ * Update {@link Contacts#IN_VISIBLE_GROUP} for a specific contact.
+ */
+ public void updateContactVisible(long aggId) {
+ final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
+ mVisibleSpecificUpdate.bindLong(1, groupMembershipMimetypeId);
+ mVisibleSpecificUpdate.bindLong(2, aggId);
+ mVisibleSpecificUpdate.execute();
+ }
+
+ /**
+ * Updates the contact ID for the specified contact.
+ */
+ public void setContactId(long rawContactId, long contactId) {
+ getWritableDatabase();
+ DatabaseUtils.bindObjectToProgram(mContactIdUpdate, 1, contactId);
+ DatabaseUtils.bindObjectToProgram(mContactIdUpdate, 2, rawContactId);
+ mContactIdUpdate.execute();
+ }
+
+ /**
+ * Returns contact ID for the given contact or zero if it is NULL.
+ */
+ public long getContactId(long rawContactId) {
+ getReadableDatabase();
+ try {
+ DatabaseUtils.bindObjectToProgram(mContactIdQuery, 1, rawContactId);
+ return mContactIdQuery.simpleQueryForLong();
+ } catch (SQLiteDoneException e) {
+ // No valid mapping found, so return -1
+ return 0;
+ }
+ }
+
+ public int getAggregationMode(long rawContactId) {
+ getReadableDatabase();
+ try {
+ DatabaseUtils.bindObjectToProgram(mAggregationModeQuery, 1, rawContactId);
+ return (int)mAggregationModeQuery.simpleQueryForLong();
+ } catch (SQLiteDoneException e) {
+ // No valid row found, so return "disabled"
+ return RawContacts.AGGREGATION_MODE_DISABLED;
+ }
+ }
+
+ /**
+ * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
+ */
+ public void insertNameLookup(long rawContactId, int lookupType, String name) {
+ getWritableDatabase();
+ DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 1, rawContactId);
+ DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 2, lookupType);
+ DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 3, name);
+ mNameLookupInsert.executeInsert();
+ }
+
+ public static void buildPhoneLookupQuery(SQLiteQueryBuilder qb, final String number) {
+ final String normalizedNumber = PhoneNumberUtils.toCallerIDMinMatch(number);
+ final StringBuilder tables = new StringBuilder();
+ tables.append(Tables.RAW_CONTACTS + ", (SELECT data_id FROM phone_lookup "
+ + "WHERE (phone_lookup.normalized_number GLOB '");
+ tables.append(normalizedNumber);
+ tables.append("*')) AS lookup, " + Tables.DATA_JOIN_MIMETYPES);
+ qb.setTables(tables.toString());
+ qb.appendWhere("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id AND ");
+ qb.appendWhere("PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
+ qb.appendWhereEscapeString(number);
+ qb.appendWhere(")");
+ }
+
+
+ /**
+ * Loads common nickname mappings into the database.
+ */
+ private void loadNicknameLookupTable(SQLiteDatabase db) {
+ String[] strings = mContext.getResources().getStringArray(
+ com.android.internal.R.array.common_nicknames);
+ if (strings == null || strings.length == 0) {
+ return;
+ }
+
+ SQLiteStatement nicknameLookupInsert = db.compileStatement("INSERT INTO "
+ + Tables.NICKNAME_LOOKUP + "(" + NicknameLookupColumns.NAME + ","
+ + NicknameLookupColumns.CLUSTER + ") VALUES (?,?)");
+
+ for (int clusterId = 0; clusterId < strings.length; clusterId++) {
+ String[] names = strings[clusterId].split(",");
+ for (int j = 0; j < names.length; j++) {
+ String name = NameNormalizer.normalize(names[j]);
+ try {
+ DatabaseUtils.bindObjectToProgram(nicknameLookupInsert, 1, name);
+ DatabaseUtils.bindObjectToProgram(nicknameLookupInsert, 2,
+ String.valueOf(clusterId));
+ nicknameLookupInsert.executeInsert();
+ } catch (SQLiteException e) {
+
+ // Print the exception and keep going - this is not a fatal error
+ Log.e(TAG, "Cannot insert nickname: " + names[j], e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns common nickname cluster IDs for a given name. For example, it
+ * will return the same value for "Robert", "Bob" and "Rob". Some names belong to multiple
+ * clusters, e.g. Leo could be Leonard or Leopold.
+ *
+ * May return null.
+ *
+ * @param normalizedName A normalized first name, see {@link NameNormalizer#normalize}.
+ */
+ public String[] getCommonNicknameClusters(String normalizedName) {
+ if (mNicknameClusterCache == null) {
+ mNicknameClusterCache = new HashMap<String, String[]>();
+ }
+
+ synchronized (mNicknameClusterCache) {
+ if (mNicknameClusterCache.containsKey(normalizedName)) {
+ return mNicknameClusterCache.get(normalizedName);
+ }
+ }
+
+ String[] clusters = null;
+ SQLiteDatabase db = getReadableDatabase();
+
+ Cursor cursor = db.query(Tables.NICKNAME_LOOKUP, NICKNAME_LOOKUP_COLUMNS,
+ NicknameLookupColumns.NAME + "=?", new String[] { normalizedName },
+ null, null, null);
+ try {
+ int count = cursor.getCount();
+ if (count > 0) {
+ clusters = new String[count];
+ for (int i = 0; i < count; i++) {
+ cursor.moveToNext();
+ clusters[i] = cursor.getString(COL_NICKNAME_LOOKUP_CLUSTER);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+
+ synchronized (mNicknameClusterCache) {
+ mNicknameClusterCache.put(normalizedName, clusters);
+ }
+
+ return clusters;
+ }
+
+ public static void copyStringValue(ContentValues toValues, String toKey,
+ ContentValues fromValues, String fromKey) {
+ if (fromValues.containsKey(fromKey)) {
+ toValues.put(toKey, fromValues.getAsString(fromKey));
+ }
+ }
+
+ public static void copyLongValue(ContentValues toValues, String toKey,
+ ContentValues fromValues, String fromKey) {
+ if (fromValues.containsKey(fromKey)) {
+ long longValue;
+ Object value = fromValues.get(fromKey);
+ if (value instanceof Boolean) {
+ if ((Boolean)value) {
+ longValue = 1;
+ } else {
+ longValue = 0;
+ }
+ } else {
+ longValue = ((Number) value).longValue();
+ }
+ toValues.put(toKey, longValue);
+ }
+ }
+
+ public SyncStateContentProviderHelper getSyncState() {
+ return mSyncState;
+ }
+
+ /**
+ * Delete the aggregate contact if it has no constituent raw contacts other
+ * than the supplied one.
+ */
+ public void removeContactIfSingleton(long rawContactId) {
+ SQLiteDatabase db = getWritableDatabase();
+
+ // Obtain contact ID from the supplied raw contact ID
+ String contactIdFromRawContactId = "(SELECT " + RawContacts.CONTACT_ID + " FROM "
+ + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=" + rawContactId + ")";
+
+ // Find other raw contacts in the same aggregate contact
+ String otherRawContacts = "(SELECT contacts1." + RawContacts._ID + " FROM "
+ + Tables.RAW_CONTACTS + " contacts1 JOIN " + Tables.RAW_CONTACTS + " contacts2 ON ("
+ + "contacts1." + RawContacts.CONTACT_ID + "=contacts2." + RawContacts.CONTACT_ID
+ + ") WHERE contacts1." + RawContacts._ID + "!=" + rawContactId + ""
+ + " AND contacts2." + RawContacts._ID + "=" + rawContactId + ")";
+
+ db.execSQL("DELETE FROM " + Tables.CONTACTS
+ + " WHERE " + Contacts._ID + "=" + contactIdFromRawContactId
+ + " AND NOT EXISTS " + otherRawContacts + ";");
+ }
+}
diff --git a/src/com/android/providers/contacts/ReorderingCursorWrapper.java b/src/com/android/providers/contacts/ReorderingCursorWrapper.java
new file mode 100644
index 0000000..5d7983a
--- /dev/null
+++ b/src/com/android/providers/contacts/ReorderingCursorWrapper.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import android.database.AbstractCursor;
+import android.database.Cursor;
+
+/**
+ * Cursor wrapper that reorders rows according to supplied specific position mapping.
+ */
+public class ReorderingCursorWrapper extends AbstractCursor {
+
+ private final Cursor mCursor;
+ private final int[] mPositionMap;
+
+ /**
+ * Constructor.
+ *
+ * @param cursor wrapped cursor
+ * @param positionMap maps wrapper cursor positions to wrapped cursor positions
+ * so that positionMap[wrapperPosition] == wrappedPosition
+ */
+ public ReorderingCursorWrapper(Cursor cursor, int[] positionMap) {
+ if (cursor.getCount() != positionMap.length) {
+ throw new IllegalArgumentException("Cursor and position map have different sizes.");
+ }
+
+ mCursor = cursor;
+ mPositionMap = positionMap;
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ return mCursor.moveToPosition(mPositionMap[newPosition]);
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mCursor.getColumnNames();
+ }
+
+ @Override
+ public int getCount() {
+ return mCursor.getCount();
+ }
+
+ @Override
+ public double getDouble(int column) {
+ return mCursor.getDouble(column);
+ }
+
+ @Override
+ public float getFloat(int column) {
+ return mCursor.getFloat(column);
+ }
+
+ @Override
+ public int getInt(int column) {
+ return mCursor.getInt(column);
+ }
+
+ @Override
+ public long getLong(int column) {
+ return mCursor.getLong(column);
+ }
+
+ @Override
+ public short getShort(int column) {
+ return mCursor.getShort(column);
+ }
+
+ @Override
+ public String getString(int column) {
+ return mCursor.getString(column);
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ return mCursor.isNull(column);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/providers/contacts/SocialProvider.java b/src/com/android/providers/contacts/SocialProvider.java
new file mode 100644
index 0000000..a5ea0c9
--- /dev/null
+++ b/src/com/android/providers/contacts/SocialProvider.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts;
+
+import com.android.providers.contacts.OpenHelper.ActivitiesColumns;
+import com.android.providers.contacts.OpenHelper.PackagesColumns;
+import com.android.providers.contacts.OpenHelper.Tables;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.SocialContract;
+import android.provider.SocialContract.Activities;
+
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Social activity content provider. The contract between this provider and
+ * applications is defined in {@link SocialContract}.
+ */
+public class SocialProvider extends ContentProvider {
+ // TODO: clean up debug tag
+ private static final String TAG = "SocialProvider ~~~~";
+
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ private static final int ACTIVITIES = 1000;
+ private static final int ACTIVITIES_ID = 1001;
+ private static final int ACTIVITIES_AUTHORED_BY = 1002;
+
+ private static final int CONTACT_STATUS_ID = 3000;
+
+ private static final String DEFAULT_SORT_ORDER = Activities.THREAD_PUBLISHED + " DESC, "
+ + Activities.PUBLISHED + " ASC";
+
+ /** Contains just the contacts columns */
+ private static final HashMap<String, String> sContactsProjectionMap;
+ /** Contains just the contacts columns */
+ private static final HashMap<String, String> sRawContactsProjectionMap;
+ /** Contains just the activities columns */
+ private static final HashMap<String, String> sActivitiesProjectionMap;
+
+ /** Contains the activities, raw contacts, and contacts columns, for joined tables */
+ private static final HashMap<String, String> sActivitiesContactsProjectionMap;
+
+ static {
+ // Contacts URI matching table
+ final UriMatcher matcher = sUriMatcher;
+
+ matcher.addURI(SocialContract.AUTHORITY, "activities", ACTIVITIES);
+ matcher.addURI(SocialContract.AUTHORITY, "activities/#", ACTIVITIES_ID);
+ matcher.addURI(SocialContract.AUTHORITY, "activities/authored_by/#", ACTIVITIES_AUTHORED_BY);
+
+ matcher.addURI(SocialContract.AUTHORITY, "contact_status/#", CONTACT_STATUS_ID);
+
+ HashMap<String, String> columns;
+
+ // Contacts projection map
+ columns = new HashMap<String, String>();
+ columns.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
+ sContactsProjectionMap = columns;
+
+ // Contacts projection map
+ columns = new HashMap<String, String>();
+ columns.put(RawContacts._ID, Tables.RAW_CONTACTS + "." + RawContacts._ID + " AS _id");
+ columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
+ sRawContactsProjectionMap = columns;
+
+ // Activities projection map
+ columns = new HashMap<String, String>();
+ columns.put(Activities._ID, "activities._id AS _id");
+ columns.put(Activities.RES_PACKAGE, PackagesColumns.PACKAGE + " AS "
+ + Activities.RES_PACKAGE);
+ columns.put(Activities.MIMETYPE, Activities.MIMETYPE);
+ columns.put(Activities.RAW_ID, Activities.RAW_ID);
+ columns.put(Activities.IN_REPLY_TO, Activities.IN_REPLY_TO);
+ columns.put(Activities.AUTHOR_CONTACT_ID, Activities.AUTHOR_CONTACT_ID);
+ columns.put(Activities.TARGET_CONTACT_ID, Activities.TARGET_CONTACT_ID);
+ columns.put(Activities.PUBLISHED, Activities.PUBLISHED);
+ columns.put(Activities.THREAD_PUBLISHED, Activities.THREAD_PUBLISHED);
+ columns.put(Activities.TITLE, Activities.TITLE);
+ columns.put(Activities.SUMMARY, Activities.SUMMARY);
+ columns.put(Activities.LINK, Activities.LINK);
+ columns.put(Activities.THUMBNAIL, Activities.THUMBNAIL);
+ sActivitiesProjectionMap = columns;
+
+ // Activities, raw contacts, and contacts projection map for joins
+ columns = new HashMap<String, String>();
+ columns.putAll(sContactsProjectionMap);
+ columns.putAll(sRawContactsProjectionMap);
+ columns.putAll(sActivitiesProjectionMap); // Final _id will be from Activities
+ sActivitiesContactsProjectionMap = columns;
+
+ }
+
+ private OpenHelper mOpenHelper;
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onCreate() {
+ final Context context = getContext();
+ mOpenHelper = OpenHelper.getInstance(context);
+
+ // TODO remove this, it's here to force opening the database on boot for testing
+ mOpenHelper.getReadableDatabase();
+
+ return true;
+ }
+
+ /**
+ * Called when a change has been made.
+ *
+ * @param uri the uri that the change was made to
+ */
+ private void onChange(Uri uri) {
+ getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isTemporary() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ final int match = sUriMatcher.match(uri);
+ long id = 0;
+ switch (match) {
+ case ACTIVITIES: {
+ id = insertActivity(values);
+ break;
+ }
+
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+
+ final Uri result = ContentUris.withAppendedId(Activities.CONTENT_URI, id);
+ onChange(result);
+ return result;
+ }
+
+ /**
+ * Inserts an item into the {@link Tables#ACTIVITIES} table.
+ *
+ * @param values the values for the new row
+ * @return the row ID of the newly created row
+ */
+ private long insertActivity(ContentValues values) {
+
+ // TODO verify that IN_REPLY_TO != RAW_ID
+
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ long id = 0;
+ db.beginTransaction();
+ try {
+ // TODO: Consider enforcing Binder.getCallingUid() for package name
+ // requested by this insert.
+
+ // Replace package name and mime-type with internal mappings
+ final String packageName = values.getAsString(Activities.RES_PACKAGE);
+ if (packageName != null) {
+ values.put(ActivitiesColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName));
+ }
+ values.remove(Activities.RES_PACKAGE);
+
+ final String mimeType = values.getAsString(Activities.MIMETYPE);
+ values.put(ActivitiesColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType));
+ values.remove(Activities.MIMETYPE);
+
+ long published = values.getAsLong(Activities.PUBLISHED);
+ long threadPublished = published;
+
+ String inReplyTo = values.getAsString(Activities.IN_REPLY_TO);
+ if (inReplyTo != null) {
+ threadPublished = getThreadPublished(db, inReplyTo, published);
+ }
+
+ values.put(Activities.THREAD_PUBLISHED, threadPublished);
+
+ // Insert the data row itself
+ id = db.insert(Tables.ACTIVITIES, Activities.RAW_ID, values);
+
+ // Adjust thread timestamps on replies that have already been inserted
+ if (values.containsKey(Activities.RAW_ID)) {
+ adjustReplyTimestamps(db, values.getAsString(Activities.RAW_ID), published);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ return id;
+ }
+
+ /**
+ * Finds the timestamp of the original message in the thread. If not found, returns
+ * {@code defaultValue}.
+ */
+ private long getThreadPublished(SQLiteDatabase db, String rawId, long defaultValue) {
+ String inReplyTo = null;
+ long threadPublished = defaultValue;
+
+ final Cursor c = db.query(Tables.ACTIVITIES,
+ new String[]{Activities.IN_REPLY_TO, Activities.PUBLISHED},
+ Activities.RAW_ID + " = ?", new String[]{rawId}, null, null, null);
+ try {
+ if (c.moveToFirst()) {
+ inReplyTo = c.getString(0);
+ threadPublished = c.getLong(1);
+ }
+ } finally {
+ c.close();
+ }
+
+ if (inReplyTo != null) {
+
+ // Call recursively to obtain the original timestamp of the entire thread
+ return getThreadPublished(db, inReplyTo, threadPublished);
+ }
+
+ return threadPublished;
+ }
+
+ /**
+ * In case the original message of a thread arrives after its reply messages, we need
+ * to check if there are any replies in the database and if so adjust their thread_published.
+ */
+ private void adjustReplyTimestamps(SQLiteDatabase db, String inReplyTo, long threadPublished) {
+
+ ContentValues values = new ContentValues();
+ values.put(Activities.THREAD_PUBLISHED, threadPublished);
+
+ /*
+ * Issuing an exploratory update. If it updates nothing, we are done. Otherwise,
+ * we will run a query to find the updated records again and repeat recursively.
+ */
+ int replies = db.update(Tables.ACTIVITIES, values,
+ Activities.IN_REPLY_TO + "= ?", new String[] {inReplyTo});
+
+ if (replies == 0) {
+ return;
+ }
+
+ /*
+ * Presumably this code will be executed very infrequently since messages tend to arrive
+ * in the order they get sent.
+ */
+ ArrayList<String> rawIds = new ArrayList<String>(replies);
+ final Cursor c = db.query(Tables.ACTIVITIES,
+ new String[]{Activities.RAW_ID},
+ Activities.IN_REPLY_TO + " = ?", new String[] {inReplyTo}, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ rawIds.add(c.getString(0));
+ }
+ } finally {
+ c.close();
+ }
+
+ for (String rawId : rawIds) {
+ adjustReplyTimestamps(db, rawId, threadPublished);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case ACTIVITIES_ID: {
+ final long activityId = ContentUris.parseId(uri);
+ return db.delete(Tables.ACTIVITIES, Activities._ID + "=" + activityId, null);
+ }
+
+ case ACTIVITIES_AUTHORED_BY: {
+ final long contactId = ContentUris.parseId(uri);
+ return db.delete(Tables.ACTIVITIES, Activities.AUTHOR_CONTACT_ID + "=" + contactId, null);
+ }
+
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ String limit = null;
+
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case ACTIVITIES: {
+ qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
+ qb.setProjectionMap(sActivitiesContactsProjectionMap);
+ break;
+ }
+
+ case ACTIVITIES_ID: {
+ // TODO: enforce that caller has read access to this data
+ long activityId = ContentUris.parseId(uri);
+ qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
+ qb.setProjectionMap(sActivitiesContactsProjectionMap);
+ qb.appendWhere(Activities._ID + "=" + activityId);
+ break;
+ }
+
+ case ACTIVITIES_AUTHORED_BY: {
+ long contactId = ContentUris.parseId(uri);
+ qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
+ qb.setProjectionMap(sActivitiesContactsProjectionMap);
+ qb.appendWhere(Activities.AUTHOR_CONTACT_ID + "=" + contactId);
+ break;
+ }
+
+ case CONTACT_STATUS_ID: {
+ long aggId = ContentUris.parseId(uri);
+ qb.setTables(Tables.ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS);
+ qb.setProjectionMap(sActivitiesContactsProjectionMap);
+
+ // Latest status of a contact is any top-level status
+ // authored by one of its children contacts.
+ qb.appendWhere(Activities.IN_REPLY_TO + " IS NULL AND ");
+ qb.appendWhere(Activities.AUTHOR_CONTACT_ID + " IN (SELECT " + BaseColumns._ID
+ + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
+ + aggId + ")");
+ sortOrder = Activities.PUBLISHED + " DESC";
+ limit = "1";
+ break;
+ }
+
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+
+ // Default to reverse-chronological sort if nothing requested
+ if (sortOrder == null) {
+ sortOrder = DEFAULT_SORT_ORDER;
+ }
+
+ // Perform the query and set the notification uri
+ final Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit);
+ if (c != null) {
+ c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
+ }
+ return c;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case ACTIVITIES:
+ case ACTIVITIES_AUTHORED_BY:
+ return Activities.CONTENT_TYPE;
+ case ACTIVITIES_ID:
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ long activityId = ContentUris.parseId(uri);
+ return mOpenHelper.getActivityMimeType(activityId);
+ case CONTACT_STATUS_ID:
+ return Contacts.CONTENT_ITEM_TYPE;
+ }
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..ec48f5a
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,17 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := ContactsProviderTests
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_INSTRUMENTATION_FOR := ContactsProvider
+LOCAL_CERTIFICATE := shared
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..5063a7d
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.contacts.tests"
+ android:sharedUserId="android.uid.shared">
+
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+ <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <!--
+ The test delcared in this instrumentation will be run along with tests declared by
+ all other applications via the command: "adb shell itr".
+ The "itr" command will find all tests declared by all applications. If you want to run just these
+ tests on their own then use the command:
+ "adb shell am instrument -w com.android.providers.contacts.tests/android.test.InstrumentationTestRunner"
+ -->
+ <instrumentation android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.providers.contacts"
+ android:label="Contacts Provider Tests">
+ </instrumentation>
+
+</manifest>
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
new file mode 100644
index 0000000..e4186d7
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -0,0 +1,582 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
+
+import android.accounts.Account;
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A common superclass for {@link ContactsProvider2}-related tests.
+ */
+@LargeTest
+public abstract class BaseContactsProvider2Test extends AndroidTestCase {
+
+ protected static final String PACKAGE = "ContactsProvider2Test";
+
+ protected ContactsActor mActor;
+ protected MockContentResolver mResolver;
+ protected Account mAccount = new Account("account1", "account type1");
+
+ protected final static Long NO_LONG = new Long(0);
+ protected final static String NO_STRING = new String("");
+ protected final static Account NO_ACCOUNT = new Account("a", "b");
+
+ protected Class<? extends ContentProvider> getProviderClass() {
+ return SynchronousContactsProvider2.class;
+ }
+
+ protected String getAuthority() {
+ return ContactsContract.AUTHORITY;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mActor = new ContactsActor(getContext(), PACKAGE_GREY, getProviderClass(), getAuthority());
+ mResolver = mActor.resolver;
+ if (mActor.provider instanceof SynchronousContactsProvider2) {
+ ((SynchronousContactsProvider2) mActor.provider)
+ .getOpenHelper(mActor.context).wipeData();
+ }
+ }
+
+ public Context getMockContext() {
+ return mActor.context;
+ }
+
+ public void addAuthority(String authority) {
+ mActor.addAuthority(authority);
+ }
+
+ public ContentProvider addProvider(Class<? extends ContentProvider> providerClass,
+ String authority) throws Exception {
+ return mActor.addProvider(providerClass, authority);
+ }
+
+ protected Uri maybeAddAccountQueryParameters(Uri uri, Account account) {
+ if (account == null) {
+ return uri;
+ }
+ return uri.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.mName)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.mType)
+ .build();
+ }
+
+ protected long createRawContact() {
+ return createRawContact(null);
+ }
+
+ protected long createRawContactWithName() {
+ long rawContactId = createRawContact(null);
+ insertStructuredName(rawContactId, "John", "Doe");
+ return rawContactId;
+ }
+
+ protected long createRawContact(Account account, String... extras) {
+ ContentValues values = new ContentValues();
+ for (int i = 0; i < extras.length; ) {
+ values.put(extras[i], extras[i + 1]);
+ i += 2;
+ }
+ final Uri uri = maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, account);
+ Uri contactUri = mResolver.insert(uri, values);
+ return ContentUris.parseId(contactUri);
+ }
+
+ protected long createGroup(Account account, String sourceId, String title) {
+ ContentValues values = new ContentValues();
+ values.put(Groups.SOURCE_ID, sourceId);
+ values.put(Groups.TITLE, title);
+ final Uri uri = maybeAddAccountQueryParameters(Groups.CONTENT_URI, account);
+ return ContentUris.parseId(mResolver.insert(uri, values));
+ }
+
+ protected Uri insertStructuredName(long rawContactId, String givenName, String familyName) {
+ ContentValues values = new ContentValues();
+ StringBuilder sb = new StringBuilder();
+ if (givenName != null) {
+ sb.append(givenName);
+ }
+ if (givenName != null && familyName != null) {
+ sb.append(" ");
+ }
+ if (familyName != null) {
+ sb.append(familyName);
+ }
+ values.put(StructuredName.DISPLAY_NAME, sb.toString());
+ values.put(StructuredName.GIVEN_NAME, givenName);
+ values.put(StructuredName.FAMILY_NAME, familyName);
+
+ return insertStructuredName(rawContactId, values);
+ }
+
+ protected Uri insertStructuredName(long rawContactId, ContentValues values) {
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+ return resultUri;
+ }
+
+ protected Uri insertPhoneNumber(long rawContactId, String phoneNumber) {
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ values.put(Phone.NUMBER, phoneNumber);
+ values.put(Phone.TYPE, Phone.TYPE_HOME);
+
+ Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+ return resultUri;
+ }
+
+ protected Uri insertEmail(long rawContactId, String email) {
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ values.put(Email.DATA, email);
+ values.put(Email.TYPE, Email.TYPE_HOME);
+
+ Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+ return resultUri;
+ }
+
+ protected Uri insertNickname(long rawContactId, String nickname) {
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
+ values.put(Nickname.NAME, nickname);
+ values.put(Nickname.TYPE, Nickname.TYPE_OTHER_NAME);
+
+ Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+ return resultUri;
+ }
+
+ protected Uri insertPhoto(long rawContactId) {
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+
+ Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+ return resultUri;
+ }
+
+ protected Uri insertGroupMembership(long rawContactId, String sourceId) {
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
+ values.put(GroupMembership.GROUP_SOURCE_ID, sourceId);
+ return mResolver.insert(Data.CONTENT_URI, values);
+ }
+
+ protected Uri insertGroupMembership(long rawContactId, Long groupId) {
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
+ values.put(GroupMembership.GROUP_ROW_ID, groupId);
+ return mResolver.insert(Data.CONTENT_URI, values);
+ }
+
+ protected Uri insertPresence(int protocol, String handle, int presence) {
+ ContentValues values = new ContentValues();
+ values.put(Presence.IM_PROTOCOL, protocol);
+ values.put(Presence.IM_HANDLE, handle);
+ values.put(Presence.PRESENCE_STATUS, presence);
+
+ Uri resultUri = mResolver.insert(Presence.CONTENT_URI, values);
+ return resultUri;
+ }
+
+ protected Uri insertImHandle(long rawContactId, int protocol, String handle) {
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+ values.put(Im.PROTOCOL, protocol);
+ values.put(Im.DATA, handle);
+ values.put(Im.TYPE, Im.TYPE_HOME);
+
+ Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+ return resultUri;
+ }
+
+ protected void setContactAccountName(long rawContactId, String accountName) {
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, accountName);
+
+ mResolver.update(ContentUris.withAppendedId(
+ RawContacts.CONTENT_URI, rawContactId), values, null, null);
+ }
+
+ protected void setAggregationException(int type, long contactId, long rawContactId) {
+ ContentValues values = new ContentValues();
+ values.put(AggregationExceptions.CONTACT_ID, contactId);
+ values.put(AggregationExceptions.RAW_CONTACT_ID, rawContactId);
+ values.put(AggregationExceptions.TYPE, type);
+ mResolver.update(AggregationExceptions.CONTENT_URI, values, null, null);
+ }
+
+ protected Cursor queryRawContact(long rawContactId) {
+ return mResolver.query(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), null,
+ null, null, null);
+ }
+
+ protected Cursor queryContact(long contactId) {
+ return mResolver.query(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
+ null, null, null, null);
+ }
+
+ protected Cursor queryContactSummary(long contactId, String[] projection) {
+ return mResolver.query(ContentUris.withAppendedId(Contacts.CONTENT_SUMMARY_URI,
+ contactId), projection, null, null, null);
+ }
+
+ protected Cursor queryContactSummary() {
+ return mResolver.query(Contacts.CONTENT_SUMMARY_URI, null, null, null, null);
+ }
+
+ protected long queryContactId(long rawContactId) {
+ Cursor c = queryRawContact(rawContactId);
+ assertTrue(c.moveToFirst());
+ long contactId = c.getLong(c.getColumnIndex(RawContacts.CONTACT_ID));
+ c.close();
+ return contactId;
+ }
+
+ protected long queryPhotoId(long contactId) {
+ Cursor c = queryContact(contactId);
+ assertTrue(c.moveToFirst());
+ long photoId = c.getInt(c.getColumnIndex(Contacts.PHOTO_ID));
+ c.close();
+ return photoId;
+ }
+
+ protected String queryDisplayName(long contactId) {
+ Cursor c = queryContact(contactId);
+ assertTrue(c.moveToFirst());
+ String displayName = c.getString(c.getColumnIndex(Contacts.DISPLAY_NAME));
+ c.close();
+ return displayName;
+ }
+
+ protected void assertAggregated(long rawContactId1, long rawContactId2) {
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ assertTrue(contactId1 == contactId2);
+ }
+
+ protected void assertAggregated(long rawContactId1, long rawContactId2,
+ String expectedDisplayName) {
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ assertTrue(contactId1 == contactId2);
+
+ String displayName = queryDisplayName(contactId1);
+ assertEquals(expectedDisplayName, displayName);
+ }
+
+ protected void assertNotAggregated(long rawContactId1, long rawContactId2) {
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ assertTrue(contactId1 != contactId2);
+ }
+
+ protected void assertStructuredName(long rawContactId, String prefix, String givenName,
+ String middleName, String familyName, String suffix) {
+ Uri uri =
+ Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+ RawContacts.Data.CONTENT_DIRECTORY);
+
+ final String[] projection = new String[] {
+ StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME,
+ StructuredName.FAMILY_NAME, StructuredName.SUFFIX
+ };
+
+ Cursor c = mResolver.query(uri, projection, Data.MIMETYPE + "='"
+ + StructuredName.CONTENT_ITEM_TYPE + "'", null, null);
+
+ assertTrue(c.moveToFirst());
+ assertEquals(prefix, c.getString(0));
+ assertEquals(givenName, c.getString(1));
+ assertEquals(middleName, c.getString(2));
+ assertEquals(familyName, c.getString(3));
+ assertEquals(suffix, c.getString(4));
+ c.close();
+ }
+
+ protected long assertSingleGroup(Long rowId, Account account, String sourceId, String title) {
+ Cursor c = mResolver.query(Groups.CONTENT_URI, null, null, null, null);
+ try {
+ assertTrue(c.moveToNext());
+ long actualRowId = assertGroup(c, rowId, account, sourceId, title);
+ assertFalse(c.moveToNext());
+ return actualRowId;
+ } finally {
+ c.close();
+ }
+ }
+
+ protected long assertSingleGroupMembership(Long rowId, Long rawContactId, Long groupRowId,
+ String sourceId) {
+ Cursor c = mResolver.query(ContactsContract.Data.CONTENT_URI, null, null, null, null);
+ try {
+ assertTrue(c.moveToNext());
+ long actualRowId = assertGroupMembership(c, rowId, rawContactId, groupRowId, sourceId);
+ assertFalse(c.moveToNext());
+ return actualRowId;
+ } finally {
+ c.close();
+ }
+ }
+
+ protected long assertGroupMembership(Cursor c, Long rowId, Long rawContactId, Long groupRowId,
+ String sourceId) {
+ assertNullOrEquals(c, rowId, Data._ID);
+ assertNullOrEquals(c, rawContactId, GroupMembership.RAW_CONTACT_ID);
+ assertNullOrEquals(c, groupRowId, GroupMembership.GROUP_ROW_ID);
+ assertNullOrEquals(c, sourceId, GroupMembership.GROUP_SOURCE_ID);
+ return c.getLong(c.getColumnIndexOrThrow("_id"));
+ }
+
+ protected long assertGroup(Cursor c, Long rowId, Account account, String sourceId, String title) {
+ assertNullOrEquals(c, rowId, Groups._ID);
+ assertNullOrEquals(c, account);
+ assertNullOrEquals(c, sourceId, Groups.SOURCE_ID);
+ assertNullOrEquals(c, title, Groups.TITLE);
+ return c.getLong(c.getColumnIndexOrThrow("_id"));
+ }
+
+ private void assertNullOrEquals(Cursor c, Account account) {
+ if (account == NO_ACCOUNT) {
+ return;
+ }
+ if (account == null) {
+ assertTrue(c.isNull(c.getColumnIndexOrThrow(Groups.ACCOUNT_NAME)));
+ assertTrue(c.isNull(c.getColumnIndexOrThrow(Groups.ACCOUNT_TYPE)));
+ } else {
+ assertEquals(account.mName, c.getString(c.getColumnIndexOrThrow(Groups.ACCOUNT_NAME)));
+ assertEquals(account.mType, c.getString(c.getColumnIndexOrThrow(Groups.ACCOUNT_TYPE)));
+ }
+ }
+
+ private void assertNullOrEquals(Cursor c, Long value, String columnName) {
+ if (value != NO_LONG) {
+ if (value == null) assertTrue(c.isNull(c.getColumnIndexOrThrow(columnName)));
+ else assertEquals((long) value, c.getLong(c.getColumnIndexOrThrow(columnName)));
+ }
+ }
+
+ private void assertNullOrEquals(Cursor c, String value, String columnName) {
+ if (value != NO_STRING) {
+ if (value == null) assertTrue(c.isNull(c.getColumnIndexOrThrow(columnName)));
+ else assertEquals(value, c.getString(c.getColumnIndexOrThrow(columnName)));
+ }
+ }
+
+ protected void assertDataRow(ContentValues actual, String expectedMimetype,
+ Object... expectedArguments) {
+ assertEquals(actual.toString(), expectedMimetype, actual.getAsString(Data.MIMETYPE));
+ for (int i = 0; i < expectedArguments.length; i += 2) {
+ String columnName = (String) expectedArguments[i];
+ Object expectedValue = expectedArguments[i + 1];
+ if (expectedValue instanceof Uri) {
+ expectedValue = ContentUris.parseId((Uri) expectedValue);
+ }
+ if (expectedValue == null) {
+ assertNull(actual.toString(), actual.get(columnName));
+ }
+ if (expectedValue instanceof Long) {
+ assertEquals("mismatch at " + columnName + " from " + actual.toString(),
+ expectedValue, actual.getAsLong(columnName));
+ } else if (expectedValue instanceof Integer) {
+ assertEquals("mismatch at " + columnName + " from " + actual.toString(),
+ expectedValue, actual.getAsInteger(columnName));
+ } else if (expectedValue instanceof String) {
+ assertEquals("mismatch at " + columnName + " from " + actual.toString(),
+ expectedValue, actual.getAsString(columnName));
+ } else {
+ assertEquals("mismatch at " + columnName + " from " + actual.toString(),
+ expectedValue, actual.get(columnName));
+ }
+ }
+ }
+
+ protected static class IdComparator implements Comparator<ContentValues> {
+ public int compare(ContentValues o1, ContentValues o2) {
+ long id1 = o1.getAsLong(ContactsContract.Data._ID);
+ long id2 = o2.getAsLong(ContactsContract.Data._ID);
+ if (id1 == id2) return 0;
+ return (id1 < id2) ? -1 : 1;
+ }
+ }
+
+ protected ContentValues[] asSortedContentValuesArray(
+ ArrayList<Entity.NamedContentValues> subValues) {
+ ContentValues[] result = new ContentValues[subValues.size()];
+ int i = 0;
+ for (Entity.NamedContentValues subValue : subValues) {
+ result[i] = subValue.values;
+ i++;
+ }
+ Arrays.sort(result, new IdComparator());
+ return result;
+ }
+
+ protected void assertDirty(Uri uri, boolean state) {
+ Cursor c = mResolver.query(uri, new String[]{"dirty"}, null, null, null);
+ assertTrue(c.moveToNext());
+ assertEquals(state, c.getLong(0) != 0);
+ assertFalse(c.moveToNext());
+ }
+
+ protected long getVersion(Uri uri) {
+ Cursor c = mResolver.query(uri, new String[]{"version"}, null, null, null);
+ assertTrue(c.moveToNext());
+ long version = c.getLong(0);
+ assertFalse(c.moveToNext());
+ return version;
+ }
+
+ protected void clearDirty(Uri uri) {
+ ContentValues values = new ContentValues();
+ values.put("dirty", 0);
+ mResolver.update(uri, values, null, null);
+ }
+
+ protected void assertStoredValues(Uri rowUri, String column, String expectedValue) {
+ String value = getStoredValue(rowUri, column);
+ assertEquals("Column value " + column, expectedValue, value);
+ }
+
+ protected String getStoredValue(Uri rowUri, String column) {
+ String value;
+ Cursor c = mResolver.query(rowUri, new String[] { column }, null, null, null);
+ try {
+ c.moveToFirst();
+ value = c.getString(c.getColumnIndex(column));
+ } finally {
+ c.close();
+ }
+ return value;
+ }
+
+ protected void assertStoredValues(Uri rowUri, ContentValues expectedValues) {
+ Cursor c = mResolver.query(rowUri, null, null, null, null);
+ try {
+ assertEquals("Record count", 1, c.getCount());
+ c.moveToFirst();
+ assertCursorValues(c, expectedValues);
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Constructs a selection (where clause) out of all supplied values, uses it
+ * to query the provider and verifies that a single row is returned and it
+ * has the same values as requested.
+ */
+ protected void assertSelection(Uri uri, ContentValues values, String idColumn, long id) {
+ StringBuilder sb = new StringBuilder();
+ ArrayList<String> selectionArgs = new ArrayList<String>(values.size());
+ if (idColumn != null) {
+ sb.append(idColumn).append("=").append(id);
+ }
+ Set<Map.Entry<String, Object>> entries = values.valueSet();
+ for (Map.Entry<String, Object> entry : entries) {
+ String column = entry.getKey();
+ Object value = entry.getValue();
+ if (sb.length() != 0) {
+ sb.append(" AND ");
+ }
+ sb.append(column);
+ if (value == null) {
+ sb.append(" IS NULL");
+ } else {
+ sb.append("=?");
+ selectionArgs.add(String.valueOf(value));
+ }
+ }
+
+ Cursor c = mResolver.query(uri, null, sb.toString(), selectionArgs.toArray(new String[0]),
+ null);
+ try {
+ assertEquals("Record count", 1, c.getCount());
+ c.moveToFirst();
+ assertCursorValues(c, values);
+ } finally {
+ c.close();
+ }
+ }
+
+ protected void assertCursorValues(Cursor cursor, ContentValues expectedValues) {
+ Set<Map.Entry<String, Object>> entries = expectedValues.valueSet();
+ for (Map.Entry<String, Object> entry : entries) {
+ String column = entry.getKey();
+ int index = cursor.getColumnIndex(column);
+ assertTrue("No such column: " + column, index != -1);
+ Object expectedValue = expectedValues.get(column);
+ String value;
+ if (expectedValue instanceof byte[]) {
+ expectedValue = Hex.encodeHex((byte[])expectedValue, false);
+ value = Hex.encodeHex(cursor.getBlob(index), false);
+ } else {
+ expectedValue = expectedValues.getAsString(column);
+ value = cursor.getString(index);
+ }
+ assertEquals("Column value " + column, expectedValue, value);
+ }
+ }
+
+ protected int getCount(Uri uri, String selection, String[] selectionArgs) {
+ Cursor c = mResolver.query(uri, null, selection, selectionArgs, null);
+ try {
+ return c.getCount();
+ } finally {
+ c.close();
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
new file mode 100644
index 0000000..75ebda9
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.Connection;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.CallLog;
+import android.provider.ContactsContract;
+import android.provider.CallLog.Calls;
+import android.provider.Contacts.ContactMethods;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Unit tests for {@link CallLogProvider}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -w \
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class CallLogProviderTest extends BaseContactsProvider2Test {
+
+ private static final boolean USE_LEGACY_PROVIDER = false;
+
+ @Override
+ protected Class<? extends ContentProvider> getProviderClass() {
+ return USE_LEGACY_PROVIDER ? ContactsProvider.class : ContactsProvider2.class;
+ }
+
+ @Override
+ protected String getAuthority() {
+ return ContactsContract.AUTHORITY;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ if (USE_LEGACY_PROVIDER) {
+ addAuthority(CallLog.AUTHORITY);
+ } else {
+ addProvider(TestCallLogProvider.class, CallLog.AUTHORITY);
+ }
+ }
+
+ public void testInsert() {
+ ContentValues values = new ContentValues();
+ putCallValues(values);
+ Uri uri = mResolver.insert(Calls.CONTENT_URI, values);
+ assertStoredValues(uri, values);
+ assertSelection(uri, values, Calls._ID, ContentUris.parseId(uri));
+ }
+
+ public void testUpdate() {
+ ContentValues values = new ContentValues();
+ putCallValues(values);
+ Uri uri = mResolver.insert(Calls.CONTENT_URI, values);
+
+ values.clear();
+ values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
+ values.put(Calls.NUMBER, "1-800-263-7643");
+ values.put(Calls.DATE, 2000);
+ values.put(Calls.DURATION, 40);
+ values.put(Calls.CACHED_NAME, "1-800-GOOG-411");
+ values.put(Calls.CACHED_NUMBER_TYPE, ContactMethods.TYPE_CUSTOM);
+ values.put(Calls.CACHED_NUMBER_LABEL, "Directory");
+
+ int count = mResolver.update(uri, values, null, null);
+ assertEquals(1, count);
+ assertStoredValues(uri, values);
+ }
+
+ public void testDelete() {
+ ContentValues values = new ContentValues();
+ putCallValues(values);
+ Uri uri = mResolver.insert(Calls.CONTENT_URI, values);
+ try {
+ mResolver.delete(uri, null, null);
+ fail();
+ } catch (UnsupportedOperationException ex) {
+ // Expected
+ }
+
+ int count = mResolver.delete(Calls.CONTENT_URI, Calls._ID + "="
+ + ContentUris.parseId(uri), null);
+ assertEquals(1, count);
+ assertEquals(0, getCount(uri, null, null));
+ }
+
+ public void testCallLogFilter() {
+ ContentValues values = new ContentValues();
+ putCallValues(values);
+ mResolver.insert(Calls.CONTENT_URI, values);
+
+ Uri filterUri = Uri.withAppendedPath(Calls.CONTENT_FILTER_URI, "1-800-4664-411");
+ Cursor c = mResolver.query(filterUri, null, null, null, null);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ assertCursorValues(c, values);
+ c.close();
+
+ filterUri = Uri.withAppendedPath(Calls.CONTENT_FILTER_URI, "1-888-4664-411");
+ c = mResolver.query(filterUri, null, null, null, null);
+ assertEquals(0, c.getCount());
+ c.close();
+ }
+
+ public void testAddCall() {
+ CallerInfo ci = new CallerInfo();
+ ci.name = "1-800-GOOG-411";
+ ci.numberType = ContactMethods.TYPE_CUSTOM;
+ ci.numberLabel = "Directory";
+ Uri uri = Calls.addCall(ci, getMockContext(), "1-800-263-7643",
+ Connection.PRESENTATION_ALLOWED, Calls.OUTGOING_TYPE, 2000, 40);
+
+ ContentValues values = new ContentValues();
+ values.put(Calls.TYPE, Calls.OUTGOING_TYPE);
+ values.put(Calls.NUMBER, "1-800-263-7643");
+ values.put(Calls.DATE, 2000);
+ values.put(Calls.DURATION, 40);
+ values.put(Calls.CACHED_NAME, "1-800-GOOG-411");
+ values.put(Calls.CACHED_NUMBER_TYPE, ContactMethods.TYPE_CUSTOM);
+ values.put(Calls.CACHED_NUMBER_LABEL, "Directory");
+ assertStoredValues(uri, values);
+ }
+
+ private void putCallValues(ContentValues values) {
+ values.put(Calls.TYPE, Calls.INCOMING_TYPE);
+ values.put(Calls.NUMBER, "1-800-4664-411");
+ values.put(Calls.DATE, 1000);
+ values.put(Calls.DURATION, 30);
+ values.put(Calls.NEW, 1);
+ }
+
+ public static class TestCallLogProvider extends CallLogProvider {
+ private static OpenHelper mOpenHelper;
+
+ @Override
+ protected OpenHelper getOpenHelper(final Context context) {
+ if (mOpenHelper == null) {
+ mOpenHelper = new OpenHelper(context);
+ }
+ return mOpenHelper;
+ }
+ }
+}
+
diff --git a/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java b/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java
new file mode 100644
index 0000000..1a98d2a
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests from {@link ContactAggregationScheduler}.
+ */
+@SmallTest
+public class ContactAggregationSchedulerTest extends TestCase {
+
+ private TestContactAggregationScheduler mScheduler;
+
+ @Override
+ protected void setUp() throws Exception {
+ mScheduler = new TestContactAggregationScheduler();
+ }
+
+ public void testScheduleInitial() {
+ mScheduler.schedule();
+ assertEquals(1, mScheduler.mRunDelayed);
+ }
+
+ public void testScheduleTwiceRapidly() {
+ mScheduler.schedule();
+
+ mScheduler.mTime += ContactAggregationScheduler.AGGREGATION_DELAY / 2;
+ mScheduler.schedule();
+ assertEquals(2, mScheduler.mRunDelayed);
+ }
+
+ public void testScheduleTwiceExceedingMaxDelay() {
+ mScheduler.schedule();
+
+ mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
+ mScheduler.schedule();
+ assertEquals(1, mScheduler.mRunDelayed);
+ }
+
+ public void testScheduleWhileRunning() {
+ mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
+ boolean mInterruptCalled;
+
+ public void interrupt() {
+ mInterruptCalled = true;
+ }
+
+ public void run() {
+ mScheduler.schedule();
+ assertTrue(mInterruptCalled);
+ }
+ });
+
+ mScheduler.run();
+ assertEquals(1, mScheduler.mRunDelayed);
+ }
+
+ public void testScheduleWhileRunningExceedingMaxDelay() {
+ mScheduler.schedule();
+
+ mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
+
+ mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
+ boolean mInterruptCalled;
+
+ public void interrupt() {
+ mInterruptCalled = true;
+ }
+
+ public void run() {
+ mScheduler.schedule();
+ assertFalse(mInterruptCalled);
+ }
+ });
+
+ mScheduler.run();
+ assertEquals(2, mScheduler.mRunDelayed);
+ }
+
+ private static class TestContactAggregationScheduler extends ContactAggregationScheduler {
+
+ long mTime = 1000;
+ int mRunDelayed;
+
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void stop() {
+ }
+
+ @Override
+ long currentTime() {
+ return mTime;
+ }
+
+ @Override
+ void runDelayed() {
+ mRunDelayed++;
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
new file mode 100644
index 0000000..06dc197
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import android.content.ContentUris;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Unit tests for {@link ContactAggregator}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -w \
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class ContactAggregatorTest extends BaseContactsProvider2Test {
+
+ private static final String[] AGGREGATION_EXCEPTION_PROJECTION = new String[] {
+ AggregationExceptions.TYPE,
+ AggregationExceptions.CONTACT_ID,
+ AggregationExceptions.RAW_CONTACT_ID
+ };
+
+ public void testCrudAggregationExceptions() throws Exception {
+ long rawContactId1 = createRawContact();
+ long contactId = queryContactId(rawContactId1);
+ long rawContactId2 = createRawContact();
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_IN, contactId, rawContactId2);
+
+ // Refetch the row we have just inserted
+ Cursor c = mResolver.query(AggregationExceptions.CONTENT_URI,
+ AGGREGATION_EXCEPTION_PROJECTION, AggregationExceptions.CONTACT_ID + "="
+ + contactId, null, null);
+
+ assertTrue(c.moveToFirst());
+ assertEquals(AggregationExceptions.TYPE_KEEP_IN, c.getInt(0));
+ assertEquals(contactId, c.getLong(1));
+ assertEquals(rawContactId2, c.getLong(2));
+ assertFalse(c.moveToNext());
+ c.close();
+
+ // Change from TYPE_KEEP_IN to TYPE_KEEP_OUT
+ setAggregationException(AggregationExceptions.TYPE_KEEP_OUT, contactId, rawContactId2);
+
+ c = mResolver.query(AggregationExceptions.CONTENT_URI,
+ AGGREGATION_EXCEPTION_PROJECTION, AggregationExceptions.CONTACT_ID + "="
+ + contactId, null, null);
+
+ assertTrue(c.moveToFirst());
+ assertEquals(AggregationExceptions.TYPE_KEEP_OUT, c.getInt(0));
+ assertEquals(contactId, c.getLong(1));
+ assertEquals(rawContactId2, c.getLong(2));
+ assertFalse(c.moveToNext());
+ c.close();
+
+ // Delete the rule
+ setAggregationException(AggregationExceptions.TYPE_AUTOMATIC, contactId, rawContactId2);
+
+ // Verify that the row is gone
+ c = mResolver.query(AggregationExceptions.CONTENT_URI,
+ AGGREGATION_EXCEPTION_PROJECTION, AggregationExceptions.CONTACT_ID + "="
+ + contactId, null, null);
+ assertFalse(c.moveToFirst());
+ c.close();
+ }
+
+ public void testAggregationCreatesNewAggregate() {
+ long rawContactId = createRawContact();
+
+ Uri resultUri = insertStructuredName(rawContactId, "Johna", "Smitha");
+
+ // Parse the URI and confirm that it contains an ID
+ assertTrue(ContentUris.parseId(resultUri) != 0);
+
+ long contactId = queryContactId(rawContactId);
+ assertTrue(contactId != 0);
+
+ String displayName = queryDisplayName(contactId);
+ assertEquals("Johna Smitha", displayName);
+ }
+
+ public void testAggregationOfExactFullNameMatch() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Johnb", "Smithb");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Johnb", "Smithb");
+
+ assertAggregated(rawContactId1, rawContactId2, "Johnb Smithb");
+ }
+
+ public void testAggregationOfCaseInsensitiveFullNameMatch() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Johnc", "Smithc");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Johnc", "smithc");
+
+ assertAggregated(rawContactId1, rawContactId2, "Johnc Smithc");
+ }
+
+ public void testAggregationOfLastNameMatch() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, null, "Johnd");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, null, "johnd");
+
+ assertAggregated(rawContactId1, rawContactId2, "Johnd");
+ }
+
+ public void testNonAggregationOfFirstNameMatch() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Johne", "Smithe");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Johne", null);
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ // TODO: should this be allowed to match?
+ public void testNonAggregationOfLastNameMatch() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Johnf", "Smithf");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, null, "Smithf");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationOfConcatenatedFullNameMatch() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Johng", "Smithg");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "johngsmithg", null);
+
+ assertAggregated(rawContactId1, rawContactId2, "Johng Smithg");
+ }
+
+ public void testAggregationOfNormalizedFullNameMatch() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "H\u00e9l\u00e8ne", "Bj\u00f8rn");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "helene bjorn", null);
+
+ assertAggregated(rawContactId1, rawContactId2, "H\u00e9l\u00e8ne Bj\u00f8rn");
+ }
+
+ public void testAggregationBasedOnPhoneNumberNoNameData() {
+ long rawContactId1 = createRawContact();
+ insertPhoneNumber(rawContactId1, "(888)555-1231");
+
+ long rawContactId2 = createRawContact();
+ insertPhoneNumber(rawContactId2, "1(888)555-1231");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnPhoneNumberWhenTargetAggregateHasNoName() {
+ long rawContactId1 = createRawContact();
+ insertPhoneNumber(rawContactId1, "(888)555-1232");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Johnl", "Smithl");
+ insertPhoneNumber(rawContactId2, "1(888)555-1232");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnPhoneNumberWhenNewContactHasNoName() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Johnm", "Smithm");
+ insertPhoneNumber(rawContactId1, "(888)555-1233");
+
+ long rawContactId2 = createRawContact();
+ insertPhoneNumber(rawContactId2, "1(888)555-1233");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnPhoneNumberWithSimilarNames() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Ogre", "Hunter");
+ insertPhoneNumber(rawContactId1, "(888)555-1234");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Opra", "Humper");
+ insertPhoneNumber(rawContactId2, "1(888)555-1234");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnPhoneNumberWithDifferentNames() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Baby", "Bear");
+ insertPhoneNumber(rawContactId1, "(888)555-1235");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Blind", "Mouse");
+ insertPhoneNumber(rawContactId2, "1(888)555-1235");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnEmailNoNameData() {
+ long rawContactId1 = createRawContact();
+ insertEmail(rawContactId1, "lightning@android.com");
+
+ long rawContactId2 = createRawContact();
+ insertEmail(rawContactId2, "lightning@android.com");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnEmailWhenTargetAggregateHasNoName() {
+ long rawContactId1 = createRawContact();
+ insertEmail(rawContactId1, "mcqueen@android.com");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Lightning", "McQueen");
+ insertEmail(rawContactId2, "mcqueen@android.com");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnEmailWhenNewContactHasNoName() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Doc", "Hudson");
+ insertEmail(rawContactId1, "doc@android.com");
+
+ long rawContactId2 = createRawContact();
+ insertEmail(rawContactId2, "doc@android.com");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnEmailWithSimilarNames() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Sally", "Carrera");
+ insertEmail(rawContactId1, "sally@android.com");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Sallie", "Carerra");
+ insertEmail(rawContactId2, "sally@android.com");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnEmailWithDifferentNames() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Chick", "Hicks");
+ insertEmail(rawContactId1, "hicky@android.com");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Luigi", "Guido");
+ insertEmail(rawContactId2, "hicky@android.com");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationByCommonNicknameWithLastName() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Bill", "Gore");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "William", "Gore");
+
+ assertAggregated(rawContactId1, rawContactId2, "William Gore");
+ }
+
+ public void testAggregationByCommonNicknameOnly() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Lawrence", null);
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Larry", null);
+
+ assertAggregated(rawContactId1, rawContactId2, "Lawrence");
+ }
+
+ public void testAggregationByNicknameNoStructuredName() {
+ long rawContactId1 = createRawContact();
+ insertNickname(rawContactId1, "Frozone");
+
+ long rawContactId2 = createRawContact();
+ insertNickname(rawContactId2, "Frozone");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationByNicknameWithSimilarNames() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Buddy", "Pine");
+ insertNickname(rawContactId1, "Syndrome");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Body", "Pane");
+ insertNickname(rawContactId2, "Syndrome");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationByNicknameWithDifferentNames() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Helen", "Parr");
+ insertNickname(rawContactId1, "Elastigirl");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Shawn", "Johnson");
+ insertNickname(rawContactId2, "Elastigirl");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationExceptionKeepIn() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Johnk", "Smithk");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Johnkx", "Smithkx");
+
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_IN,
+ queryContactId(rawContactId1), rawContactId2);
+
+ assertAggregated(rawContactId1, rawContactId2, "Johnkx Smithkx");
+
+ // Assert that the empty aggregate got removed
+ long newContactId1 = queryContactId(rawContactId1);
+ if (contactId1 != newContactId1) {
+ Cursor cursor = queryContact(contactId1);
+ assertFalse(cursor.moveToFirst());
+ cursor.close();
+ } else {
+ Cursor cursor = queryContact(contactId2);
+ assertFalse(cursor.moveToFirst());
+ cursor.close();
+ }
+ }
+
+ public void testAggregationExceptionKeepOut() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Johnh", "Smithh");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Johnh", "Smithh");
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_OUT,
+ queryContactId(rawContactId1), rawContactId2);
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationExceptionKeepOutCheckUpdatesDisplayName() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Johni", "Smithi");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Johnj", "Smithj");
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_IN,
+ queryContactId(rawContactId1), rawContactId2);
+
+ assertAggregated(rawContactId1, rawContactId2, "Johnj Smithj");
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_OUT,
+ queryContactId(rawContactId1), rawContactId2);
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+
+ String displayName1 = queryDisplayName(queryContactId(rawContactId1));
+ assertEquals("Johni Smithi", displayName1);
+
+ String displayName2 = queryDisplayName(queryContactId(rawContactId2));
+ assertEquals("Johnj Smithj", displayName2);
+ }
+
+ public void testAggregationSuggestionsBasedOnName() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Duane", null);
+
+ // Exact name match
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Duane", null);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_OUT,
+ queryContactId(rawContactId1), rawContactId2);
+
+ // Edit distance == 0.84
+ long rawContactId3 = createRawContact();
+ insertStructuredName(rawContactId3, "Dwayne", null);
+
+ // Edit distance == 0.6
+ long rawContactId4 = createRawContact();
+ insertStructuredName(rawContactId4, "Donny", null);
+
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ long contactId3 = queryContactId(rawContactId3);
+
+ assertSuggestions(contactId1, contactId2, contactId3);
+ }
+
+ public void testAggregationSuggestionsBasedOnPhoneNumber() {
+
+ // Create two contacts that would not be aggregated because of name mismatch
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Lord", "Farquaad");
+ insertPhoneNumber(rawContactId1, "(888)555-1236");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Talking", "Donkey");
+ insertPhoneNumber(rawContactId2, "1(888)555-1236");
+
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ assertTrue(contactId1 != contactId2);
+
+ assertSuggestions(contactId1, contactId2);
+ }
+
+ public void testAggregationSuggestionsBasedOnEmailAddress() {
+
+ // Create two contacts that would not be aggregated because of name mismatch
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Carl", "Fredricksen");
+ insertEmail(rawContactId1, "up@android.com");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Charles", "Muntz");
+ insertEmail(rawContactId2, "up@android.com");
+
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ assertTrue(contactId1 != contactId2);
+
+ assertSuggestions(contactId1, contactId2);
+ }
+
+ public void testAggregationSuggestionsBasedOnEmailAddressApproximateMatch() {
+
+ // Create two contacts that would not be aggregated because of name mismatch
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Bob", null);
+ insertEmail(rawContactId1, "incredible2004@android.com");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Lucius", "Best");
+ insertEmail(rawContactId2, "incrediball@androidd.com");
+
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ assertTrue(contactId1 != contactId2);
+
+ assertSuggestions(contactId1, contactId2);
+ }
+
+ public void testAggregationSuggestionsBasedOnNickname() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Peter", "Parker");
+ insertNickname(rawContactId1, "Spider-Man");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Manny", "Spider");
+
+ long contactId1 = queryContactId(rawContactId1);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_OUT, contactId1, rawContactId2);
+
+ long contactId2 = queryContactId(rawContactId2);
+ assertSuggestions(contactId1, contactId2);
+ }
+
+ public void testAggregationSuggestionsBasedOnNicknameMatchingName() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Clark", "Kent");
+ insertNickname(rawContactId1, "Superman");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Roy", "Williams");
+ insertNickname(rawContactId2, "superman");
+
+ long contactId1 = queryContactId(rawContactId1);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_OUT, contactId1, rawContactId2);
+
+ long contactId2 = queryContactId(rawContactId2);
+ assertSuggestions(contactId1, contactId2);
+ }
+
+ public void testAggregationSuggestionsBasedOnCommonNickname() {
+ long rawContactId1 = createRawContact();
+ insertStructuredName(rawContactId1, "Dick", "Cherry");
+
+ long rawContactId2 = createRawContact();
+ insertStructuredName(rawContactId2, "Richard", "Cherry");
+
+ long contactId1 = queryContactId(rawContactId1);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_OUT, contactId1, rawContactId2);
+
+ long contactId2 = queryContactId(rawContactId2);
+ assertSuggestions(contactId1, contactId2);
+ }
+
+ public void testChoosePhoto() {
+ long rawContactId1 = createRawContact();
+ setContactAccountName(rawContactId1, "donut");
+ long donutId = ContentUris.parseId(insertPhoto(rawContactId1));
+ long contactId = queryContactId(rawContactId1);
+
+ long rawContactId2 = createRawContact();
+ setAggregationException(AggregationExceptions.TYPE_KEEP_IN, contactId, rawContactId2);
+ setContactAccountName(rawContactId2, "cupcake");
+ long cupcakeId = ContentUris.parseId(insertPhoto(rawContactId2));
+
+ long rawContactId3 = createRawContact();
+ setAggregationException(AggregationExceptions.TYPE_KEEP_IN, contactId, rawContactId3);
+ setContactAccountName(rawContactId3, "flan");
+ long flanId = ContentUris.parseId(insertPhoto(rawContactId3));
+
+ assertEquals(cupcakeId, queryPhotoId(queryContactId(rawContactId2)));
+ }
+
+ private void assertSuggestions(long contactId, long... suggestions) {
+ final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ Uri uri = Uri.withAppendedPath(aggregateUri,
+ Contacts.AggregationSuggestions.CONTENT_DIRECTORY);
+ final Cursor cursor = mResolver.query(uri, new String[] { Contacts._ID },
+ null, null, null);
+
+ assertEquals(suggestions.length, cursor.getCount());
+
+ for (int i = 0; i < suggestions.length; i++) {
+ cursor.moveToNext();
+ assertEquals(suggestions[i], cursor.getLong(0));
+ }
+
+ cursor.close();
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
new file mode 100644
index 0000000..eed7b43
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.Contacts.Phones;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.test.IsolatedContext;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.test.mock.MockPackageManager;
+
+import java.util.HashMap;
+
+/**
+ * Helper class that encapsulates an "actor" which is owned by a specific
+ * package name. It correctly maintains a wrapped {@link Context} and an
+ * attached {@link MockContentResolver}. Multiple actors can be used to test
+ * security scenarios between multiple packages.
+ */
+public class ContactsActor {
+ private static final String FILENAME_PREFIX = "test.";
+
+ public static final String PACKAGE_GREY = "edu.example.grey";
+ public static final String PACKAGE_RED = "net.example.red";
+ public static final String PACKAGE_GREEN = "com.example.green";
+ public static final String PACKAGE_BLUE = "org.example.blue";
+
+ public Context context;
+ public String packageName;
+ public MockContentResolver resolver;
+ public ContentProvider provider;
+
+ private IsolatedContext mProviderContext;
+
+ /**
+ * Create an "actor" using the given parent {@link Context} and the specific
+ * package name. Internally, all {@link Context} method calls are passed to
+ * a new instance of {@link RestrictionMockContext}, which stubs out the
+ * security infrastructure.
+ */
+ public ContactsActor(Context overallContext, String packageName,
+ Class<? extends ContentProvider> providerClass, String authority) throws Exception {
+ resolver = new MockContentResolver();
+ context = new RestrictionMockContext(overallContext, packageName, resolver);
+ this.packageName = packageName;
+
+ RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(context,
+ overallContext, FILENAME_PREFIX);
+ mProviderContext = new IsolatedContext(resolver, targetContextWrapper);
+ provider = addProvider(providerClass, authority);
+ }
+
+ public void addAuthority(String authority) {
+ resolver.addProvider(authority, provider);
+ }
+
+ public ContentProvider addProvider(Class<? extends ContentProvider> providerClass,
+ String authority) throws Exception {
+ ContentProvider provider = providerClass.newInstance();
+ provider.attachInfo(mProviderContext, null);
+ resolver.addProvider(authority, provider);
+ return provider;
+ }
+
+ /**
+ * Mock {@link Context} that reports specific well-known values for testing
+ * data protection. The creator can override the owner package name, and
+ * force the {@link PackageManager} to always return a well-known package
+ * list for any call to {@link PackageManager#getPackagesForUid(int)}.
+ * <p>
+ * For example, the creator could request that the {@link Context} lives in
+ * package name "com.example.red", and also cause the {@link PackageManager}
+ * to report that no UID contains that package name.
+ */
+ private static class RestrictionMockContext extends MockContext {
+ private final Context mOverallContext;
+ private final String mReportedPackageName;
+ private final RestrictionMockPackageManager mPackageManager;
+ private final ContentResolver mResolver;
+
+ /**
+ * Create a {@link Context} under the given package name.
+ */
+ public RestrictionMockContext(Context overallContext, String reportedPackageName,
+ ContentResolver resolver) {
+ mOverallContext = overallContext;
+ mReportedPackageName = reportedPackageName;
+ mResolver = resolver;
+ mPackageManager = new RestrictionMockPackageManager();
+ mPackageManager.addPackage(1000, PACKAGE_GREY);
+ mPackageManager.addPackage(2000, PACKAGE_RED);
+ mPackageManager.addPackage(3000, PACKAGE_GREEN);
+ mPackageManager.addPackage(4000, PACKAGE_BLUE);
+ }
+
+ @Override
+ public String getPackageName() {
+ return mReportedPackageName;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mPackageManager;
+ }
+
+ @Override
+ public Resources getResources() {
+ return mOverallContext.getResources();
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mResolver;
+ }
+ }
+
+ /**
+ * Mock {@link PackageManager} that knows about a specific set of packages
+ * to help test security models. Because {@link Binder#getCallingUid()}
+ * can't be mocked, you'll have to find your mock-UID manually using your
+ * {@link Context#getPackageName()}.
+ */
+ private static class RestrictionMockPackageManager extends MockPackageManager {
+ private final HashMap<Integer, String> mForward = new HashMap<Integer, String>();
+ private final HashMap<String, Integer> mReverse = new HashMap<String, Integer>();
+
+ /**
+ * Add a UID-to-package mapping, which is then stored internally.
+ */
+ public void addPackage(int packageUid, String packageName) {
+ mForward.put(packageUid, packageName);
+ mReverse.put(packageName, packageUid);
+ }
+
+ @Override
+ public String[] getPackagesForUid(int uid) {
+ return new String[] { mForward.get(uid) };
+ }
+
+ @Override
+ public ApplicationInfo getApplicationInfo(String packageName, int flags) {
+ ApplicationInfo info = new ApplicationInfo();
+ Integer uid = mReverse.get(packageName);
+ info.uid = (uid != null) ? uid : -1;
+ return info;
+ }
+ }
+
+ public long createContact(boolean isRestricted, String name) {
+ long contactId = createContact(isRestricted);
+ createName(contactId, name);
+ return contactId;
+ }
+
+ public long createContact(boolean isRestricted) {
+ final ContentValues values = new ContentValues();
+ if (isRestricted) {
+ values.put(RawContacts.IS_RESTRICTED, 1);
+ }
+
+ Uri contactUri = resolver.insert(RawContacts.CONTENT_URI, values);
+ return ContentUris.parseId(contactUri);
+ }
+
+ public long createName(long contactId, String name) {
+ final ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, contactId);
+ values.put(Data.IS_PRIMARY, 1);
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+ values.put(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
+ values.put(CommonDataKinds.StructuredName.FAMILY_NAME, name);
+ Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+ contactId), RawContacts.Data.CONTENT_DIRECTORY);
+ Uri dataUri = resolver.insert(insertUri, values);
+ return ContentUris.parseId(dataUri);
+ }
+
+ public long createPhone(long contactId, String phoneNumber) {
+ final ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, contactId);
+ values.put(Data.IS_PRIMARY, 1);
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+ values.put(Data.MIMETYPE, Phones.CONTENT_ITEM_TYPE);
+ values.put(ContactsContract.CommonDataKinds.Phone.TYPE,
+ ContactsContract.CommonDataKinds.Phone.TYPE_HOME);
+ values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
+ Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+ contactId), RawContacts.Data.CONTENT_DIRECTORY);
+ Uri dataUri = resolver.insert(insertUri, values);
+ return ContentUris.parseId(dataUri);
+ }
+
+ public void updateException(String packageProvider, String packageClient, boolean allowAccess) {
+ throw new UnsupportedOperationException("RestrictionExceptions are hard-coded");
+ }
+
+ public long getContactForRawContact(long rawContactId) {
+ Uri contactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ final Cursor cursor = resolver.query(contactUri, Projections.PROJ_RAW_CONTACTS, null,
+ null, null);
+ if (!cursor.moveToFirst()) {
+ cursor.close();
+ throw new RuntimeException("Contact didn't have an aggregate");
+ }
+ final long aggId = cursor.getLong(Projections.COL_CONTACTS_ID);
+ cursor.close();
+ return aggId;
+ }
+
+ public int getDataCountForContact(long contactId) {
+ Uri contactUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI,
+ contactId), Contacts.Data.CONTENT_DIRECTORY);
+ final Cursor cursor = resolver.query(contactUri, Projections.PROJ_ID, null, null,
+ null);
+ final int count = cursor.getCount();
+ cursor.close();
+ return count;
+ }
+
+ public void setSuperPrimaryPhone(long dataId) {
+ final ContentValues values = new ContentValues();
+ values.put(Data.IS_PRIMARY, 1);
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+ Uri updateUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
+ resolver.update(updateUri, values, null, null);
+ }
+
+ public long getPrimaryPhoneId(long contactId) {
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ final Cursor cursor = resolver.query(contactUri, Projections.PROJ_CONTACTS, null,
+ null, null);
+ long primaryPhoneId = -1;
+ if (cursor.moveToFirst()) {
+ primaryPhoneId = cursor.getLong(Projections.COL_CONTACTS_PRIMARY_PHONE_ID);
+ }
+ cursor.close();
+ return primaryPhoneId;
+ }
+
+ public long createGroup(String groupName) {
+ final ContentValues values = new ContentValues();
+ values.put(ContactsContract.Groups.RES_PACKAGE, packageName);
+ values.put(ContactsContract.Groups.TITLE, groupName);
+ Uri groupUri = resolver.insert(ContactsContract.Groups.CONTENT_URI, values);
+ return ContentUris.parseId(groupUri);
+ }
+
+ public long createGroupMembership(long rawContactId, long groupId) {
+ final ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE);
+ values.put(CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId);
+ Uri insertUri = Uri.withAppendedPath(ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+ rawContactId), RawContacts.Data.CONTENT_DIRECTORY);
+ Uri dataUri = resolver.insert(insertUri, values);
+ return ContentUris.parseId(dataUri);
+ }
+
+ /**
+ * Various internal database projections.
+ */
+ private interface Projections {
+ static final String[] PROJ_ID = new String[] {
+ BaseColumns._ID,
+ };
+
+ static final int COL_ID = 0;
+
+ static final String[] PROJ_RAW_CONTACTS = new String[] {
+ RawContacts.CONTACT_ID
+ };
+
+ static final int COL_CONTACTS_ID = 0;
+
+ static final String[] PROJ_CONTACTS = new String[] {
+ Contacts.PRIMARY_PHONE_ID
+ };
+
+ static final int COL_CONTACTS_PRIMARY_PHONE_ID = 0;
+
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
new file mode 100644
index 0000000..de9dd14
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -0,0 +1,688 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import com.android.internal.util.ArrayUtils;
+
+import android.app.SearchManager;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.Contacts.Intents;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Unit tests for {@link ContactsProvider2}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -w \
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class ContactsProvider2Test extends BaseContactsProvider2Test {
+
+ public void testDisplayNameParsingWhenPartsUnspecified() {
+ long rawContactId = createRawContact();
+ ContentValues values = new ContentValues();
+ values.put(StructuredName.DISPLAY_NAME, "Mr.John Kevin von Smith, Jr.");
+ insertStructuredName(rawContactId, values);
+
+ assertStructuredName(rawContactId, "Mr", "John", "Kevin", "von Smith", "Jr");
+ }
+
+ public void testDisplayNameParsingWhenPartsSpecified() {
+ long rawContactId = createRawContact();
+ ContentValues values = new ContentValues();
+ values.put(StructuredName.DISPLAY_NAME, "Mr.John Kevin von Smith, Jr.");
+ values.put(StructuredName.FAMILY_NAME, "Johnson");
+ insertStructuredName(rawContactId, values);
+
+ assertStructuredName(rawContactId, null, null, null, "Johnson", null);
+ }
+
+ public void testSendToVoicemailDefault() {
+ long rawContactId = createRawContactWithName();
+ long contactId = queryContactId(rawContactId);
+
+ Cursor c = queryContact(contactId);
+ assertTrue(c.moveToNext());
+ int sendToVoicemail = c.getInt(c.getColumnIndex(Contacts.SEND_TO_VOICEMAIL));
+ assertEquals(0, sendToVoicemail);
+ c.close();
+ }
+
+ public void testSetSendToVoicemailAndRingtone() {
+ long rawContactId = createRawContactWithName();
+ long contactId = queryContactId(rawContactId);
+
+ updateSendToVoicemailAndRingtone(contactId, true, "foo");
+ assertSendToVoicemailAndRingtone(contactId, true, "foo");
+ }
+
+ public void testSendToVoicemailAndRingtoneAfterAggregation() {
+ long rawContactId1 = createRawContactWithName();
+ long contactId1 = queryContactId(rawContactId1);
+ updateSendToVoicemailAndRingtone(contactId1, true, "foo");
+
+ long rawContactId2 = createRawContactWithName();
+ long contactId2 = queryContactId(rawContactId2);
+ updateSendToVoicemailAndRingtone(contactId2, true, "bar");
+
+ // Aggregate them
+ setAggregationException(AggregationExceptions.TYPE_KEEP_IN, contactId1, rawContactId2);
+
+ // Both contacts had "send to VM", the contact now has the same value
+ assertSendToVoicemailAndRingtone(contactId1, true, "foo,bar"); // Either foo or bar
+ }
+
+ public void testDoNotSendToVoicemailAfterAggregation() {
+ long rawContactId1 = createRawContactWithName();
+ long contactId1 = queryContactId(rawContactId1);
+ updateSendToVoicemailAndRingtone(contactId1, true, null);
+
+ long rawContactId2 = createRawContactWithName();
+ long contactId2 = queryContactId(rawContactId2);
+ updateSendToVoicemailAndRingtone(contactId2, false, null);
+
+ // Aggregate them
+ setAggregationException(AggregationExceptions.TYPE_KEEP_IN, contactId1, rawContactId2);
+
+ // Since one of the contacts had "don't send to VM" that setting wins for the aggregate
+ assertSendToVoicemailAndRingtone(contactId1, false, null);
+ }
+
+ public void testSetSendToVoicemailAndRingtonePreservedAfterJoinAndSplit() {
+ long rawContactId1 = createRawContactWithName();
+ long contactId1 = queryContactId(rawContactId1);
+ updateSendToVoicemailAndRingtone(contactId1, true, "foo");
+
+ long rawContactId2 = createRawContactWithName();
+ long contactId2 = queryContactId(rawContactId2);
+ updateSendToVoicemailAndRingtone(contactId2, false, "bar");
+
+ // Aggregate them
+ setAggregationException(AggregationExceptions.TYPE_KEEP_IN, contactId1, rawContactId2);
+
+ // Split them
+ setAggregationException(AggregationExceptions.TYPE_KEEP_OUT, contactId1, rawContactId2);
+
+ assertSendToVoicemailAndRingtone(contactId1, true, "foo");
+ assertSendToVoicemailAndRingtone(queryContactId(rawContactId2), false, "bar");
+ }
+
+ public void testSinglePresenceRowPerContact() {
+ int protocol1 = Im.PROTOCOL_GOOGLE_TALK;
+ String handle1 = "test@gmail.com";
+
+ long rawContactId1 = createRawContact();
+ insertImHandle(rawContactId1, protocol1, handle1);
+
+ insertPresence(protocol1, handle1, Presence.AVAILABLE);
+ insertPresence(protocol1, handle1, Presence.AWAY);
+ insertPresence(protocol1, handle1, Presence.INVISIBLE);
+
+ Cursor c = queryContactSummary(queryContactId(rawContactId1),
+ new String[] {Presence.PRESENCE_STATUS});
+ assertEquals(c.getCount(), 1);
+
+ c.moveToFirst();
+ assertEquals(c.getInt(0), Presence.AVAILABLE);
+
+ }
+
+ private void updateSendToVoicemailAndRingtone(long contactId, boolean sendToVoicemail,
+ String ringtone) {
+ ContentValues values = new ContentValues();
+ values.put(Contacts.SEND_TO_VOICEMAIL, sendToVoicemail);
+ if (ringtone != null) {
+ values.put(Contacts.CUSTOM_RINGTONE, ringtone);
+ }
+
+ final Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ int count = mResolver.update(uri, values, null, null);
+ assertEquals(1, count);
+ }
+
+ private void assertSendToVoicemailAndRingtone(long contactId, boolean expectedSendToVoicemail,
+ String expectedRingtone) {
+ Cursor c = queryContact(contactId);
+ assertTrue(c.moveToNext());
+ int sendToVoicemail = c.getInt(c.getColumnIndex(Contacts.SEND_TO_VOICEMAIL));
+ assertEquals(expectedSendToVoicemail ? 1 : 0, sendToVoicemail);
+ String ringtone = c.getString(c.getColumnIndex(Contacts.CUSTOM_RINGTONE));
+ if (expectedRingtone == null) {
+ assertNull(ringtone);
+ } else {
+ assertTrue(ArrayUtils.contains(expectedRingtone.split(","), ringtone));
+ }
+ c.close();
+ }
+
+ public void testGroupCreationAfterMembershipInsert() {
+ long rawContactId1 = createRawContact(mAccount);
+ Uri groupMembershipUri = insertGroupMembership(rawContactId1, "gsid1");
+
+ long groupId = assertSingleGroup(NO_LONG, mAccount, "gsid1", null);
+ assertSingleGroupMembership(ContentUris.parseId(groupMembershipUri),
+ rawContactId1, groupId, "gsid1");
+ }
+
+ public void testGroupReuseAfterMembershipInsert() {
+ long rawContactId1 = createRawContact(mAccount);
+ long groupId1 = createGroup(mAccount, "gsid1", "title1");
+ Uri groupMembershipUri = insertGroupMembership(rawContactId1, "gsid1");
+
+ assertSingleGroup(groupId1, mAccount, "gsid1", "title1");
+ assertSingleGroupMembership(ContentUris.parseId(groupMembershipUri),
+ rawContactId1, groupId1, "gsid1");
+ }
+
+ public void testGroupInsertFailureOnGroupIdConflict() {
+ long rawContactId1 = createRawContact(mAccount);
+ long groupId1 = createGroup(mAccount, "gsid1", "title1");
+
+ ContentValues values = new ContentValues();
+ values.put(GroupMembership.RAW_CONTACT_ID, rawContactId1);
+ values.put(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
+ values.put(GroupMembership.GROUP_SOURCE_ID, "gsid1");
+ values.put(GroupMembership.GROUP_ROW_ID, groupId1);
+ try {
+ mResolver.insert(Data.CONTENT_URI, values);
+ fail("the insert was expected to fail, but it succeeded");
+ } catch (IllegalArgumentException e) {
+ // this was expected
+ }
+ }
+
+ public void testContentEntityIterator() throws RemoteException {
+ // create multiple contacts and check that the selected ones are returned
+ long id;
+
+ long groupId1 = createGroup(mAccount, "gsid1", "title1");
+ long groupId2 = createGroup(mAccount, "gsid2", "title2");
+
+ long c0 = id = createRawContact(mAccount, RawContacts.SOURCE_ID, "c0");
+ Uri id_0_0 = insertGroupMembership(id, "gsid1");
+ Uri id_0_1 = insertEmail(id, "c0@email.com");
+ Uri id_0_2 = insertPhoneNumber(id, "5551212c0");
+
+ long c1 = id = createRawContact(mAccount, RawContacts.SOURCE_ID, "c1");
+ Uri id_1_0 = insertGroupMembership(id, "gsid1");
+ Uri id_1_1 = insertGroupMembership(id, "gsid2");
+ Uri id_1_2 = insertEmail(id, "c1@email.com");
+ Uri id_1_3 = insertPhoneNumber(id, "5551212c1");
+
+ long c2 = id = createRawContact(mAccount, RawContacts.SOURCE_ID, "c2");
+ Uri id_2_0 = insertGroupMembership(id, "gsid1");
+ Uri id_2_1 = insertEmail(id, "c2@email.com");
+ Uri id_2_2 = insertPhoneNumber(id, "5551212c2");
+
+ long c3 = id = createRawContact(mAccount);
+ Uri id_3_0 = insertGroupMembership(id, groupId2);
+ Uri id_3_1 = insertEmail(id, "c3@email.com");
+ Uri id_3_2 = insertPhoneNumber(id, "5551212c3");
+
+ EntityIterator iterator = mResolver.queryEntities(
+ maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, mAccount),
+ RawContacts.SOURCE_ID + " in ('c1', 'c2', 'c3')", null, null);
+ Entity entity;
+ ContentValues[] subValues;
+ entity = iterator.next();
+ assertEquals(c1, (long) entity.getEntityValues().getAsLong(RawContacts._ID));
+ subValues = asSortedContentValuesArray(entity.getSubValues());
+ assertEquals(4, subValues.length);
+ assertDataRow(subValues[0], GroupMembership.CONTENT_ITEM_TYPE,
+ Data._ID, id_1_0,
+ GroupMembership.GROUP_ROW_ID, groupId1,
+ GroupMembership.GROUP_SOURCE_ID, "gsid1");
+ assertDataRow(subValues[1], GroupMembership.CONTENT_ITEM_TYPE,
+ Data._ID, id_1_1,
+ GroupMembership.GROUP_ROW_ID, groupId2,
+ GroupMembership.GROUP_SOURCE_ID, "gsid2");
+ assertDataRow(subValues[2], Email.CONTENT_ITEM_TYPE,
+ Data._ID, id_1_2,
+ Email.DATA, "c1@email.com");
+ assertDataRow(subValues[3], Phone.CONTENT_ITEM_TYPE,
+ Data._ID, id_1_3,
+ Email.DATA, "5551212c1");
+
+ entity = iterator.next();
+ assertEquals(c2, (long) entity.getEntityValues().getAsLong(RawContacts._ID));
+ subValues = asSortedContentValuesArray(entity.getSubValues());
+ assertEquals(3, subValues.length);
+ assertDataRow(subValues[0], GroupMembership.CONTENT_ITEM_TYPE,
+ Data._ID, id_2_0,
+ GroupMembership.GROUP_ROW_ID, groupId1,
+ GroupMembership.GROUP_SOURCE_ID, "gsid1");
+ assertDataRow(subValues[1], Email.CONTENT_ITEM_TYPE,
+ Data._ID, id_2_1,
+ Email.DATA, "c2@email.com");
+ assertDataRow(subValues[2], Phone.CONTENT_ITEM_TYPE,
+ Data._ID, id_2_2,
+ Email.DATA, "5551212c2");
+
+ entity = iterator.next();
+ assertEquals(c3, (long) entity.getEntityValues().getAsLong(RawContacts._ID));
+ subValues = asSortedContentValuesArray(entity.getSubValues());
+ assertEquals(3, subValues.length);
+ assertDataRow(subValues[0], GroupMembership.CONTENT_ITEM_TYPE,
+ Data._ID, id_3_0,
+ GroupMembership.GROUP_ROW_ID, groupId2,
+ GroupMembership.GROUP_SOURCE_ID, "gsid2");
+ assertDataRow(subValues[1], Email.CONTENT_ITEM_TYPE,
+ Data._ID, id_3_1,
+ Email.DATA, "c3@email.com");
+ assertDataRow(subValues[2], Phone.CONTENT_ITEM_TYPE,
+ Data._ID, id_3_2,
+ Email.DATA, "5551212c3");
+
+ assertFalse(iterator.hasNext());
+ }
+
+ public void testDataCreateUpdateDeleteByMimeType() throws Exception {
+ long rawContactId = createRawContact();
+
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, "testmimetype");
+ values.put(Data.RES_PACKAGE, "oldpackage");
+ values.put(Data.IS_PRIMARY, 1);
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+ values.put(Data.DATA1, "old1");
+ values.put(Data.DATA2, "old2");
+ values.put(Data.DATA3, "old3");
+ values.put(Data.DATA4, "old4");
+ values.put(Data.DATA5, "old5");
+ values.put(Data.DATA6, "old6");
+ values.put(Data.DATA7, "old7");
+ values.put(Data.DATA8, "old8");
+ values.put(Data.DATA9, "old9");
+ values.put(Data.DATA10, "old10");
+ values.put(Data.DATA11, "old11");
+ values.put(Data.DATA12, "old12");
+ values.put(Data.DATA13, "old13");
+ values.put(Data.DATA14, "old14");
+ values.put(Data.DATA15, "old15");
+ Uri uri = mResolver.insert(Data.CONTENT_URI, values);
+ assertStoredValues(uri, values);
+
+ values.clear();
+ values.put(Data.RES_PACKAGE, "newpackage");
+ values.put(Data.IS_PRIMARY, 0);
+ values.put(Data.IS_SUPER_PRIMARY, 0);
+ values.put(Data.DATA1, "new1");
+ values.put(Data.DATA2, "new2");
+ values.put(Data.DATA3, "new3");
+ values.put(Data.DATA4, "new4");
+ values.put(Data.DATA5, "new5");
+ values.put(Data.DATA6, "new6");
+ values.put(Data.DATA7, "new7");
+ values.put(Data.DATA8, "new8");
+ values.put(Data.DATA9, "new9");
+ values.put(Data.DATA10, "new10");
+ values.put(Data.DATA11, "new11");
+ values.put(Data.DATA12, "new12");
+ values.put(Data.DATA13, "new13");
+ values.put(Data.DATA14, "new14");
+ values.put(Data.DATA15, "new15");
+ mResolver.update(Data.CONTENT_URI, values, Data.RAW_CONTACT_ID + "=" + rawContactId +
+ " AND " + Data.MIMETYPE + "='testmimetype'", null);
+
+ // Should not be able to change IS_PRIMARY and IS_SUPER_PRIMARY by the above update
+ values.put(Data.IS_PRIMARY, 1);
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+ assertStoredValues(uri, values);
+
+ int count = mResolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=" + rawContactId
+ + " AND " + Data.MIMETYPE + "='testmimetype'", null);
+ assertEquals(1, count);
+ assertEquals(0, getCount(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=" + rawContactId
+ + " AND " + Data.MIMETYPE + "='testmimetype'", null));
+ }
+
+ public void testRawContactDeletion() {
+ long rawContactId = createRawContact();
+ Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+
+ insertImHandle(rawContactId, Im.PROTOCOL_GOOGLE_TALK, "deleteme@android.com");
+ insertPresence(Im.PROTOCOL_GOOGLE_TALK, "deleteme@android.com", Presence.AVAILABLE);
+ assertEquals(1, getCount(Uri.withAppendedPath(uri, RawContacts.Data.CONTENT_DIRECTORY),
+ null, null));
+ assertEquals(1, getCount(Presence.CONTENT_URI, Presence.RAW_CONTACT_ID + "=" + rawContactId,
+ null));
+
+ mResolver.delete(uri, null, null);
+
+ assertStoredValues(uri, RawContacts.DELETED, "1");
+
+ Uri permanentDeletionUri = uri.buildUpon().appendQueryParameter(
+ RawContacts.DELETE_PERMANENTLY, "true").build();
+ mResolver.delete(permanentDeletionUri, null, null);
+ assertEquals(0, getCount(uri, null, null));
+ assertEquals(0, getCount(Uri.withAppendedPath(uri, RawContacts.Data.CONTENT_DIRECTORY),
+ null, null));
+ assertEquals(0, getCount(Presence.CONTENT_URI, Presence.RAW_CONTACT_ID + "=" + rawContactId,
+ null));
+ }
+
+ public void testRawContactDirtySetOnChange() {
+ Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+ createRawContact(mAccount));
+ assertDirty(uri, true);
+ clearDirty(uri);
+ assertDirty(uri, false);
+ }
+
+ public void testRawContactDirtyAndVersion() {
+ final long rawContactId = createRawContact(mAccount);
+ Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId);
+ assertDirty(uri, true);
+ long version = getVersion(uri);
+
+ ContentValues values = new ContentValues();
+ values.put(ContactsContract.RawContacts.DIRTY, 0);
+ values.put(ContactsContract.RawContacts.SEND_TO_VOICEMAIL, 1);
+ values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
+ RawContacts.AGGREGATION_MODE_IMMEDITATE);
+ values.put(ContactsContract.RawContacts.STARRED, 1);
+ assertEquals(1, mResolver.update(uri, values, null, null));
+ assertEquals(version, getVersion(uri));
+
+ assertDirty(uri, false);
+
+ Uri emailUri = insertEmail(rawContactId, "goo@woo.com");
+ assertDirty(uri, true);
+ ++version;
+ assertEquals(version, getVersion(uri));
+ clearDirty(uri);
+
+ values = new ContentValues();
+ values.put(Email.DATA, "goo@hoo.com");
+ mResolver.update(emailUri, values, null, null);
+ assertDirty(uri, true);
+ ++version;
+ assertEquals(version, getVersion(uri));
+ clearDirty(uri);
+
+ mResolver.delete(emailUri, null, null);
+ assertDirty(uri, true);
+ ++version;
+ assertEquals(version, getVersion(uri));
+ }
+
+ public void testRawContactClearDirty() {
+ final long rawContactId = createRawContact(mAccount);
+ Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+ rawContactId);
+ long version = getVersion(uri);
+ insertEmail(rawContactId, "goo@woo.com");
+ assertDirty(uri, true);
+ version++;
+ assertEquals(version, getVersion(uri));
+
+ clearDirty(uri);
+ assertDirty(uri, false);
+ assertEquals(version, getVersion(uri));
+ }
+
+ public void testRawContactDeletionSetsDirty() {
+ final long rawContactId = createRawContact(mAccount);
+ Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+ rawContactId);
+ long version = getVersion(uri);
+ clearDirty(uri);
+ assertDirty(uri, false);
+
+ mResolver.delete(uri, null, null);
+ assertStoredValues(uri, RawContacts.DELETED, "1");
+ assertDirty(uri, true);
+ version++;
+ assertEquals(version, getVersion(uri));
+ }
+
+ // TODO fix and enable test
+ public void __testSearchSuggestionsNotInMyContacts() throws Exception {
+
+ long rawContactId = createRawContact();
+ insertStructuredName(rawContactId, "Deer", "Dough");
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("D").build();
+
+ // If the contact is not in the "my contacts" group, nothing should be found
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ assertEquals(0, c.getCount());
+ c.close();
+ }
+
+ public void testSearchSuggestionsByName() throws Exception {
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ false, // organization
+ false, // phone
+ false, // email
+ "D", // query
+ true, // expect icon URI
+ null, "Deer Dough", null);
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ true, // organization
+ false, // phone
+ false, // email
+ "D", // query
+ true, // expect icon URI
+ null, "Deer Dough", "Google");
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ false, // organization
+ true, // phone
+ false, // email
+ "D", // query
+ true, // expect icon URI
+ null, "Deer Dough", "1-800-4664-411");
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ false, // organization
+ false, // phone
+ true, // email
+ "D", // query
+ true, // expect icon URI
+ String.valueOf(Presence.getPresenceIconResourceId(Presence.OFFLINE)),
+ "Deer Dough", "foo@acme.com");
+
+ assertSearchSuggestion(
+ true, // name
+ false, // photo
+ true, // organization
+ false, // phone
+ false, // email
+ "D", // query
+ false, // expect icon URI
+ null, "Deer Dough", "Google");
+ }
+
+ private void assertSearchSuggestion(boolean name, boolean photo, boolean organization,
+ boolean phone, boolean email, String query, boolean expectIcon1Uri, String expectedIcon2,
+ String expectedText1, String expectedText2) throws IOException {
+ ContentValues values = new ContentValues();
+
+ long rawContactId = createRawContact();
+
+ if (name) {
+ insertStructuredName(rawContactId, "Deer", "Dough");
+ }
+
+
+ final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ if (photo) {
+ values.clear();
+ byte[] photoData = loadTestPhoto();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+ values.put(Photo.PHOTO, photoData);
+ mResolver.insert(Data.CONTENT_URI, values);
+ }
+
+ if (organization) {
+ values.clear();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+ values.put(Organization.TYPE, Organization.TYPE_WORK);
+ values.put(Organization.COMPANY, "Google");
+ mResolver.insert(Data.CONTENT_URI, values);
+ }
+
+ if (email) {
+ values.clear();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ values.put(Email.TYPE, Email.TYPE_WORK);
+ values.put(Email.DATA, "foo@acme.com");
+ mResolver.insert(Data.CONTENT_URI, values);
+
+ values.clear();
+ values.put(Presence.IM_PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
+ values.put(Presence.IM_HANDLE, "foo@acme.com");
+ values.put(Presence.IM_ACCOUNT, "foo");
+ values.put(Presence.PRESENCE_STATUS, Presence.OFFLINE);
+ values.put(Presence.PRESENCE_CUSTOM_STATUS, "Coding for Android");
+ mResolver.insert(Presence.CONTENT_URI, values);
+ }
+
+ if (phone) {
+ values.clear();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ values.put(Data.IS_PRIMARY, 1);
+ values.put(Phone.TYPE, Phone.TYPE_HOME);
+ values.put(Phone.NUMBER, "1-800-4664-411");
+ mResolver.insert(Data.CONTENT_URI, values);
+ }
+
+ long contactId = queryContactId(rawContactId);
+ Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath(query).build();
+
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ DatabaseUtils.dumpCursor(c);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ values.clear();
+
+ // SearchManager does not declare a constant for _id
+ values.put("_id", contactId);
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, expectedText1);
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, expectedText2);
+
+ String icon1 = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1));
+ if (expectIcon1Uri) {
+ assertTrue(icon1.startsWith("content:"));
+ } else {
+ assertEquals(String.valueOf(com.android.internal.R.drawable.ic_contact_picture), icon1);
+ }
+
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_2, expectedIcon2);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, contactId);
+ values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, contactId);
+ assertCursorValues(c, values);
+ c.close();
+
+ // Cleanup
+ mResolver.delete(rawContactUri, null, null);
+ }
+
+ public void testSearchSuggestionsByPhoneNumber() throws Exception {
+ ContentValues values = new ContentValues();
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("12345").build();
+
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ DatabaseUtils.dumpCursor(c);
+ assertEquals(2, c.getCount());
+ c.moveToFirst();
+
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Dial number");
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using 12345");
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
+ String.valueOf(com.android.internal.R.drawable.call_contact));
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
+ values.putNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
+ assertCursorValues(c, values);
+
+ c.moveToNext();
+ values.clear();
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Create contact");
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using 12345");
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
+ String.valueOf(com.android.internal.R.drawable.create_contact));
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
+ values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
+ SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
+ assertCursorValues(c, values);
+ c.close();
+ }
+
+ private byte[] loadTestPhoto() throws IOException {
+ final Resources resources = getContext().getResources();
+ InputStream is =
+ resources.openRawResource(com.android.internal.R.drawable.ic_contact_picture);
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1000];
+ int count;
+ while((count = is.read(buffer)) != -1) {
+ os.write(buffer, 0, count);
+ }
+ return os.toByteArray();
+ }
+}
+
diff --git a/tests/src/com/android/providers/contacts/GroupsTest.java b/tests/src/com/android/providers/contacts/GroupsTest.java
new file mode 100644
index 0000000..38715b5
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/GroupsTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Unit tests for {@link Groups} and {@link GroupMembership}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -w \
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class GroupsTest extends BaseContactsProvider2Test {
+
+ private static final String GROUP_GREY = "Grey";
+ private static final String GROUP_RED = "Red";
+ private static final String GROUP_GREEN = "Green";
+ private static final String GROUP_BLUE = "Blue";
+
+ private static final String PERSON_ALPHA = "Alpha";
+ private static final String PERSON_BRAVO = "Bravo";
+ private static final String PERSON_CHARLIE = "Charlie";
+ private static final String PERSON_DELTA = "Delta";
+
+ private static final String PHONE_ALPHA = "555-1111";
+ private static final String PHONE_BRAVO_1 = "555-2222";
+ private static final String PHONE_BRAVO_2 = "555-3333";
+ private static final String PHONE_CHARLIE_1 = "555-4444";
+ private static final String PHONE_CHARLIE_2 = "555-5555";
+
+ public void testGroupSummary() {
+
+ // Clear any existing data before starting
+ // TODO make the provider wipe data automatically
+ ((SynchronousContactsProvider2)mActor.provider).wipeData();
+
+ // Create a handful of groups
+ long groupGrey = mActor.createGroup(GROUP_GREY);
+ long groupRed = mActor.createGroup(GROUP_RED);
+ long groupGreen = mActor.createGroup(GROUP_GREEN);
+ long groupBlue = mActor.createGroup(GROUP_BLUE);
+
+ // Create a handful of contacts
+ long contactAlpha = mActor.createContact(false, PERSON_ALPHA);
+ long contactBravo = mActor.createContact(false, PERSON_BRAVO);
+ long contactCharlie = mActor.createContact(false, PERSON_CHARLIE);
+ long contactCharlieDupe = mActor.createContact(false, PERSON_CHARLIE);
+ long contactDelta = mActor.createContact(false, PERSON_DELTA);
+
+ // Make sure that Charlie was aggregated
+ {
+ long aggCharlie = mActor.getContactForRawContact(contactCharlie);
+ long aggCharlieDupe = mActor.getContactForRawContact(contactCharlieDupe);
+ assertTrue("Didn't aggregate two contacts with identical names",
+ (aggCharlie == aggCharlieDupe));
+ }
+
+ // Add phone numbers to specific contacts
+ mActor.createPhone(contactAlpha, PHONE_ALPHA);
+ mActor.createPhone(contactBravo, PHONE_BRAVO_1);
+ mActor.createPhone(contactBravo, PHONE_BRAVO_2);
+ mActor.createPhone(contactCharlie, PHONE_CHARLIE_1);
+ mActor.createPhone(contactCharlieDupe, PHONE_CHARLIE_2);
+
+ // Add contacts to various mixture of groups. Grey will have all
+ // contacts, Red only with phone numbers, Green with no phones, and Blue
+ // with no contacts at all.
+ mActor.createGroupMembership(contactAlpha, groupGrey);
+ mActor.createGroupMembership(contactBravo, groupGrey);
+ mActor.createGroupMembership(contactCharlie, groupGrey);
+ mActor.createGroupMembership(contactDelta, groupGrey);
+
+ mActor.createGroupMembership(contactAlpha, groupRed);
+ mActor.createGroupMembership(contactBravo, groupRed);
+ mActor.createGroupMembership(contactCharlie, groupRed);
+
+ mActor.createGroupMembership(contactDelta, groupGreen);
+
+ // Walk across groups summary cursor and verify returned counts.
+ final Cursor cursor = mActor.resolver.query(Groups.CONTENT_SUMMARY_URI,
+ Projections.PROJ_SUMMARY, null, null, null);
+
+ // Require that each group has a summary row
+ assertTrue("Didn't return summary for all groups", (cursor.getCount() == 4));
+
+ while (cursor.moveToNext()) {
+ final long groupId = cursor.getLong(Projections.COL_ID);
+ final int summaryCount = cursor.getInt(Projections.COL_SUMMARY_COUNT);
+ final int summaryWithPhones = cursor.getInt(Projections.COL_SUMMARY_WITH_PHONES);
+
+ if (groupId == groupGrey) {
+ // Grey should have four aggregates, three with phones.
+ assertTrue("Incorrect Grey count", (summaryCount == 4));
+ assertTrue("Incorrect Grey with phones count", (summaryWithPhones == 3));
+ } else if (groupId == groupRed) {
+ // Red should have 3 aggregates, all with phones.
+ assertTrue("Incorrect Red count", (summaryCount == 3));
+ assertTrue("Incorrect Red with phones count", (summaryWithPhones == 3));
+ } else if (groupId == groupGreen) {
+ // Green should have 1 aggregate, none with phones.
+ assertTrue("Incorrect Green count", (summaryCount == 1));
+ assertTrue("Incorrect Green with phones count", (summaryWithPhones == 0));
+ } else if (groupId == groupBlue) {
+ // Blue should have no contacts.
+ assertTrue("Incorrect Blue count", (summaryCount == 0));
+ assertTrue("Incorrect Blue with phones count", (summaryWithPhones == 0));
+ } else {
+ fail("Unrecognized group in summary cursor");
+ }
+ }
+
+ }
+
+ public void testGroupDirtySetOnChange() {
+ Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
+ createGroup(mAccount, "gsid1", "title1"));
+ assertDirty(uri, true);
+ clearDirty(uri);
+ assertDirty(uri, false);
+ }
+
+ public void testGroupDirtyClearedWhenSetExplicitly() {
+ Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
+ createGroup(mAccount, "gsid1", "title1"));
+ assertDirty(uri, true);
+
+ ContentValues values = new ContentValues();
+ values.put(Groups.DIRTY, 0);
+ values.put(Groups.NOTES, "other notes");
+ assertEquals(1, mResolver.update(uri, values, null, null));
+
+ assertDirty(uri, false);
+ }
+
+ public void testGroupVersionUpdates() {
+ Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
+ createGroup(mAccount, "gsid1", "title1"));
+ long version = getVersion(uri);
+ ContentValues values = new ContentValues();
+ values.put(Groups.TITLE, "title2");
+ mResolver.update(uri, values, null, null);
+ assertEquals(version + 1, getVersion(uri));
+ }
+
+ private interface Projections {
+ public static final String[] PROJ_SUMMARY = new String[] {
+ Groups._ID,
+ Groups.SUMMARY_COUNT,
+ Groups.SUMMARY_WITH_PHONES,
+ };
+
+ public static final int COL_ID = 0;
+ public static final int COL_SUMMARY_COUNT = 1;
+ public static final int COL_SUMMARY_WITH_PHONES = 2;
+ }
+
+}
diff --git a/tests/src/com/android/providers/contacts/JaroWinklerDistanceTest.java b/tests/src/com/android/providers/contacts/JaroWinklerDistanceTest.java
new file mode 100644
index 0000000..ad34bba
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/JaroWinklerDistanceTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+@SmallTest
+public class JaroWinklerDistanceTest extends TestCase {
+
+ private JaroWinklerDistance mJaroWinklerDistance;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mJaroWinklerDistance = new JaroWinklerDistance(10);
+ }
+
+ public void testExactMatch() {
+ assertFloat(1, "Dwayne", "Dwayne");
+ }
+
+ public void testWinklerBonus() {
+ assertFloat(0.961f, "Martha", "Marhta");
+ assertFloat(0.840f, "Dwayne", "Duane");
+ assertFloat(0.813f, "DIXON", "DICKSONX");
+ }
+
+ public void testJaroDistance() {
+ assertFloat(0.600f, "Donny", "Duane");
+ }
+
+ public void testPoorMatch() {
+ assertFloat(0.467f, "Johny", "Duane");
+ }
+
+ public void testNoMatches() {
+ assertFloat(0, "Abcd", "Efgh");
+ }
+
+ private void assertFloat(float expected, String name1, String name2) {
+ byte[] s1 = Hex.decodeHex(NameNormalizer.normalize(name1));
+ byte[] s2 = Hex.decodeHex(NameNormalizer.normalize(name2));
+
+ float actual = mJaroWinklerDistance.getDistance(s1, s2);
+ assertTrue("Expected Jaro-Winkler distance: " + expected + ", actual: " + actual,
+ Math.abs(actual - expected) < 0.001);
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
new file mode 100644
index 0000000..502010d
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
@@ -0,0 +1,862 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import android.app.SearchManager;
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Contacts;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.Extensions;
+import android.provider.Contacts.GroupMembership;
+import android.provider.Contacts.Groups;
+import android.provider.Contacts.Intents;
+import android.provider.Contacts.Organizations;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Phones;
+import android.provider.Contacts.Photos;
+import android.provider.Contacts.Presence;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Tests for legacy contacts APIs.
+ */
+@LargeTest
+public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
+
+ private static final boolean USE_LEGACY_PROVIDER = false;
+
+ @Override
+ protected Class<? extends ContentProvider> getProviderClass() {
+ return USE_LEGACY_PROVIDER ? ContactsProvider.class : SynchronousContactsProvider2.class;
+ }
+
+ @Override
+ protected String getAuthority() {
+ return Contacts.AUTHORITY;
+ }
+
+ public void testPeopleInsert() {
+ ContentValues values = new ContentValues();
+ putContactValues(values);
+
+ Uri uri = mResolver.insert(People.CONTENT_URI, values);
+ assertStoredValues(uri, values);
+ assertSelection(People.CONTENT_URI, values, People._ID, ContentUris.parseId(uri));
+ }
+
+ public void testPeopleDelete() {
+ ContentValues values = new ContentValues();
+ values.put(People.NAME, "John Doe");
+ Uri personId = mResolver.insert(People.CONTENT_URI, values);
+ mResolver.delete(personId, null, null);
+
+ Cursor c = mResolver.query(personId, null, People.NAME + "='John Doe'" , null, null);
+ assertEquals("Record count after deletion", 0, c.getCount());
+ c.close();
+
+ try {
+ mResolver.query(People.DELETED_CONTENT_URI, null, null, null, null);
+ } catch (UnsupportedOperationException e) {
+ // Expected exception
+ }
+ }
+
+ public void testPeopleFilter() {
+ ContentValues values = new ContentValues();
+ values.put(People.NAME, "Deer Doe");
+ mResolver.insert(People.CONTENT_URI, values);
+
+ values.clear();
+ values.put(People.NAME, "Dear Dough");
+ mResolver.insert(People.CONTENT_URI, values);
+
+ values.clear();
+ values.put(People.NAME, "D.R. Dauwe");
+ mResolver.insert(People.CONTENT_URI, values);
+
+ assertFilteredContacts("d", "Deer Doe", "Dear Dough", "D.R. Dauwe");
+ assertFilteredContacts("de", "Deer Doe", "Dear Dough");
+ assertFilteredContacts("dee", "Deer Doe");
+ assertFilteredContacts("der");
+ }
+
+ public void testDefaultDisplayName() {
+ ContentValues values = new ContentValues();
+ values.put(People.NAME, "John Doe");
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ assertStoredValues(personUri, People.DISPLAY_NAME, "John Doe");
+ }
+
+ public void testPrimaryOrganization() {
+ ContentValues values = new ContentValues();
+ final Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ // Primary
+ values.clear();
+ values.put(Organizations.ISPRIMARY, 1);
+ values.put(Organizations.COMPANY, "Google");
+ values.put(Organizations.TYPE, Organizations.TYPE_WORK);
+ values.put(Organizations.PERSON_ID, personId);
+ Uri orgUri1 = mResolver.insert(Organizations.CONTENT_URI, values);
+
+ // Non-primary
+ values.clear();
+ values.put(Organizations.COMPANY, "Acme");
+ values.put(Organizations.TYPE, Organizations.TYPE_WORK);
+ values.put(Organizations.PERSON_ID, personId);
+ Uri orgUri2 = mResolver.insert(Organizations.CONTENT_URI, values);
+
+ values.clear();
+ values.put(People.PRIMARY_ORGANIZATION_ID, ContentUris.parseId(orgUri1));
+ values.put(People.DISPLAY_NAME, "Google");
+ assertStoredValues(personUri, values);
+
+ // Remove the original primary organization
+ mResolver.delete(orgUri1, null, null);
+
+ values.clear();
+ values.put(People.PRIMARY_ORGANIZATION_ID, ContentUris.parseId(orgUri2));
+ values.put(People.DISPLAY_NAME, "Acme");
+ assertStoredValues(personUri, values);
+
+ // Remove the remaining organization
+ mResolver.delete(orgUri2, null, null);
+
+ values.clear();
+ values.putNull(People.PRIMARY_ORGANIZATION_ID);
+ values.putNull(People.DISPLAY_NAME);
+ assertStoredValues(personUri, values);
+ }
+
+ public void testPrimaryPhone() {
+ ContentValues values = new ContentValues();
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ // Primary
+ values.clear();
+ values.put(Phones.ISPRIMARY, 1);
+ values.put(Phones.TYPE, Phones.TYPE_WORK);
+ values.put(Phones.PERSON_ID, personId);
+ values.put(Phones.NUMBER, "12345");
+ Uri phoneUri1 = mResolver.insert(Phones.CONTENT_URI, values);
+
+ // Non-primary
+ values.clear();
+ values.put(Phones.TYPE, Phones.TYPE_WORK);
+ values.put(Phones.PERSON_ID, personId);
+ values.put(Phones.NUMBER, "67890");
+ Uri phoneUri2 = mResolver.insert(Phones.CONTENT_URI, values);
+
+ values.clear();
+ values.put(People.PRIMARY_PHONE_ID, ContentUris.parseId(phoneUri1));
+ values.put(People.DISPLAY_NAME, "12345");
+ assertStoredValues(personUri, values);
+
+ // Remove the primary phone number
+ mResolver.delete(phoneUri1, null, null);
+
+ values.clear();
+ values.put(People.PRIMARY_PHONE_ID, ContentUris.parseId(phoneUri2));
+ values.put(People.DISPLAY_NAME, "67890");
+ assertStoredValues(personUri, values);
+
+ // Remove the remaining phone number
+ mResolver.delete(phoneUri2, null, null);
+
+ values.clear();
+ values.putNull(People.PRIMARY_PHONE_ID);
+ values.putNull(People.DISPLAY_NAME);
+ assertStoredValues(personUri, values);
+ }
+
+ public void testPrimaryEmail() {
+ ContentValues values = new ContentValues();
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ // Primary
+ values.clear();
+ values.put(ContactMethods.PERSON_ID, personId);
+ values.put(ContactMethods.KIND, Contacts.KIND_EMAIL);
+ values.put(ContactMethods.TYPE, ContactMethods.TYPE_HOME);
+ values.put(ContactMethods.DATA, "foo@acme.com");
+ values.put(ContactMethods.ISPRIMARY, 1);
+ Uri emailUri1 = mResolver.insert(ContactMethods.CONTENT_URI, values);
+
+ // Non-primary
+ values.clear();
+ values.put(ContactMethods.PERSON_ID, personId);
+ values.put(ContactMethods.KIND, Contacts.KIND_EMAIL);
+ values.put(ContactMethods.TYPE, ContactMethods.TYPE_WORK);
+ values.put(ContactMethods.DATA, "bar@acme.com");
+ Uri emailUri2 = mResolver.insert(ContactMethods.CONTENT_URI, values);
+
+ values.clear();
+ values.put(People.PRIMARY_EMAIL_ID, ContentUris.parseId(emailUri1));
+ values.put(People.DISPLAY_NAME, "foo@acme.com");
+ assertStoredValues(personUri, values);
+
+ // Remove the primary email
+ mResolver.delete(emailUri1, null, null);
+
+ values.clear();
+ values.put(People.PRIMARY_EMAIL_ID, ContentUris.parseId(emailUri2));
+ values.put(People.DISPLAY_NAME, "bar@acme.com");
+ assertStoredValues(personUri, values);
+
+ // Remove the remaining email
+ mResolver.delete(emailUri2, null, null);
+
+ values.clear();
+ values.putNull(People.PRIMARY_EMAIL_ID);
+ values.putNull(People.DISPLAY_NAME);
+ assertStoredValues(personUri, values);
+ }
+
+ public void testMarkAsContacted() {
+ ContentValues values = new ContentValues();
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ int timesContactedBefore =
+ Integer.parseInt(getStoredValue(personUri, People.TIMES_CONTACTED));
+ long timeBefore = System.currentTimeMillis();
+ People.markAsContacted(mResolver, personId);
+ long timeAfter = System.currentTimeMillis();
+
+ long lastContacted = Long.parseLong(getStoredValue(personUri, People.LAST_TIME_CONTACTED));
+ int timesContactedAfter =
+ Integer.parseInt(getStoredValue(personUri, People.TIMES_CONTACTED));
+
+ assertTrue(lastContacted >= timeBefore);
+ assertTrue(lastContacted <= timeAfter);
+ assertEquals(timesContactedAfter, timesContactedBefore + 1);
+ }
+
+ public void testOrganizationsInsert() {
+ ContentValues values = new ContentValues();
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ values.clear();
+ values.put(Organizations.COMPANY, "Sierra");
+ values.put(Organizations.PERSON_ID, personId);
+ values.put(Organizations.TYPE, Organizations.TYPE_CUSTOM);
+ values.put(Organizations.LABEL, "Club");
+ values.put(Organizations.TITLE, "Member");
+ values.put(Organizations.ISPRIMARY, 1);
+
+ Uri uri = mResolver.insert(Organizations.CONTENT_URI, values);
+ assertStoredValues(uri, values);
+ assertSelection(Organizations.CONTENT_URI, values,
+ Organizations._ID, ContentUris.parseId(uri));
+
+ assertPersonIdConstraint(Organizations.CONTENT_URI, Organizations.TYPE,
+ Organizations.TYPE_WORK);
+
+ assertTypeAndLabelConstraints(Organizations.CONTENT_URI, Organizations.PERSON_ID, personId,
+ Organizations.TYPE, Organizations.TYPE_CUSTOM, Organizations.TYPE_OTHER,
+ Organizations.LABEL);
+ }
+
+ public void testPhonesInsert() {
+ ContentValues values = new ContentValues();
+ putContactValues(values);
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ values.clear();
+ values.put(Phones.PERSON_ID, personId);
+ values.put(Phones.TYPE, Phones.TYPE_CUSTOM);
+ values.put(Phones.LABEL, "Directory");
+ values.put(Phones.NUMBER, "1-800-4664-411");
+ values.put(Phones.ISPRIMARY, 1);
+
+ Uri uri = mResolver.insert(Phones.CONTENT_URI, values);
+
+ // Adding another value to assert
+ values.put(Phones.NUMBER_KEY, "11446640081");
+
+ // The result is joined with People
+ putContactValues(values);
+ assertStoredValues(uri, values);
+ assertSelection(Phones.CONTENT_URI, values,
+ Phones._ID, ContentUris.parseId(uri));
+
+ // Access the phone through People
+ Uri twigUri = Uri.withAppendedPath(personUri, People.Phones.CONTENT_DIRECTORY);
+ assertStoredValues(twigUri, values);
+
+ // Now the person should be joined with Phone
+ values.clear();
+ putContactValues(values);
+ values.put(People.TYPE, Phones.TYPE_CUSTOM);
+ values.put(People.LABEL, "Directory");
+ values.put(People.NUMBER, "1-800-4664-411");
+ assertStoredValues(personUri, values);
+
+ assertPersonIdConstraint(Phones.CONTENT_URI, Phones.TYPE, Phones.TYPE_WORK);
+
+ assertTypeAndLabelConstraints(Phones.CONTENT_URI, Phones.PERSON_ID, personId, Phones.TYPE,
+ Phones.TYPE_CUSTOM, Phones.TYPE_OTHER, Phones.LABEL);
+ }
+
+ public void testEmailInsert() {
+ assertContactMethodInsert(Contacts.KIND_EMAIL, ContactMethods.TYPE_CUSTOM,
+ "Some other way", "foo@acme.com", null, true);
+ }
+
+ public void testImInsert() {
+ assertContactMethodInsert(Contacts.KIND_IM, ContactMethods.TYPE_CUSTOM, "Some other way",
+ "Foo", "Bar", true);
+ }
+
+ public void testPostalInsert() {
+ assertContactMethodInsert(Contacts.KIND_POSTAL, ContactMethods.TYPE_CUSTOM,
+ "Some other way", "Foo", "Bar", true);
+ }
+
+ private void assertContactMethodInsert(int kind, int type, String label, String data,
+ String auxData, boolean primary) {
+ ContentValues values = new ContentValues();
+ putContactValues(values);
+ final Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ values.clear();
+ values.put(ContactMethods.PERSON_ID, personId);
+ values.put(ContactMethods.KIND, kind);
+ values.put(ContactMethods.TYPE, type);
+ values.put(ContactMethods.LABEL, label);
+ values.put(ContactMethods.DATA, data);
+ values.put(ContactMethods.AUX_DATA, auxData);
+ values.put(ContactMethods.ISPRIMARY, primary ? 1 : 0);
+
+ Uri uri = mResolver.insert(ContactMethods.CONTENT_URI, values);
+
+ // The result is joined with People
+ putContactValues(values);
+ assertStoredValues(uri, values);
+ assertSelection(ContactMethods.CONTENT_URI, values,
+ ContactMethods._ID, ContentUris.parseId(uri));
+
+ // Access the contact method through People
+ Uri twigUri = Uri.withAppendedPath(personUri, People.ContactMethods.CONTENT_DIRECTORY);
+ assertStoredValues(twigUri, values);
+
+ assertPersonIdConstraint(ContactMethods.CONTENT_URI, ContactMethods.TYPE,
+ ContactMethods.TYPE_WORK);
+
+ assertTypeAndLabelConstraints(ContactMethods.CONTENT_URI, ContactMethods.PERSON_ID,
+ personId, ContactMethods.TYPE, ContactMethods.TYPE_CUSTOM,
+ ContactMethods.TYPE_OTHER, ContactMethods.LABEL);
+ }
+
+ public void testExtensionsInsert() {
+ ContentValues values = new ContentValues();
+ final Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ values.clear();
+ values.put(Extensions.PERSON_ID, personId);
+ values.put(Extensions.NAME, "Foo");
+ values.put(Extensions.VALUE, "Bar");
+
+ Uri uri = mResolver.insert(Extensions.CONTENT_URI, values);
+ assertStoredValues(uri, values);
+ assertSelection(Extensions.CONTENT_URI, values,
+ Extensions._ID, ContentUris.parseId(uri));
+
+ // Access the extensions through People
+ Uri twigUri = Uri.withAppendedPath(personUri, People.Extensions.CONTENT_DIRECTORY);
+ assertStoredValues(twigUri, values);
+ }
+
+ public void testGroupsInsert() {
+ ContentValues values = new ContentValues();
+ values.put(Groups.NAME, "Galois");
+ values.put(Groups.NOTES, "Abel");
+ values.put(Groups.SYSTEM_ID, "12345");
+
+ Uri groupUri = mResolver.insert(Groups.CONTENT_URI, values);
+ assertStoredValues(groupUri, values);
+ }
+
+ public void testGroupMembershipsInsert() {
+ ContentValues values = new ContentValues();
+ values.put(Groups.NAME, "Galois");
+ values.put(Groups.NOTES, "Abel");
+ Uri groupUri = mResolver.insert(Groups.CONTENT_URI, values);
+
+ values.clear();
+ values.put(People.NAME, "Klein");
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+
+ long groupId = ContentUris.parseId(groupUri);
+ long personId = ContentUris.parseId(personUri);
+
+ values.clear();
+ values.put(GroupMembership.GROUP_ID, groupId);
+ values.put(GroupMembership.PERSON_ID, personId);
+ Uri membershipUri = mResolver.insert(GroupMembership.CONTENT_URI, values);
+ assertStoredValues(membershipUri, values);
+ assertSelection(GroupMembership.CONTENT_URI, values,
+ GroupMembership._ID, ContentUris.parseId(membershipUri));
+
+ Uri personsGroupsUri = Uri.withAppendedPath(personUri, GroupMembership.CONTENT_DIRECTORY);
+ assertStoredValues(personsGroupsUri, values);
+ }
+
+ public void testAddToGroup() {
+ ContentValues values = new ContentValues();
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ values.clear();
+ values.put(Groups.NAME, "Galois");
+ Uri groupUri = mResolver.insert(Groups.CONTENT_URI, values);
+
+ People.addToGroup(mResolver, personId, "Galois");
+
+ values.clear();
+ values.put(GroupMembership.GROUP_ID, ContentUris.parseId(groupUri));
+ values.put(GroupMembership.PERSON_ID, personId);
+
+ Uri personsGroupsUri = Uri.withAppendedPath(personUri, GroupMembership.CONTENT_DIRECTORY);
+ assertStoredValues(personsGroupsUri, values);
+ }
+
+ public void testPresenceInsertMatchOnHandle() {
+ ContentValues values = new ContentValues();
+ putContactValues(values);
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ String encodedProtocol =
+ ContactMethods.encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
+
+ values.clear();
+ values.put(ContactMethods.PERSON_ID, personId);
+ values.put(ContactMethods.KIND, Contacts.KIND_IM);
+ values.put(ContactMethods.TYPE, ContactMethods.TYPE_HOME);
+ values.put(ContactMethods.DATA, "Android");
+ values.put(ContactMethods.AUX_DATA, encodedProtocol);
+ mResolver.insert(ContactMethods.CONTENT_URI, values);
+
+ values.clear();
+ values.put(Presence.IM_PROTOCOL, encodedProtocol);
+ values.put(Presence.IM_HANDLE, "Android");
+ values.put(Presence.IM_ACCOUNT, "foo");
+ values.put(Presence.PRESENCE_STATUS, Presence.OFFLINE);
+ values.put(Presence.PRESENCE_CUSTOM_STATUS, "Coding for Android");
+
+ Uri presenceUri = mResolver.insert(Presence.CONTENT_URI, values);
+ assertSelection(Presence.CONTENT_URI, values,
+ Presence._ID, ContentUris.parseId(presenceUri));
+
+ values.put(Presence.PERSON_ID, personId);
+ assertStoredValues(presenceUri, values);
+
+ // Now the person should be joined with Presence
+ values.clear();
+ putContactValues(values);
+ values.put(People.IM_PROTOCOL, encodedProtocol);
+ values.put(People.IM_HANDLE, "Android");
+ values.put(People.IM_ACCOUNT, "foo");
+ values.put(People.PRESENCE_STATUS, Presence.OFFLINE);
+ values.put(People.PRESENCE_CUSTOM_STATUS, "Coding for Android");
+ assertStoredValues(personUri, values);
+ }
+
+ public void testPresenceInsertMatchOnEmail() {
+ ContentValues values = new ContentValues();
+ putContactValues(values);
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ String protocol =
+ ContactMethods.encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
+
+ values.clear();
+ values.put(ContactMethods.PERSON_ID, personId);
+ values.put(ContactMethods.KIND, Contacts.KIND_EMAIL);
+ values.put(ContactMethods.TYPE, ContactMethods.TYPE_HOME);
+ values.put(ContactMethods.DATA, "Android@android.com");
+ mResolver.insert(ContactMethods.CONTENT_URI, values);
+
+ values.clear();
+ values.put(Presence.IM_PROTOCOL, protocol);
+ values.put(Presence.IM_HANDLE, "Android@android.com");
+ values.put(Presence.IM_ACCOUNT, "foo");
+ values.put(Presence.PRESENCE_STATUS, Presence.OFFLINE);
+ values.put(Presence.PRESENCE_CUSTOM_STATUS, "Coding for Android");
+
+ Uri presenceUri = mResolver.insert(Presence.CONTENT_URI, values);
+
+ // FIXME person_id was not available in legacy ContactsProvider
+ // values.put(Presence.PERSON_ID, personId);
+ assertStoredValues(presenceUri, values);
+ assertSelection(Presence.CONTENT_URI, values,
+ Presence._ID, ContentUris.parseId(presenceUri));
+
+ // Now the person should be joined with Presence
+ values.clear();
+ putContactValues(values);
+ values.put(People.IM_PROTOCOL, protocol);
+ values.put(People.IM_HANDLE, "Android@android.com");
+ values.put(People.IM_ACCOUNT, "foo");
+ values.put(People.PRESENCE_STATUS, Presence.OFFLINE);
+ values.put(People.PRESENCE_CUSTOM_STATUS, "Coding for Android");
+ assertStoredValues(personUri, values);
+ }
+
+ public void testPhotoUpdate() throws Exception {
+ byte[] photo = loadTestPhoto();
+
+ ContentValues values = new ContentValues();
+ Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+
+ values.clear();
+ values.put(Photos.DATA, photo);
+ values.put(Photos.LOCAL_VERSION, "10");
+ // FIXME this column was unavailable for update in legacy ContactsProvider
+ // values.put(Photos.DOWNLOAD_REQUIRED, 1);
+ values.put(Photos.EXISTS_ON_SERVER, 1);
+ values.put(Photos.SYNC_ERROR, "404 does not exist");
+
+ Uri photoUri = Uri.withAppendedPath(personUri, Photos.CONTENT_DIRECTORY);
+ mResolver.update(photoUri, values, null, null);
+ assertStoredValues(photoUri, values);
+ }
+
+ /**
+ * Capturing the search suggestion requirements in test cases as a reference.
+ */
+ public void testSearchSuggestionsNotInMyContacts() throws Exception {
+
+ // We don't provide compatibility for search suggestions
+ if (!USE_LEGACY_PROVIDER) {
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+ putContactValues(values);
+ mResolver.insert(People.CONTENT_URI, values);
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(Contacts.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("D").build();
+
+ // If the contact is not in the "my contacts" group, nothing should be found
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ assertEquals(0, c.getCount());
+ c.close();
+ }
+
+ /**
+ * Capturing the search suggestion requirements in test cases as a reference.
+ */
+ public void testSearchSuggestionsByName() throws Exception {
+
+ // We don't provide compatibility for search suggestions
+ if (!USE_LEGACY_PROVIDER) {
+ return;
+ }
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ true, // organization
+ false, // phone
+ false, // email
+ "D", // query
+ true, // expect icon URI
+ null, "Deer Dough", "Google");
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ false, // organization
+ true, // phone
+ false, // email
+ "D", // query
+ true, // expect icon URI
+ null, "Deer Dough", "1-800-4664-411");
+
+ assertSearchSuggestion(
+ true, // name
+ true, // photo
+ false, // organization
+ false, // phone
+ true, // email
+ "D", // query
+ true, // expect icon URI
+ String.valueOf(Presence.getPresenceIconResourceId(Presence.OFFLINE)),
+ "Deer Dough", "foo@acme.com");
+
+ assertSearchSuggestion(
+ true, // name
+ false, // photo
+ true, // organization
+ false, // phone
+ false, // email
+ "D", // query
+ false, // expect icon URI
+ null, "Deer Dough", "Google");
+ }
+
+ private void assertSearchSuggestion(boolean name, boolean photo, boolean organization,
+ boolean phone, boolean email, String query, boolean expectIcon1Uri, String expectedIcon2,
+ String expectedText1, String expectedText2) throws IOException {
+ ContentValues values = new ContentValues();
+
+ if (name) {
+ values.put(People.NAME, "Deer Dough");
+ }
+
+ final Uri personUri = mResolver.insert(People.CONTENT_URI, values);
+ long personId = ContentUris.parseId(personUri);
+
+ People.addToMyContactsGroup(mResolver, personId);
+
+ if (photo) {
+ values.clear();
+ byte[] photoData = loadTestPhoto();
+ values.put(Photos.DATA, photoData);
+ values.put(Photos.LOCAL_VERSION, "1");
+ values.put(Photos.EXISTS_ON_SERVER, 0);
+ Uri photoUri = Uri.withAppendedPath(personUri, Photos.CONTENT_DIRECTORY);
+ mResolver.update(photoUri, values, null, null);
+ }
+
+ if (organization) {
+ values.clear();
+ values.put(Organizations.ISPRIMARY, 1);
+ values.put(Organizations.COMPANY, "Google");
+ values.put(Organizations.TYPE, Organizations.TYPE_WORK);
+ values.put(Organizations.PERSON_ID, personId);
+ mResolver.insert(Organizations.CONTENT_URI, values);
+ }
+
+ if (email) {
+ values.clear();
+ values.put(ContactMethods.PERSON_ID, personId);
+ values.put(ContactMethods.KIND, Contacts.KIND_EMAIL);
+ values.put(ContactMethods.TYPE, ContactMethods.TYPE_HOME);
+ values.put(ContactMethods.DATA, "foo@acme.com");
+ values.put(ContactMethods.ISPRIMARY, 1);
+ mResolver.insert(ContactMethods.CONTENT_URI, values);
+
+
+ String protocol = ContactMethods
+ .encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
+ values.clear();
+ values.put(Presence.IM_PROTOCOL, protocol);
+ values.put(Presence.IM_HANDLE, "foo@acme.com");
+ values.put(Presence.IM_ACCOUNT, "foo");
+ values.put(Presence.PRESENCE_STATUS, Presence.OFFLINE);
+ values.put(Presence.PRESENCE_CUSTOM_STATUS, "Coding for Android");
+ mResolver.insert(Presence.CONTENT_URI, values);
+ }
+
+ if (phone) {
+ values.clear();
+ values.put(Phones.PERSON_ID, personId);
+ values.put(Phones.TYPE, Phones.TYPE_HOME);
+ values.put(Phones.NUMBER, "1-800-4664-411");
+ values.put(Phones.ISPRIMARY, 1);
+ mResolver.insert(Phones.CONTENT_URI, values);
+ }
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(Contacts.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath(query).build();
+
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ values.clear();
+
+ String icon1 = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1));
+ if (expectIcon1Uri) {
+ assertTrue(icon1.startsWith("content:"));
+ } else {
+ assertEquals(String.valueOf(com.android.internal.R.drawable.ic_contact_picture), icon1);
+ }
+
+ // SearchManager does not declare a constant for _id
+ values.put("_id", personId);
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_2, expectedIcon2);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, personId);
+ values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, personId);
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, expectedText1);
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, expectedText2);
+ assertCursorValues(c, values);
+ c.close();
+
+ // Cleanup
+ mResolver.delete(personUri, null, null);
+ }
+
+ /**
+ * Capturing the search suggestion requirements in test cases as a reference.
+ */
+ public void testSearchSuggestionsByPhoneNumber() throws Exception {
+
+ // We don't provide compatibility for search suggestions
+ if (!USE_LEGACY_PROVIDER) {
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(Contacts.AUTHORITY)
+ .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("12345").build();
+
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ assertEquals(2, c.getCount());
+ c.moveToFirst();
+
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Execute");
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "");
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
+ String.valueOf(com.android.internal.R.drawable.call_contact));
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
+ values.putNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
+ assertCursorValues(c, values);
+
+ c.moveToNext();
+ values.clear();
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Dial number");
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using 12345");
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
+ String.valueOf(com.android.internal.R.drawable.create_contact));
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+ Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
+ values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
+ SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
+ assertCursorValues(c, values);
+ c.close();
+ }
+
+ private void putContactValues(ContentValues values) {
+ // Populating only unhidden columns
+ values.put(People.NAME, "Deer Dough");
+ values.put(People.PHONETIC_NAME, "Dear Doe");
+ values.put(People.NOTES, "Cash Cow");
+ values.put(People.TIMES_CONTACTED, 3);
+ values.put(People.LAST_TIME_CONTACTED, 10);
+ values.put(People.CUSTOM_RINGTONE, "ringtone");
+ values.put(People.SEND_TO_VOICEMAIL, 1);
+ values.put(People.STARRED, 1);
+ }
+
+ private byte[] loadTestPhoto() throws IOException {
+ final Resources resources = getContext().getResources();
+ InputStream is =
+ resources.openRawResource(com.android.internal.R.drawable.ic_contact_picture);
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1000];
+ int count;
+ while((count = is.read(buffer)) != -1) {
+ os.write(buffer, 0, count);
+ }
+ return os.toByteArray();
+ }
+
+ private void assertFilteredContacts(String filter, String... expectedNames) {
+ Uri filterUri = Uri.withAppendedPath(People.CONTENT_FILTER_URI, filter);
+ Cursor c = mResolver.query(filterUri, null, null, null, null);
+ try {
+ assertEquals("Record count", expectedNames.length, c.getCount());
+ int column = c.getColumnIndex(People.NAME);
+ for (int i = 0; i < expectedNames.length; i++) {
+ c.moveToNext();
+ assertEquals(expectedNames[i], c.getString(column));
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ private void assertPersonIdConstraint(Uri uri, String typeColumn, int defaultType) {
+ ContentValues values = new ContentValues();
+ values.put(typeColumn, defaultType);
+ try {
+ mResolver.insert(uri, values);
+ fail("Inserted row without person ID");
+ } catch (Exception e) {
+ // Exception expected
+ }
+ }
+
+ private void assertTypeAndLabelConstraints(Uri uri, String personIdColumn, long personId,
+ String typeColumn, int defaultType, int otherType, String labelColumn) {
+ ContentValues values = new ContentValues();
+ values.put(personIdColumn, personId);
+ values.put(typeColumn, defaultType);
+ try {
+ mResolver.insert(uri, values);
+ fail("Inserted row with custom type but without label");
+ } catch (Exception e) {
+ // Exception expected
+ }
+
+ values.clear();
+ values.put(personIdColumn, personId);
+ try {
+ mResolver.insert(uri, values);
+ fail("Inserted row without either type or label");
+ } catch (Exception e) {
+ // Exception expected
+ }
+
+ values.clear();
+ values.put(personIdColumn, personId);
+ values.put(typeColumn, otherType);
+ values.put(labelColumn, "Foo");
+ try {
+ mResolver.insert(uri, values);
+ fail("Inserted row with both type and label");
+ } catch (Exception e) {
+ // Exception expected
+ }
+ }
+
+ @Override
+ protected void assertSelection(Uri uri, ContentValues values, String idColumn, long id) {
+ if (USE_LEGACY_PROVIDER) {
+ // A bug in the legacy ContactsProvider prevents us from using the
+ // _id column in selection.
+ super.assertSelection(uri, values, null, 0);
+ } else {
+ super.assertSelection(uri, values, idColumn, id);
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/NameNormalizerTest.java b/tests/src/com/android/providers/contacts/NameNormalizerTest.java
new file mode 100644
index 0000000..08d873f
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/NameNormalizerTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.contacts;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link NameNormalizer}.
+ */
+@SmallTest
+public class NameNormalizerTest extends TestCase {
+
+ public void testDifferent() {
+ final String name1 = NameNormalizer.normalize("Helene");
+ final String name2 = NameNormalizer.normalize("Francesca");
+ assertFalse(name2.equals(name1));
+ }
+
+ public void testAccents() {
+ final String name1 = NameNormalizer.normalize("Helene");
+ final String name2 = NameNormalizer.normalize("H\u00e9l\u00e8ne");
+ assertTrue(name2.equals(name1));
+ }
+
+ public void testMixedCase() {
+ final String name1 = NameNormalizer.normalize("Helene");
+ final String name2 = NameNormalizer.normalize("hELENE");
+ assertTrue(name2.equals(name1));
+ }
+
+ public void testNonLetters() {
+ final String name1 = NameNormalizer.normalize("h-e?l e+n=e");
+ final String name2 = NameNormalizer.normalize("helene");
+ assertTrue(name2.equals(name1));
+ }
+
+ public void testComplexityCase() {
+ assertTrue(NameNormalizer.compareComplexity("Helene", "helene") > 0);
+ }
+
+ public void testComplexityAccent() {
+ assertTrue(NameNormalizer.compareComplexity("H\u00e9lene", "Helene") > 0);
+ }
+
+ public void testComplexityLength() {
+ assertTrue(NameNormalizer.compareComplexity("helene2009", "helene") > 0);
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/NameSplitterTest.java b/tests/src/com/android/providers/contacts/NameSplitterTest.java
new file mode 100644
index 0000000..91ce025
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/NameSplitterTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import junit.framework.TestCase;
+
+import com.android.providers.contacts.NameSplitter.Name;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+/**
+ * Tests for {@link NameSplitter}.
+ */
+@SmallTest
+public class NameSplitterTest extends TestCase {
+ private NameSplitter mNameSplitter;
+ private Name mName;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mNameSplitter = new NameSplitter("Mr, Ms, Mrs", "d', st, st., von", "Jr, M.D., MD, D.D.S.",
+ "&, AND");
+ mName = new Name();
+ }
+
+ public void testNull() {
+ assertSplitName(null, null, null, null, null, null);
+ }
+
+ public void testEmpty() {
+ assertSplitName("", null, null, null, null, null);
+ }
+
+ public void testSpaces() {
+ assertSplitName(" ", null, null, null, null, null);
+ }
+
+ public void testLastName() {
+ assertSplitName("Smith", null, null, null, "Smith", null);
+ }
+
+ public void testFirstLastName() {
+ assertSplitName("John Smith", null, "John", null, "Smith", null);
+ }
+
+ public void testFirstMiddleLastName() {
+ assertSplitName("John Edward Smith", null, "John", "Edward", "Smith", null);
+ }
+
+ public void testThreeNamesAndLastName() {
+ assertSplitName("John Edward Kevin Smith", null, "John Edward", "Kevin", "Smith", null);
+ }
+
+ public void testPrefixFirstLastName() {
+ assertSplitName("Mr. John Smith", "Mr", "John", null, "Smith", null);
+ assertSplitName("Mr.John Smith", "Mr", "John", null, "Smith", null);
+ }
+
+ public void testFirstLastNameSuffix() {
+ assertSplitName("John Smith Jr.", null, "John", null, "Smith", "Jr");
+ }
+
+ public void testFirstLastNameSuffixWithDot() {
+ assertSplitName("John Smith M.D.", null, "John", null, "Smith", "M.D.");
+ assertSplitName("John Smith D D S", null, "John", null, "Smith", "D D S");
+ }
+
+ public void testFirstSuffixLastName() {
+ assertSplitName("John von Smith", null, "John", null, "von Smith", null);
+ }
+
+ public void testFirstSuffixLastNameWithDot() {
+ assertSplitName("John St.Smith", null, "John", null, "St. Smith", null);
+ }
+
+ public void testPrefixFirstMiddleLast() {
+ assertSplitName("Mr. John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
+ assertSplitName("Mr.John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
+ }
+
+ public void testPrefixFirstMiddleLastSuffix() {
+ assertSplitName("Mr. John Kevin Smith Jr.", "Mr", "John", "Kevin", "Smith", "Jr");
+ }
+
+ public void testPrefixFirstMiddlePrefixLastSuffixWrongCapitalization() {
+ assertSplitName("MR. john keVin VON SmiTh JR.", "MR", "john", "keVin", "VON SmiTh", "JR");
+ }
+
+ public void testPrefixLastSuffix() {
+ assertSplitName("von Smith Jr.", null, null, null, "von Smith", "Jr");
+ }
+
+ public void testTwoNamesAndLastNameWithAmpersand() {
+ assertSplitName("John & Edward Smith", null, "John & Edward", null, "Smith", null);
+ assertSplitName("John and Edward Smith", null, "John and Edward", null, "Smith", null);
+ }
+
+ public void testWithMiddleInitialAndNoDot() {
+ assertSplitName("John E. Smith", null, "John", "E", "Smith", null);
+ }
+
+ public void testWithLongFirstNameAndDot() {
+ assertSplitName("John Ed. K. Smith", null, "John Ed.", "K", "Smith", null);
+ }
+
+ private void assertSplitName(String fullName, String prefix, String givenNames,
+ String middleName, String lastName, String suffix) {
+ mNameSplitter.split(mName, fullName);
+ assertEquals(prefix, mName.getPrefix());
+ assertEquals(givenNames, mName.getGivenNames());
+ assertEquals(middleName, mName.getMiddleName());
+ assertEquals(lastName, mName.getFamilyName());
+ assertEquals(suffix, mName.getSuffix());
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java b/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
new file mode 100644
index 0000000..1263fe7
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import static com.android.providers.contacts.ContactsActor.PACKAGE_BLUE;
+import static com.android.providers.contacts.ContactsActor.PACKAGE_GREEN;
+import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
+import static com.android.providers.contacts.ContactsActor.PACKAGE_RED;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.Data;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Unit tests for {@link RestrictionExceptions}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -w \
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class RestrictionExceptionsTest extends AndroidTestCase {
+ private static final String TAG = "RestrictionExceptionsTest";
+
+ private static ContactsActor mGrey;
+ private static ContactsActor mRed;
+ private static ContactsActor mGreen;
+ private static ContactsActor mBlue;
+
+ private static final String PHONE_GREY = "555-1111";
+ private static final String PHONE_RED = "555-2222";
+ private static final String PHONE_GREEN = "555-3333";
+ private static final String PHONE_BLUE = "555-4444";
+
+ private static final String GENERIC_NAME = "Smith";
+
+ public RestrictionExceptionsTest() {
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ final Context overallContext = this.getContext();
+
+ // Build each of our specific actors in their own Contexts
+ mGrey = new ContactsActor(overallContext, PACKAGE_GREY,
+ SynchronousContactsProvider2.class, ContactsContract.AUTHORITY);
+ mRed = new ContactsActor(overallContext, PACKAGE_RED,
+ SynchronousContactsProvider2.class, ContactsContract.AUTHORITY);
+ mGreen = new ContactsActor(overallContext, PACKAGE_GREEN,
+ SynchronousContactsProvider2.class, ContactsContract.AUTHORITY);
+ mBlue = new ContactsActor(overallContext, PACKAGE_BLUE,
+ SynchronousContactsProvider2.class, ContactsContract.AUTHORITY);
+
+ // TODO make the provider wipe data automatically
+ ((SynchronousContactsProvider2)mGrey.provider).wipeData();
+ }
+
+ /**
+ * Create various contacts that are both open and restricted, and assert
+ * that both {@link Contacts#IS_RESTRICTED} and
+ * {@link RestrictionExceptions} are being applied correctly.
+ */
+ public void __testDataRestriction() {
+
+ // Grey creates an unprotected contact
+ long greyContact = mGrey.createContact(false);
+ long greyData = mGrey.createPhone(greyContact, PHONE_GREY);
+ long greyAgg = mGrey.getContactForRawContact(greyContact);
+
+ // Assert that both Grey and Blue can read contact
+ assertTrue("Owner of unrestricted contact unable to read",
+ (mGrey.getDataCountForContact(greyAgg) == 1));
+ assertTrue("Non-owner of unrestricted contact unable to read",
+ (mBlue.getDataCountForContact(greyAgg) == 1));
+
+ // Red grants protected access to itself
+ mRed.updateException(PACKAGE_RED, PACKAGE_RED, true);
+
+ // Red creates a protected contact
+ long redContact = mRed.createContact(true);
+ long redData = mRed.createPhone(redContact, PHONE_RED);
+ long redAgg = mRed.getContactForRawContact(redContact);
+
+ // Assert that only Red can read contact
+ assertTrue("Owner of restricted contact unable to read",
+ (mRed.getDataCountForContact(redAgg) == 1));
+ assertTrue("Non-owner of restricted contact able to read",
+ (mBlue.getDataCountForContact(redAgg) == 0));
+ assertTrue("Non-owner of restricted contact able to read",
+ (mGreen.getDataCountForContact(redAgg) == 0));
+
+ try {
+ // Blue tries to grant an exception for Red data, which should throw
+ // exception. If it somehow worked, fail this test.
+ mBlue.updateException(PACKAGE_RED, PACKAGE_BLUE, true);
+ fail("Non-owner able to grant restriction exception");
+
+ } catch (RuntimeException e) {
+ }
+
+ // Red grants exception to Blue for contact
+ mRed.updateException(PACKAGE_RED, PACKAGE_BLUE, true);
+
+ // Both Blue and Red can read Red contact, but still not Green
+ assertTrue("Owner of restricted contact unable to read",
+ (mRed.getDataCountForContact(redAgg) == 1));
+ assertTrue("Non-owner with restriction exception unable to read",
+ (mBlue.getDataCountForContact(redAgg) == 1));
+ assertTrue("Non-owner of restricted contact able to read",
+ (mGreen.getDataCountForContact(redAgg) == 0));
+
+ // Red revokes exception to Blue
+ mRed.updateException(PACKAGE_RED, PACKAGE_BLUE, false);
+
+ // Assert that only Red can read contact
+ assertTrue("Owner of restricted contact unable to read",
+ (mRed.getDataCountForContact(redAgg) == 1));
+ assertTrue("Non-owner of restricted contact able to read",
+ (mBlue.getDataCountForContact(redAgg) == 0));
+ assertTrue("Non-owner of restricted contact able to read",
+ (mGreen.getDataCountForContact(redAgg) == 0));
+
+ }
+
+ /**
+ * Create an aggregate that has multiple contacts with various levels of
+ * protected data, and ensure that {@link Contacts#CONTENT_SUMMARY_URI}
+ * details don't expose {@link Contacts#IS_RESTRICTED} data.
+ */
+ public void __testAggregateSummary() {
+
+ // Red grants exceptions to itself and Grey
+ mRed.updateException(PACKAGE_RED, PACKAGE_RED, true);
+ mRed.updateException(PACKAGE_RED, PACKAGE_GREY, true);
+
+ // Red creates a protected contact
+ long redContact = mRed.createContact(true);
+ long redName = mRed.createName(redContact, GENERIC_NAME);
+ long redPhone = mRed.createPhone(redContact, PHONE_RED);
+
+ // Blue grants exceptions to itself and Grey
+ mBlue.updateException(PACKAGE_BLUE, PACKAGE_BLUE, true);
+ mBlue.updateException(PACKAGE_BLUE, PACKAGE_GREY, true);
+
+ // Blue creates a protected contact
+ long blueContact = mBlue.createContact(true);
+ long blueName = mBlue.createName(blueContact, GENERIC_NAME);
+ long bluePhone = mBlue.createPhone(blueContact, PHONE_BLUE);
+
+ // Set the super-primary phone number to Red
+ mRed.setSuperPrimaryPhone(redPhone);
+
+ // Make sure both aggregates were joined
+ long singleAgg;
+ {
+ long redAgg = mRed.getContactForRawContact(redContact);
+ long blueAgg = mBlue.getContactForRawContact(blueContact);
+ assertTrue("Two contacts with identical name not aggregated correctly",
+ (redAgg == blueAgg));
+ singleAgg = redAgg;
+ }
+
+ // Grey and Red querying summary should see Red phone. Blue shouldn't
+ // see any summary data, since it's own data is protected and it's not
+ // the super-primary. Green shouldn't know this aggregate exists.
+ assertTrue("Participant with restriction exception reading incorrect summary",
+ (mGrey.getPrimaryPhoneId(singleAgg) == redPhone));
+ assertTrue("Participant with super-primary restricted data reading incorrect summary",
+ (mRed.getPrimaryPhoneId(singleAgg) == redPhone));
+ assertTrue("Participant with non-super-primary restricted data reading incorrect summary",
+ (mBlue.getPrimaryPhoneId(singleAgg) == 0));
+ assertTrue("Non-participant able to discover aggregate existance",
+ (mGreen.getPrimaryPhoneId(singleAgg) == 0));
+
+ // Add an unprotected Grey contact into the mix
+ long greyContact = mGrey.createContact(false);
+ long greyName = mGrey.createName(greyContact, GENERIC_NAME);
+ long greyPhone = mGrey.createPhone(greyContact, PHONE_GREY);
+
+ // Set the super-primary phone number to Blue
+ mBlue.setSuperPrimaryPhone(bluePhone);
+
+ // Make sure all three aggregates were joined
+ {
+ long redAgg = mRed.getContactForRawContact(redContact);
+ long blueAgg = mBlue.getContactForRawContact(blueContact);
+ long greyAgg = mGrey.getContactForRawContact(greyContact);
+ assertTrue("Three contacts with identical name not aggregated correctly",
+ (redAgg == blueAgg) && (blueAgg == greyAgg));
+ singleAgg = redAgg;
+ }
+
+ // Grey and Blue querying summary should see Blue phone. Red should see
+ // the Grey phone in its summary, since it's the unprotected fallback.
+ // Red doesn't see its own phone number because it's not super-primary,
+ // and is protected. Again, green shouldn't know this exists.
+ assertTrue("Participant with restriction exception reading incorrect summary",
+ (mGrey.getPrimaryPhoneId(singleAgg) == bluePhone));
+ assertTrue("Participant with non-super-primary restricted data reading incorrect summary",
+ (mRed.getPrimaryPhoneId(singleAgg) == greyPhone));
+ assertTrue("Participant with super-primary restricted data reading incorrect summary",
+ (mBlue.getPrimaryPhoneId(singleAgg) == bluePhone));
+ assertTrue("Non-participant couldn't find unrestricted primary through summary",
+ (mGreen.getPrimaryPhoneId(singleAgg) == greyPhone));
+
+ }
+
+ /**
+ * Create a contact that is completely restricted and isolated in its own
+ * aggregate, and make sure that another actor can't detect its existence.
+ */
+ public void __testRestrictionSilence() {
+ Cursor cursor;
+
+ // Green grants exception to itself
+ mGreen.updateException(PACKAGE_GREEN, PACKAGE_GREEN, true);
+
+ // Green creates a protected contact
+ long greenContact = mGreen.createContact(true);
+ long greenData = mGreen.createPhone(greenContact, PHONE_GREEN);
+ long greenAgg = mGreen.getContactForRawContact(greenContact);
+
+ // AGGREGATES
+ cursor = mRed.resolver
+ .query(Contacts.CONTENT_URI, Projections.PROJ_ID, null, null, null);
+ while (cursor.moveToNext()) {
+ assertTrue("Discovered restricted contact",
+ (cursor.getLong(Projections.COL_ID) != greenAgg));
+ }
+ cursor.close();
+
+ // AGGREGATES_ID
+ cursor = mRed.resolver.query(ContentUris.withAppendedId(Contacts.CONTENT_URI, greenAgg),
+ Projections.PROJ_ID, null, null, null);
+ assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+ cursor.close();
+
+ // AGGREGATES_DATA
+ cursor = mRed.resolver.query(Uri.withAppendedPath(ContentUris.withAppendedId(
+ Contacts.CONTENT_URI, greenAgg), Contacts.Data.CONTENT_DIRECTORY),
+ Projections.PROJ_ID, null, null, null);
+ assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+ cursor.close();
+
+ // AGGREGATES_SUMMARY
+ cursor = mRed.resolver.query(Contacts.CONTENT_SUMMARY_URI, Projections.PROJ_ID, null,
+ null, null);
+ while (cursor.moveToNext()) {
+ assertTrue("Discovered restricted contact",
+ (cursor.getLong(Projections.COL_ID) != greenAgg));
+ }
+ cursor.close();
+
+ // AGGREGATES_SUMMARY_ID
+ cursor = mRed.resolver.query(ContentUris.withAppendedId(Contacts.CONTENT_SUMMARY_URI,
+ greenAgg), Projections.PROJ_ID, null, null, null);
+ assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+ cursor.close();
+
+ // TODO: AGGREGATES_SUMMARY_FILTER
+ // TODO: =========================
+
+ // TODO: AGGREGATION_SUGGESTIONS
+ // TODO: =======================
+
+ // CONTACTS
+ cursor = mRed.resolver.query(RawContacts.CONTENT_URI, Projections.PROJ_ID,
+ null, null, null);
+ while (cursor.moveToNext()) {
+ assertTrue("Discovered restricted contact",
+ (cursor.getLong(Projections.COL_ID) != greenContact));
+ }
+ cursor.close();
+
+ // CONTACTS_ID
+ cursor = mRed.resolver.query(ContentUris
+ .withAppendedId(RawContacts.CONTENT_URI, greenContact), Projections.PROJ_ID, null,
+ null, null);
+ assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+ cursor.close();
+
+ // CONTACTS_DATA
+ cursor = mRed.resolver.query(Uri.withAppendedPath(ContentUris.withAppendedId(
+ RawContacts.CONTENT_URI, greenContact), RawContacts.Data.CONTENT_DIRECTORY),
+ Projections.PROJ_ID, null, null, null);
+ assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+ cursor.close();
+
+ // TODO: CONTACTS_FILTER_EMAIL
+ // TODO: =====================
+
+ // DATA
+ cursor = mRed.resolver.query(Data.CONTENT_URI, Projections.PROJ_ID, null, null, null);
+ while (cursor.moveToNext()) {
+ assertTrue("Discovered restricted contact",
+ (cursor.getLong(Projections.COL_ID) != greenData));
+ }
+ cursor.close();
+
+ // DATA_ID
+ cursor = mRed.resolver.query(ContentUris.withAppendedId(Data.CONTENT_URI, greenData),
+ Projections.PROJ_ID, null, null, null);
+ assertTrue("Discovered restricted contact", (cursor.getCount() == 0));
+ cursor.close();
+
+ // TODO: PHONE_LOOKUP
+ // TODO: ============
+
+ }
+
+ private interface Projections {
+ static final String[] PROJ_ID = new String[] {
+ BaseColumns._ID,
+ };
+
+ static final int COL_ID = 0;
+ }
+
+}
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
new file mode 100644
index 0000000..fa3c2a6
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import android.content.Context;
+
+/**
+ * A version of {@link ContactsProvider2} class that performs aggregation
+ * synchronously and wipes all data at construction time.
+ */
+public class SynchronousContactsProvider2 extends ContactsProvider2 {
+ private static Boolean sDataWiped = false;
+ private static OpenHelper mOpenHelper;
+
+ public SynchronousContactsProvider2() {
+ super(new SynchronousAggregationScheduler());
+ }
+
+ @Override
+ protected OpenHelper getOpenHelper(final Context context) {
+ if (mOpenHelper == null) {
+ mOpenHelper = new OpenHelper(context);
+ }
+ return mOpenHelper;
+ }
+
+ @Override
+ public boolean onCreate() {
+ boolean created = super.onCreate();
+ synchronized (sDataWiped) {
+ if (!sDataWiped) {
+ sDataWiped = true;
+ wipeData();
+ }
+ }
+ return created;
+ }
+
+ private static class SynchronousAggregationScheduler extends ContactAggregationScheduler {
+
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void stop() {
+ }
+
+ @Override
+ long currentTime() {
+ return 0;
+ }
+
+ @Override
+ void runDelayed() {
+ super.run();
+ }
+
+ }
+}