Merge remote-tracking branch 'goog/mirror-m-wireless-internal-release'
Change-Id: I439330f8c022ce85005d84fd4286f4f1ffdced1c
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 20213d3..eeaae48 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3,22 +3,29 @@
android:sharedUserId="android.uid.shared"
android:sharedUserLabel="@string/sharedUserLabel">
- <uses-permission android:name="android.permission.READ_CONTACTS" />
- <uses-permission android:name="android.permission.WRITE_CONTACTS" />
- <uses-permission android:name="android.permission.READ_PROFILE" />
- <uses-permission android:name="android.permission.WRITE_PROFILE" />
- <uses-permission android:name="android.permission.GET_ACCOUNTS" />
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BIND_DIRECTORY_SEARCH" />
- <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
- <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
<uses-permission android:name="android.permission.MANAGE_USERS" />
+ <uses-permission android:name="android.permission.PROCESS_PHONE_ACCOUNT_REGISTRATION" />
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+ <uses-permission android:name="android.permission.READ_PROFILE" />
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+ <uses-permission android:name="android.permission.SEND_CALL_LOG_CHANGE" />
+ <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
+ <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+ <uses-permission android:name="android.permission.WRITE_PROFILE" />
<uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
<uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL" />
<uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL" />
+ <permission
+ android:name="android.permission.SEND_CALL_LOG_CHANGE"
+ android:label="Broadcast that a change happened to the call log."
+ android:protectionLevel="signature|system"/>
+
<application android:process="android.process.acore"
android:label="@string/app_label"
android:icon="@drawable/app_icon"
@@ -40,6 +47,9 @@
<path-permission
android:pathPattern="/contacts/.*/photo"
android:readPermission="android.permission.GLOBAL_SEARCH" />
+ <path-permission
+ android:pathPrefix="/data_enterprise"
+ android:readPermission="android.permission.INTERACT_ACROSS_USERS" />
<grant-uri-permission android:pathPattern=".*" />
</provider>
@@ -68,6 +78,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-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-hi/strings.xml b/res/values-hi/strings.xml
index 21ea8cb..3bf42f6 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -17,17 +17,16 @@
<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">"संपर्क संग्रहण"</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_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_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>
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 f11132b..171db41 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -16,10 +16,9 @@
<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">"Apl Inti Android"</string>
+ <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 4ca69a9..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>
@@ -27,7 +26,7 @@
<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_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>
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 cbaa2f9..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>
@@ -27,7 +26,7 @@
<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">"អ្នកហៀបនឹង ១) ចម្លងមូលដ្ឋានទិន្នន័យរបស់អ្នកដែលរួមមានព័ត៌មានទំនាក់ទំនង និងកំណត់ហេតុហៅទាំងអស់ទៅកាន់ឧបករណ៍ផ្ទុកខាងក្នុង ២) ផ្ញើអ៊ីម៉ែលវា។ ចងចាំថាអ្នកត្រូវលុបច្បាប់ចម្លងភ្លាមៗ បន្ទាប់ពីបានចម្លងចេញពីឧបករណ៍ ឬបានទទួលអ៊ីមែល។"</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"អ្នកហៀបនឹង ១) ចម្លងមូលដ្ឋានទិន្នន័យរបស់អ្នកដែលរួមមានព័ត៌មានទំនាក់ទំនង និងកំណត់ហេតុហៅទាំងអស់ទៅកាន់ឧបករណ៍ផ្ទុកខាងក្នុង ២) ផ្ញើអ៊ីមែលវា។ ចងចាំថាអ្នកត្រូវលុបច្បាប់ចម្លងភ្លាមៗ បន្ទាប់ពីបានចម្លងចេញពីឧបករណ៍ ឬបានទទួលអ៊ីមែល។"</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>
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 8632b3a..5493e4b 100644
--- a/res/values-ml-rIN/strings.xml
+++ b/res/values-ml-rIN/strings.xml
@@ -18,12 +18,11 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="sharedUserLabel" msgid="8024311725474286801">"Android കോർ അപ്ലിക്കേഷനുകൾ"</string>
<string name="app_label" msgid="3389954322874982620">"കോൺടാക്റ്റുകളുടെ സംഭരണം"</string>
- <string name="provider_label" msgid="6012150850819899907">"കോണ്ടാക്റ്റുകള്"</string>
- <string name="upgrade_msg" msgid="8640807392794309950">"കോൺടാക്റ്റുകളുടെ ഡാറ്റാബേസ് അപ്ഗ്രേഡുചെയ്യുന്നു."</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="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>
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-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 2552edf..6906957 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -16,10 +16,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="sharedUserLabel" msgid="8024311725474286801">"Principais aplicativos do Android"</string>
+ <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..ce03b98 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>
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 6c0cf53..cd34f2d 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -16,10 +16,9 @@
<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">"Osrednji programi sistema Android"</string>
+ <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>
@@ -30,7 +29,7 @@
<string name="debug_dump_database_message" msgid="406438635002392290">"V naslednjem koraku boste 1) naredili kopijo zbirke podatkov, ki vključuje vse informacije o stikih in celoten dnevnik klicev, v notranji pomnilnik ter 2) jo poslali. Ne pozabite izbrisati kopije iz naprave, ko jo boste uspešno kopirali oziroma ko jo boste prejeli po e-pošti."</string>
<string name="debug_dump_delete_button" msgid="7832879421132026435">"Izbriši zdaj"</string>
<string name="debug_dump_start_button" msgid="2837506913757600001">"Začni"</string>
- <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Izberite program za pošiljanje datoteke"</string>
+ <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Izberite aplikacijo za pošiljanje datoteke"</string>
<string name="debug_dump_email_subject" msgid="108188398416385976">"Priložena zbirka podatkov o stikih"</string>
<string name="debug_dump_email_body" msgid="4577749800871444318">"Priložena je zbirka podatkov z vsemi informacijami o stikih. Z e-pošto ravnajte previdno."</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 6bf913e..547e4d3 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -39,6 +39,9 @@
import android.os.UserManager;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.Log;
@@ -46,7 +49,6 @@
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.android.providers.contacts.util.SelectionBuilder;
import com.android.providers.contacts.util.UserUtils;
-
import com.google.common.annotations.VisibleForTesting;
import java.util.HashMap;
@@ -60,12 +62,16 @@
private static final String TAG = CallLogProvider.class.getSimpleName();
private static final int BACKGROUND_TASK_INITIALIZE = 0;
+ private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1;
/** Selection clause for selecting all calls that were made after a certain time */
private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?";
/** Selection clause to use to exclude voicemail records. */
private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause(
Calls.TYPE, Calls.VOICEMAIL_TYPE);
+ /** Selection clause to exclude hidden records. */
+ private static final String EXCLUDE_HIDDEN_SELECTION = getEqualityClause(
+ Calls.PHONE_ACCOUNT_HIDDEN, 0);
@VisibleForTesting
static final String[] CALL_LOG_SYNC_PROJECTION = new String[] {
@@ -80,12 +86,22 @@
Calls.PHONE_ACCOUNT_ID
};
+ static final String[] MINIMAL_PROJECTION = new String[] { Calls._ID };
+
private static final int CALLS = 1;
private static final int CALLS_ID = 2;
private static final int CALLS_FILTER = 3;
+ private static final String UNHIDE_BY_PHONE_ACCOUNT_QUERY =
+ "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " +
+ Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " + Calls.PHONE_ACCOUNT_ID + "=?;";
+
+ private static final String UNHIDE_BY_ADDRESS_QUERY =
+ "UPDATE " + Tables.CALLS + " SET " + Calls.PHONE_ACCOUNT_HIDDEN + "=0 WHERE " +
+ Calls.PHONE_ACCOUNT_ADDRESS + "=?;";
+
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(CallLog.AUTHORITY, "calls", CALLS);
@@ -109,6 +125,7 @@
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_COMPONENT_NAME);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ID, Calls.PHONE_ACCOUNT_ID);
sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_ADDRESS, Calls.PHONE_ACCOUNT_ADDRESS);
+ sCallsProjectionMap.put(Calls.PHONE_ACCOUNT_HIDDEN, Calls.PHONE_ACCOUNT_HIDDEN);
sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION);
@@ -122,6 +139,7 @@
sCallsProjectionMap.put(Calls.CACHED_MATCHED_NUMBER, Calls.CACHED_MATCHED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_NORMALIZED_NUMBER, Calls.CACHED_NORMALIZED_NUMBER);
sCallsProjectionMap.put(Calls.CACHED_PHOTO_ID, Calls.CACHED_PHOTO_ID);
+ sCallsProjectionMap.put(Calls.CACHED_PHOTO_URI, Calls.CACHED_PHOTO_URI);
sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER);
}
@@ -155,13 +173,13 @@
mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
- performBackgroundTask(msg.what);
+ performBackgroundTask(msg.what, msg.obj);
}
};
mReadAccessLatch = new CountDownLatch(1);
- scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE);
+ scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE, null);
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate finish");
@@ -190,6 +208,7 @@
final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, true /*isQuery*/);
+ selectionBuilder.addClause(EXCLUDE_HIDDEN_SELECTION);
final int match = sURIMatcher.match(uri);
switch (match) {
@@ -357,6 +376,10 @@
return getContext();
}
+ void adjustForNewPhoneAccount(PhoneAccountHandle handle) {
+ scheduleBackgroundTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle);
+ }
+
/**
* Returns a {@link DatabaseModifier} that takes care of sending necessary notifications
* after the operation is performed.
@@ -467,6 +490,48 @@
}
/**
+ * Un-hides any hidden call log entries that are associated with the specified handle.
+ *
+ * @param handle The handle to the newly registered {@link android.telecom.PhoneAccount}.
+ */
+ private void adjustForNewPhoneAccountInternal(PhoneAccountHandle handle) {
+ String[] handleArgs =
+ new String[] { handle.getComponentName().flattenToString(), handle.getId() };
+
+ // Check to see if any entries exist for this handle. If so (not empty), run the un-hiding
+ // update. If not, then try to identify the call from the phone number.
+ Cursor cursor = query(Calls.CONTENT_URI, MINIMAL_PROJECTION,
+ Calls.PHONE_ACCOUNT_COMPONENT_NAME + " =? AND " + Calls.PHONE_ACCOUNT_ID + " =?",
+ handleArgs, null);
+
+ if (cursor != null) {
+ try {
+ if (cursor.getCount() >= 1) {
+ // run un-hiding process based on phone account
+ mDbHelper.getWritableDatabase().execSQL(
+ UNHIDE_BY_PHONE_ACCOUNT_QUERY, handleArgs);
+ } else {
+ TelecomManager tm = TelecomManager.from(getContext());
+ if (tm != null) {
+
+ PhoneAccount account = tm.getPhoneAccount(handle);
+ if (account != null) {
+ // We did not find any items for the specific phone account, so run the
+ // query based on the phone number instead.
+ mDbHelper.getWritableDatabase().execSQL(UNHIDE_BY_ADDRESS_QUERY,
+ new String[] { account.getAddress().toString() });
+ }
+
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ }
+
+ /**
* @param cursor to copy call log entries from
*
* @return the timestamp of the last synced entry.
@@ -544,11 +609,11 @@
}
}
- private void scheduleBackgroundTask(int task) {
- mBackgroundHandler.sendEmptyMessage(task);
+ private void scheduleBackgroundTask(int task, Object arg) {
+ mBackgroundHandler.obtainMessage(task, arg).sendToTarget();
}
- private void performBackgroundTask(int task) {
+ private void performBackgroundTask(int task, Object arg) {
if (task == BACKGROUND_TASK_INITIALIZE) {
try {
final Context context = getContext();
@@ -563,6 +628,8 @@
mReadAccessLatch.countDown();
mReadAccessLatch = null;
}
+ } else if (task == BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT) {
+ adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg);
}
}
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index be9b56f..7cdda65 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.
@@ -23,8 +23,8 @@
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.database.CharArrayBuffer;
import android.database.Cursor;
@@ -90,14 +90,12 @@
import com.google.android.collect.Sets;
import com.google.common.annotations.VisibleForTesting;
-import java.util.HashMap;
-import java.util.List;
+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.
@@ -108,22 +106,22 @@
/**
* Contacts DB version ranges:
* <pre>
- * 0-98 Cupcake/Donut
- * 100-199 Eclair
- * 200-299 Eclair-MR1
- * 300-349 Froyo
- * 350-399 Gingerbread
- * 400-499 Honeycomb
- * 500-549 Honeycomb-MR1
- * 550-599 Honeycomb-MR2
- * 600-699 Ice Cream Sandwich
- * 700-799 Jelly Bean
- * 800-899 Kitkat
- * 900-999 Lollipop
+ * 0-98 Cupcake/Donut
+ * 100-199 Eclair
+ * 200-299 Eclair-MR1
+ * 300-349 Froyo
+ * 350-399 Gingerbread
+ * 400-499 Honeycomb
+ * 500-549 Honeycomb-MR1
+ * 550-599 Honeycomb-MR2
+ * 600-699 Ice Cream Sandwich
+ * 700-799 Jelly Bean
+ * 800-899 Kitkat
+ * 900-999 Lollipop
* 1000-1099 M
* </pre>
*/
- static final int DATABASE_VERSION = 1001;
+ static final int DATABASE_VERSION = 1005;
public interface Tables {
public static final String CONTACTS = "contacts";
@@ -152,6 +150,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[] {
@@ -448,8 +448,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 =
@@ -458,6 +456,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 {
@@ -680,6 +685,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.
*/
@@ -717,6 +728,13 @@
public static final int USAGE_TYPE_INT_SHORT_TEXT = 2;
}
+ public interface MetadataSyncColumns {
+ String _ID = BaseColumns._ID;
+ String RAW_CONTACT_BACKUP_ID = "raw_contact_backup_id";
+ String ACCOUNT_ID = "account_id";
+ String DATA = "data";
+ }
+
private interface EmailQuery {
public static final String TABLE = Tables.DATA;
@@ -1096,7 +1114,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 "
@@ -1201,6 +1218,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," +
@@ -1232,7 +1250,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, " +
@@ -1249,6 +1267,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, " +
@@ -1311,6 +1335,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," +
@@ -1348,6 +1373,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
@@ -1486,6 +1519,7 @@
Calls.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," +
Calls.PHONE_ACCOUNT_ID + " TEXT," +
Calls.PHONE_ACCOUNT_ADDRESS + " TEXT," +
+ Calls.PHONE_ACCOUNT_HIDDEN + " INTEGER NOT NULL DEFAULT 0," +
Calls.SUB_ID + " INTEGER DEFAULT -1," +
Calls.NEW + " INTEGER," +
Calls.CACHED_NAME + " TEXT," +
@@ -1499,6 +1533,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," +
@@ -1551,6 +1586,25 @@
DataUsageStatColumns.USAGE_TYPE_INT +
");");
+ db.execSQL("CREATE TABLE IF NOT EXISTS "
+ + Tables.METADATA_SYNC + " (" +
+ MetadataSyncColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ MetadataSyncColumns.RAW_CONTACT_BACKUP_ID + " TEXT NOT NULL," +
+ MetadataSyncColumns.ACCOUNT_ID + " INTEGER NOT NULL," +
+ MetadataSyncColumns.DATA + " TEXT," +
+ "FOREIGN KEY(" + MetadataSyncColumns.ACCOUNT_ID + ") REFERENCES "
+ + Tables.ACCOUNTS + "(" + AccountsColumns._ID + "));");
+
+ db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS metadata_sync_index ON " +
+ Tables.METADATA_SYNC + " (" +
+ MetadataSyncColumns.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);
@@ -1830,8 +1884,6 @@
+ 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_VERSION + " AS " + RawContacts.VERSION + ","
+ RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
+ RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ","
@@ -1892,6 +1944,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 + ", "
@@ -1953,6 +2006,7 @@
+ RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + ", "
+ dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ", "
+ rawContactOptionColumns + ", "
+ + RawContacts.BACKUP_ID + ", "
+ syncColumns
+ " FROM " + Tables.RAW_CONTACTS
+ " JOIN " + Tables.ACCOUNTS + " ON ("
@@ -2811,6 +2865,37 @@
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 < 1000) {
upgradeToVersion1000(db);
@@ -2927,12 +3012,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)"
);
@@ -3043,11 +3128,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,
@@ -3175,7 +3260,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;");
}
/**
@@ -3511,7 +3596,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()) {
@@ -4249,10 +4334,45 @@
}
}
- public void upgradeToVersion1000(SQLiteDatabase db) {
+ /**
+ * 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 1000, we are using the ICC ID as the phone-account ID. This code updates
+ // 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.
@@ -4280,7 +4400,16 @@
}
}
- private void upgradeToVersion1001(SQLiteDatabase db) {
+ /**
+ * 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;");
// 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;");
@@ -4416,7 +4545,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");
@@ -4460,6 +4589,9 @@
updateIndexStats(db, Tables.ACCOUNTS,
null, "3");
+ updateIndexStats(db, Tables.PRE_AUTHORIZED_URIS,
+ null, "1");
+
updateIndexStats(db, Tables.VISIBLE_CONTACTS,
null, "2000");
@@ -4475,6 +4607,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");
@@ -4728,7 +4863,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;
}
@@ -5430,26 +5572,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.
*/
@@ -5470,6 +5592,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;
}
@@ -5556,7 +5694,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..2c58f38 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;
@@ -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;
@@ -154,7 +157,6 @@
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;
@@ -338,6 +340,7 @@
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 PHONE_LOOKUP = 4000;
private static final int PHONE_LOOKUP_ENTERPRISE = 4001;
@@ -605,7 +608,6 @@
RawContacts.DATA_SET,
RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
RawContacts.DIRTY,
- RawContacts.NAME_VERIFIED,
RawContacts.SOURCE_ID,
RawContacts.VERSION,
};
@@ -666,7 +668,6 @@
.add(RawContacts.DATA_SET)
.add(RawContacts.ACCOUNT_TYPE_AND_DATA_SET)
.add(RawContacts.DIRTY)
- .add(RawContacts.NAME_VERIFIED)
.add(RawContacts.SOURCE_ID)
.add(RawContacts.VERSION)
.build();
@@ -1202,6 +1203,7 @@
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);
@@ -1347,9 +1349,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();
@@ -1410,7 +1409,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;
@@ -2145,14 +2143,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.
@@ -2225,8 +2215,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 +2235,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;
@@ -4419,14 +4419,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.
@@ -5661,7 +5653,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 =
@@ -6393,8 +6387,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,6 +6492,83 @@
return c;
}
+ private static class EnterprisePhoneCursorWrapper extends CursorWrapper {
+
+ 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}.
+ */
+ // TODO test
+ 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 {
+ // TODO: Maybe we want to have a DPM policy for it
+ final int corpUserId = UserUtils.getCorpUserId(getContext());
+ 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;
+ }
+ }
+
/**
* Handles {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
*/
@@ -6541,14 +6612,16 @@
if (VERBOSE_LOGGING) {
Log.v(TAG, "queryPhoneLookupEnterprise: corp query URI=" + remoteUri);
}
- final Cursor corp = getContext().getContentResolver().query(remoteUri, projection,
+ // 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 */ null, /* args */ null, /* order */ null,
/* cancellationsignal*/ null);
try {
if (VERBOSE_LOGGING) {
MoreDatabaseUtils.dumpCursor(TAG, "corp raw", corp);
}
- final Cursor rewritten = rewriteCorpPhoneLookup(corp);
+ final Cursor rewritten = rewriteCorpPhoneLookup(
+ (projection != null ? projection : corp.getColumnNames()), corp);
if (VERBOSE_LOGGING) {
MoreDatabaseUtils.dumpCursor(TAG, "corp rewritten", rewritten);
}
@@ -6563,18 +6636,20 @@
* Rewrite a cursor from the corp profile for {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
*/
@VisibleForTesting
- static Cursor rewriteCorpPhoneLookup(Cursor original) {
- final String[] columns = original.getColumnNames();
- final MatrixCursor ret = new MatrixCursor(columns);
+ static Cursor rewriteCorpPhoneLookup(String[] outputProjection, Cursor original) {
+ final MatrixCursor ret = new MatrixCursor(outputProjection);
original.moveToPosition(-1);
while (original.moveToNext()) {
+ // Note PhoneLookup._ID is a contact ID, not a data ID.
final int contactId = original.getInt(original.getColumnIndex(PhoneLookup._ID));
final MatrixCursor.RowBuilder builder = ret.newRow();
- for (int i = 0; i < columns.length; i++) {
- switch (columns[i]) {
+ for (int i = 0; i < outputProjection.length; i++) {
+ final String outputColumnName = outputProjection[i];
+ final int originalColumnIndex = original.getColumnIndex(outputColumnName);
+ switch (outputColumnName) {
// Set artificial photo URLs using Contacts.CORP_CONTENT_URI.
case PhoneLookup.PHOTO_THUMBNAIL_URI:
builder.add(getCorpThumbnailUri(contactId, original));
@@ -6583,7 +6658,8 @@
builder.add(getCorpDisplayPhotoUri(contactId, original));
break;
case PhoneLookup._ID:
- builder.add(original.getLong(i) + Contacts.ENTERPRISE_CONTACT_ID_BASE);
+ builder.add(original.getLong(originalColumnIndex)
+ + Contacts.ENTERPRISE_CONTACT_ID_BASE);
break;
// These columns are set to null.
@@ -6595,21 +6671,21 @@
break;
default:
// 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;
}
}
@@ -8317,6 +8393,7 @@
return mContactsHelper.getDataMimeType(id);
}
case PHONES:
+ case PHONES_ENTERPRISE:
return Phone.CONTENT_TYPE;
case PHONES_ID:
return Phone.CONTENT_ITEM_TYPE;
@@ -8391,6 +8468,7 @@
case DATA_ID:
case PHONES:
+ case PHONES_ENTERPRISE:
case PHONES_ID:
case EMAILS:
case EMAILS_ID:
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/DbModifierWithNotification.java b/src/com/android/providers/contacts/DbModifierWithNotification.java
index 1762557..3576849 100644
--- a/src/com/android/providers/contacts/DbModifierWithNotification.java
+++ b/src/com/android/providers/contacts/DbModifierWithNotification.java
@@ -59,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
@@ -76,6 +76,7 @@
private final boolean mIsCallsTable;
private final VoicemailPermissions mVoicemailPermissions;
+
public DbModifierWithNotification(String tableName, SQLiteDatabase db, Context context) {
this(tableName, db, null, context);
}
@@ -127,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) {
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/aggregation/ContactAggregator.java b/src/com/android/providers/contacts/aggregation/ContactAggregator.java
index d030687..a970d3b 100644
--- a/src/com/android/providers/contacts/aggregation/ContactAggregator.java
+++ b/src/com/android/providers/contacts/aggregation/ContactAggregator.java
@@ -260,7 +260,7 @@
long rawContactId;
String displayName;
int displayNameSource;
- boolean verified;
+ boolean isNameSuperPrimary;
boolean writableAccount;
public DisplayNameCandidate() {
@@ -271,7 +271,7 @@
rawContactId = -1;
displayName = null;
displayNameSource = DisplayNameSources.UNDEFINED;
- verified = false;
+ isNameSuperPrimary = false;
writableAccount = false;
}
}
@@ -396,11 +396,11 @@
// 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);
+ mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone);
mRawContactsQueryByContactId = String.format(Locale.US,
RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
- mMimeTypeIdPhoto, mMimeTypeIdPhone);
+ mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone);
}
public void setEnabled(boolean enabled) {
@@ -901,8 +901,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 +910,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 +935,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 +
@@ -1979,6 +1981,13 @@
}
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 + ","
@@ -1994,11 +2003,11 @@
+ 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 +
+ + 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
@@ -2030,11 +2039,11 @@
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;
+ int DATA_ID = 13;
+ int MIMETYPE_ID = 14;
+ int IS_SUPER_PRIMARY = 15;
+ int PHOTO_FILE_ID = 16;
+ int HAS_SUPER_PRIMARY_NAME = 17;
}
private interface ContactReplaceSqlStatement {
@@ -2147,10 +2156,10 @@
// Display name
String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
- int nameVerified = c.getInt(RawContactsQuery.NAME_VERIFIED);
+ int isNameSuperPrimary = c.getInt(RawContactsQuery.HAS_SUPER_PRIMARY_NAME);
processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
mContactsProvider.isWritableAccountWithDataSet(accountWithDataSet),
- nameVerified != 0);
+ isNameSuperPrimary != 0);
// Contact options
if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
@@ -2281,17 +2290,17 @@
* {@link #mDisplayNameCandidate} with the new values.
*/
private void processDisplayNameCandidate(long rawContactId, String displayName,
- int displayNameSource, boolean writableAccount, boolean verified) {
+ 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 (!mDisplayNameCandidate.verified && verified) {
- // A verified name is better than any other name
+ if (isNameSuperPrimary) {
+ // A super primary name is better than any other name
replace = true;
- } else if (mDisplayNameCandidate.verified == verified) {
+ } 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;
@@ -2313,7 +2322,7 @@
mDisplayNameCandidate.rawContactId = rawContactId;
mDisplayNameCandidate.displayName = displayName;
mDisplayNameCandidate.displayNameSource = displayNameSource;
- mDisplayNameCandidate.verified = verified;
+ mDisplayNameCandidate.isNameSuperPrimary = isNameSuperPrimary;
mDisplayNameCandidate.writableAccount = writableAccount;
}
}
@@ -2461,19 +2470,29 @@
}
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,
- };
+ 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 NAME_VERIFIED = 3;
+ int HAS_SUPER_PRIMARY_NAME = 3;
int SOURCE_ID = 4;
int ACCOUNT_TYPE_AND_DATA_SET = 5;
}
@@ -2492,20 +2511,20 @@
mDisplayNameCandidate.clear();
- mSelectionArgs1[0] = String.valueOf(contactId);
- final Cursor c = db.query(Views.RAW_CONTACTS, DisplayNameQuery.COLUMNS,
- RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
+ 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 nameVerified = c.getInt(DisplayNameQuery.NAME_VERIFIED);
+ 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),
- nameVerified != 0);
+ 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.
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..c47d4d2
--- /dev/null
+++ b/src/com/android/providers/contacts/aggregation/ContactAggregator2.java
@@ -0,0 +1,2834 @@
+/*
+ * 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.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.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;
+
+/**
+ * ContactAggregator deals with aggregating contact information coming from different sources.
+ * Two John Doe contacts from two disjoint sources are presumed to be the same
+ * person unless the user declares otherwise.
+ */
+public class ContactAggregator2 {
+
+ private static final String TAG = "ContactAggregator2";
+
+ 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;
+
+ // 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 isNameSuperPrimary;
+ boolean writableAccount;
+
+ public DisplayNameCandidate() {
+ clear();
+ }
+
+ public void clear() {
+ rawContactId = -1;
+ displayName = null;
+ displayNameSource = DisplayNameSources.UNDEFINED;
+ isNameSuperPrimary = false;
+ writableAccount = false;
+ }
+ }
+
+ /**
+ * Constructor.
+ */
+ public ContactAggregator2(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 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) {
+
+ 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;
+ }
+
+ long contactId = -1; // Best matching contact ID.
+ boolean needReaggregate = false;
+
+ final Set<Long> rawContactIdsInSameAccount = new HashSet<Long>();
+ final Set<Long> rawContactIdsInOtherAccount = new HashSet<Long>();
+ if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
+ candidates.clear();
+ matcher.clear();
+
+ contactId = pickBestMatchBasedOnExceptions(db, rawContactId, matcher);
+ if (contactId == -1) {
+
+ // If this is a newly inserted contact or a visible contact, look for
+ // data matches.
+ if (currentContactId == 0
+ || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) {
+ contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher);
+ }
+
+ // If we found an best matched contact, find out if the raw contact can be joined
+ // into it
+ if (contactId != -1 && contactId != currentContactId) {
+ // List all raw contact ID and their account ID mappings in contact
+ // [contactId] excluding raw_contact [rawContactId].
+
+ // Based on the mapping, create two sets of raw contact IDs in
+ // [rawContactAccountId] and not in [rawContactAccountId]. We don't always
+ // need them, so lazily initialize them.
+ mSelectionArgs2[0] = String.valueOf(contactId);
+ mSelectionArgs2[1] = String.valueOf(rawContactId);
+ final Cursor rawContactsToAccountsCursor = db.rawQuery(
+ "SELECT " + RawContacts._ID + ", " + RawContactsColumns.ACCOUNT_ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.CONTACT_ID + "=?" +
+ " AND " + RawContacts._ID + "!=?",
+ mSelectionArgs2);
+ try {
+ rawContactsToAccountsCursor.moveToPosition(-1);
+ while (rawContactsToAccountsCursor.moveToNext()) {
+ final long rcId = rawContactsToAccountsCursor.getLong(0);
+ final long rc_accountId = rawContactsToAccountsCursor.getLong(1);
+ if (rc_accountId == accountId) {
+ rawContactIdsInSameAccount.add(rcId);
+ } else {
+ rawContactIdsInOtherAccount.add(rcId);
+ }
+ }
+ } finally {
+ rawContactsToAccountsCursor.close();
+ }
+ final int actionCode;
+ final int totalNumOfRawContactsInCandidate = rawContactIdsInSameAccount.size()
+ + rawContactIdsInOtherAccount.size();
+ if (totalNumOfRawContactsInCandidate >= AGGREGATION_CONTACT_SIZE_LIMIT) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Too many raw contacts (" + totalNumOfRawContactsInCandidate
+ + ") in the best matching contact, so skip aggregation");
+ }
+ actionCode = KEEP_SEPARATE;
+ } else {
+ actionCode = canJoinIntoContact(db, rawContactId,
+ rawContactIdsInSameAccount, rawContactIdsInOtherAccount);
+ }
+ if (actionCode == KEEP_SEPARATE) {
+ contactId = -1;
+ } else if (actionCode == RE_AGGREGATE) {
+ needReaggregate = true;
+ }
+ }
+ }
+ } 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();
+ }
+
+ // 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 (contactId == -1
+ && currentContactId != 0
+ && (currentContactContentsCount == 0
+ || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) {
+ contactId = currentContactId;
+ }
+
+ if (contactId == currentContactId) {
+ // Aggregation unchanged
+ markAggregated(rawContactId);
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Aggregation unchanged");
+ }
+ } else if (contactId == -1) {
+ // create new contact for [rawContactId]
+ createContactForRawContacts(db, txContext, Sets.newHashSet(rawContactId), null);
+ if (currentContactContentsCount > 0) {
+ updateAggregateData(txContext, currentContactId);
+ }
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "create new contact for rid=" + rawContactId);
+ }
+ } else if (needReaggregate) {
+ // re-aggregate
+ final Set<Long> allRawContactIdSet = new HashSet<Long>();
+ allRawContactIdSet.addAll(rawContactIdsInSameAccount);
+ allRawContactIdSet.addAll(rawContactIdsInOtherAccount);
+ // If there is no other raw contacts aggregated with the given raw contact currently,
+ // we might as well reuse it.
+ currentContactId = (currentContactId != 0 && currentContactContentsCount == 0)
+ ? currentContactId : 0;
+ reAggregateRawContacts(txContext, db, contactId, currentContactId, rawContactId,
+ allRawContactIdSet);
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Re-aggregating rid=" + rawContactId + " and cid=" + contactId);
+ }
+ } else {
+ // Joining with an existing aggregate
+ if (currentContactContentsCount == 0) {
+ // Delete a previous aggregate if it only contained this raw contact
+ ContactsTableUtil.deleteContact(db, currentContactId);
+
+ mAggregatedPresenceDelete.bindLong(1, currentContactId);
+ mAggregatedPresenceDelete.execute();
+ }
+
+ clearSuperPrimarySetting(db, contactId, rawContactId);
+ setContactIdAndMarkAggregated(rawContactId, contactId);
+ computeAggregateData(db, contactId, mContactUpdate);
+ mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
+ mContactUpdate.execute();
+ mDbHelper.updateContactVisible(txContext, contactId);
+ updateAggregatedStatusUpdate(contactId);
+ // Make sure the raw contact does not contribute to the current contact
+ if (currentContactId != 0) {
+ updateAggregateData(txContext, currentContactId);
+ }
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Join rid=" + rawContactId + " with cid=" + contactId);
+ }
+ }
+ }
+
+ /**
+ * Find out which mime-types are shared by raw contact of {@code rawContactId} and raw contacts
+ * of {@code contactId}. Clear the is_super_primary settings for these mime-types.
+ */
+ private void clearSuperPrimarySetting(SQLiteDatabase db, long contactId, long rawContactId) {
+ final String[] args = {String.valueOf(contactId), String.valueOf(rawContactId)};
+
+ // 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 (");
+
+ 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.IS_SUPER_PRIMARY + " =1 AND " +
+ Data.RAW_CONTACT_ID + "=?2) AS b" +
+ " ON a." + DataColumns.MIMETYPE_ID + "=b." + DataColumns.MIMETYPE_ID,
+ args);
+ 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 (SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.CONTACT_ID + "=?1)" +
+ " OR " + Data.RAW_CONTACT_ID + "=?2)";
+
+ mimeTypeCondition.append(')');
+ superPrimaryUpdateSql += mimeTypeCondition.toString();
+ db.execSQL(superPrimaryUpdateSql, args);
+ }
+
+ /**
+ * @return JOIN if the raw contact of {@code rawContactId} can be joined into the existing
+ * contact of {@code contactId}. KEEP_SEPARATE if the raw contact of {@code rawContactId}
+ * cannot be joined into the existing contact of {@code contactId}. RE_AGGREGATE if raw contact
+ * of {@code rawContactId} and all the raw contacts of contact of {@code contactId} need to be
+ * re-aggregated.
+ *
+ * If contact of {@code contactId} doesn't contain any raw contacts from the same account as
+ * raw contact of {@code rawContactId}, join raw contact with contact if there is no identity
+ * mismatch between them on the same namespace, otherwise, keep them separate.
+ *
+ * If contact of {@code contactId} contains raw contacts from the same account as raw contact of
+ * {@code rawContactId}, join raw contact with contact if there's at least one raw contact in
+ * those raw contacts that shares at least one email address, phone number, or identity;
+ * otherwise, re-aggregate raw contact and all the raw contacts of contact.
+ */
+ private int canJoinIntoContact(SQLiteDatabase db, long rawContactId,
+ Set<Long> rawContactIdsInSameAccount, Set<Long> rawContactIdsInOtherAccount ) {
+
+ if (rawContactIdsInSameAccount.isEmpty()) {
+ final String rid = String.valueOf(rawContactId);
+ final String ridsInOtherAccts = TextUtils.join(",", rawContactIdsInOtherAccount);
+ // If there is no identity match between raw contact of [rawContactId] and
+ // any raw contact in other accounts on the same namespace, and there is at least
+ // one identity mismatch exist, keep raw contact separate from contact.
+ if (DatabaseUtils.longForQuery(db, buildIdentityMatchingSql(rid, ridsInOtherAccts,
+ /* isIdentityMatching =*/ true, /* countOnly =*/ true), null) == 0 &&
+ DatabaseUtils.longForQuery(db, buildIdentityMatchingSql(rid, ridsInOtherAccts,
+ /* isIdentityMatching =*/ false, /* countOnly =*/ true), null) > 0) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: no duplicates, but has no matching identity " +
+ "and has mis-matching identity on the same namespace between rid=" +
+ rid + " and ridsInOtherAccts=" + ridsInOtherAccts);
+ }
+ return KEEP_SEPARATE; // has identity and identity doesn't match
+ } else {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: can join the first raw contact from the same " +
+ "account without any identity mismatch.");
+ }
+ return JOIN; // no identity or identity match
+ }
+ }
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: " + rawContactIdsInSameAccount.size() +
+ " duplicate(s) found");
+ }
+
+
+ final Set<Long> rawContactIdSet = new HashSet<Long>();
+ rawContactIdSet.add(rawContactId);
+ if (rawContactIdsInSameAccount.size() > 0 &&
+ isDataMaching(db, rawContactIdSet, rawContactIdsInSameAccount)) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: join if there is a data matching found in the " +
+ "same account");
+ }
+ return JOIN;
+ } else {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: re-aggregate rid=" + rawContactId +
+ " with its best matching contact to connected component");
+ }
+ return RE_AGGREGATE;
+ }
+ }
+
+ 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.
+ */
+ private boolean isDataMaching(SQLiteDatabase db, Set<Long> rawContactIdSet1,
+ Set<Long> rawContactIdSet2) {
+ final String rawContactIds1 = TextUtils.join(",", rawContactIdSet1);
+ final String rawContactIds2 = TextUtils.join(",", rawContactIdSet2);
+ // First, check for the identity
+ if (isFirstColumnGreaterThanZero(db, buildIdentityMatchingSql(
+ rawContactIds1, rawContactIds2, /* isIdentityMatching =*/ true,
+ /* countOnly =*/true))) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: identity match found between " + rawContactIds1 +
+ " and " + rawContactIds2);
+ }
+ return true;
+ }
+
+ // Next, check for the email address.
+ if (isFirstColumnGreaterThanZero(db,
+ buildEmailMatchingSql(rawContactIds1, rawContactIds2, true))) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: email match found between " + rawContactIds1 +
+ " and " + rawContactIds2);
+ }
+ return true;
+ }
+
+ // Lastly, the phone number.
+ if (isFirstColumnGreaterThanZero(db,
+ buildPhoneMatchingSql(rawContactIds1, rawContactIds2, true))) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: phone match found between " + rawContactIds1 +
+ " and " + rawContactIds2);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Re-aggregate rawContact of {@code rawContactId} and all the raw contacts of
+ * {@code existingRawContactIds} 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 contactId, long currentContactId, long rawContactId,
+ Set<Long> existingRawContactIds) {
+ // 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<Long>();
+ allIds.add(rawContactId);
+ allIds.addAll(existingRawContactIds);
+ final Set<Set<Long>> connectedRawContactSets = findConnectedRawContacts(db, allIds);
+
+ if (connectedRawContactSets.size() == 1) {
+ // If everything is connected, create one contact with [contactId]
+ createContactForRawContacts(db, txContext, connectedRawContactSets.iterator().next(),
+ contactId);
+ } else {
+ for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
+ if (connectedRawContactIds.contains(rawContactId)) {
+ // crate contact for connect component containing [rawContactId], reuse
+ // [currentContactId] if possible.
+ createContactForRawContacts(db, txContext, connectedRawContactIds,
+ currentContactId == 0 ? null : currentContactId);
+ connectedRawContactSets.remove(connectedRawContactIds);
+ break;
+ }
+ }
+ // Create new contact for each connected component except the last one. The last one
+ // will reuse [contactId]. Only the last one can reuse [contactId] when all other raw
+ // contacts has already been assigned new contact Id, so that the contact aggregation
+ // stats could be updated correctly.
+ int index = connectedRawContactSets.size();
+ for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
+ if (index > 1) {
+ createContactForRawContacts(db, txContext, connectedRawContactIds, null);
+ index--;
+ } else {
+ createContactForRawContacts(db, txContext, connectedRawContactIds, contactId);
+ }
+ }
+ }
+ }
+
+ /**
+ * 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
+ * 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(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+ for (MatchScore matchScore : bestMatches) {
+ markContactForAggregation(db, matchScore.getContactId());
+ }
+
+ mMatcher.clear();
+ updateMatchScoresBasedOnEmailMatches(db, rawContactId, mMatcher);
+ updateMatchScoresBasedOnPhoneMatches(db, rawContactId, mMatcher);
+ bestMatches =
+ mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
+ for (MatchScore matchScore : bestMatches) {
+ markContactForAggregation(db, matchScore.getContactId());
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ 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
+ 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 "
+ + " 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.AGGREGATION_NEEDED,
+ "raw_contacts2." + RawContacts.CONTACT_ID,
+ "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED,
+ };
+
+ int TYPE = 0;
+ int RAW_CONTACT_ID1 = 1;
+ int CONTACT_ID1 = 2;
+ int AGGREGATION_NEEDED_1 = 3;
+ int CONTACT_ID2 = 4;
+ int AGGREGATION_NEEDED_2 = 5;
+ }
+
+ /**
+ * Computes match scores based on exceptions entered by the user: always match and never match.
+ * Returns the aggregate contact with the always match exception if any.
+ */
+ private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId,
+ ContactMatcher matcher) {
+ 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 -1;
+ }
+
+ 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;
+ if (rawContactId == rawContactId1) {
+ if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_2) == 0
+ && !c.isNull(AggregateExceptionQuery.CONTACT_ID2)) {
+ contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2);
+ }
+ } else {
+ if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_1) == 0
+ && !c.isNull(AggregateExceptionQuery.CONTACT_ID1)) {
+ contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1);
+ }
+ }
+ if (contactId != -1) {
+ if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) {
+ matcher.keepIn(contactId);
+ } else {
+ matcher.keepOut(contactId);
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true);
+ }
+
+ /**
+ * Picks the best matching contact based on matches between data elements. It considers
+ * name match to be primary and phone, email etc matches to be secondary. A good primary
+ * match triggers aggregation, while a good secondary match only triggers aggregation in
+ * the absence of a strong primary mismatch.
+ * <p>
+ * Consider these examples:
+ * <p>
+ * John Doe with phone number 111-111-1111 and Jon Doe with phone number 111-111-1111 should
+ * be aggregated (same number, similar names).
+ * <p>
+ * John Doe with phone number 111-111-1111 and Deborah Doe with phone number 111-111-1111 should
+ * not be aggregated (same number, different names).
+ */
+ private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId,
+ MatchCandidateList candidates, ContactMatcher matcher) {
+
+ // Find good matches based on name alone
+ long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, matcher);
+ if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) {
+ // We found multiple matches on the name - do not aggregate because of the ambiguity
+ return -1;
+ } else if (bestMatch == -1) {
+ // We haven't found a good match on name, see if we have any matches on phone, email etc
+ bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher);
+ if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) {
+ return -1;
+ }
+ }
+
+ return bestMatch;
+ }
+
+
+ /**
+ * Picks the best matching contact based on secondary data matches. The method loads
+ * structured names for all candidate contacts and recomputes match scores using approximate
+ * matching.
+ */
+ private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db,
+ long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
+ List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates(
+ ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+ if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) {
+ return -1;
+ }
+
+ loadNameMatchCandidates(db, rawContactId, candidates, true);
+
+ mSb.setLength(0);
+ mSb.append(RawContacts.CONTACT_ID).append(" IN (");
+ for (int i = 0; i < secondaryContactIds.size(); i++) {
+ if (i != 0) {
+ mSb.append(',');
+ }
+ mSb.append(secondaryContactIds.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,
+ ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
+
+ 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.
+ */
+ private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
+ ContactMatcher matcher) {
+
+ updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
+ updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+ long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY, false);
+ if (bestMatch != -1) {
+ return bestMatch;
+ }
+
+ updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+
+ return -1;
+ }
+
+ private 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[] {
+ RawContacts.CONTACT_ID
+ };
+
+ int CONTACT_ID = 0;
+ }
+
+ /**
+ * Finds contacts with exact identity matches to the the specified raw contact.
+ */
+ private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId,
+ ContactMatcher 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 contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID);
+ matcher.matchIdentity(contactId);
+ }
+ } finally {
+ c.close();
+ }
+
+ }
+
+ private 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.CONTACT_ID,
+ "nameA." + NameLookupColumns.NORMALIZED_NAME,
+ "nameA." + NameLookupColumns.NAME_TYPE,
+ "nameB." + NameLookupColumns.NAME_TYPE,
+ };
+
+ int CONTACT_ID = 0;
+ int NAME = 1;
+ int NAME_TYPE_A = 2;
+ int NAME_TYPE_B = 3;
+ }
+
+ /**
+ * Finds contacts with names matching the name of the specified raw contact.
+ */
+ private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
+ ContactMatcher 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 contactId = c.getLong(NameLookupMatchQuery.CONTACT_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(contactId, nameTypeA, name,
+ nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT);
+ if (nameTypeA == NameLookupType.NICKNAME &&
+ nameTypeB == NameLookupType.NICKNAME) {
+ matcher.updateScoreWithNicknameMatch(contactId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ 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);
+ 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 contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
+ matcher.updateScoreWithEmailMatch(contactId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ 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);
+ 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 contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
+ matcher.updateScoreWithPhoneNumberMatch(contactId);
+ }
+ } 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,
+ ContactMatcher matcher) {
+ HashSet<String> firstLetters = new HashSet<String>();
+ for (int i = 0; i < candidates.mCount; i++) {
+ final NameMatchCandidate candidate = candidates.mList.get(i);
+ if (candidate.mName.length() >= 2) {
+ String firstLetter = candidate.mName.substring(0, 2);
+ if (!firstLetters.contains(firstLetter)) {
+ firstLetters.add(firstLetter);
+ final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '"
+ + firstLetter + "*') AND "
+ + "(" + 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,
+ ContactMatcher.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.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;
+ }
+
+ 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 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.
+ */
+ 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 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 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();
+
+ 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 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,
+ ArrayList<AggregationSuggestionParameter> parameters) {
+
+ MatchCandidateList candidates = new MatchCandidateList();
+ ContactMatcher matcher = new ContactMatcher();
+
+ // Don't aggregate a contact with itself
+ matcher.keepOut(contactId);
+
+ 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);
+ updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
+ matcher);
+ }
+ } finally {
+ c.close();
+ }
+ } else {
+ updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates,
+ matcher, parameters);
+ }
+
+ return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST);
+ }
+
+ /**
+ * Computes scores for contacts that have matching data rows.
+ */
+ private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
+ long rawContactId, MatchCandidateList candidates, ContactMatcher 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);
+ }
+
+ private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
+ MatchCandidateList candidates, ContactMatcher 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
+ }
+ }
+}
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/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index 0e4d9d6..b0fb3e9 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;
@@ -318,7 +316,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,
@@ -384,7 +381,6 @@
RawContacts.SOURCE_ID,
RawContacts.VERSION,
RawContacts.DIRTY,
- RawContacts.NAME_VERIFIED,
RawContacts.RAW_CONTACT_IS_USER_PROFILE,
Contacts._ID,
Contacts.DISPLAY_NAME_PRIMARY,
@@ -550,7 +546,6 @@
RawContacts.VERSION,
RawContacts.DELETED,
RawContacts.DIRTY,
- RawContacts.NAME_VERIFIED,
RawContacts.SYNC1,
RawContacts.SYNC2,
RawContacts.SYNC3,
@@ -609,7 +604,6 @@
RawContacts.SOURCE_ID,
RawContacts.VERSION,
RawContacts.DIRTY,
- RawContacts.NAME_VERIFIED,
RawContacts.DELETED,
RawContacts.SYNC1,
RawContacts.SYNC2,
@@ -1946,7 +1940,7 @@
});
// First, convert and make sure it returns an empty cursor.
- Cursor rewritten = ContactsProvider2.rewriteCorpPhoneLookup(c);
+ Cursor rewritten = ContactsProvider2.rewriteCorpPhoneLookup(c.getColumnNames(), c);
assertEquals(0, rewritten.getCount());
assertEquals(19, rewritten.getColumnCount());
@@ -1993,8 +1987,9 @@
"label", // PhoneLookup.LABEL,
"+1234", // PhoneLookup.NORMALIZED_NUMBER
});
- rewritten = ContactsProvider2.rewriteCorpPhoneLookup(c);
+ rewritten = ContactsProvider2.rewriteCorpPhoneLookup(c.getColumnNames(), c);
assertEquals(2, rewritten.getCount());
+ assertEquals(19, rewritten.getColumnCount());
rewritten.moveToPosition(0);
int column = 0;
@@ -2042,6 +2037,25 @@
assertEquals(1, rewritten.getInt(column++));
assertEquals("label", rewritten.getString(column++));
assertEquals("+1234", rewritten.getString(column++));
+
+ // Use a narower projection.
+ rewritten = ContactsProvider2.rewriteCorpPhoneLookup(
+ new String[] {PhoneLookup.PHOTO_URI, PhoneLookup.PHOTO_THUMBNAIL_URI}, c);
+ 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() {
@@ -7252,10 +7266,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 +8632,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/aggregation/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
index 0e0264c..43fc488 100644
--- a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
@@ -55,7 +55,8 @@
*
* 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 +1315,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 +1560,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 +1601,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 +1618,74 @@
cursor.close();
}
+ public void testAggregation_phoneticNamePriority1() {
+ // Setup: one raw contact has a complex phonetic name and the other a simple given name
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertPhoneticName(mResolver, rawContactId1, "name phonetic");
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: given name is used instead of phonetic, contrary to results of
+ // testAggregation_nameComplexity
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name", queryDisplayName(contactId));
+ }
+
+ // Same as testAggregation_phoneticNamePriority1, but with setup order reversed
+ public void testAggregation_phoneticNamePriority2() {
+ // Setup: one raw contact has a complex phonetic name and the other a simple given name
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ DataUtil.insertPhoneticName(mResolver, rawContactId1, "name phonetic");
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: given name is used instead of phonetic, contrary to results of
+ // testAggregation_nameComplexity
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name", queryDisplayName(contactId));
+ }
+
+ public void testAggregation_nameComplexity1() {
+ // Setup: two names, one of which is unambiguously more complex
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name phonetic", ACCOUNT_1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: more complex name is used
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name phonetic", queryDisplayName(contactId));
+ }
+
+ // Same as testAggregation_nameComplexity1, but with setup order reversed
+ public void testAggregation_nameComplexity2() {
+ // Setup: two names, one of which is unambiguously more complex
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name phonetic", ACCOUNT_1);
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, null,
+ "name", ACCOUNT_1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: more complex name is used
+ long contactId = queryContactId(rawContactId1);
+ assertEquals("name phonetic", queryDisplayName(contactId));
+ }
+
public void testAggregation_clearSuperPrimary() {
// Three types of mime-type super primary merging are tested here
// 1. both raw contacts have super primary phone numbers
@@ -1587,7 +1693,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 +1704,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);
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