am cbc0db8f: Merge "Switching to use try-with-resources"
* commit 'cbc0db8f9e1382f4bf60b4e707c5eb6bc20f2677':
Switching to use try-with-resources
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 20213d3..ce0bda7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3,21 +3,22 @@
android:sharedUserId="android.uid.shared"
android:sharedUserLabel="@string/sharedUserLabel">
- <uses-permission android:name="android.permission.READ_CONTACTS" />
- <uses-permission android:name="android.permission.WRITE_CONTACTS" />
- <uses-permission android:name="android.permission.READ_PROFILE" />
- <uses-permission android:name="android.permission.WRITE_PROFILE" />
- <uses-permission android:name="android.permission.GET_ACCOUNTS" />
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BIND_DIRECTORY_SEARCH" />
- <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
- <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
<uses-permission android:name="android.permission.MANAGE_USERS" />
- <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
- <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL" />
- <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL" />
+ <uses-permission android:name="android.permission.PROCESS_PHONE_ACCOUNT_REGISTRATION" />
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+ <uses-permission android:name="android.permission.SEND_CALL_LOG_CHANGE" />
+ <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
+
+ <permission
+ android:name="android.permission.SEND_CALL_LOG_CHANGE"
+ android:label="Broadcast that a change happened to the call log."
+ android:protectionLevel="signature|system"/>
<application android:process="android.process.acore"
android:label="@string/app_label"
@@ -29,6 +30,7 @@
android:label="@string/provider_label"
android:multiprocess="false"
android:exported="true"
+ android:grantUriPermissions="true"
android:readPermission="android.permission.READ_CONTACTS"
android:writePermission="android.permission.WRITE_CONTACTS">
<path-permission
@@ -68,6 +70,14 @@
</intent-filter>
</receiver>
+ <receiver android:name="PhoneAccountRegistrationReceiver"
+ android:permission="android.permission.BROADCAST_PHONE_ACCOUNT_REGISTRATION">
+ <!-- Broadcast sent after a phone account is registered in telecom. -->
+ <intent-filter>
+ <action android:name="android.telecom.action.PHONE_ACCOUNT_REGISTERED"/>
+ </intent-filter>
+ </receiver>
+
<receiver android:name="PackageIntentReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index e3d207e..15b960d 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android-kernprogramme"</string>
<string name="app_label" msgid="3389954322874982620">"Kontakte-berging"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontakte"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Gradeer kontaktedatabasis op."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontakte-opgradering benodig meer geheue."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Gradeer berging vir kontakte op"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Raak om die opgradering te voltooi."</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index ea0d11d..ad32b65 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"የAndroid ኮር ትግበራ"</string>
<string name="app_label" msgid="3389954322874982620">"የዕውቂያ ማከማቻ"</string>
<string name="provider_label" msgid="6012150850819899907">"እውቅያዎች"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"የእውቂያዎችን ውሂብ ጎታ በማሻሻል ላይ፡፡"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"የእውቅያዎች አልቅ ተጨማሪ ማህደረ ትውስታ ይፈልጋል"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"ለእውቅያዎች ማከማቻ በማሻሻል ላይ"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"አሻሽሉን ለማላቅ ይንኩ"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index ae60217..847813b 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"تطبيقات Android المركزية"</string>
<string name="app_label" msgid="3389954322874982620">"تخزين جهات الاتصال"</string>
<string name="provider_label" msgid="6012150850819899907">"جهات الاتصال"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"جارٍ ترقية قاعدة بيانات جهات الاتصال."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"تتطلب ترقية جهات الاتصال مزيدًا من الذاكرة."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"ترقية وحدة التخزين لجهات الاتصال"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"المس لإكمال عملية الترقية."</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index d5f62d0..bf3f06b 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Основни приложения на Android"</string>
<string name="app_label" msgid="3389954322874982620">"Хранилище на контакти"</string>
<string name="provider_label" msgid="6012150850819899907">"Контакти"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Базата от данни на контактите се надстройва."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"За надстройването на контактите е необходима още памет."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Хранилището за контакти се надстройва"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Докоснете, за да завършите надстройването."</string>
diff --git a/res/values-bn-rBD/strings.xml b/res/values-bn-rBD/strings.xml
index 86f8df7..2b8974d 100644
--- a/res/values-bn-rBD/strings.xml
+++ b/res/values-bn-rBD/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android কোর অ্যাপ্লিকেশানগুলি"</string>
<string name="app_label" msgid="3389954322874982620">"পরিচিতিগুলির সংগ্রহস্থল"</string>
<string name="provider_label" msgid="6012150850819899907">"পরিচিতিগুলি"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"পরিচিতির ডেটাবেস আপগ্রেড করা হচ্ছে৷"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"পরিচিতিগুলি আপগ্রেড করার জন্য আরো সঞ্চয়স্থানের দরকার৷"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"পরিচিতিগুলির জন্য সঞ্চয়স্থান আপগ্রেড করা হচ্ছে"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"আপগ্রেড সম্পূর্ণ করতে স্পর্শ করুন৷"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index ba820b5..009aff9 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Aplicacions bàsiques d\'Android"</string>
<string name="app_label" msgid="3389954322874982620">"Emmagatzematge de contactes"</string>
<string name="provider_label" msgid="6012150850819899907">"Contactes"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"S\'està actualitzant la base de dades dels contactes."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Per actualitzar els contactes cal tenir més memòria."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"S\'està actualitzant l\'emmagatzematge per als contactes"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toca-ho per completar l\'actualització."</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index cfb8402..e28b255 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Úložiště kontaktů"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontakty"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Probíhá upgrade databáze kontaktů."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Upgrade kontaktů vyžaduje více paměti."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Probíhá upgrade úložiště kontaktů"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dotykem dokončíte upgrade."</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 58f36ee..cb96932 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android-kerneapplikationer"</string>
<string name="app_label" msgid="3389954322874982620">"Lagring af kontakter"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontakter"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Opgraderer databasen med kontaktpersoner."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Opgradering af kontaktpersoner kræver mere hukommelse."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Opgraderer lagring af kontaktpersoner"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Tryk for at fuldføre opgraderingen."</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index e872e7d..a5d1b06 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Kontakte-Speicher"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontakte"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Kontaktdatenbank wird aktualisiert..."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontakte-Upgrade erfordert mehr Speicher."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Speicher für Kontakte wird aktualisiert..."</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Zum Abschluss des Upgrades berühren"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 80d155f..c7bbb89 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Βασικές εφαρμογές Android"</string>
<string name="app_label" msgid="3389954322874982620">"Χώρος αποθήκευσης επαφών"</string>
<string name="provider_label" msgid="6012150850819899907">"Επαφές"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Αναβάθμιση βάσης δεδομένων επαφών"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Απαιτείται περισσότερη μνήμη για την αναβάθμιση των επαφών."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Αναβάθμιση χώρου αποθήκευσης για επαφές"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Αγγίξτε για να ολοκληρώσετε την αναβάθμιση."</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..2279f29
--- /dev/null
+++ b/res/values-en-rAU/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
+ <string name="app_label" msgid="3389954322874982620">"Contacts Storage"</string>
+ <string name="provider_label" msgid="6012150850819899907">"Contacts"</string>
+ <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Contact upgrade needs more memory."</string>
+ <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Upgrading storage for contacts"</string>
+ <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Touch to complete the upgrade."</string>
+ <string name="default_directory" msgid="93961630309570294">"Contacts"</string>
+ <string name="local_invisible_directory" msgid="705244318477396120">"Other"</string>
+ <string name="voicemail_from_column" msgid="435732568832121444">"Voicemail from "</string>
+ <string name="debug_dump_title" msgid="4916885724165570279">"Copy contacts database"</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"You are about to 1) make a copy of your database which includes all contacts related information and all call log to the internal storage, and 2) email it. Remember to delete the copy as soon as you have successfully copied it off the device or the email is received."</string>
+ <string name="debug_dump_delete_button" msgid="7832879421132026435">"Delete now"</string>
+ <string name="debug_dump_start_button" msgid="2837506913757600001">"Start"</string>
+ <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Choose a programme to send your file"</string>
+ <string name="debug_dump_email_subject" msgid="108188398416385976">"Contacts Db attached"</string>
+ <string name="debug_dump_email_body" msgid="4577749800871444318">"Attached is my contacts database with all my contacts information. Handle with care."</string>
+</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 1face82..2279f29 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Contacts Storage"</string>
<string name="provider_label" msgid="6012150850819899907">"Contacts"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Upgrading contacts database."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Contact upgrade needs more memory."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Upgrading storage for contacts"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Touch to complete the upgrade."</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 1face82..2279f29 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Contacts Storage"</string>
<string name="provider_label" msgid="6012150850819899907">"Contacts"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Upgrading contacts database."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Contact upgrade needs more memory."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Upgrading storage for contacts"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Touch to complete the upgrade."</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index eab80ff..fb9e6e1 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Aplicaciones básicas de Android"</string>
<string name="app_label" msgid="3389954322874982620">"Almacenamiento para contactos"</string>
<string name="provider_label" msgid="6012150850819899907">"Contactos"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Actualizando la base de datos de los contactos"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"La actualización de contactos necesita más memoria."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Actualizando el espacio de almacenamiento de los contactos"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toca para completar la actualización."</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 98e25cb..42f75e1 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Aplicaciones básicas de Android"</string>
<string name="app_label" msgid="3389954322874982620">"Información de los contactos"</string>
<string name="provider_label" msgid="6012150850819899907">"Contactos"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Actualizando la base de datos de contactos..."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"La actualización de contactos necesita más memoria."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Actualizando el almacenamiento para contactos..."</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toca para completar la actualización."</string>
diff --git a/res/values-et-rEE/strings.xml b/res/values-et-rEE/strings.xml
index 65ee64d..7676f73 100644
--- a/res/values-et-rEE/strings.xml
+++ b/res/values-et-rEE/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Androidi tuumrakendused"</string>
<string name="app_label" msgid="3389954322874982620">"Kontaktiruum"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontaktid"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Kontaktide andmebaasi uuendamine."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontaktisikute uuendamiseks on vaja rohkem mälu"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Kontaktide salvestusruumi uuendamine"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Puudutage uuendamise lõpuleviimiseks."</string>
diff --git a/res/values-eu-rES/strings.xml b/res/values-eu-rES/strings.xml
index d7a86cd..73da412 100644
--- a/res/values-eu-rES/strings.xml
+++ b/res/values-eu-rES/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android-en nukleoko aplikazioak"</string>
<string name="app_label" msgid="3389954322874982620">"Kontaktuen biltegia"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontaktuak"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Kontaktuen datu-basea bertsio-berritzen."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Memoria gehiago behar da kontaktuak bertsio-berritzeko."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Kontaktuen biltegia bertsio-berritzea"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Bertsio-berritzea osatzeko, ukitu hau."</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 086f085..ae71e76 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"حافظه مخاطبین"</string>
<string name="provider_label" msgid="6012150850819899907">"مخاطبین"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"ارتقا پایگاه داده مخاطبین."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"برای ارتقای مخاطبین به حافظه بیشتری نیاز است."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"ارتقا حافظه برای مخاطبین"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"برای تکمیل ارتقا لمس کنید."</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index c6f2bdd..3608237 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Androidin ydinsovellukset"</string>
<string name="app_label" msgid="3389954322874982620">"Yhteystietojen tallennus"</string>
<string name="provider_label" msgid="6012150850819899907">"Yhteystiedot"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Päivitetään yhteystietojen tietokantaa."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Yhteystietojen päivittämiseen tarvitaan enemmän muistia."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Päivitetään yhteystietojen tallennustilaa"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Suorita päivitys loppuun koskettamalla."</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 9daf1a0..20836ac 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Applications de base Android"</string>
<string name="app_label" msgid="3389954322874982620">"Liste des contacts"</string>
<string name="provider_label" msgid="6012150850819899907">"Contacts"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Mise à jour de la base de données des contacts en cours…"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"La mise à jour des contacts requiert plus de mémoire."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Mise à jour du stockage des contacts"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Appuyez pour terminer la mise à jour."</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index f5ddfad..3e06adc 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Applications de base Android"</string>
<string name="app_label" msgid="3389954322874982620">"Liste des contacts"</string>
<string name="provider_label" msgid="6012150850819899907">"Contacts"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Mise à jour de la base de données des contacts en cours…"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"La mise à jour des contacts requiert plus de mémoire."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Mise à jour du stockage des contacts"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Appuyez pour terminer la mise à jour."</string>
diff --git a/res/values-gl-rES/strings.xml b/res/values-gl-rES/strings.xml
index 3c947d5..ac85165 100644
--- a/res/values-gl-rES/strings.xml
+++ b/res/values-gl-rES/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Aplicacións básicas de Android"</string>
<string name="app_label" msgid="3389954322874982620">"Almacenamento de contactos"</string>
<string name="provider_label" msgid="6012150850819899907">"Contactos"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Actualizando a base de datos de contactos."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"A actualización dos contactos necesita máis memoria."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Actualizando almacenamento dos contactos"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toca para completar a actualización."</string>
diff --git a/res/values-gu-rIN/strings.xml b/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000..7f58e0c
--- /dev/null
+++ b/res/values-gu-rIN/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core એપ્લિકેશન્સ"</string>
+ <string name="app_label" msgid="3389954322874982620">"સંપર્કો સ્ટોરેજ"</string>
+ <string name="provider_label" msgid="6012150850819899907">"સંપર્કો"</string>
+ <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"સંપર્કો અપગ્રેડને વધુ મેમરીની જરૂર છે."</string>
+ <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"સંપર્કો માટે સ્ટોરેજને અપગ્રેડ કરી રહ્યાં છે"</string>
+ <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"અપગ્રેડ પૂર્ણ કરવા માટે ટચ કરો."</string>
+ <string name="default_directory" msgid="93961630309570294">"સંપર્કો"</string>
+ <string name="local_invisible_directory" msgid="705244318477396120">"અન્ય"</string>
+ <string name="voicemail_from_column" msgid="435732568832121444">"આમના તરફથી વૉઇસમેઇલ "</string>
+ <string name="debug_dump_title" msgid="4916885724165570279">"સંપર્કો ડેટાબેસ કૉપિ કરો"</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"તમે 1) તમારા ડેટાબેસની કૉપિ આંતરિક સ્ટોરેજ પર કરવામાં છો કે જેમાં બધા સંપર્કોથી સંબંધિત માહિતી અને તમામ કૉલ લૉગ શામેલ છે અને 2) તેને ઇમેઇલ કરવામાં છો. જેમ જ તમે ઉપકરણ પરથી તેની સફળતાપૂર્વક કૉપિ કરી લો અથવા ઇમેઇલ પ્રાપ્ત થઈ જાય તે પછી કૉપિને કાઢી નાખવાનું યાદ રાખો."</string>
+ <string name="debug_dump_delete_button" msgid="7832879421132026435">"હમણાં કાઢી નાખો"</string>
+ <string name="debug_dump_start_button" msgid="2837506913757600001">"પ્રારંભ કરો"</string>
+ <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"તમારી ફાઇલ મોકલવા માટે એક પ્રોગ્રામ પસંદ કરો"</string>
+ <string name="debug_dump_email_subject" msgid="108188398416385976">"સંપર્કો Db જોડ્યો"</string>
+ <string name="debug_dump_email_body" msgid="4577749800871444318">"જોડેલ છે તે મારી બધી સંપર્ક માહિતી સાથેનો મારો સંપર્કો ડેટાબેસ છે. સાવધાનીથી રાખો."</string>
+</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 496c990..3bf42f6 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"संपर्क मेमोरी"</string>
<string name="provider_label" msgid="6012150850819899907">"संपर्क"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"संपर्क डेटाबेस अपग्रेड हो रहा है."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"संपर्क अपग्रेड के लिए अधिक मेमोरी की आवश्यकता होती है."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"संपर्कों के लिए मेमोरी अपग्रेड करना"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"अपग्रेड पूर्ण करने के लिए स्पर्श करें."</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index c8e56b4..e9159e7 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Matične aplikacije za Android"</string>
<string name="app_label" msgid="3389954322874982620">"Prostor za pohranu kontakata"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontakti"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Nadogradnja baze podataka kontakata."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Za nadogradnju kontakata potrebno je više memorije."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Nadogradnja pohrane za kontakte"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dodirnite da biste dovršili nadogradnju."</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index ea6c206..f7ea732 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Alap Android-alkalmazások"</string>
<string name="app_label" msgid="3389954322874982620">"Névjegytár"</string>
<string name="provider_label" msgid="6012150850819899907">"Címtár"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Névjegyek adatbázisának frissítése."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"A névjegyek frissítéséhez több memóriára van szükség."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Névjegyek tárolójának frissítése"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Érintse meg a frissítés befejezéséhez."</string>
diff --git a/res/values-hy-rAM/strings.xml b/res/values-hy-rAM/strings.xml
index d614033..14475f7 100644
--- a/res/values-hy-rAM/strings.xml
+++ b/res/values-hy-rAM/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Կոնտակտների պահոց"</string>
<string name="provider_label" msgid="6012150850819899907">"Կոնտակտներ"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Կոնտակտների տվյալների շտեմարանի թարմացում:"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Կոնտակտների թարմացումը պահանջում է ավելի շատ հիշողություն:"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Կոնտակտների պահոցի թարմացում"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Հպեք` թարմացումն ավարտելու համար:"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 236ac9c..171db41 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Aplikasi Inti Android"</string>
<string name="app_label" msgid="3389954322874982620">"Penyimpanan Kontak"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontak"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Meningkatkan versi basis kontak."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Peningkatan versi kontak memerlukan lebih banyak memori."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Meningkatkan versi penyimpanan untuk kontak"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Sentuh untuk menyelesaikan peningkatan versi."</string>
diff --git a/res/values-is-rIS/strings.xml b/res/values-is-rIS/strings.xml
index 86ea9c4..7a6e04f 100644
--- a/res/values-is-rIS/strings.xml
+++ b/res/values-is-rIS/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Kjarnaforrit Android"</string>
<string name="app_label" msgid="3389954322874982620">"Tengiliðageymsla"</string>
<string name="provider_label" msgid="6012150850819899907">"Tengiliðir"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Uppfærir tengiliðagagnagrunn."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Uppfærsla tengiliða krefst meira minnis."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Uppfærsla á tengiliðageymslu"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Snertu til að ljúka uppfærslunni."</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index a90ddce..1685b2e 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Applicazioni di base Android"</string>
<string name="app_label" msgid="3389954322874982620">"Archiviazione contatti"</string>
<string name="provider_label" msgid="6012150850819899907">"Contatti"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Upgrade del database dei contatti."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"L\'upgrade dei contatti richiede più memoria."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Upgrade dell\'archivio dei contatti"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Tocca per completare l\'upgrade."</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index db4efe9..1cbd521 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"אחסון אנשי קשר"</string>
<string name="provider_label" msgid="6012150850819899907">"אנשי קשר"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"משדרג את מסד הנתונים של אנשי הקשר."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"שדרוג אנשי הקשר מחייב זיכרון נוסף."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"משדרג את האחסון של אנשי קשר"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"גע כדי לבצע את השדרוג."</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index d24aeff..4c95269 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"アドレス帳"</string>
<string name="provider_label" msgid="6012150850819899907">"連絡先"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"連絡先データベースをアップグレードしています。"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"連絡先のアップグレードに必要なメモリが不足しています。"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"連絡先用ストレージのアップグレード"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"アップグレードを完了するにはタップしてください。"</string>
diff --git a/res/values-ka-rGE/strings.xml b/res/values-ka-rGE/strings.xml
index fd7f9b3..3567fd7 100644
--- a/res/values-ka-rGE/strings.xml
+++ b/res/values-ka-rGE/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android-ის ბირთვის აპები"</string>
<string name="app_label" msgid="3389954322874982620">"კონტაქტების მეხსიერება"</string>
<string name="provider_label" msgid="6012150850819899907">"კონტაქტები"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"მონაცემთა ბაზის ახალ ვერსიაზე გადასვლა."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"კონტაქტების ახალ ვერსიაზე გადასვლას ესაჭიროება მეტი მეხსიერება."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"კონტაქტების მეხსიერების ახალ ვერსიაზე გადასვლა"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"შეეხეთ ახალ ვერსიაზე გადასვლის დასასრულებლად."</string>
diff --git a/res/values-kk-rKZ/strings.xml b/res/values-kk-rKZ/strings.xml
index 3085800..5d4b6f2 100644
--- a/res/values-kk-rKZ/strings.xml
+++ b/res/values-kk-rKZ/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Қолданбалары"</string>
<string name="app_label" msgid="3389954322874982620">"Контактілер жады"</string>
<string name="provider_label" msgid="6012150850819899907">"Контактілер"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Контактілер дерекқорын жаңарту."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Контактілерді жаңарту көбірек жад кеңістігін қажет етеді."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Контактілер үшін жадты жаңарту"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Жаңартуды аяқтау үшін түрту."</string>
diff --git a/res/values-km-rKH/strings.xml b/res/values-km-rKH/strings.xml
index efe0534..c754053 100644
--- a/res/values-km-rKH/strings.xml
+++ b/res/values-km-rKH/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"កម្មវិធីសំខាន់ៗរបស់ Android"</string>
<string name="app_label" msgid="3389954322874982620">"ឧបករណ៍ផ្ទុកទំនាក់ទំនង"</string>
<string name="provider_label" msgid="6012150850819899907">"ទំនាក់ទំនង"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"ធ្វើបច្ចុប្បន្នភាពមូលដ្ឋានទិន្នន័យទំនាក់ទំនង។"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"ការធ្វើបច្ចុប្បន្នភាពទំនាក់ទំនងត្រូវការអង្គចងចាំច្រើន"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"ធ្វើបច្ចុប្បន្នភាពឧបករណ៍ផ្ទុកទំនាក់ទំនង"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"ប៉ះដើម្បីបញ្ចប់ការធ្វើបច្ចុប្បន្នភាព។"</string>
diff --git a/res/values-kn-rIN/strings.xml b/res/values-kn-rIN/strings.xml
index 43df94a..5a67519 100644
--- a/res/values-kn-rIN/strings.xml
+++ b/res/values-kn-rIN/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core ಅಪ್ಲಿಕೇಶನ್ಗಳು"</string>
<string name="app_label" msgid="3389954322874982620">"ಸಂಪರ್ಕಗಳ ಸಂಗ್ರಹಣೆ"</string>
<string name="provider_label" msgid="6012150850819899907">"ಸಂಪರ್ಕಗಳು"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"ಸಂಪರ್ಕಗಳ ಡೇಟಾಬೇಸ್ ಅಪ್ಗ್ರೇಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"ಸಂಪರ್ಕಗಳ ಅಪ್ಗ್ರೇಡ್ಗೆ ಹೆಚ್ಚಿನ ಸ್ಮರಣೆಯ ಅಗತ್ಯವಿದೆ."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"ಸಂಪರ್ಕಗಳಿಗಾಗಿ ಸಂಗ್ರಹಣೆಯನ್ನು ಅಪ್ಗ್ರೇಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"ಅಪ್ಗ್ರೇಡ್ ಪೂರ್ಣಗೊಳಿಸಲು ಸ್ಪರ್ಶಿಸಿ."</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 3777cf1..ad1cb75 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core 앱"</string>
<string name="app_label" msgid="3389954322874982620">"주소록 저장소"</string>
<string name="provider_label" msgid="6012150850819899907">"주소록"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"주소록 데이터베이스 업그레이드 중"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"주소록을 업그레이드하려면 메모리가 더 필요합니다."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"주소록을 위한 저장소 업그레이드 중"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"업그레이드를 완료하려면 터치하세요."</string>
diff --git a/res/values-ky-rKG/strings.xml b/res/values-ky-rKG/strings.xml
index 9f92411..a886e2d 100644
--- a/res/values-ky-rKG/strings.xml
+++ b/res/values-ky-rKG/strings.xml
@@ -21,7 +21,6 @@
<skip />
<!-- no translation found for provider_label (6012150850819899907) -->
<skip />
- <string name="upgrade_msg" msgid="8640807392794309950">"Байланыштар корун жаңыртуу."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Байланыштарды жаңыртууга көбүрөөк орун керек."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Байланыштар үчүн сактагыч жаңыртылууда"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Жаңыртууну аягына чыгарыш үчүн тийиңиз."</string>
diff --git a/res/values-lo-rLA/strings.xml b/res/values-lo-rLA/strings.xml
index b98c3cd..35ce0c9 100644
--- a/res/values-lo-rLA/strings.xml
+++ b/res/values-lo-rLA/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"ແອັບພລິເຄຊັນຫຼັກຂອງ Android"</string>
<string name="app_label" msgid="3389954322874982620">"ບ່ອນຈັດເກັບຂໍ້ມູນລາຍຊື່ຜູ່ຕິດຕໍ່"</string>
<string name="provider_label" msgid="6012150850819899907">"ລາຍຊື່ຜູ່ຕິດຕໍ່"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"ກຳລັງອັບເກຣດຖານຂໍ້ມູນລາຍຊື່ຜູ່ຕິດຕໍ່."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"ການອັບເກຣດລາຍຊື່ຜູ່ຕິດຕໍ່ ຈະຕ້ອງໃຊ້ໜ່ວຍຄວາມຈຳເພີ່ມຕື່ມ."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"ອັບເກຣດບ່ອນຈັດເກັບຂໍ້ມູນລາຍຊື່ຜູ່ຕິດຕໍ່"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"ແຕະເພື່ອສິ້ນສຸດຂັ້ນຕອນການອັບເກຣດ."</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 9bd7b9e..eaa968c 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Pagrindinės „Android“ programos"</string>
<string name="app_label" msgid="3389954322874982620">"Adresatų saugykla"</string>
<string name="provider_label" msgid="6012150850819899907">"Adresinė"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Naujovinami kontaktų duomenys."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Norint naujovinti kontaktus, reikia daugiau atminties."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Naujovinama kontaktų atmintinė"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Palieskite, kad baigtumėte naujovinti."</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 6d9bb04..9227f58 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Kontaktpersonu krātuve"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontaktpersonas"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Notiek kontaktpersonu datu bāzes jaunināšana."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Lai jauninātu kontaktpersonas, nepieciešams vairāk vietas atmiņā."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Notiek kontaktpersonu krātuves jaunināšana"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Pieskarieties, lai pabeigtu jaunināšanu."</string>
diff --git a/res/values-mk-rMK/strings.xml b/res/values-mk-rMK/strings.xml
index f5afdf4..1c3a4cc 100644
--- a/res/values-mk-rMK/strings.xml
+++ b/res/values-mk-rMK/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Основни апликации на Android"</string>
<string name="app_label" msgid="3389954322874982620">"Меморирање контакти"</string>
<string name="provider_label" msgid="6012150850819899907">"Контакти"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Надградување база на податоци со контакти."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"За надградбата на контакти е потребно повеќе меморија."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Надградување меморија за контакти"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Допри за да заврши надградбата."</string>
diff --git a/res/values-ml-rIN/strings.xml b/res/values-ml-rIN/strings.xml
index a831308..5493e4b 100644
--- a/res/values-ml-rIN/strings.xml
+++ b/res/values-ml-rIN/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android കോർ അപ്ലിക്കേഷനുകൾ"</string>
<string name="app_label" msgid="3389954322874982620">"കോൺടാക്റ്റുകളുടെ സംഭരണം"</string>
<string name="provider_label" msgid="6012150850819899907">"വിലാസങ്ങൾ"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"കോൺടാക്റ്റുകളുടെ ഡാറ്റാബേസ് അപ്ഗ്രേഡുചെയ്യുന്നു."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"കോൺടാക്റ്റുകൾ അപ്ഗ്രേഡുചെയ്യാൻ കൂടുതൽ മെമ്മറി ആവശ്യമാണ്."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"കോൺടാക്റ്റുകൾക്കായുള്ള സംഭരണം അപ്ഗ്രേഡുചെയ്യുന്നു"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"അപ്ഗ്രേഡ് പൂർത്തിയാക്കാൻ സ്പർശിക്കുക."</string>
diff --git a/res/values-mn-rMN/strings.xml b/res/values-mn-rMN/strings.xml
index 64e6d11..8b7ff23 100644
--- a/res/values-mn-rMN/strings.xml
+++ b/res/values-mn-rMN/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Андройдын үндсэн апп-ууд"</string>
<string name="app_label" msgid="3389954322874982620">"Харилцагчдын сан"</string>
<string name="provider_label" msgid="6012150850819899907">"Харилцагчид"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Харилцагчдын өгөгдлийн санг сайжруулж байна."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Харилцагчдын сайжруулалт хийхэд илүү их санах ой шаардлагатай."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Харилцагчдад зориулсан санг сайжруулж байна"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Сайжруулалтыг дуусгахын тулд хүрнэ үү."</string>
diff --git a/res/values-mr-rIN/strings.xml b/res/values-mr-rIN/strings.xml
index 74bb6bb..778f6fd 100644
--- a/res/values-mr-rIN/strings.xml
+++ b/res/values-mr-rIN/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"संपर्क संचयन"</string>
<string name="provider_label" msgid="6012150850819899907">"संपर्क"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"संपर्क डेटाबेसची श्रेणीसुधारित करीत आहे."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"संपर्क श्रेणीसुधारित करण्यास अधिक मेमरी आवश्यक आहे."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"संपर्कांसाठी संचयन श्रेणीसुधारित करीत आहे"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"श्रेणीसुधारित करणे पूर्ण करण्यासाठी स्पर्श करा."</string>
diff --git a/res/values-ms-rMY/strings.xml b/res/values-ms-rMY/strings.xml
index 96770ca..10b3e5a 100644
--- a/res/values-ms-rMY/strings.xml
+++ b/res/values-ms-rMY/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Apl Teras Android"</string>
<string name="app_label" msgid="3389954322874982620">"Storan Kenalan"</string>
<string name="provider_label" msgid="6012150850819899907">"Kenalan"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Menaik taraf pangkalan data kenalan."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Peningkatan kenalan memerlukan lebih banyak memori."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Meningkatkan storan untuk kenalan"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Sentuh untuk menyelesaikan peningkatan."</string>
diff --git a/res/values-my-rMM/strings.xml b/res/values-my-rMM/strings.xml
index c514d0c..7745ea9 100644
--- a/res/values-my-rMM/strings.xml
+++ b/res/values-my-rMM/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Androidပင်မ အပ်ပလီကေးရှင်းများ"</string>
<string name="app_label" msgid="3389954322874982620">"လိပ်စာများသိမ်းဆည်းသောအပ်ပလီကေးရှင်း"</string>
<string name="provider_label" msgid="6012150850819899907">"အဆက်အသွယ်များ"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"လိပ်စာဒေတာဘေ့စ်အား အဆင့်မြှင့်ခြင်း"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"အဆက်အသွယ်များ အဆင့်မြှင့်ခြင်းအတွက် မှတ်ဉာဏ်စွမ်းရည်ပိုလိုအပ်သည်"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"အဆယ်အသွယ်များ သိမ်းဆည်းရန် နေရာ အဆင့်မြှင့်ခြင်း"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"အဆင့်မြှင့်ခြင်း ပြီးဆုံးရန် ထိကိုင်ပါ"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 125de8b..6669a21 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Kjerneapper for Android"</string>
<string name="app_label" msgid="3389954322874982620">"Kontaktlager"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontakter"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Oppgraderer kontaktdatabasen."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Du har ikke nok minne til å oppgradere kontaktene."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Oppgraderer lagring for kontakter"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Trykk for å fullføre oppgraderingen."</string>
diff --git a/res/values-ne-rNP/strings.xml b/res/values-ne-rNP/strings.xml
index e8481cb..0fd3913 100644
--- a/res/values-ne-rNP/strings.xml
+++ b/res/values-ne-rNP/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"एन्ड्रोइड कोर अनुप्रयोगहरू"</string>
<string name="app_label" msgid="3389954322874982620">"सम्पर्कहरू भण्डारण"</string>
<string name="provider_label" msgid="6012150850819899907">"सम्पर्कहरू"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"सम्पर्क डेटाबेस अद्यावधिक गर्दै।"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"सम्पर्क अद्यावधिकका लागि अझै धेरै मेमोरी चाहिन्छ।"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"सम्पर्कका लागि भणडारण अद्यावधिक गर्दै"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"अद्यावधिक कार्य पुरा गर्न छुनुहोस्।"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 049f5bd..73ea1a9 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Kernapps van Android"</string>
<string name="app_label" msgid="3389954322874982620">"Opslag contacten"</string>
<string name="provider_label" msgid="6012150850819899907">"Contacten"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Contactendatabase bijwerken."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Voor het bijwerken van contacten is meer geheugen nodig."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Opslag voor contacten bijwerken"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Raak aan om de upgrade te voltooien."</string>
diff --git a/res/values-pa-rIN/strings.xml b/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000..cfc32d1
--- /dev/null
+++ b/res/values-pa-rIN/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core ਐਪਸ"</string>
+ <string name="app_label" msgid="3389954322874982620">"ਸੰਪਰਕ ਸਟੋਰੇਜ"</string>
+ <string name="provider_label" msgid="6012150850819899907">"ਸੰਪਰਕ"</string>
+ <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"ਸੰਪਰਕ ਅਪਗ੍ਰੇਡ ਲਈ ਵੱਧ ਮੈਮਰੀ ਦੀ ਲੋੜ ਹੈ।"</string>
+ <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"ਸੰਪਰਕਾਂ ਲਈ ਸਟੋਰੇਜ ਅਪਗ੍ਰੇਡ ਕਰ ਰਿਹਾ ਹੈ"</string>
+ <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"ਅਪਗ੍ਰੇਡ ਪੂਰਾ ਕਰਨ ਲਈ ਛੋਹਵੋ।"</string>
+ <string name="default_directory" msgid="93961630309570294">"ਸੰਪਰਕ"</string>
+ <string name="local_invisible_directory" msgid="705244318477396120">"ਹੋਰ"</string>
+ <string name="voicemail_from_column" msgid="435732568832121444">"ਇਸ ਤੋਂ ਵੌਇਸਮੇਲ "</string>
+ <string name="debug_dump_title" msgid="4916885724165570279">"ਸੰਪਰਕ ਡਾਟਾਬੇਸ ਕਾਪੀ ਕਰੋ"</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"ਤੁਸੀਂ ਇਹ ਕਰਨ ਵਾਲੇ ਹੋ 1) ਆਪਣੇ ਡਾਟਾਬੇਸ ਦੀ ਇੱਕ ਕਾਪੀ ਬਣਾਓ ਜਿਸ ਵਿੱਚ ਸਾਰੀ ਸੰਪਰਕ ਸੰਬੰਧਿਤ ਜਾਣਕਾਰੀ ਅਤੇ ਅੰਦਰੂਨੀ ਸਟੋਰੇਜ ਵਿੱਚ ਸਾਰਾ ਕਾਲ ਲੌਗ ਸ਼ਾਮਲ ਹੋਵੇ ਅਤੇ 2) ਇਸਨੂੰ ਈਮੇਲ ਕਰੋ। ਜਿਵੇਂ ਹੀ ਤੁਸੀਂ ਇਸਨੂੰ ਸਫਲਤਾਪੂਰਵਕ ਕਾਪੀ ਕਰ ਲਓ ਜਾਂ ਈਮੇਲ ਪ੍ਰਾਪਤ ਕਰ ਲਓ, ਕਾਪੀ ਮਿਟਾਉਣਾ ਯਾਦ ਰੱਖੋ।"</string>
+ <string name="debug_dump_delete_button" msgid="7832879421132026435">"ਹੁਣ ਮਿਟਾਓ"</string>
+ <string name="debug_dump_start_button" msgid="2837506913757600001">"ਚਾਲੂ ਕਰੋ"</string>
+ <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"ਆਪਣੀ ਫਾਈਲ ਭੇਜਣ ਲਈ ਇੱਕ ਪ੍ਰੋਗਰਾਮ ਚੁਣੋ"</string>
+ <string name="debug_dump_email_subject" msgid="108188398416385976">"ਸੰਪਰ Db ਅਟੈਚ ਕੀਤਾ"</string>
+ <string name="debug_dump_email_body" msgid="4577749800871444318">"ਅਟੈਚ ਕੀਤਾ ਮੇਰੀ ਸਾਰੀ ਸੰਪਰਕ ਜਾਣਕਾਰੀ ਵਾਲਾ ਮੇਰਾ ਸੰਪਰਕ ਡਾਟਾਬੇਸ ਹੈ। ਸਾਵਧਾਨੀ ਨਾਲ ਵਰਤੋ।"</string>
+</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 86eb274..0934542 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Aplikacje główne Androida"</string>
<string name="app_label" msgid="3389954322874982620">"Spis kontaktów"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontakty"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Uaktualnianie bazy danych kontaktów"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Uaktualnienie kontaktów wymaga więcej pamięci."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Pamięć na uaktualnienie kontaktów"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dotknij, aby dokończyć uaktualnianie."</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index b3b621a..fcea4e0 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Aplicações Principais do Android"</string>
<string name="app_label" msgid="3389954322874982620">"Armazenamento de contactos"</string>
<string name="provider_label" msgid="6012150850819899907">"Contactos"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"A atualizar a base de dados de contactos."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"A atualização de contactos necessita de mais memória."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"A atualizar armazenamento de contactos"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toque para concluir a atualização."</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 0dba728..6906957 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Principais apps do Android"</string>
<string name="app_label" msgid="3389954322874982620">"Armazenamento de contatos"</string>
<string name="provider_label" msgid="6012150850819899907">"Contatos"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Atualizando o banco de dados de contatos."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"A atualização de contatos precisa de mais memória."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Atualização do armazenamento para contatos"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toque para concluir a atualização."</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 8fa28e5..e01744e 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Stocarea datelor din Agendă"</string>
<string name="provider_label" msgid="6012150850819899907">"Agendă"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Se actualizează baza de date a Agendei."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Actualizarea agendei necesită mai multă memorie."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Măriţi spaţiul de stocare pentru Agendă"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Atingeţi pentru a finaliza actualizarea."</string>
@@ -27,7 +26,7 @@
<string name="local_invisible_directory" msgid="705244318477396120">"Altul"</string>
<string name="voicemail_from_column" msgid="435732568832121444">"Mesaj vocal de la "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"Copiaţi baza de date a agendei"</string>
- <string name="debug_dump_database_message" msgid="406438635002392290">"Sunteţi pe cale 1) să faceţi o copie, pe stocarea internă, a bazei dvs. de date care include toate informaţiile referitoare la agendă şi întregul jurnal de apeluri şi 2) să trimiteţi această copie prin e-mail. Nu uitaţi să ştergeţi această copie după ce aţi copiat-o de pe dispozitiv sau după ce a fost primit e-mailul."</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"Sunteţi pe cale 1) să faceţi o copie, pe stocarea internă, a bazei dvs. de date care include toate informaţiile referitoare la agendă și întregul jurnal de apeluri și 2) să trimiteţi această copie prin e-mail. Nu uitaţi să ştergeţi această copie după ce aţi copiat-o de pe dispozitiv sau după ce a fost primit e-mailul."</string>
<string name="debug_dump_delete_button" msgid="7832879421132026435">"Ștergeţi acum"</string>
<string name="debug_dump_start_button" msgid="2837506913757600001">"Porniţi"</string>
<string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Alegeţi un program pentru a trimite fişierul"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index a7fc3ac..e3fe19c 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Основные приложения Android"</string>
<string name="app_label" msgid="3389954322874982620">"Хранилище контактов"</string>
<string name="provider_label" msgid="6012150850819899907">"Контакты"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Обновление базы данных контактов..."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Для обновления контактов нужно больше памяти."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Недостаточно места для обновления контактов"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Нажмите, чтобы завершить обновление."</string>
diff --git a/res/values-si-rLK/strings.xml b/res/values-si-rLK/strings.xml
index f8db75c..e5b50a3 100644
--- a/res/values-si-rLK/strings.xml
+++ b/res/values-si-rLK/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android මධ්ය යෙදුම්"</string>
<string name="app_label" msgid="3389954322874982620">"සම්බන්ධතා ආචයනය"</string>
<string name="provider_label" msgid="6012150850819899907">"සම්බන්ධතා"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"සම්බන්ධතා දත්ත සමුදාය උත්ශ්රේණි කරමින්."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"සම්බන්ධතා උත්ශ්රේණි කිරීමට තව මතකය අවශ්යයි."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"සම්බන්ධතා සඳහා ආචයනය උත්ශ්රේණි කරමින්"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"උත්ශ්රේණි කිරීම සම්පූර්ණ කිරීමට ස්පර්ශ කරන්න."</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 9c69d9c..a1e3bf2 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Úložisko kontaktov"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontakty"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Prebieha inovácia databázy kontaktov."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Inovácia kontaktov vyžaduje viac pamäte."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Prebieha inovácia úložiska pre kontakty"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dotykom na túto možnosť dokončíte inováciu."</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index d3afc16..cd34f2d 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Osrednje aplikacije sistema Android"</string>
<string name="app_label" msgid="3389954322874982620">"Shramba za stike"</string>
<string name="provider_label" msgid="6012150850819899907">"Stiki"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Nadgradnja zbirke podatkov stikov."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Za nadgradnjo stikov je potrebno več pomnilnika"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Nadgradnja shrambe za stike"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dotaknite se, da končate nadgradnjo."</string>
diff --git a/res/values-sq-rAL/strings.xml b/res/values-sq-rAL/strings.xml
new file mode 100644
index 0000000..f3c7317
--- /dev/null
+++ b/res/values-sq-rAL/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Aplikacionet bazë të androidit"</string>
+ <string name="app_label" msgid="3389954322874982620">"Ruajtja e kontakteve"</string>
+ <string name="provider_label" msgid="6012150850819899907">"Kontaktet"</string>
+ <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Përmirësimi i kontakteve ka nevojë për më shumë memorie."</string>
+ <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Po përmirëson hapësirën ruajtëse për kontaktet"</string>
+ <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Prek për të përfunduar përmirësimin."</string>
+ <string name="default_directory" msgid="93961630309570294">"Kontaktet"</string>
+ <string name="local_invisible_directory" msgid="705244318477396120">"Tjetër"</string>
+ <string name="voicemail_from_column" msgid="435732568832121444">"Postë zanore nga "</string>
+ <string name="debug_dump_title" msgid="4916885724165570279">"Kopjo bazën e të dhënave me kontaktet"</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"Je gati që 1) të bësh një kopje të bazës tënde të të dhënave që përfshin të gjitha informacionet në lidhje me kontaktet dhe evidencat e telefonatave në hapësirën ruajtëse të brendshme dhe 2) ta dërgosh atë me mail. Mos harro që ta fshish kopjen sapo ta kesh kopjuar me sukses nga pajisja ose të kesh marrë mail-in."</string>
+ <string name="debug_dump_delete_button" msgid="7832879421132026435">"Fshi tani"</string>
+ <string name="debug_dump_start_button" msgid="2837506913757600001">"Nis"</string>
+ <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Zgjidh një program për të dërguar skedarin"</string>
+ <string name="debug_dump_email_subject" msgid="108188398416385976">"Baza e të dhënave me kontaktet u bashkëngjit"</string>
+ <string name="debug_dump_email_body" msgid="4577749800871444318">"Bashkëngjitur është baza e të dhënave me kontaktet dhe të gjitha informacionet e kontakteve të mia. Administroje me kujdes."</string>
+</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 69b7753..89d5140 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core апликације"</string>
<string name="app_label" msgid="3389954322874982620">"Складиште контаката"</string>
<string name="provider_label" msgid="6012150850819899907">"Контакти"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Надограђивање базе података о контактима."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"За ажурирање контаката потребно је више меморије."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Надограђивање меморије за контакте"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Додирните да бисте довршили надоградњу."</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index ae2a8d1..12240a4 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Kontakter"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontakter"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Kontaktdatabasen uppgraderas."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontaktuppgradering kräver mer minne."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Lagringsutrymmet för kontakter uppgraderas"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Tryck om du vill slutföra uppgraderingen."</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 947371f..5249091 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Programu Msingi za Android"</string>
<string name="app_label" msgid="3389954322874982620">"Hifadhi ya Anwani"</string>
<string name="provider_label" msgid="6012150850819899907">"Anwani"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Inapandisha gredi hifadhidata ya Anwani."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kupandishwa gredi kwa anwani kunahitaji kumbukumbu zaidi."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Inapandisha gredi hifadhi ya anwani"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Gusa ili kukamilisha kupandisha gredi."</string>
diff --git a/res/values-ta-rIN/strings.xml b/res/values-ta-rIN/strings.xml
index 06f989f..22015e6 100644
--- a/res/values-ta-rIN/strings.xml
+++ b/res/values-ta-rIN/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android முக்கிய பயன்பாடுகள்"</string>
<string name="app_label" msgid="3389954322874982620">"தொடர்புகள் சேமிப்பிடம்"</string>
<string name="provider_label" msgid="6012150850819899907">"தொடர்புகள்"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"தொடர்புகள் தரவுத்தளத்தை மேம்படுத்துகிறது."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"தொடர்புகளின் மேம்படுத்தலுக்கு கூடுதல் நினைவகம் தேவை."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"தொடர்புகளுக்காகச் சேமிப்பிடத்தை மேம்படுத்துகிறது"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"மேம்படுத்தலை முடிக்க தொடவும்."</string>
diff --git a/res/values-te-rIN/strings.xml b/res/values-te-rIN/strings.xml
index 9b913d0..c385874 100644
--- a/res/values-te-rIN/strings.xml
+++ b/res/values-te-rIN/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android ప్రధాన అనువర్తనాలు"</string>
<string name="app_label" msgid="3389954322874982620">"పరిచయాల నిల్వ"</string>
<string name="provider_label" msgid="6012150850819899907">"పరిచయాలు"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"పరిచయాల డేటాబేస్ను అప్గ్రేడ్ చేస్తోంది."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"పరిచయాల అప్గ్రేడ్కు మరింత మెమరీ అవసరం."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"పరిచయాల కోసం నిల్వను అప్గ్రేడ్ చేస్తోంది"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"అప్గ్రేడ్ను పూర్తి చేయడానికి తాకండి."</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 89ae2c9..da135d4 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"แอปพลิเคชันหลักของ Android"</string>
<string name="app_label" msgid="3389954322874982620">"ที่จัดเก็บรายชื่อ"</string>
<string name="provider_label" msgid="6012150850819899907">"สมุดโทรศัพท์"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"กำลังอัปเกรดฐานข้อมูลสมุดโทรศัพท์"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"ต้องใช้หน่วยความจำเพิ่มเพื่ออัปเกรดสมุดโทรศัพท์"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"กำลังอัปเกรดที่จัดเก็บข้อมูลสำหรับสมุดโทรศัพท์"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"แตะเพื่อทำการอัปเกรดให้สมบูรณ์"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index ad4ea5d..ff6df97 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Core Apps ng Android"</string>
<string name="app_label" msgid="3389954322874982620">"Imbakan ng Mga Contact"</string>
<string name="provider_label" msgid="6012150850819899907">"Mga Contact"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Ina-upgrade ang database ng mga contact."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Nangangailangan ng higit pang memory ang pag-upgrade sa mga contact."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Ina-upgrade ang storage para sa mga contact"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Pindutin upang kumpletuhin ang pag-upgrade."</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index dd376f3..69acd87 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Çekirdek Uygulamaları"</string>
<string name="app_label" msgid="3389954322874982620">"Kişi Deposu"</string>
<string name="provider_label" msgid="6012150850819899907">"Kişiler"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Kişiler veritabanı yeni sürüme geçiriliyor."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kişileri yeni sürüme geçirmek için daha fazla bellek gerekiyor."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Kişiler için depolama alanı yeni sürüme geçiriliyor"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Yeni sürüme geçmeyi tamamlamak için dokunun."</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index a640cdd..3fd7e3d 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Пам\'ять контактів"</string>
<string name="provider_label" msgid="6012150850819899907">"Контакти"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Оновлення бази даних контактів."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Оновлення контактів потребує більше пам’яті."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Оновлення пам’яті для контактів"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Торкніться, щоб завершити оновлення."</string>
diff --git a/res/values-ur-rPK/strings.xml b/res/values-ur-rPK/strings.xml
index 018a10e..3d081d7 100644
--- a/res/values-ur-rPK/strings.xml
+++ b/res/values-ur-rPK/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android کور ایپس"</string>
<string name="app_label" msgid="3389954322874982620">"رابطوں کا اسٹوریج"</string>
<string name="provider_label" msgid="6012150850819899907">"رابطے"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"رابطوں کا ڈیٹابیس اپ گریڈ ہو رہا ہے۔"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"رابطوں کے اپ گریڈ کیلئے مزید میموری درکار ہے۔"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"رابطوں کیلئے اسٹوریج اپ گریڈ ہو رہا ہے"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"اپ گریڈ مکمل کرنے کیلئے ٹچ کریں۔"</string>
diff --git a/res/values-uz-rUZ/strings.xml b/res/values-uz-rUZ/strings.xml
index a4afdf2..1bd680a 100644
--- a/res/values-uz-rUZ/strings.xml
+++ b/res/values-uz-rUZ/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android’ga asoslangan ilovalar"</string>
<string name="app_label" msgid="3389954322874982620">"Kontaktlar xotirasi"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontaktlar"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Kontaktlar ma’lumotlar bazasi yangilanmoqda."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontaktlarni yangilash uchun ko‘proq xotira kerak."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Kontaktlar uchun xotirani yangilash"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Yangilashni tugatish uchun bosing."</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index adc1a8f..f1fa674 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Ứng dụng lõi Android"</string>
<string name="app_label" msgid="3389954322874982620">"Bộ nhớ Danh bạ"</string>
<string name="provider_label" msgid="6012150850819899907">"Danh bạ"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Đang nâng cấp cơ sở dữ liệu danh bạ."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Nâng cấp danh bạ cần thêm bộ nhớ."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Đang nâng cấp dung lượng cho danh bạ"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Chạm để hoàn tất nâng cấp."</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 4981f5b..ad4eda6 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android 核心应用"</string>
<string name="app_label" msgid="3389954322874982620">"联系人存储"</string>
<string name="provider_label" msgid="6012150850819899907">"通讯录"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"正在升级联系人数据库。"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"联系人升级需要更多的存储空间。"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"正在升级存储器以容纳更多联系人"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"触摸可完成升级。"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 9505b33..b03d7bc 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android 核心應用程式"</string>
<string name="app_label" msgid="3389954322874982620">"聯絡人儲存空間"</string>
<string name="provider_label" msgid="6012150850819899907">"通訊錄"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"正在升級聯絡人資料庫。"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"需要更多記憶體才能將通訊錄升級。"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"正在升級通訊錄儲存空間"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"輕觸即可完成升級。"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 926f4db..1e07ef1 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android 核心應用程式"</string>
<string name="app_label" msgid="3389954322874982620">"聯絡人儲存空間"</string>
<string name="provider_label" msgid="6012150850819899907">"聯絡人"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"正在升級聯絡人資料庫。"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"需要更多記憶體才能將聯絡人升級。"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"正在升級聯絡人儲存空間"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"輕觸即可完成升級。"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index f5cd7b3..1e4adc1 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -19,7 +19,6 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Izinhlelo Zokusebenza ze-Android Core"</string>
<string name="app_label" msgid="3389954322874982620">"Isitoreji Sothintana Nabo"</string>
<string name="provider_label" msgid="6012150850819899907">"Othintana nabo"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"Ukufaka ezakamuva kwimininingo egciniwe yothintana naye."</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Ukuthuthukisa othintana naye kudinga enye imemori"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Ukuthuthukiswa kwesilondolozi soxhumana nabo"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Thinta ukuqedela ukuthuthukisa."</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c326eee..8be7bca 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -26,9 +26,6 @@
<!-- What to show in messaging that refers to this provider, e.g. AccountSyncSettings -->
<string name="provider_label">Contacts</string>
- <!-- [CHAR LIMIT=NONE] Boot message while upgrading contacts. -->
- <string name="upgrade_msg">Upgrading contacts database.</string>
-
<!-- Ticker for the notification shown when updating contacts fails because of memory shortage -->
<string name="upgrade_out_of_memory_notification_ticker">Contacts upgrade needs more memory.</string>
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index d4339b0..b5dbd06 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -39,6 +39,9 @@
import android.os.UserManager;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.Log;
@@ -46,7 +49,6 @@
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.android.providers.contacts.util.SelectionBuilder;
import com.android.providers.contacts.util.UserUtils;
-
import com.google.common.annotations.VisibleForTesting;
import java.util.HashMap;
@@ -60,12 +62,16 @@
private static final String TAG = CallLogProvider.class.getSimpleName();
private static final int BACKGROUND_TASK_INITIALIZE = 0;
+ private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1;
/** Selection clause for selecting all calls that were made after a certain time */
private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?";
/** Selection clause to use to exclude voicemail records. */
private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause(
Calls.TYPE, Calls.VOICEMAIL_TYPE);
+ /** Selection clause to exclude hidden records. */
+ private static final String EXCLUDE_HIDDEN_SELECTION = getEqualityClause(
+ Calls.PHONE_ACCOUNT_HIDDEN, 0);
@VisibleForTesting
static final String[] CALL_LOG_SYNC_PROJECTION = new String[] {
@@ -80,12 +86,22 @@
Calls.PHONE_ACCOUNT_ID
};
+ static final String[] MINIMAL_PROJECTION = new String[] { Calls._ID };
+
private static final int CALLS = 1;
private static final int CALLS_ID = 2;
private static final int CALLS_FILTER = 3;
+ private static final String UNHIDE_BY_PHONE_ACCOUNT_QUERY =
+ "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " +
+ Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " + Calls.PHONE_ACCOUNT_ID + "=?;";
+
+ private static final String UNHIDE_BY_ADDRESS_QUERY =
+ "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " +
+ Calls.PHONE_ACCOUNT_ADDRESS + "=?;";
+
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
@@ -108,6 +124,8 @@
sCallsProjectionMap.put(Calls.FEATURES, Calls.FEATURES);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID);
+ sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS);
+ sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_HIDDEN, Calls.PHONE_ACCOUNT_HIDDEN);
sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION);
@@ -121,6 +139,7 @@
sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
+ sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI);
sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER);
}
@@ -154,13 +173,13 @@
mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
- performBackgroundTask(msg.what);
+ performBackgroundTask(msg.what, msg.obj);
}
};
mReadAccessLatch = new CountDownLatch(1);
- scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE);
+ scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE, null);
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate finish");
@@ -189,6 +208,7 @@
final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, true /*isQuery*/);
+ selectionBuilder.addClause(EXCLUDE_HIDDEN_SELECTION);
final int match = sURIMatcher.match(uri);
switch (match) {
@@ -283,7 +303,7 @@
// permission and also requires the additional voicemail param set.
if (hasVoicemailValue(values)) {
checkIsAllowVoicemailRequest(uri);
- mVoicemailPermissions.checkCallerHasWriteAccess();
+ mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage());
}
if (mCallsInserter == null) {
SQLiteDatabase db = mDbHelper.getWritableDatabase();
@@ -356,6 +376,10 @@
return getContext();
}
+ void adjustForNewPhoneAccount(PhoneAccountHandle handle) {
+ scheduleBackgroundTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle);
+ }
+
/**
* Returns a {@link DatabaseModifier} that takes care of sending necessary notifications
* after the operation is performed.
@@ -387,9 +411,9 @@
SelectionBuilder selectionBuilder, boolean isQuery) {
if (isAllowVoicemailRequest(uri)) {
if (isQuery) {
- mVoicemailPermissions.checkCallerHasReadAccess();
+ mVoicemailPermissions.checkCallerHasReadAccess(getCallingPackage());
} else {
- mVoicemailPermissions.checkCallerHasWriteAccess();
+ mVoicemailPermissions.checkCallerHasWriteAccess(getCallingPackage());
}
} else {
selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION);
@@ -466,6 +490,48 @@
}
/**
+ * Un-hides any hidden call log entries that are associated with the specified handle.
+ *
+ * @param handle The handle to the newly registered {@link android.telecom.PhoneAccount}.
+ */
+ private void adjustForNewPhoneAccountInternal(PhoneAccountHandle handle) {
+ String[] handleArgs =
+ new String[] { handle.getComponentName().flattenToString(), handle.getId() };
+
+ // Check to see if any entries exist for this handle. If so (not empty), run the un-hiding
+ // update. If not, then try to identify the call from the phone number.
+ Cursor cursor = query(Calls.CONTENT_URI, MINIMAL_PROJECTION,
+ Calls.PHONE_ACCOUNT_COMPONENT_NAME + " =? AND " + Calls.PHONE_ACCOUNT_ID + " =?",
+ handleArgs, null);
+
+ if (cursor != null) {
+ try {
+ if (cursor.getCount() >= 1) {
+ // run un-hiding process based on phone account
+ mDbHelper.getWritableDatabase().execSQL(
+ UNHIDE_BY_PHONE_ACCOUNT_QUERY, handleArgs);
+ } else {
+ TelecomManager tm = TelecomManager.from(getContext());
+ if (tm != null) {
+
+ PhoneAccount account = tm.getPhoneAccount(handle);
+ if (account != null && account.getAddress() != null) {
+ // We did not find any items for the specific phone account, so run the
+ // query based on the phone number instead.
+ mDbHelper.getWritableDatabase().execSQL(UNHIDE_BY_ADDRESS_QUERY,
+ new String[] { account.getAddress().toString() });
+ }
+
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ }
+
+ /**
* @param cursor to copy call log entries from
*
* @return the timestamp of the last synced entry.
@@ -543,11 +609,11 @@
}
}
- private void scheduleBackgroundTask(int task) {
- mBackgroundHandler.sendEmptyMessage(task);
+ private void scheduleBackgroundTask(int task, Object arg) {
+ mBackgroundHandler.obtainMessage(task, arg).sendToTarget();
}
- private void performBackgroundTask(int task) {
+ private void performBackgroundTask(int task, Object arg) {
if (task == BACKGROUND_TASK_INITIALIZE) {
try {
final Context context = getContext();
@@ -562,6 +628,8 @@
mReadAccessLatch.countDown();
mReadAccessLatch = null;
}
+ } else if (task == BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT) {
+ adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg);
}
}
diff --git a/src/com/android/providers/contacts/ContactDirectoryManager.java b/src/com/android/providers/contacts/ContactDirectoryManager.java
index f243e79..530a31b 100644
--- a/src/com/android/providers/contacts/ContactDirectoryManager.java
+++ b/src/com/android/providers/contacts/ContactDirectoryManager.java
@@ -199,6 +199,7 @@
@VisibleForTesting
static boolean isDirectoryProvider(ProviderInfo provider) {
+ if (provider == null) return false;
Bundle metaData = provider.metaData;
if (metaData == null) return false;
@@ -213,17 +214,26 @@
static Set<String> getDirectoryProviderPackages(PackageManager pm) {
final Set<String> ret = Sets.newHashSet();
- // Note to 3rd party developers:
- // queryContentProviders() is a public API but this method doesn't officially support
- // the GET_META_DATA flag. Don't use it in your app.
- final List<ProviderInfo> providers = pm.queryContentProviders(null, 0,
- PackageManager.GET_META_DATA);
- if (providers == null) {
+ final List<PackageInfo> packages = pm.getInstalledPackages(PackageManager.GET_PROVIDERS
+ | PackageManager.GET_META_DATA);
+ if (packages == null) {
return ret;
}
- for (ProviderInfo provider : providers) {
- if (isDirectoryProvider(provider)) {
- ret.add(provider.packageName);
+ for (PackageInfo packageInfo : packages) {
+ if (DEBUG) {
+ Log.d(TAG, "package=" + packageInfo.packageName);
+ }
+ if (packageInfo.providers == null) {
+ continue;
+ }
+ for (ProviderInfo provider : packageInfo.providers) {
+ if (DEBUG) {
+ Log.d(TAG, "provider=" + provider.authority);
+ }
+ if (isDirectoryProvider(provider)) {
+ Log.d(TAG, "Found " + provider.authority);
+ ret.add(provider.packageName);
+ }
}
}
if (DEBUG) {
diff --git a/src/com/android/providers/contacts/ContactLocaleUtils.java b/src/com/android/providers/contacts/ContactLocaleUtils.java
index 340b6a5..4ca9118 100644
--- a/src/com/android/providers/contacts/ContactLocaleUtils.java
+++ b/src/com/android/providers/contacts/ContactLocaleUtils.java
@@ -71,7 +71,7 @@
protected final ImmutableIndex mAlphabeticIndex;
private final int mAlphabeticIndexBucketCount;
private final int mNumberBucketIndex;
- private final boolean mEnableSecondaryLocalePinyin;
+ private final boolean mUsePinyinTransliterator;
public ContactLocaleUtilsBase(LocaleSet locales) {
// AlphabeticIndex.getBucketLabel() uses a binary search across
@@ -87,7 +87,8 @@
// Cyrillic because their alphabets are complementary supersets
// of Russian.
final Locale secondaryLocale = locales.getSecondaryLocale();
- mEnableSecondaryLocalePinyin = locales.isSecondaryLocaleSimplifiedChinese();
+ mUsePinyinTransliterator = locales.isPrimaryLocaleSimplifiedChinese() ||
+ locales.isSecondaryLocaleSimplifiedChinese();
AlphabeticIndex ai = new AlphabeticIndex(locales.getPrimaryLocale())
.setMaxLabelCount(300);
if (secondaryLocale != null) {
@@ -144,10 +145,13 @@
}
/**
- * TODO: ICU 52 AlphabeticIndex doesn't support Simplified Chinese
- * as a secondary locale. Remove the following if that is added.
+ * ICU 55 AlphabeticIndex doesn't support Simplified Chinese
+ * as a secondary locale so it is necessary to use the
+ * Pinyin transliterator. We also use this for a Simplified
+ * Chinese primary locale because it gives more accurate letter
+ * buckets. b/19835686
*/
- if (mEnableSecondaryLocalePinyin) {
+ if (mUsePinyinTransliterator) {
name = HanziToPinyin.getInstance().transliterate(name);
}
final int bucket = mAlphabeticIndex.getBucketIndex(name);
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index e1a47cb..bbde779 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -1,5 +1,5 @@
/*
-T * Copyright (C) 2009 The Android Open Source Project
+ * 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.
@@ -16,14 +16,15 @@
package com.android.providers.contacts;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.database.CharArrayBuffer;
import android.database.Cursor;
@@ -62,6 +63,7 @@
import android.provider.ContactsContract.DisplayPhoto;
import android.provider.ContactsContract.FullNameStyle;
import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.MetadataSync;
import android.provider.ContactsContract.PhoneticNameStyle;
import android.provider.ContactsContract.PhotoFiles;
import android.provider.ContactsContract.PinnedPositions;
@@ -73,6 +75,8 @@
import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Voicemails;
import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
@@ -84,17 +88,15 @@
import com.android.providers.contacts.database.DeletedContactsTableUtil;
import com.android.providers.contacts.database.MoreDatabaseUtils;
import com.android.providers.contacts.util.NeededForTesting;
-
import com.google.android.collect.Sets;
import com.google.common.annotations.VisibleForTesting;
-import java.util.HashMap;
+import libcore.icu.ICU;
+
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import libcore.icu.ICU;
-
/**
* Database helper for contacts. Designed as a singleton to make sure that all
* {@link android.content.ContentProvider} users get the same reference.
@@ -116,10 +118,11 @@
* 600-699 Ice Cream Sandwich
* 700-799 Jelly Bean
* 800-899 Kitkat
- * 900-999 L
+ * 900-999 Lollipop
+ * 1000-1099 M
* </pre>
*/
- static final int DATABASE_VERSION = 910;
+ static final int DATABASE_VERSION = 1009;
public interface Tables {
public static final String CONTACTS = "contacts";
@@ -148,6 +151,8 @@
public static final String DEFAULT_DIRECTORY = "default_directory";
public static final String SEARCH_INDEX = "search_index";
public static final String VOICEMAIL_STATUS = "voicemail_status";
+ public static final String METADATA_SYNC = "metadata_sync";
+ public static final String PRE_AUTHORIZED_URIS = "pre_authorized_uris";
// This list of tables contains auto-incremented sequences.
public static final String[] SEQUENCE_TABLES = new String[] {
@@ -301,6 +306,7 @@
public static final String GROUPS = "view_groups";
public static final String DATA_USAGE_STAT = "view_data_usage_stat";
public static final String STREAM_ITEMS = "view_stream_items";
+ public static final String METADATA_SYNC = "view_metadata_sync";
}
public interface Projections {
@@ -409,6 +415,8 @@
public static final String CONCRETE_ACCOUNT_ID = Tables.RAW_CONTACTS + "." + ACCOUNT_ID;
public static final String CONCRETE_SOURCE_ID =
Tables.RAW_CONTACTS + "." + RawContacts.SOURCE_ID;
+ public static final String CONCRETE_BACKUP_ID =
+ Tables.RAW_CONTACTS + "." + RawContacts.BACKUP_ID;
public static final String CONCRETE_VERSION =
Tables.RAW_CONTACTS + "." + RawContacts.VERSION;
public static final String CONCRETE_DIRTY =
@@ -444,8 +452,6 @@
Tables.RAW_CONTACTS + "." + DISPLAY_NAME;
public static final String CONCRETE_CONTACT_ID =
Tables.RAW_CONTACTS + "." + RawContacts.CONTACT_ID;
- public static final String CONCRETE_NAME_VERIFIED =
- Tables.RAW_CONTACTS + "." + RawContacts.NAME_VERIFIED;
public static final String PHONEBOOK_LABEL_PRIMARY =
ContactsColumns.PHONEBOOK_LABEL_PRIMARY;
public static final String PHONEBOOK_BUCKET_PRIMARY =
@@ -454,6 +460,13 @@
ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE;
public static final String PHONEBOOK_BUCKET_ALTERNATIVE =
ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE;
+
+ /**
+ * This column is no longer used, but we keep it in the table so an upgraded database
+ * will look the same as a new database. This reduces the chance of OEMs adding a second
+ * column with the same name.
+ */
+ public static final String NAME_VERIFIED_OBSOLETE = "name_verified";
}
public interface ViewRawContactsColumns {
@@ -676,6 +689,12 @@
public static final String TOKENS = "tokens";
}
+ public interface PreAuthorizedUris {
+ public static final String _ID = BaseColumns._ID;
+ public static final String URI = "uri";
+ public static final String EXPIRATION = "expiration";
+ }
+
/**
* Private table for calculating per-contact-method ranking.
*/
@@ -713,6 +732,12 @@
public static final int USAGE_TYPE_INT_SHORT_TEXT = 2;
}
+ public interface MetadataSyncColumns {
+ static final String CONCRETE_ID = Tables.METADATA_SYNC + "._id";
+ static final String ACCOUNT_ID = "account_id";
+ static final String CONCRETE_ACCOUNT_ID = Tables.METADATA_SYNC + "." + ACCOUNT_ID;
+ }
+
private interface EmailQuery {
public static final String TABLE = Tables.DATA;
@@ -1092,7 +1117,6 @@
StatusUpdates.CHAT_CAPABILITY + " INTEGER NOT NULL DEFAULT 0" +
");");
-
db.execSQL("CREATE TRIGGER " + DATABASE_PRESENCE + "." + Tables.PRESENCE + "_deleted"
+ " BEFORE DELETE ON " + DATABASE_PRESENCE + "." + Tables.PRESENCE
+ " BEGIN "
@@ -1197,6 +1221,7 @@
RawContactsColumns.ACCOUNT_ID + " INTEGER REFERENCES " +
Tables.ACCOUNTS + "(" + AccountsColumns._ID + ")," +
RawContacts.SOURCE_ID + " TEXT," +
+ RawContacts.BACKUP_ID + " TEXT," +
RawContacts.RAW_CONTACT_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," +
RawContacts.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
@@ -1228,7 +1253,7 @@
ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," +
RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + " TEXT," +
RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + " INTEGER," +
- RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0," +
+ RawContactsColumns.NAME_VERIFIED_OBSOLETE + " INTEGER NOT NULL DEFAULT 0," +
RawContacts.SYNC1 + " TEXT, " +
RawContacts.SYNC2 + " TEXT, " +
RawContacts.SYNC3 + " TEXT, " +
@@ -1245,6 +1270,12 @@
RawContactsColumns.ACCOUNT_ID +
");");
+ db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS raw_contacts_backup_id_account_id_index ON " +
+ Tables.RAW_CONTACTS + " (" +
+ RawContacts.BACKUP_ID + ", " +
+ RawContactsColumns.ACCOUNT_ID +
+ ");");
+
db.execSQL("CREATE TABLE " + Tables.STREAM_ITEMS + " (" +
StreamItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
StreamItems.RAW_CONTACT_ID + " INTEGER NOT NULL, " +
@@ -1307,6 +1338,7 @@
DataColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
DataColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," +
Data.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
+ Data.HASH_ID + " TEXT," +
Data.IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
Data.IS_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
Data.IS_SUPER_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
@@ -1329,7 +1361,8 @@
Data.SYNC1 + " TEXT, " +
Data.SYNC2 + " TEXT, " +
Data.SYNC3 + " TEXT, " +
- Data.SYNC4 + " TEXT " +
+ Data.SYNC4 + " TEXT, " +
+ Data.CARRIER_PRESENCE + " INTEGER NOT NULL DEFAULT 0 " +
");");
db.execSQL("CREATE INDEX data_raw_contact_id ON " + Tables.DATA + " (" +
@@ -1344,6 +1377,14 @@
Data.DATA1 +
");");
+ /**
+ * For contact backup restore queries.
+ */
+ db.execSQL("CREATE INDEX IF NOT EXISTS data_hash_id_index ON " + Tables.DATA + " (" +
+ Data.HASH_ID +
+ ");");
+
+
// Private phone numbers table used for lookup
db.execSQL("CREATE TABLE " + Tables.PHONE_LOOKUP + " (" +
PhoneLookupColumns.DATA_ID
@@ -1481,6 +1522,8 @@
Calls.FEATURES + " INTEGER NOT NULL DEFAULT 0," +
Calls.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," +
Calls.PHONE_ACCOUNT_ID + " TEXT," +
+ Calls.PHONE_ACCOUNT_ADDRESS + " TEXT," +
+ Calls.PHONE_ACCOUNT_HIDDEN + " INTEGER NOT NULL DEFAULT 0," +
Calls.SUB_ID + " INTEGER DEFAULT -1," +
Calls.NEW + " INTEGER," +
Calls.CACHED_NAME + " TEXT," +
@@ -1494,6 +1537,7 @@
Calls.CACHED_MATCHED_NUMBER + " TEXT," +
Calls.CACHED_NORMALIZED_NUMBER + " TEXT," +
Calls.CACHED_PHOTO_ID + " INTEGER NOT NULL DEFAULT 0," +
+ Calls.CACHED_PHOTO_URI + " TEXT," +
Calls.CACHED_FORMATTED_NUMBER + " TEXT," +
Voicemails._DATA + " TEXT," +
Voicemails.HAS_CONTENT + " INTEGER," +
@@ -1501,13 +1545,17 @@
Voicemails.SOURCE_DATA + " TEXT," +
Voicemails.SOURCE_PACKAGE + " TEXT," +
Voicemails.TRANSCRIPTION + " TEXT," +
- Voicemails.STATE + " INTEGER" +
+ Voicemails.STATE + " INTEGER," +
+ Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
+ Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0" +
");");
// Voicemail source status table.
db.execSQL("CREATE TABLE " + Tables.VOICEMAIL_STATUS + " (" +
VoicemailContract.Status._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
VoicemailContract.Status.SOURCE_PACKAGE + " TEXT UNIQUE NOT NULL," +
+ VoicemailContract.Status.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," +
+ VoicemailContract.Status.PHONE_ACCOUNT_ID + " TEXT," +
VoicemailContract.Status.SETTINGS_URI + " TEXT," +
VoicemailContract.Status.VOICEMAIL_ACCESS_URI + " TEXT," +
VoicemailContract.Status.CONFIGURATION_STATE + " INTEGER," +
@@ -1542,6 +1590,24 @@
DataUsageStatColumns.USAGE_TYPE_INT +
");");
+ db.execSQL("CREATE TABLE IF NOT EXISTS "
+ + Tables.METADATA_SYNC + " (" +
+ MetadataSync._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ MetadataSync.RAW_CONTACT_BACKUP_ID + " TEXT NOT NULL," +
+ MetadataSyncColumns.ACCOUNT_ID + " INTEGER NOT NULL," +
+ MetadataSync.DATA + " TEXT," +
+ MetadataSync.DELETED + " INTEGER NOT NULL DEFAULT 0);");
+
+ db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS metadata_sync_index ON " +
+ Tables.METADATA_SYNC + " (" +
+ MetadataSync.RAW_CONTACT_BACKUP_ID + ", " +
+ MetadataSyncColumns.ACCOUNT_ID +");");
+
+ db.execSQL("CREATE TABLE " + Tables.PRE_AUTHORIZED_URIS + " ("+
+ PreAuthorizedUris._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ PreAuthorizedUris.URI + " STRING NOT NULL, " +
+ PreAuthorizedUris.EXPIRATION + " INTEGER NOT NULL DEFAULT 0);");
+
// When adding new tables, be sure to also add size-estimates in updateSqliteStats
createContactsViews(db);
createGroupsView(db);
@@ -1780,6 +1846,7 @@
db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES + ";");
db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_STAT + ";");
db.execSQL("DROP VIEW IF EXISTS " + Views.STREAM_ITEMS + ";");
+ db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC + ";");
String dataColumns =
Data.IS_PRIMARY + ", "
@@ -1821,8 +1888,7 @@
+ AccountsColumns.CONCRETE_DATA_SET + " END) AS "
+ RawContacts.ACCOUNT_TYPE_AND_DATA_SET + ","
+ RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
- + RawContactsColumns.CONCRETE_NAME_VERIFIED + " AS "
- + RawContacts.NAME_VERIFIED + ","
+ + RawContactsColumns.CONCRETE_BACKUP_ID + " AS " + RawContacts.BACKUP_ID + ","
+ RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
+ RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
+ RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ","
@@ -1883,6 +1949,7 @@
String dataSelect = "SELECT "
+ DataColumns.CONCRETE_ID + " AS " + Data._ID + ","
+ + Data.HASH_ID + ", "
+ Data.RAW_CONTACT_ID + ", "
+ RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
+ syncColumns + ", "
@@ -2106,6 +2173,21 @@
+ RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")";
db.execSQL("CREATE VIEW " + Views.STREAM_ITEMS + " AS " + streamItemSelect);
+
+ String metadataSyncSelect = "SELECT " +
+ MetadataSyncColumns.CONCRETE_ID + ", " +
+ MetadataSync.RAW_CONTACT_BACKUP_ID + ", " +
+ AccountsColumns.ACCOUNT_NAME + ", " +
+ AccountsColumns.ACCOUNT_TYPE + ", " +
+ AccountsColumns.DATA_SET + ", " +
+ MetadataSync.DATA + ", " +
+ MetadataSync.DELETED +
+ " FROM " + Tables.METADATA_SYNC
+ + " JOIN " + Tables.ACCOUNTS + " ON ("
+ + MetadataSyncColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
+ + ")";
+
+ db.execSQL("CREATE VIEW " + Views.METADATA_SYNC + " AS " + metadataSyncSelect);
}
private static String buildDisplayPhotoUriAlias(String contactIdColumn, String alias) {
@@ -2802,6 +2884,58 @@
upgradeToVersion910(db);
oldVersion = 910;
}
+ if (oldVersion < 1000) {
+ upgradeToVersion1000(db);
+ upgradeViewsAndTriggers = true;
+ oldVersion = 1000;
+ }
+ if (oldVersion < 1001) {
+ upgradeToVersion1001(db);
+ rebuildSqliteStats = true;
+ oldVersion = 1001;
+ }
+
+ if (oldVersion < 1002) {
+ rebuildSqliteStats = true;
+ upgradeToVersion1002(db);
+ oldVersion = 1002;
+ }
+
+ if (oldVersion < 1003) {
+ upgradeToVersion1003(db);
+ oldVersion = 1003;
+ }
+
+ if (oldVersion < 1004) {
+ upgradeToVersion1004(db);
+ oldVersion = 1004;
+ }
+
+ if (oldVersion < 1005) {
+ upgradeToVersion1005(db);
+ oldVersion = 1005;
+ }
+
+ if (oldVersion < 1006) {
+ upgradeViewsAndTriggers = true;
+ oldVersion = 1006;
+ }
+
+ if (oldVersion < 1007) {
+ upgradeToVersion1007(db);
+ oldVersion = 1007;
+ }
+
+ if (oldVersion < 1008) {
+ upgradeToVersion1008(db);
+ upgradeViewsAndTriggers = true;
+ oldVersion = 1008;
+ }
+
+ if (oldVersion < 1009) {
+ upgradeToVersion1009(db);
+ oldVersion = 1009;
+ }
if (upgradeViewsAndTriggers) {
createContactsViews(db);
@@ -2908,12 +3042,12 @@
// For each Contact, find the RawContact that contributed the display name
db.execSQL(
"UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
+ " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
" SELECT " + RawContacts._ID +
" FROM " + Tables.RAW_CONTACTS +
" WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
" AND " + RawContactsColumns.CONCRETE_DISPLAY_NAME + "=" +
- Tables.CONTACTS + "." + Contacts.DISPLAY_NAME +
+ Tables.CONTACTS + "." + Contacts.DISPLAY_NAME +
" ORDER BY " + RawContacts._ID +
" LIMIT 1)"
);
@@ -3024,11 +3158,11 @@
SQLiteStatement structuredNameUpdate = db.compileStatement(
"UPDATE " + Tables.DATA +
- " SET " +
+ " SET " +
StructuredName.FULL_NAME_STYLE + "=?," +
StructuredName.DISPLAY_NAME + "=?," +
StructuredName.PHONETIC_NAME_STYLE + "=?" +
- " WHERE " + Data._ID + "=?");
+ " WHERE " + Data._ID + "=?");
NameSplitter.Name name = new NameSplitter.Name();
Cursor cursor = db.query(StructName205Query.TABLE,
@@ -3156,7 +3290,7 @@
private void upgrateToVersion206(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
- + " ADD " + RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0;");
+ + " ADD name_verified INTEGER NOT NULL DEFAULT 0;");
}
/**
@@ -3492,7 +3626,7 @@
private void insertNicknameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
final long mimeTypeId = lookupMimeTypeId(db, Nickname.CONTENT_ITEM_TYPE);
Cursor cursor = db.query(NicknameQuery.TABLE, NicknameQuery.COLUMNS,
- NicknameQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)},
+ NicknameQuery.SELECTION, new String[]{String.valueOf(mimeTypeId)},
null, null, null);
try {
while (cursor.moveToNext()) {
@@ -4230,6 +4364,125 @@
}
}
+ /**
+ * Add backup_id column to raw_contacts table and hash_id column to data table.
+ */
+ private void upgradeToVersion1000(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE raw_contacts ADD backup_id TEXT;");
+ db.execSQL("ALTER TABLE data ADD hash_id TEXT;");
+ db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS raw_contacts_backup_id_account_id_index ON " +
+ "raw_contacts (backup_id, account_id);");
+ db.execSQL("CREATE INDEX IF NOT EXISTS data_hash_id_index ON data (hash_id);");
+ }
+
+ /**
+ * Add new metadata_sync table to cache the meta data on raw contacts level from server before
+ * they are merged into other CP2 tables. The data column is the blob column containing all
+ * the backed up metadata for this raw_contact. This table should only be used by metadata
+ * sync adapter.
+ */
+ private void upgradeToVersion1001(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE metadata_sync (" +
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, raw_contact_backup_id TEXT NOT NULL, " +
+ "account_id INTEGER NOT NULL, data TEXT, " +
+ "FOREIGN KEY(account_id) REFERENCES accounts(_id));");
+ db.execSQL("CREATE UNIQUE INDEX metadata_sync_index ON metadata_sync (" +
+ "raw_contact_backup_id, account_id);");
+ }
+
+ @VisibleForTesting
+ public void upgradeToVersion1002(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS pre_authorized_uris;");
+ db.execSQL("CREATE TABLE pre_authorized_uris ("+
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "uri STRING NOT NULL, " +
+ "expiration INTEGER NOT NULL DEFAULT 0);");
+ }
+
+ public void upgradeToVersion1003(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE calls ADD phone_account_address TEXT;");
+
+ // After version 1003, we are using the ICC ID as the phone-account ID. This code updates
+ // any existing telephony connection-service calllog entries to the ICC ID from the
+ // previously used subscription ID.
+ // TODO: This is inconsistent, depending on the initialization state of SubscriptionManager.
+ // Sometimes it returns zero subscriptions. May want to move this upgrade to run after
+ // ON_BOOT_COMPLETE instead of PRE_BOOT_COMPLETE.
+ SubscriptionManager sm = SubscriptionManager.from(mContext);
+ if (sm != null) {
+ Log.i(TAG, "count: " + sm.getAllSubscriptionInfoCount());
+ for (SubscriptionInfo info : sm.getAllSubscriptionInfoList()) {
+ String iccId = info.getIccId();
+ int subId = info.getSubscriptionId();
+ if (!TextUtils.isEmpty(iccId) &&
+ subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("UPDATE calls SET subscription_id=");
+ DatabaseUtils.appendEscapedSQLString(sb, iccId);
+ sb.append(" WHERE subscription_id=");
+ sb.append(subId);
+ sb.append(" AND subscription_component_name='com.android.phone/"
+ + "com.android.services.telephony.TelephonyConnectionService';");
+
+ db.execSQL(sb.toString());
+ }
+ }
+ }
+ }
+
+ /**
+ * Add a "hidden" column for call log entries we want to hide after an upgrade until the user
+ * adds the right phone account to the device.
+ */
+ public void upgradeToVersion1004(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE calls ADD phone_account_hidden INTEGER NOT NULL DEFAULT 0;");
+ }
+
+ public void upgradeToVersion1005(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE calls ADD photo_uri TEXT;");
+ }
+
+ /**
+ * The try/catch pattern exists because some devices have the upgrade and some do not. This is
+ * because the below updates were merged into version 1005 after some devices had already
+ * upgraded to version 1005 and hence did not receive the below upgrades.
+ */
+ public void upgradeToVersion1007(SQLiteDatabase db) {
+ try {
+ // Add multi-sim fields
+ db.execSQL("ALTER TABLE voicemail_status ADD phone_account_component_name TEXT;");
+ db.execSQL("ALTER TABLE voicemail_status ADD phone_account_id TEXT;");
+
+ // For use by the sync adapter
+ db.execSQL("ALTER TABLE calls ADD dirty INTEGER NOT NULL DEFAULT 0;");
+ db.execSQL("ALTER TABLE calls ADD deleted INTEGER NOT NULL DEFAULT 0;");
+ } catch (SQLiteException e) {
+ // These columns already exist. Do nothing.
+ // Log verbose because this should be the majority case.
+ Log.v(TAG, "Version 1007: Columns already exist, skipping upgrade steps.");
+ }
+ }
+
+ /**
+ * The metadata_sync APIs were not in-use by anyone in the time
+ * between their initial creation (in v1001) and this update. So we're just dropping
+ * and re-creating it to get appropriate columns. The delta is as follows:
+ * - drop foreign key constraint on account_id
+ * - add deleted column
+ */
+ public void upgradeToVersion1008(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS metadata_sync");
+ db.execSQL("CREATE TABLE metadata_sync (" +
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, raw_contact_backup_id TEXT NOT NULL, " +
+ "account_id INTEGER NOT NULL, data TEXT, deleted INTEGER NOT NULL DEFAULT 0);");
+ db.execSQL("CREATE UNIQUE INDEX metadata_sync_index ON metadata_sync (" +
+ "raw_contact_backup_id, account_id);");
+ }
+
+ public void upgradeToVersion1009(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE data ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
+ }
+
public String extractHandleFromEmailAddress(String email) {
Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
if (tokens.length == 0) {
@@ -4356,7 +4609,7 @@
updateIndexStats(db, Tables.RAW_CONTACTS,
"raw_contact_sort_key1_index", "10000 2");
updateIndexStats(db, Tables.RAW_CONTACTS,
- "raw_contacts_source_id_account_id_index", "10000 1 1 1 1");
+ "raw_contacts_source_id_account_id_index", "10000 1 1");
updateIndexStats(db, Tables.NAME_LOOKUP,
"name_lookup_raw_contact_id_index", "35000 4");
@@ -4400,6 +4653,9 @@
updateIndexStats(db, Tables.ACCOUNTS,
null, "3");
+ updateIndexStats(db, Tables.PRE_AUTHORIZED_URIS,
+ null, "1");
+
updateIndexStats(db, Tables.VISIBLE_CONTACTS,
null, "2000");
@@ -4415,6 +4671,9 @@
updateIndexStats(db, Tables.DATA_USAGE_STAT,
"data_usage_stat_index", "20 2 1");
+ updateIndexStats(db, Tables.METADATA_SYNC,
+ "metadata_sync_index", "10000 1 1");
+
// Tiny tables
updateIndexStats(db, Tables.AGGREGATION_EXCEPTIONS,
null, "10");
@@ -4668,7 +4927,14 @@
return mMimeTypeIdSip;
}
- public int getDisplayNameSourceForMimeTypeId(int mimeTypeId) {
+ /**
+ * Returns a {@link ContactsContract.DisplayNameSources} value based on {@param mimeTypeId}.
+ * This does not return {@link ContactsContract.DisplayNameSources#STRUCTURED_PHONETIC_NAME}.
+ * The calling client needs to inspect the structured name itself to distinguish between
+ * {@link ContactsContract.DisplayNameSources#STRUCTURED_NAME} and
+ * {@code STRUCTURED_PHONETIC_NAME}.
+ */
+ private int getDisplayNameSourceForMimeTypeId(int mimeTypeId) {
if (mimeTypeId == mMimeTypeIdStructuredName) {
return DisplayNameSources.STRUCTURED_NAME;
}
@@ -5370,26 +5636,6 @@
}
/**
- * Resets the {@link RawContacts#NAME_VERIFIED} flag to 0 on all other raw
- * contacts in the same aggregate
- */
- public void resetNameVerifiedForOtherRawContacts(long rawContactId) {
- if (mResetNameVerifiedForOtherRawContacts == null) {
- mResetNameVerifiedForOtherRawContacts = getWritableDatabase().compileStatement(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContacts.NAME_VERIFIED + "=0" +
- " WHERE " + RawContacts.CONTACT_ID + "=(" +
- "SELECT " + RawContacts.CONTACT_ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts._ID + "=?)" +
- " AND " + RawContacts._ID + "!=?");
- }
- mResetNameVerifiedForOtherRawContacts.bindLong(1, rawContactId);
- mResetNameVerifiedForOtherRawContacts.bindLong(2, rawContactId);
- mResetNameVerifiedForOtherRawContacts.execute();
- }
-
- /**
* Updates a raw contact display name based on data rows, e.g. structured name,
* organization, email etc.
*/
@@ -5410,6 +5656,22 @@
while (c.moveToNext()) {
int mimeType = c.getInt(RawContactNameQuery.MIMETYPE);
int source = getDisplayNameSourceForMimeTypeId(mimeType);
+
+ if (source == DisplayNameSources.STRUCTURED_NAME) {
+ final String given = c.getString(RawContactNameQuery.GIVEN_NAME);
+ final String middle = c.getString(RawContactNameQuery.MIDDLE_NAME);
+ final String family = c.getString(RawContactNameQuery.FAMILY_NAME);
+ final String suffix = c.getString(RawContactNameQuery.SUFFIX);
+ final String prefix = c.getString(RawContactNameQuery.PREFIX);
+ if (TextUtils.isEmpty(given) && TextUtils.isEmpty(middle)
+ && TextUtils.isEmpty(family) && TextUtils.isEmpty(suffix)
+ && TextUtils.isEmpty(prefix)) {
+ // Every non-phonetic name component is empty. Therefore, lets lower the
+ // source score to STRUCTURED_PHONETIC_NAME.
+ source = DisplayNameSources.STRUCTURED_PHONETIC_NAME;
+ }
+ }
+
if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) {
continue;
}
@@ -5496,7 +5758,8 @@
String sortKeyAlternative = null;
int displayNameStyle = FullNameStyle.UNDEFINED;
- if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) {
+ if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME
+ || bestDisplayNameSource == DisplayNameSources.STRUCTURED_PHONETIC_NAME) {
displayNameStyle = bestName.fullNameStyle;
if (displayNameStyle == FullNameStyle.CJK
|| displayNameStyle == FullNameStyle.UNDEFINED) {
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index f36bed8..b2cbc8b 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -40,9 +40,11 @@
import android.content.res.Resources.NotFoundException;
import android.database.AbstractCursor;
import android.database.Cursor;
+import android.database.CursorWrapper;
import android.database.DatabaseUtils;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
+import android.database.MergeCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteQueryBuilder;
@@ -103,12 +105,12 @@
import android.provider.ContactsContract.StreamItemPhotos;
import android.provider.ContactsContract.StreamItems;
import android.provider.OpenableColumns;
+import android.provider.Settings.Global;
import android.provider.SyncStateContract;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
-
import com.android.common.content.ProjectionMap;
import com.android.common.content.SyncStateContentProviderHelper;
import com.android.common.io.MoreCloseables;
@@ -129,6 +131,7 @@
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.PreAuthorizedUris;
import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Projections;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
@@ -141,26 +144,32 @@
import com.android.providers.contacts.ContactsDatabaseHelper.ViewGroupsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Views;
import com.android.providers.contacts.SearchIndexManager.FtsQueryBuilder;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator.AggregationSuggestionParameter;
import com.android.providers.contacts.aggregation.ContactAggregator;
-import com.android.providers.contacts.aggregation.ContactAggregator.AggregationSuggestionParameter;
+import com.android.providers.contacts.aggregation.ContactAggregator2;
import com.android.providers.contacts.aggregation.ProfileAggregator;
import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
import com.android.providers.contacts.database.ContactsTableUtil;
import com.android.providers.contacts.database.DeletedContactsTableUtil;
import com.android.providers.contacts.database.MoreDatabaseUtils;
+import com.android.providers.contacts.MetadataEntryParser.AggregationData;
+import com.android.providers.contacts.MetadataEntryParser.FieldData;
+import com.android.providers.contacts.MetadataEntryParser.MetadataEntry;
+import com.android.providers.contacts.MetadataEntryParser.RawContactInfo;
+import com.android.providers.contacts.MetadataEntryParser.UsageStats;
import com.android.providers.contacts.util.Clock;
+import com.android.providers.contacts.util.ContactsPermissions;
import com.android.providers.contacts.util.DbQueryUtils;
import com.android.providers.contacts.util.NeededForTesting;
import com.android.providers.contacts.util.UserUtils;
import com.android.vcard.VCardComposer;
import com.android.vcard.VCardConfig;
-
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import com.google.android.collect.Sets;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
-
import libcore.io.IoUtils;
import java.io.BufferedWriter;
@@ -194,6 +203,7 @@
public class ContactsProvider2 extends AbstractContactsProvider
implements OnAccountsUpdateListener {
+ private static final String READ_PERMISSION = "android.permission.READ_CONTACTS";
private static final String WRITE_PERMISSION = "android.permission.WRITE_CONTACTS";
/* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
@@ -338,6 +348,8 @@
private static final int CALLABLES_FILTER = 3013;
private static final int CONTACTABLES = 3014;
private static final int CONTACTABLES_FILTER = 3015;
+ private static final int PHONES_ENTERPRISE = 3016;
+ private static final int EMAILS_LOOKUP_ENTERPRISE = 3017;
private static final int PHONE_LOOKUP = 4000;
private static final int PHONE_LOOKUP_ENTERPRISE = 4001;
@@ -365,6 +377,7 @@
private static final int SEARCH_SHORTCUT = 12002;
private static final int RAW_CONTACT_ENTITIES = 15001;
+ private static final int RAW_CONTACT_ENTITIES_CORP = 15002;
private static final int PROVIDER_STATUS = 16001;
@@ -605,7 +618,6 @@
RawContacts.DATA_SET,
RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
RawContacts.DIRTY,
- RawContacts.NAME_VERIFIED,
RawContacts.SOURCE_ID,
RawContacts.VERSION,
};
@@ -666,8 +678,8 @@
.add(RawContacts.DATA_SET)
.add(RawContacts.ACCOUNT_TYPE_AND_DATA_SET)
.add(RawContacts.DIRTY)
- .add(RawContacts.NAME_VERIFIED)
.add(RawContacts.SOURCE_ID)
+ .add(RawContacts.BACKUP_ID)
.add(RawContacts.VERSION)
.build();
@@ -871,6 +883,7 @@
private static final ProjectionMap sDataProjectionMap = ProjectionMap.builder()
.add(Data._ID)
.add(Data.RAW_CONTACT_ID)
+ .add(Data.HASH_ID)
.add(Data.CONTACT_ID)
.add(Data.NAME_RAW_CONTACT_ID)
.add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
@@ -1198,10 +1211,12 @@
RAW_CONTACTS_ID_STREAM_ITEMS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);
+ matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities_corp", RAW_CONTACT_ENTITIES_CORP);
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_enterprise/phones", PHONES_ENTERPRISE);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
@@ -1211,6 +1226,10 @@
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
+ matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup_enterprise",
+ EMAILS_LOOKUP_ENTERPRISE);
+ matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup_enterprise/*",
+ EMAILS_LOOKUP_ENTERPRISE);
matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
/** "*" is in CSV form with data IDs ("123,456,789") */
@@ -1333,7 +1352,8 @@
// Depending on whether the action being performed is for the profile or not, we will use one of
// two aggregator instances.
- private final ThreadLocal<ContactAggregator> mAggregator = new ThreadLocal<ContactAggregator>();
+ private final ThreadLocal<AbstractContactAggregator> mAggregator =
+ new ThreadLocal<AbstractContactAggregator>();
// Depending on whether the action being performed is for the profile or not, we will use one of
// two photo store instances (with their files stored in separate sub-directories).
@@ -1347,9 +1367,6 @@
private final ThreadLocal<TransactionContext> mTransactionContext =
new ThreadLocal<TransactionContext>();
- // Map of single-use pre-authorized URIs to expiration times.
- private final Map<Uri, Long> mPreAuthorizedUris = Maps.newHashMap();
-
// Random number generator.
private final SecureRandom mRandom = new SecureRandom();
@@ -1397,8 +1414,8 @@
private Account mAccount;
- private ContactAggregator mContactAggregator;
- private ContactAggregator mProfileAggregator;
+ private AbstractContactAggregator mContactAggregator;
+ private AbstractContactAggregator mProfileAggregator;
// Duration in milliseconds that pre-authorized URIs will remain valid.
private long mPreAuthorizedUriDuration;
@@ -1410,7 +1427,6 @@
private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
private boolean mProviderStatusUpdateNeeded;
- private long mEstimatedStorageRequirement = 0;
private volatile CountDownLatch mReadAccessLatch;
private volatile CountDownLatch mWriteAccessLatch;
private boolean mAccountUpdateListenerRegistered;
@@ -1497,8 +1513,6 @@
mProfileProvider = newProfileProvider();
mProfileProvider.setDbHelperToSerializeOn(mContactsHelper, CONTACTS_DB_TAG, this);
ProviderInfo profileInfo = new ProviderInfo();
- profileInfo.readPermission = "android.permission.READ_PROFILE";
- profileInfo.writePermission = "android.permission.WRITE_PROFILE";
profileInfo.authority = ContactsContract.AUTHORITY;
mProfileProvider.attachInfo(getContext(), profileInfo);
mProfileHelper = mProfileProvider.getDatabaseHelper(getContext());
@@ -1519,6 +1533,18 @@
return true;
}
+ @VisibleForTesting
+ public void setNewAggregatorForTest(boolean enabled) {
+ mContactAggregator = (enabled)
+ ? new ContactAggregator2(this, mContactsHelper,
+ createPhotoPriorityResolver(getContext()), mNameSplitter, mCommonNicknameCache)
+ : new ContactAggregator(this, mContactsHelper,
+ createPhotoPriorityResolver(getContext()), mNameSplitter, mCommonNicknameCache);
+ mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
+ initDataRowHandlers(mDataRowHandlers, mContactsHelper, mContactAggregator,
+ mContactsPhotoStore);
+ }
+
// Updates the locale set to reflect a new system locale.
private static LocaleSet updateLocaleSet(LocaleSet oldLocales, Locale newLocale) {
final Locale prevLocale = oldLocales.getPrimaryLocale();
@@ -1566,8 +1592,15 @@
mPostalSplitter = new PostalSplitter(mCurrentLocales.getPrimaryLocale());
mCommonNicknameCache = new CommonNicknameCache(mContactsHelper.getReadableDatabase());
ContactLocaleUtils.setLocales(mCurrentLocales);
- mContactAggregator = new ContactAggregator(this, mContactsHelper,
- createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
+
+ int value = android.provider.Settings.Global.getInt(context.getContentResolver(),
+ Global.NEW_CONTACT_AGGREGATOR, 0);
+ mContactAggregator = (value == 0)
+ ? new ContactAggregator(this, mContactsHelper,
+ createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache)
+ : new ContactAggregator2(this, mContactsHelper,
+ createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
+
mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
mProfileAggregator = new ProfileAggregator(this, mProfileHelper,
createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
@@ -1589,7 +1622,7 @@
}
private void initDataRowHandlers(Map<String, DataRowHandler> handlerMap,
- ContactsDatabaseHelper dbHelper, ContactAggregator contactAggregator,
+ ContactsDatabaseHelper dbHelper, AbstractContactAggregator contactAggregator,
PhotoStore photoStore) {
Context context = getContext();
handlerMap.put(Email.CONTENT_ITEM_TYPE,
@@ -2145,14 +2178,6 @@
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- if (mWriteAccessLatch != null) {
- // Update on PROVIDER_STATUS used to be used as a trigger to re-start legacy contact
- // import. Now that we no longer support it, we just ignore it.
- int match = sUriMatcher.match(uri);
- if (match == PROVIDER_STATUS) {
- return 0;
- }
- }
waitForAccess(mWriteAccessLatch);
// Enforce stream items access check if applicable.
@@ -2186,14 +2211,13 @@
waitForAccess(mReadAccessLatch);
switchToContactMode();
if (Authorization.AUTHORIZATION_METHOD.equals(method)) {
- Uri uri = (Uri) extras.getParcelable(Authorization.KEY_URI_TO_AUTHORIZE);
+ Uri uri = extras.getParcelable(Authorization.KEY_URI_TO_AUTHORIZE);
// Check permissions on the caller. The URI can only be pre-authorized if the caller
- // already has the necessary permissions.
+ // already has the necessary permissions. And, we can't rely on the ContentResolver to
+ // enforce permissions for the ContentProvider#call() method.
enforceSocialStreamReadPermission(uri);
- if (mapsToProfileDb(uri)) {
- mProfileProvider.enforceReadPermission(uri);
- }
+ ContactsPermissions.enforceCallingOrSelfPermission(getContext(), READ_PERMISSION);
// If there hasn't been a security violation yet, we're clear to pre-authorize the URI.
Uri authUri = preAuthorizeUri(uri);
@@ -2201,7 +2225,7 @@
response.putParcelable(Authorization.KEY_AUTHORIZED_URI, authUri);
return response;
} else if (PinnedPositions.UNDEMOTE_METHOD.equals(method)) {
- getContext().enforceCallingOrSelfPermission(WRITE_PERMISSION, null);
+ ContactsPermissions.enforceCallingOrSelfPermission(getContext(), WRITE_PERMISSION);
final long id;
try {
id = Long.valueOf(arg);
@@ -2225,8 +2249,13 @@
Uri authUri = uri.buildUpon()
.appendQueryParameter(PREAUTHORIZED_URI_TOKEN, token)
.build();
- long expiration = SystemClock.elapsedRealtime() + mPreAuthorizedUriDuration;
- mPreAuthorizedUris.put(authUri, expiration);
+ long expiration = Clock.getInstance().currentTimeMillis() + mPreAuthorizedUriDuration;
+
+ final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
+ final ContentValues values = new ContentValues();
+ values.put(PreAuthorizedUris.EXPIRATION, expiration);
+ values.put(PreAuthorizedUris.URI, authUri.toString());
+ db.insert(Tables.PRE_AUTHORIZED_URIS, null, values);
return authUri;
}
@@ -2240,22 +2269,27 @@
public boolean isValidPreAuthorizedUri(Uri uri) {
// Only proceed if the URI has a permission token parameter.
if (uri.getQueryParameter(PREAUTHORIZED_URI_TOKEN) != null) {
- // First expire any pre-authorization URIs that are no longer valid.
- long now = SystemClock.elapsedRealtime();
- Set<Uri> expiredUris = Sets.newHashSet();
- for (Uri preAuthUri : mPreAuthorizedUris.keySet()) {
- if (mPreAuthorizedUris.get(preAuthUri) < now) {
- expiredUris.add(preAuthUri);
- }
- }
- for (Uri expiredUri : expiredUris) {
- mPreAuthorizedUris.remove(expiredUri);
- }
+ final long now = Clock.getInstance().currentTimeMillis();
+ final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
+ db.beginTransaction();
+ try {
+ // First delete any pre-authorization URIs that are no longer valid. Unfortunately,
+ // this operation will grab a write lock for readonly queries. Since this only
+ // affects readonly queries that use PREAUTHORIZED_URI_TOKEN, it isn't worth moving
+ // this deletion into a BACKGROUND_TASK.
+ db.delete(Tables.PRE_AUTHORIZED_URIS, PreAuthorizedUris.EXPIRATION + " < ?1",
+ new String[]{String.valueOf(now)});
- // Now check to see if the pre-authorized URI map contains the URI.
- if (mPreAuthorizedUris.containsKey(uri)) {
- // Unexpired token - skip the permission check.
- return true;
+ // Now check to see if the pre-authorized URI map contains the URI.
+ final Cursor c = db.query(Tables.PRE_AUTHORIZED_URIS, null,
+ PreAuthorizedUris.URI + "=?1",
+ new String[]{uri.toString()}, null, null, null);
+ final boolean isValid = c.getCount() != 0;
+
+ db.setTransactionSuccessful();
+ return isValid;
+ } finally {
+ db.endTransaction();
}
}
return false;
@@ -2947,8 +2981,8 @@
private void enforceSocialStreamReadPermission(Uri uri) {
if (SOCIAL_STREAM_URIS.contains(sUriMatcher.match(uri))
&& !isValidPreAuthorizedUri(uri)) {
- getContext().enforceCallingOrSelfPermission(
- "android.permission.READ_SOCIAL_STREAM", null);
+ ContactsPermissions.enforceCallingOrSelfPermission(getContext(),
+ "android.permission.READ_SOCIAL_STREAM");
}
}
@@ -2959,8 +2993,8 @@
*/
private void enforceSocialStreamWritePermission(Uri uri) {
if (SOCIAL_STREAM_URIS.contains(sUriMatcher.match(uri))) {
- getContext().enforceCallingOrSelfPermission(
- "android.permission.WRITE_SOCIAL_STREAM", null);
+ ContactsPermissions.enforceCallingOrSelfPermission(getContext(),
+ "android.permission.WRITE_SOCIAL_STREAM");
}
}
@@ -4382,7 +4416,7 @@
int count = db.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1);
if (count != 0) {
- final ContactAggregator aggregator = mAggregator.get();
+ final AbstractContactAggregator aggregator = mAggregator.get();
int aggregationMode = getIntValue(
values, RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
@@ -4419,14 +4453,6 @@
if (values.containsKey(RawContacts.SOURCE_ID)) {
aggregator.updateLookupKeyForRawContact(db, rawContactId);
}
- if (flagExists(values, RawContacts.NAME_VERIFIED)) {
- // If setting NAME_VERIFIED for this raw contact, reset it for all
- // other raw contacts in the same aggregate
- if (flagIsSet(values, RawContacts.NAME_VERIFIED)) {
- mDbHelper.get().resetNameVerifiedForOtherRawContacts(rawContactId);
- }
- aggregator.updateDisplayNameForRawContact(db, rawContactId);
- }
if (requestUndoDelete && previousDeleted == 1) {
// Note before the accounts refactoring, we used to use the *old* account here,
// which doesn't make sense, so now we pass the *new* account.
@@ -4645,7 +4671,7 @@
db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, exceptionValues);
}
- final ContactAggregator aggregator = mAggregator.get();
+ final AbstractContactAggregator aggregator = mAggregator.get();
aggregator.invalidateAggregationExceptionCache();
aggregator.markForAggregation(rawContactId1, RawContacts.AGGREGATION_MODE_DEFAULT, true);
aggregator.markForAggregation(rawContactId2, RawContacts.AGGREGATION_MODE_DEFAULT, true);
@@ -4663,6 +4689,162 @@
scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
}
+ interface RawContactsBackupQuery {
+ String TABLE = Tables.RAW_CONTACTS;
+ String[] COLUMNS = new String[] {
+ RawContacts._ID,
+ };
+ int RAW_CONTACT_ID = 0;
+ String SELECTION = RawContacts.DELETED + "=0 AND " +
+ RawContacts.BACKUP_ID + "=? AND " +
+ RawContactsColumns.ACCOUNT_ID + "=?";
+ }
+
+ /**
+ * Fetch rawContactId related to the given backupId.
+ * Return 0 if there's no such rawContact or it's deleted.
+ */
+ private long queryRawContactId(SQLiteDatabase db, String backupId, long accountId) {
+ if (TextUtils.isEmpty(backupId)) {
+ return 0;
+ }
+ mSelectionArgs2[0] = backupId;
+ mSelectionArgs2[1] = String.valueOf(accountId);
+ long rawContactId = 0;
+ final Cursor cursor = db.query(RawContactsBackupQuery.TABLE,
+ RawContactsBackupQuery.COLUMNS, RawContactsBackupQuery.SELECTION,
+ mSelectionArgs2, null, null, null);
+ try {
+ if (cursor.moveToFirst()) {
+ rawContactId = cursor.getLong(RawContactsBackupQuery.RAW_CONTACT_ID);
+ }
+ } finally {
+ cursor.close();
+ }
+ return rawContactId;
+ }
+
+ interface DataHashQuery {
+ String TABLE = Tables.DATA;
+ String[] COLUMNS = new String[] {
+ Data._ID,
+ };
+ int DATA_ID = 0;
+ String SELECTION = Data.HASH_ID + "=?";
+ }
+
+ /**
+ * Fetch a list of dataId related to the given hashId.
+ * Return empty list if there's no such data.
+ */
+ private ArrayList<Long> queryDataId(SQLiteDatabase db, String hashId) {
+ if (TextUtils.isEmpty(hashId)) {
+ return new ArrayList<>();
+ }
+ mSelectionArgs1[0] = hashId;
+ ArrayList<Long> result = new ArrayList<>();
+ long dataId = 0;
+ final Cursor c = db.query(DataHashQuery.TABLE, DataHashQuery.COLUMNS,
+ DataHashQuery.SELECTION, mSelectionArgs1, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ dataId = c.getLong(DataHashQuery.DATA_ID);
+ result.add(dataId);
+ }
+ } finally {
+ c.close();
+ }
+ return result;
+ }
+
+ private long searchRawContactIdForRawContactInfo(SQLiteDatabase db,
+ RawContactInfo rawContactInfo) {
+ if (rawContactInfo == null) {
+ return 0;
+ }
+ final String backupId = rawContactInfo.mBackupId;
+ final String accountType = rawContactInfo.mAccountType;
+ final String accountName = rawContactInfo.mAccountName;
+ final String dataSet = rawContactInfo.mDataSet;
+ ContentValues values = new ContentValues();
+ values.put(AccountsColumns.ACCOUNT_TYPE, accountType);
+ values.put(AccountsColumns.ACCOUNT_NAME, accountName);
+ if (dataSet != null) {
+ values.put(AccountsColumns.DATA_SET, dataSet);
+ }
+
+ final long accountId = replaceAccountInfoByAccountId(RawContacts.CONTENT_URI, values);
+ final long rawContactId = queryRawContactId(db, backupId, accountId);
+ return rawContactId;
+ }
+
+ /**
+ * Update RawContact, Data, DataUsageStats, AggregationException tables from MetadataEntry.
+ */
+ @NeededForTesting
+ void updateFromMetaDataEntry(SQLiteDatabase db, MetadataEntry metadataEntry) {
+ final RawContactInfo rawContactInfo = metadataEntry.mRawContactInfo;
+ final long rawContactId = searchRawContactIdForRawContactInfo(db, rawContactInfo);
+ if (rawContactId == 0) {
+ return;
+ }
+
+ ContentValues rawContactValues = new ContentValues();
+ rawContactValues.put(RawContacts.SEND_TO_VOICEMAIL, metadataEntry.mSendToVoicemail);
+ rawContactValues.put(RawContacts.STARRED, metadataEntry.mStarred);
+ rawContactValues.put(RawContacts.PINNED, metadataEntry.mPinned);
+ updateRawContact(db, rawContactId, rawContactValues, true);
+
+ // Update Data and DataUsageStats table.
+ for (int i = 0; i < metadataEntry.mFieldDatas.size(); i++) {
+ final FieldData fieldData = metadataEntry.mFieldDatas.get(i);
+ final String dataHashId = fieldData.mDataHashId;
+ final ArrayList<Long> dataIds = queryDataId(db, dataHashId);
+
+ for (long dataId : dataIds) {
+ // Update is_primary and is_super_primary.
+ ContentValues dataValues = new ContentValues();
+ dataValues.put(Data.IS_PRIMARY, fieldData.mIsPrimary);
+ dataValues.put(Data.IS_SUPER_PRIMARY, fieldData.mIsSuperPrimary);
+ updateData(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
+ dataValues, null, null, true);
+
+ // Update UsageStats.
+ for (int j = 0; j < fieldData.mUsageStatsList.size(); j++) {
+ final UsageStats usageStats = fieldData.mUsageStatsList.get(j);
+ final String usageType = usageStats.mUsageType;
+ final int typeInt = getDataUsageFeedbackType(usageType.toLowerCase(), null);
+ final long lastTimeUsed = usageStats.mLastTimeUsed;
+ final int timesUsed = usageStats.mTimesUsed;
+ ContentValues usageStatsValues = new ContentValues();
+ usageStatsValues.put(DataUsageStatColumns.DATA_ID, dataId);
+ usageStatsValues.put(DataUsageStatColumns.USAGE_TYPE_INT, typeInt);
+ usageStatsValues.put(DataUsageStatColumns.LAST_TIME_USED, lastTimeUsed);
+ usageStatsValues.put(DataUsageStatColumns.TIMES_USED, timesUsed);
+ updateDataUsageStats(db, usageStatsValues);
+ }
+ }
+ }
+
+ // Update AggregationException table.
+ for (int i = 0; i < metadataEntry.mAggregationDatas.size(); i++) {
+ final AggregationData aggregationData = metadataEntry.mAggregationDatas.get(i);
+ final int typeInt = getAggregationType(aggregationData.mType, null);
+ final RawContactInfo aggregationContact1 = aggregationData.mRawContactInfo1;
+ final RawContactInfo aggregationContact2 = aggregationData.mRawContactInfo2;
+ final long rawContactId1 = searchRawContactIdForRawContactInfo(db, aggregationContact1);
+ final long rawContactId2 = searchRawContactIdForRawContactInfo(db, aggregationContact2);
+ if (rawContactId1 == 0 || rawContactId2 == 0) {
+ continue;
+ }
+ ContentValues values = new ContentValues();
+ values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
+ values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
+ values.put(AggregationExceptions.TYPE, typeInt);
+ updateAggregationException(db, values);
+ }
+ }
+
/** return serialized version of {@code accounts} */
@VisibleForTesting
static String accountsToString(Set<Account> accounts) {
@@ -5661,7 +5843,9 @@
new String[] {DisplayPhoto.DISPLAY_MAX_DIM, DisplayPhoto.THUMBNAIL_MAX_DIM},
new Object[] {getMaxDisplayPhotoDim(), getMaxThumbnailDim()});
}
-
+ case PHONES_ENTERPRISE: {
+ return queryMergedDataPhones(uri, projection, selection, selectionArgs, sortOrder);
+ }
case PHONES:
case CALLABLES: {
final String mimeTypeIsPhoneExpression =
@@ -5870,6 +6054,10 @@
}
break;
}
+ case EMAILS_LOOKUP_ENTERPRISE: {
+ return queryEmailsLookupEnterprise(uri, projection, selection, selectionArgs,
+ sortOrder);
+ }
case EMAILS_FILTER: {
String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
@@ -6166,10 +6354,8 @@
if (uri.getPathSegments().size() != 2) {
throw new IllegalArgumentException("Phone number missing in URI: " + uri);
}
- final String phoneNumber = Uri.decode(uri.getLastPathSegment());
- final boolean isSipAddress = uri.getBooleanQueryParameter(
- PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
- return queryPhoneLookupEnterprise(phoneNumber, projection, isSipAddress);
+ return queryPhoneLookupEnterprise(uri, projection, selection, selectionArgs,
+ sortOrder);
}
case PHONE_LOOKUP: {
// Phone lookup cannot be combined with a selection
@@ -6382,6 +6568,21 @@
setTablesAndProjectionMapForRawEntities(qb, uri);
break;
}
+ case RAW_CONTACT_ENTITIES_CORP: {
+ // As it's protected by android.permission.INTERACT_ACROSS_USERS
+ // already and it is used by Bluetooth application, we do not
+ // check caller-id policy
+ final int corpUserId = UserUtils.getCorpUserId(getContext(), false);
+ if (corpUserId < 0) {
+ // No Corp user or policy not allowed, return empty cursor
+ final String[] outputProjection = (projection != null) ? projection
+ : sRawEntityProjectionMap.getColumnNames();
+ return new MatrixCursor(outputProjection);
+ }
+ final Uri remoteUri = maybeAddUserId(RawContactsEntity.CONTENT_URI, corpUserId);
+ return getContext().getContentResolver().query(remoteUri, projection, selection,
+ selectionArgs, sortOrder);
+ }
case RAW_CONTACT_ID_ENTITY: {
long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
@@ -6393,8 +6594,8 @@
case PROVIDER_STATUS: {
return buildSingleRowResult(projection,
- new String[] {ProviderStatus.STATUS, ProviderStatus.DATA1},
- new Object[] {mProviderStatus, mEstimatedStorageRequirement});
+ new String[] {ProviderStatus.STATUS},
+ new Object[] {mProviderStatus});
}
case DIRECTORIES : {
@@ -6498,26 +6699,95 @@
return c;
}
- /**
- * Handles {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
- */
- // TODO Test
- private Cursor queryPhoneLookupEnterprise(String phoneNumber, String[] projection,
- boolean isSipAddress) {
+ private static class EnterprisePhoneCursorWrapper extends CursorWrapper {
- final int corpUserId = UserUtils.getCorpUserId(getContext());
+ public EnterprisePhoneCursorWrapper(Cursor cursor) {
+ super(cursor);
+ }
+
+ @Override
+ public int getInt(int column) {
+ return (int) getLong(column);
+ }
+
+ @Override
+ public long getLong(int column) {
+ long result = super.getLong(column);
+ String columnName = getColumnName(column);
+ // We change contactId only for now
+ switch (columnName) {
+ case Phone.CONTACT_ID:
+ return result + Contacts.ENTERPRISE_CONTACT_ID_BASE;
+ default:
+ return result;
+ }
+ }
+ }
+
+ /**
+ * Handles {@link Phone#ENTERPRISE_CONTENT_URI}.
+ */
+ private Cursor queryMergedDataPhones(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ final List<String> pathSegments = uri.getPathSegments();
+ final int pathSegmentsSize = pathSegments.size();
+ // Ignore the first 2 path segments: "/data_enterprise/phones"
+ final StringBuilder newPathBuilder = new StringBuilder(Phone.CONTENT_URI.getPath());
+ for (int i = 2; i < pathSegmentsSize; i++) {
+ newPathBuilder.append('/');
+ newPathBuilder.append(pathSegments.get(i));
+ }
+ // Change /data_enterprise/phones/... to /data/phones/...
+ final Uri localUri = uri.buildUpon().path(newPathBuilder.toString()).build();
+ final String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY);
+ final long directoryId =
+ (directory == null ? -1 :
+ (directory.equals("0") ? Directory.DEFAULT :
+ (directory.equals("1") ? Directory.LOCAL_INVISIBLE : Long.MIN_VALUE)));
+ final Cursor primaryCursor = queryLocal(localUri, projection, selection, selectionArgs,
+ sortOrder, directoryId, null);
+ try {
+ // As it's protected by android.permission.INTERACT_ACROSS_USERS
+ // already and it is used by Bluetooth application, we do not
+ // check caller-id policy
+ final int corpUserId = UserUtils.getCorpUserId(getContext(), false);
+ if (corpUserId < 0) {
+ // No Corp user or policy not allowed
+ return primaryCursor;
+ }
+ final Uri remoteUri = maybeAddUserId(localUri, corpUserId);
+ final Cursor managedCursor = getContext().getContentResolver().query(remoteUri,
+ projection, selection, selectionArgs, sortOrder, null);
+ final Cursor[] cursorArray = new Cursor[] {
+ primaryCursor, new EnterprisePhoneCursorWrapper(managedCursor)
+ };
+ // Sort order is not supported yet, will be fixed in M when we have
+ // merged provider
+ // MergeCursor will copy all the contacts from two cursors, which may
+ // cause OOM if there's a lot of contacts. But it's only used by
+ // Bluetooth, and Bluetooth will loop through the Cursor and put all
+ // content in ArrayList anyway, so we ignore OOM issue here for now
+ final MergeCursor mergeCursor = new MergeCursor(cursorArray);
+ return mergeCursor;
+ } catch (Throwable th) {
+ if (primaryCursor != null) {
+ primaryCursor.close();
+ }
+ throw th;
+ }
+ }
+
+ private Cursor queryEnterpriseIfNecessary(Uri localUri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder, String contactIdColumnName) {
+
+ final int corpUserId = UserUtils.getCorpUserId(getContext(), true);
// Step 1. Look at the database on the current profile.
- final Uri localUri = PhoneLookup.CONTENT_FILTER_URI.buildUpon().appendPath(phoneNumber)
- .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
- String.valueOf(isSipAddress))
- .build();
if (VERBOSE_LOGGING) {
Log.v(TAG, "queryPhoneLookupEnterprise: local query URI=" + localUri);
}
- final Cursor local = queryLocal(localUri, projection,
- /* selection */ null, /* args */ null, /* order */ null, /* directory */ 0,
- /* cancellationsignal*/ null);
+ final Cursor local = queryLocal(localUri, projection, selection, selectionArgs,
+ sortOrder, /* directory */ 0, /* cancellationsignal */null);
try {
if (VERBOSE_LOGGING) {
MoreDatabaseUtils.dumpCursor(TAG, "local", local);
@@ -6532,6 +6802,10 @@
throw th;
}
+ if (local != null) {
+ local.close();
+ }
+
// Step 2. No rows found in the local db, and there is a corp profile. Look at the corp
// DB.
@@ -6541,14 +6815,16 @@
if (VERBOSE_LOGGING) {
Log.v(TAG, "queryPhoneLookupEnterprise: corp query URI=" + remoteUri);
}
- final Cursor corp = getContext().getContentResolver().query(remoteUri, projection,
- /* selection */ null, /* args */ null, /* order */ null,
- /* cancellationsignal*/ null);
+ // Note in order to re-write the cursor correctly, we need all columns from the corp cp2.
+ final Cursor corp = getContext().getContentResolver().query(remoteUri, null,
+ selection, selectionArgs, sortOrder, /* cancellationsignal */null);
try {
if (VERBOSE_LOGGING) {
MoreDatabaseUtils.dumpCursor(TAG, "corp raw", corp);
}
- final Cursor rewritten = rewriteCorpPhoneLookup(corp);
+ final Cursor rewritten = rewriteCorpLookup(
+ (projection != null ? projection : corp.getColumnNames()), corp,
+ contactIdColumnName);
if (VERBOSE_LOGGING) {
MoreDatabaseUtils.dumpCursor(TAG, "corp rewritten", rewritten);
}
@@ -6560,56 +6836,100 @@
}
/**
- * Rewrite a cursor from the corp profile for {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
+ * Handles {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
+ */
+ // TODO Test
+ private Cursor queryPhoneLookupEnterprise(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ final String phoneNumber = Uri.decode(uri.getLastPathSegment());
+ final boolean isSipAddress = uri.getBooleanQueryParameter(
+ PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ final Uri localUri = PhoneLookup.CONTENT_FILTER_URI
+ .buildUpon()
+ .appendPath(phoneNumber)
+ .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
+ String.valueOf(isSipAddress)).build();
+ return queryEnterpriseIfNecessary(localUri, projection, null, null, null,
+ isSipAddress ? Data.CONTACT_ID : PhoneLookup._ID);
+ }
+
+ /**
+ * Handles {@link Email#ENTERPRISE_CONTENT_LOOKUP_URI}.
+ */
+ private Cursor queryEmailsLookupEnterprise(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ final List<String> pathSegments = uri.getPathSegments();
+ final int pathSegmentsSize = pathSegments.size();
+ // Ignore the first 3 path segments: "/data/emails_enterprise/lookup"
+ final StringBuilder newPathBuilder = new StringBuilder(Email.CONTENT_LOOKUP_URI.getPath());
+ for (int i = 3; i < pathSegmentsSize; i++) {
+ newPathBuilder.append('/');
+ newPathBuilder.append(pathSegments.get(i));
+ }
+ final Uri localUri = uri.buildUpon().path(newPathBuilder.toString()).build();
+ return queryEnterpriseIfNecessary(localUri, projection, selection, selectionArgs,
+ sortOrder, Data.CONTACT_ID);
+ }
+
+ /**
+ * Rewrite a cursor from the corp profile data
*/
@VisibleForTesting
- static Cursor rewriteCorpPhoneLookup(Cursor original) {
- final String[] columns = original.getColumnNames();
- final MatrixCursor ret = new MatrixCursor(columns);
-
+ static Cursor rewriteCorpLookup(String[] projection, Cursor original,
+ String contactIdColumnName) {
+ final MatrixCursor ret = new MatrixCursor(projection);
original.moveToPosition(-1);
while (original.moveToNext()) {
- final int contactId = original.getInt(original.getColumnIndex(PhoneLookup._ID));
-
+ final int contactId = original.getInt(original.getColumnIndex(contactIdColumnName));
final MatrixCursor.RowBuilder builder = ret.newRow();
-
- for (int i = 0; i < columns.length; i++) {
- switch (columns[i]) {
+ for (int i = 0; i < projection.length; i++) {
+ final String outputColumnName = projection[i];
+ final int originalColumnIndex = original.getColumnIndex(outputColumnName);
+ switch (outputColumnName) {
// Set artificial photo URLs using Contacts.CORP_CONTENT_URI.
- case PhoneLookup.PHOTO_THUMBNAIL_URI:
+ case Contacts.PHOTO_THUMBNAIL_URI:
builder.add(getCorpThumbnailUri(contactId, original));
break;
- case PhoneLookup.PHOTO_URI:
+ case Contacts.PHOTO_URI:
builder.add(getCorpDisplayPhotoUri(contactId, original));
break;
- case PhoneLookup._ID:
- builder.add(original.getLong(i) + Contacts.ENTERPRISE_CONTACT_ID_BASE);
- break;
-
- // These columns are set to null.
- // See the javadoc on PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI for the reasons.
- case PhoneLookup.PHOTO_FILE_ID:
- case PhoneLookup.PHOTO_ID:
- case PhoneLookup.CUSTOM_RINGTONE:
+ case Data.PHOTO_FILE_ID:
+ case Data.PHOTO_ID:
+ case Data.CUSTOM_RINGTONE:
builder.add(null);
break;
+ case Contacts.LOOKUP_KEY:
+ final String lookupKey = original.getString(originalColumnIndex);
+ if (TextUtils.isEmpty(lookupKey)) {
+ builder.add(null);
+ } else {
+ builder.add(Contacts.ENTERPRISE_CONTACT_LOOKUP_PREFIX + lookupKey);
+ }
+ break;
default:
+ if (outputColumnName.equals(contactIdColumnName)) {
+ // This will be _id if it's PhoneLookup, contacts_id
+ // if it's Data.CONTACT_ID
+ builder.add(original.getLong(originalColumnIndex)
+ + Contacts.ENTERPRISE_CONTACT_ID_BASE);
+ break;
+ }
// Copy the original value.
- switch (original.getType(i)) {
+ switch (original.getType(originalColumnIndex)) {
case Cursor.FIELD_TYPE_NULL:
builder.add(null);
break;
case Cursor.FIELD_TYPE_INTEGER:
- builder.add(original.getLong(i));
+ builder.add(original.getLong(originalColumnIndex));
break;
case Cursor.FIELD_TYPE_FLOAT:
- builder.add(original.getFloat(i));
+ builder.add(original.getFloat(originalColumnIndex));
break;
case Cursor.FIELD_TYPE_STRING:
- builder.add(original.getString(i));
+ builder.add(original.getString(originalColumnIndex));
break;
case Cursor.FIELD_TYPE_BLOB:
- builder.add(original.getBlob(i));
+ builder.add(original.getBlob(originalColumnIndex));
break;
}
}
@@ -8033,7 +8353,7 @@
throw new IllegalArgumentException(
"Photos retrieved by contact ID can only be read.");
}
- final int corpUserId = UserUtils.getCorpUserId(getContext());
+ final int corpUserId = UserUtils.getCorpUserId(getContext(), true);
if (corpUserId < 0) {
// No corp profile or the currrent profile is not the personal.
throw new FileNotFoundException(uri.toString());
@@ -8317,6 +8637,7 @@
return mContactsHelper.getDataMimeType(id);
}
case PHONES:
+ case PHONES_ENTERPRISE:
return Phone.CONTENT_TYPE;
case PHONES_ID:
return Phone.CONTENT_ITEM_TYPE;
@@ -8391,6 +8712,7 @@
case DATA_ID:
case PHONES:
+ case PHONES_ENTERPRISE:
case PHONES_ID:
case EMAILS:
case EMAILS_ID:
@@ -8911,6 +9233,52 @@
}
/**
+ * Update {@link Tables#DATA_USAGE_STAT}.
+ * Update or insert usageType, lastTimeUsed, and timesUsed for specific dataId.
+ */
+ private void updateDataUsageStats(SQLiteDatabase db, ContentValues values) {
+ final String dataId = values.getAsString(DataUsageStatColumns.DATA_ID);
+ final String type = values.getAsString(DataUsageStatColumns.USAGE_TYPE_INT);
+ final String lastTimeUsed = values.getAsString(DataUsageStatColumns.LAST_TIME_USED);
+ final String timesUsed = values.getAsString(DataUsageStatColumns.TIMES_USED);
+
+ mSelectionArgs2[0] = dataId;
+ mSelectionArgs2[1] = type;
+ final Cursor cursor = db.query(DataUsageStatQuery.TABLE,
+ DataUsageStatQuery.COLUMNS, DataUsageStatQuery.SELECTION,
+ mSelectionArgs2, null, null, null);
+
+ try {
+ if (cursor.moveToFirst()) {
+ final long id = cursor.getLong(DataUsageStatQuery.ID);
+
+ mSelectionArgs3[0] = lastTimeUsed;
+ mSelectionArgs3[1] = timesUsed;
+ mSelectionArgs3[2] = String.valueOf(id);
+ db.execSQL("UPDATE " + Tables.DATA_USAGE_STAT +
+ " SET " + DataUsageStatColumns.LAST_TIME_USED + "=?" +
+ "," + DataUsageStatColumns.TIMES_USED + "=?" +
+ " WHERE " + DataUsageStatColumns._ID + "=?",
+ mSelectionArgs3);
+ } else {
+ mSelectionArgs4[0] = dataId;
+ mSelectionArgs4[1] = type;
+ mSelectionArgs4[2] = timesUsed;
+ mSelectionArgs4[3] = lastTimeUsed;
+ db.execSQL("INSERT INTO " + Tables.DATA_USAGE_STAT +
+ "(" + DataUsageStatColumns.DATA_ID +
+ "," + DataUsageStatColumns.USAGE_TYPE_INT +
+ "," + DataUsageStatColumns.TIMES_USED +
+ "," + DataUsageStatColumns.LAST_TIME_USED +
+ ") VALUES (?,?,?,?)",
+ mSelectionArgs4);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
* Returns a sort order String for promoting data rows (email addresses, phone numbers, etc.)
* associated with a primary account. The primary account should be supplied from applications
* with {@link ContactsContract#PRIMARY_ACCOUNT_NAME} and
@@ -9052,6 +9420,9 @@
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mContactAggregator != null) {
+ pw.print("Contact aggregator type: " + mContactAggregator.getClass() + "\n");
+ }
pw.print("FastScrollingIndex stats:\n");
pw.printf("request=%d miss=%d (%d%%) avg time=%dms\n",
mFastScrollingIndexCacheRequestCount,
@@ -9081,6 +9452,22 @@
throw new IllegalArgumentException("Invalid usage type " + type);
}
+ private static final int getAggregationType(String type, Integer defaultType) {
+ if ("TOGETHER".equalsIgnoreCase(type)) {
+ return AggregationExceptions.TYPE_KEEP_TOGETHER; // 1
+ }
+ if ("SEPARATE".equalsIgnoreCase(type)) {
+ return AggregationExceptions.TYPE_KEEP_SEPARATE; // 2
+ }
+ if ("UNSET".equalsIgnoreCase(type)) {
+ return AggregationExceptions.TYPE_AUTOMATIC; // 0
+ }
+ if (defaultType != null) {
+ return defaultType;
+ }
+ throw new IllegalArgumentException("Invalid aggregation type " + type);
+ }
+
/** Use only for debug logging */
@Override
public String toString() {
diff --git a/src/com/android/providers/contacts/ContactsUpgradeReceiver.java b/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
index ba8acb8..6c35717 100644
--- a/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
+++ b/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
@@ -75,11 +75,6 @@
ContactsDatabaseHelper helper = ContactsDatabaseHelper.getInstance(context);
ProfileDatabaseHelper profileHelper = ProfileDatabaseHelper.getInstance(context);
Log.i(TAG, "Creating or opening contacts database");
- try {
- ActivityManagerNative.getDefault().showBootMessage(
- context.getText(R.string.upgrade_msg), true);
- } catch (RemoteException e) {
- }
helper.getWritableDatabase();
profileHelper.getWritableDatabase();
diff --git a/src/com/android/providers/contacts/DataRowHandler.java b/src/com/android/providers/contacts/DataRowHandler.java
index 8e3c20f..dbe8cbc 100644
--- a/src/com/android/providers/contacts/DataRowHandler.java
+++ b/src/com/android/providers/contacts/DataRowHandler.java
@@ -26,12 +26,11 @@
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Data;
import android.text.TextUtils;
-
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Handles inserts and update for a specific Data type.
@@ -74,14 +73,14 @@
protected final Context mContext;
protected final ContactsDatabaseHelper mDbHelper;
- protected final ContactAggregator mContactAggregator;
+ protected final AbstractContactAggregator mContactAggregator;
protected String[] mSelectionArgs1 = new String[1];
protected final String mMimetype;
protected long mMimetypeId;
@SuppressWarnings("all")
public DataRowHandler(Context context, ContactsDatabaseHelper dbHelper,
- ContactAggregator aggregator, String mimetype) {
+ AbstractContactAggregator aggregator, String mimetype) {
mContext = context;
mDbHelper = dbHelper;
mContactAggregator = aggregator;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
index 0bb17c2..063fcdb 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
@@ -21,8 +21,7 @@
import android.database.sqlite.SQLiteDatabase;
import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
import android.text.TextUtils;
-
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Superclass for data row handlers that deal with types (e.g. Home, Work, Other) and
@@ -34,7 +33,8 @@
private final String mLabelColumn;
public DataRowHandlerForCommonDataKind(Context context, ContactsDatabaseHelper dbHelper,
- ContactAggregator aggregator, String mimetype, String typeColumn, String labelColumn) {
+ AbstractContactAggregator aggregator, String mimetype, String typeColumn,
+ String labelColumn) {
super(context, dbHelper, aggregator, mimetype);
mTypeColumn = typeColumn;
mLabelColumn = labelColumn;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java b/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java
index 502b835..1de0823 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java
@@ -16,13 +16,12 @@
package com.android.providers.contacts;
import android.content.Context;
-
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
public class DataRowHandlerForCustomMimetype extends DataRowHandler {
- public DataRowHandlerForCustomMimetype(Context context,
- ContactsDatabaseHelper dbHelper, ContactAggregator aggregator, String mimetype) {
+ public DataRowHandlerForCustomMimetype(Context context, ContactsDatabaseHelper dbHelper,
+ AbstractContactAggregator aggregator, String mimetype) {
super(context, dbHelper, aggregator, mimetype);
}
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForEmail.java b/src/com/android/providers/contacts/DataRowHandlerForEmail.java
index 38cb2e1..539c959 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForEmail.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForEmail.java
@@ -20,9 +20,8 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.provider.ContactsContract.CommonDataKinds.Email;
-
import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Handler for email address data rows.
@@ -30,7 +29,7 @@
public class DataRowHandlerForEmail extends DataRowHandlerForCommonDataKind {
public DataRowHandlerForEmail(
- Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) {
+ Context context, ContactsDatabaseHelper dbHelper, AbstractContactAggregator aggregator) {
super(context, dbHelper, aggregator, Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL);
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
index 0d2427a..e291986 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
@@ -23,7 +23,6 @@
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
-
import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
@@ -31,7 +30,7 @@
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.android.providers.contacts.ContactsProvider2.GroupIdCacheEntry;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
import java.util.ArrayList;
import java.util.HashMap;
@@ -66,7 +65,7 @@
private final HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache;
public DataRowHandlerForGroupMembership(Context context, ContactsDatabaseHelper dbHelper,
- ContactAggregator aggregator,
+ AbstractContactAggregator aggregator,
HashMap<String, ArrayList<GroupIdCacheEntry>> groupIdCache) {
super(context, dbHelper, aggregator, GroupMembership.CONTENT_ITEM_TYPE);
mGroupIdCache = groupIdCache;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForIdentity.java b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java
index 48ce5e4..32e9757 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForIdentity.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForIdentity.java
@@ -20,15 +20,14 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.provider.ContactsContract.CommonDataKinds.Identity;
-
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Handler for Identity data rows.
*/
public class DataRowHandlerForIdentity extends DataRowHandler {
- public DataRowHandlerForIdentity(
- Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) {
+ public DataRowHandlerForIdentity(Context context, ContactsDatabaseHelper dbHelper,
+ AbstractContactAggregator aggregator) {
super(context, dbHelper, aggregator, Identity.CONTENT_ITEM_TYPE);
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForIm.java b/src/com/android/providers/contacts/DataRowHandlerForIm.java
index faf10ad..9a2a56e 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForIm.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForIm.java
@@ -18,17 +18,16 @@
import android.content.ContentValues;
import android.content.Context;
import android.provider.ContactsContract.CommonDataKinds.Im;
-
import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Handler for IM address data rows.
*/
public class DataRowHandlerForIm extends DataRowHandlerForCommonDataKind {
- public DataRowHandlerForIm(
- Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) {
+ public DataRowHandlerForIm(Context context, ContactsDatabaseHelper dbHelper,
+ AbstractContactAggregator aggregator) {
super(context, dbHelper, aggregator, Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL);
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForNickname.java b/src/com/android/providers/contacts/DataRowHandlerForNickname.java
index 9c32df9..03b96a3 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForNickname.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForNickname.java
@@ -21,17 +21,16 @@
import android.database.sqlite.SQLiteDatabase;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.text.TextUtils;
-
import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Handler for nickname data rows.
*/
public class DataRowHandlerForNickname extends DataRowHandlerForCommonDataKind {
- public DataRowHandlerForNickname(
- Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) {
+ public DataRowHandlerForNickname(Context context, ContactsDatabaseHelper dbHelper,
+ AbstractContactAggregator aggregator) {
super(context, dbHelper, aggregator, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE,
Nickname.LABEL);
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForNote.java b/src/com/android/providers/contacts/DataRowHandlerForNote.java
index ea73637..fc602f1 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForNote.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForNote.java
@@ -18,17 +18,16 @@
import android.content.ContentValues;
import android.content.Context;
import android.provider.ContactsContract.CommonDataKinds.Note;
-
import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Handler for note data rows.
*/
public class DataRowHandlerForNote extends DataRowHandler {
- public DataRowHandlerForNote(
- Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) {
+ public DataRowHandlerForNote(Context context, ContactsDatabaseHelper dbHelper,
+ AbstractContactAggregator aggregator) {
super(context, dbHelper, aggregator, Note.CONTENT_ITEM_TYPE);
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
index 629d949..66a3b1b 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
@@ -22,10 +22,9 @@
import android.database.sqlite.SQLiteDatabase;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.Data;
-
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Handler for organization data rows.
@@ -33,7 +32,7 @@
public class DataRowHandlerForOrganization extends DataRowHandlerForCommonDataKind {
public DataRowHandlerForOrganization(Context context, ContactsDatabaseHelper dbHelper,
- ContactAggregator aggregator) {
+ AbstractContactAggregator aggregator) {
super(context, dbHelper, aggregator,
Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL);
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
index 16faf2a..052252e 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
@@ -26,7 +26,7 @@
import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Handler for phone number data rows.
@@ -34,7 +34,7 @@
public class DataRowHandlerForPhoneNumber extends DataRowHandlerForCommonDataKind {
public DataRowHandlerForPhoneNumber(Context context,
- ContactsDatabaseHelper dbHelper, ContactAggregator aggregator) {
+ ContactsDatabaseHelper dbHelper, AbstractContactAggregator aggregator) {
super(context, dbHelper, aggregator, Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL);
}
diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
index bfaa501..532a852 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
@@ -22,7 +22,7 @@
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.util.Log;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
import java.io.IOException;
@@ -46,7 +46,7 @@
/* package */ static final String SKIP_PROCESSING_KEY = "skip_processing";
public DataRowHandlerForPhoto(
- Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator,
+ Context context, ContactsDatabaseHelper dbHelper, AbstractContactAggregator aggregator,
PhotoStore photoStore, int maxDisplayPhotoDim, int maxThumbnailPhotoDim) {
super(context, dbHelper, aggregator, Photo.CONTENT_ITEM_TYPE);
mPhotoStore = photoStore;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
index ba6777d..044e972 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
@@ -25,7 +25,7 @@
import android.text.TextUtils;
import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Handler for email address data rows.
@@ -36,7 +36,7 @@
private final StringBuilder mSb = new StringBuilder();
public DataRowHandlerForStructuredName(Context context, ContactsDatabaseHelper dbHelper,
- ContactAggregator aggregator, NameSplitter splitter,
+ AbstractContactAggregator aggregator, NameSplitter splitter,
NameLookupBuilder nameLookupBuilder) {
super(context, dbHelper, aggregator, StructuredName.CONTENT_ITEM_TYPE);
mSplitter = splitter;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
index 26483ed..7fc97b7 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
@@ -23,7 +23,7 @@
import android.text.TextUtils;
import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
-import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.AbstractContactAggregator;
/**
* Handler for postal address data rows.
@@ -46,7 +46,7 @@
private final PostalSplitter mSplitter;
public DataRowHandlerForStructuredPostal(Context context, ContactsDatabaseHelper dbHelper,
- ContactAggregator aggregator, PostalSplitter splitter) {
+ AbstractContactAggregator aggregator, PostalSplitter splitter) {
super(context, dbHelper, aggregator, StructuredPostal.CONTENT_ITEM_TYPE);
mSplitter = splitter;
}
diff --git a/src/com/android/providers/contacts/DbModifierWithNotification.java b/src/com/android/providers/contacts/DbModifierWithNotification.java
index 40d0927..3576849 100644
--- a/src/com/android/providers/contacts/DbModifierWithNotification.java
+++ b/src/com/android/providers/contacts/DbModifierWithNotification.java
@@ -21,6 +21,7 @@
import static android.Manifest.permission.READ_VOICEMAIL;
import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
@@ -32,6 +33,7 @@
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Binder;
+import android.os.Bundle;
import android.provider.CallLog.Calls;
import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Status;
@@ -42,6 +44,7 @@
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.android.providers.contacts.util.DbQueryUtils;
import com.google.android.collect.Lists;
+import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Collection;
@@ -56,7 +59,7 @@
* of then got affected by the change.
*/
public class DbModifierWithNotification implements DatabaseModifier {
- private static final String TAG = "DbModifierWithVmNotification";
+ private static final String TAG = "DbModifierWithNotify";
private static final String[] PROJECTION = new String[] {
VoicemailContract.SOURCE_PACKAGE_FIELD
@@ -73,6 +76,7 @@
private final boolean mIsCallsTable;
private final VoicemailPermissions mVoicemailPermissions;
+
public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) {
this(tableName, db, null, context);
}
@@ -124,6 +128,14 @@
private void notifyCallLogChange() {
mContext.getContentResolver().notifyChange(Calls.CONTENT_URI, null, false);
+
+ Intent intent = new Intent("android.intent.action.CALL_LOG_CHANGE");
+ intent.setComponent(new ComponentName("com.android.providers.calllogbackup",
+ "com.android.providers.calllogbackup.CallLogChangeReceiver"));
+
+ if (!mContext.getPackageManager().queryBroadcastReceivers(intent, 0).isEmpty()) {
+ mContext.sendBroadcast(intent);
+ }
}
private void notifyVoicemailChangeOnInsert(Uri notificationUri, Set<String> packagesModified) {
@@ -140,8 +152,20 @@
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
packagesModified.addAll(getModifiedPackages(values));
+
+ boolean isVoicemail = packagesModified.size() != 0;
+
+ if (mIsCallsTable && isVoicemail) {
+ // If a calling package is modifying its own entries, it means that the change came from
+ // the server and thus is synced or "clean". Otherwise, it means that a local change
+ // is being made to the database, so the entries should be marked as "dirty" so that
+ // the corresponding sync adapter knows they need to be synced.
+ final int isDirty = isSelfModifying(packagesModified) ? 0 : 1;
+ values.put(VoicemailContract.Voicemails.DIRTY, isDirty);
+ }
+
int count = mDb.update(table, values, whereClause, whereArgs);
- if (count > 0 && packagesModified.size() != 0) {
+ if (count > 0 && isVoicemail) {
notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
}
if (count > 0 && mIsCallsTable) {
@@ -153,8 +177,25 @@
@Override
public int delete(String table, String whereClause, String[] whereArgs) {
Set<String> packagesModified = getModifiedPackages(whereClause, whereArgs);
- int count = mDb.delete(table, whereClause, whereArgs);
- if (count > 0 && packagesModified.size() != 0) {
+ boolean isVoicemail = packagesModified.size() != 0;
+
+ // If a deletion is made by a package that is not the package that inserted the voicemail,
+ // this means that the user deleted the voicemail. However, we do not want to delete it from
+ // the database until after the server has been notified of the deletion. To ensure this,
+ // mark the entry as "deleted"--deleted entries should be hidden from the user.
+ // Once the changes are synced to the server, delete will be called again, this time
+ // removing the rows from the table.
+ final int count;
+ if (mIsCallsTable && isVoicemail && !isSelfModifying(packagesModified)) {
+ ContentValues values = new ContentValues();
+ values.put(VoicemailContract.Voicemails.DIRTY, 1);
+ values.put(VoicemailContract.Voicemails.DELETED, 1);
+ count = mDb.update(table, values, whereClause, whereArgs);
+ } else {
+ count = mDb.delete(table, whereClause, whereArgs);
+ }
+
+ if (count > 0 && isVoicemail) {
notifyVoicemailChange(mBaseUri, packagesModified, Intent.ACTION_PROVIDER_CHANGED);
}
if (count > 0 && mIsCallsTable) {
@@ -195,6 +236,11 @@
return impactedPackages;
}
+ private boolean isSelfModifying(Set<String> packagesModified) {
+ return packagesModified.size() == 1 && getCallingPackages().contains(
+ Iterables.getOnlyElement(packagesModified));
+ }
+
private void notifyVoicemailChange(Uri notificationUri, Set<String> modifiedPackages,
String... intentActions) {
// Notify the observers.
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index 6ccbd38..20307d4 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -244,8 +244,9 @@
String.valueOf(SNIPPET_START_MATCH), String.valueOf(SNIPPET_END_MATCH),
SNIPPET_ELLIPSIS, SNIPPET_MAX_TOKENS, false);
}
+ sb.append(" WHERE " + Contacts.LOOKUP_KEY + " IS NOT NULL");
if (selection != null) {
- sb.append(" WHERE ").append(selection);
+ sb.append(" AND ").append(selection);
}
if (limit != null) {
sb.append(" LIMIT " + limit);
diff --git a/src/com/android/providers/contacts/MetadataEntryParser.java b/src/com/android/providers/contacts/MetadataEntryParser.java
new file mode 100644
index 0000000..b90f938
--- /dev/null
+++ b/src/com/android/providers/contacts/MetadataEntryParser.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts;
+
+import android.text.TextUtils;
+import com.android.providers.contacts.util.NeededForTesting;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+@NeededForTesting
+public class MetadataEntryParser {
+
+ private final static String UNIQUE_CONTACT_ID = "unique_contact_id";
+ private final static String ACCOUNT_TYPE = "account_type";
+ private final static String CUSTOM_ACCOUNT_TYPE = "custom_account_type";
+ private final static String ENUM_VALUE_FOR_GOOGLE_ACCOUNT = "GOOGLE_ACCOUNT";
+ private final static String GOOGLE_ACCOUNT_TYPE = "com.google";
+ private final static String ENUM_VALUE_FOR_CUSTOM_ACCOUNT = "CUSTOM_ACCOUNT";
+ private final static String ACCOUNT_NAME = "account_name";
+ private final static String DATA_SET = "data_set";
+ private final static String ENUM_FOR_PLUS_DATA_SET = "GOOGLE_PLUS";
+ private final static String ENUM_FOR_CUSTOM_DATA_SET = "CUSTOM";
+ private final static String PLUS_DATA_SET_TYPE = "plus";
+ private final static String CUSTOM_DATA_SET = "custom_data_set";
+ private final static String CONTACT_ID = "contact_id";
+ private final static String CONTACT_PREFS = "contact_prefs";
+ private final static String SEND_TO_VOICEMAIL = "send_to_voicemail";
+ private final static String STARRED = "starred";
+ private final static String PINNED = "pinned";
+ private final static String AGGREGATION_DATA = "aggregation_data";
+ private final static String CONTACT_IDS = "contact_ids";
+ private final static String TYPE = "type";
+ private final static String FIELD_DATA = "field_data";
+ private final static String FIELD_DATA_ID = "field_data_id";
+ private final static String FIELD_DATA_PREFS = "field_data_prefs";
+ private final static String IS_PRIMARY = "is_primary";
+ private final static String IS_SUPER_PRIMARY = "is_super_primary";
+ private final static String USAGE_STATS = "usage_stats";
+ private final static String USAGE_TYPE = "usage_type";
+ private final static String LAST_TIME_USED = "last_time_used";
+ private final static String USAGE_COUNT = "usage_count";
+
+ @NeededForTesting
+ public static class UsageStats {
+ final String mUsageType;
+ final long mLastTimeUsed;
+ final int mTimesUsed;
+
+ @NeededForTesting
+ public UsageStats(String usageType, long lastTimeUsed, int timesUsed) {
+ this.mUsageType = usageType;
+ this.mLastTimeUsed = lastTimeUsed;
+ this.mTimesUsed = timesUsed;
+ }
+ }
+
+ @NeededForTesting
+ public static class FieldData {
+ final String mDataHashId;
+ final boolean mIsPrimary;
+ final boolean mIsSuperPrimary;
+ final ArrayList<UsageStats> mUsageStatsList;
+
+ @NeededForTesting
+ public FieldData(String dataHashId, boolean isPrimary, boolean isSuperPrimary,
+ ArrayList<UsageStats> usageStatsList) {
+ this.mDataHashId = dataHashId;
+ this.mIsPrimary = isPrimary;
+ this.mIsSuperPrimary = isSuperPrimary;
+ this.mUsageStatsList = usageStatsList;
+ }
+ }
+
+ @NeededForTesting
+ public static class RawContactInfo {
+ final String mBackupId;
+ final String mAccountType;
+ final String mAccountName;
+ final String mDataSet;
+
+ @NeededForTesting
+ public RawContactInfo(String backupId, String accountType, String accountName,
+ String dataSet) {
+ this.mBackupId = backupId;
+ this.mAccountType = accountType;
+ this.mAccountName = accountName;
+ mDataSet = dataSet;
+ }
+ }
+
+ @NeededForTesting
+ public static class AggregationData {
+ final RawContactInfo mRawContactInfo1;
+ final RawContactInfo mRawContactInfo2;
+ final String mType;
+
+ @NeededForTesting
+ public AggregationData(RawContactInfo rawContactInfo1, RawContactInfo rawContactInfo2,
+ String type) {
+ this.mRawContactInfo1 = rawContactInfo1;
+ this.mRawContactInfo2 = rawContactInfo2;
+ this.mType = type;
+ }
+ }
+
+ @NeededForTesting
+ public static class MetadataEntry {
+ final RawContactInfo mRawContactInfo;
+ final int mSendToVoicemail;
+ final int mStarred;
+ final int mPinned;
+ final ArrayList<FieldData> mFieldDatas;
+ final ArrayList<AggregationData> mAggregationDatas;
+
+ @NeededForTesting
+ public MetadataEntry(RawContactInfo rawContactInfo,
+ int sendToVoicemail, int starred, int pinned,
+ ArrayList<FieldData> fieldDatas,
+ ArrayList<AggregationData> aggregationDatas) {
+ this.mRawContactInfo = rawContactInfo;
+ this.mSendToVoicemail = sendToVoicemail;
+ this.mStarred = starred;
+ this.mPinned = pinned;
+ this.mFieldDatas = fieldDatas;
+ this.mAggregationDatas = aggregationDatas;
+ }
+ }
+
+ @NeededForTesting
+ static MetadataEntry parseDataToMetaDataEntry(String inputData) {
+ if (TextUtils.isEmpty(inputData)) {
+ throw new IllegalArgumentException("Input cannot be empty.");
+ }
+
+ try {
+ final JSONObject root = new JSONObject(inputData);
+ // Parse to get rawContactId and account info.
+ final JSONObject uniqueContactJSON = root.getJSONObject(UNIQUE_CONTACT_ID);
+ final RawContactInfo rawContactInfo = parseUniqueContact(uniqueContactJSON);
+
+ // Parse contactPrefs to get sendToVoicemail, starred, pinned.
+ final JSONObject contactPrefs = root.getJSONObject(CONTACT_PREFS);
+ final boolean sendToVoicemail = contactPrefs.getBoolean(SEND_TO_VOICEMAIL);
+ final boolean starred = contactPrefs.getBoolean(STARRED);
+ final int pinned = contactPrefs.getInt(PINNED);
+
+ // Parse aggregationDatas
+ final ArrayList<AggregationData> aggregationsList = new ArrayList<AggregationData>();
+ if (root.has(AGGREGATION_DATA)) {
+ final JSONArray aggregationDatas = root.getJSONArray(AGGREGATION_DATA);
+
+ for (int i = 0; i < aggregationDatas.length(); i++) {
+ final JSONObject aggregationData = aggregationDatas.getJSONObject(i);
+ final JSONArray contacts = aggregationData.getJSONArray(CONTACT_IDS);
+
+ if (contacts.length() != 2) {
+ throw new IllegalArgumentException(
+ "There should be two contacts for each aggregation.");
+ }
+ final JSONObject rawContact1 = contacts.getJSONObject(0);
+ final RawContactInfo aggregationContact1 = parseUniqueContact(rawContact1);
+ final JSONObject rawContact2 = contacts.getJSONObject(1);
+ final RawContactInfo aggregationContact2 = parseUniqueContact(rawContact2);
+ final String type = aggregationData.getString(TYPE);
+ if (TextUtils.isEmpty(type)) {
+ throw new IllegalArgumentException("Aggregation type cannot be empty.");
+ }
+
+ final AggregationData aggregation = new AggregationData(
+ aggregationContact1, aggregationContact2, type);
+ aggregationsList.add(aggregation);
+ }
+ }
+
+ // Parse fieldDatas
+ final ArrayList<FieldData> fieldDatasList = new ArrayList<FieldData>();
+ if (root.has(FIELD_DATA)) {
+ final JSONArray fieldDatas = root.getJSONArray(FIELD_DATA);
+
+ for (int i = 0; i < fieldDatas.length(); i++) {
+ final JSONObject fieldData = fieldDatas.getJSONObject(i);
+ final String dataHashId = fieldData.getString(FIELD_DATA_ID);
+ if (TextUtils.isEmpty(dataHashId)) {
+ throw new IllegalArgumentException("Field data hash id cannot be empty.");
+ }
+ final JSONObject fieldDataPrefs = fieldData.getJSONObject(FIELD_DATA_PREFS);
+ final boolean isPrimary = fieldDataPrefs.getBoolean(IS_PRIMARY);
+ final boolean isSuperPrimary = fieldDataPrefs.getBoolean(IS_SUPER_PRIMARY);
+
+ final ArrayList<UsageStats> usageStatsList = new ArrayList<UsageStats>();
+ if (fieldData.has(USAGE_STATS)) {
+ final JSONArray usageStats = fieldData.getJSONArray(USAGE_STATS);
+ for (int j = 0; j < usageStats.length(); j++) {
+ final JSONObject usageStat = usageStats.getJSONObject(j);
+ final String usageType = usageStat.getString(USAGE_TYPE);
+ if (TextUtils.isEmpty(usageType)) {
+ throw new IllegalArgumentException("Usage type cannot be empty.");
+ }
+ final long lastTimeUsed = usageStat.getLong(LAST_TIME_USED);
+ final int usageCount = usageStat.getInt(USAGE_COUNT);
+
+ final UsageStats usageStatsParsed = new UsageStats(
+ usageType, lastTimeUsed, usageCount);
+ usageStatsList.add(usageStatsParsed);
+ }
+ }
+
+ final FieldData fieldDataParse = new FieldData(dataHashId, isPrimary,
+ isSuperPrimary, usageStatsList);
+ fieldDatasList.add(fieldDataParse);
+ }
+ }
+ final MetadataEntry metaDataEntry = new MetadataEntry(rawContactInfo,
+ sendToVoicemail ? 1 : 0, starred ? 1 : 0, pinned,
+ fieldDatasList, aggregationsList);
+ return metaDataEntry;
+ } catch (JSONException e) {
+ throw new IllegalArgumentException("JSON Exception.", e);
+ }
+ }
+
+ private static RawContactInfo parseUniqueContact(JSONObject uniqueContactJSON) {
+ try {
+ final String backupId = uniqueContactJSON.getString(CONTACT_ID);
+ final String accountName = uniqueContactJSON.getString(ACCOUNT_NAME);
+ String accountType = uniqueContactJSON.getString(ACCOUNT_TYPE);
+ if (ENUM_VALUE_FOR_GOOGLE_ACCOUNT.equals(accountType)) {
+ accountType = GOOGLE_ACCOUNT_TYPE;
+ } else if (ENUM_VALUE_FOR_CUSTOM_ACCOUNT.equals(accountType)) {
+ accountType = uniqueContactJSON.getString(CUSTOM_ACCOUNT_TYPE);
+ } else {
+ throw new IllegalArgumentException("Unknown account type.");
+ }
+
+ String dataSet = null;
+ switch (uniqueContactJSON.getString(DATA_SET)) {
+ case ENUM_FOR_PLUS_DATA_SET:
+ dataSet = PLUS_DATA_SET_TYPE;
+ break;
+ case ENUM_FOR_CUSTOM_DATA_SET:
+ dataSet = uniqueContactJSON.getString(CUSTOM_DATA_SET);
+ break;
+ }
+ if (TextUtils.isEmpty(backupId) || TextUtils.isEmpty(accountType)
+ || TextUtils.isEmpty(accountName)) {
+ throw new IllegalArgumentException(
+ "Contact backup id, account type, account name cannot be empty.");
+ }
+ final RawContactInfo rawContactInfo = new RawContactInfo(
+ backupId, accountType, accountName, dataSet);
+ return rawContactInfo;
+ } catch (JSONException e) {
+ throw new IllegalArgumentException("JSON Exception.", e);
+ }
+ }
+}
diff --git a/src/com/android/providers/contacts/PhoneAccountRegistrationReceiver.java b/src/com/android/providers/contacts/PhoneAccountRegistrationReceiver.java
new file mode 100644
index 0000000..8a68889
--- /dev/null
+++ b/src/com/android/providers/contacts/PhoneAccountRegistrationReceiver.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.provider.CallLog;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+/**
+ * This will be launched when a new phone account is registered in telecom. It is used by the call
+ * log to un-hide any entries which were previously hidden after a backup-restore until it's
+ * associated phone-account is registered with telecom.
+ *
+ * IOW, after a restore, we hide call log entries until the user inserts the corresponding SIM,
+ * registers the corresponding SIP account, or registers a corresponding alternative phone-account.
+ */
+public class PhoneAccountRegistrationReceiver extends BroadcastReceiver {
+ static final String TAG = "PhoneAccountReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // We are now running with the system up, but no apps started,
+ // so can do whatever cleanup after an upgrade that we want.
+ if (TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED.equals(intent.getAction())) {
+
+ PhoneAccountHandle handle = (PhoneAccountHandle) intent.getParcelableExtra(
+ TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+
+ IContentProvider iprovider =
+ context.getContentResolver().acquireProvider(CallLog.AUTHORITY);
+ ContentProvider provider = ContentProvider.coerceToLocalContentProvider(iprovider);
+ if (provider instanceof CallLogProvider) {
+ ((CallLogProvider) provider).adjustForNewPhoneAccount(handle);
+ }
+ }
+ }
+}
diff --git a/src/com/android/providers/contacts/ProfileProvider.java b/src/com/android/providers/contacts/ProfileProvider.java
index d01195d..dfb8748 100644
--- a/src/com/android/providers/contacts/ProfileProvider.java
+++ b/src/com/android/providers/contacts/ProfileProvider.java
@@ -15,6 +15,8 @@
*/
package com.android.providers.contacts;
+import com.android.providers.contacts.util.ContactsPermissions;
+
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -33,8 +35,7 @@
* database from the rest of contacts.
*/
public class ProfileProvider extends AbstractContactsProvider {
- private static final String READ_PERMISSION = "android.permission.READ_PROFILE";
- private static final String WRITE_PERMISSION = "android.permission.WRITE_PROFILE";
+ private static final String READ_CONTACTS_PERMISSION = "android.permission.READ_CONTACTS";
// The Contacts provider handles most of the logic - this provider is only invoked when the
// URI belongs to a profile action, setting up the proper database.
@@ -44,24 +45,6 @@
mDelegate = delegate;
}
- /**
- * Performs a permission check on the read profile permission. Checks the delegate contacts
- * provider to see whether this is an authorized one-time-use URI.
- * @param uri The URI being accessed.
- */
- public void enforceReadPermission(Uri uri) {
- if (!mDelegate.isValidPreAuthorizedUri(uri)) {
- mDelegate.getContext().enforceCallingOrSelfPermission(READ_PERMISSION, null);
- }
- }
-
- /**
- * Performs a permission check on the write profile permission.
- */
- public void enforceWritePermission() {
- mDelegate.getContext().enforceCallingOrSelfPermission(WRITE_PERMISSION, null);
- }
-
@Override
protected ProfileDatabaseHelper getDatabaseHelper(Context context) {
return ProfileDatabaseHelper.getInstance(context);
@@ -81,14 +64,12 @@
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder, CancellationSignal cancellationSignal) {
- enforceReadPermission(uri);
return mDelegate.queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1,
cancellationSignal);
}
@Override
protected Uri insertInTransaction(Uri uri, ContentValues values) {
- enforceWritePermission();
useProfileDbForTransaction();
return mDelegate.insertInTransaction(uri, values);
}
@@ -96,25 +77,18 @@
@Override
protected int updateInTransaction(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
- enforceWritePermission();
useProfileDbForTransaction();
return mDelegate.updateInTransaction(uri, values, selection, selectionArgs);
}
@Override
protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
- enforceWritePermission();
useProfileDbForTransaction();
return mDelegate.deleteInTransaction(uri, selection, selectionArgs);
}
@Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
- if (mode != null && mode.contains("w")) {
- enforceWritePermission();
- } else {
- enforceReadPermission(uri);
- }
return mDelegate.openAssetFileLocal(uri, mode);
}
@@ -171,6 +145,6 @@
private void sendProfileChangedBroadcast() {
final Intent intent = new Intent(Intents.ACTION_PROFILE_CHANGED);
- mDelegate.getContext().sendBroadcast(intent, READ_PERMISSION);
+ mDelegate.getContext().sendBroadcast(intent, READ_CONTACTS_PERMISSION);
}
}
diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java
index 4773197..e4f6a05 100644
--- a/src/com/android/providers/contacts/VoicemailContentProvider.java
+++ b/src/com/android/providers/contacts/VoicemailContentProvider.java
@@ -19,6 +19,7 @@
import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
+import android.app.AppOpsManager;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
@@ -34,6 +35,7 @@
import android.util.Log;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.util.ContactsPermissions;
import com.android.providers.contacts.util.SelectionBuilder;
import com.android.providers.contacts.util.TypedUriMatcherImpl;
import com.google.common.annotations.VisibleForTesting;
@@ -59,6 +61,11 @@
Log.d(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate start");
}
Context context = context();
+
+ // ADD_VOICEMAIL permission guards read and write. We do the same with app ops.
+ // The permission name doesn't reflect its function but we cannot rename it.
+ setAppOps(AppOpsManager.OP_ADD_VOICEMAIL, AppOpsManager.OP_ADD_VOICEMAIL);
+
mVoicemailPermissions = new VoicemailPermissions(context);
mVoicemailContentTable = new VoicemailContentTable(Tables.CALLS, context,
getDatabaseHelper(context), this, createCallLogInsertionHelper(context));
@@ -249,7 +256,7 @@
}
// You must have access to the provider given in values.
- if (!mVoicemailPermissions.callerHasWriteAccess()) {
+ if (!mVoicemailPermissions.callerHasWriteAccess(getCallingPackage())) {
checkPackagesMatch(getCallingPackage_(),
values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD),
uriData.getUri());
@@ -285,12 +292,12 @@
private UriData checkPermissionsAndCreateUriDataForRead(Uri uri) {
// If the caller has been explicitly granted read permission to this URI then no need to
// check further.
- if (context().checkCallingUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
- == PackageManager.PERMISSION_GRANTED) {
+ if (ContactsPermissions.hasCallerUriPermission(
+ getContext(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)) {
return UriData.createUriData(uri);
}
- if (mVoicemailPermissions.callerHasReadAccess()) {
+ if (mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) {
return UriData.createUriData(uri);
}
@@ -347,7 +354,7 @@
* @throws SecurityException if the check fails.
*/
private void checkPackagePermission(UriData uriData) {
- if (!mVoicemailPermissions.callerHasWriteAccess()) {
+ if (!mVoicemailPermissions.callerHasWriteAccess(getCallingPackage())) {
if (!uriData.hasSourcePackage()) {
// You cannot have a match if this is not a provider URI.
throw new SecurityException(String.format(
@@ -408,14 +415,16 @@
}
/**
- * Whether or not the calling package has the appropriate read/write permission
+ * Whether or not the calling package has the appropriate read/write permission. The user
+ * selected default and/or system dialers are always allowed to read and write to the
+ * VoicemailContentProvider.
*
* @param read Whether or not this operation is a read
*
* @return True if the package has the permission required to perform the read/write operation
*/
private boolean hasReadWritePermission(boolean read) {
- return read ? mVoicemailPermissions.callerHasReadAccess() :
- mVoicemailPermissions.callerHasWriteAccess();
+ return read ? mVoicemailPermissions.callerHasReadAccess(getCallingPackage()) :
+ mVoicemailPermissions.callerHasWriteAccess(getCallingPackage());
}
}
diff --git a/src/com/android/providers/contacts/VoicemailContentTable.java b/src/com/android/providers/contacts/VoicemailContentTable.java
index 16b3df7..7f943e3 100644
--- a/src/com/android/providers/contacts/VoicemailContentTable.java
+++ b/src/com/android/providers/contacts/VoicemailContentTable.java
@@ -68,7 +68,11 @@
.add(Voicemails.SOURCE_DATA)
.add(Voicemails.SOURCE_PACKAGE)
.add(Voicemails.HAS_CONTENT)
+ .add(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME)
+ .add(Voicemails.PHONE_ACCOUNT_ID)
.add(Voicemails.MIME_TYPE)
+ .add(Voicemails.DIRTY)
+ .add(Voicemails.DELETED)
.add(OpenableColumns.DISPLAY_NAME)
.add(OpenableColumns.SIZE)
.build();
@@ -99,6 +103,10 @@
.add(Voicemails.HAS_CONTENT)
.add(Voicemails.MIME_TYPE)
.add(Voicemails._DATA)
+ .add(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME)
+ .add(Voicemails.PHONE_ACCOUNT_ID)
+ .add(Voicemails.DIRTY)
+ .add(Voicemails.DELETED)
.add(OpenableColumns.DISPLAY_NAME, createDisplayName(context))
.add(OpenableColumns.SIZE, "NULL")
.build();
diff --git a/src/com/android/providers/contacts/VoicemailPermissions.java b/src/com/android/providers/contacts/VoicemailPermissions.java
index 0aac18a..50f2447 100644
--- a/src/com/android/providers/contacts/VoicemailPermissions.java
+++ b/src/com/android/providers/contacts/VoicemailPermissions.java
@@ -16,8 +16,10 @@
package com.android.providers.contacts;
+import com.android.providers.contacts.util.ContactsPermissions;
+
import android.content.Context;
-import android.content.pm.PackageManager;
+import android.telecom.DefaultDialerManager;
/**
* Provides method related to check various voicemail permissions under the
@@ -37,13 +39,19 @@
}
/** Determine if the calling process has full read access to all voicemails. */
- public boolean callerHasReadAccess() {
+ public boolean callerHasReadAccess(String callingPackage) {
+ if (DefaultDialerManager.isDefaultOrSystemDialer(mContext, callingPackage)) {
+ return true;
+ }
return callerHasPermission(android.Manifest.permission.READ_VOICEMAIL);
}
/** Determine if the calling process has the permission required to update and remove all
* voicemails */
- public boolean callerHasWriteAccess() {
+ public boolean callerHasWriteAccess(String callingPackage) {
+ if (DefaultDialerManager.isDefaultOrSystemDialer(mContext, callingPackage)) {
+ return true;
+ }
return callerHasPermission(android.Manifest.permission.WRITE_VOICEMAIL);
}
@@ -64,17 +72,19 @@
*
* @throws SecurityException if the caller does not have the voicemail source permission.
*/
- public void checkCallerHasReadAccess() {
- if (!callerHasReadAccess()) {
- throw new SecurityException(String.format("The caller must have %s permission: ",
- android.Manifest.permission.READ_VOICEMAIL));
+ public void checkCallerHasReadAccess(String callingPackage) {
+ if (!callerHasReadAccess(callingPackage)) {
+ throw new SecurityException(String.format("The caller must be the default or system "
+ + "dialer, or have the system-only %s permission: ",
+ android.Manifest.permission.READ_VOICEMAIL));
}
}
- public void checkCallerHasWriteAccess() {
- if (!callerHasWriteAccess()) {
- throw new SecurityException(String.format("The caller must have %s permission: ",
- android.Manifest.permission.WRITE_VOICEMAIL));
+ public void checkCallerHasWriteAccess(String callingPackage) {
+ if (!callerHasWriteAccess(callingPackage)) {
+ throw new SecurityException(String.format("The caller must be the default or system "
+ + "dialer, or have the system-only %s permission: ",
+ android.Manifest.permission.WRITE_VOICEMAIL));
}
}
@@ -96,13 +106,11 @@
/** Determines if the given package has the given permission. */
private boolean packageHasPermission(String packageName, String permission) {
- return mContext.getPackageManager().checkPermission(permission, packageName)
- == PackageManager.PERMISSION_GRANTED;
+ return ContactsPermissions.hasPackagePermission(mContext, permission, packageName);
}
/** Determines if the calling process has the given permission. */
private boolean callerHasPermission(String permission) {
- return mContext.checkCallingOrSelfPermission(permission)
- == PackageManager.PERMISSION_GRANTED;
+ return ContactsPermissions.hasCallerOrSelfPermission(mContext, permission);
}
}
diff --git a/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java b/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java
new file mode 100644
index 0000000..24248b1
--- /dev/null
+++ b/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java
@@ -0,0 +1,2099 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.aggregation;
+
+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.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhotoFiles;
+import android.provider.ContactsContract.PinnedPositions;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.providers.contacts.ContactLookupKey;
+import com.android.providers.contacts.ContactsDatabaseHelper;
+import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
+import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.ContactsDatabaseHelper.Views;
+import com.android.providers.contacts.ContactsProvider2;
+import com.android.providers.contacts.NameLookupBuilder;
+import com.android.providers.contacts.NameNormalizer;
+import com.android.providers.contacts.NameSplitter;
+import com.android.providers.contacts.PhotoPriorityResolver;
+import com.android.providers.contacts.ReorderingCursorWrapper;
+import com.android.providers.contacts.TransactionContext;
+import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
+import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper;
+import com.android.providers.contacts.aggregation.util.ContactMatcher;
+import com.android.providers.contacts.aggregation.util.MatchScore;
+import com.android.providers.contacts.util.Clock;
+import com.google.android.collect.Maps;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Base class of contact aggregator and profile aggregator
+ */
+public abstract class AbstractContactAggregator {
+
+ protected static final String TAG = "ContactAggregator";
+
+ protected static final boolean DEBUG_LOGGING = Log.isLoggable(TAG, Log.DEBUG);
+ protected static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
+
+ protected static final String STRUCTURED_NAME_BASED_LOOKUP_SQL =
+ NameLookupColumns.NAME_TYPE + " IN ("
+ + NameLookupType.NAME_EXACT + ","
+ + NameLookupType.NAME_VARIANT + ","
+ + NameLookupType.NAME_COLLATION_KEY + ")";
+
+
+ /**
+ * SQL statement that sets the {@link ContactsColumns#LAST_STATUS_UPDATE_ID} column
+ * on the contact to point to the latest social status update.
+ */
+ protected static final String UPDATE_LAST_STATUS_UPDATE_ID_SQL =
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
+ "(SELECT " + DataColumns.CONCRETE_ID +
+ " FROM " + Tables.STATUS_UPDATES +
+ " JOIN " + Tables.DATA +
+ " ON (" + StatusUpdatesColumns.DATA_ID + "="
+ + DataColumns.CONCRETE_ID + ")" +
+ " JOIN " + Tables.RAW_CONTACTS +
+ " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "="
+ + RawContactsColumns.CONCRETE_ID + ")" +
+ " WHERE " + RawContacts.CONTACT_ID + "=?" +
+ " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
+ + StatusUpdates.STATUS +
+ " LIMIT 1)" +
+ " WHERE " + ContactsColumns.CONCRETE_ID + "=?";
+
+ // From system/core/logcat/event-log-tags
+ // aggregator [time, count] will be logged for each aggregator cycle.
+ // For the query (as opposed to the merge), count will be negative
+ static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747;
+
+ // If we encounter more than this many contacts with matching names, aggregate only this many
+ protected static final int PRIMARY_HIT_LIMIT = 15;
+ protected static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT);
+
+ // If we encounter more than this many contacts with matching phone number or email,
+ // don't attempt to aggregate - this is likely an error or a shared corporate data element.
+ protected static final int SECONDARY_HIT_LIMIT = 20;
+ protected static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
+
+ // If we encounter no less than this many raw contacts in the best matching contact during
+ // aggregation, don't attempt to aggregate - this is likely an error or a shared corporate
+ // data element.
+ @VisibleForTesting
+ static final int AGGREGATION_CONTACT_SIZE_LIMIT = 50;
+
+ // If we encounter more than this many contacts with matching name during aggregation
+ // suggestion lookup, ignore the remaining results.
+ protected static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100;
+
+ protected final ContactsProvider2 mContactsProvider;
+ protected final ContactsDatabaseHelper mDbHelper;
+ protected PhotoPriorityResolver mPhotoPriorityResolver;
+ protected final NameSplitter mNameSplitter;
+ protected final CommonNicknameCache mCommonNicknameCache;
+
+ protected boolean mEnabled = true;
+
+ /**
+ * Precompiled sql statement for setting an aggregated presence
+ */
+ protected SQLiteStatement mRawContactCountQuery;
+ protected SQLiteStatement mAggregatedPresenceDelete;
+ protected SQLiteStatement mAggregatedPresenceReplace;
+ protected SQLiteStatement mPresenceContactIdUpdate;
+ protected SQLiteStatement mMarkForAggregation;
+ protected SQLiteStatement mPhotoIdUpdate;
+ protected SQLiteStatement mDisplayNameUpdate;
+ protected SQLiteStatement mLookupKeyUpdate;
+ protected SQLiteStatement mStarredUpdate;
+ protected SQLiteStatement mPinnedUpdate;
+ protected SQLiteStatement mContactIdAndMarkAggregatedUpdate;
+ protected SQLiteStatement mContactIdUpdate;
+ protected SQLiteStatement mMarkAggregatedUpdate;
+ protected SQLiteStatement mContactUpdate;
+ protected SQLiteStatement mContactInsert;
+ protected SQLiteStatement mResetPinnedForRawContact;
+
+ protected HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap();
+
+ protected String[] mSelectionArgs1 = new String[1];
+ protected String[] mSelectionArgs2 = new String[2];
+
+ protected long mMimeTypeIdIdentity;
+ protected long mMimeTypeIdEmail;
+ protected long mMimeTypeIdPhoto;
+ protected long mMimeTypeIdPhone;
+ protected String mRawContactsQueryByRawContactId;
+ protected String mRawContactsQueryByContactId;
+ protected StringBuilder mSb = new StringBuilder();
+ protected MatchCandidateList mCandidates = new MatchCandidateList();
+ protected DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
+
+ /**
+ * Parameter for the suggestion lookup query.
+ */
+ public static final class AggregationSuggestionParameter {
+ public final String kind;
+ public final String value;
+
+ public AggregationSuggestionParameter(String kind, String value) {
+ this.kind = kind;
+ this.value = value;
+ }
+ }
+
+ /**
+ * 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.
+ */
+ protected 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.
+ */
+ protected static class MatchCandidateList {
+ protected final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>();
+ protected 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;
+ }
+
+ public boolean isEmpty() {
+ return mCount == 0;
+ }
+ }
+
+ /**
+ * A convenience class used in the algorithm that figures out which of available
+ * display names to use for an aggregate contact.
+ */
+ private static class DisplayNameCandidate {
+ long rawContactId;
+ String displayName;
+ int displayNameSource;
+ boolean isNameSuperPrimary;
+ boolean writableAccount;
+
+ public DisplayNameCandidate() {
+ clear();
+ }
+
+ public void clear() {
+ rawContactId = -1;
+ displayName = null;
+ displayNameSource = DisplayNameSources.UNDEFINED;
+ isNameSuperPrimary = false;
+ writableAccount = false;
+ }
+ }
+
+ /**
+ * Constructor.
+ */
+ public AbstractContactAggregator(ContactsProvider2 contactsProvider,
+ ContactsDatabaseHelper contactsDatabaseHelper,
+ PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter,
+ CommonNicknameCache commonNicknameCache) {
+ mContactsProvider = contactsProvider;
+ mDbHelper = contactsDatabaseHelper;
+ mPhotoPriorityResolver = photoPriorityResolver;
+ mNameSplitter = nameSplitter;
+ mCommonNicknameCache = commonNicknameCache;
+
+ SQLiteDatabase db = mDbHelper.getReadableDatabase();
+
+ // Since we have no way of determining which custom status was set last,
+ // we'll just pick one randomly. We are using MAX as an approximation of randomness
+ final String replaceAggregatePresenceSql =
+ "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
+ + AggregatedPresenceColumns.CONTACT_ID + ", "
+ + StatusUpdates.PRESENCE + ", "
+ + StatusUpdates.CHAT_CAPABILITY + ")"
+ + " SELECT " + PresenceColumns.CONTACT_ID + ","
+ + StatusUpdates.PRESENCE + ","
+ + StatusUpdates.CHAT_CAPABILITY
+ + " FROM " + Tables.PRESENCE
+ + " WHERE "
+ + " (" + StatusUpdates.PRESENCE
+ + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
+ + " = (SELECT "
+ + "MAX (" + StatusUpdates.PRESENCE
+ + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
+ + " FROM " + Tables.PRESENCE
+ + " WHERE " + PresenceColumns.CONTACT_ID
+ + "=?)"
+ + " AND " + PresenceColumns.CONTACT_ID
+ + "=?;";
+ mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql);
+
+ mRawContactCountQuery = db.compileStatement(
+ "SELECT COUNT(" + RawContacts._ID + ")" +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.CONTACT_ID + "=?"
+ + " AND " + RawContacts._ID + "<>?");
+
+ mAggregatedPresenceDelete = db.compileStatement(
+ "DELETE FROM " + Tables.AGGREGATED_PRESENCE +
+ " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?");
+
+ mMarkForAggregation = db.compileStatement(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" +
+ " WHERE " + RawContacts._ID + "=?"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0");
+
+ mPhotoIdUpdate = db.compileStatement(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.PHOTO_ID + "=?," + Contacts.PHOTO_FILE_ID + "=? " +
+ " WHERE " + Contacts._ID + "=?");
+
+ mDisplayNameUpdate = db.compileStatement(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
+ " WHERE " + Contacts._ID + "=?");
+
+ mLookupKeyUpdate = db.compileStatement(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.LOOKUP_KEY + "=? " +
+ " WHERE " + Contacts._ID + "=?");
+
+ mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
+ + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED
+ + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE "
+ + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND "
+ + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?");
+
+ mPinnedUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
+ + Contacts.PINNED + " = IFNULL((SELECT MIN(" + RawContacts.PINNED + ") FROM "
+ + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
+ + ContactsColumns.CONCRETE_ID + " AND " + RawContacts.PINNED + ">"
+ + PinnedPositions.UNPINNED + ")," + PinnedPositions.UNPINNED + ") "
+ + "WHERE " + Contacts._ID + "=?");
+
+ mContactIdAndMarkAggregatedUpdate = db.compileStatement(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContacts.CONTACT_ID + "=?, "
+ + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
+ " WHERE " + RawContacts._ID + "=?");
+
+ mContactIdUpdate = db.compileStatement(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContacts.CONTACT_ID + "=?" +
+ " WHERE " + RawContacts._ID + "=?");
+
+ mMarkAggregatedUpdate = db.compileStatement(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
+ " WHERE " + RawContacts._ID + "=?");
+
+ mPresenceContactIdUpdate = db.compileStatement(
+ "UPDATE " + Tables.PRESENCE +
+ " SET " + PresenceColumns.CONTACT_ID + "=?" +
+ " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
+
+ mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
+ mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
+
+ mResetPinnedForRawContact = db.compileStatement(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContacts.PINNED + "=" + PinnedPositions.UNPINNED +
+ " WHERE " + RawContacts._ID + "=?");
+
+ mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
+ mMimeTypeIdIdentity = mDbHelper.getMimeTypeId(Identity.CONTENT_ITEM_TYPE);
+ mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
+ mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+
+ // Query used to retrieve data from raw contacts to populate the corresponding aggregate
+ mRawContactsQueryByRawContactId = String.format(Locale.US,
+ RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
+ mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone);
+
+ mRawContactsQueryByContactId = String.format(Locale.US,
+ RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
+ mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone);
+ }
+
+ public final void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ public final boolean isEnabled() {
+ return mEnabled;
+ }
+
+ protected interface AggregationQuery {
+ String SQL =
+ "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
+ ", " + RawContactsColumns.ACCOUNT_ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts._ID + " IN(";
+
+ int _ID = 0;
+ int CONTACT_ID = 1;
+ int ACCOUNT_ID = 2;
+ }
+
+ /**
+ * Aggregate all raw contacts that were marked for aggregation in the current transaction.
+ * Call just before committing the transaction.
+ */
+ // Overridden by ProfileAggregator.
+ public void aggregateInTransaction(TransactionContext txContext, SQLiteDatabase db) {
+ final int markedCount = mRawContactsMarkedForAggregation.size();
+ if (markedCount == 0) {
+ return;
+ }
+
+ final long start = System.currentTimeMillis();
+ if (DEBUG_LOGGING) {
+ Log.d(TAG, "aggregateInTransaction for " + markedCount + " contacts");
+ }
+
+ EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -markedCount);
+
+ int index = 0;
+
+ // We don't use the cached string builder (namely mSb) here, as this string can be very
+ // long when upgrading (where we re-aggregate all visible contacts) and StringBuilder won't
+ // shrink the internal storage.
+ // Note: don't use selection args here. We just include all IDs directly in the selection,
+ // because there's a limit for the number of parameters in a query.
+ final StringBuilder sbQuery = new StringBuilder();
+ sbQuery.append(AggregationQuery.SQL);
+ for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) {
+ if (index > 0) {
+ sbQuery.append(',');
+ }
+ sbQuery.append(rawContactId);
+ index++;
+ }
+
+ sbQuery.append(')');
+
+ final long[] rawContactIds;
+ final long[] contactIds;
+ final long[] accountIds;
+ final int actualCount;
+ final Cursor c = db.rawQuery(sbQuery.toString(), null);
+ try {
+ actualCount = c.getCount();
+ rawContactIds = new long[actualCount];
+ contactIds = new long[actualCount];
+ accountIds = new long[actualCount];
+
+ index = 0;
+ while (c.moveToNext()) {
+ rawContactIds[index] = c.getLong(AggregationQuery._ID);
+ contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
+ accountIds[index] = c.getLong(AggregationQuery.ACCOUNT_ID);
+ index++;
+ }
+ } finally {
+ c.close();
+ }
+
+ if (DEBUG_LOGGING) {
+ Log.d(TAG, "aggregateInTransaction: initial query done.");
+ }
+
+ for (int i = 0; i < actualCount; i++) {
+ aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i],
+ mCandidates);
+ }
+
+ long elapsedTime = System.currentTimeMillis() - start;
+ EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, actualCount);
+
+ if (DEBUG_LOGGING) {
+ Log.d(TAG, "Contact aggregation complete: " + actualCount +
+ (actualCount == 0 ? "" : ", " + (elapsedTime / actualCount)
+ + " ms per raw contact"));
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ public final void triggerAggregation(TransactionContext txContext, long rawContactId) {
+ if (!mEnabled) {
+ return;
+ }
+
+ int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
+ switch (aggregationMode) {
+ case RawContacts.AGGREGATION_MODE_DISABLED:
+ break;
+
+ case RawContacts.AGGREGATION_MODE_DEFAULT: {
+ markForAggregation(rawContactId, aggregationMode, false);
+ break;
+ }
+
+ case RawContacts.AGGREGATION_MODE_SUSPENDED: {
+ long contactId = mDbHelper.getContactId(rawContactId);
+
+ if (contactId != 0) {
+ updateAggregateData(txContext, contactId);
+ }
+ break;
+ }
+
+ case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
+ aggregateContact(txContext, mDbHelper.getWritableDatabase(), rawContactId);
+ break;
+ }
+ }
+ }
+
+ public final void clearPendingAggregations() {
+ // HashMap woulnd't shrink the internal table once expands it, so let's just re-create
+ // a new one instead of clear()ing it.
+ mRawContactsMarkedForAggregation = Maps.newHashMap();
+ }
+
+ public final void markNewForAggregation(long rawContactId, int aggregationMode) {
+ mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode);
+ }
+
+ public final void markForAggregation(long rawContactId, int aggregationMode, boolean force) {
+ final int effectiveAggregationMode;
+ if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) {
+ // As per ContactsContract documentation, default aggregation mode
+ // does not override a previously set mode
+ if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
+ effectiveAggregationMode = mRawContactsMarkedForAggregation.get(rawContactId);
+ } else {
+ effectiveAggregationMode = aggregationMode;
+ }
+ } else {
+ mMarkForAggregation.bindLong(1, rawContactId);
+ mMarkForAggregation.execute();
+ effectiveAggregationMode = aggregationMode;
+ }
+
+ mRawContactsMarkedForAggregation.put(rawContactId, effectiveAggregationMode);
+ }
+
+ private static class RawContactIdAndAggregationModeQuery {
+ public static final String TABLE = Tables.RAW_CONTACTS;
+
+ public static final String[] COLUMNS = {RawContacts._ID, RawContacts.AGGREGATION_MODE};
+
+ public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
+
+ public static final int _ID = 0;
+ public static final int AGGREGATION_MODE = 1;
+ }
+
+ /**
+ * Marks all constituent raw contacts of an aggregated contact for re-aggregation.
+ */
+ protected final void markContactForAggregation(SQLiteDatabase db, long contactId) {
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ Cursor cursor = db.query(RawContactIdAndAggregationModeQuery.TABLE,
+ RawContactIdAndAggregationModeQuery.COLUMNS,
+ RawContactIdAndAggregationModeQuery.SELECTION, mSelectionArgs1, null, null, null);
+ try {
+ if (cursor.moveToFirst()) {
+ long rawContactId = cursor.getLong(RawContactIdAndAggregationModeQuery._ID);
+ int aggregationMode = cursor.getInt(
+ RawContactIdAndAggregationModeQuery.AGGREGATION_MODE);
+ // Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED.
+ // (Also just ignore deprecated AGGREGATION_MODE_IMMEDIATE)
+ if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
+ markForAggregation(rawContactId, aggregationMode, true);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Mark all visible contacts for re-aggregation.
+ *
+ * - Set {@link RawContactsColumns#AGGREGATION_NEEDED} For all visible raw_contacts with
+ * {@link RawContacts#AGGREGATION_MODE_DEFAULT}.
+ * - Also put them into {@link #mRawContactsMarkedForAggregation}.
+ */
+ public final int markAllVisibleForAggregation(SQLiteDatabase db) {
+ final long start = System.currentTimeMillis();
+
+ // Set AGGREGATION_NEEDED for all visible raw_cotnacts with AGGREGATION_MODE_DEFAULT.
+ // (Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED)
+ db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " +
+ RawContactsColumns.AGGREGATION_NEEDED + "=1" +
+ " WHERE " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY +
+ " AND " + RawContacts.AGGREGATION_MODE + "=" + RawContacts.AGGREGATION_MODE_DEFAULT
+ );
+
+ final int count;
+ final Cursor cursor = db.rawQuery("SELECT " + RawContacts._ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1", null);
+ try {
+ count = cursor.getCount();
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ final long rawContactId = cursor.getLong(0);
+ mRawContactsMarkedForAggregation.put(rawContactId,
+ RawContacts.AGGREGATION_MODE_DEFAULT);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ final long end = System.currentTimeMillis();
+ Log.i(TAG, "Marked all visible contacts for aggregation: " + count + " raw contacts, " +
+ (end - start) + " ms");
+ return count;
+ }
+
+ /**
+ * Creates a new contact based on the given raw contact. Does not perform aggregation. Returns
+ * the ID of the contact that was created.
+ */
+ // Overridden by ProfileAggregator.
+ public long onRawContactInsert(
+ TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
+ long contactId = insertContact(db, rawContactId);
+ setContactId(rawContactId, contactId);
+ mDbHelper.updateContactVisible(txContext, contactId);
+ return contactId;
+ }
+
+ protected final long insertContact(SQLiteDatabase db, long rawContactId) {
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
+ return mContactInsert.executeInsert();
+ }
+
+ private static final class RawContactIdAndAccountQuery {
+ public static final String TABLE = Tables.RAW_CONTACTS;
+
+ public static final String[] COLUMNS = {
+ RawContacts.CONTACT_ID,
+ RawContactsColumns.ACCOUNT_ID
+ };
+
+ public static final String SELECTION = RawContacts._ID + "=?";
+
+ public static final int CONTACT_ID = 0;
+ public static final int ACCOUNT_ID = 1;
+ }
+
+ // Overridden by ProfileAggregator.
+ public void aggregateContact(
+ TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
+ if (!mEnabled) {
+ return;
+ }
+
+ MatchCandidateList candidates = new MatchCandidateList();
+
+ long contactId = 0;
+ long accountId = 0;
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE,
+ RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION,
+ mSelectionArgs1, null, null, null);
+ try {
+ if (cursor.moveToFirst()) {
+ contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID);
+ accountId = cursor.getLong(RawContactIdAndAccountQuery.ACCOUNT_ID);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ aggregateContact(txContext, db, rawContactId, accountId, contactId,
+ candidates);
+ }
+
+ public void updateAggregateData(TransactionContext txContext, long contactId) {
+ if (!mEnabled) {
+ return;
+ }
+
+ final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ computeAggregateData(db, contactId, mContactUpdate);
+ mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
+ mContactUpdate.execute();
+
+ mDbHelper.updateContactVisible(txContext, contactId);
+ updateAggregatedStatusUpdate(contactId);
+ }
+
+ protected final void updateAggregatedStatusUpdate(long contactId) {
+ mAggregatedPresenceReplace.bindLong(1, contactId);
+ mAggregatedPresenceReplace.bindLong(2, contactId);
+ mAggregatedPresenceReplace.execute();
+ updateLastStatusUpdateId(contactId);
+ }
+
+ /**
+ * Adjusts the reference to the latest status update for the specified contact.
+ */
+ public final void updateLastStatusUpdateId(long contactId) {
+ String contactIdString = String.valueOf(contactId);
+ mDbHelper.getWritableDatabase().execSQL(UPDATE_LAST_STATUS_UPDATE_ID_SQL,
+ new String[]{contactIdString, contactIdString});
+ }
+
+ /**
+ * 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.
+ */
+ abstract void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
+ long rawContactId, long accountId, long currentContactId,
+ MatchCandidateList candidates);
+
+
+ protected interface RawContactMatchingSelectionStatement {
+ String SELECT_COUNT = "SELECT count(*) ";
+ String SELECT_ID = "SELECT d1." + Data.RAW_CONTACT_ID + ",d2." + Data.RAW_CONTACT_ID;
+ }
+
+ /**
+ * Build sql to check if there is any identity match/mis-match between two sets of raw contact
+ * ids on the same namespace.
+ */
+ protected final String buildIdentityMatchingSql(String rawContactIdSet1,
+ String rawContactIdSet2, boolean isIdentityMatching, boolean countOnly) {
+ final String identityType = String.valueOf(mMimeTypeIdIdentity);
+ final String matchingOperator = (isIdentityMatching) ? "=" : "!=";
+ final String sql =
+ " FROM " + Tables.DATA + " AS d1" +
+ " JOIN " + Tables.DATA + " AS d2" +
+ " ON (d1." + Identity.IDENTITY + matchingOperator +
+ " d2." + Identity.IDENTITY + " AND" +
+ " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" +
+ " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + identityType +
+ " AND d2." + DataColumns.MIMETYPE_ID + " = " + identityType +
+ " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
+ " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
+ return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
+ RawContactMatchingSelectionStatement.SELECT_ID + sql;
+ }
+
+ protected final String buildEmailMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
+ boolean countOnly) {
+ final String emailType = String.valueOf(mMimeTypeIdEmail);
+ final String sql =
+ " FROM " + Tables.DATA + " AS d1" +
+ " JOIN " + Tables.DATA + " AS d2" +
+ " ON lower(d1." + Email.ADDRESS + ")= lower(d2." + Email.ADDRESS + ")" +
+ " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + emailType +
+ " AND d2." + DataColumns.MIMETYPE_ID + " = " + emailType +
+ " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
+ " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
+ return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
+ RawContactMatchingSelectionStatement.SELECT_ID + sql;
+ }
+
+ protected final String buildPhoneMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
+ boolean countOnly) {
+ // It's a bit tricker because it has to be consistent with
+ // updateMatchScoresBasedOnPhoneMatches().
+ final String phoneType = String.valueOf(mMimeTypeIdPhone);
+ final String sql =
+ " FROM " + Tables.PHONE_LOOKUP + " AS p1" +
+ " JOIN " + Tables.DATA + " AS d1 ON " +
+ "(d1." + Data._ID + "=p1." + PhoneLookupColumns.DATA_ID + ")" +
+ " JOIN " + Tables.PHONE_LOOKUP + " AS p2 ON (p1." + PhoneLookupColumns.MIN_MATCH +
+ "=p2." + PhoneLookupColumns.MIN_MATCH + ")" +
+ " JOIN " + Tables.DATA + " AS d2 ON " +
+ "(d2." + Data._ID + "=p2." + PhoneLookupColumns.DATA_ID + ")" +
+ " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + phoneType +
+ " AND d2." + DataColumns.MIMETYPE_ID + " = " + phoneType +
+ " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
+ " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")" +
+ " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + "," +
+ String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter()) +
+ ")";
+ return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
+ RawContactMatchingSelectionStatement.SELECT_ID + sql;
+ }
+
+ protected final String buildExceptionMatchingSql(String rawContactIdSet1,
+ String rawContactIdSet2) {
+ return "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " +
+ AggregationExceptions.RAW_CONTACT_ID2 +
+ " FROM " + Tables.AGGREGATION_EXCEPTIONS +
+ " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" +
+ rawContactIdSet1 + ")" +
+ " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" +
+ " AND " + AggregationExceptions.TYPE + "=" +
+ AggregationExceptions.TYPE_KEEP_TOGETHER ;
+ }
+
+ protected final boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query) {
+ return DatabaseUtils.longForQuery(db, query, null) > 0;
+ }
+
+ /**
+ * Partition the given raw contact Ids to connected component based on aggregation exception,
+ * identity matching, email matching or phone matching.
+ */
+ protected final Set<Set<Long>> findConnectedRawContacts(SQLiteDatabase db, Set<Long>
+ rawContactIdSet) {
+ // Connections between two raw contacts
+ final Multimap<Long, Long> matchingRawIdPairs = HashMultimap.create();
+ String rawContactIds = TextUtils.join(",", rawContactIdSet);
+ findIdPairs(db, buildExceptionMatchingSql(rawContactIds, rawContactIds),
+ matchingRawIdPairs);
+ findIdPairs(db, buildIdentityMatchingSql(rawContactIds, rawContactIds,
+ /* isIdentityMatching =*/ true, /* countOnly =*/false), matchingRawIdPairs);
+ findIdPairs(db, buildEmailMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false),
+ matchingRawIdPairs);
+ findIdPairs(db, buildPhoneMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false),
+ matchingRawIdPairs);
+
+ return ContactAggregatorHelper.findConnectedComponents(rawContactIdSet, matchingRawIdPairs);
+ }
+
+ /**
+ * Given a query which will return two non-null IDs in the first two columns as results, this
+ * method will put two entries into the given result map for each pair of different IDs, one
+ * keyed by each ID.
+ */
+ protected final void findIdPairs(SQLiteDatabase db, String query,
+ Multimap<Long, Long> results) {
+ Cursor cursor = db.rawQuery(query, null);
+ try {
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ long idA = cursor.getLong(0);
+ long idB = cursor.getLong(1);
+ if (idA != idB) {
+ results.put(idA, idB);
+ results.put(idB, idA);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Creates a new Contact for a given set of the raw contacts of {@code rawContactIds} if the
+ * given contactId is null. Otherwise, regroup them into contact with {@code contactId}.
+ */
+ protected final void createContactForRawContacts(SQLiteDatabase db,
+ TransactionContext txContext, Set<Long> rawContactIds, Long contactId) {
+ if (rawContactIds.isEmpty()) {
+ // No raw contact id is provided.
+ return;
+ }
+
+ // If contactId is not provided, generates a new one.
+ if (contactId == null) {
+ mSelectionArgs1[0] = String.valueOf(rawContactIds.iterator().next());
+ computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
+ mContactInsert);
+ contactId = mContactInsert.executeInsert();
+ }
+ for (Long rawContactId : rawContactIds) {
+ // Regrouped contacts should automatically be unpinned.
+ unpinRawContact(rawContactId);
+ setContactIdAndMarkAggregated(rawContactId, contactId);
+ setPresenceContactId(rawContactId, contactId);
+ }
+ updateAggregateData(txContext, contactId);
+ }
+
+ protected static class RawContactIdQuery {
+ public static final String TABLE = Tables.RAW_CONTACTS;
+ public static final String[] COLUMNS = {RawContacts._ID, RawContactsColumns.ACCOUNT_ID };
+ public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
+ public static final int RAW_CONTACT_ID = 0;
+ public static final int ACCOUNT_ID = 1;
+ }
+
+ /**
+ * Updates the contact ID for the specified contact.
+ */
+ protected final void setContactId(long rawContactId, long contactId) {
+ mContactIdUpdate.bindLong(1, contactId);
+ mContactIdUpdate.bindLong(2, rawContactId);
+ mContactIdUpdate.execute();
+ }
+
+ /**
+ * Marks the specified raw contact ID as aggregated
+ */
+ protected final void markAggregated(long rawContactId) {
+ mMarkAggregatedUpdate.bindLong(1, rawContactId);
+ mMarkAggregatedUpdate.execute();
+ }
+
+ /**
+ * Updates the contact ID for the specified contact and marks the raw contact as aggregated.
+ */
+ private void setContactIdAndMarkAggregated(long rawContactId, long contactId) {
+ mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId);
+ mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId);
+ mContactIdAndMarkAggregatedUpdate.execute();
+ }
+
+ private void setPresenceContactId(long rawContactId, long contactId) {
+ mPresenceContactIdUpdate.bindLong(1, contactId);
+ mPresenceContactIdUpdate.bindLong(2, rawContactId);
+ mPresenceContactIdUpdate.execute();
+ }
+
+ private void unpinRawContact(long rawContactId) {
+ mResetPinnedForRawContact.bindLong(1, rawContactId);
+ mResetPinnedForRawContact.execute();
+ }
+
+ interface AggregateExceptionPrefetchQuery {
+ String TABLE = Tables.AGGREGATION_EXCEPTIONS;
+
+ String[] COLUMNS = {
+ AggregationExceptions.RAW_CONTACT_ID1,
+ AggregationExceptions.RAW_CONTACT_ID2,
+ };
+
+ int RAW_CONTACT_ID1 = 0;
+ int RAW_CONTACT_ID2 = 1;
+ }
+
+ // A set of raw contact IDs for which there are aggregation exceptions
+ protected final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>();
+ protected boolean mAggregationExceptionIdsValid;
+
+ public final void invalidateAggregationExceptionCache() {
+ mAggregationExceptionIdsValid = false;
+ }
+
+ /**
+ * Finds all raw contact IDs for which there are aggregation exceptions. The list of
+ * ids is used as an optimization in aggregation: there is no point to run a query against
+ * the agg_exceptions table if it is known that there are no records there for a given
+ * raw contact ID.
+ */
+ protected final void prefetchAggregationExceptionIds(SQLiteDatabase db) {
+ mAggregationExceptionIds.clear();
+ final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE,
+ AggregateExceptionPrefetchQuery.COLUMNS,
+ null, null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1);
+ long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2);
+ mAggregationExceptionIds.add(rawContactId1);
+ mAggregationExceptionIds.add(rawContactId2);
+ }
+ } finally {
+ c.close();
+ }
+
+ mAggregationExceptionIdsValid = true;
+ }
+
+ protected interface NameLookupQuery {
+ String TABLE = Tables.NAME_LOOKUP;
+
+ String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?";
+ String SELECTION_STRUCTURED_NAME_BASED =
+ SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL;
+
+ String[] COLUMNS = new String[] {
+ NameLookupColumns.NORMALIZED_NAME,
+ NameLookupColumns.NAME_TYPE
+ };
+
+ int NORMALIZED_NAME = 0;
+ int NAME_TYPE = 1;
+ }
+
+ protected final void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId,
+ MatchCandidateList candidates, boolean structuredNameBased) {
+ candidates.clear();
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
+ structuredNameBased
+ ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED
+ : NameLookupQuery.SELECTION,
+ mSelectionArgs1, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
+ int type = c.getInt(NameLookupQuery.NAME_TYPE);
+ candidates.add(normalizedName, type);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ protected interface IdentityLookupMatchQuery {
+ final String TABLE = Tables.DATA + " dataA"
+ + " JOIN " + Tables.DATA + " dataB" +
+ " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE +
+ " AND dataA." + Identity.IDENTITY + "=dataB." + Identity.IDENTITY + ")"
+ + " JOIN " + Tables.RAW_CONTACTS +
+ " ON (dataB." + Data.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
+ + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
+ + " AND dataA." + Identity.NAMESPACE + " NOT NULL"
+ + " AND dataA." + Identity.IDENTITY + " NOT NULL"
+ + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
+ + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
+
+ final String[] COLUMNS = new String[] {
+ RawContactsColumns.CONCRETE_ID, RawContacts.CONTACT_ID,
+ RawContactsColumns.ACCOUNT_ID
+ };
+
+ int RAW_CONTACT_ID = 0;
+ int CONTACT_ID = 1;
+ int ACCOUNT_ID = 2;
+ }
+
+ interface AggregateExceptionQuery {
+ String TABLE = Tables.AGGREGATION_EXCEPTIONS
+ + " JOIN raw_contacts raw_contacts1 "
+ + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) "
+ + " JOIN raw_contacts raw_contacts2 "
+ + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) ";
+
+ String[] COLUMNS = {
+ AggregationExceptions.TYPE,
+ AggregationExceptions.RAW_CONTACT_ID1,
+ "raw_contacts1." + RawContacts.CONTACT_ID,
+ "raw_contacts1." + RawContactsColumns.ACCOUNT_ID,
+ "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED,
+ AggregationExceptions.RAW_CONTACT_ID2,
+ "raw_contacts2." + RawContacts.CONTACT_ID,
+ "raw_contacts2." + RawContactsColumns.ACCOUNT_ID,
+ "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED,
+ };
+
+ int TYPE = 0;
+ int RAW_CONTACT_ID1 = 1;
+ int CONTACT_ID1 = 2;
+ int ACCOUNT_ID1 = 3;
+ int AGGREGATION_NEEDED_1 = 4;
+ int RAW_CONTACT_ID2 = 5;
+ int CONTACT_ID2 = 6;
+ int ACCOUNT_ID2 = 7;
+ int AGGREGATION_NEEDED_2 = 8;
+ }
+
+ protected interface NameLookupMatchQuery {
+ String TABLE = Tables.NAME_LOOKUP + " nameA"
+ + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
+ " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
+ + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
+ + " JOIN " + Tables.RAW_CONTACTS +
+ " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
+ + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
+
+ String[] COLUMNS = new String[] {
+ RawContacts._ID,
+ RawContacts.CONTACT_ID,
+ RawContactsColumns.ACCOUNT_ID,
+ "nameA." + NameLookupColumns.NORMALIZED_NAME,
+ "nameA." + NameLookupColumns.NAME_TYPE,
+ "nameB." + NameLookupColumns.NAME_TYPE,
+ };
+
+ int RAW_CONTACT_ID = 0;
+ int CONTACT_ID = 1;
+ int ACCOUNT_ID = 2;
+ int NAME = 3;
+ int NAME_TYPE_A = 4;
+ int NAME_TYPE_B = 5;
+ }
+
+ protected interface NameLookupMatchQueryWithParameter {
+ String TABLE = Tables.NAME_LOOKUP
+ + " JOIN " + Tables.RAW_CONTACTS +
+ " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ String[] COLUMNS = new String[] {
+ RawContacts._ID,
+ RawContacts.CONTACT_ID,
+ RawContactsColumns.ACCOUNT_ID,
+ NameLookupColumns.NORMALIZED_NAME,
+ NameLookupColumns.NAME_TYPE,
+ };
+
+ int RAW_CONTACT_ID = 0;
+ int CONTACT_ID = 1;
+ int ACCOUNT_ID = 2;
+ int NAME = 3;
+ int NAME_TYPE = 4;
+ }
+
+ protected final class NameLookupSelectionBuilder extends NameLookupBuilder {
+
+ private final MatchCandidateList mNameLookupCandidates;
+
+ private StringBuilder mSelection = new StringBuilder(
+ NameLookupColumns.NORMALIZED_NAME + " IN(");
+
+
+ public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) {
+ super(splitter);
+ this.mNameLookupCandidates = candidates;
+ }
+
+ @Override
+ protected String[] getCommonNicknameClusters(String normalizedName) {
+ return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
+ }
+
+ @Override
+ protected void insertNameLookup(
+ long rawContactId, long dataId, int lookupType, String string) {
+ mNameLookupCandidates.add(string, lookupType);
+ DatabaseUtils.appendEscapedSQLString(mSelection, string);
+ mSelection.append(',');
+ }
+
+ public boolean isEmpty() {
+ return mNameLookupCandidates.isEmpty();
+ }
+
+ public String getSelection() {
+ mSelection.setLength(mSelection.length() - 1); // Strip last comma
+ mSelection.append(')');
+ return mSelection.toString();
+ }
+
+ public int getLookupType(String name) {
+ for (int i = 0; i < mNameLookupCandidates.mCount; i++) {
+ if (mNameLookupCandidates.mList.get(i).mName.equals(name)) {
+ return mNameLookupCandidates.mList.get(i).mLookupType;
+ }
+ }
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Finds contacts with names matching the specified name.
+ */
+ protected final void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query,
+ MatchCandidateList candidates, ContactMatcher matcher) {
+ candidates.clear();
+ NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder(
+ mNameSplitter, candidates);
+ builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED);
+ if (builder.isEmpty()) {
+ return;
+ }
+
+ Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE,
+ NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null,
+ null, PRIMARY_HIT_LIMIT_STRING);
+ try {
+ while (c.moveToNext()) {
+ long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID);
+ String name = c.getString(NameLookupMatchQueryWithParameter.NAME);
+ int nameTypeA = builder.getLookupType(name);
+ int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE);
+ matcher.matchName(contactId, nameTypeA, name, nameTypeB, name,
+ ContactMatcher.MATCHING_ALGORITHM_EXACT);
+ if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) {
+ matcher.updateScoreWithNicknameMatch(contactId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ protected interface EmailLookupQuery {
+ String TABLE = Tables.DATA + " dataA"
+ + " JOIN " + Tables.DATA + " dataB" +
+ " ON lower(" + "dataA." + Email.DATA + ")=lower(dataB." + Email.DATA + ")"
+ + " JOIN " + Tables.RAW_CONTACTS +
+ " ON (dataB." + Data.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
+ + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
+ + " AND dataA." + Email.DATA + " NOT NULL"
+ + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
+ + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
+
+ String[] COLUMNS = new String[] {
+ Tables.RAW_CONTACTS + "." + RawContacts._ID,
+ RawContacts.CONTACT_ID,
+ RawContactsColumns.ACCOUNT_ID
+ };
+
+ int RAW_CONTACT_ID = 0;
+ int CONTACT_ID = 1;
+ int ACCOUNT_ID = 2;
+ }
+
+ protected interface PhoneLookupQuery {
+ String TABLE = Tables.PHONE_LOOKUP + " phoneA"
+ + " JOIN " + Tables.DATA + " dataA"
+ + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
+ + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
+ + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
+ + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
+ + " JOIN " + Tables.DATA + " dataB"
+ + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
+ + " JOIN " + Tables.RAW_CONTACTS
+ + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
+ + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
+ + "dataB." + Phone.NUMBER + ",?)"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
+ + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
+
+ String[] COLUMNS = new String[] {
+ Tables.RAW_CONTACTS + "." + RawContacts._ID,
+ RawContacts.CONTACT_ID,
+ RawContactsColumns.ACCOUNT_ID
+ };
+
+ int RAW_CONTACT_ID = 0;
+ int CONTACT_ID = 1;
+ int ACCOUNT_ID = 2;
+ }
+
+ private interface ContactNameLookupQuery {
+ String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
+
+ String[] COLUMNS = new String[]{
+ RawContacts.CONTACT_ID,
+ NameLookupColumns.NORMALIZED_NAME,
+ NameLookupColumns.NAME_TYPE
+ };
+
+ int CONTACT_ID = 0;
+ int NORMALIZED_NAME = 1;
+ int NAME_TYPE = 2;
+ }
+
+ /**
+ * 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, int algorithm, String limit) {
+ final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
+ selection, null, null, null, null, limit);
+
+ try {
+ while (c.moveToNext()) {
+ Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
+ String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
+ int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
+
+ // 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);
+ matcher.matchName(contactId, candidate.mLookupType, candidate.mName,
+ nameType, name, algorithm);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ private interface RawContactsQuery {
+ String SQL_FORMAT_HAS_SUPER_PRIMARY_NAME =
+ " EXISTS(SELECT 1 " +
+ " FROM " + Tables.DATA + " d " +
+ " WHERE d." + DataColumns.MIMETYPE_ID + "=%d " +
+ " AND d." + Data.RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID +
+ " AND d." + Data.IS_SUPER_PRIMARY + "=1)";
+
+ String SQL_FORMAT =
+ "SELECT "
+ + RawContactsColumns.CONCRETE_ID + ","
+ + RawContactsColumns.DISPLAY_NAME + ","
+ + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
+ + AccountsColumns.CONCRETE_ACCOUNT_TYPE + ","
+ + AccountsColumns.CONCRETE_ACCOUNT_NAME + ","
+ + AccountsColumns.CONCRETE_DATA_SET + ","
+ + RawContacts.SOURCE_ID + ","
+ + RawContacts.CUSTOM_RINGTONE + ","
+ + RawContacts.SEND_TO_VOICEMAIL + ","
+ + RawContacts.LAST_TIME_CONTACTED + ","
+ + RawContacts.TIMES_CONTACTED + ","
+ + RawContacts.STARRED + ","
+ + RawContacts.PINNED + ","
+ + DataColumns.CONCRETE_ID + ","
+ + DataColumns.CONCRETE_MIMETYPE_ID + ","
+ + Data.IS_SUPER_PRIMARY + ","
+ + Photo.PHOTO_FILE_ID + ","
+ + SQL_FORMAT_HAS_SUPER_PRIMARY_NAME +
+ " FROM " + Tables.RAW_CONTACTS +
+ " JOIN " + Tables.ACCOUNTS + " ON ("
+ + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
+ + ")" +
+ " LEFT OUTER JOIN " + Tables.DATA +
+ " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
+ + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
+ + " AND " + Photo.PHOTO + " NOT NULL)"
+ + " OR (" + DataColumns.MIMETYPE_ID + "=%d"
+ + " AND " + Phone.NUMBER + " NOT NULL)))";
+
+ String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT +
+ " WHERE " + RawContactsColumns.CONCRETE_ID + "=?";
+
+ String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT +
+ " WHERE " + RawContacts.CONTACT_ID + "=?"
+ + " AND " + RawContacts.DELETED + "=0";
+
+ int RAW_CONTACT_ID = 0;
+ int DISPLAY_NAME = 1;
+ int DISPLAY_NAME_SOURCE = 2;
+ int ACCOUNT_TYPE = 3;
+ int ACCOUNT_NAME = 4;
+ int DATA_SET = 5;
+ int SOURCE_ID = 6;
+ int CUSTOM_RINGTONE = 7;
+ int SEND_TO_VOICEMAIL = 8;
+ int LAST_TIME_CONTACTED = 9;
+ int TIMES_CONTACTED = 10;
+ int STARRED = 11;
+ int PINNED = 12;
+ int DATA_ID = 13;
+ int MIMETYPE_ID = 14;
+ int IS_SUPER_PRIMARY = 15;
+ int PHOTO_FILE_ID = 16;
+ int HAS_SUPER_PRIMARY_NAME = 17;
+ }
+
+ protected interface ContactReplaceSqlStatement {
+ String UPDATE_SQL =
+ "UPDATE " + Tables.CONTACTS +
+ " SET "
+ + Contacts.NAME_RAW_CONTACT_ID + "=?, "
+ + Contacts.PHOTO_ID + "=?, "
+ + Contacts.PHOTO_FILE_ID + "=?, "
+ + Contacts.SEND_TO_VOICEMAIL + "=?, "
+ + Contacts.CUSTOM_RINGTONE + "=?, "
+ + Contacts.LAST_TIME_CONTACTED + "=?, "
+ + Contacts.TIMES_CONTACTED + "=?, "
+ + Contacts.STARRED + "=?, "
+ + Contacts.PINNED + "=?, "
+ + Contacts.HAS_PHONE_NUMBER + "=?, "
+ + Contacts.LOOKUP_KEY + "=?, "
+ + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=? " +
+ " WHERE " + Contacts._ID + "=?";
+
+ String INSERT_SQL =
+ "INSERT INTO " + Tables.CONTACTS + " ("
+ + Contacts.NAME_RAW_CONTACT_ID + ", "
+ + Contacts.PHOTO_ID + ", "
+ + Contacts.PHOTO_FILE_ID + ", "
+ + Contacts.SEND_TO_VOICEMAIL + ", "
+ + Contacts.CUSTOM_RINGTONE + ", "
+ + Contacts.LAST_TIME_CONTACTED + ", "
+ + Contacts.TIMES_CONTACTED + ", "
+ + Contacts.STARRED + ", "
+ + Contacts.PINNED + ", "
+ + Contacts.HAS_PHONE_NUMBER + ", "
+ + Contacts.LOOKUP_KEY + ", "
+ + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
+ + ") " +
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";
+
+ int NAME_RAW_CONTACT_ID = 1;
+ int PHOTO_ID = 2;
+ int PHOTO_FILE_ID = 3;
+ int SEND_TO_VOICEMAIL = 4;
+ int CUSTOM_RINGTONE = 5;
+ int LAST_TIME_CONTACTED = 6;
+ int TIMES_CONTACTED = 7;
+ int STARRED = 8;
+ int PINNED = 9;
+ int HAS_PHONE_NUMBER = 10;
+ int LOOKUP_KEY = 11;
+ int CONTACT_LAST_UPDATED_TIMESTAMP = 12;
+ int CONTACT_ID = 13;
+ }
+
+ /**
+ * Computes aggregate-level data for the specified aggregate contact ID.
+ */
+ protected void computeAggregateData(SQLiteDatabase db, long contactId,
+ SQLiteStatement statement) {
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
+ }
+
+ /**
+ * Indicates whether the given photo entry and priority gives this photo a higher overall
+ * priority than the current best photo entry and priority.
+ */
+ private boolean hasHigherPhotoPriority(PhotoEntry photoEntry, int priority,
+ PhotoEntry bestPhotoEntry, int bestPriority) {
+ int photoComparison = photoEntry.compareTo(bestPhotoEntry);
+ return photoComparison < 0 || photoComparison == 0 && priority > bestPriority;
+ }
+
+ /**
+ * Computes aggregate-level data from constituent raw contacts.
+ */
+ protected final void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
+ SQLiteStatement statement) {
+ long currentRawContactId = -1;
+ long bestPhotoId = -1;
+ long bestPhotoFileId = 0;
+ PhotoEntry bestPhotoEntry = null;
+ boolean foundSuperPrimaryPhoto = false;
+ int photoPriority = -1;
+ int totalRowCount = 0;
+ int contactSendToVoicemail = 0;
+ String contactCustomRingtone = null;
+ long contactLastTimeContacted = 0;
+ int contactTimesContacted = 0;
+ int contactStarred = 0;
+ int contactPinned = Integer.MAX_VALUE;
+ int hasPhoneNumber = 0;
+ StringBuilder lookupKey = new StringBuilder();
+
+ mDisplayNameCandidate.clear();
+
+ Cursor c = db.rawQuery(sql, sqlArgs);
+ try {
+ while (c.moveToNext()) {
+ long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
+ if (rawContactId != currentRawContactId) {
+ currentRawContactId = rawContactId;
+ totalRowCount++;
+
+ // Assemble sub-account.
+ String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
+ String dataSet = c.getString(RawContactsQuery.DATA_SET);
+ String accountWithDataSet = (!TextUtils.isEmpty(dataSet))
+ ? accountType + "/" + dataSet
+ : accountType;
+
+ // Display name
+ String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
+ int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
+ int isNameSuperPrimary = c.getInt(RawContactsQuery.HAS_SUPER_PRIMARY_NAME);
+ processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
+ mContactsProvider.isWritableAccountWithDataSet(accountWithDataSet),
+ isNameSuperPrimary != 0);
+
+ // Contact options
+ if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
+ boolean sendToVoicemail =
+ (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0);
+ if (sendToVoicemail) {
+ contactSendToVoicemail++;
+ }
+ }
+
+ if (contactCustomRingtone == null
+ && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) {
+ contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
+ }
+
+ long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED);
+ if (lastTimeContacted > contactLastTimeContacted) {
+ contactLastTimeContacted = lastTimeContacted;
+ }
+
+ int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED);
+ if (timesContacted > contactTimesContacted) {
+ contactTimesContacted = timesContacted;
+ }
+
+ if (c.getInt(RawContactsQuery.STARRED) != 0) {
+ contactStarred = 1;
+ }
+
+ // contactPinned should be the lowest value of its constituent raw contacts,
+ // excluding negative integers
+ final int rawContactPinned = c.getInt(RawContactsQuery.PINNED);
+ if (rawContactPinned > PinnedPositions.UNPINNED) {
+ contactPinned = Math.min(contactPinned, rawContactPinned);
+ }
+
+ appendLookupKey(
+ lookupKey,
+ accountWithDataSet,
+ c.getString(RawContactsQuery.ACCOUNT_NAME),
+ rawContactId,
+ c.getString(RawContactsQuery.SOURCE_ID),
+ displayName);
+ }
+
+ if (!c.isNull(RawContactsQuery.DATA_ID)) {
+ long dataId = c.getLong(RawContactsQuery.DATA_ID);
+ long photoFileId = c.getLong(RawContactsQuery.PHOTO_FILE_ID);
+ int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
+ boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
+ if (mimetypeId == mMimeTypeIdPhoto) {
+ if (!foundSuperPrimaryPhoto) {
+ // Lookup the metadata for the photo, if available. Note that data set
+ // does not come into play here, since accounts are looked up in the
+ // account manager in the priority resolver.
+ PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
+ String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
+ int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
+ if (superPrimary || hasHigherPhotoPriority(
+ photoEntry, priority, bestPhotoEntry, photoPriority)) {
+ bestPhotoEntry = photoEntry;
+ photoPriority = priority;
+ bestPhotoId = dataId;
+ bestPhotoFileId = photoFileId;
+ foundSuperPrimaryPhoto |= superPrimary;
+ }
+ }
+ } else if (mimetypeId == mMimeTypeIdPhone) {
+ hasPhoneNumber = 1;
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ if (contactPinned == Integer.MAX_VALUE) {
+ contactPinned = PinnedPositions.UNPINNED;
+ }
+
+ statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID,
+ mDisplayNameCandidate.rawContactId);
+
+ if (bestPhotoId != -1) {
+ statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
+ } else {
+ statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
+ }
+
+ if (bestPhotoFileId != 0) {
+ statement.bindLong(ContactReplaceSqlStatement.PHOTO_FILE_ID, bestPhotoFileId);
+ } else {
+ statement.bindNull(ContactReplaceSqlStatement.PHOTO_FILE_ID);
+ }
+
+ statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL,
+ totalRowCount == contactSendToVoicemail ? 1 : 0);
+ DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
+ contactCustomRingtone);
+ statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
+ contactLastTimeContacted);
+ statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
+ contactTimesContacted);
+ statement.bindLong(ContactReplaceSqlStatement.STARRED,
+ contactStarred);
+ statement.bindLong(ContactReplaceSqlStatement.PINNED,
+ contactPinned);
+ statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER,
+ hasPhoneNumber);
+ statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
+ Uri.encode(lookupKey.toString()));
+ statement.bindLong(ContactReplaceSqlStatement.CONTACT_LAST_UPDATED_TIMESTAMP,
+ Clock.getInstance().currentTimeMillis());
+ }
+
+ /**
+ * Builds a lookup key using the given data.
+ */
+ // Overridden by ProfileAggregator.
+ protected void appendLookupKey(StringBuilder sb, String accountTypeWithDataSet,
+ String accountName, long rawContactId, String sourceId, String displayName) {
+ ContactLookupKey.appendToLookupKey(sb, accountTypeWithDataSet, accountName, rawContactId,
+ sourceId, displayName);
+ }
+
+ /**
+ * Uses the supplied values to determine if they represent a "better" display name
+ * for the aggregate contact currently evaluated. If so, it updates
+ * {@link #mDisplayNameCandidate} with the new values.
+ */
+ private void processDisplayNameCandidate(long rawContactId, String displayName,
+ int displayNameSource, boolean writableAccount, boolean isNameSuperPrimary) {
+
+ boolean replace = false;
+ if (mDisplayNameCandidate.rawContactId == -1) {
+ // No previous values available
+ replace = true;
+ } else if (!TextUtils.isEmpty(displayName)) {
+ if (isNameSuperPrimary) {
+ // A super primary name is better than any other name
+ replace = true;
+ } else if (mDisplayNameCandidate.isNameSuperPrimary == isNameSuperPrimary) {
+ if (mDisplayNameCandidate.displayNameSource < displayNameSource) {
+ // New values come from an superior source, e.g. structured name vs phone number
+ replace = true;
+ } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) {
+ if (!mDisplayNameCandidate.writableAccount && writableAccount) {
+ replace = true;
+ } else if (mDisplayNameCandidate.writableAccount == writableAccount) {
+ if (NameNormalizer.compareComplexity(displayName,
+ mDisplayNameCandidate.displayName) > 0) {
+ // New name is more complex than the previously found one
+ replace = true;
+ }
+ }
+ }
+ }
+ }
+
+ if (replace) {
+ mDisplayNameCandidate.rawContactId = rawContactId;
+ mDisplayNameCandidate.displayName = displayName;
+ mDisplayNameCandidate.displayNameSource = displayNameSource;
+ mDisplayNameCandidate.isNameSuperPrimary = isNameSuperPrimary;
+ mDisplayNameCandidate.writableAccount = writableAccount;
+ }
+ }
+
+ private interface PhotoIdQuery {
+ final String[] COLUMNS = new String[] {
+ AccountsColumns.CONCRETE_ACCOUNT_TYPE,
+ DataColumns.CONCRETE_ID,
+ Data.IS_SUPER_PRIMARY,
+ Photo.PHOTO_FILE_ID,
+ };
+
+ int ACCOUNT_TYPE = 0;
+ int DATA_ID = 1;
+ int IS_SUPER_PRIMARY = 2;
+ int PHOTO_FILE_ID = 3;
+ }
+
+ public final void updatePhotoId(SQLiteDatabase db, long rawContactId) {
+
+ long contactId = mDbHelper.getContactId(rawContactId);
+ if (contactId == 0) {
+ return;
+ }
+
+ long bestPhotoId = -1;
+ long bestPhotoFileId = 0;
+ int photoPriority = -1;
+
+ long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
+
+ String tables = Tables.RAW_CONTACTS
+ + " JOIN " + Tables.ACCOUNTS + " ON ("
+ + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
+ + ")"
+ + " JOIN " + Tables.DATA + " ON("
+ + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
+ + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
+ + Photo.PHOTO + " NOT NULL))";
+
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS,
+ RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
+ try {
+ PhotoEntry bestPhotoEntry = null;
+ while (c.moveToNext()) {
+ long dataId = c.getLong(PhotoIdQuery.DATA_ID);
+ long photoFileId = c.getLong(PhotoIdQuery.PHOTO_FILE_ID);
+ boolean superPrimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0;
+ PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
+
+ // Note that data set does not come into play here, since accounts are looked up in
+ // the account manager in the priority resolver.
+ String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE);
+ int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
+ if (superPrimary || hasHigherPhotoPriority(
+ photoEntry, priority, bestPhotoEntry, photoPriority)) {
+ bestPhotoEntry = photoEntry;
+ photoPriority = priority;
+ bestPhotoId = dataId;
+ bestPhotoFileId = photoFileId;
+ if (superPrimary) {
+ break;
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ if (bestPhotoId == -1) {
+ mPhotoIdUpdate.bindNull(1);
+ } else {
+ mPhotoIdUpdate.bindLong(1, bestPhotoId);
+ }
+
+ if (bestPhotoFileId == 0) {
+ mPhotoIdUpdate.bindNull(2);
+ } else {
+ mPhotoIdUpdate.bindLong(2, bestPhotoFileId);
+ }
+
+ mPhotoIdUpdate.bindLong(3, contactId);
+ mPhotoIdUpdate.execute();
+ }
+
+ private interface PhotoFileQuery {
+ final String[] COLUMNS = new String[] {
+ PhotoFiles.HEIGHT,
+ PhotoFiles.WIDTH,
+ PhotoFiles.FILESIZE
+ };
+
+ int HEIGHT = 0;
+ int WIDTH = 1;
+ int FILESIZE = 2;
+ }
+
+ private class PhotoEntry implements Comparable<PhotoEntry> {
+ // Pixel count (width * height) for the image.
+ final int pixelCount;
+
+ // File size (in bytes) of the image. Not populated if the image is a thumbnail.
+ final int fileSize;
+
+ private PhotoEntry(int pixelCount, int fileSize) {
+ this.pixelCount = pixelCount;
+ this.fileSize = fileSize;
+ }
+
+ @Override
+ public int compareTo(PhotoEntry pe) {
+ if (pe == null) {
+ return -1;
+ }
+ if (pixelCount == pe.pixelCount) {
+ return pe.fileSize - fileSize;
+ } else {
+ return pe.pixelCount - pixelCount;
+ }
+ }
+ }
+
+ private PhotoEntry getPhotoMetadata(SQLiteDatabase db, long photoFileId) {
+ if (photoFileId == 0) {
+ // Assume standard thumbnail size. Don't bother getting a file size for priority;
+ // we should fall back to photo priority resolver if all we have are thumbnails.
+ int thumbDim = mContactsProvider.getMaxThumbnailDim();
+ return new PhotoEntry(thumbDim * thumbDim, 0);
+ } else {
+ Cursor c = db.query(Tables.PHOTO_FILES, PhotoFileQuery.COLUMNS, PhotoFiles._ID + "=?",
+ new String[]{String.valueOf(photoFileId)}, null, null, null);
+ try {
+ if (c.getCount() == 1) {
+ c.moveToFirst();
+ int pixelCount =
+ c.getInt(PhotoFileQuery.HEIGHT) * c.getInt(PhotoFileQuery.WIDTH);
+ return new PhotoEntry(pixelCount, c.getInt(PhotoFileQuery.FILESIZE));
+ }
+ } finally {
+ c.close();
+ }
+ }
+ return new PhotoEntry(0, 0);
+ }
+
+ private interface DisplayNameQuery {
+ String SQL_HAS_SUPER_PRIMARY_NAME =
+ " EXISTS(SELECT 1 " +
+ " FROM " + Tables.DATA + " d " +
+ " WHERE d." + DataColumns.MIMETYPE_ID + "=? " +
+ " AND d." + Data.RAW_CONTACT_ID + "=" + Views.RAW_CONTACTS
+ + "." + RawContacts._ID +
+ " AND d." + Data.IS_SUPER_PRIMARY + "=1)";
+
+ String SQL =
+ "SELECT "
+ + RawContacts._ID + ","
+ + RawContactsColumns.DISPLAY_NAME + ","
+ + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
+ + SQL_HAS_SUPER_PRIMARY_NAME + ","
+ + RawContacts.SOURCE_ID + ","
+ + RawContacts.ACCOUNT_TYPE_AND_DATA_SET +
+ " FROM " + Views.RAW_CONTACTS +
+ " WHERE " + RawContacts.CONTACT_ID + "=? ";
+
+ int _ID = 0;
+ int DISPLAY_NAME = 1;
+ int DISPLAY_NAME_SOURCE = 2;
+ int HAS_SUPER_PRIMARY_NAME = 3;
+ int SOURCE_ID = 4;
+ int ACCOUNT_TYPE_AND_DATA_SET = 5;
+ }
+
+ public final void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
+ long contactId = mDbHelper.getContactId(rawContactId);
+ if (contactId == 0) {
+ return;
+ }
+
+ updateDisplayNameForContact(db, contactId);
+ }
+
+ public final void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
+ boolean lookupKeyUpdateNeeded = false;
+
+ mDisplayNameCandidate.clear();
+
+ mSelectionArgs2[0] = String.valueOf(mDbHelper.getMimeTypeIdForStructuredName());
+ mSelectionArgs2[1] = String.valueOf(contactId);
+ final Cursor c = db.rawQuery(DisplayNameQuery.SQL, mSelectionArgs2);
+ try {
+ while (c.moveToNext()) {
+ long rawContactId = c.getLong(DisplayNameQuery._ID);
+ String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
+ int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
+ int isNameSuperPrimary = c.getInt(DisplayNameQuery.HAS_SUPER_PRIMARY_NAME);
+ String accountTypeAndDataSet = c.getString(
+ DisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET);
+ processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
+ mContactsProvider.isWritableAccountWithDataSet(accountTypeAndDataSet),
+ isNameSuperPrimary != 0);
+
+ // If the raw contact has no source id, the lookup key is based on the display
+ // name, so the lookup key needs to be updated.
+ lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID);
+ }
+ } finally {
+ c.close();
+ }
+
+ if (mDisplayNameCandidate.rawContactId != -1) {
+ mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId);
+ mDisplayNameUpdate.bindLong(2, contactId);
+ mDisplayNameUpdate.execute();
+ }
+
+ if (lookupKeyUpdateNeeded) {
+ updateLookupKeyForContact(db, contactId);
+ }
+ }
+
+
+ /**
+ * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the
+ * specified raw contact.
+ */
+ public final void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) {
+
+ long contactId = mDbHelper.getContactId(rawContactId);
+ if (contactId == 0) {
+ return;
+ }
+
+ final SQLiteStatement hasPhoneNumberUpdate = db.compileStatement(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.HAS_PHONE_NUMBER + "="
+ + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)"
+ + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS
+ + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
+ + " AND " + Phone.NUMBER + " NOT NULL"
+ + " AND " + RawContacts.CONTACT_ID + "=?)" +
+ " WHERE " + Contacts._ID + "=?");
+ try {
+ hasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE));
+ hasPhoneNumberUpdate.bindLong(2, contactId);
+ hasPhoneNumberUpdate.bindLong(3, contactId);
+ hasPhoneNumberUpdate.execute();
+ } finally {
+ hasPhoneNumberUpdate.close();
+ }
+ }
+
+ private interface LookupKeyQuery {
+ String TABLE = Views.RAW_CONTACTS;
+ String[] COLUMNS = new String[] {
+ RawContacts._ID,
+ RawContactsColumns.DISPLAY_NAME,
+ RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
+ RawContacts.ACCOUNT_NAME,
+ RawContacts.SOURCE_ID,
+ };
+
+ int ID = 0;
+ int DISPLAY_NAME = 1;
+ int ACCOUNT_TYPE_AND_DATA_SET = 2;
+ int ACCOUNT_NAME = 3;
+ int SOURCE_ID = 4;
+ }
+
+ public final void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
+ long contactId = mDbHelper.getContactId(rawContactId);
+ if (contactId == 0) {
+ return;
+ }
+
+ updateLookupKeyForContact(db, contactId);
+ }
+
+ private void updateLookupKeyForContact(SQLiteDatabase db, long contactId) {
+ String lookupKey = computeLookupKeyForContact(db, contactId);
+
+ if (lookupKey == null) {
+ mLookupKeyUpdate.bindNull(1);
+ } else {
+ mLookupKeyUpdate.bindString(1, Uri.encode(lookupKey));
+ }
+ mLookupKeyUpdate.bindLong(2, contactId);
+
+ mLookupKeyUpdate.execute();
+ }
+
+ // Overridden by ProfileAggregator.
+ protected String computeLookupKeyForContact(SQLiteDatabase db, long contactId) {
+ StringBuilder sb = new StringBuilder();
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ final Cursor c = db.query(LookupKeyQuery.TABLE, LookupKeyQuery.COLUMNS,
+ RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID);
+ try {
+ while (c.moveToNext()) {
+ ContactLookupKey.appendToLookupKey(sb,
+ c.getString(LookupKeyQuery.ACCOUNT_TYPE_AND_DATA_SET),
+ c.getString(LookupKeyQuery.ACCOUNT_NAME),
+ c.getLong(LookupKeyQuery.ID),
+ c.getString(LookupKeyQuery.SOURCE_ID),
+ c.getString(LookupKeyQuery.DISPLAY_NAME));
+ }
+ } finally {
+ c.close();
+ }
+ return sb.length() == 0 ? null : sb.toString();
+ }
+
+ /**
+ * Execute {@link SQLiteStatement} that will update the
+ * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}.
+ */
+ public final void updateStarred(long rawContactId) {
+ long contactId = mDbHelper.getContactId(rawContactId);
+ if (contactId == 0) {
+ return;
+ }
+
+ mStarredUpdate.bindLong(1, contactId);
+ mStarredUpdate.execute();
+ }
+
+ /**
+ * Execute {@link SQLiteStatement} that will update the
+ * {@link Contacts#PINNED} flag for the given {@link RawContacts#_ID}.
+ */
+ public final void updatePinned(long rawContactId) {
+ long contactId = mDbHelper.getContactId(rawContactId);
+ if (contactId == 0) {
+ return;
+ }
+ mPinnedUpdate.bindLong(1, contactId);
+ mPinnedUpdate.execute();
+ }
+
+ /**
+ * Finds matching contacts and returns a cursor on those.
+ */
+ public final Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb,
+ String[] projection, long contactId, int maxSuggestions, String filter,
+ ArrayList<AggregationSuggestionParameter> parameters) {
+ final SQLiteDatabase db = mDbHelper.getReadableDatabase();
+ db.beginTransaction();
+ try {
+ List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters);
+ return queryMatchingContacts(qb, db, projection, bestMatches, maxSuggestions, filter);
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private interface ContactIdQuery {
+ String[] COLUMNS = new String[] {
+ Contacts._ID
+ };
+
+ int _ID = 0;
+ }
+
+ /**
+ * Loads contacts with specified IDs and returns them in the order of IDs in the
+ * supplied list.
+ */
+ private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db,
+ String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(Contacts._ID);
+ sb.append(" IN (");
+ for (int i = 0; i < bestMatches.size(); i++) {
+ MatchScore matchScore = bestMatches.get(i);
+ if (i != 0) {
+ sb.append(",");
+ }
+ sb.append(matchScore.getContactId());
+ }
+ sb.append(")");
+
+ if (!TextUtils.isEmpty(filter)) {
+ sb.append(" AND " + Contacts._ID + " IN ");
+ mContactsProvider.appendContactFilterAsNestedQuery(sb, filter);
+ }
+
+ // Run a query and find ids of best matching contacts satisfying the filter (if any)
+ HashSet<Long> foundIds = new HashSet<Long>();
+ Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(),
+ null, null, null, null);
+ try {
+ while(cursor.moveToNext()) {
+ foundIds.add(cursor.getLong(ContactIdQuery._ID));
+ }
+ } finally {
+ cursor.close();
+ }
+
+ // Exclude all contacts that did not match the filter
+ Iterator<MatchScore> iter = bestMatches.iterator();
+ while (iter.hasNext()) {
+ long id = iter.next().getContactId();
+ if (!foundIds.contains(id)) {
+ iter.remove();
+ }
+ }
+
+ // Limit the number of returned suggestions
+ final List<MatchScore> limitedMatches;
+ if (bestMatches.size() > maxSuggestions) {
+ limitedMatches = bestMatches.subList(0, maxSuggestions);
+ } else {
+ limitedMatches = bestMatches;
+ }
+
+ // Build an in-clause with the remaining contact IDs
+ sb.setLength(0);
+ sb.append(Contacts._ID);
+ sb.append(" IN (");
+ for (int i = 0; i < limitedMatches.size(); i++) {
+ MatchScore matchScore = limitedMatches.get(i);
+ if (i != 0) {
+ sb.append(",");
+ }
+ sb.append(matchScore.getContactId());
+ }
+ sb.append(")");
+
+ // Run the final query with the required projection and contact IDs found by the first query
+ cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID);
+
+ // Build a sorted list of discovered IDs
+ ArrayList<Long> sortedContactIds = new ArrayList<Long>(limitedMatches.size());
+ for (MatchScore matchScore : limitedMatches) {
+ sortedContactIds.add(matchScore.getContactId());
+ }
+
+ Collections.sort(sortedContactIds);
+
+ // Map cursor indexes according to the descending order of match scores
+ int[] positionMap = new int[limitedMatches.size()];
+ for (int i = 0; i < positionMap.length; i++) {
+ long id = limitedMatches.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.
+ * @param parameters
+ */
+ protected abstract List<MatchScore> findMatchingContacts(final SQLiteDatabase db,
+ long contactId, ArrayList<AggregationSuggestionParameter> parameters);
+
+ public abstract void updateAggregationAfterVisibilityChange(long contactId);
+}
diff --git a/src/com/android/providers/contacts/aggregation/ContactAggregator.java b/src/com/android/providers/contacts/aggregation/ContactAggregator.java
index d030687..48b893c 100644
--- a/src/com/android/providers/contacts/aggregation/ContactAggregator.java
+++ b/src/com/android/providers/contacts/aggregation/ContactAggregator.java
@@ -19,67 +19,32 @@
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.AggregationExceptions;
-import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Identity;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Contacts.AggregationSuggestions;
import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.DisplayNameSources;
-import android.provider.ContactsContract.FullNameStyle;
-import android.provider.ContactsContract.PhotoFiles;
-import android.provider.ContactsContract.PinnedPositions;
import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.StatusUpdates;
import android.text.TextUtils;
-import android.util.EventLog;
import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.providers.contacts.ContactLookupKey;
import com.android.providers.contacts.ContactsDatabaseHelper;
-import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
-import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
-import com.android.providers.contacts.ContactsDatabaseHelper.Views;
import com.android.providers.contacts.ContactsProvider2;
-import com.android.providers.contacts.NameLookupBuilder;
-import com.android.providers.contacts.NameNormalizer;
import com.android.providers.contacts.NameSplitter;
import com.android.providers.contacts.PhotoPriorityResolver;
-import com.android.providers.contacts.ReorderingCursorWrapper;
import com.android.providers.contacts.TransactionContext;
import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
import com.android.providers.contacts.aggregation.util.ContactMatcher;
-import com.android.providers.contacts.aggregation.util.ContactMatcher.MatchScore;
+import com.android.providers.contacts.aggregation.util.MatchScore;
import com.android.providers.contacts.database.ContactsTableUtil;
-import com.android.providers.contacts.util.Clock;
-
-import com.google.android.collect.Maps;
import com.google.android.collect.Sets;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.HashMultimap;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
-import java.util.Locale;
import java.util.Set;
/**
@@ -87,194 +52,14 @@
* Two John Doe contacts from two disjoint sources are presumed to be the same
* person unless the user declares otherwise.
*/
-public class ContactAggregator {
-
- private static final String TAG = "ContactAggregator";
-
- private static final boolean DEBUG_LOGGING = Log.isLoggable(TAG, Log.DEBUG);
- private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
-
- private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL =
- NameLookupColumns.NAME_TYPE + " IN ("
- + NameLookupType.NAME_EXACT + ","
- + NameLookupType.NAME_VARIANT + ","
- + NameLookupType.NAME_COLLATION_KEY + ")";
-
-
- /**
- * SQL statement that sets the {@link ContactsColumns#LAST_STATUS_UPDATE_ID} column
- * on the contact to point to the latest social status update.
- */
- private static final String UPDATE_LAST_STATUS_UPDATE_ID_SQL =
- "UPDATE " + Tables.CONTACTS +
- " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
- "(SELECT " + DataColumns.CONCRETE_ID +
- " FROM " + Tables.STATUS_UPDATES +
- " JOIN " + Tables.DATA +
- " ON (" + StatusUpdatesColumns.DATA_ID + "="
- + DataColumns.CONCRETE_ID + ")" +
- " JOIN " + Tables.RAW_CONTACTS +
- " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "="
- + RawContactsColumns.CONCRETE_ID + ")" +
- " WHERE " + RawContacts.CONTACT_ID + "=?" +
- " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
- + StatusUpdates.STATUS +
- " LIMIT 1)" +
- " WHERE " + ContactsColumns.CONCRETE_ID + "=?";
-
- // From system/core/logcat/event-log-tags
- // aggregator [time, count] will be logged for each aggregator cycle.
- // For the query (as opposed to the merge), count will be negative
- public static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747;
-
- // If we encounter more than this many contacts with matching names, aggregate only this many
- private static final int PRIMARY_HIT_LIMIT = 15;
- private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT);
-
- // If we encounter more than this many contacts with matching phone number or email,
- // don't attempt to aggregate - this is likely an error or a shared corporate data element.
- private static final int SECONDARY_HIT_LIMIT = 20;
- private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
-
- // If we encounter no less than this many raw contacts in the best matching contact during
- // aggregation, don't attempt to aggregate - this is likely an error or a shared corporate
- // data element.
- @VisibleForTesting
- static final int AGGREGATION_CONTACT_SIZE_LIMIT = 50;
-
- // If we encounter more than this many contacts with matching name during aggregation
- // suggestion lookup, ignore the remaining results.
- private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100;
+public class ContactAggregator extends AbstractContactAggregator {
// Return code for the canJoinIntoContact method.
private static final int JOIN = 1;
private static final int KEEP_SEPARATE = 0;
private static final int RE_AGGREGATE = -1;
- private final ContactsProvider2 mContactsProvider;
- private final ContactsDatabaseHelper mDbHelper;
- private PhotoPriorityResolver mPhotoPriorityResolver;
- private final NameSplitter mNameSplitter;
- private final CommonNicknameCache mCommonNicknameCache;
-
- private boolean mEnabled = true;
-
- /** Precompiled sql statement for setting an aggregated presence */
- private SQLiteStatement mAggregatedPresenceReplace;
- private SQLiteStatement mPresenceContactIdUpdate;
- private SQLiteStatement mRawContactCountQuery;
- private SQLiteStatement mAggregatedPresenceDelete;
- private SQLiteStatement mMarkForAggregation;
- private SQLiteStatement mPhotoIdUpdate;
- private SQLiteStatement mDisplayNameUpdate;
- private SQLiteStatement mLookupKeyUpdate;
- private SQLiteStatement mStarredUpdate;
- private SQLiteStatement mPinnedUpdate;
- private SQLiteStatement mContactIdAndMarkAggregatedUpdate;
- private SQLiteStatement mContactIdUpdate;
- private SQLiteStatement mMarkAggregatedUpdate;
- private SQLiteStatement mContactUpdate;
- private SQLiteStatement mContactInsert;
- private SQLiteStatement mResetPinnedForRawContact;
-
- private HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap();
-
- private String[] mSelectionArgs1 = new String[1];
- private String[] mSelectionArgs2 = new String[2];
-
- private long mMimeTypeIdIdentity;
- private long mMimeTypeIdEmail;
- private long mMimeTypeIdPhoto;
- private long mMimeTypeIdPhone;
- private String mRawContactsQueryByRawContactId;
- private String mRawContactsQueryByContactId;
- private StringBuilder mSb = new StringBuilder();
- private MatchCandidateList mCandidates = new MatchCandidateList();
- private ContactMatcher mMatcher = new ContactMatcher();
- private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
-
- /**
- * Parameter for the suggestion lookup query.
- */
- public static final class AggregationSuggestionParameter {
- public final String kind;
- public final String value;
-
- public AggregationSuggestionParameter(String kind, String value) {
- this.kind = kind;
- this.value = value;
- }
- }
-
- /**
- * 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;
- }
-
- public boolean isEmpty() {
- return mCount == 0;
- }
- }
-
- /**
- * A convenience class used in the algorithm that figures out which of available
- * display names to use for an aggregate contact.
- */
- private static class DisplayNameCandidate {
- long rawContactId;
- String displayName;
- int displayNameSource;
- boolean verified;
- boolean writableAccount;
-
- public DisplayNameCandidate() {
- clear();
- }
-
- public void clear() {
- rawContactId = -1;
- displayName = null;
- displayNameSource = DisplayNameSources.UNDEFINED;
- verified = false;
- writableAccount = false;
- }
- }
+ private final ContactMatcher mMatcher = new ContactMatcher();
/**
* Constructor.
@@ -283,456 +68,17 @@
ContactsDatabaseHelper contactsDatabaseHelper,
PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter,
CommonNicknameCache commonNicknameCache) {
- mContactsProvider = contactsProvider;
- mDbHelper = contactsDatabaseHelper;
- mPhotoPriorityResolver = photoPriorityResolver;
- mNameSplitter = nameSplitter;
- mCommonNicknameCache = commonNicknameCache;
-
- SQLiteDatabase db = mDbHelper.getReadableDatabase();
-
- // Since we have no way of determining which custom status was set last,
- // we'll just pick one randomly. We are using MAX as an approximation of randomness
- final String replaceAggregatePresenceSql =
- "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
- + AggregatedPresenceColumns.CONTACT_ID + ", "
- + StatusUpdates.PRESENCE + ", "
- + StatusUpdates.CHAT_CAPABILITY + ")"
- + " SELECT " + PresenceColumns.CONTACT_ID + ","
- + StatusUpdates.PRESENCE + ","
- + StatusUpdates.CHAT_CAPABILITY
- + " FROM " + Tables.PRESENCE
- + " WHERE "
- + " (" + StatusUpdates.PRESENCE
- + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
- + " = (SELECT "
- + "MAX (" + StatusUpdates.PRESENCE
- + " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
- + " FROM " + Tables.PRESENCE
- + " WHERE " + PresenceColumns.CONTACT_ID
- + "=?)"
- + " AND " + PresenceColumns.CONTACT_ID
- + "=?;";
- mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql);
-
- mRawContactCountQuery = db.compileStatement(
- "SELECT COUNT(" + RawContacts._ID + ")" +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts.CONTACT_ID + "=?"
- + " AND " + RawContacts._ID + "<>?");
-
- mAggregatedPresenceDelete = db.compileStatement(
- "DELETE FROM " + Tables.AGGREGATED_PRESENCE +
- " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?");
-
- mMarkForAggregation = db.compileStatement(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" +
- " WHERE " + RawContacts._ID + "=?"
- + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0");
-
- mPhotoIdUpdate = db.compileStatement(
- "UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.PHOTO_ID + "=?," + Contacts.PHOTO_FILE_ID + "=? " +
- " WHERE " + Contacts._ID + "=?");
-
- mDisplayNameUpdate = db.compileStatement(
- "UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
- " WHERE " + Contacts._ID + "=?");
-
- mLookupKeyUpdate = db.compileStatement(
- "UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.LOOKUP_KEY + "=? " +
- " WHERE " + Contacts._ID + "=?");
-
- mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
- + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED
- + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE "
- + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND "
- + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?");
-
- mPinnedUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
- + Contacts.PINNED + " = IFNULL((SELECT MIN(" + RawContacts.PINNED + ") FROM "
- + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
- + ContactsColumns.CONCRETE_ID + " AND " + RawContacts.PINNED + ">"
- + PinnedPositions.UNPINNED + ")," + PinnedPositions.UNPINNED + ") "
- + "WHERE " + Contacts._ID + "=?");
-
- mContactIdAndMarkAggregatedUpdate = db.compileStatement(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContacts.CONTACT_ID + "=?, "
- + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
- " WHERE " + RawContacts._ID + "=?");
-
- mContactIdUpdate = db.compileStatement(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContacts.CONTACT_ID + "=?" +
- " WHERE " + RawContacts._ID + "=?");
-
- mMarkAggregatedUpdate = db.compileStatement(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
- " WHERE " + RawContacts._ID + "=?");
-
- mPresenceContactIdUpdate = db.compileStatement(
- "UPDATE " + Tables.PRESENCE +
- " SET " + PresenceColumns.CONTACT_ID + "=?" +
- " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
-
- mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
- mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
-
- mResetPinnedForRawContact = db.compileStatement(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContacts.PINNED + "=" + PinnedPositions.UNPINNED +
- " WHERE " + RawContacts._ID + "=?");
-
- mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
- mMimeTypeIdIdentity = mDbHelper.getMimeTypeId(Identity.CONTENT_ITEM_TYPE);
- mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
- mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
-
- // Query used to retrieve data from raw contacts to populate the corresponding aggregate
- mRawContactsQueryByRawContactId = String.format(Locale.US,
- RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
- mMimeTypeIdPhoto, mMimeTypeIdPhone);
-
- mRawContactsQueryByContactId = String.format(Locale.US,
- RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
- mMimeTypeIdPhoto, mMimeTypeIdPhone);
+ super(contactsProvider, contactsDatabaseHelper, photoPriorityResolver, nameSplitter,
+ commonNicknameCache);
}
- public void setEnabled(boolean enabled) {
- mEnabled = enabled;
- }
-
- public boolean isEnabled() {
- return mEnabled;
- }
-
- private interface AggregationQuery {
- String SQL =
- "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
- ", " + RawContactsColumns.ACCOUNT_ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts._ID + " IN(";
-
- int _ID = 0;
- int CONTACT_ID = 1;
- int ACCOUNT_ID = 2;
- }
-
- /**
- * Aggregate all raw contacts that were marked for aggregation in the current transaction.
- * Call just before committing the transaction.
- */
- public void aggregateInTransaction(TransactionContext txContext, SQLiteDatabase db) {
- final int markedCount = mRawContactsMarkedForAggregation.size();
- if (markedCount == 0) {
- return;
- }
-
- final long start = System.currentTimeMillis();
- if (DEBUG_LOGGING) {
- Log.d(TAG, "aggregateInTransaction for " + markedCount + " contacts");
- }
-
- EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -markedCount);
-
- int index = 0;
-
- // We don't use the cached string builder (namely mSb) here, as this string can be very
- // long when upgrading (where we re-aggregate all visible contacts) and StringBuilder won't
- // shrink the internal storage.
- // Note: don't use selection args here. We just include all IDs directly in the selection,
- // because there's a limit for the number of parameters in a query.
- final StringBuilder sbQuery = new StringBuilder();
- sbQuery.append(AggregationQuery.SQL);
- for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) {
- if (index > 0) {
- sbQuery.append(',');
- }
- sbQuery.append(rawContactId);
- index++;
- }
-
- sbQuery.append(')');
-
- final long[] rawContactIds;
- final long[] contactIds;
- final long[] accountIds;
- final int actualCount;
- final Cursor c = db.rawQuery(sbQuery.toString(), null);
- try {
- actualCount = c.getCount();
- rawContactIds = new long[actualCount];
- contactIds = new long[actualCount];
- accountIds = new long[actualCount];
-
- index = 0;
- while (c.moveToNext()) {
- rawContactIds[index] = c.getLong(AggregationQuery._ID);
- contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
- accountIds[index] = c.getLong(AggregationQuery.ACCOUNT_ID);
- index++;
- }
- } finally {
- c.close();
- }
-
- if (DEBUG_LOGGING) {
- Log.d(TAG, "aggregateInTransaction: initial query done.");
- }
-
- for (int i = 0; i < actualCount; i++) {
- aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i],
- mCandidates, mMatcher);
- }
-
- long elapsedTime = System.currentTimeMillis() - start;
- EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, actualCount);
-
- if (DEBUG_LOGGING) {
- Log.d(TAG, "Contact aggregation complete: " + actualCount +
- (actualCount == 0 ? "" : ", " + (elapsedTime / actualCount)
- + " ms per raw contact"));
- }
- }
-
- @SuppressWarnings("deprecation")
- public void triggerAggregation(TransactionContext txContext, long rawContactId) {
- if (!mEnabled) {
- return;
- }
-
- int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
- switch (aggregationMode) {
- case RawContacts.AGGREGATION_MODE_DISABLED:
- break;
-
- case RawContacts.AGGREGATION_MODE_DEFAULT: {
- markForAggregation(rawContactId, aggregationMode, false);
- break;
- }
-
- case RawContacts.AGGREGATION_MODE_SUSPENDED: {
- long contactId = mDbHelper.getContactId(rawContactId);
-
- if (contactId != 0) {
- updateAggregateData(txContext, contactId);
- }
- break;
- }
-
- case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
- aggregateContact(txContext, mDbHelper.getWritableDatabase(), rawContactId);
- break;
- }
- }
- }
-
- public void clearPendingAggregations() {
- // HashMap woulnd't shrink the internal table once expands it, so let's just re-create
- // a new one instead of clear()ing it.
- mRawContactsMarkedForAggregation = Maps.newHashMap();
- }
-
- public void markNewForAggregation(long rawContactId, int aggregationMode) {
- mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode);
- }
-
- public void markForAggregation(long rawContactId, int aggregationMode, boolean force) {
- final int effectiveAggregationMode;
- if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) {
- // As per ContactsContract documentation, default aggregation mode
- // does not override a previously set mode
- if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
- effectiveAggregationMode = mRawContactsMarkedForAggregation.get(rawContactId);
- } else {
- effectiveAggregationMode = aggregationMode;
- }
- } else {
- mMarkForAggregation.bindLong(1, rawContactId);
- mMarkForAggregation.execute();
- effectiveAggregationMode = aggregationMode;
- }
-
- mRawContactsMarkedForAggregation.put(rawContactId, effectiveAggregationMode);
- }
-
- private static class RawContactIdAndAggregationModeQuery {
- public static final String TABLE = Tables.RAW_CONTACTS;
-
- public static final String[] COLUMNS = { RawContacts._ID, RawContacts.AGGREGATION_MODE };
-
- public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
-
- public static final int _ID = 0;
- public static final int AGGREGATION_MODE = 1;
- }
-
- /**
- * Marks all constituent raw contacts of an aggregated contact for re-aggregation.
- */
- private void markContactForAggregation(SQLiteDatabase db, long contactId) {
- mSelectionArgs1[0] = String.valueOf(contactId);
- Cursor cursor = db.query(RawContactIdAndAggregationModeQuery.TABLE,
- RawContactIdAndAggregationModeQuery.COLUMNS,
- RawContactIdAndAggregationModeQuery.SELECTION, mSelectionArgs1, null, null, null);
- try {
- if (cursor.moveToFirst()) {
- long rawContactId = cursor.getLong(RawContactIdAndAggregationModeQuery._ID);
- int aggregationMode = cursor.getInt(
- RawContactIdAndAggregationModeQuery.AGGREGATION_MODE);
- // Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED.
- // (Also just ignore deprecated AGGREGATION_MODE_IMMEDIATE)
- if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
- markForAggregation(rawContactId, aggregationMode, true);
- }
- }
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Mark all visible contacts for re-aggregation.
- *
- * - Set {@link RawContactsColumns#AGGREGATION_NEEDED} For all visible raw_contacts with
- * {@link RawContacts#AGGREGATION_MODE_DEFAULT}.
- * - Also put them into {@link #mRawContactsMarkedForAggregation}.
- */
- public int markAllVisibleForAggregation(SQLiteDatabase db) {
- final long start = System.currentTimeMillis();
-
- // Set AGGREGATION_NEEDED for all visible raw_cotnacts with AGGREGATION_MODE_DEFAULT.
- // (Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED)
- db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " +
- RawContactsColumns.AGGREGATION_NEEDED + "=1" +
- " WHERE " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY +
- " AND " + RawContacts.AGGREGATION_MODE + "=" + RawContacts.AGGREGATION_MODE_DEFAULT
- );
-
- final int count;
- final Cursor cursor = db.rawQuery("SELECT " + RawContacts._ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1", null);
- try {
- count = cursor.getCount();
- cursor.moveToPosition(-1);
- while (cursor.moveToNext()) {
- final long rawContactId = cursor.getLong(0);
- mRawContactsMarkedForAggregation.put(rawContactId,
- RawContacts.AGGREGATION_MODE_DEFAULT);
- }
- } finally {
- cursor.close();
- }
-
- final long end = System.currentTimeMillis();
- Log.i(TAG, "Marked all visible contacts for aggregation: " + count + " raw contacts, " +
- (end - start) + " ms");
- return count;
- }
-
- /**
- * Creates a new contact based on the given raw contact. Does not perform aggregation. Returns
- * the ID of the contact that was created.
- */
- public long onRawContactInsert(
- TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
- long contactId = insertContact(db, rawContactId);
- setContactId(rawContactId, contactId);
- mDbHelper.updateContactVisible(txContext, contactId);
- return contactId;
- }
-
- protected long insertContact(SQLiteDatabase db, long rawContactId) {
- mSelectionArgs1[0] = String.valueOf(rawContactId);
- computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
- return mContactInsert.executeInsert();
- }
-
- private static final class RawContactIdAndAccountQuery {
- public static final String TABLE = Tables.RAW_CONTACTS;
-
- public static final String[] COLUMNS = {
- RawContacts.CONTACT_ID,
- RawContactsColumns.ACCOUNT_ID
- };
-
- public static final String SELECTION = RawContacts._ID + "=?";
-
- public static final int CONTACT_ID = 0;
- public static final int ACCOUNT_ID = 1;
- }
-
- public void aggregateContact(
- TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
- if (!mEnabled) {
- return;
- }
-
- MatchCandidateList candidates = new MatchCandidateList();
- ContactMatcher matcher = new ContactMatcher();
-
- long contactId = 0;
- long accountId = 0;
- mSelectionArgs1[0] = String.valueOf(rawContactId);
- Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE,
- RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION,
- mSelectionArgs1, null, null, null);
- try {
- if (cursor.moveToFirst()) {
- contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID);
- accountId = cursor.getLong(RawContactIdAndAccountQuery.ACCOUNT_ID);
- }
- } finally {
- cursor.close();
- }
-
- aggregateContact(txContext, db, rawContactId, accountId, contactId,
- candidates, matcher);
- }
-
- public void updateAggregateData(TransactionContext txContext, long contactId) {
- if (!mEnabled) {
- return;
- }
-
- final SQLiteDatabase db = mDbHelper.getWritableDatabase();
- computeAggregateData(db, contactId, mContactUpdate);
- mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
- mContactUpdate.execute();
-
- mDbHelper.updateContactVisible(txContext, contactId);
- updateAggregatedStatusUpdate(contactId);
- }
-
- private void updateAggregatedStatusUpdate(long contactId) {
- mAggregatedPresenceReplace.bindLong(1, contactId);
- mAggregatedPresenceReplace.bindLong(2, contactId);
- mAggregatedPresenceReplace.execute();
- updateLastStatusUpdateId(contactId);
- }
-
- /**
- * Adjusts the reference to the latest status update for the specified contact.
- */
- public void updateLastStatusUpdateId(long contactId) {
- String contactIdString = String.valueOf(contactId);
- mDbHelper.getWritableDatabase().execSQL(UPDATE_LAST_STATUS_UPDATE_ID_SQL,
- new String[]{contactIdString, contactIdString});
- }
-
- /**
+ /**
* 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.
*/
- private synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
- long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates,
- ContactMatcher matcher) {
+ synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
+ long rawContactId, long accountId, long currentContactId,
+ MatchCandidateList candidates) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId);
@@ -748,6 +94,7 @@
long contactId = -1; // Best matching contact ID.
boolean needReaggregate = false;
+ final ContactMatcher matcher = new ContactMatcher();
final Set<Long> rawContactIdsInSameAccount = new HashSet<Long>();
final Set<Long> rawContactIdsInOtherAccount = new HashSet<Long>();
if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
@@ -901,8 +248,8 @@
private void clearSuperPrimarySetting(SQLiteDatabase db, long contactId, long rawContactId) {
final String[] args = {String.valueOf(contactId), String.valueOf(rawContactId)};
- // Find out which mime-types are shared by raw contact of rawContactId and raw contacts
- // of contactId
+ // Find out which mime-types exist with is_super_primary=true on both the raw contact of
+ // rawContactId and raw contacts of contactId
int index = 0;
final StringBuilder mimeTypeCondition = new StringBuilder();
mimeTypeCondition.append(" AND " + DataColumns.MIMETYPE_ID + " IN (");
@@ -910,10 +257,12 @@
final Cursor c = db.rawQuery(
"SELECT DISTINCT(a." + DataColumns.MIMETYPE_ID + ")" +
" FROM (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE " +
+ Data.IS_SUPER_PRIMARY + " =1 AND " +
Data.RAW_CONTACT_ID + " IN (SELECT " + RawContacts._ID + " FROM " +
Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=?1)) AS a" +
- " JOIN (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE "
- + Data.RAW_CONTACT_ID + "=?2) AS b" +
+ " JOIN (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE " +
+ Data.IS_SUPER_PRIMARY + " =1 AND " +
+ Data.RAW_CONTACT_ID + "=?2) AS b" +
" ON a." + DataColumns.MIMETYPE_ID + "=b." + DataColumns.MIMETYPE_ID,
args);
try {
@@ -933,8 +282,8 @@
return;
}
- // Clear is_super_primary setting for all the mime-types exist in both raw contact
- // of rawContactId and raw contacts of contactId
+ // Clear is_super_primary setting for all the mime-types with is_super_primary=true
+ // in both raw contact of rawContactId and raw contacts of contactId
String superPrimaryUpdateSql = "UPDATE " + Tables.DATA +
" SET " + Data.IS_SUPER_PRIMARY + "=0" +
" WHERE (" + Data.RAW_CONTACT_ID +
@@ -1014,87 +363,6 @@
}
}
- private interface RawContactMatchingSelectionStatement {
- String SELECT_COUNT = "SELECT count(*) " ;
- String SELECT_ID = "SELECT d1." + Data.RAW_CONTACT_ID + ",d2." + Data.RAW_CONTACT_ID ;
- }
-
- /**
- * Build sql to check if there is any identity match/mis-match between two sets of raw contact
- * ids on the same namespace.
- */
- private String buildIdentityMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
- boolean isIdentityMatching, boolean countOnly) {
- final String identityType = String.valueOf(mMimeTypeIdIdentity);
- final String matchingOperator = (isIdentityMatching) ? "=" : "!=";
- final String sql =
- " FROM " + Tables.DATA + " AS d1" +
- " JOIN " + Tables.DATA + " AS d2" +
- " ON (d1." + Identity.IDENTITY + matchingOperator +
- " d2." + Identity.IDENTITY + " AND" +
- " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" +
- " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + identityType +
- " AND d2." + DataColumns.MIMETYPE_ID + " = " + identityType +
- " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
- " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
- return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
- RawContactMatchingSelectionStatement.SELECT_ID + sql;
- }
-
- private String buildEmailMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
- boolean countOnly) {
- final String emailType = String.valueOf(mMimeTypeIdEmail);
- final String sql =
- " FROM " + Tables.DATA + " AS d1" +
- " JOIN " + Tables.DATA + " AS d2" +
- " ON lower(d1." + Email.ADDRESS + ")= lower(d2." + Email.ADDRESS + ")" +
- " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + emailType +
- " AND d2." + DataColumns.MIMETYPE_ID + " = " + emailType +
- " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
- " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
- return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
- RawContactMatchingSelectionStatement.SELECT_ID + sql;
- }
-
- private String buildPhoneMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
- boolean countOnly) {
- // It's a bit tricker because it has to be consistent with
- // updateMatchScoresBasedOnPhoneMatches().
- final String phoneType = String.valueOf(mMimeTypeIdPhone);
- final String sql =
- " FROM " + Tables.PHONE_LOOKUP + " AS p1" +
- " JOIN " + Tables.DATA + " AS d1 ON " +
- "(d1." + Data._ID + "=p1." + PhoneLookupColumns.DATA_ID + ")" +
- " JOIN " + Tables.PHONE_LOOKUP + " AS p2 ON (p1." + PhoneLookupColumns.MIN_MATCH +
- "=p2." + PhoneLookupColumns.MIN_MATCH + ")" +
- " JOIN " + Tables.DATA + " AS d2 ON " +
- "(d2." + Data._ID + "=p2." + PhoneLookupColumns.DATA_ID + ")" +
- " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + phoneType +
- " AND d2." + DataColumns.MIMETYPE_ID + " = " + phoneType +
- " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
- " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")" +
- " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + "," +
- String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter()) +
- ")";
- return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
- RawContactMatchingSelectionStatement.SELECT_ID + sql;
- }
-
- private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2) {
- return "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " +
- AggregationExceptions.RAW_CONTACT_ID2 +
- " FROM " + Tables.AGGREGATION_EXCEPTIONS +
- " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" +
- rawContactIdSet1 + ")" +
- " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" +
- " AND " + AggregationExceptions.TYPE + "=" +
- AggregationExceptions.TYPE_KEEP_TOGETHER ;
- }
-
- private boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query) {
- return DatabaseUtils.longForQuery(db, query, null) > 0;
- }
-
/**
* If there's any identity, email address or a phone number matching between two raw contact
* sets.
@@ -1188,112 +456,6 @@
}
/**
- * Partition the given raw contact Ids to connected component based on aggregation exception,
- * identity matching, email matching or phone matching.
- */
- private Set<Set<Long>> findConnectedRawContacts(SQLiteDatabase db, Set<Long> rawContactIdSet) {
- // Connections between two raw contacts
- final Multimap<Long, Long> matchingRawIdPairs = HashMultimap.create();
- String rawContactIds = TextUtils.join(",", rawContactIdSet);
- findIdPairs(db, buildExceptionMatchingSql(rawContactIds, rawContactIds),
- matchingRawIdPairs);
- findIdPairs(db, buildIdentityMatchingSql(rawContactIds, rawContactIds,
- /* isIdentityMatching =*/ true, /* countOnly =*/false), matchingRawIdPairs);
- findIdPairs(db, buildEmailMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false),
- matchingRawIdPairs);
- findIdPairs(db, buildPhoneMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false),
- matchingRawIdPairs);
-
- return findConnectedComponents(rawContactIdSet, matchingRawIdPairs);
- }
-
- /**
- * Given a set of raw contact ids {@code rawContactIdSet} and the connection among them
- * {@code matchingRawIdPairs}, find the connected components.
- */
- @VisibleForTesting
- static Set<Set<Long>> findConnectedComponents(Set<Long> rawContactIdSet, Multimap<Long,
- Long> matchingRawIdPairs) {
- Set<Set<Long>> connectedRawContactSets = new HashSet<Set<Long>>();
- Set<Long> visited = new HashSet<Long>();
- for (Long id : rawContactIdSet) {
- if (!visited.contains(id)) {
- Set<Long> set = new HashSet<Long>();
- findConnectedComponentForRawContact(matchingRawIdPairs, visited, id, set);
- connectedRawContactSets.add(set);
- }
- }
- return connectedRawContactSets;
- }
-
- private static void findConnectedComponentForRawContact(Multimap<Long, Long> connections,
- Set<Long> visited, Long rawContactId, Set<Long> results) {
- visited.add(rawContactId);
- results.add(rawContactId);
- for (long match : connections.get(rawContactId)) {
- if (!visited.contains(match)) {
- findConnectedComponentForRawContact(connections, visited, match, results);
- }
- }
- }
-
- /**
- * Given a query which will return two non-null IDs in the first two columns as results, this
- * method will put two entries into the given result map for each pair of different IDs, one
- * keyed by each ID.
- */
- private void findIdPairs(SQLiteDatabase db, String query, Multimap<Long, Long> results) {
- Cursor cursor = db.rawQuery(query, null);
- try {
- cursor.moveToPosition(-1);
- while (cursor.moveToNext()) {
- long idA = cursor.getLong(0);
- long idB = cursor.getLong(1);
- if (idA != idB) {
- results.put(idA, idB);
- results.put(idB, idA);
- }
- }
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Creates a new Contact for a given set of the raw contacts of {@code rawContactIds} if the
- * given contactId is null. Otherwise, regroup them into contact with {@code contactId}.
- */
- private void createContactForRawContacts(SQLiteDatabase db, TransactionContext txContext,
- Set<Long> rawContactIds, Long contactId) {
- if (rawContactIds.isEmpty()) {
- // No raw contact id is provided.
- return;
- }
-
- // If contactId is not provided, generates a new one.
- if (contactId == null) {
- mSelectionArgs1[0]= String.valueOf(rawContactIds.iterator().next());
- computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
- mContactInsert);
- contactId = mContactInsert.executeInsert();
- }
- for (Long rawContactId : rawContactIds) {
- // Regrouped contacts should automatically be unpinned.
- unpinRawContact(rawContactId);
- setContactIdAndMarkAggregated(rawContactId, contactId);
- setPresenceContactId(rawContactId, contactId);
- }
- updateAggregateData(txContext, contactId);
- }
-
- private static class RawContactIdQuery {
- public static final String TABLE = Tables.RAW_CONTACTS;
- public static final String[] COLUMNS = { RawContacts._ID };
- public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
- public static final int RAW_CONTACT_ID = 0;
- }
-
- /**
* Ensures that automatic aggregation rules are followed after a contact
* becomes visible or invisible. Specifically, consider this case: there are
* three contacts named Foo. Two of them come from account A1 and one comes
@@ -1349,23 +511,6 @@
}
/**
- * Updates the contact ID for the specified contact.
- */
- protected void setContactId(long rawContactId, long contactId) {
- mContactIdUpdate.bindLong(1, contactId);
- mContactIdUpdate.bindLong(2, rawContactId);
- mContactIdUpdate.execute();
- }
-
- /**
- * Marks the specified raw contact ID as aggregated
- */
- private void markAggregated(long rawContactId) {
- mMarkAggregatedUpdate.bindLong(1, rawContactId);
- mMarkAggregatedUpdate.execute();
- }
-
- /**
* Updates the contact ID for the specified contact and marks the raw contact as aggregated.
*/
private void setContactIdAndMarkAggregated(long rawContactId, long contactId) {
@@ -1374,63 +519,6 @@
mContactIdAndMarkAggregatedUpdate.execute();
}
- private void setPresenceContactId(long rawContactId, long contactId) {
- mPresenceContactIdUpdate.bindLong(1, contactId);
- mPresenceContactIdUpdate.bindLong(2, rawContactId);
- mPresenceContactIdUpdate.execute();
- }
-
- private void unpinRawContact(long rawContactId) {
- mResetPinnedForRawContact.bindLong(1, rawContactId);
- mResetPinnedForRawContact.execute();
- }
-
- interface AggregateExceptionPrefetchQuery {
- String TABLE = Tables.AGGREGATION_EXCEPTIONS;
-
- String[] COLUMNS = {
- AggregationExceptions.RAW_CONTACT_ID1,
- AggregationExceptions.RAW_CONTACT_ID2,
- };
-
- int RAW_CONTACT_ID1 = 0;
- int RAW_CONTACT_ID2 = 1;
- }
-
- // A set of raw contact IDs for which there are aggregation exceptions
- private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>();
- private boolean mAggregationExceptionIdsValid;
-
- public void invalidateAggregationExceptionCache() {
- mAggregationExceptionIdsValid = false;
- }
-
- /**
- * Finds all raw contact IDs for which there are aggregation exceptions. The list of
- * ids is used as an optimization in aggregation: there is no point to run a query against
- * the agg_exceptions table if it is known that there are no records there for a given
- * raw contact ID.
- */
- private void prefetchAggregationExceptionIds(SQLiteDatabase db) {
- mAggregationExceptionIds.clear();
- final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE,
- AggregateExceptionPrefetchQuery.COLUMNS,
- null, null, null, null, null);
-
- try {
- while (c.moveToNext()) {
- long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1);
- long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2);
- mAggregationExceptionIds.add(rawContactId1);
- mAggregationExceptionIds.add(rawContactId2);
- }
- } finally {
- c.close();
- }
-
- mAggregationExceptionIdsValid = true;
- }
-
interface AggregateExceptionQuery {
String TABLE = Tables.AGGREGATION_EXCEPTIONS
+ " JOIN raw_contacts raw_contacts1 "
@@ -1505,7 +593,7 @@
c.close();
}
- return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true);
+ return matcher.pickBestMatch(MatchScore.MAX_SCORE, true);
}
/**
@@ -1576,42 +664,6 @@
return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY, false);
}
- private interface NameLookupQuery {
- String TABLE = Tables.NAME_LOOKUP;
-
- String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?";
- String SELECTION_STRUCTURED_NAME_BASED =
- SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL;
-
- String[] COLUMNS = new String[] {
- NameLookupColumns.NORMALIZED_NAME,
- NameLookupColumns.NAME_TYPE
- };
-
- int NORMALIZED_NAME = 0;
- int NAME_TYPE = 1;
- }
-
- private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId,
- MatchCandidateList candidates, boolean structuredNameBased) {
- candidates.clear();
- mSelectionArgs1[0] = String.valueOf(rawContactId);
- Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
- structuredNameBased
- ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED
- : NameLookupQuery.SELECTION,
- mSelectionArgs1, null, null, null);
- try {
- while (c.moveToNext()) {
- String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
- int type = c.getInt(NameLookupQuery.NAME_TYPE);
- candidates.add(normalizedName, type);
- }
- } finally {
- c.close();
- }
- }
-
/**
* Computes scores for contacts that have matching data rows.
*/
@@ -1729,124 +781,6 @@
}
}
- private interface NameLookupMatchQueryWithParameter {
- String TABLE = Tables.NAME_LOOKUP
- + " JOIN " + Tables.RAW_CONTACTS +
- " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = "
- + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
-
- String[] COLUMNS = new String[] {
- RawContacts.CONTACT_ID,
- NameLookupColumns.NORMALIZED_NAME,
- NameLookupColumns.NAME_TYPE,
- };
-
- int CONTACT_ID = 0;
- int NAME = 1;
- int NAME_TYPE = 2;
- }
-
- private final class NameLookupSelectionBuilder extends NameLookupBuilder {
-
- private final MatchCandidateList mNameLookupCandidates;
-
- private StringBuilder mSelection = new StringBuilder(
- NameLookupColumns.NORMALIZED_NAME + " IN(");
-
-
- public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) {
- super(splitter);
- this.mNameLookupCandidates = candidates;
- }
-
- @Override
- protected String[] getCommonNicknameClusters(String normalizedName) {
- return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
- }
-
- @Override
- protected void insertNameLookup(
- long rawContactId, long dataId, int lookupType, String string) {
- mNameLookupCandidates.add(string, lookupType);
- DatabaseUtils.appendEscapedSQLString(mSelection, string);
- mSelection.append(',');
- }
-
- public boolean isEmpty() {
- return mNameLookupCandidates.isEmpty();
- }
-
- public String getSelection() {
- mSelection.setLength(mSelection.length() - 1); // Strip last comma
- mSelection.append(')');
- return mSelection.toString();
- }
-
- public int getLookupType(String name) {
- for (int i = 0; i < mNameLookupCandidates.mCount; i++) {
- if (mNameLookupCandidates.mList.get(i).mName.equals(name)) {
- return mNameLookupCandidates.mList.get(i).mLookupType;
- }
- }
- throw new IllegalStateException();
- }
- }
-
- /**
- * Finds contacts with names matching the specified name.
- */
- private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query,
- MatchCandidateList candidates, ContactMatcher matcher) {
- candidates.clear();
- NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder(
- mNameSplitter, candidates);
- builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED);
- if (builder.isEmpty()) {
- return;
- }
-
- Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE,
- NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null,
- null, PRIMARY_HIT_LIMIT_STRING);
- try {
- while (c.moveToNext()) {
- long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID);
- String name = c.getString(NameLookupMatchQueryWithParameter.NAME);
- int nameTypeA = builder.getLookupType(name);
- int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE);
- matcher.matchName(contactId, nameTypeA, name, nameTypeB, name,
- ContactMatcher.MATCHING_ALGORITHM_EXACT);
- if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) {
- matcher.updateScoreWithNicknameMatch(contactId);
- }
- }
- } finally {
- c.close();
- }
- }
-
- private interface EmailLookupQuery {
- String TABLE = Tables.DATA + " dataA"
- + " JOIN " + Tables.DATA + " dataB" +
- " ON lower(" + "dataA." + Email.DATA + ")=lower(dataB." + Email.DATA + ")"
- + " JOIN " + Tables.RAW_CONTACTS +
- " ON (dataB." + Data.RAW_CONTACT_ID + " = "
- + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
-
- String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
- + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
- + " AND dataA." + Email.DATA + " NOT NULL"
- + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
- + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
- + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
-
- String[] COLUMNS = new String[] {
- RawContacts.CONTACT_ID
- };
-
- int CONTACT_ID = 0;
- }
-
private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
ContactMatcher matcher) {
mSelectionArgs2[0] = String.valueOf(rawContactId);
@@ -1864,32 +798,6 @@
}
}
- private interface PhoneLookupQuery {
- String TABLE = Tables.PHONE_LOOKUP + " phoneA"
- + " JOIN " + Tables.DATA + " dataA"
- + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
- + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
- + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
- + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
- + " JOIN " + Tables.DATA + " dataB"
- + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
- + " JOIN " + Tables.RAW_CONTACTS
- + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
- + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
-
- String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
- + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
- + "dataB." + Phone.NUMBER + ",?)"
- + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
- + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
-
- String[] COLUMNS = new String[] {
- RawContacts.CONTACT_ID
- };
-
- int CONTACT_ID = 0;
- }
-
private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
ContactMatcher matcher) {
mSelectionArgs2[0] = String.valueOf(rawContactId);
@@ -1978,787 +886,12 @@
}
}
- private interface RawContactsQuery {
- String SQL_FORMAT =
- "SELECT "
- + RawContactsColumns.CONCRETE_ID + ","
- + RawContactsColumns.DISPLAY_NAME + ","
- + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
- + AccountsColumns.CONCRETE_ACCOUNT_TYPE + ","
- + AccountsColumns.CONCRETE_ACCOUNT_NAME + ","
- + AccountsColumns.CONCRETE_DATA_SET + ","
- + RawContacts.SOURCE_ID + ","
- + RawContacts.CUSTOM_RINGTONE + ","
- + RawContacts.SEND_TO_VOICEMAIL + ","
- + RawContacts.LAST_TIME_CONTACTED + ","
- + RawContacts.TIMES_CONTACTED + ","
- + RawContacts.STARRED + ","
- + RawContacts.PINNED + ","
- + RawContacts.NAME_VERIFIED + ","
- + DataColumns.CONCRETE_ID + ","
- + DataColumns.CONCRETE_MIMETYPE_ID + ","
- + Data.IS_SUPER_PRIMARY + ","
- + Photo.PHOTO_FILE_ID +
- " FROM " + Tables.RAW_CONTACTS +
- " JOIN " + Tables.ACCOUNTS + " ON ("
- + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
- + ")" +
- " LEFT OUTER JOIN " + Tables.DATA +
- " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
- + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
- + " AND " + Photo.PHOTO + " NOT NULL)"
- + " OR (" + DataColumns.MIMETYPE_ID + "=%d"
- + " AND " + Phone.NUMBER + " NOT NULL)))";
-
- String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT +
- " WHERE " + RawContactsColumns.CONCRETE_ID + "=?";
-
- String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT +
- " WHERE " + RawContacts.CONTACT_ID + "=?"
- + " AND " + RawContacts.DELETED + "=0";
-
- int RAW_CONTACT_ID = 0;
- int DISPLAY_NAME = 1;
- int DISPLAY_NAME_SOURCE = 2;
- int ACCOUNT_TYPE = 3;
- int ACCOUNT_NAME = 4;
- int DATA_SET = 5;
- int SOURCE_ID = 6;
- int CUSTOM_RINGTONE = 7;
- int SEND_TO_VOICEMAIL = 8;
- int LAST_TIME_CONTACTED = 9;
- int TIMES_CONTACTED = 10;
- int STARRED = 11;
- int PINNED = 12;
- int NAME_VERIFIED = 13;
- int DATA_ID = 14;
- int MIMETYPE_ID = 15;
- int IS_SUPER_PRIMARY = 16;
- int PHOTO_FILE_ID = 17;
- }
-
- private interface ContactReplaceSqlStatement {
- String UPDATE_SQL =
- "UPDATE " + Tables.CONTACTS +
- " SET "
- + Contacts.NAME_RAW_CONTACT_ID + "=?, "
- + Contacts.PHOTO_ID + "=?, "
- + Contacts.PHOTO_FILE_ID + "=?, "
- + Contacts.SEND_TO_VOICEMAIL + "=?, "
- + Contacts.CUSTOM_RINGTONE + "=?, "
- + Contacts.LAST_TIME_CONTACTED + "=?, "
- + Contacts.TIMES_CONTACTED + "=?, "
- + Contacts.STARRED + "=?, "
- + Contacts.PINNED + "=?, "
- + Contacts.HAS_PHONE_NUMBER + "=?, "
- + Contacts.LOOKUP_KEY + "=?, "
- + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=? " +
- " WHERE " + Contacts._ID + "=?";
-
- String INSERT_SQL =
- "INSERT INTO " + Tables.CONTACTS + " ("
- + Contacts.NAME_RAW_CONTACT_ID + ", "
- + Contacts.PHOTO_ID + ", "
- + Contacts.PHOTO_FILE_ID + ", "
- + Contacts.SEND_TO_VOICEMAIL + ", "
- + Contacts.CUSTOM_RINGTONE + ", "
- + Contacts.LAST_TIME_CONTACTED + ", "
- + Contacts.TIMES_CONTACTED + ", "
- + Contacts.STARRED + ", "
- + Contacts.PINNED + ", "
- + Contacts.HAS_PHONE_NUMBER + ", "
- + Contacts.LOOKUP_KEY + ", "
- + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
- + ") " +
- " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";
-
- int NAME_RAW_CONTACT_ID = 1;
- int PHOTO_ID = 2;
- int PHOTO_FILE_ID = 3;
- int SEND_TO_VOICEMAIL = 4;
- int CUSTOM_RINGTONE = 5;
- int LAST_TIME_CONTACTED = 6;
- int TIMES_CONTACTED = 7;
- int STARRED = 8;
- int PINNED = 9;
- int HAS_PHONE_NUMBER = 10;
- int LOOKUP_KEY = 11;
- int CONTACT_LAST_UPDATED_TIMESTAMP = 12;
- int CONTACT_ID = 13;
- }
-
- /**
- * Computes aggregate-level data for the specified aggregate contact ID.
- */
- private void computeAggregateData(SQLiteDatabase db, long contactId,
- SQLiteStatement statement) {
- mSelectionArgs1[0] = String.valueOf(contactId);
- computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
- }
-
- /**
- * Indicates whether the given photo entry and priority gives this photo a higher overall
- * priority than the current best photo entry and priority.
- */
- private boolean hasHigherPhotoPriority(PhotoEntry photoEntry, int priority,
- PhotoEntry bestPhotoEntry, int bestPriority) {
- int photoComparison = photoEntry.compareTo(bestPhotoEntry);
- return photoComparison < 0 || photoComparison == 0 && priority > bestPriority;
- }
-
- /**
- * Computes aggregate-level data from constituent raw contacts.
- */
- private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
- SQLiteStatement statement) {
- long currentRawContactId = -1;
- long bestPhotoId = -1;
- long bestPhotoFileId = 0;
- PhotoEntry bestPhotoEntry = null;
- boolean foundSuperPrimaryPhoto = false;
- int photoPriority = -1;
- int totalRowCount = 0;
- int contactSendToVoicemail = 0;
- String contactCustomRingtone = null;
- long contactLastTimeContacted = 0;
- int contactTimesContacted = 0;
- int contactStarred = 0;
- int contactPinned = Integer.MAX_VALUE;
- int hasPhoneNumber = 0;
- StringBuilder lookupKey = new StringBuilder();
-
- mDisplayNameCandidate.clear();
-
- Cursor c = db.rawQuery(sql, sqlArgs);
- try {
- while (c.moveToNext()) {
- long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
- if (rawContactId != currentRawContactId) {
- currentRawContactId = rawContactId;
- totalRowCount++;
-
- // Assemble sub-account.
- String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
- String dataSet = c.getString(RawContactsQuery.DATA_SET);
- String accountWithDataSet = (!TextUtils.isEmpty(dataSet))
- ? accountType + "/" + dataSet
- : accountType;
-
- // Display name
- String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
- int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
- int nameVerified = c.getInt(RawContactsQuery.NAME_VERIFIED);
- processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
- mContactsProvider.isWritableAccountWithDataSet(accountWithDataSet),
- nameVerified != 0);
-
- // Contact options
- if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
- boolean sendToVoicemail =
- (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0);
- if (sendToVoicemail) {
- contactSendToVoicemail++;
- }
- }
-
- if (contactCustomRingtone == null
- && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) {
- contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
- }
-
- long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED);
- if (lastTimeContacted > contactLastTimeContacted) {
- contactLastTimeContacted = lastTimeContacted;
- }
-
- int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED);
- if (timesContacted > contactTimesContacted) {
- contactTimesContacted = timesContacted;
- }
-
- if (c.getInt(RawContactsQuery.STARRED) != 0) {
- contactStarred = 1;
- }
-
- // contactPinned should be the lowest value of its constituent raw contacts,
- // excluding negative integers
- final int rawContactPinned = c.getInt(RawContactsQuery.PINNED);
- if (rawContactPinned > PinnedPositions.UNPINNED) {
- contactPinned = Math.min(contactPinned, rawContactPinned);
- }
-
- appendLookupKey(
- lookupKey,
- accountWithDataSet,
- c.getString(RawContactsQuery.ACCOUNT_NAME),
- rawContactId,
- c.getString(RawContactsQuery.SOURCE_ID),
- displayName);
- }
-
- if (!c.isNull(RawContactsQuery.DATA_ID)) {
- long dataId = c.getLong(RawContactsQuery.DATA_ID);
- long photoFileId = c.getLong(RawContactsQuery.PHOTO_FILE_ID);
- int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
- boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
- if (mimetypeId == mMimeTypeIdPhoto) {
- if (!foundSuperPrimaryPhoto) {
- // Lookup the metadata for the photo, if available. Note that data set
- // does not come into play here, since accounts are looked up in the
- // account manager in the priority resolver.
- PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
- String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
- int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
- if (superPrimary || hasHigherPhotoPriority(
- photoEntry, priority, bestPhotoEntry, photoPriority)) {
- bestPhotoEntry = photoEntry;
- photoPriority = priority;
- bestPhotoId = dataId;
- bestPhotoFileId = photoFileId;
- foundSuperPrimaryPhoto |= superPrimary;
- }
- }
- } else if (mimetypeId == mMimeTypeIdPhone) {
- hasPhoneNumber = 1;
- }
- }
- }
- } finally {
- c.close();
- }
-
- if (contactPinned == Integer.MAX_VALUE) {
- contactPinned = PinnedPositions.UNPINNED;
- }
-
- statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID,
- mDisplayNameCandidate.rawContactId);
-
- if (bestPhotoId != -1) {
- statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
- } else {
- statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
- }
-
- if (bestPhotoFileId != 0) {
- statement.bindLong(ContactReplaceSqlStatement.PHOTO_FILE_ID, bestPhotoFileId);
- } else {
- statement.bindNull(ContactReplaceSqlStatement.PHOTO_FILE_ID);
- }
-
- statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL,
- totalRowCount == contactSendToVoicemail ? 1 : 0);
- DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
- contactCustomRingtone);
- statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
- contactLastTimeContacted);
- statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
- contactTimesContacted);
- statement.bindLong(ContactReplaceSqlStatement.STARRED,
- contactStarred);
- statement.bindLong(ContactReplaceSqlStatement.PINNED,
- contactPinned);
- statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER,
- hasPhoneNumber);
- statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
- Uri.encode(lookupKey.toString()));
- statement.bindLong(ContactReplaceSqlStatement.CONTACT_LAST_UPDATED_TIMESTAMP,
- Clock.getInstance().currentTimeMillis());
- }
-
- /**
- * Builds a lookup key using the given data.
- */
- protected void appendLookupKey(StringBuilder sb, String accountTypeWithDataSet,
- String accountName, long rawContactId, String sourceId, String displayName) {
- ContactLookupKey.appendToLookupKey(sb, accountTypeWithDataSet, accountName, rawContactId,
- sourceId, displayName);
- }
-
- /**
- * Uses the supplied values to determine if they represent a "better" display name
- * for the aggregate contact currently evaluated. If so, it updates
- * {@link #mDisplayNameCandidate} with the new values.
- */
- private void processDisplayNameCandidate(long rawContactId, String displayName,
- int displayNameSource, boolean writableAccount, boolean verified) {
-
- boolean replace = false;
- if (mDisplayNameCandidate.rawContactId == -1) {
- // No previous values available
- replace = true;
- } else if (!TextUtils.isEmpty(displayName)) {
- if (!mDisplayNameCandidate.verified && verified) {
- // A verified name is better than any other name
- replace = true;
- } else if (mDisplayNameCandidate.verified == verified) {
- if (mDisplayNameCandidate.displayNameSource < displayNameSource) {
- // New values come from an superior source, e.g. structured name vs phone number
- replace = true;
- } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) {
- if (!mDisplayNameCandidate.writableAccount && writableAccount) {
- replace = true;
- } else if (mDisplayNameCandidate.writableAccount == writableAccount) {
- if (NameNormalizer.compareComplexity(displayName,
- mDisplayNameCandidate.displayName) > 0) {
- // New name is more complex than the previously found one
- replace = true;
- }
- }
- }
- }
- }
-
- if (replace) {
- mDisplayNameCandidate.rawContactId = rawContactId;
- mDisplayNameCandidate.displayName = displayName;
- mDisplayNameCandidate.displayNameSource = displayNameSource;
- mDisplayNameCandidate.verified = verified;
- mDisplayNameCandidate.writableAccount = writableAccount;
- }
- }
-
- private interface PhotoIdQuery {
- final String[] COLUMNS = new String[] {
- AccountsColumns.CONCRETE_ACCOUNT_TYPE,
- DataColumns.CONCRETE_ID,
- Data.IS_SUPER_PRIMARY,
- Photo.PHOTO_FILE_ID,
- };
-
- int ACCOUNT_TYPE = 0;
- int DATA_ID = 1;
- int IS_SUPER_PRIMARY = 2;
- int PHOTO_FILE_ID = 3;
- }
-
- public void updatePhotoId(SQLiteDatabase db, long rawContactId) {
-
- long contactId = mDbHelper.getContactId(rawContactId);
- if (contactId == 0) {
- return;
- }
-
- long bestPhotoId = -1;
- long bestPhotoFileId = 0;
- int photoPriority = -1;
-
- long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
-
- String tables = Tables.RAW_CONTACTS
- + " JOIN " + Tables.ACCOUNTS + " ON ("
- + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
- + ")"
- + " JOIN " + Tables.DATA + " ON("
- + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
- + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
- + Photo.PHOTO + " NOT NULL))";
-
- mSelectionArgs1[0] = String.valueOf(contactId);
- final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS,
- RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
- try {
- PhotoEntry bestPhotoEntry = null;
- while (c.moveToNext()) {
- long dataId = c.getLong(PhotoIdQuery.DATA_ID);
- long photoFileId = c.getLong(PhotoIdQuery.PHOTO_FILE_ID);
- boolean superPrimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0;
- PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
-
- // Note that data set does not come into play here, since accounts are looked up in
- // the account manager in the priority resolver.
- String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE);
- int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
- if (superPrimary || hasHigherPhotoPriority(
- photoEntry, priority, bestPhotoEntry, photoPriority)) {
- bestPhotoEntry = photoEntry;
- photoPriority = priority;
- bestPhotoId = dataId;
- bestPhotoFileId = photoFileId;
- if (superPrimary) {
- break;
- }
- }
- }
- } finally {
- c.close();
- }
-
- if (bestPhotoId == -1) {
- mPhotoIdUpdate.bindNull(1);
- } else {
- mPhotoIdUpdate.bindLong(1, bestPhotoId);
- }
-
- if (bestPhotoFileId == 0) {
- mPhotoIdUpdate.bindNull(2);
- } else {
- mPhotoIdUpdate.bindLong(2, bestPhotoFileId);
- }
-
- mPhotoIdUpdate.bindLong(3, contactId);
- mPhotoIdUpdate.execute();
- }
-
- private interface PhotoFileQuery {
- final String[] COLUMNS = new String[] {
- PhotoFiles.HEIGHT,
- PhotoFiles.WIDTH,
- PhotoFiles.FILESIZE
- };
-
- int HEIGHT = 0;
- int WIDTH = 1;
- int FILESIZE = 2;
- }
-
- private class PhotoEntry implements Comparable<PhotoEntry> {
- // Pixel count (width * height) for the image.
- final int pixelCount;
-
- // File size (in bytes) of the image. Not populated if the image is a thumbnail.
- final int fileSize;
-
- private PhotoEntry(int pixelCount, int fileSize) {
- this.pixelCount = pixelCount;
- this.fileSize = fileSize;
- }
-
- @Override
- public int compareTo(PhotoEntry pe) {
- if (pe == null) {
- return -1;
- }
- if (pixelCount == pe.pixelCount) {
- return pe.fileSize - fileSize;
- } else {
- return pe.pixelCount - pixelCount;
- }
- }
- }
-
- private PhotoEntry getPhotoMetadata(SQLiteDatabase db, long photoFileId) {
- if (photoFileId == 0) {
- // Assume standard thumbnail size. Don't bother getting a file size for priority;
- // we should fall back to photo priority resolver if all we have are thumbnails.
- int thumbDim = mContactsProvider.getMaxThumbnailDim();
- return new PhotoEntry(thumbDim * thumbDim, 0);
- } else {
- Cursor c = db.query(Tables.PHOTO_FILES, PhotoFileQuery.COLUMNS, PhotoFiles._ID + "=?",
- new String[]{String.valueOf(photoFileId)}, null, null, null);
- try {
- if (c.getCount() == 1) {
- c.moveToFirst();
- int pixelCount =
- c.getInt(PhotoFileQuery.HEIGHT) * c.getInt(PhotoFileQuery.WIDTH);
- return new PhotoEntry(pixelCount, c.getInt(PhotoFileQuery.FILESIZE));
- }
- } finally {
- c.close();
- }
- }
- return new PhotoEntry(0, 0);
- }
-
- private interface DisplayNameQuery {
- String[] COLUMNS = new String[] {
- RawContacts._ID,
- RawContactsColumns.DISPLAY_NAME,
- RawContactsColumns.DISPLAY_NAME_SOURCE,
- RawContacts.NAME_VERIFIED,
- RawContacts.SOURCE_ID,
- RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
- };
-
- int _ID = 0;
- int DISPLAY_NAME = 1;
- int DISPLAY_NAME_SOURCE = 2;
- int NAME_VERIFIED = 3;
- int SOURCE_ID = 4;
- int ACCOUNT_TYPE_AND_DATA_SET = 5;
- }
-
- public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
- long contactId = mDbHelper.getContactId(rawContactId);
- if (contactId == 0) {
- return;
- }
-
- updateDisplayNameForContact(db, contactId);
- }
-
- public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
- boolean lookupKeyUpdateNeeded = false;
-
- mDisplayNameCandidate.clear();
-
- mSelectionArgs1[0] = String.valueOf(contactId);
- final Cursor c = db.query(Views.RAW_CONTACTS, DisplayNameQuery.COLUMNS,
- RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
- try {
- while (c.moveToNext()) {
- long rawContactId = c.getLong(DisplayNameQuery._ID);
- String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
- int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
- int nameVerified = c.getInt(DisplayNameQuery.NAME_VERIFIED);
- String accountTypeAndDataSet = c.getString(
- DisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET);
- processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
- mContactsProvider.isWritableAccountWithDataSet(accountTypeAndDataSet),
- nameVerified != 0);
-
- // If the raw contact has no source id, the lookup key is based on the display
- // name, so the lookup key needs to be updated.
- lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID);
- }
- } finally {
- c.close();
- }
-
- if (mDisplayNameCandidate.rawContactId != -1) {
- mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId);
- mDisplayNameUpdate.bindLong(2, contactId);
- mDisplayNameUpdate.execute();
- }
-
- if (lookupKeyUpdateNeeded) {
- updateLookupKeyForContact(db, contactId);
- }
- }
-
-
- /**
- * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the
- * specified raw contact.
- */
- public void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) {
-
- long contactId = mDbHelper.getContactId(rawContactId);
- if (contactId == 0) {
- return;
- }
-
- final SQLiteStatement hasPhoneNumberUpdate = db.compileStatement(
- "UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.HAS_PHONE_NUMBER + "="
- + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)"
- + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS
- + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
- + " AND " + Phone.NUMBER + " NOT NULL"
- + " AND " + RawContacts.CONTACT_ID + "=?)" +
- " WHERE " + Contacts._ID + "=?");
- try {
- hasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE));
- hasPhoneNumberUpdate.bindLong(2, contactId);
- hasPhoneNumberUpdate.bindLong(3, contactId);
- hasPhoneNumberUpdate.execute();
- } finally {
- hasPhoneNumberUpdate.close();
- }
- }
-
- private interface LookupKeyQuery {
- String TABLE = Views.RAW_CONTACTS;
- String[] COLUMNS = new String[] {
- RawContacts._ID,
- RawContactsColumns.DISPLAY_NAME,
- RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
- RawContacts.ACCOUNT_NAME,
- RawContacts.SOURCE_ID,
- };
-
- int ID = 0;
- int DISPLAY_NAME = 1;
- int ACCOUNT_TYPE_AND_DATA_SET = 2;
- int ACCOUNT_NAME = 3;
- int SOURCE_ID = 4;
- }
-
- public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
- long contactId = mDbHelper.getContactId(rawContactId);
- if (contactId == 0) {
- return;
- }
-
- updateLookupKeyForContact(db, contactId);
- }
-
- private void updateLookupKeyForContact(SQLiteDatabase db, long contactId) {
- String lookupKey = computeLookupKeyForContact(db, contactId);
-
- if (lookupKey == null) {
- mLookupKeyUpdate.bindNull(1);
- } else {
- mLookupKeyUpdate.bindString(1, Uri.encode(lookupKey));
- }
- mLookupKeyUpdate.bindLong(2, contactId);
-
- mLookupKeyUpdate.execute();
- }
-
- protected String computeLookupKeyForContact(SQLiteDatabase db, long contactId) {
- StringBuilder sb = new StringBuilder();
- mSelectionArgs1[0] = String.valueOf(contactId);
- final Cursor c = db.query(LookupKeyQuery.TABLE, LookupKeyQuery.COLUMNS,
- RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID);
- try {
- while (c.moveToNext()) {
- ContactLookupKey.appendToLookupKey(sb,
- c.getString(LookupKeyQuery.ACCOUNT_TYPE_AND_DATA_SET),
- c.getString(LookupKeyQuery.ACCOUNT_NAME),
- c.getLong(LookupKeyQuery.ID),
- c.getString(LookupKeyQuery.SOURCE_ID),
- c.getString(LookupKeyQuery.DISPLAY_NAME));
- }
- } finally {
- c.close();
- }
- return sb.length() == 0 ? null : sb.toString();
- }
-
- /**
- * Execute {@link SQLiteStatement} that will update the
- * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}.
- */
- public void updateStarred(long rawContactId) {
- long contactId = mDbHelper.getContactId(rawContactId);
- if (contactId == 0) {
- return;
- }
-
- mStarredUpdate.bindLong(1, contactId);
- mStarredUpdate.execute();
- }
-
- /**
- * Execute {@link SQLiteStatement} that will update the
- * {@link Contacts#PINNED} flag for the given {@link RawContacts#_ID}.
- */
- public void updatePinned(long rawContactId) {
- long contactId = mDbHelper.getContactId(rawContactId);
- if (contactId == 0) {
- return;
- }
- mPinnedUpdate.bindLong(1, contactId);
- mPinnedUpdate.execute();
- }
-
- /**
- * Finds matching contacts and returns a cursor on those.
- */
- public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb,
- String[] projection, long contactId, int maxSuggestions, String filter,
- ArrayList<AggregationSuggestionParameter> parameters) {
- final SQLiteDatabase db = mDbHelper.getReadableDatabase();
- db.beginTransaction();
- try {
- List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters);
- return queryMatchingContacts(qb, db, projection, bestMatches, maxSuggestions, filter);
- } finally {
- db.endTransaction();
- }
- }
-
- private interface ContactIdQuery {
- String[] COLUMNS = new String[] {
- Contacts._ID
- };
-
- int _ID = 0;
- }
-
- /**
- * Loads contacts with specified IDs and returns them in the order of IDs in the
- * supplied list.
- */
- private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db,
- String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) {
- StringBuilder sb = new StringBuilder();
- sb.append(Contacts._ID);
- sb.append(" IN (");
- for (int i = 0; i < bestMatches.size(); i++) {
- MatchScore matchScore = bestMatches.get(i);
- if (i != 0) {
- sb.append(",");
- }
- sb.append(matchScore.getContactId());
- }
- sb.append(")");
-
- if (!TextUtils.isEmpty(filter)) {
- sb.append(" AND " + Contacts._ID + " IN ");
- mContactsProvider.appendContactFilterAsNestedQuery(sb, filter);
- }
-
- // Run a query and find ids of best matching contacts satisfying the filter (if any)
- HashSet<Long> foundIds = new HashSet<Long>();
- Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(),
- null, null, null, null);
- try {
- while(cursor.moveToNext()) {
- foundIds.add(cursor.getLong(ContactIdQuery._ID));
- }
- } finally {
- cursor.close();
- }
-
- // Exclude all contacts that did not match the filter
- Iterator<MatchScore> iter = bestMatches.iterator();
- while (iter.hasNext()) {
- long id = iter.next().getContactId();
- if (!foundIds.contains(id)) {
- iter.remove();
- }
- }
-
- // Limit the number of returned suggestions
- final List<MatchScore> limitedMatches;
- if (bestMatches.size() > maxSuggestions) {
- limitedMatches = bestMatches.subList(0, maxSuggestions);
- } else {
- limitedMatches = bestMatches;
- }
-
- // Build an in-clause with the remaining contact IDs
- sb.setLength(0);
- sb.append(Contacts._ID);
- sb.append(" IN (");
- for (int i = 0; i < limitedMatches.size(); i++) {
- MatchScore matchScore = limitedMatches.get(i);
- if (i != 0) {
- sb.append(",");
- }
- sb.append(matchScore.getContactId());
- }
- sb.append(")");
-
- // Run the final query with the required projection and contact IDs found by the first query
- cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID);
-
- // Build a sorted list of discovered IDs
- ArrayList<Long> sortedContactIds = new ArrayList<Long>(limitedMatches.size());
- for (MatchScore matchScore : limitedMatches) {
- sortedContactIds.add(matchScore.getContactId());
- }
-
- Collections.sort(sortedContactIds);
-
- // Map cursor indexes according to the descending order of match scores
- int[] positionMap = new int[limitedMatches.size()];
- for (int i = 0; i < positionMap.length; i++) {
- long id = limitedMatches.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.
* @param parameters
*/
- private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId,
+ protected List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId,
ArrayList<AggregationSuggestionParameter> parameters) {
MatchCandidateList candidates = new MatchCandidateList();
diff --git a/src/com/android/providers/contacts/aggregation/ContactAggregator2.java b/src/com/android/providers/contacts/aggregation/ContactAggregator2.java
new file mode 100644
index 0000000..d24b930
--- /dev/null
+++ b/src/com/android/providers/contacts/aggregation/ContactAggregator2.java
@@ -0,0 +1,899 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.aggregation;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhotoFiles;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.providers.contacts.ContactsDatabaseHelper;
+import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.ContactsProvider2;
+import com.android.providers.contacts.NameSplitter;
+import com.android.providers.contacts.PhotoPriorityResolver;
+import com.android.providers.contacts.TransactionContext;
+import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
+import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper;
+import com.android.providers.contacts.aggregation.util.MatchScore;
+import com.android.providers.contacts.aggregation.util.RawContactMatcher;
+import com.android.providers.contacts.aggregation.util.RawContactMatchingCandidates;
+import com.android.providers.contacts.database.ContactsTableUtil;
+import com.google.android.collect.Sets;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.android.providers.contacts.aggregation.util.RawContactMatcher
+ .SCORE_THRESHOLD_PRIMARY;
+import static com.android.providers.contacts.aggregation.util.RawContactMatcher
+ .SCORE_THRESHOLD_SECONDARY;
+import static com.android.providers.contacts.aggregation.util.RawContactMatcher
+ .SCORE_THRESHOLD_SUGGEST;
+
+/**
+ * ContactAggregator2 deals with aggregating contact information with sufficient matching data
+ * points. E.g., two John Doe contacts with same phone numbers are presumed to be the same
+ * person unless the user declares otherwise.
+ */
+public class ContactAggregator2 extends AbstractContactAggregator {
+
+ // Possible operation types for contacts aggregation.
+ private static final int CREATE_NEW_CONTACT = 1;
+ private static final int KEEP_INTACT = 0;
+ private static final int RE_AGGREGATE = -1;
+
+ private final RawContactMatcher mMatcher = new RawContactMatcher();
+
+ /**
+ * Constructor.
+ */
+ public ContactAggregator2(ContactsProvider2 contactsProvider,
+ ContactsDatabaseHelper contactsDatabaseHelper,
+ PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter,
+ CommonNicknameCache commonNicknameCache) {
+ super(contactsProvider, contactsDatabaseHelper, photoPriorityResolver, nameSplitter,
+ commonNicknameCache);
+ }
+
+ private static class RawContactIdAndAggregationModeQuery {
+ public static final String TABLE = Tables.RAW_CONTACTS;
+
+ public static final String[] COLUMNS = { RawContacts._ID, RawContacts.AGGREGATION_MODE };
+
+ public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
+
+ public static final int _ID = 0;
+ public static final int AGGREGATION_MODE = 1;
+ }
+
+ /**
+ * Given a specific raw contact, finds all matching raw contacts and re-aggregate them
+ * based on the matching connectivity.
+ */
+ synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
+ long rawContactId, long accountId, long currentContactId,
+ MatchCandidateList candidates) {
+
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId);
+ }
+
+ int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
+
+ Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId);
+ if (aggModeObject != null) {
+ aggregationMode = aggModeObject;
+ }
+
+ RawContactMatcher matcher = new RawContactMatcher();
+ RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates();
+ if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
+ // If this is a newly inserted contact or a visible contact, look for
+ // data matches.
+ if (currentContactId == 0
+ || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) {
+ // Find the set of matching candidates
+ matchingCandidates = findRawContactMatchingCandidates(db, rawContactId, candidates,
+ matcher);
+ }
+ } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
+ return;
+ }
+
+ // # of raw_contacts in the [currentContactId] contact excluding the [rawContactId]
+ // raw_contact.
+ long currentContactContentsCount = 0;
+
+ if (currentContactId != 0) {
+ mRawContactCountQuery.bindLong(1, currentContactId);
+ mRawContactCountQuery.bindLong(2, rawContactId);
+ currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong();
+ }
+
+ // Set aggregation operation, i.e., re-aggregate, keep intact, or create new contact based
+ // on the number of matching candidates and the number of raw_contacts in the
+ // [currentContactId] excluding the [rawContactId].
+ final int operation;
+ final int candidatesCount = matchingCandidates.getCount();
+ if (candidatesCount >= AGGREGATION_CONTACT_SIZE_LIMIT) {
+ operation = KEEP_INTACT;
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Too many matching raw contacts (" + candidatesCount
+ + ") are found, so skip aggregation");
+ }
+ } else if (candidatesCount > 0) {
+ operation = RE_AGGREGATE;
+ } else {
+ // When there is no matching raw contact found, if there are no other raw contacts in
+ // the current aggregate, we might as well reuse it. Also, if the aggregation mode is
+ // SUSPENDED, we must reuse the same aggregate.
+ if (currentContactId != 0
+ && (currentContactContentsCount == 0
+ || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) {
+ operation = KEEP_INTACT;
+ } else {
+ operation = CREATE_NEW_CONTACT;
+ }
+ }
+
+ if (operation == KEEP_INTACT) {
+ // Aggregation unchanged
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Aggregation unchanged");
+ }
+ markAggregated(rawContactId);
+ } else if (operation == CREATE_NEW_CONTACT) {
+ // create new contact for [rawContactId]
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "create new contact for rid=" + rawContactId);
+ }
+ createContactForRawContacts(db, txContext, Sets.newHashSet(rawContactId), null);
+ if (currentContactContentsCount > 0) {
+ updateAggregateData(txContext, currentContactId);
+ }
+ } else {
+ // re-aggregate
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Re-aggregating rids=" + rawContactId + ","
+ + TextUtils.join(",", matchingCandidates.getRawContactIdSet()));
+ }
+ reAggregateRawContacts(txContext, db, currentContactId, rawContactId, accountId,
+ currentContactContentsCount, matchingCandidates);
+ }
+ }
+
+ /**
+ * Find the set of matching raw contacts for given rawContactId. Add all the raw contact
+ * candidates with matching scores > threshold to RawContactMatchingCandidates. Keep doing
+ * this for every raw contact in RawContactMatchingCandidates until is it not changing.
+ */
+ private RawContactMatchingCandidates findRawContactMatchingCandidates(SQLiteDatabase db, long
+ rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
+ updateMatchScores(db, rawContactId, candidates,
+ matcher);
+ final RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates(
+ matcher.pickBestMatches());
+ Set<Long> newIds = new HashSet<>();
+ newIds.addAll(matchingCandidates.getRawContactIdSet());
+ // Keep doing the following until no new raw contact candidate is found.
+ // TODO: may need to cache the matching score to improve performance.
+ while (!newIds.isEmpty()) {
+ final Set<Long> tmpIdSet = new HashSet<>();
+ for (long rId : newIds) {
+ final RawContactMatcher rMatcher = new RawContactMatcher();
+ updateMatchScores(db, rId, new MatchCandidateList(),
+ rMatcher);
+ List<MatchScore> newMatches = rMatcher.pickBestMatches();
+ for (MatchScore newMatch : newMatches) {
+ final long newRawContactId = newMatch.getRawContactId();
+ if (!matchingCandidates.getRawContactIdSet().contains(newRawContactId)) {
+ tmpIdSet.add(newRawContactId);
+ matchingCandidates.add(newMatch);
+ }
+ }
+ }
+ newIds.clear();
+ newIds.addAll(tmpIdSet);
+ }
+ return matchingCandidates;
+ }
+
+ /**
+ * Find out which mime-types are shared by more than one contacts for {@code rawContactIds}.
+ * Clear the is_super_primary settings for these mime-types.
+ * {@code rawContactIds} should be a comma separated ID list.
+ */
+ private void clearSuperPrimarySetting(SQLiteDatabase db, String rawContactIds) {
+ final String sql =
+ "SELECT " + DataColumns.MIMETYPE_ID + ", count(1) c FROM " +
+ Tables.DATA +" WHERE " + Data.IS_SUPER_PRIMARY + " = 1 AND " +
+ Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ") group by " +
+ DataColumns.MIMETYPE_ID + " HAVING c > 1";
+
+ // Find out which mime-types exist with is_super_primary=true on more then one contacts.
+ int index = 0;
+ final StringBuilder mimeTypeCondition = new StringBuilder();
+ mimeTypeCondition.append(" AND " + DataColumns.MIMETYPE_ID + " IN (");
+
+ final Cursor c = db.rawQuery(sql, null);
+ try {
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ if (index > 0) {
+ mimeTypeCondition.append(',');
+ }
+ mimeTypeCondition.append(c.getLong((0)));
+ index++;
+ }
+ } finally {
+ c.close();
+ }
+
+ if (index == 0) {
+ return;
+ }
+
+ // Clear is_super_primary setting for all the mime-types with is_super_primary=true
+ // in both raw contact of rawContactId and raw contacts of contactId
+ String superPrimaryUpdateSql = "UPDATE " + Tables.DATA +
+ " SET " + Data.IS_SUPER_PRIMARY + "=0" +
+ " WHERE " + Data.RAW_CONTACT_ID +
+ " IN (" + rawContactIds + ")";
+
+ mimeTypeCondition.append(')');
+ superPrimaryUpdateSql += mimeTypeCondition.toString();
+ db.execSQL(superPrimaryUpdateSql);
+ }
+
+ private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
+ int aggregationType, boolean countOnly) {
+ final String idPairSelection = "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " +
+ AggregationExceptions.RAW_CONTACT_ID2;
+ final String sql =
+ " FROM " + Tables.AGGREGATION_EXCEPTIONS +
+ " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" +
+ rawContactIdSet1 + ")" +
+ " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" +
+ " AND " + AggregationExceptions.TYPE + "=" + aggregationType;
+ return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
+ idPairSelection + sql;
+ }
+
+ /**
+ * Re-aggregate rawContact of {@code rawContactId} and all the raw contacts of
+ * {@code matchingCandidates} into connected components. This only happens when a given
+ * raw contacts cannot be joined with its best matching contacts directly.
+ *
+ * Two raw contacts are considered connected if they share at least one email address, phone
+ * number or identity. Create new contact for each connected component except the very first
+ * one that doesn't contain rawContactId of {@code rawContactId}.
+ */
+ private void reAggregateRawContacts(TransactionContext txContext, SQLiteDatabase db,
+ long currentCidForRawContact, long rawContactId, long accountId,
+ long currentContactContentsCount, RawContactMatchingCandidates matchingCandidates) {
+ // Find the connected component based on the aggregation exceptions or
+ // identity/email/phone matching for all the raw contacts of [contactId] and the give
+ // raw contact.
+ final Set<Long> allIds = new HashSet<>();
+ allIds.add(rawContactId);
+ allIds.addAll(matchingCandidates.getRawContactIdSet());
+ final Set<Set<Long>> connectedRawContactSets = findConnectedRawContacts(db, allIds);
+
+ final Map<Long, Long> rawContactsToAccounts = matchingCandidates.getRawContactToAccount();
+ rawContactsToAccounts.put(rawContactId, accountId);
+ ContactAggregatorHelper.mergeComponentsWithDisjointAccounts(connectedRawContactSets,
+ rawContactsToAccounts);
+ breakComponentsByExceptions(db, connectedRawContactSets);
+
+ // Create new contact for each connected component. Use the first reusable contactId if
+ // possible. If no reusable contactId found, create new contact for the connected component.
+ // Update aggregate data for all the contactIds touched by this connected component,
+ for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
+ Long contactId = null;
+ Set<Long> cidsNeedToBeUpdated = new HashSet<>();
+ if (connectedRawContactIds.contains(rawContactId)) {
+ // If there is no other raw contacts aggregated with the given raw contact currently
+ // or all the raw contacts in [currentCidForRawContact] are still in the same
+ // connected component, we might as well reuse it.
+ if (currentCidForRawContact != 0 &&
+ (currentContactContentsCount == 0) ||
+ canBeReused(db, currentCidForRawContact, connectedRawContactIds)) {
+ contactId = currentCidForRawContact;
+ for (Long connectedRawContactId : connectedRawContactIds) {
+ Long cid = matchingCandidates.getContactId(connectedRawContactId);
+ if (cid != null && cid != contactId) {
+ cidsNeedToBeUpdated.add(cid);
+ }
+ }
+ } else if (currentCidForRawContact != 0){
+ cidsNeedToBeUpdated.add(currentCidForRawContact);
+ }
+ } else {
+ boolean foundContactId = false;
+ for (Long connectedRawContactId : connectedRawContactIds) {
+ Long currentContactId = matchingCandidates.getContactId(connectedRawContactId);
+ if (!foundContactId && currentContactId != null &&
+ canBeReused(db, currentContactId, connectedRawContactIds)) {
+ contactId = currentContactId;
+ foundContactId = true;
+ } else {
+ cidsNeedToBeUpdated.add(currentContactId);
+ }
+ }
+ }
+ clearSuperPrimarySetting(db, TextUtils.join(",", connectedRawContactIds));
+ createContactForRawContacts(db, txContext, connectedRawContactIds, contactId);
+
+ for (Long cid : cidsNeedToBeUpdated) {
+ long currentRcCount = 0;
+ if (cid != 0) {
+ mRawContactCountQuery.bindLong(1, cid);
+ mRawContactCountQuery.bindLong(2, 0);
+ currentRcCount = mRawContactCountQuery.simpleQueryForLong();
+ }
+
+ if (currentRcCount == 0) {
+ // Delete a contact if it doesn't contain anything
+ ContactsTableUtil.deleteContact(db, cid);
+ mAggregatedPresenceDelete.bindLong(1, cid);
+ mAggregatedPresenceDelete.execute();
+ } else {
+ updateAggregateData(txContext, cid);
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if contactId can be reused as the contact Id for new aggregation of all the
+ * connectedRawContactIds. If connectedRawContactIds set contains all the raw contacts
+ * currently aggregated under contactId, return true; Otherwise, return false.
+ */
+ private boolean canBeReused(SQLiteDatabase db, Long contactId,
+ Set<Long> connectedRawContactIds) {
+ final String sql = "SELECT " + RawContactsColumns.CONCRETE_ID + " FROM " +
+ Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=? AND " +
+ RawContacts.DELETED + "=0";
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ final Cursor cursor = db.rawQuery(sql, mSelectionArgs1);
+ try {
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ if (!connectedRawContactIds.contains(cursor.getLong(0))) {
+ return false;
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return true;
+ }
+
+ /**
+ * Separate all the raw_contacts which has "SEPARATE" aggregation exception to another
+ * raw_contacts in the same component.
+ */
+ private void breakComponentsByExceptions(SQLiteDatabase db,
+ Set<Set<Long>> connectedRawContacts) {
+ final Set<Set<Long>> tmpSets = new HashSet<>(connectedRawContacts);
+ for (Set<Long> component : tmpSets) {
+ final String rawContacts = TextUtils.join(",", component);
+ // If "SEPARATE" exception is found inside an connected component [component],
+ // remove the [component] from [connectedRawContacts], and create a new connected
+ // component for each raw contact of [component] and add to [connectedRawContacts].
+ if (isFirstColumnGreaterThanZero(db, buildExceptionMatchingSql(rawContacts, rawContacts,
+ AggregationExceptions.TYPE_KEEP_SEPARATE, /* countOnly =*/true))) {
+ connectedRawContacts.remove(component);
+ for (Long rId : component) {
+ final Set<Long> s= new HashSet<>();
+ s.add(rId);
+ connectedRawContacts.add(s);
+ }
+ }
+ }
+ }
+
+ /**
+ * Ensures that automatic aggregation rules are followed after a contact
+ * becomes visible or invisible. Specifically, consider this case: there are
+ * three contacts named Foo. Two of them come from account A1 and one comes
+ * from account A2. The aggregation rules say that in this case none of the
+ * three Foo's should be aggregated: two of them are in the same account, so
+ * they don't get aggregated; the third has two affinities, so it does not
+ * join either of them.
+ * <p>
+ * Consider what happens if one of the "Foo"s from account A1 becomes
+ * invisible. Nothing stands in the way of aggregating the other two
+ * anymore, so they should get joined.
+ * <p>
+ * What if the invisible "Foo" becomes visible after that? We should split the
+ * aggregate between the other two.
+ */
+ public void updateAggregationAfterVisibilityChange(long contactId) {
+ SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ boolean visible = mDbHelper.isContactInDefaultDirectory(db, contactId);
+ if (visible) {
+ markContactForAggregation(db, contactId);
+ } else {
+ // Find all contacts that _could be_ aggregated with this one and
+ // rerun aggregation for all of them
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ Cursor cursor = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
+ RawContactIdQuery.SELECTION, mSelectionArgs1, null, null, null);
+ try {
+ while (cursor.moveToNext()) {
+ long rawContactId = cursor.getLong(RawContactIdQuery.RAW_CONTACT_ID);
+ mMatcher.clear();
+
+ updateMatchScoresBasedOnIdentityMatch(db, rawContactId, mMatcher);
+ updateMatchScoresBasedOnNameMatches(db, rawContactId, mMatcher);
+ List<MatchScore> bestMatches =
+ mMatcher.pickBestMatches(SCORE_THRESHOLD_PRIMARY);
+ for (MatchScore matchScore : bestMatches) {
+ markContactForAggregation(db, matchScore.getContactId());
+ }
+
+ mMatcher.clear();
+ updateMatchScoresBasedOnEmailMatches(db, rawContactId, mMatcher);
+ updateMatchScoresBasedOnPhoneMatches(db, rawContactId, mMatcher);
+ bestMatches =
+ mMatcher.pickBestMatches(SCORE_THRESHOLD_SECONDARY);
+ for (MatchScore matchScore : bestMatches) {
+ markContactForAggregation(db, matchScore.getContactId());
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * Computes match scores based on exceptions entered by the user: always match and never match.
+ */
+ private void updateMatchScoresBasedOnExceptions(SQLiteDatabase db, long rawContactId,
+ RawContactMatcher matcher) {
+ if (!mAggregationExceptionIdsValid) {
+ prefetchAggregationExceptionIds(db);
+ }
+
+ // If there are no aggregation exceptions involving this raw contact, there is no need to
+ // run a query and we can just return -1, which stands for "nothing found"
+ if (!mAggregationExceptionIds.contains(rawContactId)) {
+ return;
+ }
+
+ final Cursor c = db.query(AggregateExceptionQuery.TABLE,
+ AggregateExceptionQuery.COLUMNS,
+ AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId
+ + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId,
+ null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ int type = c.getInt(AggregateExceptionQuery.TYPE);
+ long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
+ long contactId = -1;
+ long rId = -1;
+ long accountId = -1;
+ if (rawContactId == rawContactId1) {
+ if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_2) == 0
+ && !c.isNull(AggregateExceptionQuery.RAW_CONTACT_ID2)) {
+ rId = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID2);
+ contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2);
+ accountId = c.getLong(AggregateExceptionQuery.ACCOUNT_ID2);
+ }
+ } else {
+ if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_1) == 0
+ && !c.isNull(AggregateExceptionQuery.RAW_CONTACT_ID1)) {
+ rId = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
+ contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1);
+ accountId = c.getLong(AggregateExceptionQuery.ACCOUNT_ID1);
+ }
+ }
+ if (rId != -1) {
+ if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) {
+ matcher.keepIn(rId, contactId, accountId);
+ } else {
+ matcher.keepOut(rId, contactId, accountId);
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Finds contacts with exact identity matches to the the specified raw contact.
+ */
+ private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId,
+ RawContactMatcher matcher) {
+ mSelectionArgs2[0] = String.valueOf(rawContactId);
+ mSelectionArgs2[1] = String.valueOf(mMimeTypeIdIdentity);
+ Cursor c = db.query(IdentityLookupMatchQuery.TABLE, IdentityLookupMatchQuery.COLUMNS,
+ IdentityLookupMatchQuery.SELECTION,
+ mSelectionArgs2, RawContacts.CONTACT_ID, null, null);
+ try {
+ while (c.moveToNext()) {
+ final long rId = c.getLong(IdentityLookupMatchQuery.RAW_CONTACT_ID);
+ final long contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID);
+ final long accountId = c.getLong(IdentityLookupMatchQuery.ACCOUNT_ID);
+ matcher.matchIdentity(rId, contactId, accountId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Finds contacts with names matching the name of the specified raw contact.
+ */
+ private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
+ RawContactMatcher matcher) {
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS,
+ NameLookupMatchQuery.SELECTION,
+ mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING);
+ try {
+ while (c.moveToNext()) {
+ long rId = c.getLong(NameLookupMatchQuery.RAW_CONTACT_ID);
+ long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID);
+ long accountId = c.getLong(NameLookupMatchQuery.ACCOUNT_ID);
+ String name = c.getString(NameLookupMatchQuery.NAME);
+ int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A);
+ int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B);
+ matcher.matchName(rId, contactId, accountId, nameTypeA, name,
+ nameTypeB, name, RawContactMatcher.MATCHING_ALGORITHM_EXACT);
+ if (nameTypeA == NameLookupType.NICKNAME &&
+ nameTypeB == NameLookupType.NICKNAME) {
+ matcher.updateScoreWithNicknameMatch(rId, contactId, accountId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
+ RawContactMatcher matcher) {
+ mSelectionArgs2[0] = String.valueOf(rawContactId);
+ mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail);
+ Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
+ EmailLookupQuery.SELECTION,
+ mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
+ try {
+ while (c.moveToNext()) {
+ long rId = c.getLong(EmailLookupQuery.RAW_CONTACT_ID);
+ long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
+ long accountId = c.getLong(EmailLookupQuery.ACCOUNT_ID);
+ matcher.updateScoreWithEmailMatch(rId, contactId, accountId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Finds contacts with names matching the specified name.
+ */
+ private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query,
+ MatchCandidateList candidates, RawContactMatcher matcher) {
+ candidates.clear();
+ NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder(
+ mNameSplitter, candidates);
+ builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED);
+ if (builder.isEmpty()) {
+ return;
+ }
+
+ Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE,
+ NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null,
+ null, PRIMARY_HIT_LIMIT_STRING);
+ try {
+ while (c.moveToNext()) {
+ long rId = c.getLong(NameLookupMatchQueryWithParameter.RAW_CONTACT_ID);
+ long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID);
+ long accountId = c.getLong(NameLookupMatchQueryWithParameter.ACCOUNT_ID);
+ String name = c.getString(NameLookupMatchQueryWithParameter.NAME);
+ int nameTypeA = builder.getLookupType(name);
+ int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE);
+ matcher.matchName(rId, contactId, accountId, nameTypeA, name, nameTypeB, name,
+ RawContactMatcher.MATCHING_ALGORITHM_EXACT);
+ if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) {
+ matcher.updateScoreWithNicknameMatch(rId, contactId, accountId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
+ RawContactMatcher matcher) {
+ mSelectionArgs2[0] = String.valueOf(rawContactId);
+ mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter();
+ Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
+ PhoneLookupQuery.SELECTION,
+ mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
+ try {
+ while (c.moveToNext()) {
+ long rId = c.getLong(PhoneLookupQuery.RAW_CONTACT_ID);
+ long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
+ long accountId = c.getLong(PhoneLookupQuery.ACCOUNT_ID);
+ matcher.updateScoreWithPhoneNumberMatch(rId, contactId, accountId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Loads name lookup rows for approximate name matching and updates match scores based on that
+ * data.
+ */
+ private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
+ RawContactMatcher matcher) {
+ HashSet<String> firstLetters = new HashSet<>();
+ 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 "
+ + "(" + NameLookupColumns.NAME_TYPE + " IN("
+ + NameLookupType.NAME_COLLATION_KEY + ","
+ + NameLookupType.EMAIL_BASED_NICKNAME + ","
+ + NameLookupType.NICKNAME + ")) AND "
+ + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
+ matchAllCandidates(db, selection, candidates, matcher,
+ RawContactMatcher.MATCHING_ALGORITHM_APPROXIMATE,
+ String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT));
+ }
+ }
+ }
+ }
+
+ private interface ContactNameLookupQuery {
+ String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
+
+ String[] COLUMNS = new String[] {
+ RawContacts._ID,
+ RawContacts.CONTACT_ID,
+ RawContactsColumns.ACCOUNT_ID,
+ NameLookupColumns.NORMALIZED_NAME,
+ NameLookupColumns.NAME_TYPE
+ };
+
+ int RAW_CONTACT_ID = 0;
+ int CONTACT_ID = 1;
+ int ACCOUNT_ID = 2;
+ int NORMALIZED_NAME = 3;
+ int NAME_TYPE = 4;
+ }
+
+ /**
+ * 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, RawContactMatcher matcher, int algorithm, String limit) {
+ final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
+ selection, null, null, null, null, limit);
+
+ try {
+ while (c.moveToNext()) {
+ Long rawContactId = c.getLong(ContactNameLookupQuery.RAW_CONTACT_ID);
+ Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
+ Long accountId = c.getLong(ContactNameLookupQuery.ACCOUNT_ID);
+ String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
+ int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
+
+ // 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);
+ matcher.matchName(rawContactId, contactId, accountId, candidate.mLookupType,
+ candidate.mName, nameType, name, algorithm);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ private interface PhotoFileQuery {
+ final String[] COLUMNS = new String[] {
+ PhotoFiles.HEIGHT,
+ PhotoFiles.WIDTH,
+ PhotoFiles.FILESIZE
+ };
+
+ int HEIGHT = 0;
+ int WIDTH = 1;
+ int FILESIZE = 2;
+ }
+
+ private class PhotoEntry implements Comparable<PhotoEntry> {
+ // Pixel count (width * height) for the image.
+ final int pixelCount;
+
+ // File size (in bytes) of the image. Not populated if the image is a thumbnail.
+ final int fileSize;
+
+ private PhotoEntry(int pixelCount, int fileSize) {
+ this.pixelCount = pixelCount;
+ this.fileSize = fileSize;
+ }
+
+ @Override
+ public int compareTo(PhotoEntry pe) {
+ if (pe == null) {
+ return -1;
+ }
+ if (pixelCount == pe.pixelCount) {
+ return pe.fileSize - fileSize;
+ } else {
+ return pe.pixelCount - pixelCount;
+ }
+ }
+ }
+
+ private PhotoEntry getPhotoMetadata(SQLiteDatabase db, long photoFileId) {
+ if (photoFileId == 0) {
+ // Assume standard thumbnail size. Don't bother getting a file size for priority;
+ // we should fall back to photo priority resolver if all we have are thumbnails.
+ int thumbDim = mContactsProvider.getMaxThumbnailDim();
+ return new PhotoEntry(thumbDim * thumbDim, 0);
+ } else {
+ Cursor c = db.query(Tables.PHOTO_FILES, PhotoFileQuery.COLUMNS, PhotoFiles._ID + "=?",
+ new String[]{String.valueOf(photoFileId)}, null, null, null);
+ try {
+ if (c.getCount() == 1) {
+ c.moveToFirst();
+ int pixelCount =
+ c.getInt(PhotoFileQuery.HEIGHT) * c.getInt(PhotoFileQuery.WIDTH);
+ return new PhotoEntry(pixelCount, c.getInt(PhotoFileQuery.FILESIZE));
+ }
+ } finally {
+ c.close();
+ }
+ }
+ return new PhotoEntry(0, 0);
+ }
+ /**
+ * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
+ * descending order of match score.
+ * @param parameters
+ */
+ protected List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId,
+ ArrayList<AggregationSuggestionParameter> parameters) {
+
+ MatchCandidateList candidates = new MatchCandidateList();
+ RawContactMatcher matcher = new RawContactMatcher();
+
+ if (parameters == null || parameters.size() == 0) {
+ final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
+ RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long rawContactId = c.getLong(RawContactIdQuery.RAW_CONTACT_ID);
+ long accountId = c.getLong(RawContactIdQuery.ACCOUNT_ID);
+ // Don't aggregate a contact with its own raw contacts.
+ matcher.keepOut(rawContactId, contactId, accountId);
+ updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
+ matcher);
+ }
+ } finally {
+ c.close();
+ }
+ } else {
+ updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates,
+ matcher, parameters);
+ }
+
+ return matcher.pickBestMatches(SCORE_THRESHOLD_SUGGEST);
+ }
+
+ /**
+ * Computes suggestion scores for contacts that have matching data rows.
+ * Aggregation suggestion doesn't consider aggregation exceptions, but is purely based on the
+ * raw contacts information.
+ */
+ private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
+ long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
+
+ updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
+ updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+ loadNameMatchCandidates(db, rawContactId, candidates, false);
+ lookupApproximateNameMatches(db, candidates, matcher);
+ }
+
+ /**
+ * Computes scores for contacts that have matching data rows.
+ */
+ private void updateMatchScores(SQLiteDatabase db, long rawContactId,
+ MatchCandidateList candidates, RawContactMatcher matcher) {
+ updateMatchScoresBasedOnExceptions(db, rawContactId, matcher);
+ updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
+ updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnSecondaryData(db, rawContactId, candidates, matcher);
+ }
+
+ private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
+ MatchCandidateList candidates, RawContactMatcher matcher,
+ ArrayList<AggregationSuggestionParameter> parameters) {
+ for (AggregationSuggestionParameter parameter : parameters) {
+ if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) {
+ updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher);
+ }
+
+ // TODO: add support for other parameter kinds
+ }
+ }
+
+ /**
+ * Update scores for matches with secondary data matching but insufficient primary scores.
+ * This method loads structured names for all candidate contacts and recomputes match scores
+ * using approximate matching.
+ */
+ private void updateMatchScoresBasedOnSecondaryData(SQLiteDatabase db,
+ long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
+ final List<Long> secondaryRawContactIds = matcher.prepareSecondaryMatchCandidates();
+ if (secondaryRawContactIds == null || secondaryRawContactIds.size() > SECONDARY_HIT_LIMIT) {
+ return;
+ }
+
+ loadNameMatchCandidates(db, rawContactId, candidates, true);
+
+ mSb.setLength(0);
+ mSb.append(RawContacts._ID).append(" IN (");
+ for (int i = 0; i < secondaryRawContactIds.size(); i++) {
+ if (i != 0) {
+ mSb.append(',');
+ }
+ mSb.append(secondaryRawContactIds.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.
+ mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL);
+
+ matchAllCandidates(db, mSb.toString(), candidates, matcher,
+ RawContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
+ }
+}
diff --git a/src/com/android/providers/contacts/aggregation/util/ContactAggregatorHelper.java b/src/com/android/providers/contacts/aggregation/util/ContactAggregatorHelper.java
new file mode 100644
index 0000000..983cd60
--- /dev/null
+++ b/src/com/android/providers/contacts/aggregation/util/ContactAggregatorHelper.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.aggregation.util;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Helper class for contacts aggregation.
+ */
+public class ContactAggregatorHelper {
+
+ private ContactAggregatorHelper() {}
+
+ /**
+ * If two connected components have disjoint accounts, merge them.
+ * If there is any uncertainty, keep them separate.
+ */
+ @VisibleForTesting
+ public static void mergeComponentsWithDisjointAccounts(Set<Set<Long>> connectedRawContactSets,
+ Map<Long, Long> rawContactsToAccounts) {
+ // Index to rawContactIds mapping
+ final Map<Integer, Set<Long>> rawContactIds = new HashMap<>();
+ // AccountId to indices mapping
+ final Map<Long, Set<Integer>> accounts = new HashMap<>();
+
+ int index = 0;
+ for (Set<Long> rIds : connectedRawContactSets) {
+ rawContactIds.put(index, rIds);
+ for (Long rId : rIds) {
+ long acctId = rawContactsToAccounts.get(rId);
+ Set<Integer> s = accounts.get(acctId);
+ if (s == null) {
+ s = new HashSet<Integer>();
+ }
+ s.add(index);
+ accounts.put(acctId, s);
+ }
+ index++;
+ }
+ final Set<Long> mergedSet = new HashSet<>();
+ connectedRawContactSets.clear();
+ for (Long accountId : accounts.keySet()) {
+ final Set<Integer> s = accounts.get(accountId);
+ if (s.size() > 1) {
+ for (Integer i : s) {
+ final Set<Long> rIdSet = rawContactIds.get(i);
+ if (rIdSet != null) {
+ connectedRawContactSets.add(rawContactIds.get(i));
+ rawContactIds.remove(i);
+ }
+ }
+ } else {
+ Set<Long> ids = rawContactIds.get(Iterables.getOnlyElement(s));
+ if (ids != null) {
+ mergedSet.addAll(ids);
+ }
+ }
+ }
+ connectedRawContactSets.add(mergedSet);
+ }
+
+ /**
+ * Given a set of raw contact ids {@code rawContactIdSet} and the connection among them
+ * {@code matchingRawIdPairs}, find the connected components.
+ */
+ @VisibleForTesting
+ public static Set<Set<Long>> findConnectedComponents(Set<Long> rawContactIdSet, Multimap<Long,
+ Long> matchingRawIdPairs) {
+ Set<Set<Long>> connectedRawContactSets = new HashSet<>();
+ Set<Long> visited = new HashSet<>();
+ for (Long id : rawContactIdSet) {
+ if (!visited.contains(id)) {
+ Set<Long> set = new HashSet<>();
+ findConnectedComponentForRawContact(matchingRawIdPairs, visited, id, set);
+ connectedRawContactSets.add(set);
+ }
+ }
+ return connectedRawContactSets;
+ }
+
+ private static void findConnectedComponentForRawContact(Multimap<Long, Long> connections,
+ Set<Long> visited, Long rawContactId, Set<Long> results) {
+ visited.add(rawContactId);
+ results.add(rawContactId);
+ for (long match : connections.get(rawContactId)) {
+ if (!visited.contains(match)) {
+ findConnectedComponentForRawContact(connections, visited, match, results);
+ }
+ }
+ }
+}
diff --git a/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java b/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java
index 2e552e9..9b71651 100644
--- a/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java
+++ b/src/com/android/providers/contacts/aggregation/util/ContactMatcher.java
@@ -31,9 +31,6 @@
public class ContactMatcher {
private static final String TAG = "ContactMatcher";
- // Best possible match score
- public static final int MAX_SCORE = 100;
-
// Suggest to aggregate contacts if their match score is equal or greater than this threshold
public static final int SCORE_THRESHOLD_SUGGEST = 50;
@@ -59,9 +56,6 @@
// Maximum number of characters in a name to be considered by the matching algorithm.
private static final int MAX_MATCHED_NAME_LENGTH = 30;
- // Scores a multiplied by this number to allow room for "fractional" scores
- private static final int SCORE_SCALE = 1000;
-
public static final int MATCHING_ALGORITHM_EXACT = 0;
public static final int MATCHING_ALGORITHM_CONSERVATIVE = 1;
public static final int MATCHING_ALGORITHM_APPROXIMATE = 2;
@@ -159,88 +153,6 @@
return sMaxScore[index];
}
- /**
- * Captures the max score and match count for a specific contact. Used in an
- * contactId - MatchScore map.
- */
- public static class MatchScore implements Comparable<MatchScore> {
- private long mContactId;
- private boolean mKeepIn;
- private boolean mKeepOut;
- private int mPrimaryScore;
- private int mSecondaryScore;
- private int mMatchCount;
-
- public MatchScore(long contactId) {
- this.mContactId = contactId;
- }
-
- public void reset(long contactId) {
- this.mContactId = contactId;
- mKeepIn = false;
- mKeepOut = false;
- mPrimaryScore = 0;
- mSecondaryScore = 0;
- mMatchCount = 0;
- }
-
- public long getContactId() {
- return mContactId;
- }
-
- public void updatePrimaryScore(int score) {
- if (score > mPrimaryScore) {
- mPrimaryScore = score;
- }
- mMatchCount++;
- }
-
- public void updateSecondaryScore(int score) {
- if (score > mSecondaryScore) {
- mSecondaryScore = score;
- }
- mMatchCount++;
- }
-
- public void keepIn() {
- mKeepIn = true;
- }
-
- public void keepOut() {
- mKeepOut = true;
- }
-
- public int getScore() {
- if (mKeepOut) {
- return 0;
- }
-
- if (mKeepIn) {
- return MAX_SCORE;
- }
-
- int score = (mPrimaryScore > mSecondaryScore ? mPrimaryScore : mSecondaryScore);
-
- // Ensure that of two contacts with the same match score the one with more matching
- // data elements wins.
- return score * SCORE_SCALE + mMatchCount;
- }
-
- /**
- * Descending order of match score.
- */
- @Override
- public int compareTo(MatchScore another) {
- return another.getScore() - getScore();
- }
-
- @Override
- public String toString() {
- return mContactId + ": " + mPrimaryScore + "/" + mSecondaryScore + "(" + mMatchCount
- + ")";
- }
- }
-
private final HashMap<Long, MatchScore> mScores = new HashMap<Long, MatchScore>();
private final ArrayList<MatchScore> mScoreList = new ArrayList<MatchScore>();
private int mScoreCount = 0;
@@ -268,7 +180,7 @@
* Marks the contact as a full match, because we found an Identity match
*/
public void matchIdentity(long contactId) {
- updatePrimaryScore(contactId, MAX_SCORE);
+ updatePrimaryScore(contactId, MatchScore.MAX_SCORE);
}
/**
@@ -374,18 +286,18 @@
for (int i = 0; i < mScoreCount; i++) {
MatchScore score = mScoreList.get(i);
- if (score.mKeepOut) {
+ if (score.isKeepOut()) {
continue;
}
- int s = score.mSecondaryScore;
+ int s = score.getSecondaryScore();
if (s >= threshold) {
if (contactIds == null) {
contactIds = new ArrayList<Long>();
}
- contactIds.add(score.mContactId);
+ contactIds.add(score.getContactId());
}
- score.mPrimaryScore = NO_DATA_SCORE;
+ score.setPrimaryScore(NO_DATA_SCORE);
}
return contactIds;
}
@@ -401,17 +313,17 @@
int maxScore = 0;
for (int i = 0; i < mScoreCount; i++) {
MatchScore score = mScoreList.get(i);
- if (score.mKeepOut) {
+ if (score.isKeepOut()) {
continue;
}
- if (score.mKeepIn) {
- return score.mContactId;
+ if (score.isKeepIn()) {
+ return score.getContactId();
}
- int s = score.mPrimaryScore;
+ int s = score.getPrimaryScore();
if (s == NO_DATA_SCORE) {
- s = score.mSecondaryScore;
+ s = score.getSecondaryScore();
}
if (s >= threshold) {
@@ -420,8 +332,8 @@
}
// In order to make it stable, let's jut pick the one with the lowest ID
// if multiple candidates are found.
- if ((s > maxScore) || ((s == maxScore) && (contactId > score.mContactId))) {
- contactId = score.mContactId;
+ if ((s > maxScore) || ((s == maxScore) && (contactId > score.getContactId()))) {
+ contactId = score.getContactId();
maxScore = s;
}
}
@@ -433,7 +345,7 @@
* Returns matches in the order of descending score.
*/
public List<MatchScore> pickBestMatches(int threshold) {
- int scaledThreshold = threshold * SCORE_SCALE;
+ int scaledThreshold = threshold * MatchScore.SCORE_SCALE;
List<MatchScore> matches = mScoreList.subList(0, mScoreCount);
Collections.sort(matches);
int count = 0;
diff --git a/src/com/android/providers/contacts/aggregation/util/MatchScore.java b/src/com/android/providers/contacts/aggregation/util/MatchScore.java
new file mode 100644
index 0000000..f87731c
--- /dev/null
+++ b/src/com/android/providers/contacts/aggregation/util/MatchScore.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts.aggregation.util;
+
+/**
+ * Captures the max score and match count for a specific raw contact or contact.
+ */
+public class MatchScore implements Comparable<MatchScore> {
+ // Scores a multiplied by this number to allow room for "fractional" scores
+ public static final int SCORE_SCALE = 1000;
+ // Best possible match score
+ public static final int MAX_SCORE = 100;
+
+ private long mRawContactId;
+ private long mContactId;
+ private long mAccountId;
+
+ private boolean mKeepIn;
+ private boolean mKeepOut;
+
+ private int mPrimaryScore;
+ private int mSecondaryScore;
+ private int mMatchCount;
+
+ public MatchScore(long rawContactId, long contactId, long accountId) {
+ this.mRawContactId = rawContactId;
+ this.mContactId = contactId;
+ this.mAccountId = accountId;
+ }
+
+ public MatchScore(long contactId) {
+ this.mRawContactId = 0;
+ this.mContactId = contactId;
+ this.mAccountId = 0;
+ }
+
+ public void reset(long rawContactId, long contactId, long accountId) {
+ this.mRawContactId = rawContactId;
+ this.mContactId = contactId;
+ this.mAccountId = accountId;
+ mKeepIn = false;
+ mKeepOut = false;
+ mPrimaryScore = 0;
+ mSecondaryScore = 0;
+ mMatchCount = 0;
+ }
+
+ public void reset(long contactId) {
+ this.reset(0l, contactId, 0l);
+ }
+
+
+ public long getRawContactId() {
+ return mRawContactId;
+ }
+
+ public long getContactId() {
+ return mContactId;
+ }
+
+ public long getAccountId() {
+ return mAccountId;
+ }
+
+ 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;
+ }
+
+ public boolean isKeepIn() {
+ return mKeepIn;
+ }
+
+ public boolean isKeepOut() {
+ return mKeepOut;
+ }
+
+ public int getPrimaryScore() {
+ return mPrimaryScore;
+ }
+
+ public int getSecondaryScore() {
+ return mSecondaryScore;
+ }
+
+ public void setPrimaryScore(int mPrimaryScore) {
+ this.mPrimaryScore = mPrimaryScore;
+ }
+
+ /**
+ * Descending order of match score.
+ */
+ @Override
+ public int compareTo(MatchScore another) {
+ return another.getScore() - getScore();
+ }
+
+ @Override
+ public String toString() {
+ return mRawContactId + "/" + mContactId + "/" + mAccountId + ": " + mPrimaryScore +
+ "/" + mSecondaryScore + "(" + mMatchCount + ")";
+ }
+}
diff --git a/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java b/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java
new file mode 100644
index 0000000..88aa226
--- /dev/null
+++ b/src/com/android/providers/contacts/aggregation/util/RawContactMatcher.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts.aggregation.util;
+
+import android.util.Log;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
+import com.android.providers.contacts.util.Hex;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Logic for matching raw contacts' data.
+ */
+public class RawContactMatcher {
+ private static final String TAG = "ContactMatcher";
+
+ // Best possible match score
+ public static final int MAX_SCORE = 100;
+
+ // Suggest to aggregate contacts if their match score is equal or greater than this threshold
+ public static final int SCORE_THRESHOLD_SUGGEST = 50;
+
+ // Automatically aggregate contacts if their match score is equal or greater than this threshold
+ public static final int SCORE_THRESHOLD_PRIMARY = 70;
+
+ // Automatically aggregate contacts if the match score is equal or greater than this threshold
+ // and there is a secondary match (phone number, email etc).
+ public static final int SCORE_THRESHOLD_SECONDARY = 50;
+
+ // Score for matching phone numbers
+ private static final int PHONE_MATCH_SCORE = 71;
+
+ // Score for matching email addresses
+ private static final int EMAIL_MATCH_SCORE = 71;
+
+ // Score for matching nickname
+ private static final int NICKNAME_MATCH_SCORE = 71;
+
+ // Maximum number of characters in a name to be considered by the matching algorithm.
+ private static final int MAX_MATCHED_NAME_LENGTH = 30;
+
+ // Scores a multiplied by this number to allow room for "fractional" scores
+ private static final int SCORE_SCALE = 1000;
+
+ public static final int MATCHING_ALGORITHM_EXACT = 0;
+ public static final int MATCHING_ALGORITHM_CONSERVATIVE = 1;
+ public static final int MATCHING_ALGORITHM_APPROXIMATE = 2;
+
+ // Minimum edit distance between two names to be considered an approximate match
+ public static final float APPROXIMATE_MATCH_THRESHOLD = 0.82f;
+
+ // Minimum edit distance between two email ids to be considered an approximate match
+ public static final float APPROXIMATE_MATCH_THRESHOLD_FOR_EMAIL = 0.95f;
+
+ // Returned value when we found multiple matches and that was not allowed
+ public static final long MULTIPLE_MATCHES = -2;
+
+ /**
+ * Name matching scores: a matrix by name type vs. candidate lookup type.
+ * For example, if the name type is "full name" while we are looking for a
+ * "full name", the score may be 99. If we are looking for a "nickname" but
+ * find "first name", the score may be 50 (see specific scores defined
+ * below.)
+ * <p>
+ * For approximate matching, we have a range of scores, let's say 40-70. Depending one how
+ * similar the two strings are, the score will be somewhere between 40 and 70, with the exact
+ * match producing the score of 70. The score may also be 0 if the similarity (distance)
+ * between the strings is below the threshold.
+ * <p>
+ * We use a string matching algorithm, which is particularly suited for
+ * name matching. See {@link NameDistance}.
+ */
+ private static int[] sMinScore =
+ new int[NameLookupType.TYPE_COUNT * NameLookupType.TYPE_COUNT];
+ private static int[] sMaxScore =
+ new int[NameLookupType.TYPE_COUNT * NameLookupType.TYPE_COUNT];
+
+ /*
+ * Note: the reverse names ({@link NameLookupType#FULL_NAME_REVERSE},
+ * {@link NameLookupType#FULL_NAME_REVERSE_CONCATENATED} may appear to be redundant. They are
+ * not! They are useful in three-way aggregation cases when we have, for example, both
+ * John Smith and Smith John. A third contact with the name John Smith should be aggregated
+ * with the former rather than the latter. This is why "reverse" matches have slightly lower
+ * scores than direct matches.
+ */
+ static {
+ setScoreRange(NameLookupType.NAME_EXACT,
+ NameLookupType.NAME_EXACT, 99, 99);
+ setScoreRange(NameLookupType.NAME_VARIANT,
+ NameLookupType.NAME_VARIANT, 90, 90);
+ setScoreRange(NameLookupType.NAME_COLLATION_KEY,
+ NameLookupType.NAME_COLLATION_KEY, 50, 80);
+
+ setScoreRange(NameLookupType.NAME_COLLATION_KEY,
+ NameLookupType.EMAIL_BASED_NICKNAME, 30, 60);
+ setScoreRange(NameLookupType.NAME_COLLATION_KEY,
+ NameLookupType.NICKNAME, 50, 60);
+
+ setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+ NameLookupType.EMAIL_BASED_NICKNAME, 50, 60);
+ setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+ NameLookupType.NAME_COLLATION_KEY, 50, 60);
+ setScoreRange(NameLookupType.EMAIL_BASED_NICKNAME,
+ NameLookupType.NICKNAME, 50, 60);
+
+ setScoreRange(NameLookupType.NICKNAME,
+ NameLookupType.NICKNAME, 50, 60);
+ setScoreRange(NameLookupType.NICKNAME,
+ NameLookupType.NAME_COLLATION_KEY, 50, 60);
+ setScoreRange(NameLookupType.NICKNAME,
+ NameLookupType.EMAIL_BASED_NICKNAME, 50, 60);
+ }
+
+ /**
+ * Populates the cells of the score matrix and score span matrix
+ * corresponding to the {@code candidateNameType} and {@code nameType}.
+ */
+ private static void setScoreRange(int candidateNameType, int nameType, int scoreFrom,
+ int scoreTo) {
+ int index = nameType * NameLookupType.TYPE_COUNT + candidateNameType;
+ sMinScore[index] = scoreFrom;
+ sMaxScore[index] = scoreTo;
+ }
+
+ /**
+ * Returns the lower range for the match score for the given {@code candidateNameType} and
+ * {@code nameType}.
+ */
+ private static int getMinScore(int candidateNameType, int nameType) {
+ int index = nameType * NameLookupType.TYPE_COUNT + candidateNameType;
+ return sMinScore[index];
+ }
+
+ /**
+ * Returns the upper range for the match score for the given {@code candidateNameType} and
+ * {@code nameType}.
+ */
+ private static int getMaxScore(int candidateNameType, int nameType) {
+ int index = nameType * NameLookupType.TYPE_COUNT + candidateNameType;
+ return sMaxScore[index];
+ }
+
+ private final HashMap<Long, MatchScore> mScores = new HashMap<Long, MatchScore>();
+ private final ArrayList<MatchScore> mScoreList = new ArrayList<MatchScore>();
+ private int mScoreCount = 0;
+
+ private final NameDistance mNameDistanceConservative = new NameDistance();
+ private final NameDistance mNameDistanceApproximate = new NameDistance(MAX_MATCHED_NAME_LENGTH);
+
+ private MatchScore getMatchingScore(long rawContactId, long contactId, long accountId) {
+ MatchScore matchingScore = mScores.get(rawContactId);
+ if (matchingScore == null) {
+ if (mScoreList.size() > mScoreCount) {
+ matchingScore = mScoreList.get(mScoreCount);
+ matchingScore.reset(rawContactId, contactId, accountId);
+ } else {
+ matchingScore = new MatchScore(rawContactId, contactId, accountId);
+ mScoreList.add(matchingScore);
+ }
+ mScoreCount++;
+ mScores.put(rawContactId, matchingScore);
+ }
+ return matchingScore;
+ }
+
+ /**
+ * Marks the contact as a full match, because we found an Identity match
+ */
+ public void matchIdentity(long rawContactId, long contactId, long accountId) {
+ updatePrimaryScore(rawContactId, contactId, accountId, MAX_SCORE);
+ }
+
+ /**
+ * Checks if there is a match and updates the overall score for the
+ * specified contact for a discovered match. The new score is determined
+ * by the prior score, by the type of name we were looking for, the type
+ * of name we found and, if the match is approximate, the distance between the candidate and
+ * actual name.
+ */
+ public void matchName(long rawContactId, long contactId, long accountId, int
+ candidateNameType, String candidateName, int nameType, String name, int algorithm) {
+ int maxScore = getMaxScore(candidateNameType, nameType);
+ if (maxScore == 0) {
+ return;
+ }
+
+ if (candidateName.equals(name)) {
+ updatePrimaryScore(rawContactId, contactId, accountId, maxScore);
+ return;
+ }
+
+ if (algorithm == MATCHING_ALGORITHM_EXACT) {
+ return;
+ }
+
+ int minScore = getMinScore(candidateNameType, nameType);
+ if (minScore == maxScore) {
+ return;
+ }
+
+ final byte[] decodedCandidateName;
+ final byte[] decodedName;
+ try {
+ decodedCandidateName = Hex.decodeHex(candidateName);
+ decodedName = Hex.decodeHex(name);
+ } catch (RuntimeException e) {
+ // How could this happen?? See bug 6827136
+ Log.e(TAG, "Failed to decode normalized name. Skipping.", e);
+ return;
+ }
+
+ NameDistance nameDistance = algorithm == MATCHING_ALGORITHM_CONSERVATIVE ?
+ mNameDistanceConservative : mNameDistanceApproximate;
+
+ int score;
+ float distance = nameDistance.getDistance(decodedCandidateName, decodedName);
+ boolean emailBased = candidateNameType == NameLookupType.EMAIL_BASED_NICKNAME
+ || nameType == NameLookupType.EMAIL_BASED_NICKNAME;
+ float threshold = emailBased
+ ? APPROXIMATE_MATCH_THRESHOLD_FOR_EMAIL
+ : APPROXIMATE_MATCH_THRESHOLD;
+ if (distance > threshold) {
+ score = (int)(minScore + (maxScore - minScore) * (1.0f - distance));
+ } else {
+ score = 0;
+ }
+
+ updatePrimaryScore(rawContactId, contactId, accountId, score);
+ }
+
+ public void updateScoreWithPhoneNumberMatch(long rawContactId, long contactId, long accountId) {
+ updateSecondaryScore(rawContactId, contactId, accountId, PHONE_MATCH_SCORE);
+ }
+
+ public void updateScoreWithEmailMatch(long rawContactId, long contactId, long accountId) {
+ updateSecondaryScore(rawContactId, contactId, accountId, EMAIL_MATCH_SCORE);
+ }
+
+ public void updateScoreWithNicknameMatch(long rawContactId, long contactId, long accountId) {
+ updateSecondaryScore(rawContactId, contactId, accountId, NICKNAME_MATCH_SCORE);
+ }
+
+ private void updatePrimaryScore(long rawContactId, long contactId, long accountId, int score) {
+ getMatchingScore(rawContactId, contactId, accountId).updatePrimaryScore(score);
+ }
+
+ private void updateSecondaryScore(long rawContactId, long contactId, long accountId,
+ int score) {
+ getMatchingScore(rawContactId, contactId, accountId).updateSecondaryScore(score);
+ }
+
+ public void keepIn(long rawContactId, long contactId, long accountId) {
+ getMatchingScore(rawContactId, contactId, accountId).keepIn();
+ }
+
+ public void keepOut(long rawContactId, long contactId, long accountId) {
+ getMatchingScore(rawContactId, contactId, accountId).keepOut();
+ }
+
+ public void clear() {
+ mScores.clear();
+ mScoreCount = 0;
+ }
+ /**
+ * Returns a list of IDs for raw contacts that are matched on secondary data elements
+ * (phone number, email address, nickname). We still need to obtain the approximate
+ * primary score for those contacts to determine if any of them should be aggregated.
+ * <p>
+ * May return null.
+ */
+ public List<Long> prepareSecondaryMatchCandidates() {
+ ArrayList<Long> rawContactIds = null;
+
+ for (int i = 0; i < mScoreCount; i++) {
+ MatchScore score = mScoreList.get(i);
+ if (score.isKeepOut() || score.getPrimaryScore() > SCORE_THRESHOLD_PRIMARY){
+ continue;
+ }
+
+ if (score.getSecondaryScore() >= SCORE_THRESHOLD_PRIMARY) {
+ if (rawContactIds == null) {
+ rawContactIds = new ArrayList<Long>();
+ }
+ rawContactIds.add(score.getRawContactId());
+ }
+ score.setPrimaryScore(0);
+ }
+ return rawContactIds;
+ }
+
+ /**
+ * Returns the list of raw contact Ids with the match score over threshold.
+ */
+ public List<MatchScore> pickBestMatches() {
+ final List<MatchScore> matches = new ArrayList<>();
+ for (int i = 0; i < mScoreCount; i++) {
+ MatchScore score = mScoreList.get(i);
+ if (score.isKeepOut()) {
+ continue;
+ }
+
+ if (score.isKeepIn()) {
+ matches.add(score);
+ continue;
+ }
+
+ if (score.getPrimaryScore() >= SCORE_THRESHOLD_SECONDARY) {
+ matches.add(score);
+ }
+ }
+ return matches;
+ }
+
+ /**
+ * Returns matches in the order of descending score.
+ */
+ public List<MatchScore> pickBestMatches(int threshold) {
+ int scaledThreshold = threshold * SCORE_SCALE;
+ List<MatchScore> matches = mScoreList.subList(0, mScoreCount);
+ Collections.sort(matches);
+ int count = 0;
+ for (int i = 0; i < mScoreCount; i++) {
+ MatchScore matchScore = matches.get(i);
+ if (matchScore.getScore() >= scaledThreshold) {
+ count++;
+ } else {
+ break;
+ }
+ }
+
+ return matches.subList(0, count);
+ }
+
+ @Override
+ public String toString() {
+ return mScoreList.subList(0, mScoreCount).toString();
+ }
+}
diff --git a/src/com/android/providers/contacts/aggregation/util/RawContactMatchingCandidates.java b/src/com/android/providers/contacts/aggregation/util/RawContactMatchingCandidates.java
new file mode 100644
index 0000000..917c810
--- /dev/null
+++ b/src/com/android/providers/contacts/aggregation/util/RawContactMatchingCandidates.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.aggregation.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+/**
+ * Matching candidates for a raw contact, used in the contact aggregator.
+ */
+public class RawContactMatchingCandidates {
+ private List<MatchScore> mBestMatches;
+ private Set<Long> mRawContactIds = null;
+ private Map<Long, Long> mRawContactToContact = null;
+ private Map<Long, Long> mRawContactToAccount = null;
+
+ public RawContactMatchingCandidates(List<MatchScore> mBestMatches) {
+ checkNotNull(mBestMatches);
+ this.mBestMatches = mBestMatches;
+ }
+
+ public RawContactMatchingCandidates() {
+ mBestMatches = new ArrayList<MatchScore>();
+ }
+
+ public int getCount() {
+ return mBestMatches.size();
+ }
+
+ public void add(MatchScore score) {
+ mBestMatches.add(score);
+ if (mRawContactIds != null) {
+ mRawContactIds.add(score.getRawContactId());
+ }
+ if (mRawContactToAccount != null) {
+ mRawContactToAccount.put(score.getRawContactId(), score.getAccountId());
+ }
+ if (mRawContactToContact != null) {
+ mRawContactToContact.put(score.getRawContactId(), score.getContactId());
+ }
+ }
+
+ public Set<Long> getRawContactIdSet() {
+ if (mRawContactIds == null) {
+ createRawContactIdSet();
+ }
+ return mRawContactIds;
+ }
+
+ public Map<Long, Long> getRawContactToAccount() {
+ if (mRawContactToAccount == null) {
+ createRawContactToAccountMap();
+ }
+ return mRawContactToAccount;
+ }
+
+ public Long getContactId(Long rawContactId) {
+ if (mRawContactToContact == null) {
+ createRawContactToContactMap();
+ }
+ return mRawContactToContact.get(rawContactId);
+ }
+
+ public Long getAccountId(Long rawContactId) {
+ if (mRawContactToAccount == null) {
+ createRawContactToAccountMap();
+ }
+ return mRawContactToAccount.get(rawContactId);
+ }
+
+ private void createRawContactToContactMap() {
+ mRawContactToContact = new HashMap<Long, Long>();
+ for (int i = 0; i < mBestMatches.size(); i++) {
+ mRawContactToContact.put(mBestMatches.get(i).getRawContactId(),
+ mBestMatches.get(i).getContactId());
+ }
+ }
+
+ private void createRawContactToAccountMap() {
+ mRawContactToAccount = new HashMap<Long, Long>();
+ for (int i = 0; i < mBestMatches.size(); i++) {
+ mRawContactToAccount.put(mBestMatches.get(i).getRawContactId(),
+ mBestMatches.get(i).getAccountId());
+ }
+ }
+
+ private void createRawContactIdSet() {
+ mRawContactIds = new HashSet<Long>();
+ for (int i = 0; i < mBestMatches.size(); i++) {
+ mRawContactIds.add(mBestMatches.get(i).getRawContactId());
+ }
+ }
+}
diff --git a/src/com/android/providers/contacts/util/ContactsPermissions.java b/src/com/android/providers/contacts/util/ContactsPermissions.java
new file mode 100644
index 0000000..6dda50b
--- /dev/null
+++ b/src/com/android/providers/contacts/util/ContactsPermissions.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Process;
+import android.util.Log;
+
+public class ContactsPermissions {
+ private static final String TAG = "ContactsPermissions";
+
+ private static final boolean DEBUG = false; // DO NOT submit with true
+
+ // Normally, we allow calls from self, *except* in unit tests, where we clear this flag
+ // to emulate calls from other apps.
+ public static boolean ALLOW_SELF_CALL = true;
+
+ private ContactsPermissions() {
+ }
+
+ public static boolean hasCallerOrSelfPermission(Context context, String permission) {
+ boolean ok = false;
+
+ if (ALLOW_SELF_CALL && Binder.getCallingPid() == Process.myPid()) {
+ ok = true; // Called by self; always allow.
+ } else {
+ ok = context.checkCallingOrSelfPermission(permission)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "hasCallerOrSelfPermission: "
+ + " perm=" + permission
+ + " caller=" + Binder.getCallingPid()
+ + " self=" + Process.myPid()
+ + " ok=" + ok);
+ }
+ return ok;
+ }
+
+ public static void enforceCallingOrSelfPermission(Context context, String permission) {
+ final boolean ok = hasCallerOrSelfPermission(context, permission);
+ if (!ok) {
+ throw new SecurityException(String.format("The caller must have the %s permission.",
+ permission));
+ }
+ }
+
+ public static boolean hasPackagePermission(Context context, String permission, String pkg) {
+ boolean ok = false;
+ if (ALLOW_SELF_CALL && context.getPackageName().equals(pkg)) {
+ ok = true; // Called by self; always allow.
+ } else {
+ ok = context.getPackageManager().checkPermission(permission, pkg)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "hasCallerOrSelfPermission: "
+ + " perm=" + permission
+ + " pkg=" + pkg
+ + " self=" + context.getPackageName()
+ + " ok=" + ok);
+ }
+ return ok;
+ }
+
+ public static boolean hasCallerUriPermission(Context context, Uri uri, int modeFlags) {
+ boolean ok = false;
+ if (ALLOW_SELF_CALL && Binder.getCallingPid() == Process.myPid()) {
+ ok = true; // Called by self; always allow.
+ } else {
+ ok = context.checkCallingUriPermission(uri, modeFlags)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "hasCallerUriPermission: "
+ + " uri=" + uri
+ + " caller=" + Binder.getCallingPid()
+ + " self=" + Process.myPid()
+ + " ok=" + ok);
+ }
+ return ok;
+ }
+}
diff --git a/src/com/android/providers/contacts/util/UserUtils.java b/src/com/android/providers/contacts/util/UserUtils.java
index 74fd2e7..8ae770a 100644
--- a/src/com/android/providers/contacts/util/UserUtils.java
+++ b/src/com/android/providers/contacts/util/UserUtils.java
@@ -45,10 +45,13 @@
}
/**
- * @return the user ID of the corp user that is linked to the current user, if any.
- * If there's no such user or cross-user contacts access is disallowed by policy, returns -1.
+ * @param enforceCallerIdCheck True if we want to enforce cross profile
+ * caller-id device policy.
+ * @return the user ID of the corp user that is linked to the current user,
+ * if any. If there's no such user or cross-user contacts access is
+ * disallowed by policy, returns -1.
*/
- public static int getCorpUserId(Context context) {
+ public static int getCorpUserId(Context context, boolean enforceCallerIdCheck) {
final UserManager um = getUserManager(context);
if (um == null) {
Log.e(TAG, "No user manager service found");
@@ -75,7 +78,8 @@
// Check if profile is blocking calling id.
// TODO DevicePolicyManager is not mockable -- the constructor is private.
// Test it somehow.
- if (getDevicePolicyManager(context)
+ if (enforceCallerIdCheck
+ && getDevicePolicyManager(context)
.getCrossProfileCallerIdDisabled(ui.getUserHandle())) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "Enterprise caller-id disabled for user " + ui.id);
diff --git a/tests/Android.mk b/tests/Android.mk
index ec48f5a..35a6b39 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -4,14 +4,18 @@
# We only want this apk build for tests.
LOCAL_MODULE_TAGS := tests
+LOCAL_STATIC_JAVA_LIBRARIES := mockito-target
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
# 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
+LOCAL_PROGUARD_ENABLED := disabled
+
include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index d0e3810..15a90fa 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -18,14 +18,19 @@
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" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<application>
<uses-library android:name="android.test.runner" />
-
+
<!-- Mock contacts sync adapter -->
<service android:name=".MockSyncAdapter" android:exported="true">
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter" />
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/mock_syncadapter" />
<meta-data android:name="android.provider.CONTACTS_STRUCTURE"
android:resource="@xml/contacts" />
</service>
diff --git a/tests/assets/test1/testFileDeviceContactMetadataJSON.txt b/tests/assets/test1/testFileDeviceContactMetadataJSON.txt
new file mode 100644
index 0000000..65e624d
--- /dev/null
+++ b/tests/assets/test1/testFileDeviceContactMetadataJSON.txt
@@ -0,0 +1,69 @@
+{
+ "unique_contact_id": {
+ "account_type": "CUSTOM_ACCOUNT",
+ "custom_account_type": "facebook",
+ "account_name": "android-test",
+ "contact_id": "1111111",
+ "data_set": "FOCUS"
+ },
+ "contact_prefs": {
+ "send_to_voicemail": true,
+ "starred": false,
+ "pinned": 2
+ },
+ "aggregation_data": [
+ {
+ "type": "TOGETHER",
+ "contact_ids": [
+ {
+ "account_type": "GOOGLE_ACCOUNT",
+ "account_name": "android-test2",
+ "contact_id": "2222222",
+ "data_set": "GOOGLE_PLUS"
+ },
+ {
+ "account_type": "GOOGLE_ACCOUNT",
+ "account_name": "android-test3",
+ "contact_id": "3333333",
+ "data_set": "CUSTOM",
+ "custom_data_set": "custom type"
+ }
+ ]
+ }
+ ],
+ "field_data": [
+ {
+ "field_data_id": "1001",
+ "field_data_prefs": {
+ "is_primary": true,
+ "is_super_primary": true
+ },
+ "usage_stats": [
+ {
+ "usage_type": "CALL",
+ "last_time_used": 10000001,
+ "usage_count": 10
+ },
+ {
+ "usage_type": "SHORT_TEXT",
+ "last_time_used": 20000002,
+ "usage_count": 20
+ }
+ ]
+ },
+ {
+ "field_data_id": "1002",
+ "field_data_prefs": {
+ "is_primary": false,
+ "is_super_primary": false
+ },
+ "usage_stats": [
+ {
+ "usage_type": "LONG_TEXT",
+ "last_time_used": 30000003,
+ "usage_count": 30
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/res/xml/mock_syncadapter.xml b/tests/res/xml/mock_syncadapter.xml
new file mode 100644
index 0000000..6f77a55
--- /dev/null
+++ b/tests/res/xml/mock_syncadapter.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2015, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="com.android.contacts"
+ android:accountType="com.android.providers.contacts.tests"
+/>
+<!-- The account type doesn't exist, but looks like that's okay for our purpose. -->
+
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 3778380..45206a3 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -153,10 +153,6 @@
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);
diff --git a/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java b/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
index 547eafa..8e4121d 100644
--- a/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
@@ -98,6 +98,11 @@
public File getDir(String name, int mode) {
return getTestDirectory();
}
+
+ @Override
+ public PackageManager getPackageManager() {
+ return new MockPackageManager(mActor.getProviderContext().getPackageName());
+ }
};
}
@@ -145,6 +150,7 @@
private interface VvmProviderCalls {
public void sendOrderedBroadcast(Intent intent, String receiverPermission);
public File getDir(String name, int mode);
+ public PackageManager getPackageManager();
}
public static class TestVoicemailProvider extends VoicemailContentProvider {
@@ -171,7 +177,7 @@
}
@Override
public PackageManager getPackageManager() {
- return new MockPackageManager("com.test.package1", "com.test.package2");
+ return mDelegate.getPackageManager();
}
};
}
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
index d879dd9..ea436d8 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -60,9 +60,11 @@
Voicemails.MIME_TYPE,
Voicemails.SOURCE_PACKAGE,
Voicemails.SOURCE_DATA,
- Voicemails.STATE};
+ Voicemails.STATE,
+ Voicemails.DIRTY,
+ Voicemails.DELETED};
/** Total number of columns exposed by call_log provider. */
- private static final int NUM_CALLLOG_FIELDS = 24;
+ private static final int NUM_CALLLOG_FIELDS = 25;
private CallLogProvider mCallLogProvider;
diff --git a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
index be14f45..c5bc6f6 100644
--- a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
+++ b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
@@ -122,6 +122,9 @@
public void testIsDirectoryProvider() {
ProviderInfo provider = new ProviderInfo();
+ // Null -- just return false.
+ assertFalse(ContactDirectoryManager.isDirectoryProvider(null));
+
// No metadata
assertFalse(ContactDirectoryManager.isDirectoryProvider(provider));
diff --git a/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
index 394a1aa..ec339eb 100644
--- a/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
+++ b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
@@ -41,8 +41,9 @@
private static final String ARABIC_NAME = "\u0646\u0648\u0631"; /* Noor */
private static final String CHINESE_NAME = "\u675C\u9D51";
private static final String SERBIAN_NAME = "\u0408\u0435\u043B\u0435\u043D\u0430";
- private static final String UKRAINIAN_NAME = "\u0407";
- private static final String UKRAINIAN_NAME_2 = "\u0490";
+ private static final String UKRAINIAN_NAME = "\u0406";
+ private static final String UKRAINIAN_NAME_2 = "\u0407";
+ private static final String UKRAINIAN_NAME_3 = "\u0490";
private static final String CHINESE_LATIN_MIX_NAME_1 = "D\u675C\u9D51";
private static final String CHINESE_LATIN_MIX_NAME_2 = "MARY \u675C\u9D51";
private static final String[] CHINESE_NAME_KEY = {"\u9D51", "\u675C\u9D51", "JUAN", "DUJUAN",
@@ -88,7 +89,7 @@
"\u0635", "\u0636", "\u0637", "\u0638", "\u0639", "\u063a", "\u0641",
"\u0642", "\u0643", "\u0644", "\u0645", "\u0646", "\u0647", "\u0648",
"\u064a",
- "", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"#", ""};
@@ -163,7 +164,7 @@
assertEquals("\u0408", getLabel(SERBIAN_NAME));
}
if (hasUkrainianCollator) {
- assertEquals("\u0407", getLabel(UKRAINIAN_NAME));
+ assertEquals("\u0406", getLabel(UKRAINIAN_NAME));
}
assertNull(getNameLookupKeys(LATIN_NAME, FullNameStyle.UNDEFINED));
@@ -334,11 +335,12 @@
}
ContactLocaleUtils.setLocale(LOCALE_UKRAINIAN);
- assertEquals("\u0407", getLabel(UKRAINIAN_NAME));
- // ICU 52 has a bug whereby this letter has a bucket created only if
- // Ukrainian is the primary language. Once this is fixed also test this
- // label when in English locale.
- assertEquals("\u0490", getLabel(UKRAINIAN_NAME_2));
+ assertEquals("\u0406", getLabel(UKRAINIAN_NAME));
+ // ICU 55 has a bug whereby these letters have buckets created only if
+ // Ukrainian is the primary language. Once this is fixed also test
+ // these labels when in English locale.
+ assertEquals("\u0407", getLabel(UKRAINIAN_NAME_2));
+ assertEquals("\u0490", getLabel(UKRAINIAN_NAME_3));
assertEquals("B", getLabel("Bob Smith"));
}
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index bad7d5d..0b91d4c 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -65,6 +65,7 @@
import android.test.mock.MockContext;
import android.util.Log;
+import com.android.providers.contacts.util.ContactsPermissions;
import com.android.providers.contacts.util.MockSharedPreferences;
import com.google.android.collect.Sets;
@@ -263,6 +264,10 @@
*/
public ContactsActor(final Context overallContext, String packageName,
Class<? extends ContentProvider> providerClass, String authority) throws Exception {
+
+ // Force permission check even when called by self.
+ ContactsPermissions.ALLOW_SELF_CALL = false;
+
resolver = new MockContentResolver();
context = new RestrictionMockContext(overallContext, packageName, resolver,
mGrantedPermissions, mGrantedUriPermissions);
@@ -323,10 +328,6 @@
return mProviderContext;
}
- public void addAuthority(String authority) {
- resolver.addProvider(authority, provider);
- }
-
public <T extends ContentProvider> T addProvider(Class<T> providerClass,
String authority) throws Exception {
return addProvider(providerClass, authority, mProviderContext);
@@ -342,6 +343,12 @@
info.authority = stripOutUserIdFromAuthority(authority);
provider.attachInfoForTesting(providerContext, info);
resolver.addProvider(authority, provider);
+
+ // In case of LegacyTest, "authority" here is actually multiple authorities.
+ // Register all authority here.
+ for (String a : authority.split(";")) {
+ resolver.addProvider(a, provider);
+ }
return provider;
}
diff --git a/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java b/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
index 694f0f3..a5aa7c7 100644
--- a/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
+++ b/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
@@ -96,17 +96,4 @@
public Resources getResourcesForApplication(String appPackageName) {
return new ContactsMockResources();
}
-
- @Override
- public List<ProviderInfo> queryContentProviders(String processName, int uid, int flags) {
- final List<ProviderInfo> ret = Lists.newArrayList();
- if (mPackages == null) return ret;
- for (PackageInfo packageInfo : mPackages) {
- if (packageInfo.providers == null) continue;
- for (ProviderInfo providerInfo : packageInfo.providers) {
- ret.add(providerInfo);
- }
- }
- return ret;
- }
}
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index 0e4d9d6..1e1ef04 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -19,7 +19,6 @@
import static com.android.providers.contacts.TestUtils.cv;
import android.accounts.Account;
-import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
@@ -27,14 +26,13 @@
import android.content.ContentValues;
import android.content.Entity;
import android.content.EntityIterator;
-import android.content.pm.UserInfo;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.UserManager;
+import android.os.Bundle;
import android.provider.CallLog.Calls;
import android.provider.CallLog;
import android.provider.ContactsContract;
@@ -86,6 +84,11 @@
import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.MetadataEntryParser.AggregationData;
+import com.android.providers.contacts.MetadataEntryParser.FieldData;
+import com.android.providers.contacts.MetadataEntryParser.MetadataEntry;
+import com.android.providers.contacts.MetadataEntryParser.RawContactInfo;
+import com.android.providers.contacts.MetadataEntryParser.UsageStats;
import com.android.providers.contacts.testutil.CommonDatabaseUtils;
import com.android.providers.contacts.testutil.ContactUtil;
import com.android.providers.contacts.testutil.DataUtil;
@@ -309,6 +312,7 @@
RawContacts.DATA_SET,
RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
RawContacts.SOURCE_ID,
+ RawContacts.BACKUP_ID,
RawContacts.VERSION,
RawContacts.RAW_CONTACT_IS_USER_PROFILE,
RawContacts.DIRTY,
@@ -318,7 +322,6 @@
RawContacts.DISPLAY_NAME_SOURCE,
RawContacts.PHONETIC_NAME,
RawContacts.PHONETIC_NAME_STYLE,
- RawContacts.NAME_VERIFIED,
RawContacts.SORT_KEY_PRIMARY,
RawContacts.SORT_KEY_ALTERNATIVE,
RawContactsColumns.PHONEBOOK_LABEL_PRIMARY,
@@ -343,6 +346,7 @@
assertProjection(Data.CONTENT_URI, new String[]{
Data._ID,
Data.RAW_CONTACT_ID,
+ Data.HASH_ID,
Data.DATA_VERSION,
Data.IS_PRIMARY,
Data.IS_SUPER_PRIMARY,
@@ -382,9 +386,9 @@
RawContacts.DATA_SET,
RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
RawContacts.SOURCE_ID,
+ RawContacts.BACKUP_ID,
RawContacts.VERSION,
RawContacts.DIRTY,
- RawContacts.NAME_VERIFIED,
RawContacts.RAW_CONTACT_IS_USER_PROFILE,
Contacts._ID,
Contacts.DISPLAY_NAME_PRIMARY,
@@ -547,10 +551,10 @@
RawContacts.DATA_SET,
RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
RawContacts.SOURCE_ID,
+ RawContacts.BACKUP_ID,
RawContacts.VERSION,
RawContacts.DELETED,
RawContacts.DIRTY,
- RawContacts.NAME_VERIFIED,
RawContacts.SYNC1,
RawContacts.SYNC2,
RawContacts.SYNC3,
@@ -607,9 +611,9 @@
RawContacts.DATA_SET,
RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
RawContacts.SOURCE_ID,
+ RawContacts.BACKUP_ID,
RawContacts.VERSION,
RawContacts.DIRTY,
- RawContacts.NAME_VERIFIED,
RawContacts.DELETED,
RawContacts.SYNC1,
RawContacts.SYNC2,
@@ -1789,9 +1793,90 @@
// Note here we use a standalone CP2 so it'll have its own db helper.
// Also use AlteringUserContext here to report the corp user id.
- return mActor.addProvider(StandaloneContactsProvider2.class,
+ SynchronousContactsProvider2 provider = mActor.addProvider(
+ StandaloneContactsProvider2.class,
"" + MockUserManager.CORP_USER.id + "@com.android.contacts",
new AlteringUserContext(mActor.getProviderContext(), MockUserManager.CORP_USER.id));
+ provider.wipeData();
+ return provider;
+ }
+
+ /**
+ * Test for query of merged primary and work contacts.
+ * <p/>
+ * Note: in this test, we add one more provider instance for the authority
+ * "10@com.android.contacts" and use it as the corp cp2.
+ */
+ public void testQueryMergedDataPhones() throws Exception {
+ // Insert a contact to the primary CP2.
+ long rawContactId = ContentUris.parseId(
+ mResolver.insert(RawContacts.CONTENT_URI, new ContentValues()));
+ DataUtil.insertStructuredName(mResolver, rawContactId, "Contact1", "Primary");
+
+ insertPhoneNumber(rawContactId, "111-111-1111", false, false, Phone.TYPE_MOBILE);
+
+ // Insert a contact to the corp CP2, with different name and phone number.
+ final SynchronousContactsProvider2 corpCp2 = setUpCorpProvider();
+ rawContactId = ContentUris.parseId(
+ corpCp2.insert(RawContacts.CONTENT_URI, new ContentValues()));
+ // Insert a name.
+ ContentValues cv = cv(
+ Data.RAW_CONTACT_ID, rawContactId,
+ Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE,
+ StructuredName.DISPLAY_NAME, "Contact2 Corp",
+ StructuredName.GIVEN_NAME, "Contact2",
+ StructuredName.FAMILY_NAME, "Corp");
+ corpCp2.insert(ContactsContract.Data.CONTENT_URI, cv);
+ // Insert a number.
+ cv = cv(
+ Data.RAW_CONTACT_ID, rawContactId,
+ Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE,
+ Phone.NUMBER, "222-222-2222",
+ Phone.TYPE, Phone.TYPE_MOBILE);
+ corpCp2.insert(ContactsContract.Data.CONTENT_URI, cv);
+
+ // Insert another contact to to corp CP2, with different name phone number and phone type
+ rawContactId = ContentUris.parseId(
+ corpCp2.insert(RawContacts.CONTENT_URI, new ContentValues()));
+ // Insert a name.
+ cv = cv(
+ Data.RAW_CONTACT_ID, rawContactId,
+ Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE,
+ StructuredName.DISPLAY_NAME, "Contact3 Corp",
+ StructuredName.GIVEN_NAME, "Contact3",
+ StructuredName.FAMILY_NAME, "Corp");
+ corpCp2.insert(ContactsContract.Data.CONTENT_URI, cv);
+ // Insert a number
+ cv = cv(
+ Data.RAW_CONTACT_ID, rawContactId,
+ Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE,
+ Phone.NUMBER, "333-333-3333",
+ Phone.TYPE, Phone.TYPE_HOME);
+ corpCp2.insert(ContactsContract.Data.CONTENT_URI, cv);
+
+ // Execute the query to get the merged result.
+ Cursor c = mResolver.query(Phone.ENTERPRISE_CONTENT_URI, new String[]{Phone.CONTACT_ID,
+ Phone.DISPLAY_NAME, Phone.NUMBER}, Phone.TYPE + " = ?",
+ new String[]{String.valueOf(Phone.TYPE_MOBILE)}, null);
+ try {
+ // Verify the primary contact.
+ assertEquals(2, c.getCount());
+ assertEquals(3, c.getColumnCount());
+ c.moveToPosition(0);
+ assertEquals("Contact1 Primary", c.getString(c.getColumnIndex(Phone.DISPLAY_NAME)));
+ assertEquals("111-111-1111", c.getString(c.getColumnIndex(Phone.NUMBER)));
+ long contactId = c.getLong(c.getColumnIndex(Phone.CONTACT_ID));
+ assertFalse(Contacts.isEnterpriseContactId(contactId));
+
+ // Verify the enterprise contact.
+ c.moveToPosition(1);
+ assertEquals("Contact2 Corp", c.getString(c.getColumnIndex(Phone.DISPLAY_NAME)));
+ assertEquals("222-222-2222", c.getString(c.getColumnIndex(Phone.NUMBER)));
+ contactId = c.getLong(c.getColumnIndex(Phone.CONTACT_ID));
+ assertTrue(Contacts.isEnterpriseContactId(contactId));
+ } finally {
+ c.close();
+ }
}
/**
@@ -1890,6 +1975,58 @@
}
}
+ public void testQueryRawContactEntitiesCorp_noCorpProfile() {
+ // Insert a contact into the primary CP2.
+ long rawContactId = ContentUris.parseId(
+ mResolver.insert(RawContacts.CONTENT_URI, new ContentValues()));
+ DataUtil.insertStructuredName(mResolver, rawContactId, "Contact1", "Doe");
+ insertPhoneNumber(rawContactId, "408-111-1111");
+
+ // No corp profile, no data.
+ assertEquals(0, getCount(RawContactsEntity.CORP_CONTENT_URI));
+ }
+
+ public void testQueryRawContactEntitiesCorp_withCorpProfile() throws Exception {
+ // Insert a contact into the primary CP2.
+ long rawContactId = ContentUris.parseId(
+ mResolver.insert(RawContacts.CONTENT_URI, new ContentValues()));
+ DataUtil.insertStructuredName(mResolver, rawContactId, "Contact1", "Doe");
+ insertPhoneNumber(rawContactId, "408-111-1111");
+
+ // Insert a contact into corp CP2.
+ final SynchronousContactsProvider2 corpCp2 = setUpCorpProvider();
+ rawContactId = ContentUris.parseId(
+ corpCp2.insert(RawContacts.CONTENT_URI, new ContentValues()));
+ // Insert a name.
+ ContentValues cv = cv(
+ Data.RAW_CONTACT_ID, rawContactId,
+ Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE,
+ StructuredName.DISPLAY_NAME, "Contact2 Corp");
+ corpCp2.insert(ContactsContract.Data.CONTENT_URI, cv);
+ // Insert a number.
+ cv = cv(
+ Data.RAW_CONTACT_ID, rawContactId,
+ Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE,
+ Phone.NUMBER, "222-222-2222",
+ Phone.TYPE, Phone.TYPE_MOBILE);
+ corpCp2.insert(ContactsContract.Data.CONTENT_URI, cv);
+
+ // Do the query
+ Cursor c = mResolver.query(RawContactsEntity.CORP_CONTENT_URI,
+ new String[]{RawContactsEntity._ID, RawContactsEntity.DATA1},
+ RawContactsEntity.MIMETYPE + "=?", new String[]{
+ StructuredName.CONTENT_ITEM_TYPE}, null);
+ // The result should only contains corp data.
+ assertEquals(1, c.getCount());
+ assertEquals(2, c.getColumnCount());
+ c.moveToPosition(0);
+ long id = c.getLong(c.getColumnIndex(RawContactsEntity._ID));
+ String data1 = c.getString(c.getColumnIndex(RawContactsEntity.DATA1));
+ assertEquals("Contact2 Corp", data1);
+ assertEquals(rawContactId, id);
+ c.close();
+ }
+
public void testUpgradeToVersion910_CallsDeletedForCorpProfileOnly() throws Exception {
CallLogProvider provider =
(CallLogProvider) addProvider(TestCallLogProvider.class, CallLog.AUTHORITY);
@@ -1921,7 +2058,7 @@
assertEquals(0, getCount(Calls.CONTENT_URI));
}
- public void testRewriteCorpPhoneLookup() {
+ public void testRewriteCorpLookup() {
// 19 columns
final MatrixCursor c = new MatrixCursor(new String[] {
PhoneLookup._ID,
@@ -1946,7 +2083,8 @@
});
// First, convert and make sure it returns an empty cursor.
- Cursor rewritten = ContactsProvider2.rewriteCorpPhoneLookup(c);
+ Cursor rewritten = ContactsProvider2.rewriteCorpLookup(c.getColumnNames(), c,
+ PhoneLookup._ID);
assertEquals(0, rewritten.getCount());
assertEquals(19, rewritten.getColumnCount());
@@ -1993,8 +2131,10 @@
"label", // PhoneLookup.LABEL,
"+1234", // PhoneLookup.NORMALIZED_NUMBER
});
- rewritten = ContactsProvider2.rewriteCorpPhoneLookup(c);
+ rewritten = ContactsProvider2.rewriteCorpLookup(c.getColumnNames(), c,
+ PhoneLookup._ID);
assertEquals(2, rewritten.getCount());
+ assertEquals(19, rewritten.getColumnCount());
rewritten.moveToPosition(0);
int column = 0;
@@ -2022,7 +2162,7 @@
rewritten.moveToNext();
column = 0;
assertEquals(1000000010L, rewritten.getLong(column++)); // With offset.
- assertEquals("key", rewritten.getString(column++));
+ assertEquals("c-key", rewritten.getString(column++));
assertEquals("name", rewritten.getString(column++));
assertEquals(123, rewritten.getInt(column++));
assertEquals(456, rewritten.getInt(column++));
@@ -2042,6 +2182,26 @@
assertEquals(1, rewritten.getInt(column++));
assertEquals("label", rewritten.getString(column++));
assertEquals("+1234", rewritten.getString(column++));
+
+ // Use a narower projection.
+ rewritten = ContactsProvider2.rewriteCorpLookup(
+ new String[] {PhoneLookup.PHOTO_URI, PhoneLookup.PHOTO_THUMBNAIL_URI}, c,
+ PhoneLookup._ID);
+ assertEquals(2, rewritten.getCount());
+ assertEquals(2, rewritten.getColumnCount());
+
+ rewritten.moveToPosition(0);
+ column = 0;
+ assertEquals(null, rewritten.getString(column++));
+ assertEquals(null, rewritten.getString(column++));
+
+
+ rewritten.moveToNext();
+ column = 0;
+ assertEquals("content://com.android.contacts/contacts_corp/10/display_photo",
+ rewritten.getString(column++));
+ assertEquals("content://com.android.contacts/contacts_corp/10/photo",
+ rewritten.getString(column++));
}
public void testPhoneUpdate() {
@@ -2488,6 +2648,110 @@
assertStoredValuesOrderly(filterUri1, new ContentValues[] { v2, v1, v4, v3 });
}
+ public void testUpdateFromMetadataEntry() {
+ String accountType1 = "accountType1";
+ String accountName1 = "accountName1";
+ String dataSet1 = "plus";
+ Account account1 = new Account(accountName1, accountType1);
+ long rawContactId = RawContactUtil.createRawContactWithName(mResolver, account1);
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ // Add backup_id for the raw contact.
+ String backupId = "backupId100001";
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.BACKUP_ID, backupId);
+ assertEquals(1, mResolver.update(rawContactUri, values, null, null));
+
+ String emailAddress = "address@email.com";
+ Uri dataUri = insertEmail(rawContactId, emailAddress);
+ String hashId = "hashId100002";
+ ContentValues dataValues = new ContentValues();
+ dataValues.put(Data.HASH_ID, hashId);
+ assertEquals(1, mResolver.update(dataUri, dataValues, null, null));
+
+ // Another data that should not be updated.
+ String phoneNumber = "111-111-1111";
+ Uri dataUri2 = insertPhoneNumber(rawContactId, phoneNumber);
+ String hashId2 = "hashId100004";
+ ContentValues dataValues2 = new ContentValues();
+ dataValues.put(Data.HASH_ID, hashId2);
+ mResolver.update(dataUri2, dataValues2, null, null);
+
+ String accountType2 = "accountType2";
+ String accountName2 = "accountName2";
+ Account account2 = new Account(accountName2, accountType2);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, account2);
+ Uri rawContactUri2 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId2);
+ String backupId2 = "backupId100003";
+ ContentValues values2 = new ContentValues();
+ values2.put(RawContacts.BACKUP_ID, backupId2);
+ assertEquals(1, mResolver.update(rawContactUri2, values2, null, null));
+
+ String usageTypeString = "CALL";
+ int lastTimeUsed = 1111111;
+ int timesUsed = 5;
+ String aggregationTypeString = "SEPARATE";
+ int aggregationType = AggregationExceptions.TYPE_KEEP_SEPARATE;
+
+ RawContactInfo rawContactInfo = new RawContactInfo(
+ backupId, accountType1, accountName1, null);
+ UsageStats usageStats = new UsageStats(usageTypeString, lastTimeUsed, timesUsed);
+ ArrayList<UsageStats> usageStatsList = new ArrayList<>();
+ usageStatsList.add(usageStats);
+ FieldData fieldData = new FieldData(hashId, true, true, usageStatsList);
+ ArrayList<FieldData> fieldDataList = new ArrayList<>();
+ fieldDataList.add(fieldData);
+ ArrayList<AggregationData> aggregationDataList = new ArrayList<>();
+ MetadataEntry metadataEntry = new MetadataEntry(rawContactInfo,
+ 1, 1, 1, fieldDataList, aggregationDataList);
+
+ ContactsProvider2 provider = (ContactsProvider2) getProvider();
+ final ContactsDatabaseHelper helper =
+ ((ContactsDatabaseHelper) provider.getDatabaseHelper());
+ SQLiteDatabase db = helper.getWritableDatabase();
+
+ // Before updating tables from MetadataEntry.
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, accountType1);
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, accountName1);
+ assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "0");
+ assertStoredValue(rawContactUri, RawContacts.STARRED, "0");
+ assertStoredValue(rawContactUri, RawContacts.PINNED, "0");
+ assertStoredValue(dataUri, Data.IS_PRIMARY, 0);
+ assertStoredValue(dataUri, Data.IS_SUPER_PRIMARY, 0);
+
+ // Update tables without aggregation first, since aggregator will affect pinned value.
+ provider.updateFromMetaDataEntry(db, metadataEntry);
+
+ // After updating tables from MetadataEntry.
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_TYPE, accountType1);
+ assertStoredValue(rawContactUri, RawContacts.ACCOUNT_NAME, accountName1);
+ assertStoredValue(rawContactUri, RawContacts.SEND_TO_VOICEMAIL, "1");
+ assertStoredValue(rawContactUri, RawContacts.STARRED, "1");
+ assertStoredValue(rawContactUri, RawContacts.PINNED, "1");
+ assertStoredValue(dataUri, Data.IS_PRIMARY, 1);
+ assertStoredValue(dataUri, Data.IS_SUPER_PRIMARY, 1);
+ final Uri dataUriWithUsageType = Data.CONTENT_URI.buildUpon().appendQueryParameter(
+ DataUsageFeedback.USAGE_TYPE, usageTypeString).build();
+ assertDataUsageCursorContains(dataUriWithUsageType, emailAddress, timesUsed, lastTimeUsed);
+
+ // Update AggregationException table.
+ RawContactInfo aggregationContact = new RawContactInfo(
+ backupId2, accountType2, accountName2, null);
+ AggregationData aggregationData = new AggregationData(
+ rawContactInfo, aggregationContact, aggregationTypeString);
+ aggregationDataList.add(aggregationData);
+ metadataEntry = new MetadataEntry(rawContactInfo,
+ 1, 1, 1, fieldDataList, aggregationDataList);
+ provider.updateFromMetaDataEntry(db, metadataEntry);
+
+ // Check if AggregationException table is updated.
+ assertStoredValue(AggregationExceptions.CONTENT_URI, AggregationExceptions.RAW_CONTACT_ID1,
+ rawContactId);
+ assertStoredValue(AggregationExceptions.CONTENT_URI, AggregationExceptions.RAW_CONTACT_ID2,
+ rawContactId2);
+ assertStoredValue(AggregationExceptions.CONTENT_URI, AggregationExceptions.TYPE,
+ aggregationType);
+ }
+
public void testPostalsQuery() {
long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Alice", "Nextore");
Uri dataUri = insertPostalAddress(rawContactId, "1600 Amphiteatre Ave, Mountain View");
@@ -7252,10 +7516,10 @@
private void assertProviderStatus(int expectedProviderStatus) {
Cursor cursor = mResolver.query(ProviderStatus.CONTENT_URI,
- new String[]{ProviderStatus.DATA1, ProviderStatus.STATUS}, null, null, null);
+ new String[]{ProviderStatus.STATUS}, null, null,
+ null);
assertTrue(cursor.moveToFirst());
- assertEquals(0, cursor.getLong(0));
- assertEquals(expectedProviderStatus, cursor.getInt(1));
+ assertEquals(expectedProviderStatus, cursor.getInt(0));
cursor.close();
}
@@ -8618,6 +8882,102 @@
* End pinning support tests
******************************************************/
+ public void testAuthorization_authorize() throws Exception {
+ // Setup
+ ContentValues values = new ContentValues();
+ long id1 = createContact(values, "Noah", "Tever", "18004664411",
+ "email@email.com", StatusUpdates.OFFLINE, 0, 0, 0, 0);
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id1);
+
+ // Execute: pre authorize the contact
+ Uri authorizedUri = getPreAuthorizedUri(contactUri);
+
+ // Sanity check: URIs are different
+ assertNotSame(authorizedUri, contactUri);
+
+ // Verify: the URI is pre authorized
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ assertTrue(cp.isValidPreAuthorizedUri(authorizedUri));
+ }
+
+ public void testAuthorization_unauthorized() throws Exception {
+ // Setup
+ ContentValues values = new ContentValues();
+ long id1 = createContact(values, "Noah", "Tever", "18004664411",
+ "email@email.com", StatusUpdates.OFFLINE, 0, 0, 0, 0);
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id1);
+
+ // Verify: the URI is *not* pre authorized
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ assertFalse(cp.isValidPreAuthorizedUri(contactUri));
+ }
+
+ public void testAuthorization_invalidAuthorization() throws Exception {
+ // Setup
+ ContentValues values = new ContentValues();
+ long id1 = createContact(values, "Noah", "Tever", "18004664411",
+ "email@email.com", StatusUpdates.OFFLINE, 0, 0, 0, 0);
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id1);
+
+ // Execute: pre authorize the contact and then modify the resulting URI slightly
+ Uri authorizedUri = getPreAuthorizedUri(contactUri);
+ Uri almostAuthorizedUri = Uri.parse(authorizedUri.toString() + "2");
+
+ // Verify: the URI is not pre authorized
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ assertFalse(cp.isValidPreAuthorizedUri(almostAuthorizedUri));
+ }
+
+ public void testAuthorization_expired() throws Exception {
+ // Setup
+ ContentValues values = new ContentValues();
+ long id1 = createContact(values, "Noah", "Tever", "18004664411",
+ "email@email.com", StatusUpdates.OFFLINE, 0, 0, 0, 0);
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id1);
+ sMockClock.install();
+
+ // Execute: pre authorize the contact
+ Uri authorizedUri = getPreAuthorizedUri(contactUri);
+ sMockClock.setCurrentTimeMillis(sMockClock.currentTimeMillis() + 1000000);
+
+ // Verify: the authorization for the URI expired
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ assertFalse(cp.isValidPreAuthorizedUri(authorizedUri));
+ }
+
+ public void testAuthorization_contactUpgrade() throws Exception {
+ ContactsDatabaseHelper helper =
+ ((ContactsDatabaseHelper) ((ContactsProvider2) getProvider()).getDatabaseHelper());
+ SQLiteDatabase db = helper.getWritableDatabase();
+
+ // Perform the unit tests against an upgraded version of the database, instead of a freshly
+ // created version of the database.
+ helper.upgradeToVersion1002(db);
+ testAuthorization_authorize();
+ helper.upgradeToVersion1002(db);
+ testAuthorization_expired();
+ helper.upgradeToVersion1002(db);
+ testAuthorization_expired();
+ helper.upgradeToVersion1002(db);
+ testAuthorization_invalidAuthorization();
+ }
+
+ private Uri getPreAuthorizedUri(Uri uri) {
+ final Bundle uriBundle = new Bundle();
+ uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri);
+ final Bundle authResponse = mResolver.call(
+ ContactsContract.AUTHORITY_URI,
+ ContactsContract.Authorization.AUTHORIZATION_METHOD,
+ null,
+ uriBundle);
+ return (Uri) authResponse.getParcelable(
+ ContactsContract.Authorization.KEY_AUTHORIZED_URI);
+ }
+
+ /**
+ * End Authorization Tests
+ ******************************************************/
+
private Cursor queryGroupMemberships(Account account) {
Cursor c = mResolver.query(TestUtil.maybeAddAccountQueryParameters(Data.CONTENT_URI,
account),
diff --git a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
index 6697ea6..378c9eb 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
@@ -61,7 +61,7 @@
@Override
protected String getAuthority() {
- return Contacts.AUTHORITY;
+ return Contacts.AUTHORITY + ";" + ContactsContract.AUTHORITY;
}
public void testPeopleInsert() {
@@ -857,8 +857,6 @@
}
public void testSettings() throws Exception {
- mActor.addAuthority(ContactsContract.AUTHORITY);
-
ContentValues values = new ContentValues();
values.put(Settings._SYNC_ACCOUNT, "foo");
values.put(Settings._SYNC_ACCOUNT_TYPE, "bar");
diff --git a/tests/src/com/android/providers/contacts/MetadataEntryParserTest.java b/tests/src/com/android/providers/contacts/MetadataEntryParserTest.java
new file mode 100644
index 0000000..be4df57
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/MetadataEntryParserTest.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import com.android.providers.contacts.MetadataEntryParser.AggregationData;
+import com.android.providers.contacts.MetadataEntryParser.FieldData;
+import com.android.providers.contacts.MetadataEntryParser.MetadataEntry;
+import com.android.providers.contacts.MetadataEntryParser.RawContactInfo;
+import com.android.providers.contacts.MetadataEntryParser.UsageStats;
+import org.json.JSONException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Unit tests for {@link MetadataEntryParser}.
+ *
+ * Run the test like this:
+ * <code>
+ adb shell am instrument -e class com.android.providers.contacts.MetadataEntryParserTest -w \
+ com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@SmallTest
+public class MetadataEntryParserTest extends AndroidTestCase {
+
+ public void testErrorForEmptyInput() {
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry("");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testParseDataToMetadataEntry() throws IOException {
+ String contactBackupId = "1111111";
+ String accountType = "facebook";
+ String accountName = "android-test";
+ String dataSet = null;
+ int sendToVoicemail = 1;
+ int starred = 0;
+ int pinned = 2;
+ String dataHashId1 = "1001";
+ String usageType1_1 = "CALL";
+ long lastTimeUsed1_1 = 10000001;
+ int timesUsed1_1 = 10;
+ String usageType1_2 = "SHORT_TEXT";
+ long lastTimeUsed1_2 = 20000002;
+ int timesUsed1_2 = 20;
+ String dataHashId2 = "1002";
+ String usageType2 = "LONG_TEXT";
+ long lastTimeUsed2 = 30000003;
+ int timesUsed2 = 30;
+ String aggregationContactBackupId1 = "2222222";
+ String aggregationAccountType1 = "com.google";
+ String aggregationAccountName1 = "android-test2";
+ String aggregationDataSet1 = "plus";
+ String aggregationContactBackupId2 = "3333333";
+ String aggregationAccountType2 = "com.google";
+ String aggregationAccountName2 = "android-test3";
+ String aggregationDataSet2 = "custom type";
+ String type = "TOGETHER";
+ String inputFile = "test1/testFileDeviceContactMetadataJSON.txt";
+
+ RawContactInfo rawContactInfo = new RawContactInfo(
+ contactBackupId, accountType, accountName, dataSet);
+ RawContactInfo aggregationContact1 = new RawContactInfo(aggregationContactBackupId1,
+ aggregationAccountType1, aggregationAccountName1, aggregationDataSet1);
+ RawContactInfo aggregationContact2 = new RawContactInfo(aggregationContactBackupId2,
+ aggregationAccountType2, aggregationAccountName2, aggregationDataSet2);
+ AggregationData aggregationData = new AggregationData(
+ aggregationContact1, aggregationContact2, type);
+ ArrayList<AggregationData> aggregationDataList = new ArrayList<>();
+ aggregationDataList.add(aggregationData);
+
+ UsageStats usageStats1_1 = new UsageStats(usageType1_1, lastTimeUsed1_1, timesUsed1_1);
+ UsageStats usageStats1_2 = new UsageStats(usageType1_2, lastTimeUsed1_2, timesUsed1_2);
+ UsageStats usageStats2 = new UsageStats(usageType2, lastTimeUsed2, timesUsed2);
+
+ ArrayList<UsageStats> usageStats1List = new ArrayList<>();
+ usageStats1List.add(usageStats1_1);
+ usageStats1List.add(usageStats1_2);
+ FieldData fieldData1 = new FieldData(dataHashId1, true, true, usageStats1List);
+
+ ArrayList<UsageStats> usageStats2List = new ArrayList<>();
+ usageStats2List.add(usageStats2);
+ FieldData fieldData2 = new FieldData(dataHashId2, false, false, usageStats2List);
+
+ ArrayList<FieldData> fieldDataList = new ArrayList<>();
+ fieldDataList.add(fieldData1);
+ fieldDataList.add(fieldData2);
+
+ MetadataEntry expectedResult = new MetadataEntry(rawContactInfo,
+ sendToVoicemail, starred, pinned, fieldDataList, aggregationDataList);
+
+ String inputJson = readAssetAsString(inputFile);
+ MetadataEntry metadataEntry = MetadataEntryParser.parseDataToMetaDataEntry(
+ inputJson.toString());
+ assertMetaDataEntry(expectedResult, metadataEntry);
+ }
+
+ public void testErrorForMissingContactId() {
+ String input = "{\"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"android-test\"\n" +
+ " }}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testErrorForNullContactId() throws JSONException {
+ String input = "{\"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"android-test\",\n" +
+ " \"contact_id\": \"\"\n" +
+ " }}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testErrorForNullAccountType() throws JSONException {
+ String input = "{\"unique_contact_id\": {\n" +
+ " \"account_type\": \"\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"android-test\",\n" +
+ " \"contact_id\": \"\"\n" +
+ " }}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testErrorForNullAccountName() throws JSONException {
+ String input = "{\"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"\",\n" +
+ " \"contact_id\": \"1111111\"\n" +
+ " }}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testErrorForNullFieldDataId() throws JSONException {
+ String input = "{\"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"android-test\",\n" +
+ " \"contact_id\": \"1111111\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": false,\n" +
+ " \"pinned\": 2\n" +
+ " }," +
+ " \"field_data\": [{\n" +
+ " \"field_data_id\": \"\"}]" +
+ "}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ public void testErrorForNullAggregationType() throws JSONException {
+ String input = "{\n" +
+ " \"unique_contact_id\": {\n" +
+ " \"account_type\": \"CUSTOM_ACCOUNT\",\n" +
+ " \"custom_account_type\": \"facebook\",\n" +
+ " \"account_name\": \"android-test\",\n" +
+ " \"contact_id\": \"1111111\"\n" +
+ " },\n" +
+ " \"contact_prefs\": {\n" +
+ " \"send_to_voicemail\": true,\n" +
+ " \"starred\": false,\n" +
+ " \"pinned\": 2\n" +
+ " },\n" +
+ " \"aggregation_data\": [\n" +
+ " {\n" +
+ " \"type\": \"\",\n" +
+ " \"contact_ids\": [\n" +
+ " {\n" +
+ " \"contact_id\": \"2222222\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"contact_id\": \"3333333\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]}";
+ try {
+ MetadataEntryParser.parseDataToMetaDataEntry(input);
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ private String readAssetAsString(String fileName) throws IOException {
+ Context context = getTestContext();
+ InputStream input = context.getAssets().open(fileName);
+ ByteArrayOutputStream contents = new ByteArrayOutputStream();
+ int len;
+ byte[] data = new byte[1024];
+ do {
+ len = input.read(data);
+ if (len > 0) contents.write(data, 0, len);
+ } while (len == data.length);
+ return contents.toString();
+ }
+
+ private void assertMetaDataEntry(MetadataEntry entry1, MetadataEntry entry2) {
+ assertRawContactInfoEquals(entry1.mRawContactInfo, entry2.mRawContactInfo);
+ assertEquals(entry1.mSendToVoicemail, entry2.mSendToVoicemail);
+ assertEquals(entry1.mStarred, entry2.mStarred);
+ assertEquals(entry1.mPinned, entry2.mPinned);
+ assertAggregationDataListEquals(entry1.mAggregationDatas, entry2.mAggregationDatas);
+ assertFieldDataListEquals(entry1.mFieldDatas, entry2.mFieldDatas);
+ }
+
+ private void assertRawContactInfoEquals(RawContactInfo contact1, RawContactInfo contact2) {
+ assertEquals(contact1.mBackupId, contact2.mBackupId);
+ assertEquals(contact1.mAccountType, contact2.mAccountType);
+ assertEquals(contact1.mAccountName, contact2.mAccountName);
+ assertEquals(contact1.mDataSet, contact2.mDataSet);
+ }
+
+ private void assertAggregationDataListEquals(ArrayList<AggregationData> aggregationList1,
+ ArrayList<AggregationData> aggregationList2) {
+ assertEquals(aggregationList1.size(), aggregationList2.size());
+ for (int i = 0; i < aggregationList1.size(); i++) {
+ assertAggregationDataEquals(aggregationList1.get(i), aggregationList2.get(i));
+ }
+ }
+
+ private void assertAggregationDataEquals(AggregationData aggregationData1,
+ AggregationData aggregationData2) {
+ assertRawContactInfoEquals(aggregationData1.mRawContactInfo1,
+ aggregationData2.mRawContactInfo1);
+ assertRawContactInfoEquals(aggregationData1.mRawContactInfo2,
+ aggregationData2.mRawContactInfo2);
+ assertEquals(aggregationData1.mType, aggregationData2.mType);
+ }
+
+ private void assertFieldDataListEquals(ArrayList<FieldData> fieldDataList1,
+ ArrayList<FieldData> fieldDataList2) {
+ assertEquals(fieldDataList1.size(), fieldDataList2.size());
+ for (int i = 0; i < fieldDataList1.size(); i++) {
+ assertFieldDataEquals(fieldDataList1.get(i), fieldDataList2.get(i));
+ }
+ }
+
+ private void assertFieldDataEquals(FieldData fieldData1, FieldData fieldData2) {
+ assertEquals(fieldData1.mDataHashId, fieldData2.mDataHashId);
+ assertEquals(fieldData1.mIsPrimary, fieldData2.mIsPrimary);
+ assertEquals(fieldData1.mIsSuperPrimary, fieldData2.mIsSuperPrimary);
+ assertUsageStatsListEquals(fieldData1.mUsageStatsList, fieldData2.mUsageStatsList);
+ }
+
+ private void assertUsageStatsListEquals(ArrayList<UsageStats> usageStatsList1,
+ ArrayList<UsageStats> usageStatsList2) {
+ assertEquals(usageStatsList1.size(), usageStatsList2.size());
+ for (int i = 0; i < usageStatsList1.size(); i++) {
+ assertUsageStatsEquals(usageStatsList1.get(i), usageStatsList2.get(i));
+ }
+ }
+
+ private void assertUsageStatsEquals(UsageStats usageStats1, UsageStats usageStats2) {
+ assertEquals(usageStats1.mUsageType, usageStats2.mUsageType);
+ assertEquals(usageStats1.mLastTimeUsed, usageStats2.mLastTimeUsed);
+ assertEquals(usageStats1.mTimesUsed, usageStats2.mTimesUsed);
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/MockSyncAdapter.java b/tests/src/com/android/providers/contacts/MockSyncAdapter.java
index 9255d28..199c216 100644
--- a/tests/src/com/android/providers/contacts/MockSyncAdapter.java
+++ b/tests/src/com/android/providers/contacts/MockSyncAdapter.java
@@ -27,6 +27,9 @@
@Override
public IBinder onBind(Intent intent) {
+ // Looks like returning null is okay here, probably because the account type doesn't exist.
+ // If the system complains about it, we need to return a real sync adapter class here,
+ // and in the syncMethod -1 to isSyncable.
return null;
}
}
diff --git a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
index 4fe1907..4f79837 100644
--- a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
@@ -58,7 +58,7 @@
Calls.COUNTRY_ISO
};
/** Total number of columns exposed by voicemail provider. */
- private static final int NUM_VOICEMAIL_FIELDS = 14;
+ private static final int NUM_VOICEMAIL_FIELDS = 18;
@Override
protected void setUp() throws Exception {
@@ -115,11 +115,35 @@
values.put(Voicemails.STATE, 2);
values.put(Voicemails.HAS_CONTENT, 1);
values.put(Voicemails.SOURCE_DATA, "foo");
+ values.put(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, "dummy_name");
+ values.put(Voicemails.PHONE_ACCOUNT_ID, "dummy_account");
int count = mResolver.update(uri, values, null, null);
assertEquals(1, count);
assertStoredValues(uri, values);
}
+ public void testUpdateOwnPackageVoicemail_NotDirty() {
+ final Uri uri = mResolver.insert(voicemailUri(), getTestVoicemailValues());
+ mResolver.update(uri, new ContentValues(), null, null);
+
+ // Updating a package's own voicemail should not make the voicemail dirty.
+ ContentValues values = getTestVoicemailValues();
+ values.put(Voicemails.DIRTY, "0");
+ assertStoredValues(uri, values);
+ }
+
+ public void testUpdateOwnPackageVoicemail_RemovesDirtyStatus() {
+ ContentValues values = getTestVoicemailValues();
+ values.put(Voicemails.DIRTY, "1");
+ final Uri uri = mResolver.insert(voicemailUri(), getTestVoicemailValues());
+
+ mResolver.update(uri, new ContentValues(), null, null);
+ // At this point, the voicemail should be set back to not dirty.
+ ContentValues newValues = getTestVoicemailValues();
+ newValues.put(Voicemails.DIRTY, "0");
+ assertStoredValues(uri, newValues);
+ }
+
public void testDelete() {
Uri uri = insertVoicemail();
int count = mResolver.delete(voicemailUri(), Voicemails._ID + "="
@@ -240,8 +264,10 @@
}
});
- // If we have the manage voicemail permission, we should be able to both update and delete
- // voicemails from all packages
+ // If we have the manage voicemail permission, we should be able to both update voicemails
+ // from all packages. However, when updating or deleting a voicemail from a different
+ // package, the "dirty" flag must be set on updates and "dirty" and "delete" flags must be
+ // set on deletion.
setUpForNoPermission();
mActor.addPermissions(WRITE_VOICEMAIL_PERMISSION);
mResolver.update(anotherVoicemail, getTestVoicemailValues(), null, null);
@@ -254,10 +280,15 @@
mResolver.delete(anotherVoicemail, null, null);
- // Now add the read voicemail permission temporarily to verify that the delete actually
- // worked
+ // Now add the read voicemail permission temporarily to verify that the delete flag is set.
mActor.addPermissions(READ_VOICEMAIL_PERMISSION);
- assertEquals(0, getCount(anotherVoicemail, null, null));
+
+ ContentValues values = getTestVoicemailValues();
+ values.put(Voicemails.DIRTY, "1");
+ values.put(Voicemails.DELETED, "1");
+
+ assertEquals(1, getCount(anotherVoicemail, null, null));
+ assertStoredValues(anotherVoicemail, values);
}
private Uri withSourcePackageParam(Uri uri) {
diff --git a/tests/src/com/android/providers/contacts/aggregation/ContactAggregator2Test.java b/tests/src/com/android/providers/contacts/aggregation/ContactAggregator2Test.java
new file mode 100644
index 0000000..6796a50
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/aggregation/ContactAggregator2Test.java
@@ -0,0 +1,1858 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts.aggregation;
+
+import android.accounts.Account;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.provider.ContactsContract.Contacts.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.providers.contacts.BaseContactsProvider2Test;
+import com.android.providers.contacts.ContactsProvider2;
+import com.android.providers.contacts.TestUtils;
+import com.android.providers.contacts.tests.R;
+import com.android.providers.contacts.testutil.DataUtil;
+import com.android.providers.contacts.testutil.RawContactUtil;
+
+import com.google.android.collect.Lists;
+
+/**
+ * Unit tests for {@link ContactAggregator2}.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -e \
+ * class com.android.providers.contacts.aggregation.ContactAggregator2Test -w \
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@MediumTest
+public class ContactAggregator2Test extends BaseContactsProvider2Test {
+
+ private static final Account ACCOUNT_1 = new Account("account_name_1", "account_type_1");
+ private static final Account ACCOUNT_2 = new Account("account_name_2", "account_type_2");
+ private static final Account ACCOUNT_3 = new Account("account_name_3", "account_type_3");
+
+ private static final String[] AGGREGATION_EXCEPTION_PROJECTION = new String[] {
+ AggregationExceptions.TYPE,
+ AggregationExceptions.RAW_CONTACT_ID1,
+ AggregationExceptions.RAW_CONTACT_ID2
+ };
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ // Enable new aggregator.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setNewAggregatorForTest(true);
+ }
+
+ public void testCrudAggregationExceptions() throws Exception {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "zz", "top");
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "aa", "bottom");
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+
+ String selection = "(" + AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1
+ + " AND " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2
+ + ") OR (" + AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId2
+ + " AND " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId1 + ")";
+
+ // Refetch the row we have just inserted
+ Cursor c = mResolver.query(AggregationExceptions.CONTENT_URI,
+ AGGREGATION_EXCEPTION_PROJECTION, selection, null, null);
+
+ assertTrue(c.moveToFirst());
+ assertEquals(AggregationExceptions.TYPE_KEEP_TOGETHER, c.getInt(0));
+ assertTrue((rawContactId1 == c.getLong(1) && rawContactId2 == c.getLong(2))
+ || (rawContactId2 == c.getLong(1) && rawContactId1 == c.getLong(2)));
+ assertFalse(c.moveToNext());
+ c.close();
+
+ // Change from TYPE_KEEP_IN to TYPE_KEEP_OUT
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId2);
+
+ c = mResolver.query(AggregationExceptions.CONTENT_URI, AGGREGATION_EXCEPTION_PROJECTION,
+ selection, null, null);
+
+ assertTrue(c.moveToFirst());
+ assertEquals(AggregationExceptions.TYPE_KEEP_SEPARATE, c.getInt(0));
+ assertTrue((rawContactId1 == c.getLong(1) && rawContactId2 == c.getLong(2))
+ || (rawContactId2 == c.getLong(1) && rawContactId1 == c.getLong(2)));
+ assertFalse(c.moveToNext());
+ c.close();
+
+ // Delete the rule
+ setAggregationException(AggregationExceptions.TYPE_AUTOMATIC,
+ rawContactId1, rawContactId2);
+
+ // Verify that the row is gone
+ c = mResolver.query(AggregationExceptions.CONTENT_URI, AGGREGATION_EXCEPTION_PROJECTION,
+ selection, null, null);
+ assertFalse(c.moveToFirst());
+ c.close();
+ }
+
+ public void testAggregationCreatesNewAggregate() {
+ long rawContactId = RawContactUtil.createRawContact(mResolver);
+
+ Uri resultUri = DataUtil.insertStructuredName(mResolver, 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 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnb", "Smithb");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnb", "Smithb");
+
+ assertAggregated(rawContactId1, rawContactId2, "Johnb Smithb");
+ }
+
+ public void testAggregationIgnoresInvisibleContact() {
+ Account account = new Account("accountName", "accountType");
+ createAutoAddGroup(account);
+
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
+
+ // Hide by removing from all groups
+ removeGroupMemberships(rawContactId1);
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, account);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
+
+ long rawContactId3 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertAggregated(rawContactId2, rawContactId3, "Flynn Ryder");
+ }
+
+ public void testAggregationOfCaseInsensitiveFullNameMatch() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnc", "Smithc");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnc", "smithc");
+
+ assertAggregated(rawContactId1, rawContactId2, "Johnc Smithc");
+ }
+
+ public void testAggregationOfLastNameMatch() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, null, "Johnd");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, null, "johnd");
+
+ assertAggregated(rawContactId1, rawContactId2, "Johnd");
+ }
+
+ public void testNonAggregationOfFirstNameMatch() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Johne", "Smithe");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Johne", null);
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testNonAggregationOfLastNameMatch() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnf", "Smithf");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, null, "Smithf");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationOfConcatenatedFullNameMatch() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Johng", "Smithg");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "johngsmithg", null);
+
+ assertAggregated(rawContactId1, rawContactId2, "Johng Smithg");
+ }
+
+ public void testAggregationOfNormalizedFullNameMatch() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "H\u00e9l\u00e8ne", "Bj\u00f8rn");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "helene bjorn", null);
+
+ assertAggregated(rawContactId1, rawContactId2, "H\u00e9l\u00e8ne Bj\u00f8rn");
+ }
+
+ public void testAggregationOfNormalizedFullNameMatchWithReadOnlyAccount() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("acct",
+ READ_ONLY_ACCOUNT_TYPE));
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "H\u00e9l\u00e8ne", "Bj\u00f8rn");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "helene bjorn", null);
+
+ assertAggregated(rawContactId1, rawContactId2, "helene bjorn");
+ }
+
+ public void testAggregationOfNumericNames() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "123", null);
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "1-2-3", null);
+
+ assertAggregated(rawContactId1, rawContactId2, "1-2-3");
+ }
+
+ public void testAggregationOfInconsistentlyParsedNames() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+
+ ContentValues values = new ContentValues();
+ values.put(StructuredName.DISPLAY_NAME, "604 Arizona Ave");
+ values.put(StructuredName.GIVEN_NAME, "604");
+ values.put(StructuredName.MIDDLE_NAME, "Arizona");
+ values.put(StructuredName.FAMILY_NAME, "Ave");
+ DataUtil.insertStructuredName(mResolver, rawContactId1, values);
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ values.clear();
+ values.put(StructuredName.DISPLAY_NAME, "604 Arizona Ave");
+ values.put(StructuredName.GIVEN_NAME, "604");
+ values.put(StructuredName.FAMILY_NAME, "Arizona Ave");
+ DataUtil.insertStructuredName(mResolver, rawContactId2, values);
+
+ assertAggregated(rawContactId1, rawContactId2, "604 Arizona Ave");
+ }
+
+ public void testAggregationBasedOnMiddleName() {
+ ContentValues values = new ContentValues();
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ values.put(StructuredName.GIVEN_NAME, "John");
+ values.put(StructuredName.GIVEN_NAME, "Abigale");
+ values.put(StructuredName.FAMILY_NAME, "James");
+
+ DataUtil.insertStructuredName(mResolver, rawContactId1, values);
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ values.clear();
+ values.put(StructuredName.GIVEN_NAME, "John");
+ values.put(StructuredName.GIVEN_NAME, "Marie");
+ values.put(StructuredName.FAMILY_NAME, "James");
+ DataUtil.insertStructuredName(mResolver, rawContactId2, values);
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnPhoneNumberNoNameData() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ insertPhoneNumber(rawContactId1, "(888)555-1231");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ insertPhoneNumber(rawContactId2, "1(888)555-1231");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnPhoneNumberWhenTargetAggregateHasNoName() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ insertPhoneNumber(rawContactId1, "(888)555-1232");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnl", "Smithl");
+ insertPhoneNumber(rawContactId2, "1(888)555-1232");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnPhoneNumberWhenNewContactHasNoName() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnm", "Smithm");
+ insertPhoneNumber(rawContactId1, "(888)555-1233");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ insertPhoneNumber(rawContactId2, "1(888)555-1233");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnPhoneNumberWithDifferentNames() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Baby", "Bear");
+ insertPhoneNumber(rawContactId1, "(888)555-1235");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Blind", "Mouse");
+ insertPhoneNumber(rawContactId2, "1(888)555-1235");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnPhoneNumberWithJustFirstName() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Chick", "Notnull");
+ insertPhoneNumber(rawContactId1, "(888)555-1236");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Chick", null);
+ insertPhoneNumber(rawContactId2, "1(888)555-1236");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnEmailNoNameData() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ insertEmail(rawContactId1, "lightning@android.com");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ insertEmail(rawContactId2, "lightning@android.com");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnEmailWhenTargetAggregateHasNoName() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ insertEmail(rawContactId1, "mcqueen@android.com");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Lightning", "McQueen");
+ insertEmail(rawContactId2, "mcqueen@android.com");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnEmailWhenNewContactHasNoName() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Doc", "Hudson");
+ insertEmail(rawContactId1, "doc@android.com");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ insertEmail(rawContactId2, "doc@android.com");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationBasedOnEmailWithDifferentNames() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Chick", "Hicks");
+ insertEmail(rawContactId1, "hicky@android.com");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Luigi", "Guido");
+ insertEmail(rawContactId2, "hicky@android.com");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationByCommonNicknameWithLastName() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Bill", "Gore");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "William", "Gore");
+
+ assertAggregated(rawContactId1, rawContactId2, "William Gore");
+ }
+
+ public void testAggregationByCommonNicknameOnly() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Lawrence", null);
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Larry", null);
+
+ assertAggregated(rawContactId1, rawContactId2, "Lawrence");
+ }
+
+ public void testAggregationByNicknameNoStructuredName() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ insertNickname(rawContactId1, "Frozone");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ insertNickname(rawContactId2, "Frozone");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationByNicknameWithDifferentNames() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Helen", "Parr");
+ insertNickname(rawContactId1, "Elastigirl");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Shawn", "Johnson");
+ insertNickname(rawContactId2, "Elastigirl");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testNonAggregationOnOrganization() {
+ ContentValues values = new ContentValues();
+ values.put(Organization.TITLE, "Monsters, Inc");
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ insertOrganization(rawContactId1, values);
+ insertNickname(rawContactId1, "Boo");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ insertOrganization(rawContactId2, values);
+ insertNickname(rawContactId2, "Rendall"); // To force reaggregation
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationByIdentity() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ insertIdentity(rawContactId1, "iden1", "namespace1");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ insertIdentity(rawContactId2, "iden1", "namespace1");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationExceptionKeepIn() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnk", "Smithk");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnkx", "Smithkx");
+
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ 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 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnh", "Smithh");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnh", "Smithh");
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId2);
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationExceptionKeepOutCheckUpdatesDisplayName() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Johni", "Smithi");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnj", "Smithj");
+
+ long rawContactId3 = RawContactUtil.createRawContact(mResolver, ACCOUNT_3);
+ DataUtil.insertStructuredName(mResolver, rawContactId3, "Johnm", "Smithm");
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId3);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId2, rawContactId3);
+
+ assertAggregated(rawContactId1, rawContactId2, "Johnm Smithm");
+ assertAggregated(rawContactId1, rawContactId3);
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId2);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId3);
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ assertNotAggregated(rawContactId1, rawContactId3);
+
+ String displayName1 = queryDisplayName(queryContactId(rawContactId1));
+ assertEquals("Johni Smithi", displayName1);
+
+ assertAggregated(rawContactId2, rawContactId3, "Johnm Smithm");
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId2, rawContactId3);
+ assertNotAggregated(rawContactId1, rawContactId2);
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+
+ String displayName2 = queryDisplayName(queryContactId(rawContactId1));
+ assertEquals("Johni Smithi", displayName2);
+
+ String displayName3 = queryDisplayName(queryContactId(rawContactId2));
+ assertEquals("Johnj Smithj", displayName3);
+
+ String displayName4 = queryDisplayName(queryContactId(rawContactId3));
+ assertEquals("Johnm Smithm", displayName4);
+ }
+
+ public void testAggregationExceptionKeepOutCheckResultDisplayNames() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "c", "c", ACCOUNT_1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "b", "b", ACCOUNT_2);
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "a", "a", ACCOUNT_3);
+
+ // Join all contacts
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId3);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId2, rawContactId3);
+
+ // Separate all contacts. The order (2-3 , 1-2, 1-3) is important
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId2, rawContactId3);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId2);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId3);
+
+ // Verify that we have three different contacts
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ long contactId3 = queryContactId(rawContactId3);
+
+ assertTrue(contactId1 != contactId2);
+ assertTrue(contactId1 != contactId3);
+ assertTrue(contactId2 != contactId3);
+
+ // Verify that each raw contact contribute to the contact display name
+ assertDisplayNameEquals(contactId1, rawContactId1);
+ assertDisplayNameEquals(contactId2, rawContactId2);
+ assertDisplayNameEquals(contactId3, rawContactId3);
+ }
+
+ public void testNonAggregationWithMultipleAffinities() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ assertNotAggregated(rawContactId1, rawContactId2);
+
+ // There are two aggregates this raw contact could join, so it should join neither
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_2);
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+
+ // Just in case - let's make sure the original two did not get aggregated in the process
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testReaggregateBecauseOfMultipleAffinities() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_2);
+ assertAggregated(rawContactId1, rawContactId2);
+
+ // The aggregate this raw contact could join has a raw contact from the same account,
+ // The ambiguity will trigger re-aggregation. And since no data matching exists, all
+ // three raw contacts are broken-up.
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregation_notAggregateByPhoneticName() {
+ // Different names, but have the same phonetic name. Shouldn't be aggregated.
+
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Sergey", null, "Yamada");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Lawrence", null, "Yamada");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregation_notAggregateByPhoneticName_2() {
+ // Have the same phonetic name. One has a regular name too. Shouldn't be aggregated.
+
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, null, null, "Yamada");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Lawrence", null, "Yamada");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregation_PhoneticNameOnly() {
+ // If a contact has no name but a phonetic name, then its display will be set from the
+ // phonetic name. In this case, we still aggregate by the display name.
+
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, null, null, "Yamada");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, null, null, "Yamada");
+
+ assertAggregated(rawContactId1, rawContactId2, "Yamada");
+ }
+
+ public void testReaggregationWhenBecomesInvisible() {
+ Account account = new Account("accountName", "accountType");
+ createAutoAddGroup(account);
+
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
+
+ long rawContactId3 = RawContactUtil.createRawContact(mResolver, account);
+ DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
+
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+ assertNotAggregated(rawContactId1, rawContactId2);
+
+ // Hide by removing from all groups
+ removeGroupMemberships(rawContactId3);
+
+ assertAggregated(rawContactId1, rawContactId2, "Flynn Ryder");
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+ }
+
+ public void testReaggregationWhenBecomesInvisibleSecondaryDataMatch() {
+ Account account = new Account("accountName", "accountType");
+ createAutoAddGroup(account);
+
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
+
+ long rawContactId3 = RawContactUtil.createRawContact(mResolver, account);
+ DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
+
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+ assertNotAggregated(rawContactId1, rawContactId2);
+
+ // Hide by removing from all groups
+ removeGroupMemberships(rawContactId3);
+
+ assertAggregated(rawContactId1, rawContactId2, "Flynn Ryder");
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+ }
+
+ public void testReaggregationWhenBecomesVisible() {
+ Account account = new Account("accountName", "accountType");
+ long groupId = createAutoAddGroup(account);
+
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
+
+ long rawContactId3 = RawContactUtil.createRawContact(mResolver, account);
+ removeGroupMemberships(rawContactId3);
+ DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
+
+ assertAggregated(rawContactId1, rawContactId2, "Flynn Ryder");
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+
+ insertGroupMembership(rawContactId3, groupId);
+
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testNonSplitBecauseOfMultipleAffinitiesWhenOverridden() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_2);
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_3);
+ assertAggregated(rawContactId1, rawContactId2);
+ assertAggregated(rawContactId1, rawContactId3);
+ setAggregationException(
+ AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
+ assertAggregated(rawContactId1, rawContactId2);
+ assertAggregated(rawContactId1, rawContactId3);
+
+ // The aggregate this raw contact could join has a raw contact from the same account,
+ // Let's re-aggregate the existing aggregate because of the ambiguity
+ long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ assertAggregated(rawContactId1, rawContactId2); // Aggregation exception
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId1, rawContactId4);
+ assertNotAggregated(rawContactId3, rawContactId4);
+ }
+
+ public void testNonSplitWhenIdentityMatch() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertIdentity(rawContactId1, "iden", "namespace");
+ insertIdentity(rawContactId1, "iden2", "namespace");
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_2);
+ insertIdentity(rawContactId2, "iden", "namespace");
+ assertAggregated(rawContactId1, rawContactId2);
+
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ assertAggregated(rawContactId1, rawContactId2);
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+ }
+
+ public void testReAggregateToConnectedComponent() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertPhoneNumber(rawContactId1, "111");
+ setRawContactCustomization(rawContactId1, 1, 1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_2);
+ insertPhoneNumber(rawContactId2, "111");
+ setRawContactCustomization(rawContactId2, 1, 1);
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_3);
+ insertIdentity(rawContactId3, "iden", "namespace");
+ long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ new Account("account_name_4", "account_type_4"));
+ insertIdentity(rawContactId4, "iden", "namespace");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ assertAggregated(rawContactId1, rawContactId3);
+ assertAggregated(rawContactId1, rawContactId4);
+ assertStoredValue(getContactUriForRawContact(rawContactId1),
+ Contacts.STARRED, 1);
+ assertStoredValue(getContactUriForRawContact(rawContactId4),
+ Contacts.SEND_TO_VOICEMAIL, 0);
+
+ long rawContactId5 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+
+ assertAggregated(rawContactId1, rawContactId2);
+ assertAggregated(rawContactId3, rawContactId4);
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId1, rawContactId5);
+ assertNotAggregated(rawContactId3, rawContactId5);
+ assertStoredValue(getContactUriForRawContact(rawContactId1),
+ Contacts.STARRED, 1);
+ assertStoredValue(getContactUriForRawContact(rawContactId1),
+ Contacts.SEND_TO_VOICEMAIL, 1);
+
+ assertStoredValue(getContactUriForRawContact(rawContactId3),
+ Contacts.STARRED, 0);
+ assertStoredValue(getContactUriForRawContact(rawContactId3),
+ Contacts.SEND_TO_VOICEMAIL, 0);
+
+ assertStoredValue(getContactUriForRawContact(rawContactId5),
+ Contacts.STARRED, 0);
+ assertStoredValue(getContactUriForRawContact(rawContactId5),
+ Contacts.SEND_TO_VOICEMAIL, 0);
+ }
+
+ public void testNonAggregationFromSameAccountWithoutAnyDataMatching() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testNonAggregationFromSameAccountNoCommonData() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertEmail(rawContactId1, "lightning1@android.com");
+ insertPhoneNumber(rawContactId1, "111-222-3333");
+ insertIdentity(rawContactId1, "iden1", "namespace");
+
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertEmail(rawContactId2, "lightning2@android.com");
+ insertPhoneNumber(rawContactId2, "555-666-7777");
+ insertIdentity(rawContactId1, "iden2", "namespace");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationFromSameAccountEmailSame_IgnoreCase() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertEmail(rawContactId1, "lightning@android.com");
+
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertEmail(rawContactId2, "lightning@android.com");
+
+ assertAggregated(rawContactId1, rawContactId2);
+
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "Jane", "Doe",
+ ACCOUNT_1);
+ insertEmail(rawContactId3, "jane@android.com");
+
+ long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, "Jane", "Doe",
+ ACCOUNT_1);
+ insertEmail(rawContactId4, "JANE@ANDROID.COM");
+
+ assertAggregated(rawContactId3, rawContactId4);
+ }
+
+ public void testNonAggregationFromSameAccountEmailDifferent() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertEmail(rawContactId1, "lightning1@android.com");
+
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertEmail(rawContactId2, "lightning2@android.com");
+ insertEmail(rawContactId2, "lightning3@android.com");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationFromSameAccountIdentitySame() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertIdentity(rawContactId1, "iden", "namespace");
+
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertIdentity(rawContactId2, "iden", "namespace");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testNonAggregationFromSameAccountIdentityDifferent() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertIdentity(rawContactId1, "iden1", "namespace1");
+ insertIdentity(rawContactId1, "iden2", "namespace2");
+
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertIdentity(rawContactId2, "iden2", "namespace1");
+ insertIdentity(rawContactId2, "iden1", "namespace2");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationFromSameAccountPhoneNumberSame() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertPhoneNumber(rawContactId1, "111-222-3333");
+
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertPhoneNumber(rawContactId2, "111-222-3333");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationFromSameAccountPhoneNumberNormalizedSame() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertPhoneNumber(rawContactId1, "111-222-3333");
+
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertPhoneNumber(rawContactId2, "+1-111-222-3333");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testNonAggregationFromSameAccountPhoneNumberDifferent() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertPhoneNumber(rawContactId1, "111-222-3333");
+
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertPhoneNumber(rawContactId2, "111-222-3334");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationSuggestionsBasedOnName() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Duane", null);
+
+ // Exact name match
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Duane", null);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId2);
+
+ // Edit distance == 0.84
+ long rawContactId3 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId3, "Dwayne", null);
+
+ // Edit distance == 0.6
+ long rawContactId4 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, 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 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Lord", "Farquaad");
+ insertPhoneNumber(rawContactId1, "(888)555-1236");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, 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 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Carl", "Fredricksen");
+ insertEmail(rawContactId1, "up@android.com");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, 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 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Bob", null);
+ insertEmail(rawContactId1, "incredible@android.com");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Lucius", "Best");
+ insertEmail(rawContactId2, "incrediball@android.com");
+
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ assertTrue(contactId1 != contactId2);
+
+ assertSuggestions(contactId1, contactId2);
+ }
+
+ public void testAggregationSuggestionsBasedOnNickname() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Peter", "Parker");
+ insertNickname(rawContactId1, "Spider-Man");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Manny", "Spider");
+
+ long contactId1 = queryContactId(rawContactId1);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId2);
+
+ long contactId2 = queryContactId(rawContactId2);
+ assertSuggestions(contactId1, contactId2);
+ }
+
+ public void testAggregationSuggestionsBasedOnNicknameMatchingName() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Clark", "Kent");
+ insertNickname(rawContactId1, "Superman");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Roy", "Williams");
+ insertNickname(rawContactId2, "superman");
+
+ long contactId1 = queryContactId(rawContactId1);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId2);
+
+ long contactId2 = queryContactId(rawContactId2);
+ assertSuggestions(contactId1, contactId2);
+ }
+
+ public void testAggregationSuggestionsBasedOnCommonNickname() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Dick", "Cherry");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Richard", "Cherry");
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId2);
+
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ assertSuggestions(contactId1, contactId2);
+ }
+
+ public void testAggregationSuggestionsBasedOnPhoneNumberWithFilter() {
+
+ // Create two contacts that would not be aggregated because of name mismatch
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Lord", "Farquaad");
+ insertPhoneNumber(rawContactId1, "(888)555-1236");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Talking", "Donkey");
+ insertPhoneNumber(rawContactId2, "1(888)555-1236");
+
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+ assertTrue(contactId1 != contactId2);
+
+ assertSuggestions(contactId1, "talk", contactId2);
+ assertSuggestions(contactId1, "don", contactId2);
+ assertSuggestions(contactId1, "", contactId2);
+ assertSuggestions(contactId1, "eddie");
+ }
+
+ public void testAggregationSuggestionsDontSuggestInvisible() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "first", "last",
+ ACCOUNT_1);
+ insertPhoneNumber(rawContactId1, "111-222-3333");
+ insertNickname(rawContactId1, "Superman");
+ insertEmail(rawContactId1, "incredible@android.com");
+
+ // Create another with the exact same name, phone number, nickname and email.
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "first", "last",
+ ACCOUNT_2);
+ insertPhoneNumber(rawContactId2, "111-222-3333");
+ insertNickname(rawContactId2, "Superman");
+ insertEmail(rawContactId2, "incredible@android.com");
+
+ // The aggregator should have joined them. Split them up.
+ setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
+ rawContactId1, rawContactId2);
+
+ long contactId1 = queryContactId(rawContactId1);
+ long contactId2 = queryContactId(rawContactId2);
+
+ // Make sure they're different contacts.
+ MoreAsserts.assertNotEqual(contactId1, contactId2);
+
+ // Contact 2 should be suggested.
+ assertSuggestions(contactId1, contactId2);
+
+ // Make contact 2 invisible.
+ markInvisible(contactId2);
+
+ // Now contact 2 shuldn't be suggested.
+ assertSuggestions(contactId1, new long[0]);
+ }
+
+ public void testChoosePhotoSetBeforeAggregation() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId1, "donut", "donut_act");
+ insertPhoto(rawContactId1);
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+ long cupcakeId = ContentUris.parseId(insertPhoto(rawContactId2));
+
+ long rawContactId3 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId3, "froyo", "froyo_act");
+ insertPhoto(rawContactId3);
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId3);
+ assertEquals(cupcakeId, queryPhotoId(queryContactId(rawContactId2)));
+ }
+
+ public void testChoosePhotoSetAfterAggregation() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId1, "donut", "donut_act");
+ insertPhoto(rawContactId1);
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+ setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+ long cupcakeId = ContentUris.parseId(insertPhoto(rawContactId2));
+
+ long rawContactId3 = RawContactUtil.createRawContact(mResolver);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId3);
+ setContactAccount(rawContactId3, "froyo", "froyo_act");
+ insertPhoto(rawContactId3);
+
+ assertEquals(cupcakeId, queryPhotoId(queryContactId(rawContactId2)));
+ }
+
+ // Note that for the following tests of photo aggregation, the accounts are being used to
+ // set the typical photo priority that each raw contact would have, based on
+ // SynchronousContactsProvider2.createPhotoPriorityResolver. The relative priorities
+ // specified there are:
+ // cupcake: 3
+ // donut: 2
+ // froyo: 1
+ // <other>: 0
+
+ public void testChooseLargerPhotoByDimensions() {
+ // Donut photo is 256x256.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId1, "donut", "donut_act");
+ long normalEarthDataId = ContentUris.parseId(
+ insertPhoto(rawContactId1, R.drawable.earth_normal));
+ long normalEarthPhotoFileId = getStoredLongValue(
+ ContentUris.withAppendedId(Data.CONTENT_URI, normalEarthDataId),
+ Photo.PHOTO_FILE_ID);
+
+ // Cupcake would normally have priority, but its photo is 200x200.
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+ insertPhoto(rawContactId2, R.drawable.earth_200);
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+
+ // Larger photo (by dimensions) wins.
+ assertEquals(normalEarthPhotoFileId, queryPhotoFileId(queryContactId(rawContactId1)));
+ }
+
+ public void testChooseLargerPhotoByFileSize() {
+ // Donut photo is a 256x256 photo of a nebula.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId1, "donut", "donut_act");
+ long nebulaDataId = ContentUris.parseId(
+ insertPhoto(rawContactId1, R.drawable.nebula));
+ long nebulaPhotoFileId = getStoredLongValue(
+ ContentUris.withAppendedId(Data.CONTENT_URI, nebulaDataId),
+ Photo.PHOTO_FILE_ID);
+
+ // Cupcake would normally have priority, but its photo (of a galaxy) has the same dimensions
+ // as Donut's, but a smaller filesize.
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+ insertPhoto(rawContactId2, R.drawable.galaxy);
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+
+ // Larger photo (by filesize) wins.
+ assertEquals(nebulaPhotoFileId, queryPhotoFileId(queryContactId(rawContactId1)));
+ }
+
+ public void testChooseFilePhotoOverThumbnail() {
+ // Donut photo is a 256x256 photo of Earth.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId1, "donut", "donut_act");
+ long normalEarthDataId = ContentUris.parseId(
+ insertPhoto(rawContactId1, R.drawable.earth_normal));
+ long normalEarthPhotoFileId = getStoredLongValue(
+ ContentUris.withAppendedId(Data.CONTENT_URI, normalEarthDataId),
+ Photo.PHOTO_FILE_ID);
+
+ // Cupcake would normally have priority, but its photo of Earth is thumbnail-sized.
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+ insertPhoto(rawContactId2, R.drawable.earth_small);
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+
+ // Larger photo (by filesize) wins.
+ assertEquals(normalEarthPhotoFileId, queryPhotoFileId(queryContactId(rawContactId1)));
+ }
+
+ public void testFallbackToAccountPriorityForSamePhoto() {
+ // Donut photo is a 256x256 photo of Earth.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId1, "donut", "donut_act");
+ insertPhoto(rawContactId1, R.drawable.earth_normal);
+
+ // Cupcake has the same 256x256 photo of Earth.
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+ long cupcakeEarthDataId = ContentUris.parseId(
+ insertPhoto(rawContactId2, R.drawable.earth_normal));
+ long cupcakeEarthPhotoFileId = getStoredLongValue(
+ ContentUris.withAppendedId(Data.CONTENT_URI, cupcakeEarthDataId),
+ Photo.PHOTO_FILE_ID);
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+
+ // Cupcake's version of the photo wins, falling back to account priority.
+ assertEquals(cupcakeEarthPhotoFileId, queryPhotoFileId(queryContactId(rawContactId1)));
+ }
+
+ public void testFallbackToAccountPriorityForDifferingThumbnails() {
+ // Donut photo is a 96x96 thumbnail of Earth.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId1, "donut", "donut_act");
+ insertPhoto(rawContactId1, R.drawable.earth_small);
+
+ // Cupcake photo is the 96x96 "no contact" placeholder (smaller filesize than the Earth
+ // picture, but thumbnail filesizes are ignored in the aggregator).
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ setContactAccount(rawContactId2, "cupcake", "cupcake_act");
+ long cupcakeDataId = ContentUris.parseId(
+ insertPhoto(rawContactId2, R.drawable.ic_contact_picture));
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
+
+ // The Cupcake thumbnail wins, by account priority..
+ assertEquals(cupcakeDataId, queryPhotoId(queryContactId(rawContactId1)));
+ }
+
+ public void testDisplayNameSources() {
+ long rawContactId = RawContactUtil.createRawContact(mResolver);
+ long contactId = queryContactId(rawContactId);
+
+ assertNull(queryDisplayName(contactId));
+
+ insertEmail(rawContactId, "eclair@android.com");
+ assertEquals("eclair@android.com", queryDisplayName(contactId));
+
+ insertPhoneNumber(rawContactId, "800-555-5555");
+ assertEquals("800-555-5555", queryDisplayName(contactId));
+
+ ContentValues values = new ContentValues();
+ values.put(Organization.COMPANY, "Android");
+ insertOrganization(rawContactId, values);
+ assertEquals("Android", queryDisplayName(contactId));
+
+ insertNickname(rawContactId, "Dro");
+ assertEquals("Dro", queryDisplayName(contactId));
+
+ values.clear();
+ values.put(StructuredName.GIVEN_NAME, "Eclair");
+ values.put(StructuredName.FAMILY_NAME, "Android");
+ DataUtil.insertStructuredName(mResolver, rawContactId, values);
+ assertEquals("Eclair Android", queryDisplayName(contactId));
+ }
+
+ public void testMergeSuperPrimaryName_rawContact1() {
+ // Setup: raw contact #1 has a super primary name. #2 doesn't.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "name1", null, null,
+ /* isSuperPrimary = */ true);
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "name2", null, null,
+ /* isSuperPrimary = */ false);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: the aggregate's name comes from raw contact #1
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name1", queryDisplayName(contactId));
+ }
+
+ public void testMergeSuperPrimaryName_rawContact2AndEdit() {
+ // Setup: raw contact #2 has a super primary name. #1 doesn't.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ final Uri nameUri1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "name1",
+ null, null, /* isSuperPrimary = */ false);
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ final Uri nameUri2 = DataUtil.insertStructuredName(mResolver, rawContactId2, "name2", null,
+ null, /* isSuperPrimary = */ true);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: the aggregate's name comes from raw contact #2. This is the opposite of the check
+ // inside testMergeSuperPrimaryName_rawContact1().
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name2", queryDisplayName(contactId));
+
+ // Action: edit the super primary name
+ final ContentValues values = new ContentValues();
+ values.put(StructuredName.GIVEN_NAME, "edited name");
+ mResolver.update(nameUri2, values, null, null);
+
+ // Verify: editing the super primary name affects aggregate name
+ assertEquals("edited name", queryDisplayName(contactId));
+
+ // Action: edit the non primary name
+ values.put(StructuredName.GIVEN_NAME, "edited name2");
+ mResolver.update(nameUri1, values, null, null);
+
+ // Verify: aggregate name is still based off the primary name
+ assertEquals("edited name", queryDisplayName(contactId));
+ }
+
+ public void testMergedSuperPrimaryName_changeSuperPrimary() {
+ // Setup: aggregated contact where raw contact #1 has a super primary name. #2 doesn't.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ final Uri nameUri1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "name1",
+ null, null, /* isSuperPrimary = */ true);
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ final Uri nameUri2 = DataUtil.insertStructuredName(mResolver, rawContactId2, "name2", null,
+ null, /* isSuperPrimary = */ false);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Action: make raw contact 2's name super primary
+ storeValue(nameUri2, Data.IS_SUPER_PRIMARY, 1);
+
+ // Sanity check.
+ assertStoredValue(nameUri1, Data.IS_SUPER_PRIMARY, 0);
+ assertStoredValue(nameUri2, Data.IS_SUPER_PRIMARY, 1);
+
+ // Verify: aggregate name is based off of the newly super primary name
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name2", queryDisplayName(contactId));
+ }
+
+ public void testAggregationModeSuspendedSeparateTransactions() {
+
+ // Setting aggregation mode to SUSPENDED should prevent aggregation from happening
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ storeValue(RawContacts.CONTENT_URI, rawContactId1,
+ RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+ Uri name1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "THE", "SAME");
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ storeValue(RawContacts.CONTENT_URI, rawContactId2,
+ RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "THE", "SAME");
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+
+ // Changing aggregation mode to DEFAULT should change nothing
+ storeValue(RawContacts.CONTENT_URI, rawContactId1,
+ RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+ storeValue(RawContacts.CONTENT_URI, rawContactId2,
+ RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+ assertNotAggregated(rawContactId1, rawContactId2);
+
+ // Changing the name should trigger aggregation
+ storeValue(name1, StructuredName.GIVEN_NAME, "the");
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationModeInitializedAsSuspended() throws Exception {
+
+ // Setting aggregation mode to SUSPENDED should prevent aggregation from happening
+ ContentProviderOperation cpo1 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+ .build();
+ ContentProviderOperation cpo2 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+ .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(StructuredName.GIVEN_NAME, "John")
+ .withValue(StructuredName.FAMILY_NAME, "Doe")
+ .build();
+ ContentProviderOperation cpo3 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+ .build();
+ ContentProviderOperation cpo4 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 2)
+ .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(StructuredName.GIVEN_NAME, "John")
+ .withValue(StructuredName.FAMILY_NAME, "Doe")
+ .build();
+ ContentProviderOperation cpo5 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+ .withSelection(RawContacts._ID + "=?", new String[1])
+ .withSelectionBackReference(0, 0)
+ .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT)
+ .build();
+ ContentProviderOperation cpo6 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+ .withSelection(RawContacts._ID + "=?", new String[1])
+ .withSelectionBackReference(0, 2)
+ .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT)
+ .build();
+
+ ContentProviderResult[] results =
+ mResolver.applyBatch(ContactsContract.AUTHORITY,
+ Lists.newArrayList(cpo1, cpo2, cpo3, cpo4, cpo5, cpo6));
+
+ long rawContactId1 = ContentUris.parseId(results[0].uri);
+ long rawContactId2 = ContentUris.parseId(results[2].uri);
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationModeUpdatedToSuspended() throws Exception {
+
+ // Setting aggregation mode to SUSPENDED should prevent aggregation from happening
+ ContentProviderOperation cpo1 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValues(new ContentValues())
+ .build();
+ ContentProviderOperation cpo2 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+ .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(StructuredName.GIVEN_NAME, "John")
+ .withValue(StructuredName.FAMILY_NAME, "Doe")
+ .build();
+ ContentProviderOperation cpo3 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValues(new ContentValues())
+ .build();
+ ContentProviderOperation cpo4 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 2)
+ .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(StructuredName.GIVEN_NAME, "John")
+ .withValue(StructuredName.FAMILY_NAME, "Doe")
+ .build();
+ ContentProviderOperation cpo5 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+ .withSelection(RawContacts._ID + "=?", new String[1])
+ .withSelectionBackReference(0, 0)
+ .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+ .build();
+ ContentProviderOperation cpo6 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+ .withSelection(RawContacts._ID + "=?", new String[1])
+ .withSelectionBackReference(0, 2)
+ .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+ .build();
+
+ ContentProviderResult[] results =
+ mResolver.applyBatch(ContactsContract.AUTHORITY,
+ Lists.newArrayList(cpo1, cpo2, cpo3, cpo4, cpo5, cpo6));
+
+ long rawContactId1 = ContentUris.parseId(results[0].uri);
+ long rawContactId2 = ContentUris.parseId(results[2].uri);
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationModeSuspendedOverriddenByAggException() throws Exception {
+ ContentProviderOperation cpo1 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValue(RawContacts.ACCOUNT_NAME, "a")
+ .withValue(RawContacts.ACCOUNT_TYPE, "b")
+ .build();
+ ContentProviderOperation cpo2 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+ .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(StructuredName.GIVEN_NAME, "John")
+ .withValue(StructuredName.FAMILY_NAME, "Doe")
+ .build();
+ ContentProviderOperation cpo3 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValue(RawContacts.ACCOUNT_NAME, "c")
+ .withValue(RawContacts.ACCOUNT_TYPE, "d")
+ .build();
+ ContentProviderOperation cpo4 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 2)
+ .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(StructuredName.GIVEN_NAME, "John")
+ .withValue(StructuredName.FAMILY_NAME, "Doe")
+ .build();
+ ContentProviderOperation cpo5 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+ .withSelection(RawContacts._ID + "=?", new String[1])
+ .withSelectionBackReference(0, 0)
+ .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+ .build();
+ ContentProviderOperation cpo6 = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI)
+ .withSelection(RawContacts._ID + "=?", new String[1])
+ .withSelectionBackReference(0, 2)
+ .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED)
+ .build();
+
+ // Checking that aggregation mode SUSPENDED should be overridden by inserting
+ // an explicit aggregation exception
+ ContentProviderOperation cpo7 =
+ ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI)
+ .withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, 0)
+ .withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, 2)
+ .withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER)
+ .build();
+
+ ContentProviderResult[] results =
+ mResolver.applyBatch(ContactsContract.AUTHORITY,
+ Lists.newArrayList(cpo1, cpo2, cpo3, cpo4, cpo5, cpo6, cpo7));
+
+ long rawContactId1 = ContentUris.parseId(results[0].uri);
+ long rawContactId2 = ContentUris.parseId(results[2].uri);
+
+ assertAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testAggregationSuggestionsQueryBuilderWithContactId() throws Exception {
+ Uri uri = AggregationSuggestions.builder().setContactId(12).setLimit(7).build();
+ assertEquals("content://com.android.contacts/contacts/12/suggestions?limit=7",
+ uri.toString());
+ }
+
+ public void testAggregationSuggestionsQueryBuilderWithValues() throws Exception {
+ Uri uri = AggregationSuggestions.builder()
+ .addNameParameter("name1")
+ .addNameParameter("name2")
+ .setLimit(7)
+ .build();
+ assertEquals("content://com.android.contacts/contacts/0/suggestions?"
+ + "limit=7"
+ + "&query=name%3Aname1"
+ + "&query=name%3Aname2", uri.toString());
+ }
+
+ public void testAggregatedStatusUpdate() {
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+ Uri dataUri1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "john", "doe");
+ insertStatusUpdate(ContentUris.parseId(dataUri1), StatusUpdates.AWAY, "Green", 100,
+ StatusUpdates.CAPABILITY_HAS_CAMERA);
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+ Uri dataUri2 = DataUtil.insertStructuredName(mResolver, rawContactId2, "john", "doe");
+ insertStatusUpdate(ContentUris.parseId(dataUri2), StatusUpdates.AVAILABLE, "Red", 50,
+ StatusUpdates.CAPABILITY_HAS_CAMERA);
+ setAggregationException(
+ AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
+
+ assertStoredValue(getContactUriForRawContact(rawContactId1),
+ Contacts.CONTACT_STATUS, "Green");
+
+ // When we split these two raw contacts, their respective statuses should be restored
+ setAggregationException(
+ AggregationExceptions.TYPE_KEEP_SEPARATE, rawContactId1, rawContactId2);
+
+ assertStoredValue(getContactUriForRawContact(rawContactId1),
+ Contacts.CONTACT_STATUS, "Green");
+
+ assertStoredValue(getContactUriForRawContact(rawContactId2),
+ Contacts.CONTACT_STATUS, "Red");
+ }
+
+ public void testAggregationSuggestionsByName() throws Exception {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "first1", "last1");
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "first2", "last2");
+
+ Uri uri = AggregationSuggestions.builder()
+ .addNameParameter("last1 first1")
+ .build();
+
+ Cursor cursor = mResolver.query(
+ uri, new String[] { Contacts._ID, Contacts.DISPLAY_NAME }, null, null, null);
+
+ assertEquals(1, cursor.getCount());
+
+ cursor.moveToFirst();
+
+ ContentValues values = new ContentValues();
+ values.put(Contacts._ID, queryContactId(rawContactId1));
+ values.put(Contacts.DISPLAY_NAME, "first1 last1");
+ assertCursorValues(cursor, values);
+ cursor.close();
+ }
+
+ public void testAggregation_phoneticNamePriority1() {
+ // Setup: one raw contact has a complex phonetic name and the other a simple given name
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertPhoneticName(mResolver, rawContactId1, "name phonetic");
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: given name is used instead of phonetic, contrary to results of
+ // testAggregation_nameComplexity
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name", queryDisplayName(contactId));
+ }
+
+ // Same as testAggregation_phoneticNamePriority1, but with setup order reversed
+ public void testAggregation_phoneticNamePriority2() {
+ // Setup: one raw contact has a complex phonetic name and the other a simple given name
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertPhoneticName(mResolver, rawContactId1, "name phonetic");
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: given name is used instead of phonetic, contrary to results of
+ // testAggregation_nameComplexity
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name", queryDisplayName(contactId));
+ }
+
+ public void testAggregation_nameComplexity1() {
+ // Setup: two names, one of which is unambiguously more complex
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name phonetic", ACCOUNT_1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: more complex name is used
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name phonetic", queryDisplayName(contactId));
+ }
+
+ // Same as testAggregation_nameComplexity1, but with setup order reversed
+ public void testAggregation_nameComplexity2() {
+ // Setup: two names, one of which is unambiguously more complex
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name phonetic", ACCOUNT_1);
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: more complex name is used
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name phonetic", queryDisplayName(contactId));
+ }
+
+ public void testAggregation_clearSuperPrimary() {
+ // Three types of mime-type super primary merging are tested here
+ // 1. both raw contacts have super primary phone numbers
+ // 2. both raw contacts have emails, but only one has super primary email
+ // 3. only raw contact1 has organizations and it has set the super primary organization
+ ContentValues values = new ContentValues();
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ Uri uri_phone1a = insertPhoneNumber(rawContactId1, "(222)222-2222", true, true);
+ Uri uri_phone1b = insertPhoneNumber(rawContactId1, "(555)555-5555", false, false);
+ Uri uri_phone3 = insertPhoneNumber(rawContactId1, "(111)111-1111", true, true);
+ Uri uri_email1 = insertEmail(rawContactId1, "one@gmail.com", true, true);
+ values.clear();
+ values.put(Organization.COMPANY, "Monsters Inc");
+ Uri uri_org1 = insertOrganization(rawContactId1, values, true, true);
+ values.clear();
+ values.put(Organization.TITLE, "CEO");
+ Uri uri_org2 = insertOrganization(rawContactId1, values, false, false);
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ Uri uri_phone2 = insertPhoneNumber(rawContactId2, "(333)333-3333", true, true);
+ Uri uri_phone4 = insertPhoneNumber(rawContactId2, "1(111)111-1111", true, true);
+ Uri uri_email2 = insertEmail(rawContactId2, "two@gmail.com", false, false);
+
+ // Two raw contacts with same name will trigger the aggregation
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "Helen", "Parr");
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Helen", "Parr");
+
+ // After aggregation, the super primary flag should only be cleared for case 1,
+ // i.e., phone mime-type. Both case 2 and 3, i.e. organization and email mime-types,
+ // have the super primary flag unchanged.
+ assertAggregated(rawContactId1, rawContactId2);
+ assertSuperPrimary(ContentUris.parseId(uri_phone1a), false);
+ assertSuperPrimary(ContentUris.parseId(uri_phone1b), false);
+ assertSuperPrimary(ContentUris.parseId(uri_phone2), false);
+ assertSuperPrimary(ContentUris.parseId(uri_phone3), false);
+ assertSuperPrimary(ContentUris.parseId(uri_phone4), false);
+
+ assertSuperPrimary(ContentUris.parseId(uri_email1), true);
+ assertSuperPrimary(ContentUris.parseId(uri_email2), false);
+
+ assertSuperPrimary(ContentUris.parseId(uri_org1), true);
+ assertSuperPrimary(ContentUris.parseId(uri_org2), false);
+ }
+
+ public void testAggregation_clearSuperPrimarySingleMimetype() {
+ // Setup: two raw contacts, each has a single name. One of the names is super primary.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ final Uri uri = DataUtil.insertStructuredName(mResolver, rawContactId1, "name1",
+ null, null, /* isSuperPrimary = */ true);
+
+ // Sanity check.
+ assertStoredValue(uri, Data.IS_SUPER_PRIMARY, 1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: name is still super primary
+ assertStoredValue(uri, Data.IS_SUPER_PRIMARY, 1);
+ }
+
+ public void testNotAggregate_TooManyRawContactsInCandidate() {
+ long preId= 0;
+ for (int i = 0; i < ContactAggregator.AGGREGATION_CONTACT_SIZE_LIMIT; i++) {
+ long id = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
+ if (i > 0) {
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, preId, id);
+ }
+ preId = id;
+ }
+ // Although the newly added raw contact matches the names with other raw contacts,
+ // but the best matching contact has already meets the size limit, so keep the new raw
+ // contact separate from other raw contacts.
+ long newId = RawContactUtil.createRawContact(mResolver,
+ new Account("account_new", "new account type"));
+ DataUtil.insertStructuredName(mResolver, newId, "John", "Doe");
+ assertNotAggregated(preId, newId);
+ assertTrue(queryContactId(newId) > 0);
+ }
+
+ public void testDoctorsWithSameWorkPhoneNumber() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ ContentValues values = new ContentValues();
+ values.put(Organization.TITLE, "Weight Management");
+ insertOrganization(rawContactId1, values);
+ insertPhoneNumber(rawContactId1,"8296", false, 3);
+ insertPhoneNumber(rawContactId1,"8270", false, 4);
+
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "Jane", "Smith",
+ ACCOUNT_1);
+ insertEmail(rawContactId2, "s@hospital.org");
+ values.put(Organization.TITLE, "Weight Management Program");
+ insertOrganization(rawContactId2, values);
+ insertPhoneNumber(rawContactId2,"8264", false, 3);
+ insertPhoneNumber(rawContactId2,"8270", false, 4);
+
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "Karen", "Lee",
+ ACCOUNT_1);
+ insertEmail(rawContactId3, "l@hospital.org");
+ values.put(Organization.TITLE, "Weight Management");
+ insertOrganization(rawContactId3, values);
+ insertPhoneNumber(rawContactId3,"8268", false, 3);
+ insertPhoneNumber(rawContactId3,"8270", false, 4);
+
+ long rawContactId4 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ insertEmail(rawContactId4, "orders@v.org");
+ values.put(Organization.TITLE, "Weight Management");
+ insertOrganization(rawContactId4, values);
+ insertPhoneNumber(rawContactId4,"8262", false, 3);
+ insertPhoneNumber(rawContactId4,"8260", false, 3);
+ insertPhoneNumber(rawContactId4,"8270", false, 4);
+
+ long rawContactId5 = RawContactUtil.createRawContactWithName(mResolver, "Rachel", "Fred",
+ ACCOUNT_1);
+ insertEmail(rawContactId5, "f@hospital.org");
+ values.put(Organization.TITLE, "Weight Management");
+ insertOrganization(rawContactId5, values);
+ insertPhoneNumber(rawContactId5,"8261", false, 3);
+ insertPhoneNumber(rawContactId5,"8270", false, 4);
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId1, rawContactId4);
+ assertNotAggregated(rawContactId1, rawContactId5);
+ assertNotAggregated(rawContactId2, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId4);
+ assertNotAggregated(rawContactId2, rawContactId5);
+ assertNotAggregated(rawContactId3, rawContactId4);
+ assertNotAggregated(rawContactId3, rawContactId5);
+ assertNotAggregated(rawContactId4, rawContactId5);
+ }
+
+ 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);
+ assertSuggestions(uri, suggestions);
+ }
+
+ private void assertSuggestions(long contactId, String filter, long... suggestions) {
+ final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ Uri uri = Uri.withAppendedPath(Uri.withAppendedPath(aggregateUri,
+ Contacts.AggregationSuggestions.CONTENT_DIRECTORY), Uri.encode(filter));
+ assertSuggestions(uri, suggestions);
+ }
+
+ private void assertSuggestions(Uri uri, long... suggestions) {
+ final Cursor cursor = mResolver.query(uri,
+ new String[] { Contacts._ID, Contacts.CONTACT_PRESENCE },
+ null, null, null);
+
+ try {
+ assertEquals(suggestions.length, cursor.getCount());
+
+ for (int i = 0; i < suggestions.length; i++) {
+ cursor.moveToNext();
+ assertEquals(suggestions[i], cursor.getLong(0));
+ }
+ } finally {
+ TestUtils.dumpCursor(cursor);
+ }
+
+ cursor.close();
+ }
+
+ private void assertDisplayNameEquals(long contactId, long rawContactId) {
+
+ String contactDisplayName = queryDisplayName(contactId);
+
+ Cursor c = queryRawContact(rawContactId);
+ assertTrue(c.moveToFirst());
+ String rawDisplayName = c.getString(c.getColumnIndex(RawContacts.DISPLAY_NAME_PRIMARY));
+ c.close();
+
+ assertTrue(contactDisplayName != null);
+ assertTrue(rawDisplayName != null);
+ assertEquals(rawDisplayName, contactDisplayName);
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
index 0e0264c..d67e13b 100644
--- a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
@@ -43,19 +43,14 @@
import com.android.providers.contacts.testutil.RawContactUtil;
import com.google.android.collect.Lists;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Multimap;
-
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
/**
* Unit tests for {@link ContactAggregator}.
*
* Run the test like this:
* <code>
- * adb shell am instrument -e class com.android.providers.contacts.ContactAggregatorTest -w \
+ * adb shell am instrument -e \
+ * class com.android.providers.contacts.aggregation.ContactAggregatorTest -w \
* com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
* </code>
*/
@@ -1314,34 +1309,79 @@
assertEquals("Eclair Android", queryDisplayName(contactId));
}
- public void testVerifiedName() {
- long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "test1", "TEST1",
- ACCOUNT_1);
- storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.NAME_VERIFIED, "1");
- long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "test2", "TEST2",
- ACCOUNT_2);
- long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "test3",
- "TEST3 LONG", ACCOUNT_3);
+ public void testMergeSuperPrimaryName_rawContact1() {
+ // Setup: raw contact #1 has a super primary name. #2 doesn't.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "name1", null, null,
+ /* isSuperPrimary = */ true);
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "name2", null, null,
+ /* isSuperPrimary = */ false);
+ // Action: aggregate
setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
rawContactId2);
- setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
- rawContactId3);
+ // Verify: the aggregate's name comes from raw contact #1
long contactId = queryContactId(rawContactId1);
+ assertEquals("name1", queryDisplayName(contactId));
+ }
- // Should be the verified name
- assertEquals("test1 TEST1", queryDisplayName(contactId));
+ public void testMergeSuperPrimaryName_rawContact2AndEdit() {
+ // Setup: raw contact #2 has a super primary name. #1 doesn't.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ final Uri nameUri1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "name1",
+ null, null, /* isSuperPrimary = */ false);
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ final Uri nameUri2 = DataUtil.insertStructuredName(mResolver, rawContactId2, "name2", null,
+ null, /* isSuperPrimary = */ true);
- // Mark a different name as verified - this should reset the NAME_VERIFIED field
- // for the other rawContacts
- storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.NAME_VERIFIED, "1");
- assertStoredValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.NAME_VERIFIED, 0);
- assertEquals("test2 TEST2", queryDisplayName(contactId));
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
- // Reset the NAME_VERIFIED flag - now the most complex of the three names should win
- storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.NAME_VERIFIED, "0");
- assertEquals("test3 TEST3 LONG", queryDisplayName(contactId));
+ // Verify: the aggregate's name comes from raw contact #2. This is the opposite of the check
+ // inside testMergeSuperPrimaryName_rawContact1().
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name2", queryDisplayName(contactId));
+
+ // Action: edit the super primary name
+ final ContentValues values = new ContentValues();
+ values.put(StructuredName.GIVEN_NAME, "edited name");
+ mResolver.update(nameUri2, values, null, null);
+
+ // Verify: editing the super primary name affects aggregate name
+ assertEquals("edited name", queryDisplayName(contactId));
+
+ // Action: edit the non primary name
+ values.put(StructuredName.GIVEN_NAME, "edited name2");
+ mResolver.update(nameUri1, values, null, null);
+
+ // Verify: aggregate name is still based off the primary name
+ assertEquals("edited name", queryDisplayName(contactId));
+ }
+
+ public void testMergedSuperPrimaryName_changeSuperPrimary() {
+ // Setup: aggregated contact where raw contact #1 has a super primary name. #2 doesn't.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ final Uri nameUri1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "name1",
+ null, null, /* isSuperPrimary = */ true);
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ final Uri nameUri2 = DataUtil.insertStructuredName(mResolver, rawContactId2, "name2", null,
+ null, /* isSuperPrimary = */ false);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Action: make raw contact 2's name super primary
+ storeValue(nameUri2, Data.IS_SUPER_PRIMARY, 1);
+
+ // Sanity check.
+ assertStoredValue(nameUri1, Data.IS_SUPER_PRIMARY, 0);
+ assertStoredValue(nameUri2, Data.IS_SUPER_PRIMARY, 1);
+
+ // Verify: aggregate name is based off of the newly super primary name
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name2", queryDisplayName(contactId));
}
public void testAggregationModeSuspendedSeparateTransactions() {
@@ -1514,22 +1554,14 @@
public void testAggregationSuggestionsQueryBuilderWithValues() throws Exception {
Uri uri = AggregationSuggestions.builder()
- .addParameter(AggregationSuggestions.PARAMETER_MATCH_NAME, "name1")
- .addParameter(AggregationSuggestions.PARAMETER_MATCH_NAME, "name2")
- .addParameter(AggregationSuggestions.PARAMETER_MATCH_EMAIL, "email1")
- .addParameter(AggregationSuggestions.PARAMETER_MATCH_EMAIL, "email2")
- .addParameter(AggregationSuggestions.PARAMETER_MATCH_PHONE, "phone1")
- .addParameter(AggregationSuggestions.PARAMETER_MATCH_NICKNAME, "nickname1")
+ .addNameParameter("name1")
+ .addNameParameter("name2")
.setLimit(7)
.build();
assertEquals("content://com.android.contacts/contacts/0/suggestions?"
+ "limit=7"
+ "&query=name%3Aname1"
- + "&query=name%3Aname2"
- + "&query=email%3Aemail1"
- + "&query=email%3Aemail2"
- + "&query=phone%3Aphone1"
- + "&query=nickname%3Anickname1", uri.toString());
+ + "&query=name%3Aname2", uri.toString());
}
public void testAggregatedStatusUpdate() {
@@ -1563,7 +1595,7 @@
long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "first2", "last2");
Uri uri = AggregationSuggestions.builder()
- .addParameter(AggregationSuggestions.PARAMETER_MATCH_NAME, "last1 first1")
+ .addNameParameter("last1 first1")
.build();
Cursor cursor = mResolver.query(
@@ -1580,6 +1612,74 @@
cursor.close();
}
+ public void testAggregation_phoneticNamePriority1() {
+ // Setup: one raw contact has a complex phonetic name and the other a simple given name
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertPhoneticName(mResolver, rawContactId1, "name phonetic");
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: given name is used instead of phonetic, contrary to results of
+ // testAggregation_nameComplexity
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name", queryDisplayName(contactId));
+ }
+
+ // Same as testAggregation_phoneticNamePriority1, but with setup order reversed
+ public void testAggregation_phoneticNamePriority2() {
+ // Setup: one raw contact has a complex phonetic name and the other a simple given name
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertPhoneticName(mResolver, rawContactId1, "name phonetic");
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: given name is used instead of phonetic, contrary to results of
+ // testAggregation_nameComplexity
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name", queryDisplayName(contactId));
+ }
+
+ public void testAggregation_nameComplexity1() {
+ // Setup: two names, one of which is unambiguously more complex
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name phonetic", ACCOUNT_1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: more complex name is used
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name phonetic", queryDisplayName(contactId));
+ }
+
+ // Same as testAggregation_nameComplexity1, but with setup order reversed
+ public void testAggregation_nameComplexity2() {
+ // Setup: two names, one of which is unambiguously more complex
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name phonetic", ACCOUNT_1);
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: more complex name is used
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name phonetic", queryDisplayName(contactId));
+ }
+
public void testAggregation_clearSuperPrimary() {
// Three types of mime-type super primary merging are tested here
// 1. both raw contacts have super primary phone numbers
@@ -1587,7 +1687,8 @@
// 3. only raw contact1 has organizations and it has set the super primary organization
ContentValues values = new ContentValues();
long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
- Uri uri_phone1 = insertPhoneNumber(rawContactId1, "(222)222-2222", false, false);
+ Uri uri_phone1a = insertPhoneNumber(rawContactId1, "(222)222-2222", true, true);
+ Uri uri_phone1b = insertPhoneNumber(rawContactId1, "(555)555-5555", false, false);
Uri uri_email1 = insertEmail(rawContactId1, "one@gmail.com", true, true);
values.clear();
values.put(Organization.COMPANY, "Monsters Inc");
@@ -1597,23 +1698,24 @@
Uri uri_org2 = insertOrganization(rawContactId1, values, false, false);
long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
- Uri uri_phone2 = insertPhoneNumber(rawContactId2, "(333)333-3333", false, false);
+ Uri uri_phone2 = insertPhoneNumber(rawContactId2, "(333)333-3333", true, true);
Uri uri_email2 = insertEmail(rawContactId2, "two@gmail.com", false, false);
// Two raw contacts with same phone number will trigger the aggregation
Uri uri_phone3 = insertPhoneNumber(rawContactId1, "(111)111-1111", true, true);
Uri uri_phone4 = insertPhoneNumber(rawContactId2, "1(111)111-1111", true, true);
- // After aggregation, the super primary flag should be cleared for both case 1 and case 2,
- // i.e., phone and email mime-types. Only case 3, i.e. organization mime-type, has the
- // super primary flag unchanged.
+ // After aggregation, the super primary flag should only be cleared for case 1,
+ // i.e., phone mime-type. Both case 2 and 3, i.e. organization and email mime-types,
+ // have the super primary flag unchanged.
assertAggregated(rawContactId1, rawContactId2);
- assertSuperPrimary(ContentUris.parseId(uri_phone1), false);
+ assertSuperPrimary(ContentUris.parseId(uri_phone1a), false);
+ assertSuperPrimary(ContentUris.parseId(uri_phone1b), false);
assertSuperPrimary(ContentUris.parseId(uri_phone2), false);
assertSuperPrimary(ContentUris.parseId(uri_phone3), false);
assertSuperPrimary(ContentUris.parseId(uri_phone4), false);
- assertSuperPrimary(ContentUris.parseId(uri_email1), false);
+ assertSuperPrimary(ContentUris.parseId(uri_email1), true);
assertSuperPrimary(ContentUris.parseId(uri_email2), false);
assertSuperPrimary(ContentUris.parseId(uri_org1), true);
@@ -1657,44 +1759,6 @@
assertTrue(queryContactId(newId) > 0);
}
- public void testFindConnectedRawContacts() {
- Set<Long> rawContactIdSet = new HashSet<Long>();
- rawContactIdSet.addAll(Arrays.asList(1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9l));
-
- Multimap<Long, Long> matchingrawIdPairs = HashMultimap.create();
- matchingrawIdPairs.put(1l, 2l);
- matchingrawIdPairs.put(2l, 1l);
-
- matchingrawIdPairs.put(1l, 7l);
- matchingrawIdPairs.put(7l, 1l);
-
- matchingrawIdPairs.put(2l, 3l);
- matchingrawIdPairs.put(3l, 2l);
-
- matchingrawIdPairs.put(2l, 8l);
- matchingrawIdPairs.put(8l, 2l);
-
- matchingrawIdPairs.put(8l, 9l);
- matchingrawIdPairs.put(9l, 8l);
-
- matchingrawIdPairs.put(4l, 5l);
- matchingrawIdPairs.put(5l, 4l);
-
- Set<Set<Long>> actual = ContactAggregator.findConnectedComponents(rawContactIdSet,
- matchingrawIdPairs);
-
- Set<Set<Long>> expected = new HashSet<Set<Long>>();
- Set<Long> result1 = new HashSet<Long>();
- result1.addAll(Arrays.asList(1l, 2l, 3l, 7l, 8l, 9l));
- Set<Long> result2 = new HashSet<Long>();
- result2.addAll(Arrays.asList(4l, 5l));
- Set<Long> result3 = new HashSet<Long>();
- result3.addAll(Arrays.asList(6l));
- expected.addAll(Arrays.asList(result1, result2, result3));
-
- assertEquals(expected, actual);
- }
-
private void assertSuggestions(long contactId, long... suggestions) {
final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
Uri uri = Uri.withAppendedPath(aggregateUri,
diff --git a/tests/src/com/android/providers/contacts/aggregation/util/ContactAggregatorHelperTest.java b/tests/src/com/android/providers/contacts/aggregation/util/ContactAggregatorHelperTest.java
new file mode 100644
index 0000000..4373a76
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/aggregation/util/ContactAggregatorHelperTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.aggregation.util;
+
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.SmallTest;
+import com.google.android.collect.Sets;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+@SmallTest
+public class ContactAggregatorHelperTest extends TestCase {
+
+ private static final long ACCOUNT_1 = 1;
+ private static final long ACCOUNT_2 = 2;
+ private static final long ACCOUNT_3 = 3;
+ private static final long ACCOUNT_4 = 4;
+ private static final long ACCOUNT_5 = 5;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ public void testMergeComponentsWithDisjointAccounts() {
+ Set<Set<Long>> connectedRawContactSets = new HashSet<>();
+ Map<Long, Long> rawContactsToAccounts = new HashMap<>();
+ for (long i = 100; i < 108; ) {
+ Set<Long> rawContactSet = new HashSet<>();
+ rawContactSet.add(i++);
+ rawContactSet.add(i++);
+ connectedRawContactSets.add(rawContactSet);
+ }
+
+ for (long i = 100; i < 103; i++) {
+ rawContactsToAccounts.put(i, ACCOUNT_1);
+ }
+ rawContactsToAccounts.put(100l, ACCOUNT_1);
+ rawContactsToAccounts.put(101l, ACCOUNT_1);
+ rawContactsToAccounts.put(102l, ACCOUNT_1);
+ rawContactsToAccounts.put(103l, ACCOUNT_2);
+ rawContactsToAccounts.put(104l, ACCOUNT_3);
+ rawContactsToAccounts.put(105l, ACCOUNT_4);
+ rawContactsToAccounts.put(106l, ACCOUNT_5);
+ rawContactsToAccounts.put(107l, ACCOUNT_5);
+ // Component1: [rawContactId=100, accountId=1; raw_contactId=101, accountId=1]
+ // Component2: [rawContactId=102, accountId=1; raw_contactId=103, accountId=2]
+ // Component3: [rawContactId=104, accountId=3; raw_contactId=105, accountId=4]
+ // Component4: [rawContactId=106, accountId=5; raw_contactId=107, accountId=5]
+ // Component3 and component4 can be merged because they have disjoint accounts, but cannot
+ // merge with either component1 or component2 due to the uncertainty.
+
+ ContactAggregatorHelper.mergeComponentsWithDisjointAccounts(connectedRawContactSets,
+ rawContactsToAccounts);
+
+ MoreAsserts.assertContentsInAnyOrder(connectedRawContactSets, Sets.newHashSet(100l,
+ 101l), Sets.newHashSet(102l, 103l), Sets.newHashSet(104l, 105l, 106l, 107l));
+ }
+
+ public void testFindConnectedRawContacts() {
+ Set<Long> rawContactIdSet = new HashSet<>();
+ rawContactIdSet.addAll(Arrays.asList(1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9l));
+
+ Multimap<Long, Long> matchingrawIdPairs = HashMultimap.create();
+ matchingrawIdPairs.put(1l, 2l);
+ matchingrawIdPairs.put(2l, 1l);
+
+ matchingrawIdPairs.put(1l, 7l);
+ matchingrawIdPairs.put(7l, 1l);
+
+ matchingrawIdPairs.put(2l, 3l);
+ matchingrawIdPairs.put(3l, 2l);
+
+ matchingrawIdPairs.put(2l, 8l);
+ matchingrawIdPairs.put(8l, 2l);
+
+ matchingrawIdPairs.put(8l, 9l);
+ matchingrawIdPairs.put(9l, 8l);
+
+ matchingrawIdPairs.put(4l, 5l);
+ matchingrawIdPairs.put(5l, 4l);
+
+ Set<Set<Long>> actual = ContactAggregatorHelper.findConnectedComponents(rawContactIdSet,
+ matchingrawIdPairs);
+
+ Set<Set<Long>> expected = new HashSet<>();
+ Set<Long> result1 = new HashSet<>();
+ result1.addAll(Arrays.asList(1l, 2l, 3l, 7l, 8l, 9l));
+ Set<Long> result2 = new HashSet<>();
+ result2.addAll(Arrays.asList(4l, 5l));
+ Set<Long> result3 = new HashSet<>();
+ result3.addAll(Arrays.asList(6l));
+ expected.addAll(Arrays.asList(result1, result2, result3));
+
+ assertEquals(expected, actual);
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/testutil/DataUtil.java b/tests/src/com/android/providers/contacts/testutil/DataUtil.java
index 1f4f35a..2afd567 100644
--- a/tests/src/com/android/providers/contacts/testutil/DataUtil.java
+++ b/tests/src/com/android/providers/contacts/testutil/DataUtil.java
@@ -59,14 +59,14 @@
public static Uri insertStructuredName(
ContentResolver resolver, long rawContactId, String givenName, String familyName,
- String phoneticGiven) {
- return insertStructuredName(resolver, rawContactId, givenName, familyName, phoneticGiven,
+ String phoneticFamily) {
+ return insertStructuredName(resolver, rawContactId, givenName, familyName, phoneticFamily,
/* isSuperPrimary = true */ false);
}
public static Uri insertStructuredName(
ContentResolver resolver, long rawContactId, String givenName, String familyName,
- String phoneticGiven, boolean isSuperPrimary) {
+ String phoneticFamily, boolean isSuperPrimary) {
ContentValues values = new ContentValues();
StringBuilder sb = new StringBuilder();
if (givenName != null) {
@@ -78,14 +78,16 @@
if (familyName != null) {
sb.append(familyName);
}
- if (sb.length() == 0 && phoneticGiven != null) {
- sb.append(phoneticGiven);
+ if (sb.length() == 0 && phoneticFamily != null) {
+ sb.append(phoneticFamily);
}
values.put(StructuredName.DISPLAY_NAME, sb.toString());
values.put(StructuredName.GIVEN_NAME, givenName);
values.put(StructuredName.FAMILY_NAME, familyName);
- if (phoneticGiven != null) {
- values.put(StructuredName.PHONETIC_GIVEN_NAME, phoneticGiven);
+ if (phoneticFamily != null) {
+ // When creating phonetic names, be careful to use PHONETIC_FAMILY_NAME instead of
+ // PHONETIC_GIVEN_NAME, to work around b/19612393.
+ values.put(StructuredName.PHONETIC_FAMILY_NAME, phoneticFamily);
}
if (isSuperPrimary) {
values.put(Data.IS_PRIMARY, 1);
@@ -94,4 +96,13 @@
return insertStructuredName(resolver, rawContactId, values);
}
+
+ public static Uri insertPhoneticName(ContentResolver resolver, long rawContactId,
+ String phoneticFamilyName) {
+ ContentValues values = new ContentValues();
+ // When creating phonetic names, be careful to use PHONETIC_FAMILY_NAME instead of
+ // PHONETIC_GIVEN_NAME, to work around b/19612393.
+ values.put(StructuredName.PHONETIC_FAMILY_NAME, phoneticFamilyName);
+ return insertStructuredName(resolver, rawContactId, values);
+ }
}
\ No newline at end of file
diff --git a/tests/src/com/android/providers/contacts/util/UserUtilsTest.java b/tests/src/com/android/providers/contacts/util/UserUtilsTest.java
index 4417ad5..7482ee6 100644
--- a/tests/src/com/android/providers/contacts/util/UserUtilsTest.java
+++ b/tests/src/com/android/providers/contacts/util/UserUtilsTest.java
@@ -47,37 +47,37 @@
final MockUserManager um = mActor.mockUserManager;
// No corp user. Primary only.
- assertEquals(-1, UserUtils.getCorpUserId(c));
+ assertEquals(-1, UserUtils.getCorpUserId(c, false));
// Primary + corp
um.setUsers(MockUserManager.PRIMARY_USER, MockUserManager.CORP_USER);
um.myUser = MockUserManager.PRIMARY_USER.id;
- assertEquals(MockUserManager.CORP_USER.id, UserUtils.getCorpUserId(c));
+ assertEquals(MockUserManager.CORP_USER.id, UserUtils.getCorpUserId(c, false));
um.myUser = MockUserManager.CORP_USER.id;
- assertEquals(-1, UserUtils.getCorpUserId(c));
+ assertEquals(-1, UserUtils.getCorpUserId(c, false));
// Primary + secondary + corp
um.setUsers(MockUserManager.PRIMARY_USER, MockUserManager.SECONDARY_USER,
MockUserManager.CORP_USER);
um.myUser = MockUserManager.PRIMARY_USER.id;
- assertEquals(MockUserManager.CORP_USER.id, UserUtils.getCorpUserId(c));
+ assertEquals(MockUserManager.CORP_USER.id, UserUtils.getCorpUserId(c, false));
um.myUser = MockUserManager.CORP_USER.id;
- assertEquals(-1, UserUtils.getCorpUserId(c));
+ assertEquals(-1, UserUtils.getCorpUserId(c, false));
um.myUser = MockUserManager.SECONDARY_USER.id;
- assertEquals(-1, UserUtils.getCorpUserId(c));
+ assertEquals(-1, UserUtils.getCorpUserId(c, false));
// Primary + secondary
um.setUsers(MockUserManager.PRIMARY_USER, MockUserManager.SECONDARY_USER);
um.myUser = MockUserManager.PRIMARY_USER.id;
- assertEquals(-1, UserUtils.getCorpUserId(c));
+ assertEquals(-1, UserUtils.getCorpUserId(c, false));
um.myUser = MockUserManager.SECONDARY_USER.id;
- assertEquals(-1, UserUtils.getCorpUserId(c));
+ assertEquals(-1, UserUtils.getCorpUserId(c, false));
}
}
diff --git a/tools/contacts-db-schema.sh b/tools/contacts-db-schema.sh
new file mode 100755
index 0000000..3aa164b
--- /dev/null
+++ b/tools/contacts-db-schema.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+set -e
+
+db=/data/data/com.android.providers.contacts/databases/contacts2.db
+
+# Use ls to make sure the file already exists.
+# Otherwise sqlite3 would create an empty file owned by root.
+
+# Sed inserts a newline after each ( and ,
+adb shell "(ls $db >/dev/null)&& sqlite3 $db \"select name, sql from sqlite_master where type in('table','index') order by name\"" |
+ sed -e 's/\([(,]\)/\1\n /g'
+echo "> sqlite_stat1"
+adb shell "(ls $db >/dev/null)&& sqlite3 $db \"select * from sqlite_stat1 order by tbl, idx, stat\""
+
+