aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml24
-rw-r--r--res/values-az/strings.xml (renamed from res/values-az-rAZ/strings.xml)0
-rw-r--r--res/values-b+sr+Latn/strings.xml2
-rw-r--r--res/values-be/strings.xml35
-rw-r--r--res/values-bn/strings.xml (renamed from res/values-bn-rBD/strings.xml)0
-rw-r--r--res/values-bs/strings.xml35
-rw-r--r--res/values-de/strings.xml2
-rw-r--r--res/values-et/strings.xml (renamed from res/values-et-rEE/strings.xml)0
-rw-r--r--res/values-eu/strings.xml (renamed from res/values-eu-rES/strings.xml)0
-rw-r--r--res/values-gl/strings.xml (renamed from res/values-gl-rES/strings.xml)0
-rw-r--r--res/values-gu/strings.xml (renamed from res/values-gu-rIN/strings.xml)0
-rw-r--r--res/values-hy/strings.xml (renamed from res/values-hy-rAM/strings.xml)0
-rw-r--r--res/values-is/strings.xml (renamed from res/values-is-rIS/strings.xml)0
-rw-r--r--res/values-ka/strings.xml (renamed from res/values-ka-rGE/strings.xml)0
-rw-r--r--res/values-kk/strings.xml (renamed from res/values-kk-rKZ/strings.xml)0
-rw-r--r--res/values-km/strings.xml (renamed from res/values-km-rKH/strings.xml)0
-rw-r--r--res/values-kn/strings.xml (renamed from res/values-kn-rIN/strings.xml)0
-rw-r--r--res/values-ko/strings.xml2
-rw-r--r--res/values-ky/strings.xml (renamed from res/values-ky-rKG/strings.xml)0
-rw-r--r--res/values-lo/strings.xml (renamed from res/values-lo-rLA/strings.xml)0
-rw-r--r--res/values-mk/strings.xml (renamed from res/values-mk-rMK/strings.xml)0
-rw-r--r--res/values-ml/strings.xml (renamed from res/values-ml-rIN/strings.xml)0
-rw-r--r--res/values-mn/strings.xml (renamed from res/values-mn-rMN/strings.xml)0
-rw-r--r--res/values-mr/strings.xml (renamed from res/values-mr-rIN/strings.xml)0
-rw-r--r--res/values-ms/strings.xml (renamed from res/values-ms-rMY/strings.xml)0
-rw-r--r--res/values-my/strings.xml (renamed from res/values-my-rMM/strings.xml)0
-rw-r--r--res/values-ne/strings.xml (renamed from res/values-ne-rNP/strings.xml)0
-rw-r--r--res/values-pa/strings.xml (renamed from res/values-pa-rIN/strings.xml)0
-rw-r--r--res/values-si/strings.xml (renamed from res/values-si-rLK/strings.xml)0
-rw-r--r--res/values-sq/strings.xml (renamed from res/values-sq-rAL/strings.xml)0
-rw-r--r--res/values-ta/strings.xml (renamed from res/values-ta-rIN/strings.xml)0
-rw-r--r--res/values-te/strings.xml (renamed from res/values-te-rIN/strings.xml)0
-rw-r--r--res/values-ur/strings.xml (renamed from res/values-ur-rPK/strings.xml)0
-rw-r--r--res/values-uz/strings.xml (renamed from res/values-uz-rUZ/strings.xml)0
-rwxr-xr-xrun-all-tests.sh41
-rw-r--r--src/com/android/providers/contacts/AbstractContactsProvider.java214
-rw-r--r--src/com/android/providers/contacts/CallLogDatabaseHelper.java40
-rw-r--r--src/com/android/providers/contacts/CallLogProvider.java46
-rw-r--r--src/com/android/providers/contacts/ContactDirectoryManager.java144
-rw-r--r--src/com/android/providers/contacts/ContactMetadataProvider.java10
-rw-r--r--src/com/android/providers/contacts/ContactsDatabaseHelper.java2445
-rw-r--r--src/com/android/providers/contacts/ContactsPackageMonitor.java165
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java426
-rw-r--r--src/com/android/providers/contacts/ContactsTaskScheduler.java159
-rw-r--r--src/com/android/providers/contacts/ContactsTransaction.java4
-rw-r--r--src/com/android/providers/contacts/ContactsUpgradeReceiver.java3
-rw-r--r--src/com/android/providers/contacts/DbModifierWithNotification.java11
-rw-r--r--src/com/android/providers/contacts/GlobalSearchSupport.java2
-rw-r--r--src/com/android/providers/contacts/LegacyApiSupport.java105
-rw-r--r--src/com/android/providers/contacts/PackageIntentReceiver.java57
-rw-r--r--src/com/android/providers/contacts/ProfileAwareUriMatcher.java3
-rw-r--r--src/com/android/providers/contacts/ProfileDatabaseHelper.java26
-rw-r--r--src/com/android/providers/contacts/ProfileProvider.java17
-rw-r--r--src/com/android/providers/contacts/SearchIndexManager.java2
-rw-r--r--src/com/android/providers/contacts/VoicemailCleanupService.java57
-rw-r--r--src/com/android/providers/contacts/VoicemailContentProvider.java188
-rw-r--r--src/com/android/providers/contacts/VoicemailContentTable.java11
-rw-r--r--src/com/android/providers/contacts/VoicemailStatusTable.java9
-rw-r--r--src/com/android/providers/contacts/VoicemailTable.java2
-rw-r--r--src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java33
-rw-r--r--src/com/android/providers/contacts/sqlite/DatabaseAnalyzer.java103
-rw-r--r--src/com/android/providers/contacts/sqlite/SqlChecker.java259
-rw-r--r--src/com/android/providers/contacts/util/DbQueryUtils.java2
-rw-r--r--src/com/android/providers/contacts/util/PackageUtils.java44
-rw-r--r--test_common/Android.mk31
-rw-r--r--test_common/src/com/android/providers/contacts/testutil/CommonDatabaseUtils.java (renamed from tests/src/com/android/providers/contacts/testutil/CommonDatabaseUtils.java)0
-rw-r--r--test_common/src/com/android/providers/contacts/testutil/ContactUtil.java (renamed from tests/src/com/android/providers/contacts/testutil/ContactUtil.java)0
-rw-r--r--test_common/src/com/android/providers/contacts/testutil/DataUtil.java (renamed from tests/src/com/android/providers/contacts/testutil/DataUtil.java)1
-rw-r--r--test_common/src/com/android/providers/contacts/testutil/DatabaseAsserts.java (renamed from tests/src/com/android/providers/contacts/testutil/DatabaseAsserts.java)0
-rw-r--r--test_common/src/com/android/providers/contacts/testutil/DeletedContactUtil.java (renamed from tests/src/com/android/providers/contacts/testutil/DeletedContactUtil.java)0
-rw-r--r--test_common/src/com/android/providers/contacts/testutil/RawContactUtil.java (renamed from tests/src/com/android/providers/contacts/testutil/RawContactUtil.java)18
-rw-r--r--test_common/src/com/android/providers/contacts/testutil/TestUtil.java (renamed from tests/src/com/android/providers/contacts/testutil/TestUtil.java)11
-rw-r--r--tests/Android.mk7
-rw-r--r--tests/AndroidTest.xml27
-rw-r--r--tests/assets/upgradeTest/pre_upgrade1201.sql27
-rw-r--r--tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java28
-rw-r--r--tests/src/com/android/providers/contacts/BaseDatabaseHelperUpgradeTest.java13
-rw-r--r--tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java9
-rw-r--r--tests/src/com/android/providers/contacts/CallLogProviderTest.java2
-rw-r--r--tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java64
-rw-r--r--tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java9
-rw-r--r--tests/src/com/android/providers/contacts/ContactsActor.java26
-rw-r--r--tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java178
-rw-r--r--tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java148
-rw-r--r--tests/src/com/android/providers/contacts/ContactsMockPackageManager.java42
-rw-r--r--tests/src/com/android/providers/contacts/ContactsProvider2Test.java328
-rw-r--r--tests/src/com/android/providers/contacts/ContactsProvider2TransactionTest.java18
-rw-r--r--tests/src/com/android/providers/contacts/ContactsTaskSchedulerTest.java103
-rw-r--r--tests/src/com/android/providers/contacts/DirectoryTest.java10
-rw-r--r--tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java30
-rw-r--r--tests/src/com/android/providers/contacts/PhotoStoreTest.java2
-rw-r--r--tests/src/com/android/providers/contacts/RenamingDelegatingContext.java255
-rw-r--r--tests/src/com/android/providers/contacts/SecondaryUserContactsProvider2.java (renamed from tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java)23
-rw-r--r--tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java93
-rw-r--r--tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java30
-rw-r--r--tests/src/com/android/providers/contacts/SynchronousProfileProvider.java5
-rw-r--r--tests/src/com/android/providers/contacts/TestUtils.java181
-rw-r--r--tests/src/com/android/providers/contacts/VoicemailCleanupServiceTest.java111
-rw-r--r--tests/src/com/android/providers/contacts/VoicemailCleanupTest.java135
-rw-r--r--tests/src/com/android/providers/contacts/VoicemailProviderTest.java2
-rw-r--r--tests/src/com/android/providers/contacts/sqlite/DatabaseAnalyzerTest.java48
-rw-r--r--tests/src/com/android/providers/contacts/sqlite/SqlCheckerTest.java252
-rw-r--r--tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java10
-rw-r--r--tests/src/com/android/providers/contacts/util/MockClock.java4
-rw-r--r--tests2/Android.mk40
-rw-r--r--tests2/AndroidManifest.xml35
-rw-r--r--tests2/AndroidTest.xml27
-rw-r--r--tests2/src/com/android/providers/contacts/tests2/AllUriTest.java718
-rwxr-xr-xtools/contacts-db-schema.sh2
109 files changed, 5026 insertions, 2805 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7829feee..aa1c396f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -34,7 +34,8 @@
android:exported="true"
android:grantUriPermissions="true"
android:readPermission="android.permission.READ_CONTACTS"
- android:writePermission="android.permission.WRITE_CONTACTS">
+ android:writePermission="android.permission.WRITE_CONTACTS"
+ android:visibleToInstantApps="true">
<path-permission
android:pathPrefix="/search_suggest_query"
android:readPermission="android.permission.GLOBAL_SEARCH" />
@@ -95,33 +96,12 @@
</intent-filter>
</receiver>
- <receiver android:name="PackageIntentReceiver">
- <intent-filter>
- <action android:name="android.intent.action.PACKAGE_ADDED" />
- <data android:scheme="package" />
- </intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.PACKAGE_REPLACED" />
- <data android:scheme="package" />
- </intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.PACKAGE_REMOVED" />
- <data android:scheme="package" />
- </intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.PACKAGE_CHANGED" />
- <data android:scheme="package" />
- </intent-filter>
- </receiver>
-
<receiver android:name="LocaleChangeReceiver">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED"/>
</intent-filter>
</receiver>
- <service android:name="VoicemailCleanupService"/>
-
<activity android:name=".debug.ContactsDumpActivity"
android:label="@string/debug_dump_title"
android:theme="@android:style/Theme.Holo.Dialog"
diff --git a/res/values-az-rAZ/strings.xml b/res/values-az/strings.xml
index 9088a5c2..9088a5c2 100644
--- a/res/values-az-rAZ/strings.xml
+++ b/res/values-az/strings.xml
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index ae8b46c9..b9427e4c 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -21,7 +21,7 @@
<string name="provider_label" msgid="6012150850819899907">"Kontakti"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Za ažuriranje kontakata potrebno je više memorije."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Nadograđivanje memorije za kontakte"</string>
- <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dodirnite da biste dovršili nadogradnju."</string>
+ <string name="upgrade_out_of_memory_notification_text" msgid="2581831842693151968">"Dodirnite da biste dovršili nadogradnju."</string>
<string name="default_directory" msgid="93961630309570294">"Kontakti"</string>
<string name="local_invisible_directory" msgid="705244318477396120">"Drugo"</string>
<string name="voicemail_from_column" msgid="435732568832121444">"Govorna pošta od "</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
new file mode 100644
index 00000000..0eeb7d44
--- /dev/null
+++ b/res/values-be/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Асноўныя праграмы для Android"</string>
+ <string name="app_label" msgid="3389954322874982620">"Сховішча кантактаў"</string>
+ <string name="provider_label" msgid="6012150850819899907">"Кантакты"</string>
+ <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Для абнаўлення кантактаў патрабуецца больш памяці."</string>
+ <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Каб абнавiць кантакты, патрабуецца больш памяцi"</string>
+ <string name="upgrade_out_of_memory_notification_text" msgid="2581831842693151968">"Дакраніцеся, каб скончыць абнаўленне."</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">"Капiраваць базу дадзеных кантактаў"</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"Вы збіраецеся 1) зрабіць копію базы дадзеных, якая ўключае ў сябе ўсе звесткi пра кантакты і званкi на ўнутранай памяці, і 2) адправiць яго па электроннай пошце. Не забудзьцеся выдаліць копію, як толькі вы паспяхова скапіруеце іх на прыладу ці атрымаеце па электроннай пошце."</string>
+ <string name="debug_dump_delete_button" msgid="7832879421132026435">"Выдаліць зараз"</string>
+ <string name="debug_dump_start_button" msgid="2837506913757600001">"Пачаць"</string>
+ <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Выберыце праграму для адпраўкі файла"</string>
+ <string name="debug_dump_email_subject" msgid="108188398416385976">"Далучаны кантакты Dd"</string>
+ <string name="debug_dump_email_body" msgid="4577749800871444318">"Далучана база дадзеных маiх кантактаў з усёй інфармацыяй. Працуйце з ёй уважліва."</string>
+</resources>
diff --git a/res/values-bn-rBD/strings.xml b/res/values-bn/strings.xml
index dc803da9..dc803da9 100644
--- a/res/values-bn-rBD/strings.xml
+++ b/res/values-bn/strings.xml
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
new file mode 100644
index 00000000..c23e9ef4
--- /dev/null
+++ b/res/values-bs/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Android osnovne aplikacije"</string>
+ <string name="app_label" msgid="3389954322874982620">"Pohrana za kontakte"</string>
+ <string name="provider_label" msgid="6012150850819899907">"Kontakti"</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="2581831842693151968">"Dodirnite da završite nadogradnju."</string>
+ <string name="default_directory" msgid="93961630309570294">"Kontakti"</string>
+ <string name="local_invisible_directory" msgid="705244318477396120">"Ostalo"</string>
+ <string name="voicemail_from_column" msgid="435732568832121444">"Govorna pošta od "</string>
+ <string name="debug_dump_title" msgid="4916885724165570279">"Kopiraj bazu podataka kontakata"</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"Upravo ćete 1) napraviti kopiju svoje baze podataka koja sadrži sve informacije o kontaktima i sve popise poziva u unutrašnjoj pohrani i 2) poslati tu kopiju e-poštom. Ne zaboravite izbrisati kopiju čim je uspješno kopirate s uređaja ili čim primite poruku e-pošte."</string>
+ <string name="debug_dump_delete_button" msgid="7832879421132026435">"Izbriši sada"</string>
+ <string name="debug_dump_start_button" msgid="2837506913757600001">"Počni"</string>
+ <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Izaberite program za slanje fajla"</string>
+ <string name="debug_dump_email_subject" msgid="108188398416385976">"Baza podataka kontakata je u prilogu"</string>
+ <string name="debug_dump_email_body" msgid="4577749800871444318">"U prilogu je moja baza podataka kontakata sa svim informacijama o kontaktima. Rukujte oprezno."</string>
+</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index a7259534..4784c4aa 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -24,7 +24,7 @@
<string name="upgrade_out_of_memory_notification_text" msgid="2581831842693151968">"Zum Abschließen der Aktualisierung tippen."</string>
<string name="default_directory" msgid="93961630309570294">"Kontakte"</string>
<string name="local_invisible_directory" msgid="705244318477396120">"Sonstige"</string>
- <string name="voicemail_from_column" msgid="435732568832121444">"Mailboxnachricht von "</string>
+ <string name="voicemail_from_column" msgid="435732568832121444">"Mailbox-Nachricht von "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"Kontaktdatenbank kopieren"</string>
<string name="debug_dump_database_message" msgid="406438635002392290">"Du 1) erstellst eine Kopie deiner Datenbank, die alle Kontaktinformationen und Anruflisten auf dem internen Speicher enthält, und 2) sendest diese Kopie per E-Mail. Denke daran, die Kopie so schnell wie möglich zu löschen, nachdem du sie vom Gerät kopiert hast oder die E-Mail empfangen wurde."</string>
<string name="debug_dump_delete_button" msgid="7832879421132026435">"Jetzt löschen"</string>
diff --git a/res/values-et-rEE/strings.xml b/res/values-et/strings.xml
index d5dbf950..d5dbf950 100644
--- a/res/values-et-rEE/strings.xml
+++ b/res/values-et/strings.xml
diff --git a/res/values-eu-rES/strings.xml b/res/values-eu/strings.xml
index e9f97d2a..e9f97d2a 100644
--- a/res/values-eu-rES/strings.xml
+++ b/res/values-eu/strings.xml
diff --git a/res/values-gl-rES/strings.xml b/res/values-gl/strings.xml
index 121853dc..121853dc 100644
--- a/res/values-gl-rES/strings.xml
+++ b/res/values-gl/strings.xml
diff --git a/res/values-gu-rIN/strings.xml b/res/values-gu/strings.xml
index 087ccc2b..087ccc2b 100644
--- a/res/values-gu-rIN/strings.xml
+++ b/res/values-gu/strings.xml
diff --git a/res/values-hy-rAM/strings.xml b/res/values-hy/strings.xml
index 2d31277f..2d31277f 100644
--- a/res/values-hy-rAM/strings.xml
+++ b/res/values-hy/strings.xml
diff --git a/res/values-is-rIS/strings.xml b/res/values-is/strings.xml
index 7e9feac0..7e9feac0 100644
--- a/res/values-is-rIS/strings.xml
+++ b/res/values-is/strings.xml
diff --git a/res/values-ka-rGE/strings.xml b/res/values-ka/strings.xml
index 28599979..28599979 100644
--- a/res/values-ka-rGE/strings.xml
+++ b/res/values-ka/strings.xml
diff --git a/res/values-kk-rKZ/strings.xml b/res/values-kk/strings.xml
index b8cd1676..b8cd1676 100644
--- a/res/values-kk-rKZ/strings.xml
+++ b/res/values-kk/strings.xml
diff --git a/res/values-km-rKH/strings.xml b/res/values-km/strings.xml
index 2a2b50ad..2a2b50ad 100644
--- a/res/values-km-rKH/strings.xml
+++ b/res/values-km/strings.xml
diff --git a/res/values-kn-rIN/strings.xml b/res/values-kn/strings.xml
index 0e37eae9..0e37eae9 100644
--- a/res/values-kn-rIN/strings.xml
+++ b/res/values-kn/strings.xml
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index d5c8a947..6e769120 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core 앱"</string>
- <string name="app_label" msgid="3389954322874982620">"연락처 저장소"</string>
+ <string name="app_label" msgid="3389954322874982620">"주소록 저장소"</string>
<string name="provider_label" msgid="6012150850819899907">"주소록"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"주소록을 업그레이드하려면 메모리가 더 필요합니다."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"주소록을 위한 저장소 업그레이드 중"</string>
diff --git a/res/values-ky-rKG/strings.xml b/res/values-ky/strings.xml
index d8251c52..d8251c52 100644
--- a/res/values-ky-rKG/strings.xml
+++ b/res/values-ky/strings.xml
diff --git a/res/values-lo-rLA/strings.xml b/res/values-lo/strings.xml
index b75c8161..b75c8161 100644
--- a/res/values-lo-rLA/strings.xml
+++ b/res/values-lo/strings.xml
diff --git a/res/values-mk-rMK/strings.xml b/res/values-mk/strings.xml
index dc1a07a9..dc1a07a9 100644
--- a/res/values-mk-rMK/strings.xml
+++ b/res/values-mk/strings.xml
diff --git a/res/values-ml-rIN/strings.xml b/res/values-ml/strings.xml
index dc2abdf9..dc2abdf9 100644
--- a/res/values-ml-rIN/strings.xml
+++ b/res/values-ml/strings.xml
diff --git a/res/values-mn-rMN/strings.xml b/res/values-mn/strings.xml
index b15c2d18..b15c2d18 100644
--- a/res/values-mn-rMN/strings.xml
+++ b/res/values-mn/strings.xml
diff --git a/res/values-mr-rIN/strings.xml b/res/values-mr/strings.xml
index 7e6d6054..7e6d6054 100644
--- a/res/values-mr-rIN/strings.xml
+++ b/res/values-mr/strings.xml
diff --git a/res/values-ms-rMY/strings.xml b/res/values-ms/strings.xml
index b638974f..b638974f 100644
--- a/res/values-ms-rMY/strings.xml
+++ b/res/values-ms/strings.xml
diff --git a/res/values-my-rMM/strings.xml b/res/values-my/strings.xml
index a8f2cb96..a8f2cb96 100644
--- a/res/values-my-rMM/strings.xml
+++ b/res/values-my/strings.xml
diff --git a/res/values-ne-rNP/strings.xml b/res/values-ne/strings.xml
index e8139577..e8139577 100644
--- a/res/values-ne-rNP/strings.xml
+++ b/res/values-ne/strings.xml
diff --git a/res/values-pa-rIN/strings.xml b/res/values-pa/strings.xml
index 12a5c6e5..12a5c6e5 100644
--- a/res/values-pa-rIN/strings.xml
+++ b/res/values-pa/strings.xml
diff --git a/res/values-si-rLK/strings.xml b/res/values-si/strings.xml
index 906978b0..906978b0 100644
--- a/res/values-si-rLK/strings.xml
+++ b/res/values-si/strings.xml
diff --git a/res/values-sq-rAL/strings.xml b/res/values-sq/strings.xml
index 37830ff6..37830ff6 100644
--- a/res/values-sq-rAL/strings.xml
+++ b/res/values-sq/strings.xml
diff --git a/res/values-ta-rIN/strings.xml b/res/values-ta/strings.xml
index 1116f19f..1116f19f 100644
--- a/res/values-ta-rIN/strings.xml
+++ b/res/values-ta/strings.xml
diff --git a/res/values-te-rIN/strings.xml b/res/values-te/strings.xml
index d6cbf41b..d6cbf41b 100644
--- a/res/values-te-rIN/strings.xml
+++ b/res/values-te/strings.xml
diff --git a/res/values-ur-rPK/strings.xml b/res/values-ur/strings.xml
index 16e28332..16e28332 100644
--- a/res/values-ur-rPK/strings.xml
+++ b/res/values-ur/strings.xml
diff --git a/res/values-uz-rUZ/strings.xml b/res/values-uz/strings.xml
index ae88a57d..ae88a57d 100644
--- a/res/values-uz-rUZ/strings.xml
+++ b/res/values-uz/strings.xml
diff --git a/run-all-tests.sh b/run-all-tests.sh
new file mode 100755
index 00000000..84a2ce37
--- /dev/null
+++ b/run-all-tests.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+set -e
+
+cd $ANDROID_BUILD_TOP
+
+. build/envsetup.sh
+
+mmm -j32 packages/providers/ContactsProvider
+adb install -t -r -g $ANDROID_PRODUCT_OUT/system/priv-app/ContactsProvider/ContactsProvider.apk
+adb install -t -r -g $ANDROID_PRODUCT_OUT/data/app/ContactsProviderTests/ContactsProviderTests.apk
+adb install -t -r -g $ANDROID_PRODUCT_OUT/data/app/ContactsProviderTests2/ContactsProviderTests2.apk
+
+runtest() {
+ log=/tmp/$$.log
+ adb shell am instrument -w "${@}" |& tee $log
+ if grep -q FAILURES $log || ! grep -P -q 'OK \([1-9]' $log ; then
+ return 1
+ else
+ return 0
+ fi
+
+}
+
+runtest com.android.providers.contacts.tests
+runtest com.android.providers.contacts.tests2 \ No newline at end of file
diff --git a/src/com/android/providers/contacts/AbstractContactsProvider.java b/src/com/android/providers/contacts/AbstractContactsProvider.java
index 0e67d103..ebf0a1b9 100644
--- a/src/com/android/providers/contacts/AbstractContactsProvider.java
+++ b/src/com/android/providers/contacts/AbstractContactsProvider.java
@@ -17,7 +17,6 @@
package com.android.providers.contacts;
import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
@@ -84,7 +83,7 @@ public abstract class AbstractContactsProvider extends ContentProvider
/**
* The DB helper to use for this content provider.
*/
- private SQLiteOpenHelper mDbHelper;
+ private ContactsDatabaseHelper mDbHelper;
/**
* The database helper to serialize all transactions on. If non-null, any new transaction
@@ -130,15 +129,20 @@ public abstract class AbstractContactsProvider extends ContentProvider
protected final SparseLongArray mUpdateInBatchStats = new SparseLongArray();
protected final SparseLongArray mDeleteInBatchStats = new SparseLongArray();
+ private final SparseLongArray mOperationDurationMicroStats = new SparseLongArray();
+
+ private final ThreadLocal<Integer> mOperationNest = ThreadLocal.withInitial(() -> 0);
+ private final ThreadLocal<Long> mOperationStartNs = ThreadLocal.withInitial(() -> 0L);
+
@Override
public boolean onCreate() {
Context context = getContext();
- mDbHelper = getDatabaseHelper(context);
+ mDbHelper = newDatabaseHelper(context);
mTransactionHolder = getTransactionHolder();
return true;
}
- public SQLiteOpenHelper getDatabaseHelper() {
+ public ContactsDatabaseHelper getDatabaseHelper() {
return mDbHelper;
}
@@ -159,6 +163,12 @@ public abstract class AbstractContactsProvider extends ContentProvider
synchronized (mStatsLock) {
stats.put(callingUid, stats.get(callingUid) + 1);
mAllCallingUids.put(callingUid, true);
+
+ final int nest = mOperationNest.get();
+ mOperationNest.set(nest + 1);
+ if (nest == 0) {
+ mOperationStartNs.set(SystemClock.elapsedRealtimeNanos());
+ }
}
}
@@ -169,6 +179,19 @@ public abstract class AbstractContactsProvider extends ContentProvider
incrementStats(inBatch ? statsInBatch : statsNonBatch);
}
+ protected void finishOperation() {
+ final int callingUid = Binder.getCallingUid();
+ synchronized (mStatsLock) {
+ final int nest = mOperationNest.get();
+ mOperationNest.set(nest - 1);
+ if (nest == 1) {
+ final long duration = SystemClock.elapsedRealtimeNanos() - mOperationStartNs.get();
+ mOperationDurationMicroStats.put(callingUid,
+ mOperationDurationMicroStats.get(callingUid) + duration / 1000L);
+ }
+ }
+ }
+
public ContactsTransaction getCurrentTransaction() {
return mTransactionHolder.get();
}
@@ -176,119 +199,139 @@ public abstract class AbstractContactsProvider extends ContentProvider
@Override
public Uri insert(Uri uri, ContentValues values) {
incrementStats(mInsertStats, mInsertInBatchStats);
- ContactsTransaction transaction = startTransaction(false);
try {
- Uri result = insertInTransaction(uri, values);
- if (result != null) {
- transaction.markDirty();
+ ContactsTransaction transaction = startTransaction(false);
+ try {
+ Uri result = insertInTransaction(uri, values);
+ if (result != null) {
+ transaction.markDirty();
+ }
+ transaction.markSuccessful(false);
+ return result;
+ } finally {
+ endTransaction(false);
}
- transaction.markSuccessful(false);
- return result;
} finally {
- endTransaction(false);
+ finishOperation();
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
incrementStats(mDeleteStats, mDeleteInBatchStats);
- ContactsTransaction transaction = startTransaction(false);
try {
- int deleted = deleteInTransaction(uri, selection, selectionArgs);
- if (deleted > 0) {
- transaction.markDirty();
+ ContactsTransaction transaction = startTransaction(false);
+ try {
+ int deleted = deleteInTransaction(uri, selection, selectionArgs);
+ if (deleted > 0) {
+ transaction.markDirty();
+ }
+ transaction.markSuccessful(false);
+ return deleted;
+ } finally {
+ endTransaction(false);
}
- transaction.markSuccessful(false);
- return deleted;
} finally {
- endTransaction(false);
+ finishOperation();
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
incrementStats(mUpdateStats, mUpdateInBatchStats);
- ContactsTransaction transaction = startTransaction(false);
try {
- int updated = updateInTransaction(uri, values, selection, selectionArgs);
- if (updated > 0) {
- transaction.markDirty();
+ ContactsTransaction transaction = startTransaction(false);
+ try {
+ int updated = updateInTransaction(uri, values, selection, selectionArgs);
+ if (updated > 0) {
+ transaction.markDirty();
+ }
+ transaction.markSuccessful(false);
+ return updated;
+ } finally {
+ endTransaction(false);
}
- transaction.markSuccessful(false);
- return updated;
} finally {
- endTransaction(false);
+ finishOperation();
}
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
incrementStats(mBatchStats);
- ContactsTransaction transaction = startTransaction(true);
- int numValues = values.length;
- int opCount = 0;
try {
- for (int i = 0; i < numValues; i++) {
- insert(uri, values[i]);
- if (++opCount >= BULK_INSERTS_PER_YIELD_POINT) {
- opCount = 0;
- try {
- yield(transaction);
- } catch (RuntimeException re) {
- transaction.markYieldFailed();
- throw re;
+ ContactsTransaction transaction = startTransaction(true);
+ int numValues = values.length;
+ int opCount = 0;
+ try {
+ for (int i = 0; i < numValues; i++) {
+ insert(uri, values[i]);
+ if (++opCount >= BULK_INSERTS_PER_YIELD_POINT) {
+ opCount = 0;
+ try {
+ yield(transaction);
+ } catch (RuntimeException re) {
+ transaction.markYieldFailed();
+ throw re;
+ }
}
}
+ transaction.markSuccessful(true);
+ } finally {
+ endTransaction(true);
}
- transaction.markSuccessful(true);
+ return numValues;
} finally {
- endTransaction(true);
+ finishOperation();
}
- return numValues;
}
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
incrementStats(mBatchStats);
- if (VERBOSE_LOGGING) {
- Log.v(TAG, "applyBatch: " + operations.size() + " ops");
- }
- int ypCount = 0;
- int opCount = 0;
- ContactsTransaction transaction = startTransaction(true);
try {
- final int numOperations = operations.size();
- final ContentProviderResult[] results = new ContentProviderResult[numOperations];
- for (int i = 0; i < numOperations; i++) {
- if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
- throw new OperationApplicationException(
- "Too many content provider operations between yield points. "
- + "The maximum number of operations per yield point is "
- + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
- }
- final ContentProviderOperation operation = operations.get(i);
- if (i > 0 && operation.isYieldAllowed()) {
- if (VERBOSE_LOGGING) {
- Log.v(TAG, "applyBatch: " + opCount + " ops finished; about to yield...");
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "applyBatch: " + operations.size() + " ops");
+ }
+ int ypCount = 0;
+ int opCount = 0;
+ ContactsTransaction transaction = startTransaction(true);
+ try {
+ final int numOperations = operations.size();
+ final ContentProviderResult[] results = new ContentProviderResult[numOperations];
+ for (int i = 0; i < numOperations; i++) {
+ if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
+ throw new OperationApplicationException(
+ "Too many content provider operations between yield points. "
+ + "The maximum number of operations per yield point is "
+ + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
}
- opCount = 0;
- try {
- if (yield(transaction)) {
- ypCount++;
+ final ContentProviderOperation operation = operations.get(i);
+ if (i > 0 && operation.isYieldAllowed()) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "applyBatch: " + opCount + " ops finished; about to yield...");
+ }
+ opCount = 0;
+ try {
+ if (yield(transaction)) {
+ ypCount++;
+ }
+ } catch (RuntimeException re) {
+ transaction.markYieldFailed();
+ throw re;
}
- } catch (RuntimeException re) {
- transaction.markYieldFailed();
- throw re;
}
- }
- results[i] = operation.apply(this, results, i);
+ results[i] = operation.apply(this, results, i);
+ }
+ transaction.markSuccessful(true);
+ return results;
+ } finally {
+ endTransaction(true);
}
- transaction.markSuccessful(true);
- return results;
} finally {
- endTransaction(true);
+ finishOperation();
}
}
@@ -345,8 +388,9 @@ public abstract class AbstractContactsProvider extends ContentProvider
/**
* Gets the database helper for this contacts provider. This is called once, during onCreate().
+ * Do not call in other places.
*/
- protected abstract SQLiteOpenHelper getDatabaseHelper(Context context);
+ protected abstract ContactsDatabaseHelper newDatabaseHelper(Context context);
/**
* Gets the thread-local transaction holder to use for keeping track of the transaction. This
@@ -425,20 +469,22 @@ public abstract class AbstractContactsProvider extends ContentProvider
synchronized (mStatsLock) {
pw.println();
pw.println(" Client activities:");
- pw.println(" UID Query Insert Update Delete Batch Insert Update Delete:");
+ pw.println(" UID Query Insert Update Delete Batch Insert Update Delete"
+ + " Sec");
for (int i = 0; i < mAllCallingUids.size(); i++) {
- final int pid = mAllCallingUids.keyAt(i);
+ final int uid = mAllCallingUids.keyAt(i);
pw.println(String.format(
- " %-9d %6d %6d %6d %6d %6d %6d %6d %6d",
- pid,
- mQueryStats.get(pid),
- mInsertStats.get(pid),
- mUpdateStats.get(pid),
- mDeleteStats.get(pid),
- mBatchStats.get(pid),
- mInsertInBatchStats.get(pid),
- mUpdateInBatchStats.get(pid),
- mDeleteInBatchStats.get(pid)
+ " %-9d %6d %6d %6d %6d %6d %6d %6d %6d %12.3f",
+ uid,
+ mQueryStats.get(uid),
+ mInsertStats.get(uid),
+ mUpdateStats.get(uid),
+ mDeleteStats.get(uid),
+ mBatchStats.get(uid),
+ mInsertInBatchStats.get(uid),
+ mUpdateInBatchStats.get(uid),
+ mDeleteInBatchStats.get(uid),
+ (mOperationDurationMicroStats.get(uid) / 1000000.0)
));
}
}
diff --git a/src/com/android/providers/contacts/CallLogDatabaseHelper.java b/src/com/android/providers/contacts/CallLogDatabaseHelper.java
index c88b742d..d4ed9304 100644
--- a/src/com/android/providers/contacts/CallLogDatabaseHelper.java
+++ b/src/com/android/providers/contacts/CallLogDatabaseHelper.java
@@ -26,6 +26,8 @@ import android.provider.CallLog.Calls;
import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Status;
import android.provider.VoicemailContract.Voicemails;
+import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -37,7 +39,7 @@ import com.android.providers.contacts.util.PropertyUtils;
public class CallLogDatabaseHelper {
private static final String TAG = "CallLogDatabaseHelper";
- private static final int DATABASE_VERSION = 4;
+ private static final int DATABASE_VERSION = 5;
private static final boolean DEBUG = false; // DON'T SUBMIT WITH TRUE
@@ -149,6 +151,7 @@ public class CallLogDatabaseHelper {
Voicemails.SOURCE_DATA + " TEXT," +
Voicemails.SOURCE_PACKAGE + " TEXT," +
Voicemails.TRANSCRIPTION + " TEXT," +
+ Voicemails.TRANSCRIPTION_STATE + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.STATE + " INTEGER," +
Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0," +
@@ -193,6 +196,10 @@ public class CallLogDatabaseHelper {
if (oldVersion < 4) {
upgradeToVersion4(db);
}
+
+ if (oldVersion < 5) {
+ upgradeToVersion5(db);
+ }
}
}
@@ -262,6 +269,13 @@ public class CallLogDatabaseHelper {
}
/**
+ * Add {@link Voicemails.TRANSCRIPTION_STATE} column to the CallLog database.
+ */
+ private void upgradeToVersion5(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE calls ADD transcription_state INTEGER NOT NULL DEFAULT 0");
+ }
+
+ /**
* Perform the migration from the contacts2.db (of the latest version) to the current calllog/
* voicemail status tables.
*/
@@ -355,6 +369,30 @@ public class CallLogDatabaseHelper {
return ContactsDatabaseHelper.getInstance(mContext).getWritableDatabase();
}
+ public ArraySet<String> selectDistinctColumn(String table, String column) {
+ final ArraySet<String> ret = new ArraySet<>();
+ final SQLiteDatabase db = getReadableDatabase();
+ final Cursor c = db.rawQuery("SELECT DISTINCT "
+ + column
+ + " FROM " + table, null);
+ try {
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ if (c.isNull(0)) {
+ continue;
+ }
+ final String s = c.getString(0);
+
+ if (!TextUtils.isEmpty(s)) {
+ ret.add(s);
+ }
+ }
+ return ret;
+ } finally {
+ c.close();
+ }
+ }
+
@VisibleForTesting
void closeForTest() {
mOpenHelper.close();
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index 9a5b7c48..9b53c0ef 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -33,10 +33,6 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Message;
-import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.CallLog;
@@ -46,11 +42,13 @@ import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.Log;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.providers.contacts.CallLogDatabaseHelper.DbProperties;
import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
import com.android.providers.contacts.util.SelectionBuilder;
import com.android.providers.contacts.util.UserUtils;
+
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -60,9 +58,9 @@ import java.util.concurrent.CountDownLatch;
* Call log content provider.
*/
public class CallLogProvider extends ContentProvider {
- private static final String TAG = CallLogProvider.class.getSimpleName();
+ private static final String TAG = "CallLogProvider";
- public static final boolean VERBOSE_LOGGING = false; // DO NOT SUBMIT WITH TRUE
+ public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
private static final int BACKGROUND_TASK_INITIALIZE = 0;
private static final int BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT = 1;
@@ -137,6 +135,7 @@ public class CallLogProvider extends ContentProvider {
sCallsProjectionMap.put(Calls.NEW, Calls.NEW);
sCallsProjectionMap.put(Calls.VOICEMAIL_URI, Calls.VOICEMAIL_URI);
sCallsProjectionMap.put(Calls.TRANSCRIPTION, Calls.TRANSCRIPTION);
+ sCallsProjectionMap.put(Calls.TRANSCRIPTION_STATE, Calls.TRANSCRIPTION_STATE);
sCallsProjectionMap.put(Calls.IS_READ, Calls.IS_READ);
sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
@@ -166,8 +165,8 @@ public class CallLogProvider extends ContentProvider {
private static Long sTimeForTestMillis;
- private HandlerThread mBackgroundThread;
- private Handler mBackgroundHandler;
+ private ContactsTaskScheduler mTaskScheduler;
+
private volatile CountDownLatch mReadAccessLatch;
private CallLogDatabaseHelper mDbHelper;
@@ -186,6 +185,11 @@ public class CallLogProvider extends ContentProvider {
@Override
public boolean onCreate() {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "onCreate: " + this.getClass().getSimpleName()
+ + " user=" + android.os.Process.myUserHandle().getIdentifier());
+ }
+
setAppOps(AppOpsManager.OP_READ_CALL_LOG, AppOpsManager.OP_WRITE_CALL_LOG);
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate start");
@@ -198,19 +202,16 @@ public class CallLogProvider extends ContentProvider {
mVoicemailPermissions = new VoicemailPermissions(context);
mCallLogInsertionHelper = createCallLogInsertionHelper(context);
- mBackgroundThread = new HandlerThread(getProviderName() + "Worker",
- Process.THREAD_PRIORITY_BACKGROUND);
- mBackgroundThread.start();
- mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
+ mReadAccessLatch = new CountDownLatch(1);
+
+ mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
@Override
- public void handleMessage(Message msg) {
- performBackgroundTask(msg.what, msg.obj);
+ public void onPerformTask(int taskId, Object arg) {
+ performBackgroundTask(taskId, arg);
}
};
- mReadAccessLatch = new CountDownLatch(1);
-
- scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE, null);
+ mTaskScheduler.scheduleTask(BACKGROUND_TASK_INITIALIZE, null);
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, getProviderName() + ".onCreate finish");
@@ -452,7 +453,7 @@ public class CallLogProvider extends ContentProvider {
}
void adjustForNewPhoneAccount(PhoneAccountHandle handle) {
- scheduleBackgroundTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle);
+ mTaskScheduler.scheduleTask(BACKGROUND_TASK_ADJUST_PHONE_ACCOUNT, handle);
}
/**
@@ -735,10 +736,6 @@ public class CallLogProvider extends ContentProvider {
}
}
- private void scheduleBackgroundTask(int task, Object arg) {
- mBackgroundHandler.obtainMessage(task, arg).sendToTarget();
- }
-
private void performBackgroundTask(int task, Object arg) {
if (task == BACKGROUND_TASK_INITIALIZE) {
try {
@@ -751,4 +748,9 @@ public class CallLogProvider extends ContentProvider {
adjustForNewPhoneAccountInternal((PhoneAccountHandle) arg);
}
}
+
+ @Override
+ public void shutdown() {
+ mTaskScheduler.shutdownForTest();
+ }
}
diff --git a/src/com/android/providers/contacts/ContactDirectoryManager.java b/src/com/android/providers/contacts/ContactDirectoryManager.java
index 385fa3ff..33e541d3 100644
--- a/src/com/android/providers/contacts/ContactDirectoryManager.java
+++ b/src/com/android/providers/contacts/ContactDirectoryManager.java
@@ -16,6 +16,7 @@
package com.android.providers.contacts;
+import android.annotation.NonNull;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageInfo;
@@ -29,6 +30,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Directory;
import android.text.TextUtils;
@@ -43,6 +45,7 @@ import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
/**
@@ -51,7 +54,7 @@ import java.util.Set;
public class ContactDirectoryManager {
private static final String TAG = "ContactDirectoryManager";
- private static final boolean DEBUG = false; // DON'T SUBMIT WITH TRUE
+ private static final boolean DEBUG = AbstractContactsProvider.VERBOSE_LOGGING;
public static final String CONTACT_DIRECTORY_META_DATA = "android.content.ContactDirectory";
@@ -101,6 +104,8 @@ public class ContactDirectoryManager {
private final Context mContext;
private final PackageManager mPackageManager;
+ private volatile boolean mDirectoriesForceUpdated = false;
+
public ContactDirectoryManager(ContactsProvider2 contactsProvider) {
mContactsProvider = contactsProvider;
mContext = contactsProvider.getContext();
@@ -111,6 +116,10 @@ public class ContactDirectoryManager {
return (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper();
}
+ public void setDirectoriesForceUpdated(boolean updated) {
+ mDirectoriesForceUpdated = updated;
+ }
+
/**
* Scans through existing directories to see if the cached resource IDs still
* match their original resource names. If not - plays it safe by refreshing all directories.
@@ -120,9 +129,11 @@ public class ContactDirectoryManager {
private boolean areTypeResourceIdsValid() {
SQLiteDatabase db = getDbHelper().getReadableDatabase();
- Cursor cursor = db.query(Tables.DIRECTORIES,
- new String[] { Directory.TYPE_RESOURCE_ID, Directory.PACKAGE_NAME,
- DirectoryColumns.TYPE_RESOURCE_NAME }, null, null, null, null, null);
+ final Cursor cursor = db.rawQuery("SELECT DISTINCT "
+ + Directory.TYPE_RESOURCE_ID + ","
+ + Directory.PACKAGE_NAME + ","
+ + DirectoryColumns.TYPE_RESOURCE_NAME
+ + " FROM " + Tables.DIRECTORIES, null);
try {
while (cursor.moveToNext()) {
int resourceId = cursor.getInt(0);
@@ -131,6 +142,13 @@ public class ContactDirectoryManager {
String storedResourceName = cursor.getString(2);
String resourceName = getResourceNameById(packageName, resourceId);
if (!TextUtils.equals(storedResourceName, resourceName)) {
+ if (DEBUG) {
+ Log.d(TAG, "areTypeResourceIdsValid:"
+ + " resourceId=" + resourceId
+ + " packageName=" + packageName
+ + " storedResourceName=" + storedResourceName
+ + " resourceName=" + resourceName);
+ }
return false;
}
}
@@ -157,25 +175,71 @@ public class ContactDirectoryManager {
}
}
+ private void saveKnownDirectoryProviders(Set<String> packages) {
+ getDbHelper().setProperty(DbProperties.KNOWN_DIRECTORY_PACKAGES,
+ TextUtils.join(",", packages));
+ }
+
+ private boolean haveKnownDirectoryProvidersChanged(Set<String> packages) {
+ final String directoryPackages = TextUtils.join(",", packages);
+ final String prev = getDbHelper().getProperty(DbProperties.KNOWN_DIRECTORY_PACKAGES, "");
+
+ final boolean changed = !Objects.equals(directoryPackages, prev);
+ if (DEBUG) {
+ Log.d(TAG, "haveKnownDirectoryProvidersChanged=" + changed + "\nprev=" + prev
+ + " current=" + directoryPackages);
+ }
+ return changed;
+ }
+
+ @VisibleForTesting
+ boolean isRescanNeeded() {
+ if ("1".equals(SystemProperties.get("debug.cp2.scan_all_packages", "0"))) {
+ Log.w(TAG, "debug.cp2.scan_all_packages set to 1.");
+ return true; // For debugging.
+ }
+ final String scanComplete =
+ getDbHelper().getProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
+ if (!"1".equals(scanComplete)) {
+ if (DEBUG) {
+ Log.d(TAG, "DIRECTORY_SCAN_COMPLETE is 0.");
+ }
+ return true;
+ }
+ if (haveKnownDirectoryProvidersChanged(getDirectoryProviderPackages(mPackageManager))) {
+ Log.i(TAG, "Directory provider packages have changed.");
+ return true;
+ }
+ return false;
+ }
+
/**
* Scans all packages for directory content providers.
*/
- public void scanAllPackages(boolean rescan) {
- if (rescan || !areTypeResourceIdsValid()) {
- getDbHelper().clearDirectoryScanComplete();
+ public int scanAllPackages(boolean rescan) {
+ if (!areTypeResourceIdsValid()) {
+ rescan = true;
+ Log.i(TAG, "!areTypeResourceIdsValid.");
+ }
+ if (rescan) {
+ getDbHelper().forceDirectoryRescan();
}
- scanAllPackagesIfNeeded();
+ return scanAllPackagesIfNeeded();
}
- private void scanAllPackagesIfNeeded() {
- String scanComplete = getDbHelper().getProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
- if (!"0".equals(scanComplete)) {
- return;
+ private int scanAllPackagesIfNeeded() {
+ if (!isRescanNeeded()) {
+ return 0;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "scanAllPackagesIfNeeded()");
}
-
final long start = SystemClock.elapsedRealtime();
- int count = scanAllPackages();
+ // Reset directory updated flag to false. If it's changed to true
+ // then we need to rescan directories.
+ mDirectoriesForceUpdated = false;
+ final int count = scanAllPackages();
getDbHelper().setProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "1");
final long end = SystemClock.elapsedRealtime();
Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms");
@@ -183,6 +247,14 @@ public class ContactDirectoryManager {
// Announce the change to listeners of the contacts authority
mContactsProvider.notifyChange(/* syncToNetwork =*/false,
/* syncToMetadataNetwork =*/false);
+
+ // We schedule a rescan if update(DIRECTORIES) is called while we're scanning all packages.
+ if (mDirectoriesForceUpdated) {
+ mDirectoriesForceUpdated = false;
+ mContactsProvider.scheduleRescanDirectories();
+ }
+
+ return count;
}
@VisibleForTesting
@@ -195,34 +267,25 @@ public class ContactDirectoryManager {
return trueFalse != null && Boolean.TRUE.equals(trueFalse);
}
+ @NonNull
+ static private List<ProviderInfo> getDirectoryProviderInfos(PackageManager pm) {
+ return pm.queryContentProviders(null, 0, 0, CONTACT_DIRECTORY_META_DATA);
+ }
+
/**
* @return List of packages that contain a directory provider.
*/
@VisibleForTesting
+ @NonNull
static Set<String> getDirectoryProviderPackages(PackageManager pm) {
final Set<String> ret = Sets.newHashSet();
- final List<PackageInfo> packages = pm.getInstalledPackages(PackageManager.GET_PROVIDERS
- | PackageManager.GET_META_DATA);
- if (packages == null) {
- return ret;
+ if (DEBUG) {
+ Log.d(TAG, "Listing directory provider packages...");
}
- for (PackageInfo packageInfo : packages) {
- if (DEBUG) {
- Log.d(TAG, "package=" + packageInfo.packageName);
- }
- if (packageInfo.providers == null) {
- continue;
- }
- for (ProviderInfo provider : packageInfo.providers) {
- if (DEBUG) {
- Log.d(TAG, "provider=" + provider.authority);
- }
- if (isDirectoryProvider(provider)) {
- Log.d(TAG, "Found " + provider.authority);
- ret.add(provider.packageName);
- }
- }
+
+ for (ProviderInfo provider : getDirectoryProviderInfos(pm)) {
+ ret.add(provider.packageName);
}
if (DEBUG) {
Log.d(TAG, "Found " + ret.size() + " directory provider packages");
@@ -231,8 +294,7 @@ public class ContactDirectoryManager {
return ret;
}
- @VisibleForTesting
- int scanAllPackages() {
+ private int scanAllPackages() {
SQLiteDatabase db = getDbHelper().getWritableDatabase();
insertDefaultDirectory(db);
insertLocalInvisibleDirectory(db);
@@ -251,7 +313,8 @@ public class ContactDirectoryManager {
+ Directory.ACCOUNT_NAME + "=? AND "
+ Directory.ACCOUNT_TYPE + "=?)";
- for (String packageName : getDirectoryProviderPackages(mPackageManager)) {
+ final Set<String> directoryProviderPackages = getDirectoryProviderPackages(mPackageManager);
+ for (String packageName : directoryProviderPackages) {
if (DEBUG) Log.d(TAG, "package=" + packageName);
// getDirectoryProviderPackages() shouldn't return the contacts provider package
@@ -291,6 +354,9 @@ public class ContactDirectoryManager {
int deletedRows = db.delete(Tables.DIRECTORIES, deleteWhereBuilder.toString(),
deleteWhereArgs.toArray(new String[0]));
+
+ saveKnownDirectoryProviders(directoryProviderPackages);
+
Log.i(TAG, "deleted " + deletedRows
+ " stale rows which don't have any relevant directory");
return count;
@@ -299,7 +365,7 @@ public class ContactDirectoryManager {
private void insertDefaultDirectory(SQLiteDatabase db) {
ContentValues values = new ContentValues();
values.put(Directory._ID, Directory.DEFAULT);
- values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName);
+ values.put(Directory.PACKAGE_NAME, mContext.getPackageName());
values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
values.put(Directory.TYPE_RESOURCE_ID, R.string.default_directory);
values.put(DirectoryColumns.TYPE_RESOURCE_NAME,
@@ -313,7 +379,7 @@ public class ContactDirectoryManager {
private void insertLocalInvisibleDirectory(SQLiteDatabase db) {
ContentValues values = new ContentValues();
values.put(Directory._ID, Directory.LOCAL_INVISIBLE);
- values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName);
+ values.put(Directory.PACKAGE_NAME, mContext.getPackageName());
values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
values.put(Directory.TYPE_RESOURCE_ID, R.string.local_invisible_directory);
values.put(DirectoryColumns.TYPE_RESOURCE_NAME,
diff --git a/src/com/android/providers/contacts/ContactMetadataProvider.java b/src/com/android/providers/contacts/ContactMetadataProvider.java
index 4396ea66..3cf7df2c 100644
--- a/src/com/android/providers/contacts/ContactMetadataProvider.java
+++ b/src/com/android/providers/contacts/ContactMetadataProvider.java
@@ -181,7 +181,7 @@ public class ContactMetadataProvider extends ContentProvider {
ensureCaller();
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
- db.beginTransaction();
+ db.beginTransactionNonExclusive();
try {
final int matchedUriId = sURIMatcher.match(uri);
switch (matchedUriId) {
@@ -212,7 +212,7 @@ public class ContactMetadataProvider extends ContentProvider {
ensureCaller();
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
- db.beginTransaction();
+ db.beginTransactionNonExclusive();
try {
final int matchedUriId = sURIMatcher.match(uri);
int numDeletes = 0;
@@ -260,7 +260,7 @@ public class ContactMetadataProvider extends ContentProvider {
ensureCaller();
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
- db.beginTransaction();
+ db.beginTransactionNonExclusive();
try {
final int matchedUriId = sURIMatcher.match(uri);
switch (matchedUriId) {
@@ -296,7 +296,7 @@ public class ContactMetadataProvider extends ContentProvider {
Log.v(TAG, "applyBatch: " + operations.size() + " ops");
}
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
- db.beginTransaction();
+ db.beginTransactionNonExclusive();
try {
ContentProviderResult[] results = super.applyBatch(operations);
db.setTransactionSuccessful();
@@ -315,7 +315,7 @@ public class ContactMetadataProvider extends ContentProvider {
Log.v(TAG, "bulkInsert: " + values.length + " inserts");
}
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
- db.beginTransaction();
+ db.beginTransactionNonExclusive();
try {
final int numValues = super.bulkInsert(uri, values);
db.setTransactionSuccessful();
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 00f08b1c..89cc3964 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -16,9 +16,10 @@
package com.android.providers.contacts;
+import com.android.providers.contacts.sqlite.DatabaseAnalyzer;
+import com.android.providers.contacts.sqlite.SqlChecker;
+import com.android.providers.contacts.sqlite.SqlChecker.InvalidSqlException;
import com.android.providers.contacts.util.PropertyUtils;
-import com.google.android.collect.Sets;
-import com.google.common.annotations.VisibleForTesting;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -49,14 +50,19 @@ import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Contacts.Photo;
import android.provider.ContactsContract.Data;
@@ -70,6 +76,7 @@ import android.provider.ContactsContract.MetadataSyncState;
import android.provider.ContactsContract.PhoneticNameStyle;
import android.provider.ContactsContract.PhotoFiles;
import android.provider.ContactsContract.PinnedPositions;
+import android.provider.ContactsContract.ProviderStatus;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.Settings;
import android.provider.ContactsContract.StatusUpdates;
@@ -81,10 +88,14 @@ import android.telephony.SubscriptionManager;
import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Base64;
import android.util.Log;
+import android.util.Slog;
import com.android.common.content.SyncStateContentProviderHelper;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
import com.android.providers.contacts.database.ContactsTableUtil;
import com.android.providers.contacts.database.DeletedContactsTableUtil;
@@ -95,9 +106,9 @@ import libcore.icu.ICU;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
import java.util.Locale;
import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
/**
* Database helper for contacts. Designed as a singleton to make sure that all
@@ -123,9 +134,14 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
* 900-999 Lollipop
* 1000-1099 M
* 1100-1199 N
+ * 1200-1299 O
* </pre>
*/
- static final int DATABASE_VERSION = 1111;
+ static final int DATABASE_VERSION = 1202;
+ private static final int MINIMUM_SUPPORTED_VERSION = 700;
+
+ @VisibleForTesting
+ static final boolean DISALLOW_SUB_QUERIES = false;
public interface Tables {
public static final String CONTACTS = "contacts";
@@ -313,7 +329,12 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
public static final String ENTITIES = "view_entities";
public static final String RAW_ENTITIES = "view_raw_entities";
public static final String GROUPS = "view_groups";
+
+ /** The data_usage_stat table joined with other tables. */
public static final String DATA_USAGE_STAT = "view_data_usage_stat";
+
+ /** The data_usage_stat table with the low-res columns. */
+ public static final String DATA_USAGE_LR = "view_data_usage";
public static final String STREAM_ITEMS = "view_stream_items";
public static final String METADATA_SYNC = "view_metadata_sync";
public static final String METADATA_SYNC_STATE = "view_metadata_sync_state";
@@ -335,6 +356,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
String ICU_VERSION = "icu_version";
String LOCALE = "locale";
String DATABASE_TIME_CREATED = "database_time_created";
+ String KNOWN_DIRECTORY_PACKAGES = "knownDirectoryPackages";
}
public interface Clauses {
@@ -396,10 +418,12 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
public static final String CONCRETE_PHOTO_FILE_ID = Tables.CONTACTS + "."
+ Contacts.PHOTO_FILE_ID;
- public static final String CONCRETE_TIMES_CONTACTED = Tables.CONTACTS + "."
- + Contacts.TIMES_CONTACTED;
- public static final String CONCRETE_LAST_TIME_CONTACTED = Tables.CONTACTS + "."
- + Contacts.LAST_TIME_CONTACTED;
+
+ public static final String CONCRETE_RAW_TIMES_CONTACTED = Tables.CONTACTS + "."
+ + Contacts.RAW_TIMES_CONTACTED;
+ public static final String CONCRETE_RAW_LAST_TIME_CONTACTED = Tables.CONTACTS + "."
+ + Contacts.RAW_LAST_TIME_CONTACTED;
+
public static final String CONCRETE_STARRED = Tables.CONTACTS + "." + Contacts.STARRED;
public static final String CONCRETE_PINNED = Tables.CONTACTS + "." + Contacts.PINNED;
public static final String CONCRETE_CUSTOM_RINGTONE = Tables.CONTACTS + "."
@@ -444,10 +468,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
Tables.RAW_CONTACTS + "." + RawContacts.CUSTOM_RINGTONE;
public static final String CONCRETE_SEND_TO_VOICEMAIL =
Tables.RAW_CONTACTS + "." + RawContacts.SEND_TO_VOICEMAIL;
- public static final String CONCRETE_LAST_TIME_CONTACTED =
- Tables.RAW_CONTACTS + "." + RawContacts.LAST_TIME_CONTACTED;
- public static final String CONCRETE_TIMES_CONTACTED =
- Tables.RAW_CONTACTS + "." + RawContacts.TIMES_CONTACTED;
+ public static final String CONCRETE_RAW_LAST_TIME_CONTACTED =
+ Tables.RAW_CONTACTS + "." + RawContacts.RAW_LAST_TIME_CONTACTED;
+ public static final String CONCRETE_RAW_TIMES_CONTACTED =
+ Tables.RAW_CONTACTS + "." + RawContacts.RAW_TIMES_CONTACTED;
public static final String CONCRETE_STARRED =
Tables.RAW_CONTACTS + "." + RawContacts.STARRED;
public static final String CONCRETE_PINNED =
@@ -714,14 +738,24 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
public static final String CONCRETE_DATA_ID = Tables.DATA_USAGE_STAT + "." + DATA_ID;
/** type: INTEGER (long) */
- public static final String LAST_TIME_USED = "last_time_used";
- public static final String CONCRETE_LAST_TIME_USED =
- Tables.DATA_USAGE_STAT + "." + LAST_TIME_USED;
+ public static final String RAW_LAST_TIME_USED = Data.RAW_LAST_TIME_USED;
+ public static final String LR_LAST_TIME_USED = Data.LR_LAST_TIME_USED;
/** type: INTEGER */
- public static final String TIMES_USED = "times_used";
- public static final String CONCRETE_TIMES_USED =
- Tables.DATA_USAGE_STAT + "." + TIMES_USED;
+ public static final String RAW_TIMES_USED = Data.RAW_TIMES_USED;
+ public static final String LR_TIMES_USED = Data.LR_TIMES_USED;
+
+ public static final String CONCRETE_RAW_LAST_TIME_USED =
+ Tables.DATA_USAGE_STAT + "." + RAW_LAST_TIME_USED;
+
+ public static final String CONCRETE_RAW_TIMES_USED =
+ Tables.DATA_USAGE_STAT + "." + RAW_TIMES_USED;
+
+ public static final String CONCRETE_LR_LAST_TIME_USED =
+ Tables.DATA_USAGE_STAT + "." + LR_LAST_TIME_USED;
+
+ public static final String CONCRETE_LR_TIMES_USED =
+ Tables.DATA_USAGE_STAT + "." + LR_TIMES_USED;
/** type: INTEGER */
public static final String USAGE_TYPE_INT = "usage_type";
@@ -770,26 +804,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
public static final int ADDRESS = 2;
}
- private interface Upgrade303Query {
- public static final String TABLE = Tables.DATA;
-
- public static final String SELECTION =
- DataColumns.MIMETYPE_ID + "=?" +
- " AND " + Data._ID + " NOT IN " +
- "(SELECT " + NameLookupColumns.DATA_ID + " FROM " + Tables.NAME_LOOKUP + ")" +
- " AND " + Data.DATA1 + " NOT NULL";
-
- public static final String COLUMNS[] = {
- Data._ID,
- Data.RAW_CONTACT_ID,
- Data.DATA1,
- };
-
- public static final int ID = 0;
- public static final int RAW_CONTACT_ID = 1;
- public static final int DATA1 = 2;
- }
-
private interface StructuredNameQuery {
public static final String TABLE = Tables.DATA;
@@ -823,37 +837,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
public static final int NAME = 2;
}
- private interface StructName205Query {
- String TABLE = Tables.DATA_JOIN_RAW_CONTACTS;
- String COLUMNS[] = {
- DataColumns.CONCRETE_ID,
- Data.RAW_CONTACT_ID,
- RawContacts.DISPLAY_NAME_SOURCE,
- RawContacts.DISPLAY_NAME_PRIMARY,
- StructuredName.PREFIX,
- StructuredName.GIVEN_NAME,
- StructuredName.MIDDLE_NAME,
- StructuredName.FAMILY_NAME,
- StructuredName.SUFFIX,
- StructuredName.PHONETIC_FAMILY_NAME,
- StructuredName.PHONETIC_MIDDLE_NAME,
- StructuredName.PHONETIC_GIVEN_NAME,
- };
-
- int ID = 0;
- int RAW_CONTACT_ID = 1;
- int DISPLAY_NAME_SOURCE = 2;
- int DISPLAY_NAME = 3;
- int PREFIX = 4;
- int GIVEN_NAME = 5;
- int MIDDLE_NAME = 6;
- int FAMILY_NAME = 7;
- int SUFFIX = 8;
- int PHONETIC_FAMILY_NAME = 9;
- int PHONETIC_MIDDLE_NAME = 10;
- int PHONETIC_GIVEN_NAME = 11;
- }
-
private interface RawContactNameQuery {
public static final String RAW_SQL =
"SELECT "
@@ -896,21 +879,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
public static final int PHONETIC_NAME_STYLE = 12; // data11
}
- private interface Organization205Query {
- String TABLE = Tables.DATA_JOIN_RAW_CONTACTS;
- String COLUMNS[] = {
- DataColumns.CONCRETE_ID,
- Data.RAW_CONTACT_ID,
- Organization.COMPANY,
- Organization.PHONETIC_NAME,
- };
-
- int ID = 0;
- int RAW_CONTACT_ID = 1;
- int COMPANY = 2;
- int PHONETIC_NAME = 3;
- }
-
public final static class NameLookupType {
public static final int NAME_EXACT = 0;
public static final int NAME_VARIANT = 1;
@@ -957,62 +925,101 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
}
}
+ /** Placeholder for the methods to build the "low-res" SQL expressions. */
+ @VisibleForTesting
+ interface LowRes {
+ /** To be replaced with a real column name. Only used within this interface. */
+ String TEMPLATE_PLACEHOLDER = "XX";
+
+ /**
+ * To be replaced with a constant in the expression.
+ * Only used within this interface.
+ */
+ String CONSTANT_PLACEHOLDER = "YY";
+
+ /** Only used within this interface. */
+ int TIMES_USED_GRANULARITY = 10;
+
+ /** Only used within this interface. */
+ int LAST_TIME_USED_GRANULARITY = 24 * 60 * 60;
+
+ /**
+ * Template to build the "low-res times used/contacted". Only used within this interface.
+ * The outermost cast is needed to tell SQLite that the result is of the integer type.
+ */
+ String TEMPLATE_TIMES_USED =
+ ("cast(ifnull((case when (XX) <= 0 then 0"
+ + " when (XX) < (YY) then (XX)"
+ + " else (cast((XX) as int) / (YY)) * (YY) end), 0) as int)")
+ .replaceAll(CONSTANT_PLACEHOLDER, String.valueOf(TIMES_USED_GRANULARITY));
+
+ /**
+ * Template to build the "low-res last time used/contacted".
+ * Only used within this interface.
+ * The outermost cast is needed to tell SQLite that the result is of the integer type.
+ */
+ String TEMPLATE_LAST_TIME_USED =
+ ("cast((cast((XX) as int) / (YY)) * (YY) as int)")
+ .replaceAll(CONSTANT_PLACEHOLDER, String.valueOf(LAST_TIME_USED_GRANULARITY));
+
+ /**
+ * Build the SQL expression for the "low-res times used/contacted" expression from the
+ * give column name.
+ */
+ static String getTimesUsedExpression(String column) {
+ return TEMPLATE_TIMES_USED.replaceAll(TEMPLATE_PLACEHOLDER, column);
+ }
+
+ /**
+ * Build the SQL expression for the "low-res last time used/contacted" expression from the
+ * give column name.
+ */
+ static String getLastTimeUsedExpression(String column) {
+ return TEMPLATE_LAST_TIME_USED.replaceAll(TEMPLATE_PLACEHOLDER, column);
+ }
+ }
+
private static final String TAG = "ContactsDatabaseHelper";
private static final String DATABASE_NAME = "contacts2.db";
- private static final String DATABASE_PRESENCE = "presence_db";
private static ContactsDatabaseHelper sSingleton = null;
- /** In-memory cache of previously found MIME-type mappings */
+ /** In-memory map of commonly found MIME-types to their ids in the MIMETYPES table */
@VisibleForTesting
- final ConcurrentHashMap<String, Long> mMimetypeCache = new ConcurrentHashMap<>();
+ final ArrayMap<String, Long> mCommonMimeTypeIdsCache = new ArrayMap<>();
- /** In-memory cache the packages table */
@VisibleForTesting
- final ConcurrentHashMap<String, Long> mPackageCache = new ConcurrentHashMap<>();
+ static final String[] COMMON_MIME_TYPES = {
+ Email.CONTENT_ITEM_TYPE,
+ Im.CONTENT_ITEM_TYPE,
+ Nickname.CONTENT_ITEM_TYPE,
+ Organization.CONTENT_ITEM_TYPE,
+ Phone.CONTENT_ITEM_TYPE,
+ SipAddress.CONTENT_ITEM_TYPE,
+ StructuredName.CONTENT_ITEM_TYPE,
+ StructuredPostal.CONTENT_ITEM_TYPE,
+ Identity.CONTENT_ITEM_TYPE,
+ android.provider.ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE,
+ GroupMembership.CONTENT_ITEM_TYPE,
+ Note.CONTENT_ITEM_TYPE,
+ Event.CONTENT_ITEM_TYPE,
+ Website.CONTENT_ITEM_TYPE,
+ Relation.CONTENT_ITEM_TYPE,
+ "vnd.com.google.cursor.item/contact_misc"
+ };
private final Context mContext;
private final boolean mDatabaseOptimizationEnabled;
+ private final boolean mIsTestInstance;
private final SyncStateContentProviderHelper mSyncState;
private final CountryMonitor mCountryMonitor;
- private long mMimeTypeIdEmail;
- private long mMimeTypeIdIm;
- private long mMimeTypeIdNickname;
- private long mMimeTypeIdOrganization;
- private long mMimeTypeIdPhone;
- private long mMimeTypeIdSip;
- private long mMimeTypeIdStructuredName;
- private long mMimeTypeIdStructuredPostal;
-
- /** Compiled statements for querying and inserting mappings */
- private SQLiteStatement mContactIdQuery;
- private SQLiteStatement mAggregationModeQuery;
- private SQLiteStatement mDataMimetypeQuery;
-
- /** Precompiled SQL statement for setting a data record to the primary. */
- private SQLiteStatement mSetPrimaryStatement;
- /** Precompiled SQL statement for setting a data record to the super primary. */
- private SQLiteStatement mSetSuperPrimaryStatement;
- /** Precompiled SQL statement for clearing super primary of a single record. */
- private SQLiteStatement mClearSuperPrimaryStatement;
- /** Precompiled SQL statement for updating a contact display name */
- private SQLiteStatement mRawContactDisplayNameUpdate;
-
- private SQLiteStatement mNameLookupInsert;
- private SQLiteStatement mNameLookupDelete;
- private SQLiteStatement mStatusUpdateAutoTimestamp;
- private SQLiteStatement mStatusUpdateInsert;
- private SQLiteStatement mStatusUpdateReplace;
- private SQLiteStatement mStatusAttributionUpdate;
- private SQLiteStatement mStatusUpdateDelete;
- private SQLiteStatement mResetNameVerifiedForOtherRawContacts;
- private SQLiteStatement mContactInDefaultDirectoryQuery;
- private SQLiteStatement mMetadataSyncInsert;
- private SQLiteStatement mMetadataSyncUpdate;
-
- private StringBuilder mSb = new StringBuilder();
+ /**
+ * Time when the DB was created. It's persisted in {@link DbProperties#DATABASE_TIME_CREATED},
+ * but loaded into memory so it can be accessed even when the DB is busy.
+ */
+ private long mDatabaseCreationTime;
private MessageDigest mMessageDigest;
{
@@ -1032,7 +1039,8 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
public static synchronized ContactsDatabaseHelper getInstance(Context context) {
if (sSingleton == null) {
- sSingleton = new ContactsDatabaseHelper(context, DATABASE_NAME, true);
+ sSingleton = new ContactsDatabaseHelper(context, DATABASE_NAME, true,
+ /* isTestInstance=*/ false);
}
return sSingleton;
}
@@ -1041,16 +1049,20 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
* Returns a new instance for unit tests.
*/
@NeededForTesting
- static ContactsDatabaseHelper getNewInstanceForTest(Context context) {
- return new ContactsDatabaseHelper(context, null, false);
+ public static ContactsDatabaseHelper getNewInstanceForTest(Context context, String filename) {
+ return new ContactsDatabaseHelper(context, filename, false, /* isTestInstance=*/ true);
}
protected ContactsDatabaseHelper(
- Context context, String databaseName, boolean optimizationEnabled) {
- super(context, databaseName, null, DATABASE_VERSION);
+ Context context, String databaseName, boolean optimizationEnabled,
+ boolean isTestInstance) {
+ super(context, databaseName, null, DATABASE_VERSION, MINIMUM_SUPPORTED_VERSION, null);
+ boolean enableWal = android.provider.Settings.Global.getInt(context.getContentResolver(),
+ android.provider.Settings.Global.CONTACTS_DATABASE_WAL_ENABLED, 1) == 1;
+ setWriteAheadLoggingEnabled(enableWal);
mDatabaseOptimizationEnabled = optimizationEnabled;
+ mIsTestInstance = isTestInstance;
Resources resources = context.getResources();
-
mContext = context;
mSyncState = new SyncStateContentProviderHelper();
mCountryMonitor = new CountryMonitor(context);
@@ -1063,61 +1075,70 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
}
/**
- * Clear all the cached database information and re-initialize it.
+ * Populate ids of known mimetypes into a map for easy access
*
* @param db target database
*/
- private void refreshDatabaseCaches(SQLiteDatabase db) {
- mStatusUpdateDelete = null;
- mStatusUpdateReplace = null;
- mStatusUpdateInsert = null;
- mStatusUpdateAutoTimestamp = null;
- mStatusAttributionUpdate = null;
- mResetNameVerifiedForOtherRawContacts = null;
- mRawContactDisplayNameUpdate = null;
- mSetPrimaryStatement = null;
- mClearSuperPrimaryStatement = null;
- mSetSuperPrimaryStatement = null;
- mNameLookupInsert = null;
- mNameLookupDelete = null;
- mDataMimetypeQuery = null;
- mContactIdQuery = null;
- mAggregationModeQuery = null;
- mContactInDefaultDirectoryQuery = null;
-
- initializeCache(db);
+ private void prepopulateCommonMimeTypes(SQLiteDatabase db) {
+ mCommonMimeTypeIdsCache.clear();
+ for(String commonMimeType: COMMON_MIME_TYPES) {
+ mCommonMimeTypeIdsCache.put(commonMimeType, insertMimeType(db, commonMimeType));
+ }
}
- /**
- * (Re-)initialize the cached database information.
- *
- * @param db target database
- */
- private void initializeCache(SQLiteDatabase db) {
- mMimetypeCache.clear();
- mPackageCache.clear();
-
- // TODO: This could be optimized into one query instead of 7
- // Also: We shouldn't have those fields in the first place. This should just be
- // in the cache
- mMimeTypeIdEmail = lookupMimeTypeId(Email.CONTENT_ITEM_TYPE, db);
- mMimeTypeIdIm = lookupMimeTypeId(Im.CONTENT_ITEM_TYPE, db);
- mMimeTypeIdNickname = lookupMimeTypeId(Nickname.CONTENT_ITEM_TYPE, db);
- mMimeTypeIdOrganization = lookupMimeTypeId(Organization.CONTENT_ITEM_TYPE, db);
- mMimeTypeIdPhone = lookupMimeTypeId(Phone.CONTENT_ITEM_TYPE, db);
- mMimeTypeIdSip = lookupMimeTypeId(SipAddress.CONTENT_ITEM_TYPE, db);
- mMimeTypeIdStructuredName = lookupMimeTypeId(StructuredName.CONTENT_ITEM_TYPE, db);
- mMimeTypeIdStructuredPostal = lookupMimeTypeId(StructuredPostal.CONTENT_ITEM_TYPE, db);
+ @Override
+ public void onBeforeDelete(SQLiteDatabase db) {
+ Log.w(TAG, "Database version " + db.getVersion() + " for " + DATABASE_NAME
+ + " is no longer supported. Data will be lost on upgrading to " + DATABASE_VERSION);
}
@Override
public void onOpen(SQLiteDatabase db) {
- refreshDatabaseCaches(db);
-
+ Log.d(TAG, "WAL enabled for " + getDatabaseName() + ": " + db.isWriteAheadLoggingEnabled());
+ prepopulateCommonMimeTypes(db);
mSyncState.onDatabaseOpened(db);
+ // Deleting any state from the presence tables to mimic their behavior from the time they
+ // were in-memory tables
+ db.execSQL("DELETE FROM " + Tables.PRESENCE + ";");
+ db.execSQL("DELETE FROM " + Tables.AGGREGATED_PRESENCE + ";");
+
+ loadDatabaseCreationTime(db);
+ }
+
+ protected void setDatabaseCreationTime(SQLiteDatabase db) {
+ // Note we don't do this in the profile DB helper.
+ mDatabaseCreationTime = System.currentTimeMillis();
+ PropertyUtils.setProperty(db, DbProperties.DATABASE_TIME_CREATED, String.valueOf(
+ mDatabaseCreationTime));
+ }
+
+ protected void loadDatabaseCreationTime(SQLiteDatabase db) {
+ // Note we don't do this in the profile DB helper.
+
+ mDatabaseCreationTime = 0;
+ final String timestamp = PropertyUtils.getProperty(db,
+ DbProperties.DATABASE_TIME_CREATED, "");
+ if (!TextUtils.isEmpty(timestamp)) {
+ try {
+ mDatabaseCreationTime = Long.parseLong(timestamp);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Failed to parse timestamp: " + timestamp);
+ }
+ }
+ if (AbstractContactsProvider.VERBOSE_LOGGING) {
+ Log.v(TAG, "Open: creation time=" + mDatabaseCreationTime);
+ }
+ if (mDatabaseCreationTime == 0) {
+ Log.w(TAG, "Unable to load creating time; resetting.");
+ // Hmm, failed to load the timestamp. Just set the current time then.
+ mDatabaseCreationTime = System.currentTimeMillis();
+ PropertyUtils.setProperty(db,
+ DbProperties.DATABASE_TIME_CREATED, Long.toString(mDatabaseCreationTime));
+ }
+ }
- db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
- db.execSQL("CREATE TABLE IF NOT EXISTS " + DATABASE_PRESENCE + "." + Tables.PRESENCE + " ("+
+ private void createPresenceTables(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.PRESENCE + " ("+
StatusUpdates.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
StatusUpdates.PROTOCOL + " INTEGER NOT NULL," +
StatusUpdates.CUSTOM_PROTOCOL + " TEXT," +
@@ -1131,21 +1152,21 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
+ ", " + StatusUpdates.IM_HANDLE + ", " + StatusUpdates.IM_ACCOUNT + ")" +
");");
- db.execSQL("CREATE INDEX IF NOT EXISTS " + DATABASE_PRESENCE + ".presenceIndex" + " ON "
+ db.execSQL("CREATE INDEX IF NOT EXISTS presenceIndex" + " ON "
+ Tables.PRESENCE + " (" + PresenceColumns.RAW_CONTACT_ID + ");");
- db.execSQL("CREATE INDEX IF NOT EXISTS " + DATABASE_PRESENCE + ".presenceIndex2" + " ON "
+ db.execSQL("CREATE INDEX IF NOT EXISTS presenceIndex2" + " ON "
+ Tables.PRESENCE + " (" + PresenceColumns.CONTACT_ID + ");");
db.execSQL("CREATE TABLE IF NOT EXISTS "
- + DATABASE_PRESENCE + "." + Tables.AGGREGATED_PRESENCE + " ("+
+ + Tables.AGGREGATED_PRESENCE + " ("+
AggregatedPresenceColumns.CONTACT_ID
+ " INTEGER PRIMARY KEY REFERENCES contacts(_id)," +
StatusUpdates.PRESENCE + " INTEGER," +
StatusUpdates.CHAT_CAPABILITY + " INTEGER NOT NULL DEFAULT 0" +
");");
- db.execSQL("CREATE TRIGGER " + DATABASE_PRESENCE + "." + Tables.PRESENCE + "_deleted"
- + " BEFORE DELETE ON " + DATABASE_PRESENCE + "." + Tables.PRESENCE
+ db.execSQL("CREATE TRIGGER IF NOT EXISTS " + Tables.PRESENCE + "_deleted"
+ + " BEFORE DELETE ON " + Tables.PRESENCE
+ " BEGIN "
+ " DELETE FROM " + Tables.AGGREGATED_PRESENCE
+ " WHERE " + AggregatedPresenceColumns.CONTACT_ID + " = " +
@@ -1184,14 +1205,14 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
+ ")"
+ " AND " + PresenceColumns.CONTACT_ID + "=NEW." + PresenceColumns.CONTACT_ID + ";";
- db.execSQL("CREATE TRIGGER " + DATABASE_PRESENCE + "." + Tables.PRESENCE + "_inserted"
- + " AFTER INSERT ON " + DATABASE_PRESENCE + "." + Tables.PRESENCE
+ db.execSQL("CREATE TRIGGER IF NOT EXISTS " + Tables.PRESENCE + "_inserted"
+ + " AFTER INSERT ON " + Tables.PRESENCE
+ " BEGIN "
+ replaceAggregatePresenceSql
+ " END");
- db.execSQL("CREATE TRIGGER " + DATABASE_PRESENCE + "." + Tables.PRESENCE + "_updated"
- + " AFTER UPDATE ON " + DATABASE_PRESENCE + "." + Tables.PRESENCE
+ db.execSQL("CREATE TRIGGER IF NOT EXISTS " + Tables.PRESENCE + "_updated"
+ + " AFTER UPDATE ON " + Tables.PRESENCE
+ " BEGIN "
+ replaceAggregatePresenceSql
+ " END");
@@ -1199,7 +1220,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase db) {
- Log.i(TAG, "Bootstrapping database version: " + DATABASE_VERSION);
+ Log.i(TAG, "Bootstrapping database " + DATABASE_NAME + " version: " + DATABASE_VERSION);
mSyncState.createDatabase(db);
@@ -1207,8 +1228,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
// The create time is needed by BOOT_COMPLETE to send broadcasts.
PropertyUtils.createPropertiesTable(db);
- PropertyUtils.setProperty(db, DbProperties.DATABASE_TIME_CREATED, String.valueOf(
- System.currentTimeMillis()));
+ setDatabaseCreationTime(db);
db.execSQL("CREATE TABLE " + Tables.ACCOUNTS + " (" +
AccountsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -1217,6 +1237,20 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
AccountsColumns.DATA_SET + " TEXT" +
");");
+ // Note, there are two sets of the usage stat columns: LR_* and RAW_*.
+ // RAW_* contain the real values, which clients can't access. The column names start
+ // with a special prefix, which clients are prohibited from using in queries (including
+ // "where" of deletes/updates.)
+ // The LR_* columns have the original, public names. The views have the LR columns too,
+ // which contain the "low-res" numbers. The tables, though, do *not* have to have these
+ // columns, because we won't use them anyway. However, because old versions of the tables
+ // had those columns, and SQLite doesn't allow removing existing columns, meaning upgraded
+ // tables will have these LR_* columns anyway. So, in order to make a new database look
+ // the same as an upgraded database, we create the LR columns in a new database too.
+ // Otherwise, we would easily end up with writing SQLs that will run fine in a new DB
+ // but not in an upgraded database, and because all unit tests will run with a new database,
+ // we can't easily catch these sort of issues.
+
// One row per group of contacts corresponding to the same person
db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" +
BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -1225,8 +1259,13 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
Contacts.PHOTO_FILE_ID + " INTEGER REFERENCES photo_files(_id)," +
Contacts.CUSTOM_RINGTONE + " TEXT," +
Contacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
- Contacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
- Contacts.LAST_TIME_CONTACTED + " INTEGER," +
+
+ Contacts.RAW_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
+ Contacts.RAW_LAST_TIME_CONTACTED + " INTEGER," +
+
+ Contacts.LR_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
+ Contacts.LR_LAST_TIME_CONTACTED + " INTEGER," +
+
Contacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
Contacts.PINNED + " INTEGER NOT NULL DEFAULT " + PinnedPositions.UNPINNED + "," +
Contacts.HAS_PHONE_NUMBER + " INTEGER NOT NULL DEFAULT 0," +
@@ -1258,8 +1297,13 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
RawContactsColumns.AGGREGATION_NEEDED + " INTEGER NOT NULL DEFAULT 1," +
RawContacts.CUSTOM_RINGTONE + " TEXT," +
RawContacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
- RawContacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
- RawContacts.LAST_TIME_CONTACTED + " INTEGER," +
+
+ RawContacts.RAW_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
+ RawContacts.RAW_LAST_TIME_CONTACTED + " INTEGER," +
+
+ RawContacts.LR_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
+ RawContacts.LR_LAST_TIME_CONTACTED + " INTEGER," +
+
RawContacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
RawContacts.PINNED + " INTEGER NOT NULL DEFAULT " + PinnedPositions.UNPINNED +
"," + RawContacts.DISPLAY_NAME_PRIMARY + " TEXT," +
@@ -1547,8 +1591,13 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
DataUsageStatColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
DataUsageStatColumns.DATA_ID + " INTEGER NOT NULL, " +
DataUsageStatColumns.USAGE_TYPE_INT + " INTEGER NOT NULL DEFAULT 0, " +
- DataUsageStatColumns.TIMES_USED + " INTEGER NOT NULL DEFAULT 0, " +
- DataUsageStatColumns.LAST_TIME_USED + " INTEGER NOT NULL DEFAULT 0, " +
+
+ DataUsageStatColumns.RAW_TIMES_USED + " INTEGER NOT NULL DEFAULT 0, " +
+ DataUsageStatColumns.RAW_LAST_TIME_USED + " INTEGER NOT NULL DEFAULT 0, " +
+
+ DataUsageStatColumns.LR_TIMES_USED + " INTEGER NOT NULL DEFAULT 0, " +
+ DataUsageStatColumns.LR_LAST_TIME_USED + " INTEGER NOT NULL DEFAULT 0, " +
+
"FOREIGN KEY(" + DataUsageStatColumns.DATA_ID + ") REFERENCES "
+ Tables.DATA + "(" + Data._ID + ")" +
");");
@@ -1591,6 +1640,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
createGroupsView(db);
createContactsTriggers(db);
createContactsIndexes(db, false /* we build stats table later */);
+ createPresenceTables(db);
loadNicknameLookupTable(db);
@@ -1607,16 +1657,23 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
updateSqliteStats(db);
}
+ postOnCreate();
+ }
+
+ protected void postOnCreate() {
+ // Only do this for the main DB, but not for the profile DB.
+
+ notifyProviderStatusChange(mContext);
+
+ // Trigger all sync adapters.
ContentResolver.requestSync(null /* all accounts */,
ContactsContract.AUTHORITY, new Bundle());
- // Only send broadcasts for regular contacts db.
- if (dbForProfile() == 0) {
- final Intent dbCreatedIntent = new Intent(
- ContactsContract.Intents.CONTACTS_DATABASE_CREATED);
- dbCreatedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- mContext.sendBroadcast(dbCreatedIntent, android.Manifest.permission.READ_CONTACTS);
- }
+ // Send the broadcast.
+ final Intent dbCreatedIntent = new Intent(
+ ContactsContract.Intents.CONTACTS_DATABASE_CREATED);
+ dbCreatedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ mContext.sendBroadcast(dbCreatedIntent, android.Manifest.permission.READ_CONTACTS);
}
protected void initializeAutoIncrementSequences(SQLiteDatabase db) {
@@ -1646,7 +1703,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
}
public void createSearchIndexTable(SQLiteDatabase db, boolean rebuildSqliteStats) {
- db.beginTransaction();
+ db.beginTransactionNonExclusive();
try {
db.execSQL("DROP TABLE IF EXISTS " + Tables.SEARCH_INDEX);
db.execSQL("CREATE VIRTUAL TABLE " + Tables.SEARCH_INDEX
@@ -1829,7 +1886,9 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_ENTITIES + ";");
db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES + ";");
db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_STAT + ";");
+ db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_LR + ";");
db.execSQL("DROP VIEW IF EXISTS " + Views.STREAM_ITEMS + ";");
+ db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC_STATE + ";");
db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC + ";");
String dataColumns =
@@ -1896,17 +1955,24 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
String contactOptionColumns =
ContactsColumns.CONCRETE_CUSTOM_RINGTONE
- + " AS " + RawContacts.CUSTOM_RINGTONE + ","
+ + " AS " + Contacts.CUSTOM_RINGTONE + ","
+ ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
- + " AS " + RawContacts.SEND_TO_VOICEMAIL + ","
- + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
- + " AS " + RawContacts.LAST_TIME_CONTACTED + ","
- + ContactsColumns.CONCRETE_TIMES_CONTACTED
- + " AS " + RawContacts.TIMES_CONTACTED + ","
+ + " AS " + Contacts.SEND_TO_VOICEMAIL + ","
+
+ + ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED
+ + " AS " + Contacts.RAW_LAST_TIME_CONTACTED + ","
+ + ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED
+ + " AS " + Contacts.RAW_TIMES_CONTACTED + ","
+
+ + LowRes.getLastTimeUsedExpression(ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED)
+ + " AS " + Contacts.LR_LAST_TIME_CONTACTED + ","
+ + LowRes.getTimesUsedExpression(ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED)
+ + " AS " + Contacts.LR_TIMES_CONTACTED + ","
+
+ ContactsColumns.CONCRETE_STARRED
- + " AS " + RawContacts.STARRED + ","
+ + " AS " + Contacts.STARRED + ","
+ ContactsColumns.CONCRETE_PINNED
- + " AS " + RawContacts.PINNED;
+ + " AS " + Contacts.PINNED;
String contactNameColumns =
"name_raw_contact." + RawContacts.DISPLAY_NAME_SOURCE
@@ -1972,8 +2038,12 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
String rawContactOptionColumns =
RawContacts.CUSTOM_RINGTONE + ","
+ RawContacts.SEND_TO_VOICEMAIL + ","
- + RawContacts.LAST_TIME_CONTACTED + ","
- + RawContacts.TIMES_CONTACTED + ","
+ + RawContacts.RAW_LAST_TIME_CONTACTED + ","
+ + LowRes.getLastTimeUsedExpression(RawContacts.RAW_LAST_TIME_CONTACTED)
+ + " AS " + RawContacts.LR_LAST_TIME_CONTACTED + ","
+ + RawContacts.RAW_TIMES_CONTACTED + ","
+ + LowRes.getTimesUsedExpression(RawContacts.RAW_TIMES_CONTACTED)
+ + " AS " + RawContacts.LR_TIMES_CONTACTED + ","
+ RawContacts.STARRED + ","
+ RawContacts.PINNED;
@@ -2010,16 +2080,23 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
+ " AS " + Contacts.CUSTOM_RINGTONE + ", "
+ contactNameColumns + ", "
+ baseContactColumns + ", "
- + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
- + " AS " + Contacts.LAST_TIME_CONTACTED + ", "
+
+ + ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED
+ + " AS " + Contacts.RAW_LAST_TIME_CONTACTED + ", "
+ + LowRes.getLastTimeUsedExpression(ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED)
+ + " AS " + Contacts.LR_LAST_TIME_CONTACTED + ", "
+
+ ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
+ " AS " + Contacts.SEND_TO_VOICEMAIL + ", "
+ ContactsColumns.CONCRETE_STARRED
+ " AS " + Contacts.STARRED + ", "
+ ContactsColumns.CONCRETE_PINNED
+ " AS " + Contacts.PINNED + ", "
- + ContactsColumns.CONCRETE_TIMES_CONTACTED
- + " AS " + Contacts.TIMES_CONTACTED;
+
+ + ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED
+ + " AS " + Contacts.RAW_TIMES_CONTACTED + ", "
+ + LowRes.getTimesUsedExpression(ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED)
+ + " AS " + Contacts.LR_TIMES_CONTACTED;
String contactsSelect = "SELECT "
+ ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
@@ -2109,15 +2186,34 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("CREATE VIEW " + Views.ENTITIES + " AS "
+ entitiesSelect);
+ // Data usage view, with the low res columns, with no joins.
+ final String dataUsageViewSelect = "SELECT "
+ + DataUsageStatColumns._ID + ", "
+ + DataUsageStatColumns.DATA_ID + ", "
+ + DataUsageStatColumns.USAGE_TYPE_INT + ", "
+ + DataUsageStatColumns.RAW_TIMES_USED + ", "
+ + DataUsageStatColumns.RAW_LAST_TIME_USED + ","
+ + LowRes.getTimesUsedExpression(DataUsageStatColumns.RAW_TIMES_USED)
+ + " AS " + DataUsageStatColumns.LR_TIMES_USED + ","
+ + LowRes.getLastTimeUsedExpression(DataUsageStatColumns.RAW_LAST_TIME_USED)
+ + " AS " + DataUsageStatColumns.LR_LAST_TIME_USED
+ + " FROM " + Tables.DATA_USAGE_STAT;
+
+ // When the data_usage_stat table is needed with the low-res columns, use this, which is
+ // faster than the DATA_USAGE_STAT view since it doesn't involve joins.
+ db.execSQL("CREATE VIEW " + Views.DATA_USAGE_LR + " AS " + dataUsageViewSelect);
+
String dataUsageStatSelect = "SELECT "
+ DataUsageStatColumns.CONCRETE_ID + " AS " + DataUsageStatColumns._ID + ", "
+ DataUsageStatColumns.DATA_ID + ", "
+ RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
+ MimetypesColumns.CONCRETE_MIMETYPE + " AS " + Data.MIMETYPE + ", "
+ DataUsageStatColumns.USAGE_TYPE_INT + ", "
- + DataUsageStatColumns.TIMES_USED + ", "
- + DataUsageStatColumns.LAST_TIME_USED
- + " FROM " + Tables.DATA_USAGE_STAT
+ + DataUsageStatColumns.RAW_TIMES_USED + ", "
+ + DataUsageStatColumns.RAW_LAST_TIME_USED + ", "
+ + DataUsageStatColumns.LR_TIMES_USED + ", "
+ + DataUsageStatColumns.LR_LAST_TIME_USED
+ + " FROM " + Views.DATA_USAGE_LR + " AS " + Tables.DATA_USAGE_STAT
+ " JOIN " + Tables.DATA + " ON ("
+ DataColumns.CONCRETE_ID + "=" + DataUsageStatColumns.CONCRETE_DATA_ID + ")"
+ " JOIN " + Tables.RAW_CONTACTS + " ON ("
@@ -2279,34 +2375,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- if (oldVersion < 99) {
- Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion
- + ", data will be lost!");
-
- db.execSQL("DROP TABLE IF EXISTS " + Tables.CONTACTS + ";");
- db.execSQL("DROP TABLE IF EXISTS " + Tables.RAW_CONTACTS + ";");
- db.execSQL("DROP TABLE IF EXISTS " + Tables.PACKAGES + ";");
- db.execSQL("DROP TABLE IF EXISTS " + Tables.MIMETYPES + ";");
- db.execSQL("DROP TABLE IF EXISTS " + Tables.DATA + ";");
- db.execSQL("DROP TABLE IF EXISTS " + Tables.PHONE_LOOKUP + ";");
- db.execSQL("DROP TABLE IF EXISTS " + Tables.NAME_LOOKUP + ";");
- db.execSQL("DROP TABLE IF EXISTS " + Tables.NICKNAME_LOOKUP + ";");
- db.execSQL("DROP TABLE IF EXISTS " + Tables.GROUPS + ";");
- db.execSQL("DROP TABLE IF EXISTS activities;");
- db.execSQL("DROP TABLE IF EXISTS calls;");
- db.execSQL("DROP TABLE IF EXISTS " + Tables.SETTINGS + ";");
- db.execSQL("DROP TABLE IF EXISTS " + Tables.STATUS_UPDATES + ";");
+ Log.i(TAG,
+ "Upgrading " + DATABASE_NAME + " from version " + oldVersion + " to " + newVersion);
- // TODO: we should not be dropping agg_exceptions and contact_options. In case that
- // table's schema changes, we should try to preserve the data, because it was entered
- // by the user and has never been synched to the server.
- db.execSQL("DROP TABLE IF EXISTS " + Tables.AGGREGATION_EXCEPTIONS + ";");
-
- onCreate(db);
- return;
- }
-
- Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion);
+ prepopulateCommonMimeTypes(db);
boolean upgradeViewsAndTriggers = false;
boolean upgradeNameLookup = false;
@@ -2316,428 +2388,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
boolean rebuildSqliteStats = false;
boolean upgradeLocaleSpecificData = false;
- if (oldVersion == 99) {
- upgradeViewsAndTriggers = true;
- oldVersion++;
- }
-
- if (oldVersion == 100) {
- db.execSQL("CREATE INDEX IF NOT EXISTS mimetypes_mimetype_index ON "
- + Tables.MIMETYPES + " ("
- + MimetypesColumns.MIMETYPE + ","
- + MimetypesColumns._ID + ");");
- updateIndexStats(db, Tables.MIMETYPES,
- "mimetypes_mimetype_index", "50 1 1");
-
- upgradeViewsAndTriggers = true;
- oldVersion++;
- }
-
- if (oldVersion == 101) {
- upgradeViewsAndTriggers = true;
- oldVersion++;
- }
-
- if (oldVersion == 102) {
- upgradeViewsAndTriggers = true;
- oldVersion++;
- }
-
- if (oldVersion == 103) {
- upgradeViewsAndTriggers = true;
- oldVersion++;
- }
-
- if (oldVersion == 104 || oldVersion == 201) {
- LegacyApiSupport.createSettingsTable(db);
- upgradeViewsAndTriggers = true;
- oldVersion++;
- }
-
- if (oldVersion == 105) {
- upgradeToVersion202(db);
- upgradeNameLookup = true;
- oldVersion = 202;
- }
-
- if (oldVersion == 202) {
- upgradeToVersion203(db);
- upgradeViewsAndTriggers = true;
- oldVersion++;
- }
-
- if (oldVersion == 203) {
- upgradeViewsAndTriggers = true;
- oldVersion++;
- }
-
- if (oldVersion == 204) {
- upgradeToVersion205(db);
- upgradeViewsAndTriggers = true;
- oldVersion++;
- }
-
- if (oldVersion == 205) {
- upgrateToVersion206(db);
- upgradeViewsAndTriggers = true;
- oldVersion++;
- }
-
- if (oldVersion == 206) {
- // Fix for the bug where name lookup records for organizations would get removed by
- // unrelated updates of the data rows. No longer needed.
- oldVersion = 300;
- }
-
- if (oldVersion == 300) {
- upgradeViewsAndTriggers = true;
- oldVersion = 301;
- }
-
- if (oldVersion == 301) {
- upgradeViewsAndTriggers = true;
- oldVersion = 302;
- }
-
- if (oldVersion == 302) {
- upgradeEmailToVersion303(db);
- upgradeNicknameToVersion303(db);
- oldVersion = 303;
- }
-
- if (oldVersion == 303) {
- upgradeToVersion304(db);
- oldVersion = 304;
- }
-
- if (oldVersion == 304) {
- upgradeNameLookup = true;
- oldVersion = 305;
- }
-
- if (oldVersion == 305) {
- upgradeToVersion306(db);
- oldVersion = 306;
- }
-
- if (oldVersion == 306) {
- upgradeToVersion307(db);
- oldVersion = 307;
- }
-
- if (oldVersion == 307) {
- upgradeToVersion308(db);
- oldVersion = 308;
- }
-
- // Gingerbread upgrades.
- if (oldVersion < 350) {
- upgradeViewsAndTriggers = true;
- oldVersion = 351;
- }
-
- if (oldVersion == 351) {
- upgradeNameLookup = true;
- oldVersion = 352;
- }
-
- if (oldVersion == 352) {
- upgradeToVersion353(db);
- oldVersion = 353;
- }
-
- // Honeycomb upgrades.
- if (oldVersion < 400) {
- upgradeViewsAndTriggers = true;
- upgradeToVersion400(db);
- oldVersion = 400;
- }
-
- if (oldVersion == 400) {
- upgradeViewsAndTriggers = true;
- upgradeToVersion401(db);
- oldVersion = 401;
- }
-
- if (oldVersion == 401) {
- upgradeToVersion402(db);
- oldVersion = 402;
- }
-
- if (oldVersion == 402) {
- upgradeViewsAndTriggers = true;
- upgradeToVersion403(db);
- oldVersion = 403;
- }
-
- if (oldVersion == 403) {
- upgradeViewsAndTriggers = true;
- oldVersion = 404;
- }
-
- if (oldVersion == 404) {
- upgradeViewsAndTriggers = true;
- upgradeToVersion405(db);
- oldVersion = 405;
- }
-
- if (oldVersion == 405) {
- upgradeViewsAndTriggers = true;
- upgradeToVersion406(db);
- oldVersion = 406;
- }
-
- if (oldVersion == 406) {
- upgradeViewsAndTriggers = true;
- oldVersion = 407;
- }
-
- if (oldVersion == 407) {
- oldVersion = 408; // Obsolete.
- }
-
- if (oldVersion == 408) {
- upgradeViewsAndTriggers = true;
- upgradeToVersion409(db);
- oldVersion = 409;
- }
-
- if (oldVersion == 409) {
- upgradeViewsAndTriggers = true;
- oldVersion = 410;
- }
-
- if (oldVersion == 410) {
- upgradeToVersion411(db);
- oldVersion = 411;
- }
-
- if (oldVersion == 411) {
- // Same upgrade as 353, only on Honeycomb devices.
- upgradeToVersion353(db);
- oldVersion = 412;
- }
-
- if (oldVersion == 412) {
- upgradeToVersion413(db);
- oldVersion = 413;
- }
-
- if (oldVersion == 413) {
- upgradeNameLookup = true;
- oldVersion = 414;
- }
-
- if (oldVersion == 414) {
- upgradeToVersion415(db);
- upgradeViewsAndTriggers = true;
- oldVersion = 415;
- }
-
- if (oldVersion == 415) {
- upgradeToVersion416(db);
- oldVersion = 416;
- }
-
- if (oldVersion == 416) {
- upgradeLegacyApiSupport = true;
- oldVersion = 417;
- }
-
- // Honeycomb-MR1 upgrades.
- if (oldVersion < 500) {
- upgradeSearchIndex = true;
- }
-
- if (oldVersion < 501) {
- upgradeSearchIndex = true;
- upgradeToVersion501(db);
- oldVersion = 501;
- }
-
- if (oldVersion < 502) {
- upgradeSearchIndex = true;
- upgradeToVersion502(db);
- oldVersion = 502;
- }
-
- if (oldVersion < 503) {
- upgradeSearchIndex = true;
- oldVersion = 503;
- }
-
- if (oldVersion < 504) {
- upgradeToVersion504(db);
- oldVersion = 504;
- }
-
- if (oldVersion < 600) {
- // This change used to add the profile raw contact ID to the Accounts table. That
- // column is no longer needed (as of version 614) since the profile records are stored in
- // a separate copy of the database for security reasons. So this change is now a no-op.
- upgradeViewsAndTriggers = true;
- oldVersion = 600;
- }
-
- if (oldVersion < 601) {
- upgradeToVersion601(db);
- oldVersion = 601;
- }
-
- if (oldVersion < 602) {
- upgradeToVersion602(db);
- oldVersion = 602;
- }
-
- if (oldVersion < 603) {
- upgradeViewsAndTriggers = true;
- oldVersion = 603;
- }
-
- if (oldVersion < 604) {
- upgradeToVersion604(db);
- oldVersion = 604;
- }
-
- if (oldVersion < 605) {
- upgradeViewsAndTriggers = true;
- // This version used to create the stream item and stream item photos tables, but
- // a newer version of those tables is created in version 609 below. So omitting the
- // creation in this upgrade step to avoid a create->drop->create.
- oldVersion = 605;
- }
-
- if (oldVersion < 606) {
- upgradeViewsAndTriggers = true;
- upgradeLegacyApiSupport = true;
- upgradeToVersion606(db);
- oldVersion = 606;
- }
-
- if (oldVersion < 607) {
- upgradeViewsAndTriggers = true;
- // We added "action" and "action_uri" to groups here, but realized this was not a smart
- // move. This upgrade step has been removed (all dogfood phones that executed this step
- // will have those columns, but that shouldn't hurt. Unfortunately, SQLite makes it
- // hard to remove columns).
- oldVersion = 607;
- }
-
- if (oldVersion < 608) {
- upgradeViewsAndTriggers = true;
- upgradeToVersion608(db);
- oldVersion = 608;
- }
-
- if (oldVersion < 609) {
- // This version used to create the stream item and stream item photos tables, but a
- // newer version of those tables is created in version 613 below. So omitting the
- // creation in this upgrade step to avoid a create->drop->create.
- oldVersion = 609;
- }
-
- if (oldVersion < 610) {
- upgradeToVersion610(db);
- oldVersion = 610;
- }
-
- if (oldVersion < 611) {
- upgradeViewsAndTriggers = true;
- upgradeToVersion611(db);
- oldVersion = 611;
- }
-
- if (oldVersion < 612) {
- upgradeViewsAndTriggers = true;
- upgradeToVersion612(db);
- oldVersion = 612;
- }
-
- if (oldVersion < 613) {
- upgradeToVersion613(db);
- oldVersion = 613;
- }
-
- if (oldVersion < 614) {
- // This creates the "view_stream_items" view.
- upgradeViewsAndTriggers = true;
- oldVersion = 614;
- }
-
- if (oldVersion < 615) {
- upgradeToVersion615(db);
- oldVersion = 615;
- }
-
- if (oldVersion < 616) {
- // This updates the "view_stream_items" view.
- upgradeViewsAndTriggers = true;
- oldVersion = 616;
- }
-
- if (oldVersion < 617) {
- // This version upgrade obsoleted the profile_raw_contact_id field of the Accounts
- // table, but we aren't removing the column because it is very little data (and not
- // referenced anymore). We do need to upgrade the views to handle the simplified
- // per-database "is profile" columns.
- upgradeViewsAndTriggers = true;
- oldVersion = 617;
- }
-
- if (oldVersion < 618) {
- upgradeToVersion618(db);
- oldVersion = 618;
- }
-
- if (oldVersion < 619) {
- upgradeViewsAndTriggers = true;
- oldVersion = 619;
- }
-
- if (oldVersion < 620) {
- upgradeViewsAndTriggers = true;
- oldVersion = 620;
- }
-
- if (oldVersion < 621) {
- upgradeSearchIndex = true;
- oldVersion = 621;
- }
-
- if (oldVersion < 622) {
- upgradeToVersion622(db);
- oldVersion = 622;
- }
-
- if (oldVersion < 623) {
- // Change FTS to normalize names using collation key.
- upgradeSearchIndex = true;
- oldVersion = 623;
- }
-
- if (oldVersion < 624) {
- // Upgraded the SQLite index stats.
- upgradeViewsAndTriggers = true;
- oldVersion = 624;
- }
-
- if (oldVersion < 625) {
- // Fix for search for hyphenated names
- upgradeSearchIndex = true;
- oldVersion = 625;
- }
-
- if (oldVersion < 626) {
- upgradeToVersion626(db);
- upgradeViewsAndTriggers = true;
- oldVersion = 626;
- }
-
- if (oldVersion < 700) {
- rescanDirectories = true;
- oldVersion = 700;
- }
-
if (oldVersion < 701) {
upgradeToVersion701(db);
oldVersion = 701;
@@ -2992,6 +2642,22 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
oldVersion = 1111;
}
+ if (isUpgradeRequired(oldVersion, newVersion, 1200)) {
+ createPresenceTables(db);
+ oldVersion = 1200;
+ }
+
+ if (isUpgradeRequired(oldVersion, newVersion, 1201)) {
+ upgradeToVersion1201(db);
+ upgradeViewsAndTriggers = true;
+ oldVersion = 1201;
+ }
+
+ if (isUpgradeRequired(oldVersion, newVersion, 1202)) {
+ upgradeViewsAndTriggers = true;
+ oldVersion = 1202;
+ }
+
// We extracted "calls" and "voicemail_status" at this point, but we can't remove them here
// yet, until CallLogDatabaseHelper moves the data.
@@ -3046,471 +2712,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
return oldVersion < version && newVersion >= version;
}
- private void upgradeToVersion202(SQLiteDatabase db) {
- db.execSQL(
- "ALTER TABLE " + Tables.PHONE_LOOKUP +
- " ADD " + PhoneLookupColumns.MIN_MATCH + " TEXT;");
-
- db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
- PhoneLookupColumns.MIN_MATCH + "," +
- PhoneLookupColumns.RAW_CONTACT_ID + "," +
- PhoneLookupColumns.DATA_ID +
- ");");
-
- updateIndexStats(db, Tables.PHONE_LOOKUP,
- "phone_lookup_min_match_index", "10000 2 2 1");
-
- SQLiteStatement update = db.compileStatement(
- "UPDATE " + Tables.PHONE_LOOKUP +
- " SET " + PhoneLookupColumns.MIN_MATCH + "=?" +
- " WHERE " + PhoneLookupColumns.DATA_ID + "=?");
-
- // Populate the new column
- Cursor c = db.query(Tables.PHONE_LOOKUP + " JOIN " + Tables.DATA +
- " ON (" + PhoneLookupColumns.DATA_ID + "=" + DataColumns.CONCRETE_ID + ")",
- new String[] {Data._ID, Phone.NUMBER}, null, null, null, null, null);
- try {
- while (c.moveToNext()) {
- long dataId = c.getLong(0);
- String number = c.getString(1);
- if (!TextUtils.isEmpty(number)) {
- update.bindString(1, PhoneNumberUtils.toCallerIDMinMatch(number));
- update.bindLong(2, dataId);
- update.execute();
- }
- }
- } finally {
- c.close();
- }
- }
-
- private void upgradeToVersion203(SQLiteDatabase db) {
- // Garbage-collect first. A bug in Eclair was sometimes leaving
- // raw_contacts in the database that no longer had contacts associated
- // with them. To avoid failures during this database upgrade, drop
- // the orphaned raw_contacts.
- db.execSQL(
- "DELETE FROM raw_contacts" +
- " WHERE contact_id NOT NULL" +
- " AND contact_id NOT IN (SELECT _id FROM contacts)");
-
- db.execSQL(
- "ALTER TABLE " + Tables.CONTACTS +
- " ADD " + Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)");
- db.execSQL(
- "ALTER TABLE " + Tables.RAW_CONTACTS +
- " ADD contact_in_visible_group INTEGER NOT NULL DEFAULT 0");
-
- // For each Contact, find the RawContact that contributed the display name
- db.execSQL(
- "UPDATE " + Tables.CONTACTS +
- " 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 +
- " ORDER BY " + RawContacts._ID +
- " LIMIT 1)"
- );
-
- db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" +
- Contacts.NAME_RAW_CONTACT_ID +
- ");");
-
- // If for some unknown reason we missed some names, let's make sure there are
- // no contacts without a name, picking a raw contact "at random".
- db.execSQL(
- "UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
- " SELECT " + RawContacts._ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
- " ORDER BY " + RawContacts._ID +
- " LIMIT 1)" +
- " WHERE " + Contacts.NAME_RAW_CONTACT_ID + " IS NULL"
- );
-
- // Wipe out DISPLAY_NAME on the Contacts table as it is no longer in use.
- db.execSQL(
- "UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.DISPLAY_NAME + "=NULL"
- );
-
- // Copy the IN_VISIBLE_GROUP flag down to all raw contacts to allow
- // indexing on (display_name, in_visible_group)
- db.execSQL(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET contact_in_visible_group=(" +
- "SELECT " + Contacts.IN_VISIBLE_GROUP +
- " FROM " + Tables.CONTACTS +
- " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")" +
- " WHERE " + RawContacts.CONTACT_ID + " NOT NULL"
- );
-
- db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
- "contact_in_visible_group" + "," +
- RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" +
- ");");
-
- db.execSQL("DROP INDEX contacts_visible_index");
- db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
- Contacts.IN_VISIBLE_GROUP +
- ");");
- }
-
- private void upgradeToVersion205(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
- + " ADD " + RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT;");
- db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
- + " ADD " + RawContacts.PHONETIC_NAME + " TEXT;");
- db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
- + " ADD " + RawContacts.PHONETIC_NAME_STYLE + " INTEGER;");
- db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
- + " ADD " + RawContacts.SORT_KEY_PRIMARY
- + " TEXT COLLATE " + ContactsProvider2.PHONEBOOK_COLLATOR_NAME + ";");
- db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
- + " ADD " + RawContacts.SORT_KEY_ALTERNATIVE
- + " TEXT COLLATE " + ContactsProvider2.PHONEBOOK_COLLATOR_NAME + ";");
-
- NameSplitter splitter = createNameSplitter();
-
- SQLiteStatement rawContactUpdate = db.compileStatement(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET " +
- RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
- RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
- RawContacts.PHONETIC_NAME + "=?," +
- RawContacts.PHONETIC_NAME_STYLE + "=?," +
- RawContacts.SORT_KEY_PRIMARY + "=?," +
- RawContacts.SORT_KEY_ALTERNATIVE + "=?" +
- " WHERE " + RawContacts._ID + "=?");
-
- upgradeStructuredNamesToVersion205(db, rawContactUpdate, splitter);
- upgradeOrganizationsToVersion205(db, rawContactUpdate, splitter);
-
- db.execSQL("DROP INDEX raw_contact_sort_key1_index");
- db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
- "contact_in_visible_group" + "," +
- RawContacts.SORT_KEY_PRIMARY +
- ");");
-
- db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
- "contact_in_visible_group" + "," +
- RawContacts.SORT_KEY_ALTERNATIVE +
- ");");
- }
-
- private void upgradeStructuredNamesToVersion205(
- SQLiteDatabase db, SQLiteStatement rawContactUpdate, NameSplitter splitter) {
-
- // Process structured names to detect the style of the full name and phonetic name.
- long mMimeType;
- try {
- mMimeType = DatabaseUtils.longForQuery(db,
- "SELECT " + MimetypesColumns._ID +
- " FROM " + Tables.MIMETYPES +
- " WHERE " + MimetypesColumns.MIMETYPE
- + "='" + StructuredName.CONTENT_ITEM_TYPE + "'", null);
-
- } catch (SQLiteDoneException e) {
- // No structured names in the database.
- return;
- }
-
- SQLiteStatement structuredNameUpdate = db.compileStatement(
- "UPDATE " + Tables.DATA +
- " SET " +
- StructuredName.FULL_NAME_STYLE + "=?," +
- StructuredName.DISPLAY_NAME + "=?," +
- StructuredName.PHONETIC_NAME_STYLE + "=?" +
- " WHERE " + Data._ID + "=?");
-
- NameSplitter.Name name = new NameSplitter.Name();
- Cursor cursor = db.query(StructName205Query.TABLE,
- StructName205Query.COLUMNS,
- DataColumns.MIMETYPE_ID + "=" + mMimeType, null, null, null, null);
- try {
- while (cursor.moveToNext()) {
- long dataId = cursor.getLong(StructName205Query.ID);
- long rawContactId = cursor.getLong(StructName205Query.RAW_CONTACT_ID);
- int displayNameSource = cursor.getInt(StructName205Query.DISPLAY_NAME_SOURCE);
-
- name.clear();
- name.prefix = cursor.getString(StructName205Query.PREFIX);
- name.givenNames = cursor.getString(StructName205Query.GIVEN_NAME);
- name.middleName = cursor.getString(StructName205Query.MIDDLE_NAME);
- name.familyName = cursor.getString(StructName205Query.FAMILY_NAME);
- name.suffix = cursor.getString(StructName205Query.SUFFIX);
- name.phoneticFamilyName = cursor.getString(StructName205Query.PHONETIC_FAMILY_NAME);
- name.phoneticMiddleName = cursor.getString(StructName205Query.PHONETIC_MIDDLE_NAME);
- name.phoneticGivenName = cursor.getString(StructName205Query.PHONETIC_GIVEN_NAME);
-
- upgradeNameToVersion205(dataId, rawContactId, displayNameSource, name,
- structuredNameUpdate, rawContactUpdate, splitter);
- }
- } finally {
- cursor.close();
- }
- }
-
- private void upgradeNameToVersion205(
- long dataId,
- long rawContactId,
- int displayNameSource,
- NameSplitter.Name name,
- SQLiteStatement structuredNameUpdate,
- SQLiteStatement rawContactUpdate,
- NameSplitter splitter) {
-
- splitter.guessNameStyle(name);
- int unadjustedFullNameStyle = name.fullNameStyle;
- name.fullNameStyle = splitter.getAdjustedFullNameStyle(name.fullNameStyle);
- String displayName = splitter.join(name, true, true);
-
- // Don't update database with the adjusted fullNameStyle as it is locale
- // related
- structuredNameUpdate.bindLong(1, unadjustedFullNameStyle);
- DatabaseUtils.bindObjectToProgram(structuredNameUpdate, 2, displayName);
- structuredNameUpdate.bindLong(3, name.phoneticNameStyle);
- structuredNameUpdate.bindLong(4, dataId);
- structuredNameUpdate.execute();
-
- if (displayNameSource == DisplayNameSources.STRUCTURED_NAME) {
- String displayNameAlternative = splitter.join(name, false, false);
- String phoneticName = splitter.joinPhoneticName(name);
- String sortKey = null;
- String sortKeyAlternative = null;
-
- if (phoneticName != null) {
- sortKey = sortKeyAlternative = phoneticName;
- } else if (name.fullNameStyle == FullNameStyle.CHINESE ||
- name.fullNameStyle == FullNameStyle.CJK) {
- sortKey = sortKeyAlternative = displayName;
- }
-
- if (sortKey == null) {
- sortKey = displayName;
- sortKeyAlternative = displayNameAlternative;
- }
-
- updateRawContact205(rawContactUpdate, rawContactId, displayName,
- displayNameAlternative, name.phoneticNameStyle, phoneticName, sortKey,
- sortKeyAlternative);
- }
- }
-
- private void upgradeOrganizationsToVersion205(
- SQLiteDatabase db, SQLiteStatement rawContactUpdate, NameSplitter splitter) {
-
- final long mimeType = lookupMimeTypeId(db, Organization.CONTENT_ITEM_TYPE);
- SQLiteStatement organizationUpdate = db.compileStatement(
- "UPDATE " + Tables.DATA +
- " SET " +
- Organization.PHONETIC_NAME_STYLE + "=?" +
- " WHERE " + Data._ID + "=?");
-
- Cursor cursor = db.query(Organization205Query.TABLE, Organization205Query.COLUMNS,
- DataColumns.MIMETYPE_ID + "=" + mimeType + " AND "
- + RawContacts.DISPLAY_NAME_SOURCE + "=" + DisplayNameSources.ORGANIZATION,
- null, null, null, null);
- try {
- while (cursor.moveToNext()) {
- long dataId = cursor.getLong(Organization205Query.ID);
- long rawContactId = cursor.getLong(Organization205Query.RAW_CONTACT_ID);
- String company = cursor.getString(Organization205Query.COMPANY);
- String phoneticName = cursor.getString(Organization205Query.PHONETIC_NAME);
-
- int phoneticNameStyle = splitter.guessPhoneticNameStyle(phoneticName);
-
- organizationUpdate.bindLong(1, phoneticNameStyle);
- organizationUpdate.bindLong(2, dataId);
- organizationUpdate.execute();
-
- String sortKey = company;
-
- updateRawContact205(rawContactUpdate, rawContactId, company,
- company, phoneticNameStyle, phoneticName, sortKey, sortKey);
- }
- } finally {
- cursor.close();
- }
- }
-
- private void updateRawContact205(SQLiteStatement rawContactUpdate, long rawContactId,
- String displayName, String displayNameAlternative, int phoneticNameStyle,
- String phoneticName, String sortKeyPrimary, String sortKeyAlternative) {
- bindString(rawContactUpdate, 1, displayName);
- bindString(rawContactUpdate, 2, displayNameAlternative);
- bindString(rawContactUpdate, 3, phoneticName);
- rawContactUpdate.bindLong(4, phoneticNameStyle);
- bindString(rawContactUpdate, 5, sortKeyPrimary);
- bindString(rawContactUpdate, 6, sortKeyAlternative);
- rawContactUpdate.bindLong(7, rawContactId);
- rawContactUpdate.execute();
- }
-
- private void upgrateToVersion206(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
- + " ADD name_verified INTEGER NOT NULL DEFAULT 0;");
- }
-
- /**
- * The {@link ContactsProvider2#update} method was deleting name lookup for new
- * emails during the sync. We need to restore the lost name lookup rows.
- */
- private void upgradeEmailToVersion303(SQLiteDatabase db) {
- final long mimeTypeId = lookupMimeTypeId(db, Email.CONTENT_ITEM_TYPE);
- if (mimeTypeId == -1) {
- return;
- }
-
- ContentValues values = new ContentValues();
-
- // Find all data rows with the mime type "email" that are missing name lookup
- Cursor cursor = db.query(Upgrade303Query.TABLE, Upgrade303Query.COLUMNS,
- Upgrade303Query.SELECTION, new String[] {String.valueOf(mimeTypeId)},
- null, null, null);
- try {
- while (cursor.moveToNext()) {
- long dataId = cursor.getLong(Upgrade303Query.ID);
- long rawContactId = cursor.getLong(Upgrade303Query.RAW_CONTACT_ID);
- String value = cursor.getString(Upgrade303Query.DATA1);
- value = extractHandleFromEmailAddress(value);
-
- if (value != null) {
- values.put(NameLookupColumns.DATA_ID, dataId);
- values.put(NameLookupColumns.RAW_CONTACT_ID, rawContactId);
- values.put(NameLookupColumns.NAME_TYPE, NameLookupType.EMAIL_BASED_NICKNAME);
- values.put(NameLookupColumns.NORMALIZED_NAME, NameNormalizer.normalize(value));
- db.insert(Tables.NAME_LOOKUP, null, values);
- }
- }
- } finally {
- cursor.close();
- }
- }
-
- /**
- * The {@link ContactsProvider2#update} method was deleting name lookup for new
- * nicknames during the sync. We need to restore the lost name lookup rows.
- */
- private void upgradeNicknameToVersion303(SQLiteDatabase db) {
- final long mimeTypeId = lookupMimeTypeId(db, Nickname.CONTENT_ITEM_TYPE);
- if (mimeTypeId == -1) {
- return;
- }
-
- ContentValues values = new ContentValues();
-
- // Find all data rows with the mime type "nickname" that are missing name lookup
- Cursor cursor = db.query(Upgrade303Query.TABLE, Upgrade303Query.COLUMNS,
- Upgrade303Query.SELECTION, new String[] {String.valueOf(mimeTypeId)},
- null, null, null);
- try {
- while (cursor.moveToNext()) {
- long dataId = cursor.getLong(Upgrade303Query.ID);
- long rawContactId = cursor.getLong(Upgrade303Query.RAW_CONTACT_ID);
- String value = cursor.getString(Upgrade303Query.DATA1);
-
- values.put(NameLookupColumns.DATA_ID, dataId);
- values.put(NameLookupColumns.RAW_CONTACT_ID, rawContactId);
- values.put(NameLookupColumns.NAME_TYPE, NameLookupType.NICKNAME);
- values.put(NameLookupColumns.NORMALIZED_NAME, NameNormalizer.normalize(value));
- db.insert(Tables.NAME_LOOKUP, null, values);
- }
- } finally {
- cursor.close();
- }
- }
-
- private void upgradeToVersion304(SQLiteDatabase db) {
- // Mimetype table requires an index on mime type.
- db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS mime_type ON " + Tables.MIMETYPES + " (" +
- MimetypesColumns.MIMETYPE +
- ");");
- }
-
- private void upgradeToVersion306(SQLiteDatabase db) {
- // Fix invalid lookup that was used for Exchange contacts (it was not escaped)
- // It happened when a new contact was created AND synchronized
- final StringBuilder lookupKeyBuilder = new StringBuilder();
- final SQLiteStatement updateStatement = db.compileStatement(
- "UPDATE contacts " +
- "SET lookup=? " +
- "WHERE _id=?");
- final Cursor contactIdCursor = db.rawQuery(
- "SELECT DISTINCT contact_id " +
- "FROM raw_contacts " +
- "WHERE deleted=0 AND account_type='com.android.exchange'",
- null);
- try {
- while (contactIdCursor.moveToNext()) {
- final long contactId = contactIdCursor.getLong(0);
- lookupKeyBuilder.setLength(0);
- final Cursor c = db.rawQuery(
- "SELECT account_type, account_name, _id, sourceid, display_name " +
- "FROM raw_contacts " +
- "WHERE contact_id=? " +
- "ORDER BY _id",
- new String[] {String.valueOf(contactId)});
- try {
- while (c.moveToNext()) {
- ContactLookupKey.appendToLookupKey(lookupKeyBuilder,
- c.getString(0),
- c.getString(1),
- c.getLong(2),
- c.getString(3),
- c.getString(4));
- }
- } finally {
- c.close();
- }
-
- if (lookupKeyBuilder.length() == 0) {
- updateStatement.bindNull(1);
- } else {
- updateStatement.bindString(1, Uri.encode(lookupKeyBuilder.toString()));
- }
- updateStatement.bindLong(2, contactId);
-
- updateStatement.execute();
- }
- } finally {
- updateStatement.close();
- contactIdCursor.close();
- }
- }
-
- private void upgradeToVersion307(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE properties (" +
- "property_key TEXT PRIMARY_KEY, " +
- "property_value TEXT" +
- ");");
- }
-
- private void upgradeToVersion308(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE accounts (" +
- "account_name TEXT, " +
- "account_type TEXT " +
- ");");
-
- db.execSQL("INSERT INTO accounts " +
- "SELECT DISTINCT account_name, account_type FROM raw_contacts");
- }
-
- private void upgradeToVersion400(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE " + Tables.GROUPS
- + " ADD " + Groups.FAVORITES + " INTEGER NOT NULL DEFAULT 0;");
- db.execSQL("ALTER TABLE " + Tables.GROUPS
- + " ADD " + Groups.AUTO_ADD + " INTEGER NOT NULL DEFAULT 0;");
- }
-
- private void upgradeToVersion353(SQLiteDatabase db) {
- db.execSQL("DELETE FROM contacts " +
- "WHERE NOT EXISTS (SELECT 1 FROM raw_contacts WHERE contact_id=contacts._id)");
- }
-
private void rebuildNameLookup(SQLiteDatabase db, boolean rebuildSqliteStats) {
db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
insertNameLookup(db);
@@ -3551,7 +2752,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
Log.i(TAG, "Upgrading locale data for " + locales
+ " (ICU v" + ICU.getIcuVersion() + ")");
final long start = SystemClock.elapsedRealtime();
- initializeCache(db);
rebuildLocaleData(db, locales, rebuildSqliteStats);
Log.i(TAG, "Locale update completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
}
@@ -3618,7 +2818,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
private void insertNameLookup(SQLiteDatabase db) {
db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP);
- SQLiteStatement nameLookupInsert = db.compileStatement(
+ final SQLiteStatement nameLookupInsert = db.compileStatement(
"INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
+ NameLookupColumns.RAW_CONTACT_ID + ","
+ NameLookupColumns.DATA_ID + ","
@@ -3729,442 +2929,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
stmt.executeInsert();
}
- /**
- * Changing the VISIBLE bit from a field on both RawContacts and Contacts to a separate table.
- */
- private void upgradeToVersion401(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" +
- Contacts._ID + " INTEGER PRIMARY KEY" +
- ");");
- db.execSQL("INSERT INTO " + Tables.VISIBLE_CONTACTS +
- " SELECT " + Contacts._ID +
- " FROM " + Tables.CONTACTS +
- " WHERE " + Contacts.IN_VISIBLE_GROUP + "!=0");
- db.execSQL("DROP INDEX contacts_visible_index");
- }
-
- /**
- * Introducing a new table: directories.
- */
- private void upgradeToVersion402(SQLiteDatabase db) {
- createDirectoriesTable(db);
- }
-
- private void upgradeToVersion403(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS directories;");
- createDirectoriesTable(db);
-
- db.execSQL("ALTER TABLE raw_contacts"
- + " ADD raw_contact_is_read_only INTEGER NOT NULL DEFAULT 0;");
-
- db.execSQL("ALTER TABLE data"
- + " ADD is_read_only INTEGER NOT NULL DEFAULT 0;");
- }
-
- private void upgradeToVersion405(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS phone_lookup;");
- // Private phone numbers table used for lookup
- db.execSQL("CREATE TABLE " + Tables.PHONE_LOOKUP + " (" +
- PhoneLookupColumns.DATA_ID
- + " INTEGER REFERENCES data(_id) NOT NULL," +
- PhoneLookupColumns.RAW_CONTACT_ID
- + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
- PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," +
- PhoneLookupColumns.MIN_MATCH + " TEXT NOT NULL" +
- ");");
-
- db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" +
- PhoneLookupColumns.NORMALIZED_NUMBER + "," +
- PhoneLookupColumns.RAW_CONTACT_ID + "," +
- PhoneLookupColumns.DATA_ID +
- ");");
-
- db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
- PhoneLookupColumns.MIN_MATCH + "," +
- PhoneLookupColumns.RAW_CONTACT_ID + "," +
- PhoneLookupColumns.DATA_ID +
- ");");
-
- final long mimeTypeId = lookupMimeTypeId(db, Phone.CONTENT_ITEM_TYPE);
- if (mimeTypeId == -1) {
- return;
- }
-
- Cursor cursor = db.rawQuery(
- "SELECT _id, " + Phone.RAW_CONTACT_ID + ", " + Phone.NUMBER +
- " FROM " + Tables.DATA +
- " WHERE " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId
- + " AND " + Phone.NUMBER + " NOT NULL", null);
-
- ContentValues phoneValues = new ContentValues();
- try {
- while (cursor.moveToNext()) {
- long dataID = cursor.getLong(0);
- long rawContactID = cursor.getLong(1);
- String number = cursor.getString(2);
- String normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
- if (!TextUtils.isEmpty(normalizedNumber)) {
- phoneValues.clear();
- phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactID);
- phoneValues.put(PhoneLookupColumns.DATA_ID, dataID);
- phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
- phoneValues.put(PhoneLookupColumns.MIN_MATCH,
- PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber));
- db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
- }
- }
- } finally {
- cursor.close();
- }
- }
-
- private void upgradeToVersion406(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE calls ADD countryiso TEXT;");
- }
-
- private void upgradeToVersion409(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS directories;");
- createDirectoriesTable(db);
- }
-
- /**
- * Adding DEFAULT_DIRECTORY table.
- * DEFAULT_DIRECTORY should contain every contact which should be shown to users in default.
- * - if a contact doesn't belong to any account (local contact), it should be in
- * default_directory
- * - if a contact belongs to an account that doesn't have a "default" group, it should be in
- * default_directory
- * - if a contact belongs to an account that has a "default" group (like Google directory,
- * which has "My contacts" group as default), it should be in default_directory.
- *
- * This logic assumes that accounts with the "default" group should have at least one
- * group with AUTO_ADD (implying it is the default group) flag in the groups table.
- */
- private void upgradeToVersion411(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS " + Tables.DEFAULT_DIRECTORY);
- db.execSQL("CREATE TABLE default_directory (_id INTEGER PRIMARY KEY);");
-
- // Process contacts without an account
- db.execSQL("INSERT OR IGNORE INTO default_directory " +
- " SELECT contact_id " +
- " FROM raw_contacts " +
- " WHERE raw_contacts.account_name IS NULL " +
- " AND raw_contacts.account_type IS NULL ");
-
- // Process accounts that don't have a default group (e.g. Exchange).
- db.execSQL("INSERT OR IGNORE INTO default_directory " +
- " SELECT contact_id " +
- " FROM raw_contacts " +
- " WHERE NOT EXISTS" +
- " (SELECT _id " +
- " FROM groups " +
- " WHERE raw_contacts.account_name = groups.account_name" +
- " AND raw_contacts.account_type = groups.account_type" +
- " AND groups.auto_add != 0)");
-
- final long mimetype = lookupMimeTypeId(db, GroupMembership.CONTENT_ITEM_TYPE);
-
- // Process accounts that do have a default group (e.g. Google)
- db.execSQL("INSERT OR IGNORE INTO default_directory " +
- " SELECT contact_id " +
- " FROM raw_contacts " +
- " JOIN data " +
- " ON (raw_contacts._id=raw_contact_id)" +
- " WHERE mimetype_id=" + mimetype +
- " AND EXISTS" +
- " (SELECT _id" +
- " FROM groups" +
- " WHERE raw_contacts.account_name = groups.account_name" +
- " AND raw_contacts.account_type = groups.account_type" +
- " AND groups.auto_add != 0)");
- }
-
- private void upgradeToVersion413(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS directories;");
- createDirectoriesTable(db);
- }
-
- private void upgradeToVersion415(SQLiteDatabase db) {
- db.execSQL(
- "ALTER TABLE " + Tables.GROUPS +
- " ADD " + Groups.GROUP_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0");
- db.execSQL(
- "UPDATE " + Tables.GROUPS +
- " SET " + Groups.GROUP_IS_READ_ONLY + "=1" +
- " WHERE " + Groups.SYSTEM_ID + " NOT NULL");
- }
-
- private void upgradeToVersion416(SQLiteDatabase db) {
- db.execSQL("CREATE INDEX phone_lookup_data_id_min_match_index ON " + Tables.PHONE_LOOKUP +
- " (" + PhoneLookupColumns.DATA_ID + ", " + PhoneLookupColumns.MIN_MATCH + ");");
- }
-
- private void upgradeToVersion501(SQLiteDatabase db) {
- // Remove organization rows from the name lookup, we now use search index for that
- db.execSQL("DELETE FROM name_lookup WHERE name_type=5");
- }
-
- private void upgradeToVersion502(SQLiteDatabase db) {
- // Remove Chinese and Korean name lookup - this data is now in the search index
- db.execSQL("DELETE FROM name_lookup WHERE name_type IN (6, 7)");
- }
-
- private void upgradeToVersion504(SQLiteDatabase db) {
- initializeCache(db);
-
- // Find all names with prefixes and recreate display name
- Cursor cursor = db.rawQuery(
- "SELECT " + StructuredName.RAW_CONTACT_ID +
- " FROM " + Tables.DATA +
- " WHERE " + DataColumns.MIMETYPE_ID + "=?"
- + " AND " + StructuredName.PREFIX + " NOT NULL",
- new String[] {String.valueOf(mMimeTypeIdStructuredName)});
-
- try {
- while(cursor.moveToNext()) {
- long rawContactId = cursor.getLong(0);
- updateRawContactDisplayName(db, rawContactId);
- }
-
- } finally {
- cursor.close();
- }
- }
-
- private void upgradeToVersion601(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE data_usage_stat(" +
- "stat_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
- "data_id INTEGER NOT NULL, " +
- "usage_type INTEGER NOT NULL DEFAULT 0, " +
- "times_used INTEGER NOT NULL DEFAULT 0, " +
- "last_time_used INTEGER NOT NULL DEFAULT 0, " +
- "FOREIGN KEY(data_id) REFERENCES data(_id));");
- db.execSQL("CREATE UNIQUE INDEX data_usage_stat_index ON " +
- "data_usage_stat (data_id, usage_type)");
- }
-
- private void upgradeToVersion602(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE calls ADD voicemail_uri TEXT;");
- db.execSQL("ALTER TABLE calls ADD _data TEXT;");
- db.execSQL("ALTER TABLE calls ADD has_content INTEGER;");
- db.execSQL("ALTER TABLE calls ADD mime_type TEXT;");
- db.execSQL("ALTER TABLE calls ADD source_data TEXT;");
- db.execSQL("ALTER TABLE calls ADD source_package TEXT;");
- db.execSQL("ALTER TABLE calls ADD state INTEGER;");
- }
-
- private void upgradeToVersion604(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE voicemail_status (" +
- "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "source_package TEXT UNIQUE NOT NULL," +
- "settings_uri TEXT," +
- "voicemail_access_uri TEXT," +
- "configuration_state INTEGER," +
- "data_channel_state INTEGER," +
- "notification_channel_state INTEGER" +
- ");");
- }
-
- private void upgradeToVersion606(SQLiteDatabase db) {
- db.execSQL("DROP VIEW IF EXISTS view_contacts_restricted;");
- db.execSQL("DROP VIEW IF EXISTS view_data_restricted;");
- db.execSQL("DROP VIEW IF EXISTS view_raw_contacts_restricted;");
- db.execSQL("DROP VIEW IF EXISTS view_raw_entities_restricted;");
- db.execSQL("DROP VIEW IF EXISTS view_entities_restricted;");
- db.execSQL("DROP VIEW IF EXISTS view_data_usage_stat_restricted;");
- db.execSQL("DROP INDEX IF EXISTS contacts_restricted_index");
-
- // We should remove the restricted columns here as well, but unfortunately SQLite doesn't
- // provide ALTER TABLE DROP COLUMN. As they have DEFAULT 0, we can keep but ignore them
- }
-
- private void upgradeToVersion608(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE contacts ADD photo_file_id INTEGER REFERENCES photo_files(_id);");
-
- db.execSQL("CREATE TABLE photo_files(" +
- "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
- "height INTEGER NOT NULL, " +
- "width INTEGER NOT NULL, " +
- "filesize INTEGER NOT NULL);");
- }
-
- private void upgradeToVersion610(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE calls ADD is_read INTEGER;");
- }
-
- private void upgradeToVersion611(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE raw_contacts ADD data_set TEXT DEFAULT NULL;");
- db.execSQL("ALTER TABLE groups ADD data_set TEXT DEFAULT NULL;");
- db.execSQL("ALTER TABLE accounts ADD data_set TEXT DEFAULT NULL;");
-
- db.execSQL("CREATE INDEX raw_contacts_source_id_data_set_index ON raw_contacts " +
- "(sourceid, account_type, account_name, data_set);");
-
- db.execSQL("CREATE INDEX groups_source_id_data_set_index ON groups " +
- "(sourceid, account_type, account_name, data_set);");
- }
-
- private void upgradeToVersion612(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE calls ADD geocoded_location TEXT DEFAULT NULL;");
- // Old calls will not have a geocoded location; new calls will get it when inserted.
- }
-
- private void upgradeToVersion613(SQLiteDatabase db) {
- // The stream item and stream item photos APIs were not in-use by anyone in the time
- // between their initial creation (in v609) and this update. So we're just dropping
- // and re-creating them to get appropriate columns. The delta is as follows:
- // - In stream_items, package_id was replaced by res_package.
- // - In stream_item_photos, picture was replaced by photo_file_id.
- // - Instead of resource IDs for icon and label, we use resource name strings now
- // - Added sync columns
- // - Removed action and action_uri
- // - Text and comments are now nullable
-
- db.execSQL("DROP TABLE IF EXISTS stream_items");
- db.execSQL("DROP TABLE IF EXISTS stream_item_photos");
-
- db.execSQL("CREATE TABLE stream_items(" +
- "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
- "raw_contact_id INTEGER NOT NULL, " +
- "res_package TEXT, " +
- "icon TEXT, " +
- "label TEXT, " +
- "text TEXT, " +
- "timestamp INTEGER NOT NULL, " +
- "comments TEXT, " +
- "stream_item_sync1 TEXT, " +
- "stream_item_sync2 TEXT, " +
- "stream_item_sync3 TEXT, " +
- "stream_item_sync4 TEXT, " +
- "FOREIGN KEY(raw_contact_id) REFERENCES raw_contacts(_id));");
-
- db.execSQL("CREATE TABLE stream_item_photos(" +
- "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
- "stream_item_id INTEGER NOT NULL, " +
- "sort_index INTEGER, " +
- "photo_file_id INTEGER NOT NULL, " +
- "stream_item_photo_sync1 TEXT, " +
- "stream_item_photo_sync2 TEXT, " +
- "stream_item_photo_sync3 TEXT, " +
- "stream_item_photo_sync4 TEXT, " +
- "FOREIGN KEY(stream_item_id) REFERENCES stream_items(_id));");
- }
-
- private void upgradeToVersion615(SQLiteDatabase db) {
- // Old calls will not have up to date values for these columns, they will be filled in
- // as needed.
- db.execSQL("ALTER TABLE calls ADD lookup_uri TEXT DEFAULT NULL;");
- db.execSQL("ALTER TABLE calls ADD matched_number TEXT DEFAULT NULL;");
- db.execSQL("ALTER TABLE calls ADD normalized_number TEXT DEFAULT NULL;");
- db.execSQL("ALTER TABLE calls ADD photo_id INTEGER NOT NULL DEFAULT 0;");
- }
-
- private void upgradeToVersion618(SQLiteDatabase db) {
- // The Settings table needs a data_set column which technically should be part of the
- // primary key but can't be because it may be null. Since SQLite doesn't support nuking
- // the primary key, we'll drop the old table, re-create it, and copy the settings back in.
- db.execSQL("CREATE TEMPORARY TABLE settings_backup(" +
- "account_name STRING NOT NULL," +
- "account_type STRING NOT NULL," +
- "ungrouped_visible INTEGER NOT NULL DEFAULT 0," +
- "should_sync INTEGER NOT NULL DEFAULT 1" +
- ");");
- db.execSQL("INSERT INTO settings_backup " +
- "SELECT account_name, account_type, ungrouped_visible, should_sync" +
- " FROM settings");
- db.execSQL("DROP TABLE settings");
- db.execSQL("CREATE TABLE settings (" +
- "account_name STRING NOT NULL," +
- "account_type STRING NOT NULL," +
- "data_set STRING," +
- "ungrouped_visible INTEGER NOT NULL DEFAULT 0," +
- "should_sync INTEGER NOT NULL DEFAULT 1" +
- ");");
- db.execSQL("INSERT INTO settings " +
- "SELECT account_name, account_type, NULL, ungrouped_visible, should_sync " +
- "FROM settings_backup");
- db.execSQL("DROP TABLE settings_backup");
- }
-
- private void upgradeToVersion622(SQLiteDatabase db) {
- db.execSQL("ALTER TABLE calls ADD formatted_number TEXT DEFAULT NULL;");
- }
-
- private void upgradeToVersion626(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS accounts");
-
- db.execSQL("CREATE TABLE accounts (" +
- "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "account_name TEXT, " +
- "account_type TEXT, " +
- "data_set TEXT" +
- ");");
-
- // Add "account_id" column to groups and raw_contacts
- db.execSQL("ALTER TABLE raw_contacts ADD " +
- "account_id INTEGER REFERENCES accounts(_id)");
- db.execSQL("ALTER TABLE groups ADD " +
- "account_id INTEGER REFERENCES accounts(_id)");
-
- // Update indexes.
- db.execSQL("DROP INDEX IF EXISTS raw_contacts_source_id_index");
- db.execSQL("DROP INDEX IF EXISTS raw_contacts_source_id_data_set_index");
- db.execSQL("DROP INDEX IF EXISTS groups_source_id_index");
- db.execSQL("DROP INDEX IF EXISTS groups_source_id_data_set_index");
-
- db.execSQL("CREATE INDEX raw_contacts_source_id_account_id_index ON raw_contacts ("
- + "sourceid, account_id);");
- db.execSQL("CREATE INDEX groups_source_id_account_id_index ON groups ("
- + "sourceid, account_id);");
-
- // Migrate account_name/account_type/data_set to accounts table
-
- final Set<AccountWithDataSet> accountsWithDataSets = Sets.newHashSet();
- upgradeToVersion626_findAccountsWithDataSets(accountsWithDataSets, db, "raw_contacts");
- upgradeToVersion626_findAccountsWithDataSets(accountsWithDataSets, db, "groups");
-
- for (AccountWithDataSet accountWithDataSet : accountsWithDataSets) {
- db.execSQL("INSERT INTO accounts (account_name,account_type,data_set)VALUES(?, ?, ?)",
- new String[] {
- accountWithDataSet.getAccountName(),
- accountWithDataSet.getAccountType(),
- accountWithDataSet.getDataSet()
- });
- }
- upgradeToVersion626_fillAccountId(db, "raw_contacts");
- upgradeToVersion626_fillAccountId(db, "groups");
- }
-
- private static void upgradeToVersion626_findAccountsWithDataSets(
- Set<AccountWithDataSet> result, SQLiteDatabase db, String table) {
- Cursor c = db.rawQuery(
- "SELECT DISTINCT account_name, account_type, data_set FROM " + table, null);
- try {
- while (c.moveToNext()) {
- result.add(AccountWithDataSet.get(c.getString(0), c.getString(1), c.getString(2)));
- }
- } finally {
- c.close();
- }
- }
-
- private static void upgradeToVersion626_fillAccountId(SQLiteDatabase db, String table) {
- StringBuilder sb = new StringBuilder();
-
- // Set account_id and null out account_name, account_type and data_set
-
- sb.append("UPDATE " + table + " SET account_id = (SELECT _id FROM accounts WHERE ");
-
- addJoinExpressionAllowingNull(sb, table + ".account_name", "accounts.account_name");
- sb.append("AND");
- addJoinExpressionAllowingNull(sb, table + ".account_type", "accounts.account_type");
- sb.append("AND");
- addJoinExpressionAllowingNull(sb, table + ".data_set", "accounts.data_set");
-
- sb.append("), account_name = null, account_type = null, data_set = null");
- db.execSQL(sb.toString());
- }
-
private void upgradeToVersion701(SQLiteDatabase db) {
db.execSQL("UPDATE raw_contacts SET last_time_contacted =" +
" max(ifnull(last_time_contacted, 0), " +
@@ -4637,6 +3401,35 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
FastScrollingIndexCache.getInstance(mContext).invalidate();
}
+ private void upgradeToVersion1201(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE contacts ADD x_times_contacted INTEGER NOT NULL DEFAULT 0");
+ db.execSQL("ALTER TABLE contacts ADD x_last_time_contacted INTEGER");
+
+ db.execSQL("ALTER TABLE raw_contacts ADD x_times_contacted INTEGER NOT NULL DEFAULT 0");
+ db.execSQL("ALTER TABLE raw_contacts ADD x_last_time_contacted INTEGER");
+
+ db.execSQL("ALTER TABLE data_usage_stat ADD x_times_used INTEGER NOT NULL DEFAULT 0");
+ db.execSQL("ALTER TABLE data_usage_stat ADD x_last_time_used INTEGER NOT NULL DEFAULT 0");
+
+ db.execSQL("UPDATE contacts SET "
+ + "x_times_contacted = ifnull(times_contacted,0),"
+ + "x_last_time_contacted = ifnull(last_time_contacted,0),"
+ + "times_contacted = 0,"
+ + "last_time_contacted = 0");
+
+ db.execSQL("UPDATE raw_contacts SET "
+ + "x_times_contacted = ifnull(times_contacted,0),"
+ + "x_last_time_contacted = ifnull(last_time_contacted,0),"
+ + "times_contacted = 0,"
+ + "last_time_contacted = 0");
+
+ db.execSQL("UPDATE data_usage_stat SET "
+ + "x_times_used = ifnull(times_used,0),"
+ + "x_last_time_used = ifnull(last_time_used,0),"
+ + "times_used = 0,"
+ + "last_time_used = 0");
+ }
+
/**
* This method is only used in upgradeToVersion1101 method, and should not be used in other
* places now. Because data15 is not used to generate hash_id for photo, and the new generating
@@ -4711,20 +3504,52 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
return tokens[0].getAddress().trim();
}
- private static long lookupMimeTypeId(SQLiteDatabase db, String mimeType) {
- try {
- return DatabaseUtils.longForQuery(db,
- "SELECT " + MimetypesColumns._ID +
- " FROM " + Tables.MIMETYPES +
- " WHERE " + MimetypesColumns.MIMETYPE
- + "='" + mimeType + "'", null);
- } catch (SQLiteDoneException e) {
- // No rows of this type in the database.
- return -1;
+ /**
+ * Inserts a new mimetype into the table Tables.MIMETYPES and returns its id. Use
+ * {@link #lookupMimeTypeId(SQLiteDatabase, String)} to lookup id of a mimetype that is
+ * guaranteed to be in the database
+ *
+ * @param db the SQLiteDatabase object returned by {@link #getWritableDatabase()}
+ * @param mimeType The mimetype to insert
+ * @return the id of the newly inserted row
+ */
+ private long insertMimeType(SQLiteDatabase db, String mimeType) {
+ final String insert = "INSERT INTO " + Tables.MIMETYPES + "("
+ + MimetypesColumns.MIMETYPE +
+ ") VALUES (?)";
+ long id = insertWithOneArgAndReturnId(db, insert, mimeType);
+ if (id >= 0) {
+ return id;
+ }
+ return lookupMimeTypeId(db, mimeType);
+ }
+
+ /**
+ * Looks up Tables.MIMETYPES for the mime type and returns its id. Returns -1 if the mime type
+ * is not found. Use {@link #insertMimeType(SQLiteDatabase, String)} when it is doubtful whether
+ * the mimetype already exists in the table or not.
+ *
+ * @param db
+ * @param mimeType
+ * @return the id of the row containing the mime type or -1 if the mime type was not found.
+ */
+ private long lookupMimeTypeId(SQLiteDatabase db, String mimeType) {
+ Long id = mCommonMimeTypeIdsCache.get(mimeType);
+ if (id != null) {
+ return id;
}
+ final String query = "SELECT " +
+ MimetypesColumns._ID + " FROM " + Tables.MIMETYPES + " WHERE "
+ + MimetypesColumns.MIMETYPE +
+ "=?";
+ id = queryIdWithOneArg(db, query, mimeType);
+ if (id < 0) {
+ Log.e(TAG, "Mimetype " + mimeType + " not found in the MIMETYPES table");
+ }
+ return id;
}
- private void bindString(SQLiteStatement stmt, int index, String value) {
+ private static void bindString(SQLiteStatement stmt, int index, String value) {
if (value == null) {
stmt.bindNull(index);
} else {
@@ -4741,18 +3566,6 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
}
/**
- * Add a string like "(((column1) = (column2)) OR ((column1) IS NULL AND (column2) IS NULL))"
- */
- private static StringBuilder addJoinExpressionAllowingNull(
- StringBuilder sb, String column1, String column2) {
-
- sb.append("(((").append(column1).append(")=(").append(column2);
- sb.append("))OR((");
- sb.append(column1).append(") IS NULL AND (").append(column2).append(") IS NULL))");
- return sb;
- }
-
- /**
* Adds index stats into the SQLite database to force it to always use the lookup indexes.
*
* Note if you drop a table or an index, the corresponding row will be removed from this table.
@@ -4909,6 +3722,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
updateIndexStats(db, "search_index_segdir",
"sqlite_autoindex_search_index_segdir_1", "9 5 1");
+ updateIndexStats(db, Tables.PRESENCE, "presenceIndex", "1 1");
+ updateIndexStats(db, Tables.PRESENCE, "presenceIndex2", "1 1");
+ updateIndexStats(db, Tables.AGGREGATED_PRESENCE, null, "1");
+
// Force SQLite to reload sqlite_stat1.
db.execSQL("ANALYZE sqlite_master;");
} catch (SQLException e) {
@@ -4958,9 +3775,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("DELETE FROM " + Tables.DELETED_CONTACTS + ";");
db.execSQL("DELETE FROM " + Tables.MIMETYPES + ";");
db.execSQL("DELETE FROM " + Tables.PACKAGES + ";");
+ db.execSQL("DELETE FROM " + Tables.PRESENCE + ";");
+ db.execSQL("DELETE FROM " + Tables.AGGREGATED_PRESENCE + ";");
- initializeCache(db);
-
+ prepopulateCommonMimeTypes(db);
// Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
}
@@ -4990,53 +3808,11 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
}
}
- /**
- * Internal method used by {@link #getPackageId} and {@link #getMimeTypeId}.
- *
- * Note in the contacts provider we avoid using synchronization because it could risk deadlocks.
- * So here, instead of using locks, we use ConcurrentHashMap + retry.
- *
- * Note we can't use a transaction here becuause this method is called from
- * onCommitTransaction() too, unfortunately.
- */
- private static long getIdCached(SQLiteDatabase db, ConcurrentHashMap<String, Long> cache,
- String querySql, String insertSql, String value) {
- // First, try the in-memory cache.
- if (cache.containsKey(value)) {
- return cache.get(value);
- }
-
- // Then, try the database.
- long id = queryIdWithOneArg(db, querySql, value);
- if (id >= 0) {
- cache.put(value, id);
- return id;
- }
-
- // Not found in the database. Try inserting.
- id = insertWithOneArgAndReturnId(db, insertSql, value);
- if (id >= 0) {
- cache.put(value, id);
- return id;
- }
-
- // Insert failed, which means a race. Let's retry...
-
- // We log here to detect an infinity loop (which shouldn't happen).
- // Conflicts should be pretty rare, so it shouldn't spam logcat.
- Log.i(TAG, "Cache conflict detected: value=" + value);
- try {
- Thread.sleep(1); // Just wait a little bit before retry.
- } catch (InterruptedException ignore) {
- }
- return getIdCached(db, cache, querySql, insertSql, value);
- }
-
@VisibleForTesting
static long queryIdWithOneArg(SQLiteDatabase db, String sql, String sqlArgument) {
final SQLiteStatement query = db.compileStatement(sql);
try {
- DatabaseUtils.bindObjectToProgram(query, 1, sqlArgument);
+ bindString(query, 1, sqlArgument);
try {
return query.simpleQueryForLong();
} catch (SQLiteDoneException notFound) {
@@ -5051,7 +3827,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
static long insertWithOneArgAndReturnId(SQLiteDatabase db, String sql, String sqlArgument) {
final SQLiteStatement insert = db.compileStatement(sql);
try {
- DatabaseUtils.bindObjectToProgram(insert, 1, sqlArgument);
+ bindString(insert, 1, sqlArgument);
try {
return insert.executeInsert();
} catch (SQLiteConstraintException conflict) {
@@ -5076,57 +3852,59 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
"INSERT INTO " + Tables.PACKAGES + "("
+ PackagesColumns.PACKAGE +
") VALUES (?)";
- return getIdCached(getWritableDatabase(), mPackageCache, query, insert, packageName);
+
+ SQLiteDatabase db = getWritableDatabase();
+ long id = queryIdWithOneArg(db, query, packageName);
+ if (id >= 0) {
+ return id;
+ }
+ id = insertWithOneArgAndReturnId(db, insert, packageName);
+ if (id >= 0) {
+ return id;
+ }
+ // just in case there was a race while doing insert above
+ return queryIdWithOneArg(db, query, packageName);
}
/**
* Convert a mimetype into an integer, using {@link Tables#MIMETYPES} for
* lookups and possible allocation of new IDs as needed.
*/
- public long getMimeTypeId(String mimetype) {
- return lookupMimeTypeId(mimetype, getWritableDatabase());
- }
-
- private long lookupMimeTypeId(String mimetype, SQLiteDatabase db) {
- final String query =
- "SELECT " + MimetypesColumns._ID +
- " FROM " + Tables.MIMETYPES +
- " WHERE " + MimetypesColumns.MIMETYPE + "=?";
-
- final String insert =
- "INSERT INTO " + Tables.MIMETYPES + "("
- + MimetypesColumns.MIMETYPE +
- ") VALUES (?)";
-
- return getIdCached(db, mMimetypeCache, query, insert, mimetype);
+ public long getMimeTypeId(String mimeType) {
+ SQLiteDatabase db = getWritableDatabase();
+ long id = lookupMimeTypeId(db, mimeType);
+ if (id < 0) {
+ return insertMimeType(db, mimeType);
+ }
+ return id;
}
public long getMimeTypeIdForStructuredName() {
- return mMimeTypeIdStructuredName;
+ return lookupMimeTypeId(getWritableDatabase(), StructuredName.CONTENT_ITEM_TYPE);
}
public long getMimeTypeIdForStructuredPostal() {
- return mMimeTypeIdStructuredPostal;
+ return lookupMimeTypeId(getWritableDatabase(), StructuredPostal.CONTENT_ITEM_TYPE);
}
public long getMimeTypeIdForOrganization() {
- return mMimeTypeIdOrganization;
+ return lookupMimeTypeId(getWritableDatabase(), Organization.CONTENT_ITEM_TYPE);
}
public long getMimeTypeIdForIm() {
- return mMimeTypeIdIm;
+ return lookupMimeTypeId(getWritableDatabase(), Im.CONTENT_ITEM_TYPE);
}
public long getMimeTypeIdForEmail() {
- return mMimeTypeIdEmail;
+ return lookupMimeTypeId(getWritableDatabase(), Email.CONTENT_ITEM_TYPE);
}
public long getMimeTypeIdForPhone() {
- return mMimeTypeIdPhone;
+ return lookupMimeTypeId(getWritableDatabase(), Phone.CONTENT_ITEM_TYPE);
}
public long getMimeTypeIdForSip() {
- return mMimeTypeIdSip;
+ return lookupMimeTypeId(getWritableDatabase(), SipAddress.CONTENT_ITEM_TYPE);
}
/**
@@ -5137,19 +3915,19 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
* {@code STRUCTURED_PHONETIC_NAME}.
*/
private int getDisplayNameSourceForMimeTypeId(int mimeTypeId) {
- if (mimeTypeId == mMimeTypeIdStructuredName) {
+ if (mimeTypeId == mCommonMimeTypeIdsCache.get(StructuredName.CONTENT_ITEM_TYPE)) {
return DisplayNameSources.STRUCTURED_NAME;
}
- if (mimeTypeId == mMimeTypeIdEmail) {
+ if (mimeTypeId == mCommonMimeTypeIdsCache.get(Email.CONTENT_ITEM_TYPE)) {
return DisplayNameSources.EMAIL;
}
- if (mimeTypeId == mMimeTypeIdPhone) {
+ if (mimeTypeId == mCommonMimeTypeIdsCache.get(Phone.CONTENT_ITEM_TYPE)) {
return DisplayNameSources.PHONE;
}
- if (mimeTypeId == mMimeTypeIdOrganization) {
+ if (mimeTypeId == mCommonMimeTypeIdsCache.get(Organization.CONTENT_ITEM_TYPE)) {
return DisplayNameSources.ORGANIZATION;
}
- if (mimeTypeId == mMimeTypeIdNickname) {
+ if (mimeTypeId == mCommonMimeTypeIdsCache.get(Nickname.CONTENT_ITEM_TYPE)) {
return DisplayNameSources.NICKNAME;
}
return DisplayNameSources.UNDEFINED;
@@ -5159,35 +3937,25 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
* Find the mimetype for the given {@link Data#_ID}.
*/
public String getDataMimeType(long dataId) {
- if (mDataMimetypeQuery == null) {
- mDataMimetypeQuery = getWritableDatabase().compileStatement(
+ final SQLiteStatement dataMimetypeQuery = getWritableDatabase().compileStatement(
"SELECT " + MimetypesColumns.MIMETYPE +
" FROM " + Tables.DATA_JOIN_MIMETYPES +
" WHERE " + Tables.DATA + "." + Data._ID + "=?");
- }
try {
// Try database query to find mimetype
- DatabaseUtils.bindObjectToProgram(mDataMimetypeQuery, 1, dataId);
- String mimetype = mDataMimetypeQuery.simpleQueryForString();
- return mimetype;
+ dataMimetypeQuery.bindLong(1, dataId);
+ return dataMimetypeQuery.simpleQueryForString();
} catch (SQLiteDoneException e) {
// No valid mapping found, so return null
return null;
}
}
- public void invalidateAllCache() {
- Log.w(TAG, "invalidateAllCache: [" + getClass().getSimpleName() + "]");
-
- mMimetypeCache.clear();
- mPackageCache.clear();
- }
-
/**
* Gets all accounts in the accounts table.
*/
public Set<AccountWithDataSet> getAllAccountsWithDataSets() {
- final Set<AccountWithDataSet> result = Sets.newHashSet();
+ final ArraySet<AccountWithDataSet> result = new ArraySet<>();
Cursor c = getReadableDatabase().rawQuery(
"SELECT DISTINCT " + AccountsColumns._ID + "," + AccountsColumns.ACCOUNT_NAME +
"," + AccountsColumns.ACCOUNT_TYPE + "," + AccountsColumns.DATA_SET +
@@ -5351,14 +4119,12 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
}
public boolean isContactInDefaultDirectory(SQLiteDatabase db, long contactId) {
- if (mContactInDefaultDirectoryQuery == null) {
- mContactInDefaultDirectoryQuery = db.compileStatement(
+ final SQLiteStatement contactInDefaultDirectoryQuery = db.compileStatement(
"SELECT EXISTS (" +
"SELECT 1 FROM " + Tables.DEFAULT_DIRECTORY +
" WHERE " + Contacts._ID + "=?)");
- }
- mContactInDefaultDirectoryQuery.bindLong(1, contactId);
- return mContactInDefaultDirectoryQuery.simpleQueryForLong() != 0;
+ contactInDefaultDirectoryQuery.bindLong(1, contactId);
+ return contactInDefaultDirectoryQuery.simpleQueryForLong() != 0;
}
/**
@@ -5400,30 +4166,26 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
* Returns contact ID for the given contact or zero if it is NULL.
*/
public long getContactId(long rawContactId) {
- if (mContactIdQuery == null) {
- mContactIdQuery = getWritableDatabase().compileStatement(
+ final SQLiteStatement contactIdQuery = getWritableDatabase().compileStatement(
"SELECT " + RawContacts.CONTACT_ID +
" FROM " + Tables.RAW_CONTACTS +
" WHERE " + RawContacts._ID + "=?");
- }
try {
- DatabaseUtils.bindObjectToProgram(mContactIdQuery, 1, rawContactId);
- return mContactIdQuery.simpleQueryForLong();
+ contactIdQuery.bindLong(1, rawContactId);
+ return contactIdQuery.simpleQueryForLong();
} catch (SQLiteDoneException e) {
return 0; // No valid mapping found.
}
}
public int getAggregationMode(long rawContactId) {
- if (mAggregationModeQuery == null) {
- mAggregationModeQuery = getWritableDatabase().compileStatement(
+ final SQLiteStatement aggregationModeQuery = getWritableDatabase().compileStatement(
"SELECT " + RawContacts.AGGREGATION_MODE +
" FROM " + Tables.RAW_CONTACTS +
" WHERE " + RawContacts._ID + "=?");
- }
try {
- DatabaseUtils.bindObjectToProgram(mAggregationModeQuery, 1, rawContactId);
- return (int)mAggregationModeQuery.simpleQueryForLong();
+ aggregationModeQuery.bindLong(1, rawContactId);
+ return (int) aggregationModeQuery.simpleQueryForLong();
} catch (SQLiteDoneException e) {
return RawContacts.AGGREGATION_MODE_DISABLED; // No valid row found.
}
@@ -5580,7 +4342,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
return;
}
- SQLiteStatement nicknameLookupInsert = db.compileStatement("INSERT INTO "
+ final SQLiteStatement nicknameLookupInsert = db.compileStatement("INSERT INTO "
+ Tables.NICKNAME_LOOKUP + "(" + NicknameLookupColumns.NAME + ","
+ NicknameLookupColumns.CLUSTER + ") VALUES (?,?)");
@@ -5590,9 +4352,8 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
for (String name : names) {
String normalizedName = NameNormalizer.normalize(name);
try {
- DatabaseUtils.bindObjectToProgram(nicknameLookupInsert, 1, normalizedName);
- DatabaseUtils.bindObjectToProgram(
- nicknameLookupInsert, 2, String.valueOf(clusterId));
+ nicknameLookupInsert.bindString(1, normalizedName);
+ nicknameLookupInsert.bindString(2, String.valueOf(clusterId));
nicknameLookupInsert.executeInsert();
} catch (SQLiteException e) {
// Print the exception and keep going (this is not a fatal error).
@@ -5648,7 +4409,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
PropertyUtils.setProperty(getWritableDatabase(), key, value);
}
- public void clearDirectoryScanComplete() {
+ public void forceDirectoryRescan() {
setProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
}
@@ -5733,19 +4494,16 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
}
public void deleteStatusUpdate(long dataId) {
- if (mStatusUpdateDelete == null) {
- mStatusUpdateDelete = getWritableDatabase().compileStatement(
+ final SQLiteStatement statusUpdateDelete = getWritableDatabase().compileStatement(
"DELETE FROM " + Tables.STATUS_UPDATES +
" WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
- }
- mStatusUpdateDelete.bindLong(1, dataId);
- mStatusUpdateDelete.execute();
+ statusUpdateDelete.bindLong(1, dataId);
+ statusUpdateDelete.execute();
}
public void replaceStatusUpdate(Long dataId, long timestamp, String status, String resPackage,
Integer iconResource, Integer labelResource) {
- if (mStatusUpdateReplace == null) {
- mStatusUpdateReplace = getWritableDatabase().compileStatement(
+ final SQLiteStatement statusUpdateReplace = getWritableDatabase().compileStatement(
"INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "("
+ StatusUpdatesColumns.DATA_ID + ", "
+ StatusUpdates.STATUS_TIMESTAMP + ","
@@ -5754,20 +4512,18 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
+ StatusUpdates.STATUS_ICON + ","
+ StatusUpdates.STATUS_LABEL + ")" +
" VALUES (?,?,?,?,?,?)");
- }
- mStatusUpdateReplace.bindLong(1, dataId);
- mStatusUpdateReplace.bindLong(2, timestamp);
- bindString(mStatusUpdateReplace, 3, status);
- bindString(mStatusUpdateReplace, 4, resPackage);
- bindLong(mStatusUpdateReplace, 5, iconResource);
- bindLong(mStatusUpdateReplace, 6, labelResource);
- mStatusUpdateReplace.execute();
+ statusUpdateReplace.bindLong(1, dataId);
+ statusUpdateReplace.bindLong(2, timestamp);
+ bindString(statusUpdateReplace, 3, status);
+ bindString(statusUpdateReplace, 4, resPackage);
+ bindLong(statusUpdateReplace, 5, iconResource);
+ bindLong(statusUpdateReplace, 6, labelResource);
+ statusUpdateReplace.execute();
}
public void insertStatusUpdate(Long dataId, String status, String resPackage,
Integer iconResource, Integer labelResource) {
- if (mStatusUpdateInsert == null) {
- mStatusUpdateInsert = getWritableDatabase().compileStatement(
+ final SQLiteStatement statusUpdateInsert = getWritableDatabase().compileStatement(
"INSERT INTO " + Tables.STATUS_UPDATES + "("
+ StatusUpdatesColumns.DATA_ID + ", "
+ StatusUpdates.STATUS + ","
@@ -5775,45 +4531,41 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
+ StatusUpdates.STATUS_ICON + ","
+ StatusUpdates.STATUS_LABEL + ")" +
" VALUES (?,?,?,?,?)");
- }
try {
- mStatusUpdateInsert.bindLong(1, dataId);
- bindString(mStatusUpdateInsert, 2, status);
- bindString(mStatusUpdateInsert, 3, resPackage);
- bindLong(mStatusUpdateInsert, 4, iconResource);
- bindLong(mStatusUpdateInsert, 5, labelResource);
- mStatusUpdateInsert.executeInsert();
+ statusUpdateInsert.bindLong(1, dataId);
+ bindString(statusUpdateInsert, 2, status);
+ bindString(statusUpdateInsert, 3, resPackage);
+ bindLong(statusUpdateInsert, 4, iconResource);
+ bindLong(statusUpdateInsert, 5, labelResource);
+ statusUpdateInsert.executeInsert();
} catch (SQLiteConstraintException e) {
// The row already exists - update it
- if (mStatusUpdateAutoTimestamp == null) {
- mStatusUpdateAutoTimestamp = getWritableDatabase().compileStatement(
+ final SQLiteStatement statusUpdateAutoTimestamp = getWritableDatabase()
+ .compileStatement(
"UPDATE " + Tables.STATUS_UPDATES +
" SET " + StatusUpdates.STATUS_TIMESTAMP + "=?,"
+ StatusUpdates.STATUS + "=?" +
" WHERE " + StatusUpdatesColumns.DATA_ID + "=?"
+ " AND " + StatusUpdates.STATUS + "!=?");
- }
long timestamp = System.currentTimeMillis();
- mStatusUpdateAutoTimestamp.bindLong(1, timestamp);
- bindString(mStatusUpdateAutoTimestamp, 2, status);
- mStatusUpdateAutoTimestamp.bindLong(3, dataId);
- bindString(mStatusUpdateAutoTimestamp, 4, status);
- mStatusUpdateAutoTimestamp.execute();
-
- if (mStatusAttributionUpdate == null) {
- mStatusAttributionUpdate = getWritableDatabase().compileStatement(
+ statusUpdateAutoTimestamp.bindLong(1, timestamp);
+ bindString(statusUpdateAutoTimestamp, 2, status);
+ statusUpdateAutoTimestamp.bindLong(3, dataId);
+ bindString(statusUpdateAutoTimestamp, 4, status);
+ statusUpdateAutoTimestamp.execute();
+
+ final SQLiteStatement statusAttributionUpdate = getWritableDatabase().compileStatement(
"UPDATE " + Tables.STATUS_UPDATES +
" SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?,"
+ StatusUpdates.STATUS_ICON + "=?,"
+ StatusUpdates.STATUS_LABEL + "=?" +
" WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
- }
- bindString(mStatusAttributionUpdate, 1, resPackage);
- bindLong(mStatusAttributionUpdate, 2, iconResource);
- bindLong(mStatusAttributionUpdate, 3, labelResource);
- mStatusAttributionUpdate.bindLong(4, dataId);
- mStatusAttributionUpdate.execute();
+ bindString(statusAttributionUpdate, 1, resPackage);
+ bindLong(statusAttributionUpdate, 2, iconResource);
+ bindLong(statusAttributionUpdate, 3, labelResource);
+ statusAttributionUpdate.bindLong(4, dataId);
+ statusAttributionUpdate.execute();
}
}
@@ -6013,8 +4765,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
: localeUtils.getBucketIndex(sortKeyAlternative);
String phonebookLabelAlternative = localeUtils.getBucketLabel(phonebookBucketAlternative);
- if (mRawContactDisplayNameUpdate == null) {
- mRawContactDisplayNameUpdate = db.compileStatement(
+ final SQLiteStatement rawContactDisplayNameUpdate = db.compileStatement(
"UPDATE " + Tables.RAW_CONTACTS +
" SET " +
RawContacts.DISPLAY_NAME_SOURCE + "=?," +
@@ -6029,21 +4780,20 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + "=?," +
RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + "=?" +
" WHERE " + RawContacts._ID + "=?");
- }
- mRawContactDisplayNameUpdate.bindLong(1, bestDisplayNameSource);
- bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary);
- bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative);
- bindString(mRawContactDisplayNameUpdate, 4, bestPhoneticName);
- mRawContactDisplayNameUpdate.bindLong(5, bestPhoneticNameStyle);
- bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary);
- bindString(mRawContactDisplayNameUpdate, 7, phonebookLabelPrimary);
- mRawContactDisplayNameUpdate.bindLong(8, phonebookBucketPrimary);
- bindString(mRawContactDisplayNameUpdate, 9, sortKeyAlternative);
- bindString(mRawContactDisplayNameUpdate, 10, phonebookLabelAlternative);
- mRawContactDisplayNameUpdate.bindLong(11, phonebookBucketAlternative);
- mRawContactDisplayNameUpdate.bindLong(12, rawContactId);
- mRawContactDisplayNameUpdate.execute();
+ rawContactDisplayNameUpdate.bindLong(1, bestDisplayNameSource);
+ bindString(rawContactDisplayNameUpdate, 2, displayNamePrimary);
+ bindString(rawContactDisplayNameUpdate, 3, displayNameAlternative);
+ bindString(rawContactDisplayNameUpdate, 4, bestPhoneticName);
+ rawContactDisplayNameUpdate.bindLong(5, bestPhoneticNameStyle);
+ bindString(rawContactDisplayNameUpdate, 6, sortKeyPrimary);
+ bindString(rawContactDisplayNameUpdate, 7, phonebookLabelPrimary);
+ rawContactDisplayNameUpdate.bindLong(8, phonebookBucketPrimary);
+ bindString(rawContactDisplayNameUpdate, 9, sortKeyAlternative);
+ bindString(rawContactDisplayNameUpdate, 10, phonebookLabelAlternative);
+ rawContactDisplayNameUpdate.bindLong(11, phonebookBucketAlternative);
+ rawContactDisplayNameUpdate.bindLong(12, rawContactId);
+ rawContactDisplayNameUpdate.execute();
}
/**
@@ -6054,17 +4804,15 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
* flag of all data items of this raw contacts
*/
public void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
- if (mSetPrimaryStatement == null) {
- mSetPrimaryStatement = getWritableDatabase().compileStatement(
+ final SQLiteStatement setPrimaryStatement = getWritableDatabase().compileStatement(
"UPDATE " + Tables.DATA +
" SET " + Data.IS_PRIMARY + "=(_id=?)" +
" WHERE " + DataColumns.MIMETYPE_ID + "=?" +
" AND " + Data.RAW_CONTACT_ID + "=?");
- }
- mSetPrimaryStatement.bindLong(1, dataId);
- mSetPrimaryStatement.bindLong(2, mimeTypeId);
- mSetPrimaryStatement.bindLong(3, rawContactId);
- mSetPrimaryStatement.execute();
+ setPrimaryStatement.bindLong(1, dataId);
+ setPrimaryStatement.bindLong(2, mimeTypeId);
+ setPrimaryStatement.bindLong(3, rawContactId);
+ setPrimaryStatement.execute();
}
/**
@@ -6072,16 +4820,14 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
* other raw contacts of the same joined aggregate
*/
public void clearSuperPrimary(long rawContactId, long mimeTypeId) {
- if (mClearSuperPrimaryStatement == null) {
- mClearSuperPrimaryStatement = getWritableDatabase().compileStatement(
+ final SQLiteStatement clearSuperPrimaryStatement = getWritableDatabase().compileStatement(
"UPDATE " + Tables.DATA +
" SET " + Data.IS_SUPER_PRIMARY + "=0" +
" WHERE " + DataColumns.MIMETYPE_ID + "=?" +
" AND " + Data.RAW_CONTACT_ID + "=?");
- }
- mClearSuperPrimaryStatement.bindLong(1, mimeTypeId);
- mClearSuperPrimaryStatement.bindLong(2, rawContactId);
- mClearSuperPrimaryStatement.execute();
+ clearSuperPrimaryStatement.bindLong(1, mimeTypeId);
+ clearSuperPrimaryStatement.bindLong(2, rawContactId);
+ clearSuperPrimaryStatement.execute();
}
/**
@@ -6091,8 +4837,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
* @param dataId the id of the data record to be set to primary.
*/
public void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
- if (mSetSuperPrimaryStatement == null) {
- mSetSuperPrimaryStatement = getWritableDatabase().compileStatement(
+ final SQLiteStatement setSuperPrimaryStatement = getWritableDatabase().compileStatement(
"UPDATE " + Tables.DATA +
" SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" +
" WHERE " + DataColumns.MIMETYPE_ID + "=?" +
@@ -6103,11 +4848,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
"SELECT " + RawContacts.CONTACT_ID +
" FROM " + Tables.RAW_CONTACTS +
" WHERE " + RawContacts._ID + "=?))");
- }
- mSetSuperPrimaryStatement.bindLong(1, dataId);
- mSetSuperPrimaryStatement.bindLong(2, mimeTypeId);
- mSetSuperPrimaryStatement.bindLong(3, rawContactId);
- mSetSuperPrimaryStatement.execute();
+ setSuperPrimaryStatement.bindLong(1, dataId);
+ setSuperPrimaryStatement.bindLong(2, mimeTypeId);
+ setSuperPrimaryStatement.bindLong(3, rawContactId);
+ setSuperPrimaryStatement.execute();
}
/**
@@ -6118,33 +4862,29 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
return;
}
- if (mNameLookupInsert == null) {
- mNameLookupInsert = getWritableDatabase().compileStatement(
+ final SQLiteStatement nameLookupInsert = getWritableDatabase().compileStatement(
"INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
+ NameLookupColumns.RAW_CONTACT_ID + ","
+ NameLookupColumns.DATA_ID + ","
+ NameLookupColumns.NAME_TYPE + ","
+ NameLookupColumns.NORMALIZED_NAME
+ ") VALUES (?,?,?,?)");
- }
- mNameLookupInsert.bindLong(1, rawContactId);
- mNameLookupInsert.bindLong(2, dataId);
- mNameLookupInsert.bindLong(3, lookupType);
- bindString(mNameLookupInsert, 4, name);
- mNameLookupInsert.executeInsert();
+ nameLookupInsert.bindLong(1, rawContactId);
+ nameLookupInsert.bindLong(2, dataId);
+ nameLookupInsert.bindLong(3, lookupType);
+ bindString(nameLookupInsert, 4, name);
+ nameLookupInsert.executeInsert();
}
/**
* Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
*/
public void deleteNameLookup(long dataId) {
- if (mNameLookupDelete == null) {
- mNameLookupDelete = getWritableDatabase().compileStatement(
+ final SQLiteStatement nameLookupDelete = getWritableDatabase().compileStatement(
"DELETE FROM " + Tables.NAME_LOOKUP +
" WHERE " + NameLookupColumns.DATA_ID + "=?");
- }
- mNameLookupDelete.bindLong(1, dataId);
- mNameLookupDelete.execute();
+ nameLookupDelete.bindLong(1, dataId);
+ nameLookupDelete.execute();
}
public String insertNameLookupForEmail(long rawContactId, long dataId, String email) {
@@ -6224,20 +4964,125 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper {
}
public long upsertMetadataSync(String backupId, Long accountId, String data, Integer deleted) {
- if (mMetadataSyncInsert == null) {
- mMetadataSyncInsert = getWritableDatabase().compileStatement(
+ final SQLiteStatement metadataSyncInsert = getWritableDatabase().compileStatement(
"INSERT OR REPLACE INTO " + Tables.METADATA_SYNC + "("
+ MetadataSync.RAW_CONTACT_BACKUP_ID + ", "
+ MetadataSyncColumns.ACCOUNT_ID + ", "
+ MetadataSync.DATA + ","
+ MetadataSync.DELETED + ")" +
" VALUES (?,?,?,?)");
- }
- mMetadataSyncInsert.bindString(1, backupId);
- mMetadataSyncInsert.bindLong(2, accountId);
+ metadataSyncInsert.bindString(1, backupId);
+ metadataSyncInsert.bindLong(2, accountId);
data = (data == null) ? "" : data;
- mMetadataSyncInsert.bindString(3, data);
- mMetadataSyncInsert.bindLong(4, deleted);
- return mMetadataSyncInsert.executeInsert();
+ metadataSyncInsert.bindString(3, data);
+ metadataSyncInsert.bindLong(4, deleted);
+ return metadataSyncInsert.executeInsert();
+ }
+
+ public static void notifyProviderStatusChange(Context context) {
+ context.getContentResolver().notifyChange(ProviderStatus.CONTENT_URI,
+ /* observer= */ null, /* syncToNetwork= */ false);
+ }
+
+ public long getDatabaseCreationTime() {
+ return mDatabaseCreationTime;
+ }
+
+ private SqlChecker mCachedSqlChecker;
+
+ private SqlChecker getSqlChecker() {
+ // No need for synchronization on mCachedSqlChecker, because worst-case we'll just
+ // initialize it twice.
+ if (mCachedSqlChecker != null) {
+ return mCachedSqlChecker;
+ }
+ final ArrayList<String> invalidTokens = new ArrayList<>();
+
+ if (DISALLOW_SUB_QUERIES) {
+ // Disallow referring to tables and views. However, we exempt tables whose names are
+ // also used as column names of any tables. (Right now it's only 'data'.)
+ invalidTokens.addAll(
+ DatabaseAnalyzer.findTableViewsAllowingColumns(getReadableDatabase()));
+
+ // Disallow token "select" to disallow subqueries.
+ invalidTokens.add("select");
+
+ // Allow the use of "default_directory" for now, as it used to be sort of commonly used...
+ invalidTokens.remove(Tables.DEFAULT_DIRECTORY.toLowerCase());
+ }
+
+ mCachedSqlChecker = new SqlChecker(invalidTokens);
+
+ return mCachedSqlChecker;
+ }
+
+ /**
+ * Ensure (a piece of) SQL is valid and doesn't contain disallowed tokens.
+ */
+ public void validateSql(String callerPackage, String sqlPiece) {
+ // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
+ runSqlValidation(callerPackage, new Runnable() {
+ @Override
+ public void run() {
+ ContactsDatabaseHelper.this.getSqlChecker().ensureNoInvalidTokens(sqlPiece);
+ }
+ });
+ }
+
+ /**
+ * Ensure all keys in {@code values} are valid. (i.e. they're all single token.)
+ */
+ public void validateContentValues(String callerPackage, ContentValues values) {
+ // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
+ runSqlValidation(callerPackage, new Runnable() {
+ @Override
+ public void run() {
+ for (String key : values.keySet()) {
+ ContactsDatabaseHelper.this.getSqlChecker().ensureSingleTokenOnly(key);
+ }
+ }
+ });
+ }
+
+ /**
+ * Ensure all column names in {@code projection} are valid. (i.e. they're all single token.)
+ */
+ public void validateProjection(String callerPackage, String[] projection) {
+ // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
+ if (projection != null) {
+ runSqlValidation(callerPackage, new Runnable() {
+ @Override
+ public void run() {
+ for (String column : projection) {
+ ContactsDatabaseHelper.this.getSqlChecker().ensureSingleTokenOnly(column);
+ }
+ }
+ });
+ }
+ }
+
+ private void runSqlValidation(String callerPackage, Runnable r) {
+ try {
+ r.run();
+ } catch (InvalidSqlException e) {
+ reportInvalidSql(callerPackage, e);
+ }
+ }
+
+ private void reportInvalidSql(String callerPackage, InvalidSqlException e) {
+ Log.e(TAG, String.format("%s caller=%s", e.getMessage(), callerPackage));
+ throw e;
+ }
+
+ /**
+ * Calls WTF without crashing, so we can collect errors in the wild. During unit tests, it'll
+ * log only.
+ */
+ public void logWtf(String message) {
+ if (mIsTestInstance) {
+ Slog.w(TAG, "[Test mode, warning only] " + message);
+ } else {
+ Slog.wtfStack(TAG, message);
+ }
}
}
diff --git a/src/com/android/providers/contacts/ContactsPackageMonitor.java b/src/com/android/providers/contacts/ContactsPackageMonitor.java
new file mode 100644
index 00000000..06565cd9
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactsPackageMonitor.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2017 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.BroadcastReceiver.PendingResult;
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.provider.ContactsContract;
+import android.provider.VoicemailContract;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.providers.contacts.util.PackageUtils;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * - Handles package related broadcasts.
+ * - Also scan changed packages while the process wasn't running using PM.getChangedPackages().
+ */
+public class ContactsPackageMonitor {
+ private static final String TAG = "ContactsPackageMonitor";
+
+ private static final boolean VERBOSE_LOGGING = AbstractContactsProvider.VERBOSE_LOGGING;
+
+ private static final int BACKGROUND_TASK_PACKAGE_EVENT = 0;
+
+ private static ContactsPackageMonitor sInstance;
+
+ private Context mContext;
+
+ /** We run all BG tasks on this thread/handler sequentially. */
+ private final ContactsTaskScheduler mTaskScheduler;
+
+ private static class PackageEventArg {
+ final String packageName;
+ final PendingResult broadcastPendingResult;
+
+ private PackageEventArg(String packageName, PendingResult broadcastPendingResult) {
+ this.packageName = packageName;
+ this.broadcastPendingResult = broadcastPendingResult;
+ }
+ }
+
+ private ContactsPackageMonitor(Context context) {
+ mContext = context; // Can't use the app context due to a bug with shared process.
+
+ // Start the BG thread and register the receiver.
+ mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
+ @Override
+ public void onPerformTask(int taskId, Object arg) {
+ switch (taskId) {
+ case BACKGROUND_TASK_PACKAGE_EVENT:
+ onPackageChanged((PackageEventArg) arg);
+ break;
+ }
+ }
+ };
+ }
+
+ private void start() {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Starting... user="
+ + android.os.Process.myUserHandle().getIdentifier());
+ }
+
+ registerReceiver();
+ }
+
+ public static synchronized void start(Context context) {
+ if (sInstance == null) {
+ sInstance = new ContactsPackageMonitor(context);
+ sInstance.start();
+ }
+ }
+
+ private void registerReceiver() {
+ final IntentFilter filter = new IntentFilter();
+
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addDataScheme("package");
+
+ mContext.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getData() == null) {
+ return; // Shouldn't happen.
+ }
+ final String changedPackage = intent.getData().getSchemeSpecificPart();
+ final PendingResult result = goAsync();
+
+ mTaskScheduler.scheduleTask(BACKGROUND_TASK_PACKAGE_EVENT,
+ new PackageEventArg(changedPackage, result));
+ }
+ }, filter);
+ }
+
+ private void onPackageChanged(PackageEventArg arg) {
+ try {
+ final String packageName = arg.packageName;
+ if (TextUtils.isEmpty(packageName)) {
+ Log.w(TAG, "Empty package name detected.");
+ return;
+ }
+ if (VERBOSE_LOGGING) Log.d(TAG, "onPackageChanged: Scanning package: " + packageName);
+
+ // First, tell CP2.
+ final ContactsProvider2 provider = getProvider(mContext, ContactsContract.AUTHORITY);
+ if (provider != null) {
+ provider.onPackageChanged(packageName);
+ }
+
+ // Next, if the package is gone, clean up the voicemail.
+ cleanupVoicemail(mContext, packageName);
+ } finally {
+ if (VERBOSE_LOGGING) Log.v(TAG, "Calling PendingResult.finish()...");
+ arg.broadcastPendingResult.finish();
+ }
+ }
+
+ @VisibleForTesting
+ static void cleanupVoicemail(Context context, String packageName) {
+ if (PackageUtils.isPackageInstalled(context, packageName)) {
+ return; // Still installed.
+ }
+ if (VERBOSE_LOGGING) Log.d(TAG, "Cleaning up data for package: " + packageName);
+
+ // Delete both voicemail content and voicemail status entries for this package.
+ final VoicemailContentProvider provider = getProvider(context, VoicemailContract.AUTHORITY);
+ if (provider != null) {
+ provider.removeBySourcePackage(packageName);
+ }
+ }
+
+ private static <T extends ContentProvider> T getProvider(Context context, String authority) {
+ final IContentProvider iprovider = context.getContentResolver().acquireProvider(authority);
+ final ContentProvider provider = ContentProvider.coerceToLocalContentProvider(iprovider);
+ if (provider != null) {
+ return (T) provider;
+ }
+ Slog.wtf(TAG, "Provider for " + authority + " not found");
+ return null;
+ }
+}
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index d47ddcc7..4feb71e6 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -20,6 +20,7 @@ import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdateListener;
import android.annotation.Nullable;
+import android.annotation.WorkerThread;
import android.app.AppOpsManager;
import android.app.SearchManager;
import android.content.ContentProviderOperation;
@@ -56,16 +57,13 @@ import android.os.AsyncTask;
import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
-import android.os.Process;
import android.os.RemoteException;
import android.os.StrictMode;
import android.os.SystemClock;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
@@ -220,13 +218,13 @@ public class ContactsProvider2 extends AbstractContactsProvider
private static final String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS";
/* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
- "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
- " ifnull(" + Contacts.TIMES_CONTACTED + ",0)+1" +
+ "UPDATE " + Tables.CONTACTS + " SET " + Contacts.RAW_TIMES_CONTACTED + "=" +
+ " ifnull(" + Contacts.RAW_TIMES_CONTACTED + ",0)+1" +
" WHERE " + Contacts._ID + "=?";
/* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
- "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
- " ifnull(" + RawContacts.TIMES_CONTACTED + ",0)+1 " +
+ "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.RAW_TIMES_CONTACTED + "=" +
+ " ifnull(" + RawContacts.RAW_TIMES_CONTACTED + ",0)+1 " +
" WHERE " + RawContacts.CONTACT_ID + "=?";
/* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK";
@@ -251,10 +249,10 @@ public class ContactsProvider2 extends AbstractContactsProvider
private static final int BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM = 5;
private static final int BACKGROUND_TASK_UPDATE_SEARCH_INDEX = 6;
private static final int BACKGROUND_TASK_UPDATE_PROVIDER_STATUS = 7;
- private static final int BACKGROUND_TASK_UPDATE_DIRECTORIES = 8;
private static final int BACKGROUND_TASK_CHANGE_LOCALE = 9;
private static final int BACKGROUND_TASK_CLEANUP_PHOTOS = 10;
private static final int BACKGROUND_TASK_CLEAN_DELETE_LOG = 11;
+ private static final int BACKGROUND_TASK_RESCAN_DIRECTORY = 12;
protected static final int STATUS_NORMAL = 0;
protected static final int STATUS_UPGRADING = 1;
@@ -313,7 +311,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
public static final ProfileAwareUriMatcher sUriMatcher =
new ProfileAwareUriMatcher(UriMatcher.NO_MATCH);
- private static final String FREQUENT_ORDER_BY = DataUsageStatColumns.TIMES_USED + " DESC,"
+ private static final String FREQUENT_ORDER_BY = DataUsageStatColumns.RAW_TIMES_USED + " DESC,"
+ Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
public static final int CONTACTS = 1000;
@@ -610,20 +608,23 @@ public class ContactsProvider2 extends AbstractContactsProvider
// Contacts contacted within the last 30 days (in seconds)
private static final long LAST_TIME_USED_30_DAYS_SEC = 30L * 24 * 60 * 60;
- private static final String TIME_SINCE_LAST_USED_SEC =
- "(strftime('%s', 'now') - " + DataUsageStatColumns.LAST_TIME_USED + "/1000)";
+ private static final String RAW_TIME_SINCE_LAST_USED_SEC =
+ "(strftime('%s', 'now') - " + DataUsageStatColumns.RAW_LAST_TIME_USED + "/1000)";
+
+ private static final String LR_TIME_SINCE_LAST_USED_SEC =
+ "(strftime('%s', 'now') - " + DataUsageStatColumns.LR_LAST_TIME_USED + "/1000)";
private static final String SORT_BY_DATA_USAGE =
- "(CASE WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_3_DAYS_SEC +
+ "(CASE WHEN " + RAW_TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_3_DAYS_SEC +
" THEN 0 " +
- " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_7_DAYS_SEC +
+ " WHEN " + RAW_TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_7_DAYS_SEC +
" THEN 1 " +
- " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_14_DAYS_SEC +
+ " WHEN " + RAW_TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_14_DAYS_SEC +
" THEN 2 " +
- " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_30_DAYS_SEC +
+ " WHEN " + RAW_TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_30_DAYS_SEC +
" THEN 3 " +
" ELSE 4 END), " +
- DataUsageStatColumns.TIMES_USED + " DESC";
+ DataUsageStatColumns.RAW_TIMES_USED + " DESC";
/*
* Sorting order for email address suggestions: first starred, then the rest.
@@ -676,7 +677,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
.add(Contacts.DISPLAY_NAME_SOURCE)
.add(Contacts.IN_DEFAULT_DIRECTORY)
.add(Contacts.IN_VISIBLE_GROUP)
- .add(Contacts.LAST_TIME_CONTACTED)
+ .add(Contacts.LR_LAST_TIME_CONTACTED)
.add(Contacts.LOOKUP_KEY)
.add(Contacts.PHONETIC_NAME)
.add(Contacts.PHONETIC_NAME_STYLE)
@@ -693,7 +694,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
.add(ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE)
.add(Contacts.STARRED)
.add(Contacts.PINNED)
- .add(Contacts.TIMES_CONTACTED)
+ .add(Contacts.LR_TIMES_CONTACTED)
.add(Contacts.HAS_PHONE_NUMBER)
.add(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP)
.build();
@@ -794,8 +795,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
.build();
private static final ProjectionMap sDataUsageColumns = ProjectionMap.builder()
- .add(Data.TIMES_USED, Tables.DATA_USAGE_STAT + "." + Data.TIMES_USED)
- .add(Data.LAST_TIME_USED, Tables.DATA_USAGE_STAT + "." + Data.LAST_TIME_USED)
+ .add(Data.LR_TIMES_USED, Tables.DATA_USAGE_STAT + "." + Data.LR_TIMES_USED)
+ .add(Data.LR_LAST_TIME_USED, Tables.DATA_USAGE_STAT + "." + Data.LR_LAST_TIME_USED)
.build();
/** Contains just BaseColumns._COUNT */
@@ -822,16 +823,18 @@ public class ContactsProvider2 extends AbstractContactsProvider
/** Used for pushing starred contacts to the top of a times contacted list **/
private static final ProjectionMap sStrequentStarredProjectionMap = ProjectionMap.builder()
.addAll(sContactsProjectionMap)
- .add(DataUsageStatColumns.TIMES_USED, String.valueOf(Long.MAX_VALUE))
- .add(DataUsageStatColumns.LAST_TIME_USED, String.valueOf(Long.MAX_VALUE))
+ .add(DataUsageStatColumns.LR_TIMES_USED, String.valueOf(Long.MAX_VALUE))
+ .add(DataUsageStatColumns.LR_LAST_TIME_USED, String.valueOf(Long.MAX_VALUE))
.build();
private static final ProjectionMap sStrequentFrequentProjectionMap = ProjectionMap.builder()
.addAll(sContactsProjectionMap)
- .add(DataUsageStatColumns.TIMES_USED,
- "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED + ")")
- .add(DataUsageStatColumns.LAST_TIME_USED,
- "MAX(" + DataUsageStatColumns.CONCRETE_LAST_TIME_USED + ")")
+ // Note this should ideally be "lowres(SUM)" rather than "SUM(lowres)", but we do it
+ // this way for performance reasons.
+ .add(DataUsageStatColumns.LR_TIMES_USED,
+ "SUM(" + DataUsageStatColumns.CONCRETE_LR_TIMES_USED + ")")
+ .add(DataUsageStatColumns.LR_LAST_TIME_USED,
+ "MAX(" + DataUsageStatColumns.CONCRETE_LR_LAST_TIME_USED + ")")
.build();
/**
@@ -843,8 +846,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
private static final ProjectionMap sStrequentPhoneOnlyProjectionMap
= ProjectionMap.builder()
.addAll(sContactsProjectionMap)
- .add(DataUsageStatColumns.TIMES_USED, DataUsageStatColumns.CONCRETE_TIMES_USED)
- .add(DataUsageStatColumns.LAST_TIME_USED, DataUsageStatColumns.CONCRETE_LAST_TIME_USED)
+ .add(DataUsageStatColumns.LR_TIMES_USED)
+ .add(DataUsageStatColumns.LR_LAST_TIME_USED)
.add(Phone.NUMBER)
.add(Phone.TYPE)
.add(Phone.LABEL)
@@ -876,8 +879,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
.add(RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY)
.add(RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE)
.add(RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE)
- .add(RawContacts.TIMES_CONTACTED)
- .add(RawContacts.LAST_TIME_CONTACTED)
+ .add(RawContacts.LR_TIMES_CONTACTED)
+ .add(RawContacts.LR_LAST_TIME_CONTACTED)
.add(RawContacts.CUSTOM_RINGTONE)
.add(RawContacts.SEND_TO_VOICEMAIL)
.add(RawContacts.STARRED)
@@ -976,9 +979,16 @@ public class ContactsProvider2 extends AbstractContactsProvider
.add(PhoneLookup.CONTACT_ID, "contacts_view." + Contacts._ID)
.add(PhoneLookup.DATA_ID, PhoneLookup.DATA_ID)
.add(PhoneLookup.LOOKUP_KEY, "contacts_view." + Contacts.LOOKUP_KEY)
+ .add(PhoneLookup.DISPLAY_NAME_SOURCE, "contacts_view." + Contacts.DISPLAY_NAME_SOURCE)
.add(PhoneLookup.DISPLAY_NAME, "contacts_view." + Contacts.DISPLAY_NAME)
- .add(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED)
- .add(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED)
+ .add(PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
+ "contacts_view." + Contacts.DISPLAY_NAME_ALTERNATIVE)
+ .add(PhoneLookup.PHONETIC_NAME, "contacts_view." + Contacts.PHONETIC_NAME)
+ .add(PhoneLookup.PHONETIC_NAME_STYLE, "contacts_view." + Contacts.PHONETIC_NAME_STYLE)
+ .add(PhoneLookup.SORT_KEY_PRIMARY, "contacts_view." + Contacts.SORT_KEY_PRIMARY)
+ .add(PhoneLookup.SORT_KEY_ALTERNATIVE, "contacts_view." + Contacts.SORT_KEY_ALTERNATIVE)
+ .add(PhoneLookup.LR_LAST_TIME_CONTACTED, "contacts_view." + Contacts.LR_LAST_TIME_CONTACTED)
+ .add(PhoneLookup.LR_TIMES_CONTACTED, "contacts_view." + Contacts.LR_TIMES_CONTACTED)
.add(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED)
.add(PhoneLookup.IN_DEFAULT_DIRECTORY, "contacts_view." + Contacts.IN_DEFAULT_DIRECTORY)
.add(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP)
@@ -1519,8 +1529,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
private LocaleSet mCurrentLocales;
private int mContactsAccountCount;
- private HandlerThread mBackgroundThread;
- private Handler mBackgroundHandler;
+ private ContactsTaskScheduler mTaskScheduler;
private long mLastPhotoCleanup = 0;
@@ -1539,6 +1548,11 @@ public class ContactsProvider2 extends AbstractContactsProvider
@Override
public boolean onCreate() {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "onCreate user="
+ + android.os.Process.myUserHandle().getIdentifier());
+ }
+
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate start");
}
@@ -1575,7 +1589,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
mMetadataSyncEnabled = android.provider.Settings.Global.getInt(
getContext().getContentResolver(), Global.CONTACT_METADATA_SYNC_ENABLED, 0) == 1;
- mContactsHelper = getDatabaseHelper(getContext());
+ mContactsHelper = getDatabaseHelper();
mDbHelper.set(mContactsHelper);
// Set up the DB helper for keeping transactions serialized.
@@ -1588,13 +1602,10 @@ public class ContactsProvider2 extends AbstractContactsProvider
mReadAccessLatch = new CountDownLatch(1);
mWriteAccessLatch = new CountDownLatch(1);
- mBackgroundThread = new HandlerThread("ContactsProviderWorker",
- Process.THREAD_PRIORITY_BACKGROUND);
- mBackgroundThread.start();
- mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
+ mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
@Override
- public void handleMessage(Message msg) {
- performBackgroundTask(msg.what, msg.obj);
+ public void onPerformTask(int taskId, Object arg) {
+ performBackgroundTask(taskId, arg);
}
};
@@ -1604,7 +1615,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
ProviderInfo profileInfo = new ProviderInfo();
profileInfo.authority = ContactsContract.AUTHORITY;
mProfileProvider.attachInfo(getContext(), profileInfo);
- mProfileHelper = mProfileProvider.getDatabaseHelper(getContext());
+ mProfileHelper = mProfileProvider.getDatabaseHelper();
mEnterprisePolicyGuard = new EnterprisePolicyGuard(getContext());
// Initialize the pre-authorized URI duration.
@@ -1620,6 +1631,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
scheduleBackgroundTask(BACKGROUND_TASK_CLEAN_DELETE_LOG);
+ ContactsPackageMonitor.start(getContext());
+
return true;
}
@@ -1720,11 +1733,11 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
protected void scheduleBackgroundTask(int task) {
- mBackgroundHandler.sendEmptyMessage(task);
+ scheduleBackgroundTask(task, null);
}
protected void scheduleBackgroundTask(int task, Object arg) {
- mBackgroundHandler.sendMessage(mBackgroundHandler.obtainMessage(task, arg));
+ mTaskScheduler.scheduleTask(task, arg);
}
protected void performBackgroundTask(int task, Object arg) {
@@ -1767,6 +1780,11 @@ public class ContactsProvider2 extends AbstractContactsProvider
break;
}
+ case BACKGROUND_TASK_RESCAN_DIRECTORY: {
+ updateDirectoriesInBackground(true);
+ break;
+ }
+
case BACKGROUND_TASK_UPDATE_LOCALE: {
updateLocaleInBackground();
break;
@@ -1795,13 +1813,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
break;
}
- case BACKGROUND_TASK_UPDATE_DIRECTORIES: {
- if (arg != null) {
- mContactDirectoryManager.onPackageChanged((String) arg);
- }
- break;
- }
-
case BACKGROUND_TASK_CLEANUP_PHOTOS: {
// Check rate limit.
long now = System.currentTimeMillis();
@@ -2053,7 +2064,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
@Override
- public ContactsDatabaseHelper getDatabaseHelper(final Context context) {
+ public ContactsDatabaseHelper newDatabaseHelper(final Context context) {
return ContactsDatabaseHelper.getInstance(context);
}
@@ -2233,6 +2244,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
public Uri insert(Uri uri, ContentValues values) {
waitForAccess(mWriteAccessLatch);
+ mContactsHelper.validateContentValues(getCallingPackage(), values);
+
if (mapsToProfileDbWithInsertedValues(uri, values)) {
switchToProfileMode();
return mProfileProvider.insert(uri, values);
@@ -2245,6 +2258,9 @@ public class ContactsProvider2 extends AbstractContactsProvider
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
waitForAccess(mWriteAccessLatch);
+ mContactsHelper.validateContentValues(getCallingPackage(), values);
+ mContactsHelper.validateSql(getCallingPackage(), selection);
+
if (mapsToProfileDb(uri)) {
switchToProfileMode();
return mProfileProvider.update(uri, values, selection, selectionArgs);
@@ -2257,6 +2273,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
public int delete(Uri uri, String selection, String[] selectionArgs) {
waitForAccess(mWriteAccessLatch);
+ mContactsHelper.validateSql(getCallingPackage(), selection);
+
if (mapsToProfileDb(uri)) {
switchToProfileMode();
return mProfileProvider.delete(uri, selection, selectionArgs);
@@ -2327,7 +2345,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
if (uri.getQueryParameter(PREAUTHORIZED_URI_TOKEN) != null) {
final long now = Clock.getInstance().currentTimeMillis();
final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
- db.beginTransaction();
+ db.beginTransactionNonExclusive();
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
@@ -2453,8 +2471,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
} else {
switchToContactMode();
}
-
- mDbHelper.get().invalidateAllCache();
}
private void updateSearchIndexInTransaction() {
@@ -2608,7 +2624,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
protected void notifyChange(boolean syncToNetwork, boolean syncToMetadataNetwork) {
getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
- syncToNetwork);
+ syncToNetwork || syncToMetadataNetwork);
getContext().getContentResolver().notifyChange(MetadataSync.METADATA_AUTHORITY_URI,
null, syncToMetadataNetwork);
@@ -2617,7 +2633,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
protected void setProviderStatus(int status) {
if (mProviderStatus != status) {
mProviderStatus = status;
- getContext().getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, null, false);
+ ContactsDatabaseHelper.notifyProviderStatusChange(getContext());
}
}
@@ -2866,6 +2882,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
private long insertRawContact(
Uri uri, ContentValues inputValues, boolean callerIsSyncAdapter) {
+ inputValues = fixUpUsageColumnsForEdit(inputValues);
+
// Create a shallow copy and initialize the contact ID to null.
final ContentValues values = new ContentValues(inputValues);
values.putNull(RawContacts.CONTACT_ID);
@@ -2891,8 +2909,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
if (needToUpdateMetadata) {
mTransactionContext.get().markRawContactMetadataDirty(rawContactId,
/* isMetadataSyncAdapter =*/false);
- mTransactionContext.get().markRawContactDirtyAndChanged(
- rawContactId, callerIsSyncAdapter);
}
// If the new raw contact is inserted by a sync adapter, mark mSyncToMetadataNetWork as true
// so that it can trigger the metadata syncing from the server.
@@ -3988,12 +4004,12 @@ public class ContactsProvider2 extends AbstractContactsProvider
private int deleteDataUsage() {
final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " +
- Contacts.TIMES_CONTACTED + "=0," +
- Contacts.LAST_TIME_CONTACTED + "=NULL");
+ Contacts.RAW_TIMES_CONTACTED + "=0," +
+ Contacts.RAW_LAST_TIME_CONTACTED + "=NULL");
db.execSQL("UPDATE " + Tables.CONTACTS + " SET " +
- Contacts.TIMES_CONTACTED + "=0," +
- Contacts.LAST_TIME_CONTACTED + "=NULL");
+ Contacts.RAW_TIMES_CONTACTED + "=0," +
+ Contacts.RAW_LAST_TIME_CONTACTED + "=NULL");
db.delete(Tables.DATA_USAGE_STAT, null, null);
return 1;
@@ -4051,9 +4067,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
case PROFILE: {
invalidateFastScrollingIndexCache();
count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter);
- if (count > 0) {
- mSyncToNetwork |= !callerIsSyncAdapter;
- }
break;
}
@@ -4061,9 +4074,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
invalidateFastScrollingIndexCache();
count = updateContactOptions(db, ContentUris.parseId(uri), values,
callerIsSyncAdapter);
- if (count > 0) {
- mSyncToNetwork |= !callerIsSyncAdapter;
- }
break;
}
@@ -4126,9 +4136,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
invalidateFastScrollingIndexCache();
selection = appendAccountIdToSelection(uri, selection);
count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
- if (count > 0) {
- mSyncToNetwork |= !callerIsSyncAdapter;
- }
break;
}
@@ -4145,9 +4152,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1,
callerIsSyncAdapter);
}
- if (count > 0) {
- mSyncToNetwork |= !callerIsSyncAdapter;
- }
break;
}
@@ -4173,12 +4177,9 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
case AGGREGATION_EXCEPTIONS: {
- count = updateAggregationException(db, values, callerIsSyncAdapter,
+ count = updateAggregationException(db, values,
/* callerIsMetadataSyncAdapter =*/false);
invalidateFastScrollingIndexCache();
- if (count > 0) {
- mSyncToNetwork |= !callerIsSyncAdapter;
- }
break;
}
@@ -4238,16 +4239,14 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
case DIRECTORIES: {
+ mContactDirectoryManager.setDirectoriesForceUpdated(true);
scanPackagesByUid(Binder.getCallingUid());
count = 1;
break;
}
case DATA_USAGE_FEEDBACK_ID: {
- count = handleDataUsageFeedback(uri, callerIsSyncAdapter) ? 1 : 0;
- if (count > 0) {
- mSyncToNetwork |= !callerIsSyncAdapter;
- }
+ count = handleDataUsageFeedback(uri) ? 1 : 0;
break;
}
@@ -4516,11 +4515,40 @@ public class ContactsProvider2 extends AbstractContactsProvider
return count;
}
+ /**
+ * Used for insert/update raw_contacts/contacts to adjust TIMES_CONTACTED and
+ * LAST_TIME_CONTACTED.
+ */
+ private ContentValues fixUpUsageColumnsForEdit(ContentValues cv) {
+ if (!cv.containsKey(Contacts.LR_LAST_TIME_CONTACTED)
+ && !cv.containsKey(Contacts.LR_TIMES_CONTACTED)) {
+ return cv;
+ }
+ final ContentValues ret = new ContentValues(cv);
+
+ ContactsDatabaseHelper.copyLongValue(
+ ret, Contacts.RAW_LAST_TIME_CONTACTED,
+ ret, Contacts.LR_LAST_TIME_CONTACTED);
+ ContactsDatabaseHelper.copyLongValue(
+ ret, Contacts.RAW_TIMES_CONTACTED,
+ ret, Contacts.LR_TIMES_CONTACTED);
+
+ ret.remove(Contacts.LR_LAST_TIME_CONTACTED);
+ ret.remove(Contacts.LR_TIMES_CONTACTED);
+ return ret;
+ }
+
private int updateRawContact(SQLiteDatabase db, long rawContactId, ContentValues values,
boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
final String selection = RawContactsColumns.CONCRETE_ID + " = ?";
mSelectionArgs1[0] = Long.toString(rawContactId);
+ values = fixUpUsageColumnsForEdit(values);
+
+ if (values.size() == 0) {
+ return 0; // Nothing to update; bail out.
+ }
+
final ContactsDatabaseHelper dbHelper = mDbHelper.get();
final boolean requestUndoDelete = flagIsClear(values, RawContacts.DELETED);
@@ -4596,8 +4624,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
if (shouldMarkMetadataDirtyForRawContact(values)) {
mTransactionContext.get().markRawContactMetadataDirty(
rawContactId, callerIsMetadataSyncAdapter);
- mTransactionContext.get().markRawContactDirtyAndChanged(
- rawContactId, callerIsSyncAdapter);
}
if (isBackupIdChanging) {
Cursor cursor = db.query(Tables.RAW_CONTACTS,
@@ -4755,6 +4781,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
private int updateContactOptions(
SQLiteDatabase db, long contactId, ContentValues inputValues, boolean callerIsSyncAdapter) {
+ inputValues = fixUpUsageColumnsForEdit(inputValues);
+
final ContentValues values = new ContentValues();
ContactsDatabaseHelper.copyStringValue(
values, RawContacts.CUSTOM_RINGTONE,
@@ -4763,11 +4791,11 @@ public class ContactsProvider2 extends AbstractContactsProvider
values, RawContacts.SEND_TO_VOICEMAIL,
inputValues, Contacts.SEND_TO_VOICEMAIL);
ContactsDatabaseHelper.copyLongValue(
- values, RawContacts.LAST_TIME_CONTACTED,
- inputValues, Contacts.LAST_TIME_CONTACTED);
+ values, RawContacts.RAW_LAST_TIME_CONTACTED,
+ inputValues, Contacts.RAW_LAST_TIME_CONTACTED);
ContactsDatabaseHelper.copyLongValue(
- values, RawContacts.TIMES_CONTACTED,
- inputValues, Contacts.TIMES_CONTACTED);
+ values, RawContacts.RAW_TIMES_CONTACTED,
+ inputValues, Contacts.RAW_TIMES_CONTACTED);
ContactsDatabaseHelper.copyLongValue(
values, RawContacts.STARRED,
inputValues, Contacts.STARRED);
@@ -4782,9 +4810,11 @@ public class ContactsProvider2 extends AbstractContactsProvider
final boolean hasStarredValue = flagExists(values, RawContacts.STARRED);
final boolean hasPinnedValue = flagExists(values, RawContacts.PINNED);
final boolean hasVoiceMailValue = flagExists(values, RawContacts.SEND_TO_VOICEMAIL);
- if (hasStarredValue || hasPinnedValue || hasVoiceMailValue) {
+ if (hasStarredValue) {
// Mark dirty when changing starred to trigger sync.
values.put(RawContacts.DIRTY, 1);
+ }
+ if (mMetadataSyncEnabled && (hasStarredValue || hasPinnedValue || hasVoiceMailValue)) {
// Mark dirty to trigger metadata syncing.
values.put(RawContacts.METADATA_DIRTY, 1);
}
@@ -4825,11 +4855,11 @@ public class ContactsProvider2 extends AbstractContactsProvider
values, RawContacts.SEND_TO_VOICEMAIL,
inputValues, Contacts.SEND_TO_VOICEMAIL);
ContactsDatabaseHelper.copyLongValue(
- values, RawContacts.LAST_TIME_CONTACTED,
- inputValues, Contacts.LAST_TIME_CONTACTED);
+ values, RawContacts.RAW_LAST_TIME_CONTACTED,
+ inputValues, Contacts.RAW_LAST_TIME_CONTACTED);
ContactsDatabaseHelper.copyLongValue(
- values, RawContacts.TIMES_CONTACTED,
- inputValues, Contacts.TIMES_CONTACTED);
+ values, RawContacts.RAW_TIMES_CONTACTED,
+ inputValues, Contacts.RAW_TIMES_CONTACTED);
ContactsDatabaseHelper.copyLongValue(
values, RawContacts.STARRED,
inputValues, Contacts.STARRED);
@@ -4843,8 +4873,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
int rslt = db.update(Tables.CONTACTS, values, Contacts._ID + "=?",
mSelectionArgs1);
- if (inputValues.containsKey(Contacts.LAST_TIME_CONTACTED) &&
- !inputValues.containsKey(Contacts.TIMES_CONTACTED)) {
+ if (inputValues.containsKey(Contacts.RAW_LAST_TIME_CONTACTED) &&
+ !inputValues.containsKey(Contacts.RAW_TIMES_CONTACTED)) {
db.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
db.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
}
@@ -4852,7 +4882,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
private int updateAggregationException(SQLiteDatabase db, ContentValues values,
- boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter) {
+ boolean callerIsMetadataSyncAdapter) {
Integer exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
Long rcId1 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID1);
Long rcId2 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID2);
@@ -4896,11 +4926,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
mTransactionContext.get().markRawContactMetadataDirty(rawContactId2,
callerIsMetadataSyncAdapter);
- mTransactionContext.get().markRawContactDirtyAndChanged(rawContactId1,
- callerIsSyncAdapter);
- mTransactionContext.get().markRawContactDirtyAndChanged(rawContactId2,
- callerIsSyncAdapter);
-
// The return value is fake - we just confirm that we made a change, not count actual
// rows changed.
return 1;
@@ -4916,6 +4941,10 @@ public class ContactsProvider2 extends AbstractContactsProvider
scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
}
+ public void scheduleRescanDirectories() {
+ scheduleBackgroundTask(BACKGROUND_TASK_RESCAN_DIRECTORY);
+ }
+
interface RawContactsBackupQuery {
String TABLE = Tables.RAW_CONTACTS;
String[] COLUMNS = new String[] {
@@ -5085,8 +5114,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
ContentValues usageStatsValues = new ContentValues();
usageStatsValues.put(DataUsageStatColumns.DATA_ID, dataId);
usageStatsValues.put(DataUsageStatColumns.USAGE_TYPE_INT, typeInt);
- usageStatsValues.put(DataUsageStatColumns.LAST_TIME_USED, lastTimeUsed);
- usageStatsValues.put(DataUsageStatColumns.TIMES_USED, timesUsed);
+ usageStatsValues.put(DataUsageStatColumns.RAW_LAST_TIME_USED, lastTimeUsed);
+ usageStatsValues.put(DataUsageStatColumns.RAW_TIMES_USED, timesUsed);
updateDataUsageStats(db, usageStatsValues);
}
}
@@ -5108,8 +5137,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
values.put(AggregationExceptions.TYPE, typeInt);
- updateAggregationException(db, values, /*callerIsSyncAdapter=*/true,
- /* callerIsMetadataSyncAdapter =*/true);
+ updateAggregationException(db, values, /* callerIsMetadataSyncAdapter =*/true);
if (rawContactId1 != rawContactId) {
aggregationRawContactIdsInServer.add(rawContactId1);
}
@@ -5127,8 +5155,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
values.put(AggregationExceptions.RAW_CONTACT_ID2, deleteRawContactId);
values.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE_AUTOMATIC);
- updateAggregationException(db, values, /*callerIsSyncAdapter=*/true,
- /* callerIsMetadataSyncAdapter =*/true);
+ updateAggregationException(db, values, /* callerIsMetadataSyncAdapter =*/true);
}
}
@@ -5440,8 +5467,9 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
}
+ @WorkerThread
public void onPackageChanged(String packageName) {
- scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_DIRECTORIES, packageName);
+ mContactDirectoryManager.onPackageChanged(packageName);
}
private void removeStaleAccountRows(String table, String accountNameColumn,
@@ -5488,6 +5516,11 @@ public class ContactsProvider2 extends AbstractContactsProvider
" order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
+
+ mContactsHelper.validateProjection(getCallingPackage(), projection);
+ mContactsHelper.validateSql(getCallingPackage(), selection);
+ mContactsHelper.validateSql(getCallingPackage(), sortOrder);
+
waitForAccess(mReadAccessLatch);
if (!isDirectoryParamValid(uri)) {
@@ -5505,12 +5538,15 @@ public class ContactsProvider2 extends AbstractContactsProvider
cancellationSignal);
}
incrementStats(mQueryStats);
+ try {
+ // Otherwise proceed with a normal query against the contacts DB.
+ switchToContactMode();
- // Otherwise proceed with a normal query against the contacts DB.
- switchToContactMode();
-
- return queryDirectoryIfNecessary(uri, projection, selection, selectionArgs, sortOrder,
- cancellationSignal);
+ return queryDirectoryIfNecessary(uri, projection, selection, selectionArgs, sortOrder,
+ cancellationSignal);
+ } finally {
+ finishOperation();
+ }
}
private boolean isCallerFromSameUser() {
@@ -5560,6 +5596,35 @@ public class ContactsProvider2 extends AbstractContactsProvider
return new MatrixCursor(projection);
}
+ private String getRealCallerPackageName(Uri queryUri) {
+ // If called by another CP2, then the URI should contain the original package name.
+ if (calledByAnotherSelf()) {
+ final String passedPackage = queryUri.getQueryParameter(
+ Directory.CALLER_PACKAGE_PARAM_KEY);
+ if (TextUtils.isEmpty(passedPackage)) {
+ Log.wtfStack(TAG,
+ "Cross-profile query with no " + Directory.CALLER_PACKAGE_PARAM_KEY);
+ return "UNKNOWN";
+ }
+ return passedPackage;
+ } else {
+ // Otherwise, just return the real calling package name.
+ return getCallingPackage();
+ }
+ }
+
+ /**
+ * Returns true if called by a different user's CP2.
+ */
+ private boolean calledByAnotherSelf() {
+ // Note normally myUid is always different from the callerUid in the code path where
+ // this method is used, except during unit tests, where the caller is always the same
+ // process.
+ final int myUid = android.os.Process.myUid();
+ final int callerUid = Binder.getCallingUid();
+ return (myUid != callerUid) && UserHandle.isSameApp(myUid, callerUid);
+ }
+
private Cursor queryDirectoryAuthority(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder, String directory,
final CancellationSignal cancellationSignal) {
@@ -5579,6 +5644,11 @@ public class ContactsProvider2 extends AbstractContactsProvider
if (directoryInfo.accountType != null) {
builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType);
}
+ // Pass the caller package name.
+ // Note the request may come from the CP2 on the primary profile. In that case, the
+ // real caller package is passed via the query paramter. See getRealCallerPackageName().
+ builder.appendQueryParameter(Directory.CALLER_PACKAGE_PARAM_KEY,
+ getRealCallerPackageName(uri));
String limit = getLimit(uri);
if (limit != null) {
@@ -5593,6 +5663,14 @@ public class ContactsProvider2 extends AbstractContactsProvider
Cursor cursor;
try {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Making directory query: uri=" + directoryUri +
+ " projection=" + Arrays.toString(projection) +
+ " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
+ " order=[" + sortOrder + "]" +
+ " Caller=" + getCallingPackage() +
+ " User=" + UserUtils.getCurrentUserHandle(getContext()));
+ }
cursor = getContext().getContentResolver().query(
directoryUri, projection, selection, selectionArgs, sortOrder);
if (cursor == null) {
@@ -5631,7 +5709,10 @@ public class ContactsProvider2 extends AbstractContactsProvider
throw new IllegalArgumentException(
"Authority " + localUri.getAuthority() + " is not a valid CP2 authority.");
}
- final Uri remoteUri = maybeAddUserId(localUri, corpUserId);
+ // Add the "user-id @" to the URI, and also pass the caller package name.
+ final Uri remoteUri = maybeAddUserId(localUri, corpUserId).buildUpon()
+ .appendQueryParameter(Directory.CALLER_PACKAGE_PARAM_KEY, getCallingPackage())
+ .build();
Cursor cursor = getContext().getContentResolver().query(remoteUri, projection, selection,
selectionArgs, sortOrder, cancellationSignal);
if (cursor == null) {
@@ -5930,8 +6011,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
if (projection != null) {
subProjection = new String[projection.length + 2];
System.arraycopy(projection, 0, subProjection, 0, projection.length);
- subProjection[projection.length + 0] = DataUsageStatColumns.TIMES_USED;
- subProjection[projection.length + 1] = DataUsageStatColumns.LAST_TIME_USED;
+ subProjection[projection.length + 0] = DataUsageStatColumns.LR_TIMES_USED;
+ subProjection[projection.length + 1] = DataUsageStatColumns.LR_LAST_TIME_USED;
}
// String that will store the query for starred contacts. For phone only queries,
@@ -5954,7 +6035,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
// it is included in the list of strequent numbers.
tableBuilder.append("(SELECT * FROM " + Views.DATA + " WHERE "
+ Contacts.STARRED + "=1)" + " AS " + Tables.DATA
- + " LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT
+ + " LEFT OUTER JOIN " + Views.DATA_USAGE_LR
+ + " AS " + Tables.DATA_USAGE_STAT
+ " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "="
+ DataColumns.CONCRETE_ID + " AND "
+ DataUsageStatColumns.CONCRETE_USAGE_TYPE + "="
@@ -5987,7 +6069,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
// data rows (almost always it should be), and we don't want any phone
// numbers not used by the user. This way sqlite is able to drop a number of
// rows in view_data in the early stage of data lookup.
- tableBuilder.append(Tables.DATA_USAGE_STAT
+ tableBuilder.append(Views.DATA_USAGE_LR + " AS " + Tables.DATA_USAGE_STAT
+ " INNER JOIN " + Views.DATA + " " + Tables.DATA
+ " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "="
+ DataColumns.CONCRETE_ID + " AND "
@@ -6040,7 +6122,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
// Phone numbers that were used more than 30 days ago are dropped from frequents
final String frequentQuery = "SELECT * FROM (" + frequentInnerQuery + ") WHERE " +
- TIME_SINCE_LAST_USED_SEC + "<" + LAST_TIME_USED_30_DAYS_SEC;
+ LR_TIME_SINCE_LAST_USED_SEC + "<" + LAST_TIME_USED_30_DAYS_SEC;
final String starredQuery = "SELECT * FROM (" + starredInnerQuery + ")";
// Put them together
@@ -6992,8 +7074,9 @@ public class ContactsProvider2 extends AbstractContactsProvider
providerStatus = ProviderStatus.STATUS_EMPTY;
}
return buildSingleRowResult(projection,
- new String[] {ProviderStatus.STATUS},
- new Object[] {providerStatus});
+ new String[] {ProviderStatus.STATUS,
+ ProviderStatus.DATABASE_CREATION_TIMESTAMP},
+ new Object[] {providerStatus, mDbHelper.get().getDatabaseCreationTime()});
}
case DIRECTORIES : {
@@ -7960,7 +8043,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
if (includeDataUsageStat) {
sb.append(" ON (" +
DbQueryUtils.concatenateClauses(
- DataUsageStatColumns.CONCRETE_TIMES_USED + " > 0",
+ DataUsageStatColumns.CONCRETE_RAW_TIMES_USED + " > 0",
RawContacts.CONTACT_ID + "=" + Views.CONTACTS + "." + Contacts._ID) +
")");
}
@@ -8347,7 +8430,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
private void appendDataUsageStatJoin(StringBuilder sb, int usageType, String dataIdColumn) {
if (usageType != USAGE_TYPE_ALL) {
- sb.append(" LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT +
+ sb.append(" LEFT OUTER JOIN " + Views.DATA_USAGE_LR +
+ " as " + Tables.DATA_USAGE_STAT +
" ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=");
sb.append(dataIdColumn);
sb.append(" AND " + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=");
@@ -8357,13 +8441,21 @@ public class ContactsProvider2 extends AbstractContactsProvider
sb.append(
" LEFT OUTER JOIN " +
"(SELECT " +
- DataUsageStatColumns.CONCRETE_DATA_ID + " as STAT_DATA_ID, " +
- "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED +
- ") as " + DataUsageStatColumns.TIMES_USED + ", " +
- "MAX(" + DataUsageStatColumns.CONCRETE_LAST_TIME_USED +
- ") as " + DataUsageStatColumns.LAST_TIME_USED +
- " FROM " + Tables.DATA_USAGE_STAT + " GROUP BY " +
- DataUsageStatColumns.CONCRETE_DATA_ID + ") as " + Tables.DATA_USAGE_STAT
+ DataUsageStatColumns.DATA_ID + " as STAT_DATA_ID," +
+ " SUM(ifnull(" + DataUsageStatColumns.RAW_TIMES_USED +
+ ",0)) as " + DataUsageStatColumns.RAW_TIMES_USED + ", " +
+ " MAX(ifnull(" + DataUsageStatColumns.RAW_LAST_TIME_USED +
+ ",0)) as " + DataUsageStatColumns.RAW_LAST_TIME_USED + "," +
+
+ // Note this is not ideal -- we should use "lowres(sum(LR_TIMES_USED))"
+ // here, but for performance reasons we just do it simple.
+ " SUM(ifnull(" + DataUsageStatColumns.LR_TIMES_USED +
+ ",0)) as " + DataUsageStatColumns.LR_TIMES_USED + ", " +
+
+ " MAX(ifnull(" + DataUsageStatColumns.LR_LAST_TIME_USED +
+ ",0)) as " + DataUsageStatColumns.LR_LAST_TIME_USED +
+ " FROM " + Views.DATA_USAGE_LR + " GROUP BY " +
+ DataUsageStatColumns.DATA_ID + ") as " + Tables.DATA_USAGE_STAT
);
sb.append(" ON (STAT_DATA_ID=");
sb.append(dataIdColumn);
@@ -8793,6 +8885,9 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
case PROFILE_AS_VCARD: {
+ if (!mode.equals("r")) {
+ throw new IllegalArgumentException("Write is not supported.");
+ }
// When opening a contact as file, we pass back contents as a
// vCard-encoded stream. We build into a local buffer first,
// then pipe into MemoryFile once the exact size is known.
@@ -8802,6 +8897,9 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
case CONTACTS_AS_VCARD: {
+ if (!mode.equals("r")) {
+ throw new IllegalArgumentException("Write is not supported.");
+ }
// When opening a contact as file, we pass back contents as a
// vCard-encoded stream. We build into a local buffer first,
// then pipe into MemoryFile once the exact size is known.
@@ -8811,6 +8909,9 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
case CONTACTS_AS_MULTI_VCARD: {
+ if (!mode.equals("r")) {
+ throw new IllegalArgumentException("Write is not supported.");
+ }
final String lookupKeys = uri.getPathSegments().get(2);
final String[] lookupKeyList = lookupKeys.split(":");
final StringBuilder inBuilder = new StringBuilder();
@@ -8856,7 +8957,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
default:
throw new FileNotFoundException(
- mDbHelper.get().exceptionMessage("File does not exist", uri));
+ mDbHelper.get().exceptionMessage(
+ "Stream I/O not supported on this URI.", uri));
}
}
@@ -9274,6 +9376,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
return StreamItems.StreamItemPhotos.CONTENT_ITEM_TYPE;
case STREAM_ITEMS_PHOTOS:
throw new UnsupportedOperationException("Not supported for write-only URI " + uri);
+ case PROVIDER_STATUS:
+ return ProviderStatus.CONTENT_TYPE;
default:
waitForAccess(mReadAccessLatch);
return mLegacyApiSupport.getType(uri);
@@ -9678,7 +9782,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
// just bump the aggregation algorithm version and let the provider start normally.
try {
final SQLiteDatabase db = mContactsHelper.getWritableDatabase();
- db.beginTransaction();
+ db.beginTransactionNonExclusive();
try {
updateAggregationAlgorithmVersion();
db.setTransactionSuccessful();
@@ -9731,7 +9835,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
db.execSQL(UNDEMOTE_RAW_CONTACT, arg);
}
- private boolean handleDataUsageFeedback(Uri uri, boolean callerIsSyncAdapter) {
+ private boolean handleDataUsageFeedback(Uri uri) {
final long currentTimeMillis = Clock.getInstance().currentTimeMillis();
final String usageType = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
final String[] ids = uri.getLastPathSegment().trim().split(",");
@@ -9770,7 +9874,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
final long rid = cursor.getLong(0);
mTransactionContext.get().markRawContactMetadataDirty(rid,
/* isMetadataSyncAdapter =*/false);
- mTransactionContext.get().markRawContactDirtyAndChanged(rid, callerIsSyncAdapter);
rawContactIds.add(rid);
}
} finally {
@@ -9781,15 +9884,15 @@ public class ContactsProvider2 extends AbstractContactsProvider
final String rids = TextUtils.join(",", rawContactIds);
db.execSQL("UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContacts.LAST_TIME_CONTACTED + "=?" +
- "," + RawContacts.TIMES_CONTACTED + "=" +
- "ifnull(" + RawContacts.TIMES_CONTACTED + ",0) + 1" +
+ " SET " + RawContacts.RAW_LAST_TIME_CONTACTED + "=?" +
+ "," + RawContacts.RAW_TIMES_CONTACTED + "=" +
+ "ifnull(" + RawContacts.RAW_TIMES_CONTACTED + ",0) + 1" +
" WHERE " + RawContacts._ID + " IN (" + rids + ")"
, mSelectionArgs1);
db.execSQL("UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.LAST_TIME_CONTACTED + "=?1" +
- "," + Contacts.TIMES_CONTACTED + "=" +
- "ifnull(" + Contacts.TIMES_CONTACTED + ",0) + 1" +
+ " SET " + Contacts.RAW_LAST_TIME_CONTACTED + "=?1" +
+ "," + Contacts.RAW_TIMES_CONTACTED + "=" +
+ "ifnull(" + Contacts.RAW_TIMES_CONTACTED + ",0) + 1" +
"," + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=?1" +
" WHERE " + Contacts._ID + " IN (SELECT " + RawContacts.CONTACT_ID +
" FROM " + Tables.RAW_CONTACTS +
@@ -9836,9 +9939,9 @@ public class ContactsProvider2 extends AbstractContactsProvider
mSelectionArgs2[1] = String.valueOf(id);
db.execSQL("UPDATE " + Tables.DATA_USAGE_STAT +
- " SET " + DataUsageStatColumns.TIMES_USED + "=" +
- "ifnull(" + DataUsageStatColumns.TIMES_USED +",0)+1" +
- "," + DataUsageStatColumns.LAST_TIME_USED + "=?" +
+ " SET " + DataUsageStatColumns.RAW_TIMES_USED + "=" +
+ "ifnull(" + DataUsageStatColumns.RAW_TIMES_USED +",0)+1" +
+ "," + DataUsageStatColumns.RAW_LAST_TIME_USED + "=?" +
" WHERE " + DataUsageStatColumns._ID + "=?",
mSelectionArgs2);
} else {
@@ -9849,8 +9952,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
db.execSQL("INSERT INTO " + Tables.DATA_USAGE_STAT +
"(" + DataUsageStatColumns.DATA_ID +
"," + DataUsageStatColumns.USAGE_TYPE_INT +
- "," + DataUsageStatColumns.TIMES_USED +
- "," + DataUsageStatColumns.LAST_TIME_USED +
+ "," + DataUsageStatColumns.RAW_TIMES_USED +
+ "," + DataUsageStatColumns.RAW_LAST_TIME_USED +
") VALUES (?,?,?,?)",
mSelectionArgs4);
}
@@ -9863,14 +9966,14 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
/**
- * Update {@link Tables#DATA_USAGE_STAT}.
+ * Directly update {@link Tables#DATA_USAGE_STAT}; used for metadata sync.
* Update or insert usageType, lastTimeUsed, and timesUsed for specific dataId.
*/
private void updateDataUsageStats(SQLiteDatabase db, ContentValues values) {
final String dataId = values.getAsString(DataUsageStatColumns.DATA_ID);
final String type = values.getAsString(DataUsageStatColumns.USAGE_TYPE_INT);
- final String lastTimeUsed = values.getAsString(DataUsageStatColumns.LAST_TIME_USED);
- final String timesUsed = values.getAsString(DataUsageStatColumns.TIMES_USED);
+ final String lastTimeUsed = values.getAsString(DataUsageStatColumns.RAW_LAST_TIME_USED);
+ final String timesUsed = values.getAsString(DataUsageStatColumns.RAW_TIMES_USED);
mSelectionArgs2[0] = dataId;
mSelectionArgs2[1] = type;
@@ -9886,8 +9989,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
mSelectionArgs3[1] = timesUsed;
mSelectionArgs3[2] = String.valueOf(id);
db.execSQL("UPDATE " + Tables.DATA_USAGE_STAT +
- " SET " + DataUsageStatColumns.LAST_TIME_USED + "=?" +
- "," + DataUsageStatColumns.TIMES_USED + "=?" +
+ " SET " + DataUsageStatColumns.RAW_LAST_TIME_USED + "=?" +
+ "," + DataUsageStatColumns.RAW_TIMES_USED + "=?" +
" WHERE " + DataUsageStatColumns._ID + "=?",
mSelectionArgs3);
} else {
@@ -9898,8 +10001,8 @@ public class ContactsProvider2 extends AbstractContactsProvider
db.execSQL("INSERT INTO " + Tables.DATA_USAGE_STAT +
"(" + DataUsageStatColumns.DATA_ID +
"," + DataUsageStatColumns.USAGE_TYPE_INT +
- "," + DataUsageStatColumns.TIMES_USED +
- "," + DataUsageStatColumns.LAST_TIME_USED +
+ "," + DataUsageStatColumns.RAW_TIMES_USED +
+ "," + DataUsageStatColumns.RAW_LAST_TIME_USED +
") VALUES (?,?,?,?)",
mSelectionArgs4);
}
@@ -10120,4 +10223,19 @@ public class ContactsProvider2 extends AbstractContactsProvider
public void switchToProfileModeForTest() {
switchToProfileMode();
}
+
+ @Override
+ public void shutdown() {
+ mTaskScheduler.shutdownForTest();
+ }
+
+ @VisibleForTesting
+ public ContactsDatabaseHelper getContactsDatabaseHelperForTest() {
+ return mContactsHelper;
+ }
+
+ @VisibleForTesting
+ public ProfileProvider getProfileProviderForTest() {
+ return mProfileProvider;
+ }
}
diff --git a/src/com/android/providers/contacts/ContactsTaskScheduler.java b/src/com/android/providers/contacts/ContactsTaskScheduler.java
new file mode 100644
index 00000000..16283877
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactsTaskScheduler.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Runs tasks in a worker thread, which is created on-demand and shuts down after a timeout.
+ */
+public abstract class ContactsTaskScheduler {
+ private static final String TAG = "ContactsTaskScheduler";
+
+ public static final boolean VERBOSE_LOGGING = AbstractContactsProvider.VERBOSE_LOGGING;
+
+ private static final int SHUTDOWN_TIMEOUT_SECONDS = 60;
+
+ private final AtomicInteger mThreadSequenceNumber = new AtomicInteger();
+
+ private final Object mLock = new Object();
+
+ /**
+ * Name of this scheduler for logging.
+ */
+ private final String mName;
+
+ @GuardedBy("mLock")
+ private HandlerThread mThread;
+
+ @GuardedBy("mLock")
+ private MyHandler mHandler;
+
+ private final int mShutdownTimeoutSeconds;
+
+ public ContactsTaskScheduler(String name) {
+ this(name, SHUTDOWN_TIMEOUT_SECONDS);
+ }
+
+ /** With explicit timeout seconds, for testing. */
+ protected ContactsTaskScheduler(String name, int shutdownTimeoutSeconds) {
+ mName = name;
+ mShutdownTimeoutSeconds = shutdownTimeoutSeconds;
+ }
+
+ private class MyHandler extends Handler {
+ public MyHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "[" + mName + "] " + mThread + " dispatching " + msg.what);
+ }
+ onPerformTask(msg.what, msg.obj);
+ }
+ }
+
+ private final Runnable mQuitter = () -> {
+ synchronized (mLock) {
+ stopThread(/* joinOnlyForTest=*/ false);
+ }
+ };
+
+ private boolean isRunning() {
+ synchronized (mLock) {
+ return mThread != null;
+ }
+ }
+
+ /** Schedule a task with no arguments. */
+ @VisibleForTesting
+ public void scheduleTask(int taskId) {
+ scheduleTask(taskId, null);
+ }
+
+ /** Schedule a task with an argument. */
+ @VisibleForTesting
+ public void scheduleTask(int taskId, Object arg) {
+ synchronized (mLock) {
+ if (!isRunning()) {
+ mThread = new HandlerThread("Worker-" + mThreadSequenceNumber.incrementAndGet());
+ mThread.start();
+ mHandler = new MyHandler(mThread.getLooper());
+
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "[" + mName + "] " + mThread + " started.");
+ }
+ }
+ if (arg == null) {
+ mHandler.sendEmptyMessage(taskId);
+ } else {
+ mHandler.sendMessage(mHandler.obtainMessage(taskId, arg));
+ }
+
+ // Schedule thread shutdown.
+ mHandler.removeCallbacks(mQuitter);
+ mHandler.postDelayed(mQuitter, mShutdownTimeoutSeconds * 1000);
+ }
+ }
+
+ public abstract void onPerformTask(int taskId, Object arg);
+
+ @VisibleForTesting
+ public void shutdownForTest() {
+ stopThread(/* joinOnlyForTest=*/ true);
+ }
+
+ private void stopThread(boolean joinOnlyForTest) {
+ synchronized (mLock) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "[" + mName + "] " + mThread + " stopping...");
+ }
+ if (mThread != null) {
+ mThread.quit();
+ if (joinOnlyForTest) {
+ try {
+ mThread.join();
+ } catch (InterruptedException ignore) {
+ }
+ }
+ }
+ mThread = null;
+ mHandler = null;
+ }
+ }
+
+ @VisibleForTesting
+ public int getThreadSequenceNumber() {
+ return mThreadSequenceNumber.get();
+ }
+
+ @VisibleForTesting
+ public boolean isRunningForTest() {
+ return isRunning();
+ }
+}
diff --git a/src/com/android/providers/contacts/ContactsTransaction.java b/src/com/android/providers/contacts/ContactsTransaction.java
index c6c11d99..e220dd92 100644
--- a/src/com/android/providers/contacts/ContactsTransaction.java
+++ b/src/com/android/providers/contacts/ContactsTransaction.java
@@ -113,9 +113,9 @@ public class ContactsTransaction {
mDatabasesForTransaction.add(0, db);
mDatabaseTagMap.put(tag, db);
if (listener != null) {
- db.beginTransactionWithListener(listener);
+ db.beginTransactionWithListenerNonExclusive(listener);
} else {
- db.beginTransaction();
+ db.beginTransactionNonExclusive();
}
}
}
diff --git a/src/com/android/providers/contacts/ContactsUpgradeReceiver.java b/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
index 57c0cd0c..6f50a145 100644
--- a/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
+++ b/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
@@ -16,7 +16,6 @@
package com.android.providers.contacts;
-import android.app.ActivityManagerNative;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -87,7 +86,7 @@ public class ContactsUpgradeReceiver extends BroadcastReceiver {
Log.i(TAG, "Creating or opening contacts database");
helper.getWritableDatabase();
- helper.clearDirectoryScanComplete();
+ helper.forceDirectoryRescan();
profileHelper.getWritableDatabase();
calllogHelper.getWritableDatabase();
diff --git a/src/com/android/providers/contacts/DbModifierWithNotification.java b/src/com/android/providers/contacts/DbModifierWithNotification.java
index f67a2a8f..cb5460ce 100644
--- a/src/com/android/providers/contacts/DbModifierWithNotification.java
+++ b/src/com/android/providers/contacts/DbModifierWithNotification.java
@@ -37,9 +37,11 @@ import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Status;
import android.provider.VoicemailContract.Voicemails;
import android.util.Log;
+
import com.android.common.io.MoreCloseables;
import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
import com.android.providers.contacts.util.DbQueryUtils;
+
import com.google.android.collect.Lists;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
@@ -172,7 +174,14 @@ public class DbModifierWithNotification implements DatabaseModifier {
// from the server and thus is synced or "clean". Otherwise, it means that a local
// change is being made to the database, so the entries should be marked as "dirty"
// so that the corresponding sync adapter knows they need to be synced.
- final int isDirty = isSelfModifyingOrInternal(packagesModified) ? 0 : 1;
+ int isDirty;
+ Integer callerSetDirty = values.getAsInteger(Voicemails.DIRTY);
+ if (callerSetDirty != null) {
+ // Respect the calling package if it sets the dirty flag
+ isDirty = callerSetDirty == 0 ? 0 : 1;
+ } else {
+ isDirty = isSelfModifyingOrInternal(packagesModified) ? 0 : 1;
+ }
values.put(VoicemailContract.Voicemails.DIRTY, isDirty);
if (isDirty == 0 && values.containsKey(Calls.IS_READ) && getAsBoolean(values,
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index 20307d49..6678b954 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -232,7 +232,7 @@ public class GlobalSearchSupport {
+ Contacts.PHOTO_THUMBNAIL_URI + ", "
+ Contacts.DISPLAY_NAME + ", "
+ PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE + ", "
- + Contacts.LAST_TIME_CONTACTED);
+ + Contacts.LR_LAST_TIME_CONTACTED);
if (haveFilter) {
sb.append(", " + SearchSnippets.SNIPPET);
}
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
index 598a4a06..741639a2 100644
--- a/src/com/android/providers/contacts/LegacyApiSupport.java
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -107,10 +107,6 @@ public class LegacyApiSupport {
private static final int SEARCH_SUGGESTIONS = 32;
private static final int SEARCH_SHORTCUT = 33;
private static final int PHONES_FILTER = 34;
- private static final int LIVE_FOLDERS_PEOPLE = 35;
- private static final int LIVE_FOLDERS_PEOPLE_GROUP_NAME = 36;
- private static final int LIVE_FOLDERS_PEOPLE_WITH_PHONES = 37;
- private static final int LIVE_FOLDERS_PEOPLE_FAVORITES = 38;
private static final int CONTACTMETHODS_EMAIL = 39;
private static final int GROUP_NAME_MEMBERS = 40;
private static final int GROUP_SYSTEM_ID_MEMBERS = 41;
@@ -185,24 +181,6 @@ public class LegacyApiSupport {
+ " ELSE " + Tables.DATA + "." + Email.DATA
+ " END)";
- private static final Uri LIVE_FOLDERS_CONTACTS_URI = Uri.withAppendedPath(
- ContactsContract.AUTHORITY_URI, "live_folders/contacts");
-
- private static final Uri LIVE_FOLDERS_CONTACTS_WITH_PHONES_URI = Uri.withAppendedPath(
- ContactsContract.AUTHORITY_URI, "live_folders/contacts_with_phones");
-
- private static final Uri LIVE_FOLDERS_CONTACTS_FAVORITES_URI = Uri.withAppendedPath(
- ContactsContract.AUTHORITY_URI, "live_folders/favorites");
-
- private static final String CONTACTS_UPDATE_LASTTIMECONTACTED =
- "UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.LAST_TIME_CONTACTED + "=? " +
- "WHERE " + Contacts._ID + "=?";
- private static final String RAWCONTACTS_UPDATE_LASTTIMECONTACTED =
- "UPDATE " + Tables.RAW_CONTACTS + " SET "
- + RawContacts.LAST_TIME_CONTACTED + "=? WHERE "
- + RawContacts._ID + "=?";
-
private String[] mSelectionArgs1 = new String[1];
private String[] mSelectionArgs2 = new String[2];
@@ -357,15 +335,6 @@ public class LegacyApiSupport {
SEARCH_SHORTCUT);
matcher.addURI(authority, "settings", SETTINGS);
- matcher.addURI(authority, "live_folders/people", LIVE_FOLDERS_PEOPLE);
- matcher.addURI(authority, "live_folders/people/*",
- LIVE_FOLDERS_PEOPLE_GROUP_NAME);
- matcher.addURI(authority, "live_folders/people_with_phones",
- LIVE_FOLDERS_PEOPLE_WITH_PHONES);
- matcher.addURI(authority, "live_folders/favorites",
- LIVE_FOLDERS_PEOPLE_FAVORITES);
-
-
HashMap<String, String> peopleProjectionMap = new HashMap<String, String>();
peopleProjectionMap.put(People.NAME, People.NAME);
peopleProjectionMap.put(People.DISPLAY_NAME, People.DISPLAY_NAME);
@@ -568,10 +537,12 @@ public class LegacyApiSupport {
+ " AS " + People.NOTES + ", " +
AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
- Tables.RAW_CONTACTS + "." + RawContacts.TIMES_CONTACTED
- + " AS " + People.TIMES_CONTACTED + ", " +
- Tables.RAW_CONTACTS + "." + RawContacts.LAST_TIME_CONTACTED
- + " AS " + People.LAST_TIME_CONTACTED + ", " +
+
+ // We no longer return even low-res values from CP1.
+ // Note if we just use the value 0 below, certain seletion wouldn't work.
+ "cast(0 as int) AS " + People.TIMES_CONTACTED + ", " +
+ "cast(0 as int) AS " + People.LAST_TIME_CONTACTED + ", " +
+
Tables.RAW_CONTACTS + "." + RawContacts.CUSTOM_RINGTONE
+ " AS " + People.CUSTOM_RINGTONE + ", " +
Tables.RAW_CONTACTS + "." + RawContacts.SEND_TO_VOICEMAIL
@@ -948,7 +919,7 @@ public class LegacyApiSupport {
int count = 0;
switch(match) {
case PEOPLE_UPDATE_CONTACT_TIME: {
- count = updateContactTime(uri, values);
+ count = 0; // No longer supported.
break;
}
@@ -1077,11 +1048,6 @@ public class LegacyApiSupport {
}
}
- if (values.containsKey(People.LAST_TIME_CONTACTED) &&
- !values.containsKey(People.TIMES_CONTACTED)) {
- updateContactTime(rawContactId, values);
- }
-
return count;
}
@@ -1143,35 +1109,6 @@ public class LegacyApiSupport {
Groups._ID + "=" + groupId, null);
}
- private int updateContactTime(Uri uri, ContentValues values) {
- long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
- updateContactTime(rawContactId, values);
- return 1;
- }
-
- private void updateContactTime(long rawContactId, ContentValues values) {
- final Long storedTimeContacted = values.getAsLong(People.LAST_TIME_CONTACTED);
- final long lastTimeContacted = storedTimeContacted != null ?
- storedTimeContacted : System.currentTimeMillis();
-
- // TODO check sanctions
- long contactId = mDbHelper.getContactId(rawContactId);
- SQLiteDatabase mDb = mDbHelper.getWritableDatabase();
- mSelectionArgs2[0] = String.valueOf(lastTimeContacted);
- if (contactId != 0) {
- mSelectionArgs2[1] = String.valueOf(contactId);
- mDb.execSQL(CONTACTS_UPDATE_LASTTIMECONTACTED, mSelectionArgs2);
- // increment times_contacted column
- mSelectionArgs1[0] = String.valueOf(contactId);
- mDb.execSQL(ContactsProvider2.UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
- }
- mSelectionArgs2[1] = String.valueOf(rawContactId);
- mDb.execSQL(RAWCONTACTS_UPDATE_LASTTIMECONTACTED, mSelectionArgs2);
- // increment times_contacted column
- mSelectionArgs1[0] = String.valueOf(contactId);
- mDb.execSQL(ContactsProvider2.UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
- }
-
private int updatePhoto(long rawContactId, ContentValues values) {
// TODO check sanctions
@@ -1359,10 +1296,12 @@ public class LegacyApiSupport {
values, People.CUSTOM_RINGTONE);
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
values, People.SEND_TO_VOICEMAIL);
- ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
- values, People.LAST_TIME_CONTACTED);
- ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
- values, People.TIMES_CONTACTED);
+
+ // We no longer support the following fields in CP1.
+ // ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
+ // values, People.LAST_TIME_CONTACTED);
+ // ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
+ // values, People.TIMES_CONTACTED);
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
values, People.STARRED);
if (mAccount != null) {
@@ -1884,23 +1823,6 @@ public class LegacyApiSupport {
db, projection, lookupKey, filter, null);
}
- case LIVE_FOLDERS_PEOPLE:
- return mContactsProvider.query(LIVE_FOLDERS_CONTACTS_URI,
- projection, selection, selectionArgs, sortOrder);
-
- case LIVE_FOLDERS_PEOPLE_WITH_PHONES:
- return mContactsProvider.query(LIVE_FOLDERS_CONTACTS_WITH_PHONES_URI,
- projection, selection, selectionArgs, sortOrder);
-
- case LIVE_FOLDERS_PEOPLE_FAVORITES:
- return mContactsProvider.query(LIVE_FOLDERS_CONTACTS_FAVORITES_URI,
- projection, selection, selectionArgs, sortOrder);
-
- case LIVE_FOLDERS_PEOPLE_GROUP_NAME:
- return mContactsProvider.query(Uri.withAppendedPath(LIVE_FOLDERS_CONTACTS_URI,
- Uri.encode(uri.getLastPathSegment())),
- projection, selection, selectionArgs, sortOrder);
-
case DELETED_PEOPLE:
case DELETED_GROUPS:
throw new UnsupportedOperationException(mDbHelper.exceptionMessage(uri));
@@ -1985,7 +1907,6 @@ public class LegacyApiSupport {
* a group with a particular system id. The projection map of the query must include
* {@link People#_ID}.
*
- * @param groupName The name of the group
* @return The where clause.
*/
private String buildGroupSystemIdMatchWhereClause(String systemId) {
diff --git a/src/com/android/providers/contacts/PackageIntentReceiver.java b/src/com/android/providers/contacts/PackageIntentReceiver.java
deleted file mode 100644
index 57362491..00000000
--- a/src/com/android/providers/contacts/PackageIntentReceiver.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.providers.contacts;
-
-import android.content.BroadcastReceiver;
-import android.content.ContentProvider;
-import android.content.Context;
-import android.content.IContentProvider;
-import android.content.Intent;
-import android.net.Uri;
-import android.provider.ContactsContract;
-
-/**
- * Package intent receiver that invokes {@link ContactsProvider2#onPackageChanged} to update
- * the contact directory list.
- */
-public class PackageIntentReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- Uri packageUri = intent.getData();
- String packageName = packageUri.getSchemeSpecificPart();
- IContentProvider iprovider =
- context.getContentResolver().acquireProvider(ContactsContract.AUTHORITY);
- ContentProvider provider = ContentProvider.coerceToLocalContentProvider(iprovider);
- if (provider instanceof ContactsProvider2) {
- ((ContactsProvider2)provider).onPackageChanged(packageName);
- }
- handlePackageChangedForVoicemail(context, intent);
- }
-
- private void handlePackageChangedForVoicemail(Context context, Intent intent) {
- if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED) &&
- !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
- // Forward the intent to the cleanup service for handling the event.
- Intent intentToForward = new Intent(context, VoicemailCleanupService.class);
- intentToForward.setData(intent.getData());
- intentToForward.setAction(intent.getAction());
- intentToForward.putExtras(intent.getExtras());
- context.startService(intentToForward);
- }
- }
-}
diff --git a/src/com/android/providers/contacts/ProfileAwareUriMatcher.java b/src/com/android/providers/contacts/ProfileAwareUriMatcher.java
index ee5ec03c..744addc1 100644
--- a/src/com/android/providers/contacts/ProfileAwareUriMatcher.java
+++ b/src/com/android/providers/contacts/ProfileAwareUriMatcher.java
@@ -123,6 +123,9 @@ public class ProfileAwareUriMatcher extends UriMatcher {
}
} else if (PROFILE_URI_LOOKUP_KEY_MAP.containsKey(match)) {
int lookupKeySegment = PROFILE_URI_LOOKUP_KEY_MAP.get(match);
+ if (lookupKeySegment >= uri.getPathSegments().size()) {
+ return false;
+ }
String lookupKey = uri.getPathSegments().get(lookupKeySegment);
if (ContactLookupKey.PROFILE_LOOKUP_KEY.equals(lookupKey)) {
return true;
diff --git a/src/com/android/providers/contacts/ProfileDatabaseHelper.java b/src/com/android/providers/contacts/ProfileDatabaseHelper.java
index a23e5217..966ee7e8 100644
--- a/src/com/android/providers/contacts/ProfileDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ProfileDatabaseHelper.java
@@ -42,18 +42,20 @@ public class ProfileDatabaseHelper extends ContactsDatabaseHelper {
* Returns a new instance for unit tests.
*/
@NeededForTesting
- public static ProfileDatabaseHelper getNewInstanceForTest(Context context) {
- return new ProfileDatabaseHelper(context, null, false);
+ public static ProfileDatabaseHelper getNewInstanceForTest(Context context, String filename) {
+ return new ProfileDatabaseHelper(context, filename, false, /* isTestInstance=*/ true);
}
private ProfileDatabaseHelper(
- Context context, String databaseName, boolean optimizationEnabled) {
- super(context, databaseName, optimizationEnabled);
+ Context context, String databaseName, boolean optimizationEnabled,
+ boolean isTestInstance) {
+ super(context, databaseName, optimizationEnabled, isTestInstance);
}
public static synchronized ProfileDatabaseHelper getInstance(Context context) {
if (sSingleton == null) {
- sSingleton = new ProfileDatabaseHelper(context, DATABASE_NAME, true);
+ sSingleton = new ProfileDatabaseHelper(context, DATABASE_NAME, true,
+ /* isTestInstance=*/ false);
}
return sSingleton;
}
@@ -72,4 +74,18 @@ public class ProfileDatabaseHelper extends ContactsDatabaseHelper {
db.insert(SEQUENCE_TABLE, null, values);
}
}
+
+ @Override
+ protected void postOnCreate() {
+ }
+
+ @Override
+ protected void setDatabaseCreationTime(SQLiteDatabase db) {
+ // We don't need the creation time for the profile DB.
+ }
+
+ @Override
+ protected void loadDatabaseCreationTime(SQLiteDatabase db) {
+ // We don't need the creation time for the profile DB.
+ }
}
diff --git a/src/com/android/providers/contacts/ProfileProvider.java b/src/com/android/providers/contacts/ProfileProvider.java
index 88ae4c3a..6c84e4b0 100644
--- a/src/com/android/providers/contacts/ProfileProvider.java
+++ b/src/com/android/providers/contacts/ProfileProvider.java
@@ -48,10 +48,14 @@ public class ProfileProvider extends AbstractContactsProvider {
}
@Override
- protected ProfileDatabaseHelper getDatabaseHelper(Context context) {
+ protected ProfileDatabaseHelper newDatabaseHelper(Context context) {
return ProfileDatabaseHelper.getInstance(context);
}
+ public ProfileDatabaseHelper getDatabaseHelper() {
+ return (ProfileDatabaseHelper) super.getDatabaseHelper();
+ }
+
@Override
protected ThreadLocal<ContactsTransaction> getTransactionHolder() {
return mDelegate.getTransactionHolder();
@@ -67,8 +71,12 @@ public class ProfileProvider extends AbstractContactsProvider {
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder, CancellationSignal cancellationSignal) {
incrementStats(mQueryStats);
- return mDelegate.queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1,
- cancellationSignal);
+ try {
+ return mDelegate.queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1,
+ cancellationSignal);
+ } finally {
+ finishOperation();
+ }
}
@Override
@@ -149,6 +157,9 @@ public class ProfileProvider extends AbstractContactsProvider {
private void sendProfileChangedBroadcast() {
final Intent intent = new Intent(Intents.ACTION_PROFILE_CHANGED);
mDelegate.getContext().sendBroadcast(intent, READ_CONTACTS_PERMISSION);
+ // TODO b/35323708 update user profile data here instead of notifying Settings
+ intent.setPackage("com.android.settings");
+ mDelegate.getContext().sendBroadcast(intent, READ_CONTACTS_PERMISSION);
}
@Override
diff --git a/src/com/android/providers/contacts/SearchIndexManager.java b/src/com/android/providers/contacts/SearchIndexManager.java
index ba2a60d0..768fb978 100644
--- a/src/com/android/providers/contacts/SearchIndexManager.java
+++ b/src/com/android/providers/contacts/SearchIndexManager.java
@@ -113,7 +113,7 @@ public class SearchIndexManager {
@Override
public String toString() {
- return "Content: " + mSbContent + "\n Name: " + mSbTokens + "\n Tokens: " + mSbTokens;
+ return "Content: " + mSbContent + "\n Name: " + mSbName + "\n Tokens: " + mSbTokens;
}
public void commit() {
diff --git a/src/com/android/providers/contacts/VoicemailCleanupService.java b/src/com/android/providers/contacts/VoicemailCleanupService.java
deleted file mode 100644
index 4ad1406a..00000000
--- a/src/com/android/providers/contacts/VoicemailCleanupService.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.providers.contacts;
-
-import android.app.IntentService;
-import android.content.ContentResolver;
-import android.content.Intent;
-import android.provider.VoicemailContract.Status;
-import android.provider.VoicemailContract.Voicemails;
-import android.util.Log;
-
-import com.google.common.annotations.VisibleForTesting;
-
-/**
- * A service that cleans up voicemail related data for packages that are uninstalled.
- */
-public class VoicemailCleanupService extends IntentService {
- private static final String TAG = "VoicemailCleanupService";
-
- public VoicemailCleanupService() {
- super("VoicemailCleanupService");
- }
-
- @Override
- protected void onHandleIntent(Intent intent) {
- handleIntentInternal(intent, getContentResolver());
- }
-
- @VisibleForTesting
- void handleIntentInternal(Intent intent,
- ContentResolver contentResolver) {
- if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED) &&
- !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
- String packageUninstalled = intent.getData().getSchemeSpecificPart();
- Log.d(TAG, "Cleaning up data for package: " + packageUninstalled);
- // Delete both voicemail content and voicemail status entries for this package.
- contentResolver.delete(Voicemails.buildSourceUri(packageUninstalled), null, null);
- contentResolver.delete(Status.buildSourceUri(packageUninstalled), null, null);
- } else {
- Log.w(TAG, "Unexpected intent: " + intent);
- }
- }
-}
diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java
index 099e924d..160a1a99 100644
--- a/src/com/android/providers/contacts/VoicemailContentProvider.java
+++ b/src/com/android/providers/contacts/VoicemailContentProvider.java
@@ -16,11 +16,13 @@
package com.android.providers.contacts;
import static android.provider.VoicemailContract.SOURCE_PACKAGE_FIELD;
+
import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
import android.app.AppOpsManager;
import android.content.ContentProvider;
+import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -30,15 +32,24 @@ import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
import android.provider.VoicemailContract.Voicemails;
+import android.util.ArraySet;
import android.util.Log;
+
import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
import com.android.providers.contacts.util.ContactsPermissions;
+import com.android.providers.contacts.util.PackageUtils;
import com.android.providers.contacts.util.SelectionBuilder;
import com.android.providers.contacts.util.TypedUriMatcherImpl;
+import com.android.providers.contacts.util.UserUtils;
+
import com.google.common.annotations.VisibleForTesting;
+
import java.io.FileNotFoundException;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
/**
* An implementation of the Voicemail content provider. This class in the entry point for both
@@ -48,12 +59,24 @@ import java.util.List;
*/
public class VoicemailContentProvider extends ContentProvider
implements VoicemailTable.DelegateHelper {
+ private static final String TAG = "VoicemailProvider";
+
+ public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
+
+ private static final int BACKGROUND_TASK_SCAN_STALE_PACKAGES = 0;
+
+ private ContactsTaskScheduler mTaskScheduler;
+
private VoicemailPermissions mVoicemailPermissions;
private VoicemailTable.Delegate mVoicemailContentTable;
private VoicemailTable.Delegate mVoicemailStatusTable;
@Override
public boolean onCreate() {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "onCreate: " + this.getClass().getSimpleName()
+ + " user=" + android.os.Process.myUserHandle().getIdentifier());
+ }
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.INFO)) {
Log.i(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate start");
}
@@ -68,6 +91,18 @@ public class VoicemailContentProvider extends ContentProvider
getDatabaseHelper(context), this, createCallLogInsertionHelper(context));
mVoicemailStatusTable = new VoicemailStatusTable(Tables.VOICEMAIL_STATUS, context,
getDatabaseHelper(context), this);
+
+ mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
+ @Override
+ public void onPerformTask(int taskId, Object arg) {
+ performBackgroundTask(taskId, arg);
+ }
+ };
+
+ scheduleScanStalePackages();
+
+ ContactsPackageMonitor.start(getContext());
+
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.INFO)) {
Log.i(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate finish");
}
@@ -75,6 +110,16 @@ public class VoicemailContentProvider extends ContentProvider
}
@VisibleForTesting
+ void scheduleScanStalePackages() {
+ scheduleTask(BACKGROUND_TASK_SCAN_STALE_PACKAGES, null);
+ }
+
+ @VisibleForTesting
+ void scheduleTask(int taskId, Object arg) {
+ mTaskScheduler.scheduleTask(taskId, arg);
+ }
+
+ @VisibleForTesting
/*package*/ CallLogInsertionHelper createCallLogInsertionHelper(Context context) {
return DefaultCallLogInsertionHelper.getInstance(context);
}
@@ -103,6 +148,10 @@ public class VoicemailContentProvider extends ContentProvider
@Override
public Uri insert(Uri uri, ContentValues values) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "insert: uri=" + uri + " values=[" + values + "]" +
+ " CPID=" + Binder.getCallingPid());
+ }
UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values);
return getTableDelegate(uriData).insert(uriData, values);
}
@@ -110,6 +159,12 @@ public class VoicemailContentProvider extends ContentProvider
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) +
+ " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
+ " order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
+ " User=" + UserUtils.getCurrentUserHandle(getContext()));
+ }
UriData uriData = checkPermissionsAndCreateUriDataForRead(uri);
SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
selectionBuilder.addClause(getPackageRestrictionClause(true/*isQuery*/));
@@ -119,6 +174,12 @@ public class VoicemailContentProvider extends ContentProvider
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "update: uri=" + uri +
+ " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
+ " values=[" + values + "] CPID=" + Binder.getCallingPid() +
+ " User=" + UserUtils.getCurrentUserHandle(getContext()));
+ }
UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values);
SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
selectionBuilder.addClause(getPackageRestrictionClause(false/*isQuery*/));
@@ -128,6 +189,12 @@ public class VoicemailContentProvider extends ContentProvider
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "delete: uri=" + uri +
+ " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
+ " CPID=" + Binder.getCallingPid() +
+ " User=" + UserUtils.getCurrentUserHandle(getContext()));
+ }
UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri);
SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
selectionBuilder.addClause(getPackageRestrictionClause(false/*isQuery*/));
@@ -136,14 +203,25 @@ public class VoicemailContentProvider extends ContentProvider
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
- UriData uriData = null;
- if (mode.equals("r")) {
- uriData = checkPermissionsAndCreateUriDataForRead(uri);
- } else {
- uriData = checkPermissionsAndCreateUriDataForWrite(uri);
+ boolean success = false;
+ try {
+ UriData uriData = null;
+ if (mode.equals("r")) {
+ uriData = checkPermissionsAndCreateUriDataForRead(uri);
+ } else {
+ uriData = checkPermissionsAndCreateUriDataForWrite(uri);
+ }
+ // openFileHelper() relies on "_data" column to be populated with the file path.
+ final ParcelFileDescriptor ret = getTableDelegate(uriData).openFile(uriData, mode);
+ success = true;
+ return ret;
+ } finally {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "openFile uri=" + uri + " mode=" + mode + " success=" + success +
+ " CPID=" + Binder.getCallingPid() +
+ " User=" + UserUtils.getCurrentUserHandle(getContext()));
+ }
}
- // openFileHelper() relies on "_data" column to be populated with the file path.
- return getTableDelegate(uriData).openFile(uriData, mode);
}
/** Returns the correct table delegate object that can handle this URI. */
@@ -171,7 +249,7 @@ public class VoicemailContentProvider extends ContentProvider
private final String mSourcePackage;
private final VoicemailUriType mUriType;
- public UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage) {
+ private UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage) {
mUriType = uriType;
mUri = uri;
mId = id;
@@ -248,13 +326,13 @@ public class VoicemailContentProvider extends ContentProvider
// If content values don't contain the provider, calculate the right provider to use.
if (!values.containsKey(SOURCE_PACKAGE_FIELD)) {
String provider = uriData.hasSourcePackage() ?
- uriData.getSourcePackage() : getCallingPackage_();
+ uriData.getSourcePackage() : getInjectedCallingPackage();
values.put(SOURCE_PACKAGE_FIELD, provider);
}
// You must have access to the provider given in values.
if (!mVoicemailPermissions.callerHasWriteAccess(getCallingPackage())) {
- checkPackagesMatch(getCallingPackage_(),
+ checkPackagesMatch(getInjectedCallingPackage(),
values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD),
uriData.getUri());
}
@@ -357,47 +435,17 @@ public class VoicemailContentProvider extends ContentProvider
throw new SecurityException(String.format(
"Provider %s does not have %s permission." +
"\nPlease set query parameter '%s' in the URI.\nURI: %s",
- getCallingPackage_(), android.Manifest.permission.WRITE_VOICEMAIL,
+ getInjectedCallingPackage(), android.Manifest.permission.WRITE_VOICEMAIL,
VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, uriData.getUri()));
}
- checkPackagesMatch(getCallingPackage_(), uriData.getSourcePackage(), uriData.getUri());
+ checkPackagesMatch(getInjectedCallingPackage(), uriData.getSourcePackage(),
+ uriData.getUri());
}
}
- /**
- * Gets the name of the calling package.
- * <p>
- * It's possible (though unlikely) for there to be more than one calling package (requires that
- * your manifest say you want to share process ids) in which case we will return an arbitrary
- * package name. It's also possible (though very unlikely) for us to be unable to work out what
- * your calling package is, in which case we will return null.
- */
- /* package for test */String getCallingPackage_() {
- int caller = Binder.getCallingUid();
- if (caller == 0) {
- return null;
- }
- String[] callerPackages = context().getPackageManager().getPackagesForUid(caller);
- if (callerPackages == null || callerPackages.length == 0) {
- return null;
- }
- if (callerPackages.length == 1) {
- return callerPackages[0];
- }
- // If we have more than one caller package, which is very unlikely, let's return the one
- // with the highest permissions. If more than one has the same permission, we don't care
- // which one we return.
- String bestSoFar = callerPackages[0];
- for (String callerPackage : callerPackages) {
- if (mVoicemailPermissions.packageHasWriteAccess(callerPackage)) {
- // Full always wins, we can return early.
- return callerPackage;
- }
- if (mVoicemailPermissions.packageHasOwnVoicemailAccess(callerPackage)) {
- bestSoFar = callerPackage;
- }
- }
- return bestSoFar;
+ @VisibleForTesting
+ String getInjectedCallingPackage() {
+ return super.getCallingPackage();
}
/**
@@ -408,7 +456,7 @@ public class VoicemailContentProvider extends ContentProvider
if (hasReadWritePermission(isQuery)) {
return null;
}
- return getEqualityClause(Voicemails.SOURCE_PACKAGE, getCallingPackage_());
+ return getEqualityClause(Voicemails.SOURCE_PACKAGE, getInjectedCallingPackage());
}
/**
@@ -424,4 +472,50 @@ public class VoicemailContentProvider extends ContentProvider
return read ? mVoicemailPermissions.callerHasReadAccess(getCallingPackage()) :
mVoicemailPermissions.callerHasWriteAccess(getCallingPackage());
}
+
+ /** Remove all records from a given source package. */
+ public void removeBySourcePackage(String packageName) {
+ delete(Voicemails.buildSourceUri(packageName), null, null);
+ delete(Status.buildSourceUri(packageName), null, null);
+ }
+
+ @VisibleForTesting
+ void performBackgroundTask(int task, Object arg) {
+ switch (task) {
+ case BACKGROUND_TASK_SCAN_STALE_PACKAGES:
+ removeStalePackages();
+ break;
+ }
+ }
+
+ /**
+ * Remove all records made by packages that no longer exist.
+ */
+ private void removeStalePackages() {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "scanStalePackages start");
+ }
+
+ // Make sure all source tables still exists.
+
+ // First, list all source packages.
+ final ArraySet<String> packages = mVoicemailContentTable.getSourcePackages();
+ packages.addAll(mVoicemailStatusTable.getSourcePackages());
+
+ // Remove the ones that still exist.
+ for (int i = packages.size() - 1; i >= 0; i--) {
+ final String pkg = packages.valueAt(i);
+ final boolean installed = PackageUtils.isPackageInstalled(getContext(), pkg);
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, " " + pkg + (installed ? " installed" : " removed"));
+ }
+ if (!installed) {
+ removeBySourcePackage(pkg);
+ }
+ }
+
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "scanStalePackages finish");
+ }
+ }
}
diff --git a/src/com/android/providers/contacts/VoicemailContentTable.java b/src/com/android/providers/contacts/VoicemailContentTable.java
index 75f95741..09a8c1f0 100644
--- a/src/com/android/providers/contacts/VoicemailContentTable.java
+++ b/src/com/android/providers/contacts/VoicemailContentTable.java
@@ -31,11 +31,15 @@ import android.os.ParcelFileDescriptor;
import android.provider.CallLog.Calls;
import android.provider.OpenableColumns;
import android.provider.VoicemailContract.Voicemails;
+import android.util.ArraySet;
import android.util.Log;
+
import com.android.common.content.ProjectionMap;
import com.android.providers.contacts.VoicemailContentProvider.UriData;
import com.android.providers.contacts.util.CloseUtils;
+
import com.google.common.collect.ImmutableSet;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -60,6 +64,7 @@ public class VoicemailContentTable implements VoicemailTable.Delegate {
.add(Voicemails.DURATION)
.add(Voicemails.IS_READ)
.add(Voicemails.TRANSCRIPTION)
+ .add(Voicemails.TRANSCRIPTION_STATE)
.add(Voicemails.STATE)
.add(Voicemails.SOURCE_DATA)
.add(Voicemails.SOURCE_PACKAGE)
@@ -98,6 +103,7 @@ public class VoicemailContentTable implements VoicemailTable.Delegate {
.add(Voicemails.DURATION)
.add(Voicemails.IS_READ)
.add(Voicemails.TRANSCRIPTION)
+ .add(Voicemails.TRANSCRIPTION_STATE)
.add(Voicemails.STATE)
.add(Voicemails.SOURCE_DATA)
.add(Voicemails.SOURCE_PACKAGE)
@@ -282,6 +288,11 @@ public class VoicemailContentTable implements VoicemailTable.Delegate {
return mDelegateHelper.openDataFile(uriData, mode);
}
+ @Override
+ public ArraySet<String> getSourcePackages() {
+ return mDbHelper.selectDistinctColumn(mTableName, Voicemails.SOURCE_PACKAGE);
+ }
+
/** Creates a clause to restrict the selection to only voicemail call type.*/
private String getCallTypeClause() {
return getEqualityClause(Calls.TYPE, Calls.VOICEMAIL_TYPE);
diff --git a/src/com/android/providers/contacts/VoicemailStatusTable.java b/src/com/android/providers/contacts/VoicemailStatusTable.java
index 52da2927..f3008c0e 100644
--- a/src/com/android/providers/contacts/VoicemailStatusTable.java
+++ b/src/com/android/providers/contacts/VoicemailStatusTable.java
@@ -27,9 +27,13 @@ import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.VoicemailContract.Status;
+import android.util.ArraySet;
+
import com.android.common.content.ProjectionMap;
import com.android.providers.contacts.VoicemailContentProvider.UriData;
+import java.util.Set;
+
/**
* Implementation of {@link VoicemailTable.Delegate} for the voicemail status table.
*
@@ -153,4 +157,9 @@ public class VoicemailStatusTable implements VoicemailTable.Delegate {
private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) {
return new DbModifierWithNotification(mTableName, db, mContext);
}
+
+ @Override
+ public ArraySet<String> getSourcePackages() {
+ return mDbHelper.selectDistinctColumn(mTableName, Status.SOURCE_PACKAGE);
+ }
}
diff --git a/src/com/android/providers/contacts/VoicemailTable.java b/src/com/android/providers/contacts/VoicemailTable.java
index 9e6c4312..fcb653ce 100644
--- a/src/com/android/providers/contacts/VoicemailTable.java
+++ b/src/com/android/providers/contacts/VoicemailTable.java
@@ -20,6 +20,7 @@ import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
+import android.util.ArraySet;
import com.android.providers.contacts.VoicemailContentProvider.UriData;
@@ -44,6 +45,7 @@ public interface VoicemailTable {
public String getType(UriData uriData);
public ParcelFileDescriptor openFile(UriData uriData, String mode)
throws FileNotFoundException;
+ public ArraySet<String> getSourcePackages();
}
/**
diff --git a/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java b/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java
index 1501138c..20e3bbec 100644
--- a/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java
+++ b/src/com/android/providers/contacts/aggregation/AbstractContactAggregator.java
@@ -69,6 +69,7 @@ import android.provider.ContactsContract.StatusUpdates;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
+import android.util.Slog;
import java.util.ArrayList;
import java.util.Collections;
@@ -917,6 +918,10 @@ public abstract class AbstractContactAggregator {
* Updates the contact ID for the specified contact and marks the raw contact as aggregated.
*/
private void setContactIdAndMarkAggregated(long rawContactId, long contactId) {
+ if (contactId == 0) {
+ // Use Slog instead of Log, to prevent the process from crashing.
+ Slog.wtfStack(TAG, "Detected contact-id 0");
+ }
mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId);
mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId);
mContactIdAndMarkAggregatedUpdate.execute();
@@ -1263,8 +1268,8 @@ public abstract class AbstractContactAggregator {
+ RawContacts.SOURCE_ID + ","
+ RawContacts.CUSTOM_RINGTONE + ","
+ RawContacts.SEND_TO_VOICEMAIL + ","
- + RawContacts.LAST_TIME_CONTACTED + ","
- + RawContacts.TIMES_CONTACTED + ","
+ + RawContacts.RAW_LAST_TIME_CONTACTED + ","
+ + RawContacts.RAW_TIMES_CONTACTED + ","
+ RawContacts.STARRED + ","
+ RawContacts.PINNED + ","
+ DataColumns.CONCRETE_ID + ","
@@ -1299,8 +1304,8 @@ public abstract class AbstractContactAggregator {
int SOURCE_ID = 6;
int CUSTOM_RINGTONE = 7;
int SEND_TO_VOICEMAIL = 8;
- int LAST_TIME_CONTACTED = 9;
- int TIMES_CONTACTED = 10;
+ int RAW_LAST_TIME_CONTACTED = 9;
+ int RAW_TIMES_CONTACTED = 10;
int STARRED = 11;
int PINNED = 12;
int DATA_ID = 13;
@@ -1319,8 +1324,8 @@ public abstract class AbstractContactAggregator {
+ Contacts.PHOTO_FILE_ID + "=?, "
+ Contacts.SEND_TO_VOICEMAIL + "=?, "
+ Contacts.CUSTOM_RINGTONE + "=?, "
- + Contacts.LAST_TIME_CONTACTED + "=?, "
- + Contacts.TIMES_CONTACTED + "=?, "
+ + Contacts.RAW_LAST_TIME_CONTACTED + "=?, "
+ + Contacts.RAW_TIMES_CONTACTED + "=?, "
+ Contacts.STARRED + "=?, "
+ Contacts.PINNED + "=?, "
+ Contacts.HAS_PHONE_NUMBER + "=?, "
@@ -1335,8 +1340,8 @@ public abstract class AbstractContactAggregator {
+ Contacts.PHOTO_FILE_ID + ", "
+ Contacts.SEND_TO_VOICEMAIL + ", "
+ Contacts.CUSTOM_RINGTONE + ", "
- + Contacts.LAST_TIME_CONTACTED + ", "
- + Contacts.TIMES_CONTACTED + ", "
+ + Contacts.RAW_LAST_TIME_CONTACTED + ", "
+ + Contacts.RAW_TIMES_CONTACTED + ", "
+ Contacts.STARRED + ", "
+ Contacts.PINNED + ", "
+ Contacts.HAS_PHONE_NUMBER + ", "
@@ -1350,8 +1355,8 @@ public abstract class AbstractContactAggregator {
int PHOTO_FILE_ID = 3;
int SEND_TO_VOICEMAIL = 4;
int CUSTOM_RINGTONE = 5;
- int LAST_TIME_CONTACTED = 6;
- int TIMES_CONTACTED = 7;
+ int RAW_LAST_TIME_CONTACTED = 6;
+ int RAW_TIMES_CONTACTED = 7;
int STARRED = 8;
int PINNED = 9;
int HAS_PHONE_NUMBER = 10;
@@ -1439,12 +1444,12 @@ public abstract class AbstractContactAggregator {
contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
}
- long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED);
+ long lastTimeContacted = c.getLong(RawContactsQuery.RAW_LAST_TIME_CONTACTED);
if (lastTimeContacted > contactLastTimeContacted) {
contactLastTimeContacted = lastTimeContacted;
}
- int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED);
+ int timesContacted = c.getInt(RawContactsQuery.RAW_TIMES_CONTACTED);
if (timesContacted > contactTimesContacted) {
contactTimesContacted = timesContacted;
}
@@ -1523,9 +1528,9 @@ public abstract class AbstractContactAggregator {
totalRowCount == contactSendToVoicemail ? 1 : 0);
DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
contactCustomRingtone);
- statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
+ statement.bindLong(ContactReplaceSqlStatement.RAW_LAST_TIME_CONTACTED,
contactLastTimeContacted);
- statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
+ statement.bindLong(ContactReplaceSqlStatement.RAW_TIMES_CONTACTED,
contactTimesContacted);
statement.bindLong(ContactReplaceSqlStatement.STARRED,
contactStarred);
diff --git a/src/com/android/providers/contacts/sqlite/DatabaseAnalyzer.java b/src/com/android/providers/contacts/sqlite/DatabaseAnalyzer.java
new file mode 100644
index 00000000..facd02e2
--- /dev/null
+++ b/src/com/android/providers/contacts/sqlite/DatabaseAnalyzer.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2016 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.sqlite;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import com.android.providers.contacts.AbstractContactsProvider;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class to extract table/view/column names from databases.
+ */
+@VisibleForTesting
+public class DatabaseAnalyzer {
+ private static final String TAG = "DatabaseAnalyzer";
+
+ private static final boolean VERBOSE_LOGGING = AbstractContactsProvider.VERBOSE_LOGGING;
+
+ private DatabaseAnalyzer() {
+ }
+
+ /**
+ * Find and return all table/view names in a db.
+ */
+ private static List<String> findTablesAndViews(SQLiteDatabase db) {
+ final List<String> ret = new ArrayList<>();
+ try (final Cursor c = db.rawQuery(
+ "SELECT name FROM sqlite_master WHERE type in (\"table\", \"view\")", null)) {
+ while (c.moveToNext()) {
+ ret.add(c.getString(0).toLowerCase());
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Find all columns in a table/view.
+ */
+ private static List<String> findColumns(SQLiteDatabase db, String table) {
+ final List<String> ret = new ArrayList<>();
+
+ // Open the table/view but requests 0 rows.
+ final Cursor c = db.rawQuery("SELECT * FROM " + table + " WHERE 0 LIMIT 0", null);
+ try {
+ // Collect the column names.
+ for (int i = 0; i < c.getColumnCount(); i++) {
+ ret.add(c.getColumnName(i).toLowerCase());
+ }
+ } finally {
+ c.close();
+ }
+ return ret;
+ }
+
+ /**
+ * Return all table/view names that clients shouldn't use in their queries -- basically the
+ * result contains all table/view names, except for the names that are column names of any
+ * tables.
+ */
+ @VisibleForTesting
+ public static List<String> findTableViewsAllowingColumns(SQLiteDatabase db) {
+ final List<String> tables = findTablesAndViews(db);
+ if (VERBOSE_LOGGING) {
+ Log.d(TAG, "Tables and views:");
+ }
+ final List<String> ret = new ArrayList<>(tables); // Start with the table/view list.
+ for (String name : tables) {
+ if (VERBOSE_LOGGING) {
+ Log.d(TAG, " " + name);
+ }
+ final List<String> columns = findColumns(db, name);
+ if (VERBOSE_LOGGING) {
+ Log.d(TAG, " Columns: " + columns);
+ }
+ for (String c : columns) {
+ if (ret.remove(c)) {
+ Log.d(TAG, "Removing [" + c + "] from disallow list");
+ }
+ }
+ }
+ return ret;
+ }
+}
diff --git a/src/com/android/providers/contacts/sqlite/SqlChecker.java b/src/com/android/providers/contacts/sqlite/SqlChecker.java
new file mode 100644
index 00000000..2db1f479
--- /dev/null
+++ b/src/com/android/providers/contacts/sqlite/SqlChecker.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2016 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.sqlite;
+
+import android.annotation.Nullable;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.providers.contacts.AbstractContactsProvider;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+/**
+ * Simple SQL validator to detect uses of hidden tables / columns as well as invalid SQLs.
+ */
+public class SqlChecker {
+ private static final String TAG = "SqlChecker";
+
+ private static final String PRIVATE_PREFIX = "x_"; // MUST BE LOWERCASE.
+
+ private static final boolean VERBOSE_LOGGING = AbstractContactsProvider.VERBOSE_LOGGING;
+
+ private final ArraySet<String> mInvalidTokens;
+
+ /**
+ * Create a new instance with given invalid tokens.
+ */
+ public SqlChecker(List<String> invalidTokens) {
+ mInvalidTokens = new ArraySet<>(invalidTokens.size());
+
+ for (int i = invalidTokens.size() - 1; i >= 0; i--) {
+ mInvalidTokens.add(invalidTokens.get(i).toLowerCase());
+ }
+ if (VERBOSE_LOGGING) {
+ Log.d(TAG, "Initialized with invalid tokens: " + invalidTokens);
+ }
+ }
+
+ private static boolean isAlpha(char ch) {
+ return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_');
+ }
+
+ private static boolean isNum(char ch) {
+ return ('0' <= ch && ch <= '9');
+ }
+
+ private static boolean isAlNum(char ch) {
+ return isAlpha(ch) || isNum(ch);
+ }
+
+ private static boolean isAnyOf(char ch, String set) {
+ return set.indexOf(ch) >= 0;
+ }
+
+ /**
+ * Exception for invalid queries.
+ */
+ @VisibleForTesting
+ public static final class InvalidSqlException extends IllegalArgumentException {
+ public InvalidSqlException(String s) {
+ super(s);
+ }
+ }
+
+ private static InvalidSqlException genException(String message, String sql) {
+ throw new InvalidSqlException(message + " in '" + sql + "'");
+ }
+
+ private void throwIfContainsToken(String token, String sql) {
+ final String lower = token.toLowerCase();
+ if (mInvalidTokens.contains(lower) || lower.startsWith(PRIVATE_PREFIX)) {
+ throw genException("Detected disallowed token: " + token, sql);
+ }
+ }
+
+ /**
+ * Ensure {@code sql} is valid and doesn't contain invalid tokens.
+ */
+ public void ensureNoInvalidTokens(@Nullable String sql) {
+ findTokens(sql, OPTION_NONE, token -> throwIfContainsToken(token, sql));
+ }
+
+ /**
+ * Ensure {@code sql} only contains a single, valid token. Use to validate column names
+ * in {@link android.content.ContentValues}.
+ */
+ public void ensureSingleTokenOnly(@Nullable String sql) {
+ final AtomicBoolean tokenFound = new AtomicBoolean();
+
+ findTokens(sql, OPTION_TOKEN_ONLY, token -> {
+ if (tokenFound.get()) {
+ throw genException("Multiple tokens detected", sql);
+ }
+ tokenFound.set(true);
+ throwIfContainsToken(token, sql);
+ });
+ if (!tokenFound.get()) {
+ throw genException("Token not found", sql);
+ }
+ }
+
+ @VisibleForTesting
+ static final int OPTION_NONE = 0;
+
+ @VisibleForTesting
+ static final int OPTION_TOKEN_ONLY = 1 << 0;
+
+ private static char peek(String s, int index) {
+ return index < s.length() ? s.charAt(index) : '\0';
+ }
+
+ /**
+ * SQL Tokenizer specialized to extract tokens from SQL (snippets).
+ *
+ * Based on sqlite3GetToken() in tokenzie.c in SQLite.
+ *
+ * Source for v3.8.6 (which android uses): http://www.sqlite.org/src/artifact/ae45399d6252b4d7
+ * (Latest source as of now: http://www.sqlite.org/src/artifact/78c8085bc7af1922)
+ *
+ * Also draft spec: http://www.sqlite.org/draft/tokenreq.html
+ */
+ @VisibleForTesting
+ static void findTokens(@Nullable String sql, int options, Consumer<String> checker) {
+ if (sql == null) {
+ return;
+ }
+ int pos = 0;
+ final int len = sql.length();
+ while (pos < len) {
+ final char ch = peek(sql, pos);
+
+ // Regular token.
+ if (isAlpha(ch)) {
+ final int start = pos;
+ pos++;
+ while (isAlNum(peek(sql, pos))) {
+ pos++;
+ }
+ final int end = pos;
+
+ final String token = sql.substring(start, end);
+ checker.accept(token);
+
+ continue;
+ }
+
+ // Handle quoted tokens
+ if (isAnyOf(ch, "'\"`")) {
+ final int quoteStart = pos;
+ pos++;
+
+ for (;;) {
+ pos = sql.indexOf(ch, pos);
+ if (pos < 0) {
+ throw genException("Unterminated quote", sql);
+ }
+ if (peek(sql, pos + 1) != ch) {
+ break;
+ }
+ // Quoted quote char -- e.g. "abc""def" is a single string.
+ pos += 2;
+ }
+ final int quoteEnd = pos;
+ pos++;
+
+ if (ch != '\'') {
+ // Extract the token
+ final String tokenUnquoted = sql.substring(quoteStart + 1, quoteEnd);
+
+ final String token;
+
+ // Unquote if needed. i.e. "aa""bb" -> aa"bb
+ if (tokenUnquoted.indexOf(ch) >= 0) {
+ token = tokenUnquoted.replaceAll(
+ String.valueOf(ch) + ch, String.valueOf(ch));
+ } else {
+ token = tokenUnquoted;
+ }
+ checker.accept(token);
+ } else {
+ if ((options &= OPTION_TOKEN_ONLY) != 0) {
+ throw genException("Non-token detected", sql);
+ }
+ }
+ continue;
+ }
+ // Handle tokens enclosed in [...]
+ if (ch == '[') {
+ final int quoteStart = pos;
+ pos++;
+
+ pos = sql.indexOf(']', pos);
+ if (pos < 0) {
+ throw genException("Unterminated quote", sql);
+ }
+ final int quoteEnd = pos;
+ pos++;
+
+ final String token = sql.substring(quoteStart + 1, quoteEnd);
+
+ checker.accept(token);
+ continue;
+ }
+ if ((options &= OPTION_TOKEN_ONLY) != 0) {
+ throw genException("Non-token detected", sql);
+ }
+
+ // Detect comments.
+ if (ch == '-' && peek(sql, pos + 1) == '-') {
+ pos += 2;
+ pos = sql.indexOf('\n', pos);
+ if (pos < 0) {
+ // We disallow strings ending in an inline comment.
+ throw genException("Unterminated comment", sql);
+ }
+ pos++;
+
+ continue;
+ }
+ if (ch == '/' && peek(sql, pos + 1) == '*') {
+ pos += 2;
+ pos = sql.indexOf("*/", pos);
+ if (pos < 0) {
+ throw genException("Unterminated comment", sql);
+ }
+ pos += 2;
+
+ continue;
+ }
+
+ // Semicolon is never allowed.
+ if (ch == ';') {
+ throw genException("Semicolon is not allowed", sql);
+ }
+
+ // For this purpose, we can simply ignore other characters.
+ // (Note it doesn't handle the X'' literal properly and reports this X as a token,
+ // but that should be fine...)
+ pos++;
+ }
+ }
+}
diff --git a/src/com/android/providers/contacts/util/DbQueryUtils.java b/src/com/android/providers/contacts/util/DbQueryUtils.java
index d719313a..23c144ac 100644
--- a/src/com/android/providers/contacts/util/DbQueryUtils.java
+++ b/src/com/android/providers/contacts/util/DbQueryUtils.java
@@ -125,7 +125,7 @@ public class DbQueryUtils {
public static void escapeLikeValue(StringBuilder sb, String value, char escapeChar) {
for (int i = 0; i < value.length(); i++) {
char ch = value.charAt(i);
- if (ch == '%' || ch == '_') {
+ if (ch == '%' || ch == '_' || ch == escapeChar) {
sb.append(escapeChar);
}
sb.append(ch);
diff --git a/src/com/android/providers/contacts/util/PackageUtils.java b/src/com/android/providers/contacts/util/PackageUtils.java
new file mode 100644
index 00000000..f4840458
--- /dev/null
+++ b/src/com/android/providers/contacts/util/PackageUtils.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts.util;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+
+public class PackageUtils {
+ private PackageUtils() {
+ }
+
+ /**
+ * @return TRUE if the given package is installed for this user.
+ */
+ public static boolean isPackageInstalled(Context context, String packageName) {
+ try {
+ // Need to pass MATCH_UNINSTALLED_PACKAGES to fetch it even if the package is
+ // being updated. Then use FLAG_INSTALLED to see if it's actually installed for this
+ // user.
+ final ApplicationInfo ai = context.getPackageManager().getApplicationInfo(packageName,
+ PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_UNINSTALLED_PACKAGES);
+ return (ai != null) && ((ai.flags & ApplicationInfo.FLAG_INSTALLED) != 0);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+}
diff --git a/test_common/Android.mk b/test_common/Android.mk
new file mode 100644
index 00000000..2965f8a0
--- /dev/null
+++ b/test_common/Android.mk
@@ -0,0 +1,31 @@
+# Copyright (C) 2016 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, src)
+
+LOCAL_JAVA_LIBRARIES := \
+ android.test.runner \
+ android-support-test \
+ mockito-target-minus-junit4
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := ContactsProviderTestUtils
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/src/com/android/providers/contacts/testutil/CommonDatabaseUtils.java b/test_common/src/com/android/providers/contacts/testutil/CommonDatabaseUtils.java
index cb78a1b6..cb78a1b6 100644
--- a/tests/src/com/android/providers/contacts/testutil/CommonDatabaseUtils.java
+++ b/test_common/src/com/android/providers/contacts/testutil/CommonDatabaseUtils.java
diff --git a/tests/src/com/android/providers/contacts/testutil/ContactUtil.java b/test_common/src/com/android/providers/contacts/testutil/ContactUtil.java
index 442c5e71..442c5e71 100644
--- a/tests/src/com/android/providers/contacts/testutil/ContactUtil.java
+++ b/test_common/src/com/android/providers/contacts/testutil/ContactUtil.java
diff --git a/tests/src/com/android/providers/contacts/testutil/DataUtil.java b/test_common/src/com/android/providers/contacts/testutil/DataUtil.java
index 2afd5674..32e76197 100644
--- a/tests/src/com/android/providers/contacts/testutil/DataUtil.java
+++ b/test_common/src/com/android/providers/contacts/testutil/DataUtil.java
@@ -23,7 +23,6 @@ import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Data;
-import android.test.mock.MockContentResolver;
/**
* Convenience methods for operating on the Data table.
diff --git a/tests/src/com/android/providers/contacts/testutil/DatabaseAsserts.java b/test_common/src/com/android/providers/contacts/testutil/DatabaseAsserts.java
index 2af98299..2af98299 100644
--- a/tests/src/com/android/providers/contacts/testutil/DatabaseAsserts.java
+++ b/test_common/src/com/android/providers/contacts/testutil/DatabaseAsserts.java
diff --git a/tests/src/com/android/providers/contacts/testutil/DeletedContactUtil.java b/test_common/src/com/android/providers/contacts/testutil/DeletedContactUtil.java
index 2dab7f90..2dab7f90 100644
--- a/tests/src/com/android/providers/contacts/testutil/DeletedContactUtil.java
+++ b/test_common/src/com/android/providers/contacts/testutil/DeletedContactUtil.java
diff --git a/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java b/test_common/src/com/android/providers/contacts/testutil/RawContactUtil.java
index f24875b7..cef6d6df 100644
--- a/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java
+++ b/test_common/src/com/android/providers/contacts/testutil/RawContactUtil.java
@@ -23,8 +23,6 @@ import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
-import android.test.mock.MockContentResolver;
-import com.android.providers.contacts.AccountWithDataSet;
import java.util.List;
@@ -49,18 +47,6 @@ public class RawContactUtil {
return CommonDatabaseUtils.singleRecordToArray(cursor);
}
- /**
- * Returns a list of raw contact records.
- *
- * @return A list of records. Where each record is represented as an array of strings.
- */
- public static List<String[]> queryByContactId(ContentResolver resolver, long contactId,
- String[] projection) {
- Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, contactId);
- Cursor cursor = resolver.query(uri, projection, null, null, null);
- return CommonDatabaseUtils.multiRecordToArray(cursor);
- }
-
public static void delete(ContentResolver resolver, long rawContactId,
boolean isSyncAdapter) {
Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId)
@@ -98,11 +84,11 @@ public class RawContactUtil {
}
public static long createRawContactWithAccountDataSet(ContentResolver resolver,
- AccountWithDataSet accountWithDataSet, String... extras) {
+ String accountName, String accountType, String dataSet, String... extras) {
ContentValues values = new ContentValues();
CommonDatabaseUtils.extrasVarArgsToValues(values, extras);
final Uri uri = TestUtil.maybeAddAccountWithDataSetQueryParameters(
- ContactsContract.RawContacts.CONTENT_URI, accountWithDataSet);
+ ContactsContract.RawContacts.CONTENT_URI, accountName, accountType, dataSet);
Uri contactUri = resolver.insert(uri, values);
return ContentUris.parseId(contactUri);
}
diff --git a/tests/src/com/android/providers/contacts/testutil/TestUtil.java b/test_common/src/com/android/providers/contacts/testutil/TestUtil.java
index 05ff61d1..6c8c689a 100644
--- a/tests/src/com/android/providers/contacts/testutil/TestUtil.java
+++ b/test_common/src/com/android/providers/contacts/testutil/TestUtil.java
@@ -20,7 +20,6 @@ import android.accounts.Account;
import android.net.Uri;
import android.provider.ContactsContract.RawContacts;
import android.util.Log;
-import com.android.providers.contacts.AccountWithDataSet;
/**
* Common methods used for testing.
@@ -53,14 +52,14 @@ public class TestUtil {
}
public static Uri maybeAddAccountWithDataSetQueryParameters(Uri uri,
- AccountWithDataSet account) {
- if (account == null) {
+ String accountName, String accountType, String dataSet) {
+ if (accountName == null && accountType == null) {
return uri;
}
return uri.buildUpon()
- .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.getAccountName())
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.getAccountType())
- .appendQueryParameter(RawContacts.DATA_SET, account.getDataSet())
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType)
+ .appendQueryParameter(RawContacts.DATA_SET, dataSet)
.build();
}
}
diff --git a/tests/Android.mk b/tests/Android.mk
index f2e0fc1a..8df1d6d4 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -4,7 +4,11 @@ include $(CLEAR_VARS)
# We only want this apk build for tests.
LOCAL_MODULE_TAGS := tests
-LOCAL_STATIC_JAVA_LIBRARIES := mockito-target legacy-android-test
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ ContactsProviderTestUtils \
+ android-support-test \
+ mockito-target-minus-junit4 \
+ legacy-android-test
LOCAL_JAVA_LIBRARIES := android.test.runner
@@ -12,6 +16,7 @@ LOCAL_JAVA_LIBRARIES := android.test.runner
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_PACKAGE_NAME := ContactsProviderTests
+LOCAL_COMPATIBILITY_SUITE := device-tests
LOCAL_INSTRUMENTATION_FOR := ContactsProvider
LOCAL_CERTIFICATE := shared
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
new file mode 100644
index 00000000..46e97bc4
--- /dev/null
+++ b/tests/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<configuration description="Runs Contacts Provider Tests.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="ContactsProviderTests.apk" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-tag" value="ContactsProviderTests" />
+ <test class="com.android.tradefed.testtype.InstrumentationTest" >
+ <option name="package" value="com.android.providers.contacts.tests" />
+ <option name="runner" value="android.test.InstrumentationTestRunner" />
+ </test>
+</configuration>
diff --git a/tests/assets/upgradeTest/pre_upgrade1201.sql b/tests/assets/upgradeTest/pre_upgrade1201.sql
new file mode 100644
index 00000000..6a284ead
--- /dev/null
+++ b/tests/assets/upgradeTest/pre_upgrade1201.sql
@@ -0,0 +1,27 @@
+DELETE FROM accounts;
+DELETE FROM contacts;
+DELETE FROM raw_contacts;
+DELETE FROM data;
+DELETE FROM data_usage_stat;
+
+--CREATE TABLE accounts (_id INTEGER PRIMARY KEY AUTOINCREMENT,account_name TEXT, account_type TEXT, data_set TEXT);
+
+INSERT INTO "accounts" VALUES(1,NULL,NULL,NULL);
+
+--CREATE TABLE contacts (_id INTEGER PRIMARY KEY AUTOINCREMENT,name_raw_contact_id INTEGER REFERENCES raw_contacts(_id),photo_id INTEGER REFERENCES data(_id),photo_file_id INTEGER REFERENCES photo_files(_id),custom_ringtone TEXT,send_to_voicemail INTEGER NOT NULL DEFAULT 0,times_contacted INTEGER NOT NULL DEFAULT 0,last_time_contacted INTEGER,starred INTEGER NOT NULL DEFAULT 0,pinned INTEGER NOT NULL DEFAULT 0,has_phone_number INTEGER NOT NULL DEFAULT 0,lookup TEXT,status_update_id INTEGER REFERENCES data(_id),contact_last_updated_timestamp INTEGER);
+
+INSERT INTO "contacts" VALUES(1,1,NULL,NULL,NULL,0,4,9940760264,0,0,1,NULL,NULL,9940760265);
+INSERT INTO "contacts" VALUES(2,2,NULL,NULL,NULL,0,0,0,0,0,1,NULL,NULL,9940366668);
+
+--CREATE TABLE raw_contacts (_id INTEGER PRIMARY KEY AUTOINCREMENT,account_id INTEGER REFERENCES accounts(_id),sourceid TEXT,backup_id TEXT,raw_contact_is_read_only INTEGER NOT NULL DEFAULT 0,version INTEGER NOT NULL DEFAULT 1,dirty INTEGER NOT NULL DEFAULT 0,deleted INTEGER NOT NULL DEFAULT 0,metadata_dirty INTEGER NOT NULL DEFAULT 0,contact_id INTEGER REFERENCES contacts(_id),aggregation_mode INTEGER NOT NULL DEFAULT 0,aggregation_needed INTEGER NOT NULL DEFAULT 1,custom_ringtone TEXT,send_to_voicemail INTEGER NOT NULL DEFAULT 0,times_contacted INTEGER NOT NULL DEFAULT 0,last_time_contacted INTEGER,starred INTEGER NOT NULL DEFAULT 0,pinned INTEGER NOT NULL DEFAULT 0,display_name TEXT,display_name_alt TEXT,display_name_source INTEGER NOT NULL DEFAULT 0,phonetic_name TEXT,phonetic_name_style TEXT,sort_key TEXT COLLATE PHONEBOOK,phonebook_label TEXT,phonebook_bucket INTEGER,sort_key_alt TEXT COLLATE PHONEBOOK,phonebook_label_alt TEXT,phonebook_bucket_alt INTEGER,name_verified INTEGER NOT NULL DEFAULT 0,sync1 TEXT, sync2 TEXT, sync3 TEXT, sync4 TEXT );
+
+INSERT INTO "raw_contacts" VALUES(1,1,NULL,NULL,0,3,1,0,0,1,0,0,NULL,0,4,9940760264,0,0,'Test','Test',40,NULL,'0','Test','T',20,'Test','T',20,0,NULL,NULL,NULL,NULL);
+INSERT INTO "raw_contacts" VALUES(2,1,NULL,NULL,0,3,1,0,0,2,0,0,NULL,0,0,NULL,0,0,'Test2','Test2',40,NULL,'0','Test2','T',20,'Test2','T',20,0,NULL,NULL,NULL,NULL);
+
+--CREATE TABLE data (_id INTEGER PRIMARY KEY AUTOINCREMENT,package_id INTEGER REFERENCES package(_id),mimetype_id INTEGER REFERENCES mimetype(_id) NOT NULL,raw_contact_id INTEGER REFERENCES raw_contacts(_id) NOT NULL,hash_id TEXT,is_read_only INTEGER NOT NULL DEFAULT 0,is_primary INTEGER NOT NULL DEFAULT 0,is_super_primary INTEGER NOT NULL DEFAULT 0,data_version INTEGER NOT NULL DEFAULT 0,data1 TEXT,data2 TEXT,data3 TEXT,data4 TEXT,data5 TEXT,data6 TEXT,data7 TEXT,data8 TEXT,data9 TEXT,data10 TEXT,data11 TEXT,data12 TEXT,data13 TEXT,data14 TEXT,data15 TEXT,data_sync1 TEXT, data_sync2 TEXT, data_sync3 TEXT, data_sync4 TEXT, carrier_presence INTEGER NOT NULL DEFAULT 0 );
+
+INSERT INTO "data" VALUES(1,NULL,5,1,NULL,0,0,0,0,'555','2',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0);
+
+--CREATE TABLE data_usage_stat(stat_id INTEGER PRIMARY KEY AUTOINCREMENT, data_id INTEGER NOT NULL, usage_type INTEGER NOT NULL DEFAULT 0, times_used INTEGER NOT NULL DEFAULT 0, last_time_used INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(data_id) REFERENCES data(_id));
+
+INSERT INTO "data_usage_stat" VALUES(1,1,0,4,9940760264);
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 09241549..74642cb7 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -18,6 +18,7 @@ package com.android.providers.contacts;
import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
import static com.android.providers.contacts.TestUtils.cv;
+import static com.android.providers.contacts.TestUtils.dumpCursor;
import android.accounts.Account;
import android.content.ContentProvider;
@@ -127,7 +128,8 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase {
protected void setUp() throws Exception {
super.setUp();
- mActor = new ContactsActor(getContext(), PACKAGE_GREY, getProviderClass(), getAuthority());
+ mActor = new ContactsActor(
+ getContext(), getContextPackageName(), getProviderClass(), getAuthority());
mResolver = mActor.resolver;
if (mActor.provider instanceof SynchronousContactsProvider2) {
getContactsProvider().wipeData();
@@ -142,8 +144,13 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase {
"android.permission.WRITE_SOCIAL_STREAM");
}
+ protected String getContextPackageName() {
+ return PACKAGE_GREY;
+ }
+
@Override
protected void tearDown() throws Exception {
+ mActor.shutdown();
sMockClock.uninstall();
super.tearDown();
}
@@ -1095,6 +1102,9 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase {
assertCursorValues(c, values);
} catch (Error e) {
TestUtils.dumpCursor(c);
+
+ // Dump with no selection.
+ TestUtils.dumpUri(mResolver, uri);
throw e;
} finally {
c.close();
@@ -1158,7 +1168,7 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase {
}
}
- private void assertCursorValuesOrderly(Cursor cursor, ContentValues... expectedValues) {
+ public static void assertCursorValuesOrderly(Cursor cursor, ContentValues... expectedValues) {
StringBuilder message = new StringBuilder();
cursor.moveToPosition(-1);
for (ContentValues v : expectedValues) {
@@ -1188,7 +1198,7 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase {
return true;
}
- private boolean equalsWithExpectedValues(Cursor cursor, ContentValues expectedValues,
+ private static boolean equalsWithExpectedValues(Cursor cursor, ContentValues expectedValues,
StringBuilder msgBuffer) {
for (String column : expectedValues.keySet()) {
int index = cursor.getColumnIndex(column);
@@ -1229,6 +1239,7 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase {
final Cursor cursor = mResolver.query(uri, DATA_USAGE_PROJECTION, null, null,
null);
try {
+ dumpCursor(cursor);
assertCursorHasAnyRecordMatch(cursor, cv(Data.DATA1, data1, Data.TIMES_USED, timesUsed,
Data.LAST_TIME_USED, lastTimeUsed));
} finally {
@@ -1372,6 +1383,17 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase {
.build();
mResolver.query(uri, null, null, null, null);
}
+
+ protected Uri insertRawContact(ContentValues values) {
+ return TestUtils.insertRawContact(mResolver,
+ getContactsProvider().getDatabaseHelper(), values);
+ }
+
+ protected Uri insertProfileRawContact(ContentValues values) {
+ return TestUtils.insertProfileRawContact(mResolver,
+ getContactsProvider().getProfileProviderForTest().getDatabaseHelper(), values);
+ }
+
/**
* A contact in the database, and the attributes used to create it. Construct using
* {@link GoldenContactBuilder#build()}.
diff --git a/tests/src/com/android/providers/contacts/BaseDatabaseHelperUpgradeTest.java b/tests/src/com/android/providers/contacts/BaseDatabaseHelperUpgradeTest.java
index 73303d00..0eb12ad8 100644
--- a/tests/src/com/android/providers/contacts/BaseDatabaseHelperUpgradeTest.java
+++ b/tests/src/com/android/providers/contacts/BaseDatabaseHelperUpgradeTest.java
@@ -29,7 +29,7 @@ import java.util.HashMap;
* Run the test like this: <code> runtest -c com.android.providers.contacts.BaseDatabaseHelperUpgradeTest
* contactsprov </code>
*/
-public class BaseDatabaseHelperUpgradeTest extends AndroidTestCase {
+public abstract class BaseDatabaseHelperUpgradeTest extends AndroidTestCase {
protected static final String INTEGER = "INTEGER";
protected static final String TEXT = "TEXT";
@@ -168,7 +168,14 @@ public class BaseDatabaseHelperUpgradeTest extends AndroidTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
- mDb = SQLiteDatabase.create(null);
+
+ final String filename = getDatabaseFilename();
+ if (filename == null) {
+ mDb = SQLiteDatabase.create(null);
+ } else {
+ getContext().deleteDatabase(filename);
+ mDb = SQLiteDatabase.openOrCreateDatabase(filename, null);
+ }
}
@Override
@@ -177,6 +184,8 @@ public class BaseDatabaseHelperUpgradeTest extends AndroidTestCase {
super.tearDown();
}
+ protected abstract String getDatabaseFilename();
+
protected void assertDatabaseStructureSameAsList(TableListEntry[] list, boolean isNewDatabase) {
for (TableListEntry entry : list) {
if (!entry.shouldBeInNewDb) {
diff --git a/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java b/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
index e70492d3..1020da89 100644
--- a/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
@@ -43,7 +43,6 @@ public abstract class BaseVoicemailProviderTest extends BaseContactsProvider2Tes
@Override
protected void setUp() throws Exception {
super.setUp();
- addProvider(TestVoicemailProvider.class, VoicemailContract.AUTHORITY);
TestVoicemailProvider.setVvmProviderCallDelegate(createMockProviderCalls());
mPackageManager = (ContactsMockPackageManager) getProvider()
@@ -165,6 +164,12 @@ public abstract class BaseVoicemailProviderTest extends BaseContactsProvider2Tes
mDelegate = delegate;
}
+ // Run the tasks synchronously.
+ @Override
+ void scheduleTask(int taskId, Object arg) {
+ performBackgroundTask(taskId, arg);
+ }
+
@Override
protected CallLogDatabaseHelper getDatabaseHelper(Context context) {
return new CallLogDatabaseHelperTestable(context, /* contactsDbForMigration = */ null);
@@ -189,7 +194,7 @@ public abstract class BaseVoicemailProviderTest extends BaseContactsProvider2Tes
}
@Override
- protected String getCallingPackage_() {
+ protected String getInjectedCallingPackage() {
return getContext().getPackageName();
}
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
index 8ee7a5be..f6078464 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -61,7 +61,7 @@ public class CallLogProviderTest extends BaseContactsProvider2Test {
Voicemails.DIRTY,
Voicemails.DELETED};
/** Total number of columns exposed by call_log provider. */
- private static final int NUM_CALLLOG_FIELDS = 30;
+ private static final int NUM_CALLLOG_FIELDS = 31;
private CallLogProviderTestable mCallLogProvider;
diff --git a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
index 8ad5ca17..b15d01ae 100644
--- a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
+++ b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
@@ -16,6 +16,8 @@
package com.android.providers.contacts;
+import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
+
import android.accounts.Account;
import android.content.ContentValues;
import android.content.Context;
@@ -120,6 +122,13 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
.getContext().getPackageManager();
}
+ @Override
+ protected String getContextPackageName() {
+ // In this test, we need to use the real package name, because that'll be recorded in the
+ // directory table, and if it's wrong, the tests would get confused.
+ return getContext().getPackageName();
+ }
+
public void testIsDirectoryProvider() {
ProviderInfo provider = new ProviderInfo();
@@ -172,7 +181,7 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY, Directory.SHORTCUT_SUPPORT_FULL,
Directory.PHOTO_SUPPORT_FULL);
- mDirectoryManager.scanAllPackages();
+ assertEquals(3, mDirectoryManager.scanAllPackages(/* rescan=*/ false));
Cursor cursor = mResolver.query(Directory.CONTENT_URI, null, null, null,
/* order by=*/ Directory.DIRECTORY_AUTHORITY + "," + Directory.ACCOUNT_NAME +
@@ -197,18 +206,51 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
Directory.SHORTCUT_SUPPORT_FULL, Directory.PHOTO_SUPPORT_FULL);
assertTrue(cursor.moveToPosition(3));
- assertDirectoryRow(cursor, "contactsTestPackage", "com.android.contacts", null, null,
+ assertDirectoryRow(cursor, getContext().getPackageName(),
+ "com.android.contacts", null, null,
null, -1 /* =any */, Directory.EXPORT_SUPPORT_NONE,
Directory.SHORTCUT_SUPPORT_FULL, Directory.PHOTO_SUPPORT_FULL);
assertTrue(cursor.moveToPosition(4));
- assertDirectoryRow(cursor, "contactsTestPackage", "com.android.contacts", null, null,
+ assertDirectoryRow(cursor, getContext().getPackageName(),
+ "com.android.contacts", null, null,
null, -1 /* =any */, Directory.EXPORT_SUPPORT_NONE,
Directory.SHORTCUT_SUPPORT_FULL, Directory.PHOTO_SUPPORT_FULL);
cursor.close();
}
+ public void testScanAllProviders_scanCondition() throws Exception {
+ testScanAllProviders();
+
+ // Nothing has changed, so no scanning.
+ assertEquals(0, mDirectoryManager.scanAllPackages(/* rescan=*/ false));
+
+ // rescan = true, so a full-scan should happen.
+ assertEquals(3, mDirectoryManager.scanAllPackages(/* rescan=*/ true));
+
+ // Change GAL packages, a scan should happen.
+ mPackageManager.setInstalledPackages(
+ Lists.newArrayList(
+ createProviderPackage("test.package2", "authority2"),
+ createPackage("test.packageX", "authorityX", false)));
+ assertEquals(1, mDirectoryManager.scanAllPackages(/* rescan=*/ false));
+
+ // Remove the non-GAL package, no scan should happen.
+ mPackageManager.setInstalledPackages(
+ Lists.newArrayList(
+ createProviderPackage("test.package2", "authority2")));
+ assertEquals(0, mDirectoryManager.scanAllPackages(/* rescan=*/ false));
+
+ // Remove GAL package 2 and add 1, a scan should happen.
+ mPackageManager.setInstalledPackages(
+ Lists.newArrayList(
+ createProviderPackage("test.package1", "authority1"),
+ createPackage("test.packageX", "authorityX", false)));
+ assertEquals(2, mDirectoryManager.scanAllPackages(/* rescan=*/ false));
+
+ }
+
public void testPackageInstalled() throws Exception {
mPackageManager.setInstalledPackages(
Lists.newArrayList(createProviderPackage("test.package1", "authority1"),
@@ -222,7 +264,7 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
Directory.PHOTO_SUPPORT_FULL);
- mDirectoryManager.scanAllPackages();
+ mDirectoryManager.scanAllPackages(/* rescan=*/ false);
// At this point the manager has discovered a single directory (plus two
// standard ones).
@@ -283,7 +325,7 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY, Directory.SHORTCUT_SUPPORT_FULL,
Directory.PHOTO_SUPPORT_FULL);
- mDirectoryManager.scanAllPackages();
+ mDirectoryManager.scanAllPackages(/* rescan=*/ false);
// At this point the manager has discovered two custom directories.
Cursor cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
@@ -329,7 +371,7 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY, Directory.SHORTCUT_SUPPORT_FULL,
Directory.PHOTO_SUPPORT_FULL);
- mDirectoryManager.scanAllPackages();
+ mDirectoryManager.scanAllPackages(/* rescan=*/ false);
// At this point the manager has discovered two custom directories.
Cursor cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
@@ -387,7 +429,7 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY, Directory.SHORTCUT_SUPPORT_FULL,
Directory.PHOTO_SUPPORT_FULL);
- mDirectoryManager.scanAllPackages();
+ mDirectoryManager.scanAllPackages(/* rescan=*/ false);
// At this point the manager has discovered two custom directories.
Cursor cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
@@ -451,7 +493,7 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
Directory.EXPORT_SUPPORT_ANY_ACCOUNT, Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY,
Directory.PHOTO_SUPPORT_FULL_SIZE_ONLY);
- mDirectoryManager.scanAllPackages();
+ mDirectoryManager.scanAllPackages(/* rescan=*/ false);
accounts = new Account[]{new Account("account-name1", "account-type1")};
mActor.setAccounts(accounts);
@@ -481,7 +523,7 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
Directory.PHOTO_SUPPORT_NONE);
- mDirectoryManager.scanAllPackages();
+ mDirectoryManager.scanAllPackages(/* rescan=*/ false);
// Pretend to replace the package with a different provider inside
MatrixCursor response2 = provider1.createResponseCursor();
@@ -515,7 +557,7 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
Directory.PHOTO_SUPPORT_NONE);
- mDirectoryManager.scanAllPackages();
+ mDirectoryManager.scanAllPackages(/* rescan=*/ false);
Cursor cursor = mResolver.query(
Directory.CONTENT_URI, new String[] { Directory._ID }, null, null, null);
@@ -555,7 +597,7 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
Directory.PHOTO_SUPPORT_NONE);
- mDirectoryManager.scanAllPackages();
+ mDirectoryManager.scanAllPackages(/* rescan=*/ false);
Cursor cursor = mResolver.query(
Directory.CONTENT_URI, new String[] { Directory._ID }, null, null, null);
diff --git a/tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java b/tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java
index 8c2a754f..3d8b8eb3 100644
--- a/tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java
+++ b/tests/src/com/android/providers/contacts/ContactMetadataProviderTest.java
@@ -93,7 +93,6 @@ public class ContactMetadataProviderTest extends BaseContactsProvider2Test {
"' AND " + MetadataSync.DATA_SET + "='" + TEST_DATA_SET2 + "'";
private ContactMetadataProvider mContactMetadataProvider;
- private AccountWithDataSet mTestAccount;
private ContentValues defaultValues;
@Override
@@ -104,7 +103,7 @@ public class ContactMetadataProviderTest extends BaseContactsProvider2Test {
// Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
// are using different dbHelpers.
mContactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
- mActor.provider).getDatabaseHelper(getContext()));
+ mActor.provider).getDatabaseHelper());
setupData();
}
@@ -175,7 +174,7 @@ public class ContactMetadataProviderTest extends BaseContactsProvider2Test {
// Create a raw contact with backupId.
String backupId = "backupId10001";
long rawContactId = RawContactUtil.createRawContactWithAccountDataSet(
- mResolver, mTestAccount);
+ mResolver, TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1);
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
ContentValues values = new ContentValues();
values.put(RawContacts.BACKUP_ID, backupId);
@@ -521,10 +520,8 @@ public class ContactMetadataProviderTest extends BaseContactsProvider2Test {
}
private void setupData() {
- mTestAccount = new AccountWithDataSet(TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1,
- TEST_DATA_SET1);
long rawContactId1 = RawContactUtil.createRawContactWithAccountDataSet(
- mResolver, mTestAccount);
+ mResolver, TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1);
createAccount(TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1);
insertMetadata(getDefaultValues());
insertMetadataSyncState(TEST_ACCOUNT_NAME1, TEST_ACCOUNT_TYPE1, TEST_DATA_SET1,
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index e3561073..11a13c27 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -23,7 +23,6 @@ import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OnAccountsUpdateListener;
import android.accounts.OperationCanceledException;
-import android.app.admin.DevicePolicyManager;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -45,7 +44,6 @@ import android.location.CountryListener;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
-import android.os.IUserManager;
import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
@@ -60,10 +58,8 @@ import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
import android.test.IsolatedContext;
-import android.test.RenamingDelegatingContext;
import android.test.mock.MockContentResolver;
import android.test.mock.MockContext;
-import android.util.Log;
import com.android.providers.contacts.util.ContactsPermissions;
import com.android.providers.contacts.util.MockSharedPreferences;
@@ -103,6 +99,8 @@ public class ContactsActor {
private Set<String> mGrantedPermissions = Sets.newHashSet();
private final Set<Uri> mGrantedUriPermissions = Sets.newHashSet();
+ private List<ContentProvider> mAllProviders = new ArrayList<>();
+
private CountryDetector mMockCountryDetector = new CountryDetector(null){
@Override
public Country detectCountry() {
@@ -360,6 +358,11 @@ public class ContactsActor {
public void sendBroadcast(Intent intent, String receiverPermission) {
// Ignore.
}
+
+ @Override
+ public Context getApplicationContext() {
+ return this;
+ }
};
mMockAccountManager = new MockAccountManager(mProviderContext);
@@ -378,7 +381,11 @@ public class ContactsActor {
public <T extends ContentProvider> T addProvider(Class<T> providerClass,
String authority, Context providerContext) throws Exception {
- T provider = providerClass.newInstance();
+ return addProvider(providerClass.newInstance(), authority, providerContext);
+ }
+
+ public <T extends ContentProvider> T addProvider(T provider,
+ String authority, Context providerContext) throws Exception {
ProviderInfo info = new ProviderInfo();
// Here, authority can have "user-id@". We want to use it for addProvider, but provider
@@ -392,6 +399,7 @@ public class ContactsActor {
resolver.addProvider(a, provider);
resolver.addProvider("0@" + a, provider);
}
+ mAllProviders.add(provider);
return provider;
}
@@ -450,7 +458,7 @@ public class ContactsActor {
mGrantedPermissions = grantedPermissions;
mGrantedUriPermissions = grantedUriPermissions;
- mPackageManager = new ContactsMockPackageManager();
+ mPackageManager = new ContactsMockPackageManager(overallContext);
mPackageManager.addPackage(1000, PACKAGE_GREY);
mPackageManager.addPackage(2000, PACKAGE_RED);
mPackageManager.addPackage(3000, PACKAGE_GREEN);
@@ -734,4 +742,10 @@ public class ContactsActor {
static final int COL_CONTACTS_ID = 0;
}
+
+ public void shutdown() {
+ for (ContentProvider provider : mAllProviders) {
+ provider.shutdown();
+ }
+ }
}
diff --git a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
index ff6a7a2c..8cf9ea42 100644
--- a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
+++ b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
@@ -17,14 +17,21 @@
package com.android.providers.contacts;
import android.content.ContentValues;
+import android.database.ContentObserver;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
import android.provider.ContactsContract;
+import android.provider.ContactsContract.ProviderStatus;
import android.provider.ContactsContract.RawContacts;
import android.test.MoreAsserts;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+import com.android.providers.contacts.ContactsDatabaseHelper.LowRes;
import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
@@ -32,16 +39,21 @@ import com.google.android.collect.Sets;
import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
@SmallTest
public class ContactsDatabaseHelperTest extends BaseContactsProvider2Test {
+ private static final String TAG = "ContactsDHT";
+
private ContactsDatabaseHelper mDbHelper;
private SQLiteDatabase mDb;
@Override
protected void setUp() throws Exception {
super.setUp();
- mDbHelper = getContactsProvider().getDatabaseHelper(getContext());
+ mDbHelper = getContactsProvider().getDatabaseHelper();
mDb = mDbHelper.getWritableDatabase();
}
@@ -156,13 +168,9 @@ public class ContactsDatabaseHelperTest extends BaseContactsProvider2Test {
}
/**
- * Test for {@link ContactsDatabaseHelper#getPackageId(String)} and
- * {@link ContactsDatabaseHelper#getMimeTypeId(String)}.
- *
- * We test them at the same time here, to make sure they're not mixing up the caches.
+ * Test for {@link ContactsDatabaseHelper#getPackageId(String)}
*/
- public void testGetPackageId_getMimeTypeId() {
-
+ public void testGetPackageId() {
// Test for getPackageId.
final long packageId1 = mDbHelper.getPackageId("value1");
final long packageId2 = mDbHelper.getPackageId("value2");
@@ -173,44 +181,51 @@ public class ContactsDatabaseHelperTest extends BaseContactsProvider2Test {
set.add(packageId1);
set.add(packageId2);
set.add(packageId3);
-
assertEquals(3, set.size());
+ // Make sure that repeated calls return the same value
+ assertEquals(packageId1, mDbHelper.getPackageId("value1"));
+ }
+
+ /**
+ * Test for {@link ContactsDatabaseHelper#getMimeTypeId(String)}
+ */
+ public void testGetMimeTypeId() {
// Test for getMimeTypeId.
final long mimetypeId1 = mDbHelper.getMimeTypeId("value1");
final long mimetypeId2 = mDbHelper.getMimeTypeId("value2");
final long mimetypeId3 = mDbHelper.getMimeTypeId("value3");
// Make sure they're all different.
+ final HashSet<Long> set = new HashSet<>();
set.clear();
set.add(mimetypeId1);
set.add(mimetypeId2);
set.add(mimetypeId3);
-
assertEquals(3, set.size());
- // Call with the same values and make sure they return the cached value.
- final long packageId1b = mDbHelper.getPackageId("value1");
- final long mimetypeId1b = mDbHelper.getMimeTypeId("value1");
-
- assertEquals(packageId1, packageId1b);
- assertEquals(mimetypeId1, mimetypeId1b);
-
- // Make sure the caches are also updated.
- assertEquals(packageId2, (long) mDbHelper.mPackageCache.get("value2"));
- assertEquals(mimetypeId2, (long) mDbHelper.mMimetypeCache.get("value2"));
-
- // Clear the cache, but they should still return the values, selecting from the database.
- mDbHelper.mPackageCache.clear();
- mDbHelper.mMimetypeCache.clear();
- assertEquals(packageId1, mDbHelper.getPackageId("value1"));
+ // Make sure repeated calls return the same value
assertEquals(mimetypeId1, mDbHelper.getMimeTypeId("value1"));
+ }
- // Empty the table
- mDb.execSQL("DELETE FROM " + Tables.MIMETYPES);
+ /**
+ * Test for cache {@link ContactsDatabaseHelper#mCommonMimeTypeIdsCache} which stores ids for
+ * common mime types for faster access.
+ */
+ public void testGetCommonMimeTypeIds() {
+ // getMimeTypeId should return the same value as the value stored in the cache
+ for (String commonMimeType : ContactsDatabaseHelper.COMMON_MIME_TYPES) {
+ assertEquals(mDbHelper.mCommonMimeTypeIdsCache.get(commonMimeType).longValue(),
+ mDbHelper.getMimeTypeId(commonMimeType));
+ }
- // We should still have the cached value.
- assertEquals(mimetypeId1, mDbHelper.getMimeTypeId("value1"));
+ // The ids should be available even after deleting them from the table
+ mDb.execSQL("DELETE FROM " + Tables.MIMETYPES + ";");
+
+ for (String commonMimeType : ContactsDatabaseHelper.COMMON_MIME_TYPES) {
+ assertEquals(mDbHelper.mCommonMimeTypeIdsCache.get(commonMimeType).longValue(),
+ mDbHelper.getMimeTypeId(commonMimeType));
+ }
}
/**
@@ -429,4 +444,111 @@ public class ContactsDatabaseHelperTest extends BaseContactsProvider2Test {
cursor.close();
}
}
+
+ private Integer getIntegerFromExpression(String expression) {
+ try (Cursor c = mDb.rawQuery("SELECT " + expression, null)) {
+ assertTrue(c.moveToPosition(0));
+ if (c.isNull(0)) {
+ return null;
+ }
+ return c.getInt(0);
+ }
+ }
+
+ private Integer checkGetTimesUsedExpression(Integer value) {
+ return getIntegerFromExpression(LowRes.getTimesUsedExpression(
+ value == null ? "NULL" : String.valueOf(value)));
+ }
+
+ public void testGetTimesUsedExpression() {
+ assertEquals((Object) 0, checkGetTimesUsedExpression(null));
+ assertEquals((Object) 0, checkGetTimesUsedExpression(-1));
+ assertEquals((Object) 0, checkGetTimesUsedExpression(-10));
+ assertEquals((Object) 0, checkGetTimesUsedExpression(0));
+ for (int i = 1; i < 10; i++) {
+ assertEquals("value=" + i, (Object) i, checkGetTimesUsedExpression(i));
+ }
+ for (int i = 10; i < 20; i++) {
+ assertEquals("value=" + i, (Object) 10, checkGetTimesUsedExpression(i));
+ }
+ for (int i = 20; i < 30; i++) {
+ assertEquals("value=" + i, (Object) 20, checkGetTimesUsedExpression(i));
+ }
+
+ assertEquals((Object) 123450, checkGetTimesUsedExpression(123456));
+ }
+
+ private Integer checkGetLastTimeUsedExpression(Integer value) {
+ return getIntegerFromExpression(LowRes.getLastTimeUsedExpression(
+ value == null ? "NULL" : String.valueOf(value)));
+ }
+
+ public void testGetLastTimeUsedExpression() {
+ assertEquals((Object) null, checkGetLastTimeUsedExpression(null));
+ assertEquals((Object) 0, checkGetLastTimeUsedExpression(0));
+ assertEquals((Object) 0, checkGetLastTimeUsedExpression(1));
+ assertEquals((Object) 0, checkGetLastTimeUsedExpression(86399));
+ assertEquals((Object) 86400, checkGetLastTimeUsedExpression(86400));
+
+ for (int i = 1; i < 3; i++) {
+ assertEquals((Object) (86400 * i), checkGetLastTimeUsedExpression(86400 * i));
+ assertEquals((Object) (86400 * i), checkGetLastTimeUsedExpression(86400 * i + 1));
+ assertEquals((Object) (86400 * i), checkGetLastTimeUsedExpression(86400 * i + 86399));
+ }
+ }
+
+ public void testNotifyProviderStatusChange() throws Exception {
+ final AtomicReference<Uri> calledUri = new AtomicReference<>();
+
+ final Handler h = new Handler(Looper.getMainLooper());
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ final ContentObserver observer = new ContentObserver(h) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ calledUri.set(uri);
+ latch.countDown();
+ }
+ };
+
+ // Notify on ProviderStatus.CONTENT_URI.
+ getContext().getContentResolver().registerContentObserver(
+ ProviderStatus.CONTENT_URI,
+ /* notifyForDescendants= */ false, observer);
+
+ // This should trigger it.
+ calledUri.set(null);
+ ContactsDatabaseHelper.notifyProviderStatusChange(getContext());
+
+ assertTrue(latch.await(30, TimeUnit.SECONDS));
+ assertEquals(ProviderStatus.CONTENT_URI, calledUri.get());
+ }
+
+ public void testOpenTimestamp() {
+ final long startTime = System.currentTimeMillis();
+
+ final String dbFilename = "testOpenTimestamp.db";
+
+ getContext().deleteDatabase(dbFilename);
+
+ final ContactsDatabaseHelper dbHelper = ContactsDatabaseHelper.getNewInstanceForTest(
+ mContext, dbFilename);
+
+ dbHelper.getReadableDatabase(); // Open the DB.
+
+ final long creationTime = dbHelper.getDatabaseCreationTime();
+
+ assertTrue("Expected " + creationTime + " >= " + startTime, creationTime >= startTime);
+
+ dbHelper.close();
+
+ // Open again.
+ final ContactsDatabaseHelper dbHelper2 = ContactsDatabaseHelper.getNewInstanceForTest(
+ mContext, dbFilename);
+
+ dbHelper2.getReadableDatabase(); // Open the DB.
+
+ assertEquals(creationTime, dbHelper2.getDatabaseCreationTime());
+ }
}
diff --git a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java
index a6077119..185fa031 100644
--- a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java
+++ b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperUpgradeTest.java
@@ -16,6 +16,11 @@
package com.android.providers.contacts;
+import static com.android.providers.contacts.TestUtils.createDatabaseSnapshot;
+import static com.android.providers.contacts.TestUtils.cv;
+import static com.android.providers.contacts.TestUtils.executeSqlFromAssetFile;
+
+import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.provider.BaseColumns;
import android.provider.CallLog.Calls;
@@ -40,6 +45,7 @@ import android.provider.VoicemailContract.Voicemails;
import android.test.suitebuilder.annotation.LargeTest;
import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
@@ -54,9 +60,11 @@ import com.android.providers.contacts.ContactsDatabaseHelper.NicknameLookupColum
import com.android.providers.contacts.ContactsDatabaseHelper.PackagesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PreAuthorizedUris;
+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.testutil.TestUtil;
import com.android.providers.contacts.util.PropertyUtils;
import junit.framework.AssertionFailedError;
@@ -81,13 +89,17 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade
private static final String CONTACTS2_DB_1108_ASSET_NAME = "upgradeTest/contacts2_1108.sql";
+ /**
+ * The helper instance. Note we just use it to call the upgrade method. The database
+ * hold by this instance is not used in this test.
+ */
private ContactsDatabaseHelper mHelper;
-
@Override
protected void setUp() throws Exception {
super.setUp();
- mHelper = ContactsDatabaseHelper.getNewInstanceForTest(getContext());
+ mHelper = ContactsDatabaseHelper.getNewInstanceForTest(getContext(),
+ TestUtils.getContactsDatabaseFilename(getContext()));
mHelper.onConfigure(mDb);
}
@@ -97,6 +109,11 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade
super.tearDown();
}
+ @Override
+ protected String getDatabaseFilename() {
+ return TestUtils.getContactsDatabaseFilename(getContext(), "-upgrade-test");
+ }
+
public void testDatabaseCreate() {
mHelper.onCreate(mDb);
assertDatabaseStructureSameAsList(TABLE_LIST, /* isNewDatabase =*/ true);
@@ -104,7 +121,10 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade
public void testDatabaseUpgrade_UpgradeToCurrent() {
create1108(mDb);
- mHelper.onUpgrade(mDb, 1108, mHelper.DATABASE_VERSION);
+ int oldVersion = upgrade(1108, 1200);
+ oldVersion = upgradeTo1201(oldVersion);
+ oldVersion = upgrade(oldVersion, ContactsDatabaseHelper.DATABASE_VERSION);
+
assertDatabaseStructureSameAsList(TABLE_LIST, /* isNewDatabase =*/ false);
}
@@ -113,24 +133,84 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade
*/
public void testDatabaseUpgrade_Incremental() {
create1108(mDb);
- upgradeTo1109();
- upgradeTo1110();
+
+ int oldVersion = 1108;
+ oldVersion = upgradeTo1109(oldVersion);
+ oldVersion = upgrade(oldVersion, ContactsDatabaseHelper.DATABASE_VERSION);
+ assertEquals(ContactsDatabaseHelper.DATABASE_VERSION, oldVersion);
assertDatabaseStructureSameAsList(TABLE_LIST, /* isNewDatabase =*/ false);
}
- private void upgradeTo1109() {
- mHelper.onUpgrade(mDb, 1108, 1109);
+ private int upgradeTo1109(int upgradeFrom) {
+ final int MY_VERSION = 1109;
+ mHelper.onUpgrade(mDb, upgradeFrom, MY_VERSION);
TableStructure calls = new TableStructure(mDb, "calls");
calls.assertHasColumn(Calls.LAST_MODIFIED, INTEGER, false, "0");
TableStructure voicemailStatus = new TableStructure(mDb, "voicemail_status");
voicemailStatus.assertHasColumn(Status.QUOTA_OCCUPIED, INTEGER, false, "-1");
voicemailStatus.assertHasColumn(Status.QUOTA_TOTAL, INTEGER, false, "-1");
+
+ return MY_VERSION;
+ }
+
+ private int upgradeTo1201(int upgradeFrom) {
+ final int MY_VERSION = 1201;
+
+ executeSqlFromAssetFile(getTestContext(), mDb, "upgradeTest/pre_upgrade1201.sql");
+
+ mHelper.onUpgrade(mDb, upgradeFrom, MY_VERSION);
+
+ try (Cursor c = mDb.rawQuery("select * from contacts order by _id", null)) {
+ BaseContactsProvider2Test.assertCursorValuesOrderly(c,
+ cv(Contacts._ID, 1,
+ "last_time_contacted", 0,
+ "x_last_time_contacted", 9940760264L,
+ "times_contacted", 0,
+ "x_times_contacted", 4
+ ),
+ cv(
+ "last_time_contacted", 0,
+ "x_last_time_contacted", 0,
+ "times_contacted", 0,
+ "x_times_contacted", 0
+ ));
+ }
+
+ try (Cursor c = mDb.rawQuery("select * from raw_contacts order by _id", null)) {
+ BaseContactsProvider2Test.assertCursorValuesOrderly(c,
+ cv("_id", 1,
+ "last_time_contacted", 0,
+ "x_last_time_contacted", 9940760264L,
+ "times_contacted", 0,
+ "x_times_contacted", 4
+ ),
+ cv(
+ "last_time_contacted", 0,
+ "x_last_time_contacted", 0,
+ "times_contacted", 0,
+ "x_times_contacted", 0
+ ));
+ }
+
+ try (Cursor c = mDb.rawQuery("select * from data_usage_stat", null)) {
+ BaseContactsProvider2Test.assertCursorValuesOrderly(c,
+ cv(
+ "last_time_used", 0,
+ "x_last_time_used", 9940760264L,
+ "times_used", 0,
+ "x_times_used", 4
+ ));
+ }
+
+ return MY_VERSION;
}
- private void upgradeTo1110() {
- mHelper.onUpgrade(mDb, 1109, 1110);
- // TODO: Test this upgrade.
+ private int upgrade(int upgradeFrom, int upgradeTo) {
+ if (upgradeFrom < upgradeTo) {
+ mHelper.onUpgrade(mDb, upgradeFrom, upgradeTo);
+ }
+ return upgradeTo;
}
/**
@@ -138,15 +218,7 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade
* incrementally from this version.
*/
private void create1108(SQLiteDatabase db) {
- try (InputStream input = getTestContext().getAssets().open(CONTACTS2_DB_1108_ASSET_NAME);) {
- BufferedReader r = new BufferedReader(new InputStreamReader(input));
- String query;
- while ((query = r.readLine()) != null) {
- db.execSQL(query);
- }
- } catch (IOException e) {
- throw new RuntimeException(e.toString());
- }
+ executeSqlFromAssetFile(getTestContext(), db, CONTACTS2_DB_1108_ASSET_NAME);
}
/**
@@ -171,8 +243,10 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade
new TableColumn(Contacts.PHOTO_FILE_ID, INTEGER, false, null),
new TableColumn(Contacts.CUSTOM_RINGTONE, TEXT, false, null),
new TableColumn(Contacts.SEND_TO_VOICEMAIL, INTEGER, true, "0"),
- new TableColumn(Contacts.TIMES_CONTACTED, INTEGER, true, "0"),
- new TableColumn(Contacts.LAST_TIME_CONTACTED, INTEGER, false, null),
+ new TableColumn(Contacts.RAW_TIMES_CONTACTED, INTEGER, true, "0"),
+ new TableColumn(Contacts.RAW_LAST_TIME_CONTACTED, INTEGER, false, null),
+ new TableColumn(Contacts.LR_TIMES_CONTACTED, INTEGER, true, "0"),
+ new TableColumn(Contacts.LR_LAST_TIME_CONTACTED, INTEGER, false, null),
new TableColumn(Contacts.STARRED, INTEGER, true, "0"),
new TableColumn(Contacts.PINNED, INTEGER, true,
String.valueOf(PinnedPositions.UNPINNED)),
@@ -203,8 +277,10 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade
new TableColumn(RawContactsColumns.AGGREGATION_NEEDED, INTEGER, true, "1"),
new TableColumn(RawContacts.CUSTOM_RINGTONE, TEXT, false, null),
new TableColumn(RawContacts.SEND_TO_VOICEMAIL, INTEGER, true, "0"),
- new TableColumn(RawContacts.TIMES_CONTACTED, INTEGER, true, "0"),
- new TableColumn(RawContacts.LAST_TIME_CONTACTED, INTEGER, false, null),
+ new TableColumn(RawContacts.RAW_TIMES_CONTACTED, INTEGER, true, "0"),
+ new TableColumn(RawContacts.RAW_LAST_TIME_CONTACTED, INTEGER, false, null),
+ new TableColumn(RawContacts.LR_TIMES_CONTACTED, INTEGER, true, "0"),
+ new TableColumn(RawContacts.LR_LAST_TIME_CONTACTED, INTEGER, false, null),
new TableColumn(RawContacts.STARRED, INTEGER, true, "0"),
new TableColumn(RawContacts.PINNED, INTEGER, true,
String.valueOf(PinnedPositions.UNPINNED)),
@@ -454,8 +530,10 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade
new TableColumn(DataUsageStatColumns._ID, INTEGER, false, null),
new TableColumn(DataUsageStatColumns.DATA_ID, INTEGER, true, null),
new TableColumn(DataUsageStatColumns.USAGE_TYPE_INT, INTEGER, true, "0"),
- new TableColumn(DataUsageStatColumns.TIMES_USED, INTEGER, true, "0"),
- new TableColumn(DataUsageStatColumns.LAST_TIME_USED, INTEGER, true, "0"),
+ new TableColumn(DataUsageStatColumns.RAW_TIMES_USED, INTEGER, true, "0"),
+ new TableColumn(DataUsageStatColumns.RAW_LAST_TIME_USED, INTEGER, true, "0"),
+ new TableColumn(DataUsageStatColumns.LR_TIMES_USED, INTEGER, true, "0"),
+ new TableColumn(DataUsageStatColumns.LR_LAST_TIME_USED, INTEGER, true, "0"),
};
private static final TableColumn[] METADATA_SYNC_COLUMNS = new TableColumn[] {
@@ -478,6 +556,24 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade
new TableColumn(MetadataSyncState.STATE, BLOB, false, null),
};
+ private static final TableColumn[] PRESENCE_COLUMNS = new TableColumn[] {
+ new TableColumn(StatusUpdates.DATA_ID, INTEGER, false, null),
+ new TableColumn(StatusUpdates.PROTOCOL, INTEGER, true, null),
+ new TableColumn(StatusUpdates.CUSTOM_PROTOCOL, TEXT, false, null),
+ new TableColumn(StatusUpdates.IM_HANDLE, TEXT, false, null),
+ new TableColumn(StatusUpdates.IM_ACCOUNT, TEXT, false, null),
+ new TableColumn(PresenceColumns.CONTACT_ID, INTEGER, false, null),
+ new TableColumn(PresenceColumns.RAW_CONTACT_ID, INTEGER, false, null),
+ new TableColumn(StatusUpdates.PRESENCE, INTEGER, false, null),
+ new TableColumn(StatusUpdates.CHAT_CAPABILITY, INTEGER, true, "0")
+ };
+
+ private static final TableColumn[] AGGREGATED_PRESENCE_COLUMNS = new TableColumn[] {
+ new TableColumn(AggregatedPresenceColumns.CONTACT_ID, INTEGER, false, null),
+ new TableColumn(StatusUpdates.PRESENCE, INTEGER, false, null),
+ new TableColumn(StatusUpdates.CHAT_CAPABILITY, INTEGER, true, "0")
+ };
+
private static final TableListEntry[] TABLE_LIST = {
new TableListEntry(PropertyUtils.Tables.PROPERTIES, PROPERTIES_COLUMNS),
new TableListEntry(Tables.ACCOUNTS, ACCOUNTS_COLUMNS),
@@ -506,6 +602,8 @@ public class ContactsDatabaseHelperUpgradeTest extends BaseDatabaseHelperUpgrade
new TableListEntry(Tables.METADATA_SYNC, METADATA_SYNC_COLUMNS),
new TableListEntry(Tables.PRE_AUTHORIZED_URIS, PRE_AUTHORIZED_URIS_COLUMNS),
new TableListEntry(Tables.METADATA_SYNC_STATE, METADATA_SYNC_STATE_COLUMNS),
+ new TableListEntry(Tables.PRESENCE, PRESENCE_COLUMNS),
+ new TableListEntry(Tables.AGGREGATED_PRESENCE, AGGREGATED_PRESENCE_COLUMNS)
};
}
diff --git a/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java b/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
index 1f3f52e3..a9420dda 100644
--- a/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
+++ b/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
@@ -20,6 +20,7 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.os.Binder;
@@ -36,11 +37,15 @@ import java.util.List;
* {@link Context#getPackageName()}.
*/
public class ContactsMockPackageManager extends MockPackageManager {
+
+ private final Context mRealContext;
+
private final HashMap<Integer, String> mForward = new HashMap<Integer, String>();
private final HashMap<String, Integer> mReverse = new HashMap<String, Integer>();
private List<PackageInfo> mPackages;
- public ContactsMockPackageManager() {
+ public ContactsMockPackageManager(Context realContext) {
+ mRealContext = realContext;
}
/**
@@ -76,10 +81,15 @@ public class ContactsMockPackageManager extends MockPackageManager {
}
@Override
- public ApplicationInfo getApplicationInfo(String packageName, int flags) {
+ public ApplicationInfo getApplicationInfo(String packageName, int flags)
+ throws NameNotFoundException {
ApplicationInfo info = new ApplicationInfo();
Integer uid = mReverse.get(packageName);
+ if (uid == null) {
+ throw new NameNotFoundException();
+ }
info.uid = (uid != null) ? uid : -1;
+ info.flags = ApplicationInfo.FLAG_INSTALLED;
return info;
}
@@ -109,6 +119,34 @@ public class ContactsMockPackageManager extends MockPackageManager {
@Override
public Resources getResourcesForApplication(String appPackageName) {
+ if (mRealContext.getPackageName().equals(appPackageName)) {
+ return mRealContext.getResources();
+ }
return new ContactsMockResources();
}
+
+ @Override
+ public List<ProviderInfo> queryContentProviders(String processName, int uid, int flags,
+ String metaDataKey) {
+ final List<ProviderInfo> ret = new ArrayList<>();
+ final List<PackageInfo> packages = getInstalledPackages(flags);
+ if (packages == null) {
+ return ret;
+ }
+ for (PackageInfo pkg : packages) {
+ if (pkg.providers == null) {
+ continue;
+ }
+ for (ProviderInfo proi : pkg.providers) {
+ if (metaDataKey == null) {
+ ret.add(proi);
+ } else {
+ if (proi.metaData != null && proi.metaData.containsKey(metaDataKey)) {
+ ret.add(proi);
+ }
+ }
+ }
+ }
+ return ret;
+ }
}
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index f432b0bb..8930338e 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -16,7 +16,9 @@
package com.android.providers.contacts;
+import static com.android.providers.contacts.TestUtils.createDatabaseSnapshot;
import static com.android.providers.contacts.TestUtils.cv;
+import static com.android.providers.contacts.TestUtils.dumpCursor;
import android.accounts.Account;
import android.content.ContentProviderOperation;
@@ -254,8 +256,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
Contacts.CONTACT_STATUS_LABEL,
Contacts.CONTACT_STATUS_ICON,
Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
- DataUsageStatColumns.TIMES_USED,
- DataUsageStatColumns.LAST_TIME_USED,
+ DataUsageStatColumns.LR_TIMES_USED,
+ DataUsageStatColumns.LR_LAST_TIME_USED,
});
}
@@ -299,8 +301,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
Contacts.CONTACT_STATUS_LABEL,
Contacts.CONTACT_STATUS_ICON,
Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
- DataUsageStatColumns.TIMES_USED,
- DataUsageStatColumns.LAST_TIME_USED,
+ DataUsageStatColumns.LR_TIMES_USED,
+ DataUsageStatColumns.LR_LAST_TIME_USED,
Phone.NUMBER,
Phone.TYPE,
Phone.LABEL,
@@ -650,8 +652,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
Contacts.CONTACT_STATUS_ICON,
Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
GroupMembership.GROUP_SOURCE_ID,
- DataUsageStatColumns.TIMES_USED,
- DataUsageStatColumns.LAST_TIME_USED,
+ Contacts.Entity.TIMES_USED,
+ Contacts.Entity.LAST_TIME_USED,
});
}
@@ -711,7 +713,13 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
PhoneLookup.CONTACT_ID,
PhoneLookup.DATA_ID,
PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.DISPLAY_NAME_SOURCE,
PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
+ PhoneLookup.PHONETIC_NAME,
+ PhoneLookup.PHONETIC_NAME_STYLE,
+ PhoneLookup.SORT_KEY_PRIMARY,
+ PhoneLookup.SORT_KEY_ALTERNATIVE,
PhoneLookup.LAST_TIME_CONTACTED,
PhoneLookup.TIMES_CONTACTED,
PhoneLookup.STARRED,
@@ -739,7 +747,13 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
PhoneLookup.CONTACT_ID,
PhoneLookup.DATA_ID,
PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.DISPLAY_NAME_SOURCE,
PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
+ PhoneLookup.PHONETIC_NAME,
+ PhoneLookup.PHONETIC_NAME_STYLE,
+ PhoneLookup.SORT_KEY_PRIMARY,
+ PhoneLookup.SORT_KEY_ALTERNATIVE,
PhoneLookup.LAST_TIME_CONTACTED,
PhoneLookup.TIMES_CONTACTED,
PhoneLookup.STARRED,
@@ -933,6 +947,13 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
});
}
+ public void testProviderStatusProjection() {
+ assertProjection(ProviderStatus.CONTENT_URI, new String[]{
+ ProviderStatus.STATUS,
+ ProviderStatus.DATABASE_CREATION_TIMESTAMP,
+ });
+ }
+
public void testRawContactsInsert() {
ContentValues values = new ContentValues();
@@ -946,7 +967,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
values.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
values.put(RawContacts.CUSTOM_RINGTONE, "d");
values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
- values.put(RawContacts.LAST_TIME_CONTACTED, 12345);
+ values.put(RawContacts.LAST_TIME_CONTACTED, 86400 + 123);
+ values.put(RawContacts.TIMES_CONTACTED, 12);
values.put(RawContacts.STARRED, 1);
values.put(RawContacts.SYNC1, "e");
values.put(RawContacts.SYNC2, "f");
@@ -956,6 +978,9 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
Uri rowUri = mResolver.insert(RawContacts.CONTENT_URI, values);
long rawContactId = ContentUris.parseId(rowUri);
+ values.put(RawContacts.LAST_TIME_CONTACTED, 86400);
+ values.put(RawContacts.TIMES_CONTACTED, 10);
+
assertStoredValues(rowUri, values);
assertSelection(RawContacts.CONTENT_URI, values, RawContacts._ID, rawContactId);
assertNetworkNotified(true);
@@ -1161,7 +1186,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
Uri dataUri = mResolver.insert(Data.CONTENT_URI, values);
final ContactsProvider2 cp = (ContactsProvider2) getProvider();
- final ContactsDatabaseHelper helper = cp.getDatabaseHelper(mContext);
+ final ContactsDatabaseHelper helper = cp.getDatabaseHelper();
String data1 = values.getAsString(Data.DATA1);
String data2 = values.getAsString(Data.DATA2);
String combineString = data1+data2;
@@ -1222,7 +1247,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Check for photo data's hashId is correct or not.
final ContactsProvider2 cp = (ContactsProvider2) getProvider();
- final ContactsDatabaseHelper helper = cp.getDatabaseHelper(mContext);
+ final ContactsDatabaseHelper helper = cp.getDatabaseHelper();
String hashId = helper.getPhotoHashId();
assertStoredValue(dataUri, Data.HASH_ID, hashId);
@@ -1278,11 +1303,11 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
ContentValues values = new ContentValues();
values.put(RawContacts.CUSTOM_RINGTONE, "d");
values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
- values.put(RawContacts.LAST_TIME_CONTACTED, 12345);
+ values.put(RawContacts.LAST_TIME_CONTACTED, 86400 + 5);
values.put(RawContacts.TIMES_CONTACTED, 54321);
values.put(RawContacts.STARRED, 1);
- Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
+ Uri rawContactUri = insertRawContact(values);
long rawContactId = ContentUris.parseId(rawContactUri);
DataUtil.insertStructuredName(mResolver, rawContactId, "Meghan", "Knox");
@@ -1302,8 +1327,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
values.put(Contacts.DISPLAY_NAME, "Meghan Knox");
values.put(Contacts.CUSTOM_RINGTONE, "d");
values.put(Contacts.SEND_TO_VOICEMAIL, 1);
- values.put(Contacts.LAST_TIME_CONTACTED, 12345);
- values.put(Contacts.TIMES_CONTACTED, 54321);
+ values.put(Contacts.LAST_TIME_CONTACTED, 86400);
+ values.put(Contacts.TIMES_CONTACTED, 54320);
values.put(Contacts.STARRED, 1);
assertStoredValues(ContentUris.withAppendedId(Phone.CONTENT_URI, phoneId), values);
@@ -2055,10 +2080,11 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Note here we use a standalone CP2 so it'll have its own db helper.
// Also use AlteringUserContext here to report the corp user id.
+ final int userId = MockUserManager.CORP_USER.id;
SynchronousContactsProvider2 provider = mActor.addProvider(
- StandaloneContactsProvider2.class,
- "" + MockUserManager.CORP_USER.id + "@com.android.contacts",
- new AlteringUserContext(mActor.getProviderContext(), MockUserManager.CORP_USER.id));
+ new SecondaryUserContactsProvider2(userId),
+ "" + userId + "@com.android.contacts",
+ new AlteringUserContext(mActor.getProviderContext(), userId));
provider.wipeData();
return provider;
}
@@ -2463,11 +2489,11 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
ContentValues values = new ContentValues();
values.put(RawContacts.CUSTOM_RINGTONE, "d");
values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
- values.put(RawContacts.LAST_TIME_CONTACTED, 12345);
+ values.put(RawContacts.LAST_TIME_CONTACTED, 86400 + 5);
values.put(RawContacts.TIMES_CONTACTED, 54321);
values.put(RawContacts.STARRED, 1);
- Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
+ Uri rawContactUri = insertRawContact(values);
final long rawContactId = ContentUris.parseId(rawContactUri);
DataUtil.insertStructuredName(mResolver, rawContactId, "Meghan", "Knox");
@@ -2486,8 +2512,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
values.put(Contacts.DISPLAY_NAME, "Meghan Knox");
values.put(Contacts.CUSTOM_RINGTONE, "d");
values.put(Contacts.SEND_TO_VOICEMAIL, 1);
- values.put(Contacts.LAST_TIME_CONTACTED, 12345);
- values.put(Contacts.TIMES_CONTACTED, 54321);
+ values.put(Contacts.LAST_TIME_CONTACTED, 86400);
+ values.put(Contacts.TIMES_CONTACTED, 54320);
values.put(Contacts.STARRED, 1);
assertStoredValues(Email.CONTENT_URI, values);
@@ -2842,7 +2868,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
/**
* Tests {@link DataUsageFeedback} correctly bucketize contacts using each
- * {@link DataUsageStatColumns#LAST_TIME_USED}
+ * {@link DataUsageStatColumns#RAW_LAST_TIME_USED}
*/
public void testEmailFilterSortOrderWithOldHistory() {
long rawContactId1 = RawContactUtil.createRawContact(mResolver);
@@ -2997,7 +3023,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
assertStoredValue(dataUri2, Data.IS_SUPER_PRIMARY, 0);
final Uri dataUriWithUsageType = Data.CONTENT_URI.buildUpon().appendQueryParameter(
DataUsageFeedback.USAGE_TYPE, usageTypeString).build();
- assertDataUsageCursorContains(dataUriWithUsageType, emailAddress, timesUsed, lastTimeUsed);
+ assertDataUsageCursorContains(dataUriWithUsageType, emailAddress, 5,
+ 1111111 / 86400 * 86400);
// Update AggregationException table.
RawContactInfo aggregationContact = new RawContactInfo(
@@ -3034,7 +3061,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
// are using different dbHelpers.
contactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
- mActor.provider).getDatabaseHelper(getContext()));
+ mActor.provider).getDatabaseHelper());
// Create an account first.
String backupId = "backupId001";
String accountType = "accountType";
@@ -3090,7 +3117,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
// are using different dbHelpers.
contactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
- mActor.provider).getDatabaseHelper(getContext()));
+ mActor.provider).getDatabaseHelper());
// Create an account first.
String backupId = "backupId001";
String accountType = "accountType";
@@ -3156,7 +3183,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
// are using different dbHelpers.
contactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
- mActor.provider).getDatabaseHelper(getContext()));
+ mActor.provider).getDatabaseHelper());
// Enable metadataSync flag.
final ContactsProvider2 cp = (ContactsProvider2) getProvider();
cp.setMetadataSyncForTest(true);
@@ -3462,6 +3489,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO);
Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ values.put(Contacts.TIMES_CONTACTED, 4);
assertStoredValues(contactUri, values);
assertSelection(Contacts.CONTENT_URI, values, Contacts._ID, contactId);
}
@@ -3474,6 +3502,9 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
values.put(Contacts.CONTACT_CHAT_CAPABILITY, StatusUpdates.CAPABILITY_HAS_CAMERA);
Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+
+ values.put(Contacts.TIMES_CONTACTED, 4);
+
assertStoredValuesWithProjection(contactUri, values);
assertSelectionWithProjection(Contacts.CONTENT_URI, values, Contacts._ID, contactId);
}
@@ -3496,6 +3527,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
Uri filterUri1 = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, "goulash");
+ values.put(Contacts.TIMES_CONTACTED, 4);
assertStoredValuesWithProjection(filterUri1, values);
assertContactFilter(contactId, "goolash");
@@ -3530,6 +3562,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
Uri filterUri1 = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, "goog411@acme.com");
+ values.put(Contacts.TIMES_CONTACTED, 4);
assertStoredValuesWithProjection(filterUri1, values);
assertContactFilter(contactId, "goog");
@@ -3556,6 +3589,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
Uri filterUri1 = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, "18004664411");
+ values.put(Contacts.TIMES_CONTACTED, 4);
assertStoredValuesWithProjection(filterUri1, values);
assertContactFilter(contactId, "18004664411");
@@ -3587,7 +3621,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
StatusUpdates.CAPABILITY_HAS_CAMERA);
ContentValues values3 = new ContentValues();
final String phoneNumber3 = "18004664413";
- final int timesContacted3 = 5;
+ final int timesContacted3 = 9;
createContact(values3, "Lotta", "Calling", phoneNumber3,
"c@acme.com", StatusUpdates.AWAY, timesContacted3, 0, 0,
StatusUpdates.CAPABILITY_HAS_VIDEO);
@@ -3603,6 +3637,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Send feedback for the 3rd phone number, pretending we called that person via phone.
sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3);
+ values3.put(Contacts.TIMES_CONTACTED, 10); // Low res.
+
// After the feedback, 3rd contact should be shown after starred one.
assertStoredValuesOrderly(Contacts.CONTENT_STREQUENT_URI,
new ContentValues[] { values4, values3 });
@@ -3612,6 +3648,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values1);
// After the feedback, 1st and 3rd contacts should be shown after starred one.
+ values1.put(Contacts.TIMES_CONTACTED, 2);
assertStoredValuesOrderly(Contacts.CONTENT_STREQUENT_URI,
new ContentValues[] { values4, values1, values3 });
@@ -3654,6 +3691,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Send feedback for the 2rd phone number, pretending we send the person a SMS message.
sendFeedback(phoneNumber2, DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, values1);
+ values1.put(Contacts.TIMES_CONTACTED, 1); // Low res.
+
// SMS feedback shouldn't affect phone-only results.
assertStoredValuesOrderly(phoneOnlyStrequentUri, new ContentValues[] {values5, values6,
values4, values3});
@@ -3736,6 +3775,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Contact cid1 again, but it's an email, not a phone call.
updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1e);
+ updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1e);
+ updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1e);
// Contacts in this bucket are considered more than 3 days old
sMockClock.setCurrentTimeMillis(fourDaysAgoInMillis);
@@ -3808,12 +3849,15 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
sendFeedback(email2, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values2);
sendFeedback(email2, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values2);
+ values1.put(Contacts.TIMES_CONTACTED, 1);
+ values2.put(Contacts.TIMES_CONTACTED, 2);
assertStoredValues(Contacts.CONTENT_FREQUENT_URI, new ContentValues[] {values2, values1});
- // Three times
- sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3);
- sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3);
- sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3);
+ for (int i = 0; i < 10; i++) {
+ sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3);
+ }
+
+ values3.put(Contacts.TIMES_CONTACTED, 10); // low res.
assertStoredValues(Contacts.CONTENT_FREQUENT_URI,
new ContentValues[] {values3, values2, values1});
@@ -3874,34 +3918,51 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values1);
- assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 1, 100);
+ assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 1, 0);
- sMockClock.setCurrentTimeMillis(111);
+ sMockClock.setCurrentTimeMillis(86400 + 123);
sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values1);
- assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 2, 111);
+ assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 2, 86400);
- sMockClock.setCurrentTimeMillis(123);
- sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, values1);
+ sMockClock.setCurrentTimeMillis(86400 * 3 + 123);
+ for (int i = 0; i < 11; i++) {
+ sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, values1);
+ }
- assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 3, 123);
+ // Note here, "a@acme.com" has two data stats rows, 2 and 11. What we get here's the sum
+ // of the lowres values, so # times will be 12, instead of 10 (which is the lowres of the
+ // sum).
+ assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 12, 86400 * 3);
final Uri dataUriWithUsageTypeLongText = Data.CONTENT_URI.buildUpon().appendQueryParameter(
DataUsageFeedback.USAGE_TYPE, DataUsageFeedback.USAGE_TYPE_LONG_TEXT).build();
- assertDataUsageCursorContains(dataUriWithUsageTypeLongText, "a@acme.com", 2, 111);
+ assertDataUsageCursorContains(dataUriWithUsageTypeLongText, "a@acme.com", 2, 86400 * 1);
- sMockClock.setCurrentTimeMillis(200);
+ sMockClock.setCurrentTimeMillis(86400 * 4 + 123);
sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
- assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 6, 200);
+ assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 15, 86400 * 4);
+
+ sMockClock.setCurrentTimeMillis(86400 * 5 + 123);
+ for (int i = 0; i < 10; i++) {
+ sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
+ }
+ assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 22, 86400 * 5);
+
+ sMockClock.setCurrentTimeMillis(86400 * 6 + 123);
+ for (int i = 0; i < 10; i++) {
+ sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
+ }
+ assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 32, 86400 * 6);
final Uri dataUriWithUsageTypeCall = Data.CONTENT_URI.buildUpon().appendQueryParameter(
DataUsageFeedback.USAGE_TYPE, DataUsageFeedback.USAGE_TYPE_CALL).build();
- assertDataUsageCursorContains(dataUriWithUsageTypeCall, "a@acme.com", 3, 200);
+ assertDataUsageCursorContains(dataUriWithUsageTypeCall, "a@acme.com", 20, 86400 * 6);
}
public void testQueryContactGroup() {
@@ -3921,6 +3982,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
Cursor c = mResolver.query(filterUri1, null, null, null, Contacts._ID);
assertEquals(1, c.getCount());
c.moveToFirst();
+ dumpCursor(c);
assertCursorValues(c, values1);
c.close();
@@ -4102,6 +4164,9 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
long nonProfileRawContactId = createBasicNonProfileContact(nonProfileValues);
long nonProfileContactId = queryContactId(nonProfileRawContactId);
+ nonProfileValues.put(Contacts.TIMES_CONTACTED, 4);
+ profileValues.put(Contacts.TIMES_CONTACTED, 4);
+
assertStoredValues(Contacts.CONTENT_URI, nonProfileValues);
assertSelection(Contacts.CONTENT_URI, nonProfileValues, Contacts._ID, nonProfileContactId);
@@ -4115,7 +4180,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Create a non-profile contact - this should be returned.
ContentValues nonProfileValues = new ContentValues();
createBasicNonProfileContact(nonProfileValues);
-
+ nonProfileValues.put(Contacts.TIMES_CONTACTED, 4);
assertStoredValues(Contacts.CONTENT_URI, new ContentValues[] {nonProfileValues});
}
@@ -4123,6 +4188,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
ContentValues profileValues = new ContentValues();
createBasicProfileContact(profileValues);
+ profileValues.put(Contacts.TIMES_CONTACTED, 4);
assertStoredValues(Profile.CONTENT_URI, profileValues);
}
@@ -4171,6 +4237,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// The raw contact view doesn't include the photo ID.
profileValues.remove(Contacts.PHOTO_ID);
+ profileValues.put(Contacts.TIMES_CONTACTED, 4);
assertStoredValues(Profile.CONTENT_RAW_CONTACTS_URI, profileValues);
}
@@ -4180,6 +4247,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// The raw contact view doesn't include the photo ID.
profileValues.remove(Contacts.PHOTO_ID);
+ profileValues.put(Contacts.TIMES_CONTACTED, 4);
assertStoredValues(ContentUris.withAppendedId(
Profile.CONTENT_RAW_CONTACTS_URI, profileRawContactId), profileValues);
}
@@ -5225,18 +5293,29 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
}
public void testSetSendToVoicemailAndRingtone() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ assertDirty(rawContactUri, true);
+ clearDirty(rawContactUri);
long contactId = queryContactId(rawContactId);
updateSendToVoicemailAndRingtone(contactId, true, "foo");
assertSendToVoicemailAndRingtone(contactId, true, "foo");
- assertNetworkNotified(true);
- assertDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), true);
+ assertNetworkNotified(false);
+ assertMetadataNetworkNotified(true);
+ assertDirty(rawContactUri, false);
+ assertMetadataDirty(rawContactUri, true);
updateSendToVoicemailAndRingtoneWithSelection(contactId, false, "bar");
assertSendToVoicemailAndRingtone(contactId, false, "bar");
- assertNetworkNotified(true);
- assertDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), true);
+ assertNetworkNotified(false);
+ assertMetadataNetworkNotified(true);
+ assertDirty(rawContactUri, false);
+ assertMetadataDirty(rawContactUri, true);
}
public void testSendToVoicemailAndRingtoneAfterAggregation() {
@@ -5294,17 +5373,30 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
assertSendToVoicemailAndRingtone(queryContactId(rawContactId2), false, "bar");
}
- public void testMarkDirtyAfterAggregation() {
+ public void testMarkMetadataDirtyAfterAggregation() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "i", "j");
long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "k", "l");
+ Uri rawContactUri1 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1);
+ Uri rawContactUri2 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId2);
+ assertDirty(rawContactUri1, true);
+ assertDirty(rawContactUri2, true);
+ clearDirty(rawContactUri1);
+ clearDirty(rawContactUri2);
// Aggregate them
setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
rawContactId1, rawContactId2);
- assertDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1), true);
- assertDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId2), true);
- assertNetworkNotified(true);
+ assertDirty(rawContactUri1, false);
+ assertDirty(rawContactUri2, false);
+ assertMetadataDirty(rawContactUri1, true);
+ assertMetadataDirty(rawContactUri2, true);
+ assertNetworkNotified(false);
+ assertMetadataNetworkNotified(true);
}
public void testStatusUpdateInsert() {
@@ -6780,7 +6872,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Reset the dbHelper to be the one ContactsProvider2 is using. Before this, two providers
// are using different dbHelpers.
contactMetadataProvider.setDatabaseHelper(((SynchronousContactsProvider2)
- mActor.provider).getDatabaseHelper(getContext()));
+ mActor.provider).getDatabaseHelper());
// Create a doomed metadata.
String backupId = "backupIdForDoomed";
@@ -6891,28 +6983,41 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
}
public void testDirtyWhenRawContactInsert() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
+ // When inserting a rawcontact without metadata.
long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
assertDirty(rawContactUri, false);
+ assertMetadataDirty(rawContactUri, false);
assertNetworkNotified(true);
+ assertMetadataNetworkNotified(true);
+ // When inserting a rawcontact with metadata.
ContentValues values = new ContentValues();
values.put(ContactsContract.RawContacts.STARRED, 1);
values.put(ContactsContract.RawContacts.ACCOUNT_NAME, mAccount.name);
values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, mAccount.type);
Uri rawContactId2Uri = mResolver.insert(RawContacts.CONTENT_URI, values);
- assertDirty(rawContactId2Uri, true);
+ assertDirty(rawContactId2Uri, false);
+ assertMetadataDirty(rawContactId2Uri, true);
assertNetworkNotified(true);
+ assertMetadataNetworkNotified(true);
}
public void testRawContactDirtyAndVersion() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
final long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId);
assertDirty(uri, false);
long version = getVersion(uri);
ContentValues values = new ContentValues();
- values.put(ContactsContract.RawContacts.DIRTY, 0);
values.put(ContactsContract.RawContacts.SEND_TO_VOICEMAIL, 1);
values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
RawContacts.AGGREGATION_MODE_IMMEDIATE);
@@ -6920,9 +7025,10 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
assertEquals(1, mResolver.update(uri, values, null, null));
assertEquals(version, getVersion(uri));
- // Mark dirty when send_to_voicemail/starred was set.
- assertDirty(uri, true);
- assertNetworkNotified(true);
+ assertDirty(uri, false);
+ assertNetworkNotified(false);
+ assertMetadataDirty(uri, true);
+ assertMetadataNetworkNotified(true);
Uri emailUri = insertEmail(rawContactId, "goo@woo.com");
assertDirty(uri, true);
@@ -8679,13 +8785,22 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
}
}
- public void testMarkDirtyWhenDataUsageUpdate() {
+ public void testMarkMetadataDirtyWhenDataUsageUpdate() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
final long rid1 = RawContactUtil.createRawContactWithName(mResolver, "contact", "a");
final long did1a = ContentUris.parseId(insertEmail(rid1, "email_1_a@email.com"));
+ final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rid1);
+ assertDirty(rawContactUri, true);
+ clearDirty(rawContactUri);
updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1a);
- assertDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rid1), true);
- assertNetworkNotified(true);
+ assertDirty(rawContactUri, false);
+ assertMetadataDirty(rawContactUri, true);
+ assertNetworkNotified(false);
+ assertMetadataNetworkNotified(true);
}
public void testDataUsageFeedbackAndDelete() {
@@ -8734,8 +8849,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Now, there's a single frequent. (contact 1)
assertRowCount(1, Contacts.CONTENT_STREQUENT_URI, null, null);
- // time = startTime + 1
- sMockClock.advance();
+ sMockClock.advanceDay();
// Test 2. touch data 1a, 2a and 3a
updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, did1a, did2a, did3a);
@@ -8743,8 +8857,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// Now, contact 1 and 3 are in frequent.
assertRowCount(2, Contacts.CONTENT_STREQUENT_URI, null, null);
- // time = startTime + 2
- sMockClock.advance();
+ sMockClock.advanceDay();
// Test 2. touch data 2p (call)
updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_CALL, did2p);
@@ -8752,55 +8865,54 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
// There're still two frequent.
assertRowCount(2, Contacts.CONTENT_STREQUENT_URI, null, null);
- // time = startTime + 3
- sMockClock.advance();
+ sMockClock.advanceDay();
// Test 3. touch data 2p and 3p (short text)
updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, did2p, did3p);
// Let's check the tables.
-
+// TODO more tests?
// Fist, check the data_usage_stat table, which has no public URI.
assertStoredValuesDb("SELECT " + DataUsageStatColumns.DATA_ID +
"," + DataUsageStatColumns.USAGE_TYPE_INT +
- "," + DataUsageStatColumns.TIMES_USED +
- "," + DataUsageStatColumns.LAST_TIME_USED +
+ "," + DataUsageStatColumns.RAW_TIMES_USED +
+ "," + DataUsageStatColumns.RAW_LAST_TIME_USED +
" FROM " + Tables.DATA_USAGE_STAT, null,
cv(DataUsageStatColumns.DATA_ID, did1a,
DataUsageStatColumns.USAGE_TYPE_INT,
DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT,
- DataUsageStatColumns.TIMES_USED, 2,
- DataUsageStatColumns.LAST_TIME_USED, startTime + 1
+ DataUsageStatColumns.RAW_TIMES_USED, 2,
+ DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400
),
cv(DataUsageStatColumns.DATA_ID, did2a,
DataUsageStatColumns.USAGE_TYPE_INT,
DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT,
- DataUsageStatColumns.TIMES_USED, 1,
- DataUsageStatColumns.LAST_TIME_USED, startTime + 1
+ DataUsageStatColumns.RAW_TIMES_USED, 1,
+ DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400
),
cv(DataUsageStatColumns.DATA_ID, did3a,
DataUsageStatColumns.USAGE_TYPE_INT,
DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT,
- DataUsageStatColumns.TIMES_USED, 1,
- DataUsageStatColumns.LAST_TIME_USED, startTime + 1
+ DataUsageStatColumns.RAW_TIMES_USED, 1,
+ DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400
),
cv(DataUsageStatColumns.DATA_ID, did2p,
DataUsageStatColumns.USAGE_TYPE_INT,
DataUsageStatColumns.USAGE_TYPE_INT_CALL,
- DataUsageStatColumns.TIMES_USED, 1,
- DataUsageStatColumns.LAST_TIME_USED, startTime + 2
+ DataUsageStatColumns.RAW_TIMES_USED, 1,
+ DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400 * 2
),
cv(DataUsageStatColumns.DATA_ID, did2p,
DataUsageStatColumns.USAGE_TYPE_INT,
DataUsageStatColumns.USAGE_TYPE_INT_SHORT_TEXT,
- DataUsageStatColumns.TIMES_USED, 1,
- DataUsageStatColumns.LAST_TIME_USED, startTime + 3
+ DataUsageStatColumns.RAW_TIMES_USED, 1,
+ DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400 * 3
),
cv(DataUsageStatColumns.DATA_ID, did3p,
DataUsageStatColumns.USAGE_TYPE_INT,
DataUsageStatColumns.USAGE_TYPE_INT_SHORT_TEXT,
- DataUsageStatColumns.TIMES_USED, 1,
- DataUsageStatColumns.LAST_TIME_USED, startTime + 3
+ DataUsageStatColumns.RAW_TIMES_USED, 1,
+ DataUsageStatColumns.RAW_LAST_TIME_USED, startTime + 86400 * 3
)
);
@@ -8808,15 +8920,15 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
assertStoredValuesWithProjection(RawContacts.CONTENT_URI,
cv(RawContacts._ID, rid1,
RawContacts.TIMES_CONTACTED, 2,
- RawContacts.LAST_TIME_CONTACTED, startTime + 1
+ RawContacts.LAST_TIME_CONTACTED, (startTime + 86400) / 86400 * 86400
),
cv(RawContacts._ID, rid2,
RawContacts.TIMES_CONTACTED, 3,
- RawContacts.LAST_TIME_CONTACTED, startTime + 3
+ RawContacts.LAST_TIME_CONTACTED, (startTime + 86400 * 3) / 86400 * 86400
),
cv(RawContacts._ID, rid3,
RawContacts.TIMES_CONTACTED, 2,
- RawContacts.LAST_TIME_CONTACTED, startTime + 3
+ RawContacts.LAST_TIME_CONTACTED, (startTime + 86400 * 3) / 86400 * 86400
),
cv(RawContacts._ID, rid4,
RawContacts.TIMES_CONTACTED, 0,
@@ -8832,15 +8944,15 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
assertStoredValuesWithProjection(Contacts.CONTENT_URI,
cv(Contacts._ID, cid1,
Contacts.TIMES_CONTACTED, 4,
- Contacts.LAST_TIME_CONTACTED, startTime + 3
+ Contacts.LAST_TIME_CONTACTED, (startTime + 86400 * 3) / 86400 * 86400
),
cv(Contacts._ID, cid3,
Contacts.TIMES_CONTACTED, 2,
- Contacts.LAST_TIME_CONTACTED, startTime + 3
+ Contacts.LAST_TIME_CONTACTED, (startTime + 86400 * 3) / 86400 * 86400
),
cv(Contacts._ID, cid4,
Contacts.TIMES_CONTACTED, 0,
- Contacts.LAST_TIME_CONTACTED, 0 // For contacts, the default is 0, not null.
+ Contacts.LAST_TIME_CONTACTED, 0
)
);
@@ -8883,26 +8995,45 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
String[] projection = new String[]{ContactsContract.RawContacts.DIRTY,
ContactsContract.RawContacts.DELETED};
- List<String[]> records = RawContactUtil.queryByContactId(mResolver, ids.mContactId,
+ String[] record = RawContactUtil.queryByRawContactId(mResolver, ids.mRawContactId,
projection);
- for (String[] arr : records) {
- assertEquals("1", arr[0]);
- assertEquals("1", arr[1]);
- }
+ assertEquals("1", record[0]);
+ assertEquals("1", record[1]);
// Clean up
RawContactUtil.delete(mResolver, ids.mRawContactId, true);
}
- public void testContactUpdate_dirtyForMetadataChange() {
+ public void testContactDelete_checkRawContactContactId() {
+ DatabaseAsserts.ContactIdPair ids = assertContactCreateDelete();
+
+ String[] projection = new String[]{ContactsContract.RawContacts.CONTACT_ID};
+ String[] record = RawContactUtil.queryByRawContactId(mResolver, ids.mRawContactId,
+ projection);
+ assertNull(record[0]);
+
+ // Clean up
+ RawContactUtil.delete(mResolver, ids.mRawContactId, true);
+ }
+
+ public void testContactUpdate_metadataChange() {
+ // Enable metadataSync flag.
+ final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+ cp.setMetadataSyncForTest(true);
+
DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, ids.mRawContactId);
+ assertDirty(rawContactUri, true);
+ clearDirty(rawContactUri);
ContentValues values = new ContentValues();
values.put(Contacts.PINNED, 1);
ContactUtil.update(mResolver, ids.mContactId, values);
- assertDirty(ContentUris.withAppendedId(RawContacts.CONTENT_URI, ids.mRawContactId), true);
- assertNetworkNotified(true);
+ assertDirty(rawContactUri, false);
+ assertMetadataDirty(rawContactUri, true);
+ assertNetworkNotified(false);
+ assertMetadataNetworkNotified(true);
}
public void testContactUpdate_updatesContactUpdatedTimestamp() {
@@ -9675,10 +9806,13 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
values.put(RawContacts.CUSTOM_RINGTONE, "beethoven5");
values.put(RawContacts.TIMES_CONTACTED, timesContacted);
- Uri insertionUri = isUserProfile
- ? Profile.CONTENT_RAW_CONTACTS_URI
- : RawContacts.CONTENT_URI;
- Uri rawContactUri = mResolver.insert(insertionUri, values);
+ Uri rawContactUri;
+ if (isUserProfile) {
+ rawContactUri = insertProfileRawContact(values);
+ } else {
+ rawContactUri = insertRawContact(values);
+ }
+
long rawContactId = ContentUris.parseId(rawContactUri);
Uri photoUri = insertPhoto(rawContactId);
long photoId = ContentUris.parseId(photoUri);
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2TransactionTest.java b/tests/src/com/android/providers/contacts/ContactsProvider2TransactionTest.java
index 6a82bf9f..03c9e75c 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2TransactionTest.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2TransactionTest.java
@@ -60,7 +60,7 @@ public class ContactsProvider2TransactionTest extends BaseContactsProvider2Test
*/
public void testTransactionCallback_insert() {
- final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 12345);
+ final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);
// Insert a raw contact.
mProvider.resetTrasactionCallbackCalledFlags();
@@ -87,14 +87,14 @@ public class ContactsProvider2TransactionTest extends BaseContactsProvider2Test
*/
public void testTransactionCallback_update() {
- final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 12345);
+ final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);
// Make sure to create a raw contact and a profile raw contact.
mResolver.insert(RawContacts.CONTENT_URI, values);
mResolver.insert(Profile.CONTENT_RAW_CONTACTS_URI, values);
values.clear();
- values.put(RawContacts.LAST_TIME_CONTACTED, 99999);
+ values.put(RawContacts.LAST_TIME_CONTACTED, 86400 * 2);
// Update all raw contacts.
mProvider.resetTrasactionCallbackCalledFlags();
@@ -121,7 +121,7 @@ public class ContactsProvider2TransactionTest extends BaseContactsProvider2Test
*/
public void testTransactionCallback_delete() {
- final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 12345);
+ final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);
// Make sure to create a raw contact and a profile raw contact.
mResolver.insert(RawContacts.CONTENT_URI, values);
@@ -150,7 +150,7 @@ public class ContactsProvider2TransactionTest extends BaseContactsProvider2Test
*/
public void testTransactionCallback_bulkInsert() {
- final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 12345);
+ final ContentValues values = cv(RawContacts.LAST_TIME_CONTACTED, 86400);
// Insert a raw contact.
mProvider.resetTrasactionCallbackCalledFlags();
@@ -179,7 +179,7 @@ public class ContactsProvider2TransactionTest extends BaseContactsProvider2Test
ContentProviderOperation.Builder b;
b = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
b.withValue(RawContacts.STARRED, 1);
- b.withValue(RawContacts.TIMES_CONTACTED, 200001);
+ b.withValue(RawContacts.LAST_TIME_CONTACTED, 86400 * 21);
ops.add(b.build());
b = ContentProviderOperation.newInsert(Data.CONTENT_URI);
@@ -197,7 +197,7 @@ public class ContactsProvider2TransactionTest extends BaseContactsProvider2Test
private void checkStoredContact() {
assertStoredValues(Contacts.CONTENT_URI, cv(
Contacts.DISPLAY_NAME, "Regular Contact",
- RawContacts.TIMES_CONTACTED, 200001
+ RawContacts.LAST_TIME_CONTACTED, 86400 * 21
));
}
@@ -208,7 +208,7 @@ public class ContactsProvider2TransactionTest extends BaseContactsProvider2Test
ContentProviderOperation.Builder b;
b = ContentProviderOperation.newInsert(Profile.CONTENT_RAW_CONTACTS_URI);
b.withValue(RawContacts.STARRED, 1);
- b.withValue(RawContacts.TIMES_CONTACTED, 100001);
+ b.withValue(RawContacts.LAST_TIME_CONTACTED, 86400 * 11);
ops.add(b.build());
b = ContentProviderOperation.newInsert(Data.CONTENT_URI);
@@ -227,7 +227,7 @@ public class ContactsProvider2TransactionTest extends BaseContactsProvider2Test
private void checkStoredProfile() {
assertStoredValues(Profile.CONTENT_URI, cv(
Contacts.DISPLAY_NAME, "Profile Contact",
- RawContacts.TIMES_CONTACTED, 100001
+ RawContacts.LAST_TIME_CONTACTED, 86400 * 11
));
}
diff --git a/tests/src/com/android/providers/contacts/ContactsTaskSchedulerTest.java b/tests/src/com/android/providers/contacts/ContactsTaskSchedulerTest.java
new file mode 100644
index 00000000..df7196dc
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactsTaskSchedulerTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@LargeTest
+public class ContactsTaskSchedulerTest extends AndroidTestCase {
+ private static final int SHUTDOWN_SECONDS = 3;
+
+ private static class MyContactsTaskScheduler extends ContactsTaskScheduler {
+ final CountDownLatch latch;
+
+ final List<String> executed = new ArrayList<>();
+
+ public MyContactsTaskScheduler(int numExpectedTasks) {
+ super("Test", SHUTDOWN_SECONDS);
+ latch = new CountDownLatch(numExpectedTasks);
+ }
+
+ @Override
+ public void onPerformTask(int taskId, Object arg) {
+ executed.add("" + taskId + "," + arg);
+
+ latch.countDown();
+ }
+ }
+
+ public void testSimple() throws Exception {
+ final MyContactsTaskScheduler scheduler = new MyContactsTaskScheduler(3);
+
+ scheduler.scheduleTask(1);
+ scheduler.scheduleTask(10);
+ scheduler.scheduleTask(2, "arg");
+
+ assertTrue(scheduler.latch.await(10, TimeUnit.SECONDS));
+
+ assertEquals(Arrays.asList("1,null", "10,null", "2,arg"), scheduler.executed);
+
+ // Only one thread has been created.
+ assertEquals(1, scheduler.getThreadSequenceNumber());
+ }
+
+ public void testAutoShutdown() throws Exception {
+ final MyContactsTaskScheduler scheduler = new MyContactsTaskScheduler(7);
+
+ scheduler.scheduleTask(1);
+
+ // Wait for 10 seconds and the thread should shut down.
+ assertTrue(scheduler.isRunningForTest());
+ Thread.sleep(10 * 1000);
+ assertFalse(scheduler.isRunningForTest());
+
+ scheduler.scheduleTask(2);
+ assertTrue(scheduler.isRunningForTest());
+
+ Thread.sleep(1 * 1000);
+ scheduler.scheduleTask(3);
+
+ Thread.sleep(1 * 1000);
+ scheduler.scheduleTask(4);
+
+ Thread.sleep(1 * 1000);
+ scheduler.scheduleTask(5);
+
+ Thread.sleep(1 * 1000);
+ scheduler.scheduleTask(6);
+ assertTrue(scheduler.isRunningForTest()); // Should still alive.
+
+ // Wait for 10 seconds and the thread should shut down.
+ Thread.sleep(10 * 1000);
+ assertFalse(scheduler.isRunningForTest());
+
+ scheduler.scheduleTask(7);
+ assertTrue(scheduler.isRunningForTest());
+
+ assertTrue(scheduler.latch.await(10, TimeUnit.SECONDS));
+ assertEquals(7, scheduler.executed.size());
+
+ // Only one thread has been created.
+ assertEquals(3, scheduler.getThreadSequenceNumber());
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/DirectoryTest.java b/tests/src/com/android/providers/contacts/DirectoryTest.java
index 99f05ce8..a832a95b 100644
--- a/tests/src/com/android/providers/contacts/DirectoryTest.java
+++ b/tests/src/com/android/providers/contacts/DirectoryTest.java
@@ -16,6 +16,8 @@
package com.android.providers.contacts;
+import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
+
import android.accounts.Account;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -43,12 +45,16 @@ import com.android.providers.contacts.testutil.RawContactUtil;
@MediumTest
public class DirectoryTest extends BaseContactsProvider2Test {
+ protected String getContextPackageName() {
+ return getContext().getPackageName();
+ }
+
public void testDefaultDirectory() {
ContentValues values = new ContentValues();
Uri defaultDirectoryUri =
ContentUris.withAppendedId(Directory.CONTENT_URI, Directory.DEFAULT);
- values.put(Directory.PACKAGE_NAME, "contactsTestPackage");
+ values.put(Directory.PACKAGE_NAME, getContext().getPackageName());
values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
values.put(Directory.TYPE_RESOURCE_ID, R.string.default_directory);
values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
@@ -64,7 +70,7 @@ public class DirectoryTest extends BaseContactsProvider2Test {
Uri defaultDirectoryUri =
ContentUris.withAppendedId(Directory.CONTENT_URI, Directory.LOCAL_INVISIBLE);
- values.put(Directory.PACKAGE_NAME, "contactsTestPackage");
+ values.put(Directory.PACKAGE_NAME, getContext().getPackageName());
values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
values.put(Directory.TYPE_RESOURCE_ID, R.string.local_invisible_directory);
values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
diff --git a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
index 378c9eb3..21d148c9 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
@@ -16,6 +16,9 @@
package com.android.providers.contacts;
+import static com.android.providers.contacts.TestUtils.dumpTable;
+import static com.android.providers.contacts.TestUtils.dumpUri;
+
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentUris;
@@ -64,12 +67,23 @@ public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
return Contacts.AUTHORITY + ";" + ContactsContract.AUTHORITY;
}
+ private static ContentValues noStats(ContentValues v) {
+ final ContentValues ret = new ContentValues(v);
+ ret.put(People.TIMES_CONTACTED, 0);
+ ret.put(People.LAST_TIME_CONTACTED, 0);
+ return ret;
+ }
+
public void testPeopleInsert() {
ContentValues values = new ContentValues();
putContactValues(values);
Uri uri = mResolver.insert(People.CONTENT_URI, values);
+
+ values = noStats(values);
+
assertStoredValues(uri, values);
+
assertSelection(People.CONTENT_URI, values, "people", People._ID, ContentUris.parseId(uri));
}
@@ -78,6 +92,7 @@ public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
putContactValues(values);
Uri uri = mResolver.insert(People.CONTENT_URI, values);
+ values = noStats(values);
long personId = ContentUris.parseId(uri);
assertStoredValues(uri, values);
assertSelection(People.CONTENT_URI, values, "people", People._ID, personId);
@@ -85,11 +100,13 @@ public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
values.clear();
putContactValues2(values);
mResolver.update(uri, values, null, null);
+ values = noStats(values);
assertStoredValues(uri, values);
values.clear();
putContactValues(values);
mResolver.update(People.CONTENT_URI, values, People._ID + "=" + personId, null);
+ values = noStats(values);
assertStoredValues(uri, values);
}
@@ -207,6 +224,7 @@ public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
values.clear();
putContactValuesExceptName(values);
+ values = noStats(values);
values.put(People.PRIMARY_PHONE_ID, ContentUris.parseId(phoneUri1));
assertStoredValues(phoneUri2, values);
@@ -279,6 +297,7 @@ public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
values.clear();
putContactValuesExceptName(values);
+ values = noStats(values);
values.put(People.PRIMARY_EMAIL_ID, ContentUris.parseId(emailUri1));
assertStoredValues(emailUri2, values);
@@ -314,9 +333,9 @@ public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
int timesContactedAfter =
Integer.parseInt(getStoredValue(personUri, People.TIMES_CONTACTED));
- assertTrue(lastContacted >= timeBefore);
- assertTrue(lastContacted <= timeAfter);
- assertEquals(timesContactedAfter, timesContactedBefore + 1);
+ // No longer supported as of O.
+ assertEquals(0, lastContacted);
+ assertEquals(0, timesContactedAfter);
}
public void testOrganizationsInsert() {
@@ -401,6 +420,7 @@ public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
// The result is joined with People
putContactValues(expectedResults[0]);
+ expectedResults[0] = noStats(expectedResults[0]);
assertStoredValues(uri, expectedResults);
assertSelection(Phones.CONTENT_URI, values, "phones",
Phones._ID, ContentUris.parseId(uri));
@@ -412,6 +432,7 @@ public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
// Now the person should be joined with Phone
values.clear();
putContactValues(values);
+ values = noStats(values);
values.put(People.TYPE, Phones.TYPE_CUSTOM);
values.put(People.LABEL, "Directory");
values.put(People.NUMBER, "1-800-4664-411");
@@ -541,6 +562,9 @@ public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
// The result is joined with People
putContactValues(values);
+
+ values = noStats(values);
+
assertStoredValues(uri, values);
assertSelection(ContactMethods.CONTENT_URI, values, "contact_methods",
ContactMethods._ID, ContentUris.parseId(uri));
diff --git a/tests/src/com/android/providers/contacts/PhotoStoreTest.java b/tests/src/com/android/providers/contacts/PhotoStoreTest.java
index 4e797f75..8ba0438d 100644
--- a/tests/src/com/android/providers/contacts/PhotoStoreTest.java
+++ b/tests/src/com/android/providers/contacts/PhotoStoreTest.java
@@ -56,7 +56,7 @@ public class PhotoStoreTest extends PhotoLoadingTestCase {
mProvider = ((SynchronousContactsProvider2) mActor.provider);
mPhotoStore = mProvider.getPhotoStore();
mProvider.wipeData();
- mDb = mProvider.getDatabaseHelper(getContext()).getReadableDatabase();
+ mDb = mProvider.getDatabaseHelper().getReadableDatabase();
}
@Override
diff --git a/tests/src/com/android/providers/contacts/RenamingDelegatingContext.java b/tests/src/com/android/providers/contacts/RenamingDelegatingContext.java
new file mode 100644
index 00000000..260b730f
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/RenamingDelegatingContext.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.database.DatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.FileUtils;
+import android.util.Log;
+
+import com.google.android.collect.Sets;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.util.Set;
+
+/**
+ * This file was copied from framework/base. The DB related file names now understand fullpath
+ * filenames and will not append the prefix for them.
+ */
+public class RenamingDelegatingContext extends ContextWrapper {
+
+ private Context mFileContext;
+ private String mFilePrefix = null;
+ private File mCacheDir;
+ private final Object mSync = new Object();
+
+ private Set<String> mDatabaseNames = Sets.newHashSet();
+ private Set<String> mFileNames = Sets.newHashSet();
+
+ public static <T extends ContentProvider> T providerWithRenamedContext(
+ Class<T> contentProvider, Context c, String filePrefix)
+ throws IllegalAccessException, InstantiationException {
+ return providerWithRenamedContext(contentProvider, c, filePrefix, false);
+ }
+
+ public static <T extends ContentProvider> T providerWithRenamedContext(
+ Class<T> contentProvider, Context c, String filePrefix,
+ boolean allowAccessToExistingFilesAndDbs)
+ throws IllegalAccessException, InstantiationException {
+ Class<T> mProviderClass = contentProvider;
+ T mProvider = mProviderClass.newInstance();
+ RenamingDelegatingContext mContext = new RenamingDelegatingContext(c, filePrefix);
+ if (allowAccessToExistingFilesAndDbs) {
+ mContext.makeExistingFilesAndDbsAccessible();
+ }
+ mProvider.attachInfoForTesting(mContext, null);
+ return mProvider;
+ }
+
+ /**
+ * Makes accessible all files and databases whose names match the filePrefix that was passed to
+ * the constructor. Normally only files and databases that were created through this context are
+ * accessible.
+ */
+ public void makeExistingFilesAndDbsAccessible() {
+ String[] databaseList = mFileContext.databaseList();
+ for (String diskName : databaseList) {
+ if (shouldDiskNameBeVisible(diskName)) {
+ mDatabaseNames.add(publicNameFromDiskName(diskName));
+ }
+ }
+ String[] fileList = mFileContext.fileList();
+ for (String diskName : fileList) {
+ if (shouldDiskNameBeVisible(diskName)) {
+ mFileNames.add(publicNameFromDiskName(diskName));
+ }
+ }
+ }
+
+ /**
+ * Returns if the given diskName starts with the given prefix or not.
+ * @param diskName name of the database/file.
+ */
+ boolean shouldDiskNameBeVisible(String diskName) {
+ return diskName.startsWith(mFilePrefix);
+ }
+
+ /**
+ * Returns the public name (everything following the prefix) of the given diskName.
+ * @param diskName name of the database/file.
+ */
+ String publicNameFromDiskName(String diskName) {
+ if (!shouldDiskNameBeVisible(diskName)) {
+ throw new IllegalArgumentException("disk file should not be visible: " + diskName);
+ }
+ return diskName.substring(mFilePrefix.length(), diskName.length());
+ }
+
+ /**
+ * @param context : the context that will be delegated.
+ * @param filePrefix : a prefix with which database and file names will be
+ * prefixed.
+ */
+ public RenamingDelegatingContext(Context context, String filePrefix) {
+ super(context);
+ mFileContext = context;
+ mFilePrefix = filePrefix;
+ }
+
+ /**
+ * @param context : the context that will be delegated.
+ * @param fileContext : the context that file and db methods will be delegated to
+ * @param filePrefix : a prefix with which database and file names will be
+ * prefixed.
+ */
+ public RenamingDelegatingContext(Context context, Context fileContext, String filePrefix) {
+ super(context);
+ mFileContext = fileContext;
+ mFilePrefix = filePrefix;
+ }
+
+ public String getDatabasePrefix() {
+ return mFilePrefix;
+ }
+
+ private String renamedFileName(String name) {
+ return mFilePrefix + name;
+ }
+
+ @Override
+ public SQLiteDatabase openOrCreateDatabase(String name,
+ int mode, SQLiteDatabase.CursorFactory factory) {
+ if (name.startsWith("/")) {
+ return mFileContext.openOrCreateDatabase(name, mode, factory);
+ }
+ final String internalName = renamedFileName(name);
+ if (!mDatabaseNames.contains(name)) {
+ mDatabaseNames.add(name);
+ mFileContext.deleteDatabase(internalName);
+ }
+ return mFileContext.openOrCreateDatabase(internalName, mode, factory);
+ }
+
+ @Override
+ public SQLiteDatabase openOrCreateDatabase(String name,
+ int mode, SQLiteDatabase.CursorFactory factory, DatabaseErrorHandler errorHandler) {
+ if (name.startsWith("/")) {
+ return mFileContext.openOrCreateDatabase(name, mode, factory, errorHandler);
+ }
+ final String internalName = renamedFileName(name);
+ if (!mDatabaseNames.contains(name)) {
+ mDatabaseNames.add(name);
+ mFileContext.deleteDatabase(internalName);
+ }
+ return mFileContext.openOrCreateDatabase(internalName, mode, factory, errorHandler);
+ }
+
+ @Override
+ public boolean deleteDatabase(String name) {
+ if (name.startsWith("/")) {
+ return mFileContext.deleteDatabase(name);
+ }
+ if (mDatabaseNames.contains(name)) {
+ mDatabaseNames.remove(name);
+ return mFileContext.deleteDatabase(renamedFileName(name));
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public File getDatabasePath(String name) {
+ if (name.startsWith("/")) {
+ return mFileContext.getDatabasePath(name);
+ }
+ return mFileContext.getDatabasePath(renamedFileName(name));
+ }
+
+ @Override
+ public String[] databaseList() {
+ return mDatabaseNames.toArray(new String[]{});
+ }
+
+ @Override
+ public FileInputStream openFileInput(String name)
+ throws FileNotFoundException {
+ final String internalName = renamedFileName(name);
+ if (mFileNames.contains(name)) {
+ return mFileContext.openFileInput(internalName);
+ } else {
+ throw new FileNotFoundException(internalName);
+ }
+ }
+
+ @Override
+ public FileOutputStream openFileOutput(String name, int mode)
+ throws FileNotFoundException {
+ mFileNames.add(name);
+ return mFileContext.openFileOutput(renamedFileName(name), mode);
+ }
+
+ @Override
+ public File getFileStreamPath(String name) {
+ return mFileContext.getFileStreamPath(renamedFileName(name));
+ }
+
+ @Override
+ public boolean deleteFile(String name) {
+ if (mFileNames.contains(name)) {
+ mFileNames.remove(name);
+ return mFileContext.deleteFile(renamedFileName(name));
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String[] fileList() {
+ return mFileNames.toArray(new String[]{});
+ }
+
+ /**
+ * In order to support calls to getCacheDir(), we create a temp cache dir (inside the real
+ * one) and return it instead. This code is basically getCacheDir(), except it uses the real
+ * cache dir as the parent directory and creates a test cache dir inside that.
+ */
+ @Override
+ public File getCacheDir() {
+ synchronized (mSync) {
+ if (mCacheDir == null) {
+ mCacheDir = new File(mFileContext.getCacheDir(), renamedFileName("cache"));
+ }
+ if (!mCacheDir.exists()) {
+ if(!mCacheDir.mkdirs()) {
+ Log.w("RenamingDelegatingContext", "Unable to create cache directory");
+ return null;
+ }
+ FileUtils.setPermissions(
+ mCacheDir.getPath(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ }
+ }
+ return mCacheDir;
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java b/tests/src/com/android/providers/contacts/SecondaryUserContactsProvider2.java
index 8dd09bfa..61ae1ca3 100644
--- a/tests/src/com/android/providers/contacts/StandaloneContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SecondaryUserContactsProvider2.java
@@ -18,27 +18,22 @@ package com.android.providers.contacts;
import android.content.Context;
/**
- * A subclass of {@link SynchronousContactsProvider2} that doesn't reuse the database helper.
+ * A subclass of {@link SynchronousContactsProvider2} that uses a different DB for secondary users.
*/
-public class StandaloneContactsProvider2 extends SynchronousContactsProvider2 {
- private static ContactsDatabaseHelper mDbHelper;
+public class SecondaryUserContactsProvider2 extends SynchronousContactsProvider2 {
+ private final String mDbSuffix;
+ private ContactsDatabaseHelper mDbHelper;
- public StandaloneContactsProvider2() {
- // No need to wipe data for this instance since it doesn't reuse the db helper.
- setDataWipeEnabled(false);
+ public SecondaryUserContactsProvider2(int userId) {
+ mDbSuffix = "-u" + userId;
}
@Override
- public ContactsDatabaseHelper getDatabaseHelper(final Context context) {
+ public ContactsDatabaseHelper newDatabaseHelper(final Context context) {
if (mDbHelper == null) {
- mDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context);
+ mDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context,
+ TestUtils.getContactsDatabaseFilename(context, mDbSuffix));
}
return mDbHelper;
}
-
- @Override
- public void setDataWipeEnabled(boolean flag) {
- // No need to wipe data for this instance since it doesn't reuse the db helper.
- super.setDataWipeEnabled(false);
- }
}
diff --git a/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java b/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java
index e7b80a04..3dd6d57b 100644
--- a/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java
+++ b/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java
@@ -17,14 +17,15 @@
package com.android.providers.contacts;
import static com.android.providers.contacts.EvenMoreAsserts.assertThrows;
+import static com.android.providers.contacts.TestUtils.cv;
import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.net.Uri.Builder;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.providers.contacts.testutil.RawContactUtil;
@@ -42,36 +43,54 @@ import com.android.providers.contacts.testutil.RawContactUtil;
public class SqlInjectionDetectionTest extends BaseContactsProvider2Test {
private static final String[] PHONE_ID_PROJECTION = new String[] { Phone._ID };
- public void testPhoneQueryValid() {
- long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
- insertPhoneNumber(rawContactId, "555-123-4567");
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+ public void testQueryValid() {
assertQueryValid(Phone.CONTENT_URI, PHONE_ID_PROJECTION,
Phone.NUMBER + "='555-123-4567'", null);
+
+ // The following tables are whitelisted.
+ assertQueryValid(Data.CONTENT_URI, null,
+ "data._id in default_directory", null);
}
public void testPhoneQueryBadProjection() {
- long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
- insertPhoneNumber(rawContactId, "555-123-4567");
-
- assertQueryThrows(IllegalArgumentException.class, Phone.CONTENT_URI,
+ assertQueryThrows(Phone.CONTENT_URI,
new String[] { "0 UNION SELECT _id FROM view_data--" }, null, null);
+
+ // Invalid column names should be detected too.
+ assertQueryThrows(Phone.CONTENT_URI, new String[] { "a" }, null, null);
+ assertQueryThrows(Phone.CONTENT_URI, new String[] { " _id" }, null, null);
+
+ // This is still invalid because we only allow exact column names in projections.
+ assertQueryThrows(Phone.CONTENT_URI, new String[] { "[_id]" }, null, null);
}
public void testPhoneQueryBadSelection() {
- long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
- insertPhoneNumber(rawContactId, "555-123-4567");
-
- assertQueryThrows(SQLiteException.class, Phone.CONTENT_URI, PHONE_ID_PROJECTION,
+ assertQueryThrows(Phone.CONTENT_URI, PHONE_ID_PROJECTION,
"0=1) UNION SELECT _id FROM view_data--", null);
+ assertQueryThrows(Phone.CONTENT_URI, PHONE_ID_PROJECTION, ";delete from contacts", null);
+ if (ContactsDatabaseHelper.DISALLOW_SUB_QUERIES) {
+ assertQueryThrows(Phone.CONTENT_URI, PHONE_ID_PROJECTION,
+ "_id in data_usage_stat", null);
+ assertQueryThrows(Phone.CONTENT_URI, PHONE_ID_PROJECTION,
+ "_id in (select _id from default_directory)", null);
+ }
}
public void testPhoneQueryBadSortOrder() {
- long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
- insertPhoneNumber(rawContactId, "555-123-4567");
-
- assertQueryThrows(SQLiteException.class, Phone.CONTENT_URI,
+ assertQueryThrows(Phone.CONTENT_URI,
PHONE_ID_PROJECTION, null, "_id UNION SELECT _id FROM view_data--");
+ assertQueryThrows(Phone.CONTENT_URI, PHONE_ID_PROJECTION, null, ";delete from contacts");
+ if (ContactsDatabaseHelper.DISALLOW_SUB_QUERIES) {
+ assertQueryThrows(Phone.CONTENT_URI, PHONE_ID_PROJECTION, null,
+ "_id in data_usage_stat");
+ assertQueryThrows(Phone.CONTENT_URI, PHONE_ID_PROJECTION,
+ null, "exists (select _id from default_directory)");
+ }
}
public void testPhoneQueryBadLimit() {
@@ -100,14 +119,46 @@ public class SqlInjectionDetectionTest extends BaseContactsProvider2Test {
c.close();
}
- private <T extends Exception> void assertQueryThrows(Class<T> exception, final Uri uri,
+ private <T extends Exception> void assertQueryThrows(final Uri uri,
final String[] projection, final String selection, final String sortOrder) {
- assertThrows(exception, new Runnable() {
- @Override
- public void run() {
+ assertThrows(IllegalArgumentException.class, () -> {
final Cursor c = mResolver.query(uri, projection, selection, null, sortOrder);
c.close();
- }
});
}
+
+ public void testBadDelete() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ mResolver.delete(Contacts.CONTENT_URI, ";delete from contacts;--", null);
+ });
+ if (ContactsDatabaseHelper.DISALLOW_SUB_QUERIES) {
+ assertThrows(IllegalArgumentException.class, () -> {
+ mResolver.delete(Contacts.CONTENT_URI, "_id in data_usage_stat", null);
+ });
+ }
+ }
+
+ public void testBadUpdate() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ mResolver.update(Data.CONTENT_URI, cv(), ";delete from contacts;--", null);
+ });
+ if (ContactsDatabaseHelper.DISALLOW_SUB_QUERIES) {
+ assertThrows(IllegalArgumentException.class, () -> {
+ mResolver.update(Data.CONTENT_URI, cv(), "_id in data_usage_stat", null);
+ });
+ assertThrows(IllegalArgumentException.class, () -> {
+ mResolver.update(Data.CONTENT_URI, cv("_id/**/", 1), null, null);
+ });
+
+ mResolver.update(Data.CONTENT_URI, cv("[data1]", 1), null, null);
+ }
+ }
+
+ public void testBadInsert() {
+ if (ContactsDatabaseHelper.DISALLOW_SUB_QUERIES) {
+ assertThrows(IllegalArgumentException.class, () -> {
+ mResolver.insert(Data.CONTENT_URI, cv("_id/**/", 1));
+ });
+ }
+ }
}
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index 19878f81..f674dd5b 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -34,7 +34,6 @@ public class SynchronousContactsProvider2 extends ContactsProvider2 {
private static Boolean sDataWiped = false;
private static ContactsDatabaseHelper sDbHelper;
- private boolean mDataWipeEnabled = true;
private Account mAccount;
private boolean mNetworkNotified;
private boolean mMetadataNetworkNotified;
@@ -42,9 +41,10 @@ public class SynchronousContactsProvider2 extends ContactsProvider2 {
private boolean mIsVoiceCapable = true;
@Override
- public ContactsDatabaseHelper getDatabaseHelper(final Context context) {
+ public ContactsDatabaseHelper newDatabaseHelper(final Context context) {
if (sDbHelper == null) {
- sDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context);
+ sDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context,
+ TestUtils.getContactsDatabaseFilename(getContext()));
}
return sDbHelper;
}
@@ -54,8 +54,8 @@ public class SynchronousContactsProvider2 extends ContactsProvider2 {
return new SynchronousProfileProvider(this);
}
- public void setDataWipeEnabled(boolean flag) {
- mDataWipeEnabled = flag;
+ public ProfileDatabaseHelper getProfileDatabaseHelper() {
+ return getProfileProviderForTest().getDatabaseHelper();
}
@Override
@@ -100,12 +100,10 @@ public class SynchronousContactsProvider2 extends ContactsProvider2 {
@Override
public boolean onCreate() {
boolean created = super.onCreate();
- if (mDataWipeEnabled) {
- synchronized (sDataWiped) {
- if (!sDataWiped) {
- sDataWiped = true;
- wipeData();
- }
+ synchronized (sDataWiped) {
+ if (!sDataWiped) {
+ sDataWiped = true;
+ wipeData();
}
}
return created;
@@ -192,7 +190,7 @@ public class SynchronousContactsProvider2 extends ContactsProvider2 {
}
public void prepareForFullAggregation(int maxContact) {
- SQLiteDatabase db = getDatabaseHelper(getContext()).getWritableDatabase();
+ SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
db.execSQL("UPDATE raw_contacts SET aggregation_mode=0,aggregation_needed=1;");
long rowId =
db.compileStatement("SELECT _id FROM raw_contacts LIMIT 1 OFFSET " + maxContact)
@@ -201,12 +199,12 @@ public class SynchronousContactsProvider2 extends ContactsProvider2 {
}
public long getRawContactCount() {
- SQLiteDatabase db = getDatabaseHelper(getContext()).getReadableDatabase();
+ SQLiteDatabase db = getDatabaseHelper().getReadableDatabase();
return db.compileStatement("SELECT COUNT(*) FROM raw_contacts").simpleQueryForLong();
}
public long getContactCount() {
- SQLiteDatabase db = getDatabaseHelper(getContext()).getReadableDatabase();
+ SQLiteDatabase db = getDatabaseHelper().getReadableDatabase();
return db.compileStatement("SELECT COUNT(*) FROM contacts").simpleQueryForLong();
}
@@ -214,12 +212,12 @@ public class SynchronousContactsProvider2 extends ContactsProvider2 {
public void wipeData() {
Log.i(TAG, "wipeData");
super.wipeData();
- SQLiteDatabase db = getDatabaseHelper(getContext()).getWritableDatabase();
+ SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
db.execSQL("replace into SQLITE_SEQUENCE (name,seq) values('raw_contacts', 42)");
db.execSQL("replace into SQLITE_SEQUENCE (name,seq) values('contacts', 2009)");
db.execSQL("replace into SQLITE_SEQUENCE (name,seq) values('data', 777)");
- getContactDirectoryManagerForTest().scanAllPackages();
+ getContactDirectoryManagerForTest().scanAllPackages(/* rescan= */ true);
}
// Flags to remember which transaction callback has been called for which mode.
diff --git a/tests/src/com/android/providers/contacts/SynchronousProfileProvider.java b/tests/src/com/android/providers/contacts/SynchronousProfileProvider.java
index 308e67a5..79356a51 100644
--- a/tests/src/com/android/providers/contacts/SynchronousProfileProvider.java
+++ b/tests/src/com/android/providers/contacts/SynchronousProfileProvider.java
@@ -32,9 +32,10 @@ public class SynchronousProfileProvider extends ProfileProvider {
}
@Override
- protected ProfileDatabaseHelper getDatabaseHelper(final Context context) {
+ protected ProfileDatabaseHelper newDatabaseHelper(final Context context) {
if (sDbHelper == null) {
- sDbHelper = ProfileDatabaseHelper.getNewInstanceForTest(context);
+ sDbHelper = ProfileDatabaseHelper.getNewInstanceForTest(context,
+ TestUtils.getProfileDatabaseFilename(getContext()));
}
return sDbHelper;
}
diff --git a/tests/src/com/android/providers/contacts/TestUtils.java b/tests/src/com/android/providers/contacts/TestUtils.java
index b6d6a271..322e5b46 100644
--- a/tests/src/com/android/providers/contacts/TestUtils.java
+++ b/tests/src/com/android/providers/contacts/TestUtils.java
@@ -16,21 +16,118 @@
package com.android.providers.contacts;
+import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.os.FileUtils;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Profile;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.Nullable;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
import android.util.Log;
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+
import junit.framework.Assert;
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
public class TestUtils {
+ private static final String TAG = "ContactsTestUtils";
+
private TestUtils() {
}
+ /**
+ * We normally use in-memory DBs in unit tests, because that's faster, but it's impossible to
+ * look at intermediate databases when something is failing. When this flag is set to true,
+ * we'll switch to file-based DBs, so we can call {@link #createDatabaseSnapshot}
+ * , pull the snapshot DBs and take a look at them.
+ */
+ public static final boolean ENABLE_DATABASE_SNAPSHOT = false; // DO NOT SUBMIT WITH TRUE.
+
+ private static final Object sDatabasePathLock = new Object();
+ private static File sDatabasePath = null;
+
+ private static String getDatabaseFile(Context context, @Nullable String name) {
+ if (!ENABLE_DATABASE_SNAPSHOT) {
+ return null; // Use the in-memory DB.
+ }
+ synchronized (sDatabasePathLock) {
+ if (sDatabasePath == null) {
+ final File path = new File(context.getCacheDir(), "test-db");
+ if (path.exists()) {
+ Assert.assertTrue("Unable to delete directory: " + path,
+ FileUtils.deleteContents(path));
+ } else {
+ Assert.assertTrue("Unable to create directory: " + path, path.mkdirs());
+ }
+ Log.i(TAG, "Test DB directory: " + path);
+
+ sDatabasePath = path;
+ }
+ final File ret;
+ if (name == null) {
+ ret = sDatabasePath;
+ } else {
+ ret = new File(sDatabasePath, name);
+ Log.i(TAG, "Test DB file: " + ret);
+ }
+ return ret.getAbsolutePath();
+ }
+ }
+
+ public static String getContactsDatabaseFilename(Context context) {
+ return getContactsDatabaseFilename(context, "");
+ }
+
+ public static String getContactsDatabaseFilename(Context context, String suffix) {
+ return getDatabaseFile(context, "contacts2" + suffix + ".db");
+ }
+
+ public static String getProfileDatabaseFilename(Context context) {
+ return getProfileDatabaseFilename(context, "");
+ }
+
+ public static String getProfileDatabaseFilename(Context context, String suffix) {
+ return getDatabaseFile(context, "profile.db" + suffix + ".db");
+ }
+
+ public static void createDatabaseSnapshot(Context context, String name) {
+ Assert.assertTrue(
+ "ENABLE_DATABASE_SNAPSHOT must be set to true to create database snapshot",
+ ENABLE_DATABASE_SNAPSHOT);
+
+ final File fromDir = new File(getDatabaseFile(context, null));
+ final File toDir = new File(context.getCacheDir(), "snapshot-" + name);
+ if (toDir.exists()) {
+ Assert.assertTrue("Unable to delete directory: " + toDir,
+ FileUtils.deleteContents(toDir));
+ } else {
+ Assert.assertTrue("Unable to create directory: " + toDir, toDir.mkdirs());
+ }
+
+ Log.w(TAG, "Copying database files from '" + fromDir + "' into '" + toDir + "'...");
+
+ for (File file : fromDir.listFiles()) {
+ try {
+ final File to = new File(toDir, file.getName());
+ FileUtils.copyFileOrThrow(file, to);
+ Log.i(TAG, "Created: " + to);
+ } catch (IOException e) {
+ Assert.fail("Failed to copy file: " + e.toString());
+ }
+ }
+ }
+
/** Convenient method to create a ContentValues */
public static ContentValues cv(Object... namesAndValues) {
Assert.assertTrue((namesAndValues.length % 2) == 0);
@@ -58,20 +155,20 @@ public class TestUtils {
* Writes the content of a cursor to the log.
*/
public static final void dumpCursor(Cursor c) {
- final String TAG = "contacts";
-
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < c.getColumnCount(); i++) {
- if (sb.length() > 0) sb.append("|");
+ if (i > 0) sb.append("|");
sb.append(c.getColumnName(i));
}
Log.i(TAG, sb.toString());
+ final int pos = c.getPosition();
+
c.moveToPosition(-1);
while (c.moveToNext()) {
sb.setLength(0);
for (int i = 0; i < c.getColumnCount(); i++) {
- if (sb.length() > 0) sb.append("|");
+ if (i > 0) sb.append("|");
if (c.getType(i) == Cursor.FIELD_TYPE_BLOB) {
byte[] blob = c.getBlob(i);
@@ -84,6 +181,29 @@ public class TestUtils {
}
Log.i(TAG, sb.toString());
}
+
+ c.moveToPosition(pos);
+ }
+
+ public static void dumpTable(SQLiteDatabase db, String name) {
+ Log.i(TAG, "Dumping table: " + name);
+ try (Cursor c = db.rawQuery(String.format("SELECT * FROM %s", name), null)) {
+ dumpCursor(c);
+ }
+ }
+
+ public static void dumpUri(Context context, Uri uri) {
+ Log.i(TAG, "Dumping URI: " + uri);
+ try (Cursor c = context.getContentResolver().query(uri, null, null, null, null)) {
+ dumpCursor(c);
+ }
+ }
+
+ public static void dumpUri(ContentResolver resolver, Uri uri) {
+ Log.i(TAG, "Dumping URI: " + uri);
+ try (Cursor c = resolver.query(uri, null, null, null, null)) {
+ dumpCursor(c);
+ }
}
/**
@@ -101,4 +221,57 @@ public class TestUtils {
return "[Failed to write to file: " + e.getMessage() + "]";
}
}
+
+ public static Uri insertRawContact(
+ ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values) {
+ return insertRawContact(RawContacts.CONTENT_URI, resolver, dbh, values);
+ }
+
+ public static Uri insertProfileRawContact(
+ ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values) {
+ return insertRawContact(Profile.CONTENT_RAW_CONTACTS_URI, resolver, dbh, values);
+ }
+
+ private static Uri insertRawContact(Uri tableUri,
+ ContentResolver resolver, ContactsDatabaseHelper dbh, ContentValues values) {
+ final SQLiteDatabase db = dbh.getWritableDatabase();
+
+ final Uri rowUri = resolver.insert(tableUri, values);
+ Long timesContacted = values.getAsLong(RawContacts.LR_TIMES_CONTACTED);
+ if (timesContacted != null) {
+ // TIMES_CONTACTED is no longer modifiable via resolver, so we update the DB directly.
+ final long rid = Long.parseLong(rowUri.getLastPathSegment());
+
+ final String[] args = {String.valueOf(rid)};
+
+ db.update(Tables.RAW_CONTACTS,
+ cv(RawContacts.RAW_TIMES_CONTACTED, (long) timesContacted),
+ "_id=?", args);
+
+ // Then propagate it to contacts too.
+ db.execSQL("UPDATE " + Tables.CONTACTS
+ + " SET " + Contacts.RAW_TIMES_CONTACTED + " = ("
+ + " SELECT sum(" + RawContacts.RAW_TIMES_CONTACTED + ") FROM "
+ + Tables.RAW_CONTACTS + " AS r "
+ + " WHERE " + Tables.CONTACTS + "._id = r." + RawContacts.CONTACT_ID
+ + " GROUP BY r." + RawContacts.CONTACT_ID + ")");
+ }
+ return rowUri;
+ }
+
+ public static void executeSqlFromAssetFile(
+ Context context, SQLiteDatabase db, String assetName) {
+ try (InputStream input = context.getAssets().open(assetName);) {
+ BufferedReader r = new BufferedReader(new InputStreamReader(input));
+ String query;
+ while ((query = r.readLine()) != null) {
+ if (query.trim().length() == 0 || query.startsWith("--")) {
+ continue;
+ }
+ db.execSQL(query);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e.toString());
+ }
+ }
}
diff --git a/tests/src/com/android/providers/contacts/VoicemailCleanupServiceTest.java b/tests/src/com/android/providers/contacts/VoicemailCleanupServiceTest.java
deleted file mode 100644
index f8d76ee1..00000000
--- a/tests/src/com/android/providers/contacts/VoicemailCleanupServiceTest.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2011 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.ContentValues;
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.VoicemailContract;
-import android.provider.VoicemailContract.Status;
-import android.provider.VoicemailContract.Voicemails;
-import android.test.suitebuilder.annotation.SmallTest;
-
-/**
- * Unit tests for {@link VoicemailCleanupService}.
- */
-@SmallTest
-public class VoicemailCleanupServiceTest extends BaseVoicemailProviderTest {
- private static final String TEST_PACKAGE_1 = "package1";
- private static final String TEST_PACKAGE_2 = "package2";
- // Object under test.
- private VoicemailCleanupService mCleanupService;
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- setUpForFullPermission();
- mCleanupService = new VoicemailCleanupService();
- }
-
- public void testIntentHandling() {
- mCleanupService = new VoicemailCleanupService();
- insertDataForPackage(TEST_PACKAGE_1);
- insertDataForPackage(TEST_PACKAGE_2);
- checkDataExistsForPackages(TEST_PACKAGE_1, TEST_PACKAGE_2);
- // No action on PACKAGE_CHANGED.
- sendIntent(TEST_PACKAGE_1, Intent.ACTION_PACKAGE_CHANGED, null);
- checkDataExistsForPackages(TEST_PACKAGE_1, TEST_PACKAGE_2);
-
- // No action on PACKAGE_REPLACED.
- sendIntent(TEST_PACKAGE_1, Intent.ACTION_PACKAGE_REPLACED, null);
- checkDataExistsForPackages(TEST_PACKAGE_1, TEST_PACKAGE_2);
-
- // No action on PACKAGE_REMOVED with EXTRA_REPLACING = true.
- sendIntent(TEST_PACKAGE_1, Intent.ACTION_PACKAGE_REMOVED, true);
- checkDataExistsForPackages(TEST_PACKAGE_1, TEST_PACKAGE_2);
-
- // Data removed on PACKAGE_REMOVED but with no EXTRA_REPLACING.
- sendIntent(TEST_PACKAGE_1, Intent.ACTION_PACKAGE_REMOVED, null);
- checkDataDoesNotExistForPackage(TEST_PACKAGE_1);
- checkDataExistsForPackages(TEST_PACKAGE_2);
-
- // Data removed on PACKAGE_REMOVED with EXTRA_REPLACING = false.
- sendIntent(TEST_PACKAGE_2, Intent.ACTION_PACKAGE_REMOVED, false);
- checkDataDoesNotExistForPackage(TEST_PACKAGE_1);
- checkDataDoesNotExistForPackage(TEST_PACKAGE_2);
- }
-
- private void sendIntent(String sourcePackage, String action, Boolean replacingExtra) {
- Intent packageIntent = new Intent(action, Uri.parse("package:" + sourcePackage));
- if (replacingExtra != null) {
- packageIntent.putExtra(Intent.EXTRA_REPLACING, replacingExtra);
- }
- mCleanupService.handleIntentInternal(packageIntent, mResolver);
- }
-
- private void insertDataForPackage(String sourcePackage) {
- ContentValues values = new ContentValues();
- values.put(VoicemailContract.SOURCE_PACKAGE_FIELD, sourcePackage);
- mResolver.insert(Voicemails.buildSourceUri(sourcePackage), values);
- mResolver.insert(Status.buildSourceUri(sourcePackage), values);
- }
-
- void checkDataExistsForPackages(String... sourcePackages) {
- for (String sourcePackage : sourcePackages) {
- checkDataExistsForPackage(sourcePackage);
- }
- }
-
- private void checkDataExistsForPackage(String sourcePackage) {
- Cursor cursor = mResolver.query(
- Voicemails.buildSourceUri(sourcePackage), null, null, null, null);
- assertNotSame(0, cursor.getCount());
- cursor = mResolver.query(
- Status.buildSourceUri(sourcePackage), null, null, null, null);
- assertNotSame(0, cursor.getCount());
- }
-
- private void checkDataDoesNotExistForPackage(String sourcePackage) {
- Cursor cursor = mResolver.query(
- Voicemails.buildSourceUri(sourcePackage), null, null, null, null);
- assertEquals(0, cursor.getCount());
- cursor = mResolver.query(
- Status.buildSourceUri(sourcePackage), null, null, null, null);
- assertEquals(0, cursor.getCount());
- }
-}
diff --git a/tests/src/com/android/providers/contacts/VoicemailCleanupTest.java b/tests/src/com/android/providers/contacts/VoicemailCleanupTest.java
new file mode 100644
index 00000000..50ecbffc
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/VoicemailCleanupTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2017 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.ContentValues;
+import android.database.Cursor;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import android.provider.VoicemailContract.Voicemails;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.providers.contacts.testutil.TestUtil;
+
+/**
+ * Tests for {@link VoicemailCleanupTest}.
+ */
+@SmallTest
+public class VoicemailCleanupTest extends BaseVoicemailProviderTest {
+ private static final String TEST_PACKAGE_1 = "package1";
+ private static final String TEST_PACKAGE_2 = "package2";
+ private static final String TEST_PACKAGE_3 = "package3";
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ setUpForFullPermission();
+ }
+
+ /**
+ * Test for {@link ContactsPackageMonitor#cleanupVoicemail}.
+ */
+ public void testCleanupVoicemail() {
+ insertDataForPackage(TEST_PACKAGE_1);
+ insertDataForPackage(TEST_PACKAGE_2);
+
+ mPackageManager.addPackage(123456, TEST_PACKAGE_2);
+
+ checkDataExistsForPackage(TEST_PACKAGE_1);
+ checkDataExistsForPackage(TEST_PACKAGE_2);
+
+ ContactsPackageMonitor.cleanupVoicemail(getMockContext(), TEST_PACKAGE_1);
+
+ checkDataDoesNotExistForPackage(TEST_PACKAGE_1);
+ checkDataExistsForPackage(TEST_PACKAGE_2);
+
+ // Call for TEST_PACKAGE_2, which is still installed.
+ ContactsPackageMonitor.cleanupVoicemail(getMockContext(), TEST_PACKAGE_2);
+
+ checkDataDoesNotExistForPackage(TEST_PACKAGE_1);
+ checkDataExistsForPackage(TEST_PACKAGE_2);
+
+ // Uninstall the package and try again.
+ mPackageManager.removePackage(123456);
+ ContactsPackageMonitor.cleanupVoicemail(getMockContext(), TEST_PACKAGE_2);
+
+ checkDataDoesNotExistForPackage(TEST_PACKAGE_1);
+ checkDataDoesNotExistForPackage(TEST_PACKAGE_2);
+ }
+
+ private void insertDataForPackage(String sourcePackage) {
+ ContentValues values = new ContentValues();
+ values.put(VoicemailContract.SOURCE_PACKAGE_FIELD, sourcePackage);
+ mResolver.insert(Voicemails.buildSourceUri(sourcePackage), values);
+ mResolver.insert(Status.buildSourceUri(sourcePackage), values);
+ }
+
+ private static void assertBigger(int smaller, int actual) {
+ if (smaller >= actual) {
+ fail("Expected to be bigger than " + smaller + ", but was " + actual);
+ }
+ }
+
+ private void checkDataExistsForPackage(String sourcePackage) {
+ Cursor cursor = mResolver.query(
+ Voicemails.buildSourceUri(sourcePackage), null, null, null, null);
+ assertBigger(0, cursor.getCount());
+ cursor = mResolver.query(
+ Status.buildSourceUri(sourcePackage), null, null, null, null);
+ assertBigger(0, cursor.getCount());
+ }
+
+ private void checkDataDoesNotExistForPackage(String sourcePackage) {
+ Cursor cursor = mResolver.query(
+ Voicemails.buildSourceUri(sourcePackage), null,
+ "(ifnull(" + Voicemails.DELETED + ",0)==0)", null, null);
+ assertEquals(0, cursor.getCount());
+ cursor = mResolver.query(
+ Status.buildSourceUri(sourcePackage), null, null, null, null);
+ assertEquals(0, cursor.getCount());
+ }
+
+ public void testRemoveStalePackagesAtStartUp() {
+ insertDataForPackage(TEST_PACKAGE_1);
+ insertDataForPackage(TEST_PACKAGE_2);
+ insertDataForPackage(TEST_PACKAGE_3);
+
+ mPackageManager.addPackage(10001, TEST_PACKAGE_1);
+ mPackageManager.addPackage(10002, TEST_PACKAGE_2);
+
+ checkDataExistsForPackage(TEST_PACKAGE_1);
+ checkDataExistsForPackage(TEST_PACKAGE_2);
+ checkDataExistsForPackage(TEST_PACKAGE_3);
+
+ final VoicemailContentProvider provider =
+ (VoicemailContentProvider) mActor.provider;
+
+ // In unit tests, BG tasks are synchronous.
+ provider.scheduleScanStalePackages();
+
+ checkDataExistsForPackage(TEST_PACKAGE_1);
+ checkDataExistsForPackage(TEST_PACKAGE_2);
+ checkDataDoesNotExistForPackage(TEST_PACKAGE_3);
+
+ mPackageManager.removePackage(10001);
+
+ provider.scheduleScanStalePackages();
+
+ checkDataDoesNotExistForPackage(TEST_PACKAGE_1);
+ checkDataExistsForPackage(TEST_PACKAGE_2);
+ checkDataDoesNotExistForPackage(TEST_PACKAGE_3);
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
index 16abf2f6..4fa935fa 100644
--- a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
@@ -61,7 +61,7 @@ public class VoicemailProviderTest extends BaseVoicemailProviderTest {
Calls.COUNTRY_ISO
};
/** Total number of columns exposed by voicemail provider. */
- private static final int NUM_VOICEMAIL_FIELDS = 23;
+ private static final int NUM_VOICEMAIL_FIELDS = 24;
@Override
protected void setUp() throws Exception {
diff --git a/tests/src/com/android/providers/contacts/sqlite/DatabaseAnalyzerTest.java b/tests/src/com/android/providers/contacts/sqlite/DatabaseAnalyzerTest.java
new file mode 100644
index 00000000..568e1e8f
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/sqlite/DatabaseAnalyzerTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 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.sqlite;
+
+import android.test.AndroidTestCase;
+
+import com.android.providers.contacts.ContactsDatabaseHelper;
+import com.android.providers.contacts.TestUtils;
+
+import java.util.List;
+
+public class DatabaseAnalyzerTest extends AndroidTestCase {
+ public void testFindTableViewsAllowingColumns() {
+ final ContactsDatabaseHelper dbh =
+ ContactsDatabaseHelper.getNewInstanceForTest(getContext(),
+ TestUtils.getContactsDatabaseFilename(getContext()));
+ try {
+ final List<String> list = DatabaseAnalyzer.findTableViewsAllowingColumns(
+ dbh.getReadableDatabase());
+
+ assertTrue(list.contains("contacts"));
+ assertTrue(list.contains("raw_contacts"));
+ assertTrue(list.contains("view_contacts"));
+ assertTrue(list.contains("view_raw_contacts"));
+ assertTrue(list.contains("view_data"));
+
+ assertFalse(list.contains("data"));
+ assertFalse(list.contains("_id"));
+
+ } finally {
+ dbh.close();
+ }
+ }
+} \ No newline at end of file
diff --git a/tests/src/com/android/providers/contacts/sqlite/SqlCheckerTest.java b/tests/src/com/android/providers/contacts/sqlite/SqlCheckerTest.java
new file mode 100644
index 00000000..ee2b5be1
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/sqlite/SqlCheckerTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2016 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.sqlite;
+
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+
+import com.android.providers.contacts.sqlite.SqlChecker.InvalidSqlException;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class SqlCheckerTest extends AndroidTestCase {
+ private ArrayList<String> getTokens(String sql) {
+ final ArrayList<String> tokens = new ArrayList<>();
+
+ SqlChecker.findTokens(sql, SqlChecker.OPTION_NONE, token -> tokens.add(token));
+
+ return tokens;
+ }
+
+ private void checkTokens(String sql, String spaceSeparatedExpectedTokens) {
+ final List<String> expected = spaceSeparatedExpectedTokens == null
+ ? new ArrayList<>()
+ : Arrays.asList(spaceSeparatedExpectedTokens.split(" +"));
+
+ assertEquals(expected, getTokens(sql));
+ }
+
+ private void assertInvalidSql(String sql, String message) {
+ try {
+ getTokens(sql);
+ fail("Didn't throw InvalidSqlException");
+ } catch (InvalidSqlException e) {
+ MoreAsserts.assertContainsRegex(message, e.getMessage());
+ }
+ }
+
+ public void testWhitespaces() {
+ checkTokens(" select \t\r\n a\n\n ", "select a");
+ checkTokens("a b", "a b");
+ }
+
+ public void testComment() {
+ checkTokens("--\n", null);
+ checkTokens("a--\n", "a");
+ checkTokens("a--abcdef\n", "a");
+ checkTokens("a--abcdef\nx", "a x");
+ checkTokens("a--\nx", "a x");
+ assertInvalidSql("a--abcdef", "Unterminated comment");
+ assertInvalidSql("a--abcdef\ndef--", "Unterminated comment");
+
+ checkTokens("/**/", null);
+ assertInvalidSql("/*", "Unterminated comment");
+ assertInvalidSql("/*/", "Unterminated comment");
+ assertInvalidSql("/*\n* /*a", "Unterminated comment");
+ checkTokens("a/**/", "a");
+ checkTokens("/**/b", "b");
+ checkTokens("a/**/b", "a b");
+ checkTokens("a/* -- \n* /* **/b", "a b");
+ }
+
+ public void testStrings() {
+ assertInvalidSql("'", "Unterminated quote");
+ assertInvalidSql("a'", "Unterminated quote");
+ assertInvalidSql("a'''", "Unterminated quote");
+ assertInvalidSql("a''' ", "Unterminated quote");
+ checkTokens("''", null);
+ checkTokens("''''", null);
+ checkTokens("a''''b", "a b");
+ checkTokens("a' '' 'b", "a b");
+ checkTokens("'abc'", null);
+ checkTokens("'abc\ndef'", null);
+ checkTokens("a'abc\ndef'", "a");
+ checkTokens("'abc\ndef'b", "b");
+ checkTokens("a'abc\ndef'b", "a b");
+ checkTokens("a'''abc\nd''ef'''b", "a b");
+ }
+
+ public void testDoubleQuotes() {
+ assertInvalidSql("\"", "Unterminated quote");
+ assertInvalidSql("a\"", "Unterminated quote");
+ assertInvalidSql("a\"\"\"", "Unterminated quote");
+ assertInvalidSql("a\"\"\" ", "Unterminated quote");
+ checkTokens("\"\"", "");
+ checkTokens("\"\"\"\"", "\"");
+ checkTokens("a\"\"\"\"b", "a \" b");
+ checkTokens("a\"\t\"\"\t\"b", "a \t\"\t b");
+ checkTokens("\"abc\"", "abc");
+ checkTokens("\"abc\ndef\"", "abc\ndef");
+ checkTokens("a\"abc\ndef\"", "a abc\ndef");
+ checkTokens("\"abc\ndef\"b", "abc\ndef b");
+ checkTokens("a\"abc\ndef\"b", "a abc\ndef b");
+ checkTokens("a\"\"\"abc\nd\"\"ef\"\"\"b", "a \"abc\nd\"ef\" b");
+ }
+
+ public void testBackQuotes() {
+ assertInvalidSql("`", "Unterminated quote");
+ assertInvalidSql("a`", "Unterminated quote");
+ assertInvalidSql("a```", "Unterminated quote");
+ assertInvalidSql("a``` ", "Unterminated quote");
+ checkTokens("``", "");
+ checkTokens("````", "`");
+ checkTokens("a````b", "a ` b");
+ checkTokens("a`\t``\t`b", "a \t`\t b");
+ checkTokens("`abc`", "abc");
+ checkTokens("`abc\ndef`", "abc\ndef");
+ checkTokens("a`abc\ndef`", "a abc\ndef");
+ checkTokens("`abc\ndef`b", "abc\ndef b");
+ checkTokens("a`abc\ndef`b", "a abc\ndef b");
+ checkTokens("a```abc\nd``ef```b", "a `abc\nd`ef` b");
+ }
+
+ public void testBrackets() {
+ assertInvalidSql("[", "Unterminated quote");
+ assertInvalidSql("a[", "Unterminated quote");
+ assertInvalidSql("a[ ", "Unterminated quote");
+ assertInvalidSql("a[[ ", "Unterminated quote");
+ checkTokens("[]", "");
+ checkTokens("[[]", "[");
+ checkTokens("a[[]b", "a [ b");
+ checkTokens("a[\t[\t]b", "a \t[\t b");
+ checkTokens("[abc]", "abc");
+ checkTokens("[abc\ndef]", "abc\ndef");
+ checkTokens("a[abc\ndef]", "a abc\ndef");
+ checkTokens("[abc\ndef]b", "abc\ndef b");
+ checkTokens("a[abc\ndef]b", "a abc\ndef b");
+ checkTokens("a[[abc\nd[ef[]b", "a [abc\nd[ef[ b");
+ }
+
+ public void testSemicolons() {
+ assertInvalidSql(";", "Semicolon is not allowed");
+ assertInvalidSql(" ;", "Semicolon is not allowed");
+ assertInvalidSql("; ", "Semicolon is not allowed");
+ assertInvalidSql("-;-", "Semicolon is not allowed");
+ checkTokens("--;\n", null);
+ checkTokens("/*;*/", null);
+ checkTokens("';'", null);
+ checkTokens("[;]", ";");
+ checkTokens("`;`", ";");
+ }
+
+ public void testTokens() {
+ checkTokens("a,abc,a00b,_1,_123,abcdef", "a abc a00b _1 _123 abcdef");
+ checkTokens("a--\nabc/**/a00b''_1'''ABC'''`_123`abc[d]\"e\"f",
+ "a abc a00b _1 _123 abc d e f");
+ }
+
+ private SqlChecker getChecker(String... tokens) {
+ return new SqlChecker(Arrays.asList(tokens));
+ }
+
+ private void checkEnsureNoInvalidTokens(boolean ok, String sql, String... tokens) {
+ if (ok) {
+ getChecker(tokens).ensureNoInvalidTokens(sql);
+ } else {
+ try {
+ getChecker(tokens).ensureNoInvalidTokens(sql);
+ fail("Should have thrown");
+ } catch (InvalidSqlException e) {
+ // okay
+ }
+ }
+ }
+
+ public void testEnsureNoInvalidTokens() {
+ checkEnsureNoInvalidTokens(true, "a b c", "Select");
+
+ checkEnsureNoInvalidTokens(false, "a b ;c", "Select");
+ checkEnsureNoInvalidTokens(false, "a b seLeCt", "Select");
+
+ checkEnsureNoInvalidTokens(true, "a b select", "x");
+
+ checkEnsureNoInvalidTokens(false, "A b select", "x", "a");
+ checkEnsureNoInvalidTokens(false, "A b select", "a", "x");
+
+ checkEnsureNoInvalidTokens(true, "a /*select*/ b c ", "select");
+ checkEnsureNoInvalidTokens(true, "a 'select' b c ", "select");
+
+ checkEnsureNoInvalidTokens(true, "a b ';' c");
+ checkEnsureNoInvalidTokens(true, "a b /*;*/ c");
+
+ checkEnsureNoInvalidTokens(false, "a b x_ c");
+ checkEnsureNoInvalidTokens(false, "a b [X_OK] c");
+ checkEnsureNoInvalidTokens(true, "a b 'x_' c");
+ checkEnsureNoInvalidTokens(true, "a b /*x_*/ c");
+ }
+
+ private void checkEnsureSingleTokenOnly(boolean ok, String sql, String... tokens) {
+ if (ok) {
+ getChecker(tokens).ensureSingleTokenOnly(sql);
+ } else {
+ try {
+ getChecker(tokens).ensureSingleTokenOnly(sql);
+ fail("Should have thrown");
+ } catch (InvalidSqlException e) {
+ // okay
+ }
+ }
+ }
+
+ public void testEnsureSingleTokenOnly() {
+ checkEnsureSingleTokenOnly(true, "a", "select");
+ checkEnsureSingleTokenOnly(true, "ab", "select");
+ checkEnsureSingleTokenOnly(true, "selec", "select");
+ checkEnsureSingleTokenOnly(true, "selectx", "select");
+
+ checkEnsureSingleTokenOnly(false, "select", "select");
+ checkEnsureSingleTokenOnly(false, "select", "a", "select");
+ checkEnsureSingleTokenOnly(false, "select", "select", "b");
+ checkEnsureSingleTokenOnly(false, "select", "a", "select", "b");
+
+
+ checkEnsureSingleTokenOnly(true, "`a`", "select");
+ checkEnsureSingleTokenOnly(true, "[a]", "select");
+ checkEnsureSingleTokenOnly(true, "\"a\"", "select");
+
+ checkEnsureSingleTokenOnly(false, "'a'", "select");
+
+ checkEnsureSingleTokenOnly(false, "b`a`", "select");
+ checkEnsureSingleTokenOnly(false, "b[a]", "select");
+ checkEnsureSingleTokenOnly(false, "b\"a\"", "select");
+ checkEnsureSingleTokenOnly(false, "b'a'", "select");
+
+ checkEnsureSingleTokenOnly(false, "`a`c", "select");
+ checkEnsureSingleTokenOnly(false, "[a]c", "select");
+ checkEnsureSingleTokenOnly(false, "\"a\"c", "select");
+ checkEnsureSingleTokenOnly(false, "'a'c", "select");
+
+ checkEnsureSingleTokenOnly(false, "", "select");
+ checkEnsureSingleTokenOnly(false, "--", "select");
+ checkEnsureSingleTokenOnly(false, "/**/", "select");
+ checkEnsureSingleTokenOnly(false, " \n", "select");
+ checkEnsureSingleTokenOnly(false, "a--", "select");
+ checkEnsureSingleTokenOnly(false, "a/**/", "select");
+ checkEnsureSingleTokenOnly(false, "a \n", "select");
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java b/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
index e09e59ea..1bfcb17a 100644
--- a/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
+++ b/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
@@ -94,6 +94,16 @@ public class DBQueryUtilsTest extends TestCase {
assertEquals("\\%test\\%", sb.toString());
}
+ public void testEscapeLikeValuesEscapesEscapes() {
+ StringBuilder sb = new StringBuilder();
+ escapeLikeValue(sb, "my\\test\\string", '\\');
+ assertEquals("my\\\\test\\\\string", sb.toString());
+
+ sb = new StringBuilder();
+ escapeLikeValue(sb, "\\test\\", '\\');
+ assertEquals("\\\\test\\\\", sb.toString());
+ }
+
public void testEscapeLikeValuesNoChanges() {
StringBuilder sb = new StringBuilder();
escapeLikeValue(sb, "my test string", '\\');
diff --git a/tests/src/com/android/providers/contacts/util/MockClock.java b/tests/src/com/android/providers/contacts/util/MockClock.java
index dce06c9f..03e82657 100644
--- a/tests/src/com/android/providers/contacts/util/MockClock.java
+++ b/tests/src/com/android/providers/contacts/util/MockClock.java
@@ -42,4 +42,8 @@ public class MockClock extends Clock {
public void advance() {
mCurrentTimeMillis++;
}
+
+ public void advanceDay() {
+ mCurrentTimeMillis += 24 * 60 * 60;
+ }
}
diff --git a/tests2/Android.mk b/tests2/Android.mk
new file mode 100644
index 00000000..bb4443f3
--- /dev/null
+++ b/tests2/Android.mk
@@ -0,0 +1,40 @@
+#
+# Copyright (C) 2016 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ ContactsProviderTestUtils \
+ android-support-test \
+ mockito-target-minus-junit4 \
+ legacy-android-test
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := ContactsProviderTests2
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+LOCAL_INSTRUMENTATION_FOR := ContactsProvider
+LOCAL_CERTIFICATE := shared
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
diff --git a/tests2/AndroidManifest.xml b/tests2/AndroidManifest.xml
new file mode 100644
index 00000000..7678bd2f
--- /dev/null
+++ b/tests2/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<!--
+ Another unit tests against CP2 that runs in a separate process.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.contacts.tests2" >
+
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+ <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.providers.contacts.tests2" />
+</manifest>
diff --git a/tests2/AndroidTest.xml b/tests2/AndroidTest.xml
new file mode 100644
index 00000000..9f541e5d
--- /dev/null
+++ b/tests2/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<configuration description="Runs Contacts Provider Tests.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="ContactsProviderTests2.apk" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-tag" value="ContactsProviderTests2" />
+ <test class="com.android.tradefed.testtype.InstrumentationTest" >
+ <option name="package" value="com.android.providers.contacts.tests2" />
+ <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/tests2/src/com/android/providers/contacts/tests2/AllUriTest.java b/tests2/src/com/android/providers/contacts/tests2/AllUriTest.java
new file mode 100644
index 00000000..2aa6a619
--- /dev/null
+++ b/tests2/src/com/android/providers/contacts/tests2/AllUriTest.java
@@ -0,0 +1,718 @@
+/*
+ * Copyright (C) 2016 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.tests2;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.SyncState;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import junit.framework.AssertionFailedError;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+/*
+ * TODO The following operations would fail, not because they're not supported, but because of
+ * missing parameters. Fix them.
+insert for 'content://com.android.contacts/contacts' failed: Aggregate contacts are created automatically
+insert for 'content://com.android.contacts/raw_contacts/1/data' failed: mimetype is required
+update for 'content://com.android.contacts/raw_contacts/1/stream_items/1' failed: Empty values
+insert for 'content://com.android.contacts/data' failed: raw_contact_id is required
+insert for 'content://com.android.contacts/settings' failed: Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE; URI: content://com.android.contacts/settings?account_type=1, calling user: com.android.providers.contacts.tests2, calling package:com.android.providers.contacts.tests2
+insert for 'content://com.android.contacts/status_updates' failed: PROTOCOL and IM_HANDLE are required
+insert for 'content://com.android.contacts/profile' failed: The profile contact is created automatically
+insert for 'content://com.android.contacts/profile/data' failed: raw_contact_id is required
+insert for 'content://com.android.contacts/profile/raw_contacts/1/data' failed: mimetype is required
+insert for 'content://com.android.contacts/profile/status_updates' failed: PROTOCOL and IM_HANDLE are required
+
+
+openInputStream for 'content://com.android.contacts/contacts/as_multi_vcard/XXX' failed: Caught Exception: Invalid lookup id: XXX
+openInputStream for 'content://com.android.contacts/directory_file_enterprise/XXX?directory=0' failed: Caught Exception: java.lang.IllegalArgumentException: Directory is not a remote directory: content://com.android.contacts/directory_file_enterprise/XXX?directory=0
+openOutputStream for 'content://com.android.contacts/directory_file_enterprise/XXX?directory=0' failed: Caught Exception: java.lang.IllegalArgumentException: Directory is not a remote directory: content://com.android.contacts/directory_file_enterprise/XXX?directory=0
+*/
+
+/**
+ * TODO Add test for delete/update/insert too.
+ * TODO Copy it to CTS
+ */
+@LargeTest
+public class AllUriTest extends AndroidTestCase {
+ private static final String TAG = "AllUrlTest";
+
+ // "-" : Query not supported.
+ // "!" : Can't query because it requires the cross-user permission.
+ // The following markers are planned, but not implemented and the definition below is not all
+ // correct yet.
+ // "d" : supports delete.
+ // "u" : supports update.
+ // "i" : supports insert.
+ // "r" : supports read.
+ // "w" : supports write.
+ // "s" : has x_times_contacted and x_last_time_contacted.
+ // "t" : has x_times_used and x_last_time_used.
+ private static final String[][] URIs = {
+ {"content://com.android.contacts/contacts", "sud"},
+ {"content://com.android.contacts/contacts/1", "sud"},
+ {"content://com.android.contacts/contacts/1/data", "t"},
+ {"content://com.android.contacts/contacts/1/entities", "t"},
+ {"content://com.android.contacts/contacts/1/suggestions"},
+ {"content://com.android.contacts/contacts/1/suggestions/XXX"},
+ {"content://com.android.contacts/contacts/1/photo", "r"},
+ {"content://com.android.contacts/contacts/1/display_photo", "-r"},
+ {"content://com.android.contacts/contacts_corp/1/photo", "-r"},
+ {"content://com.android.contacts/contacts_corp/1/display_photo", "-r"},
+
+ {"content://com.android.contacts/contacts/filter", "s"},
+ {"content://com.android.contacts/contacts/filter/XXX", "s"},
+
+ {"content://com.android.contacts/contacts/lookup/nlookup", "sud"},
+ {"content://com.android.contacts/contacts/lookup/nlookup/data", "t"},
+ {"content://com.android.contacts/contacts/lookup/nlookup/photo", "tr"},
+
+ {"content://com.android.contacts/contacts/lookup/nlookup/1", "sud"},
+ {"content://com.android.contacts/contacts/lookup/nlookup/1/data"},
+ {"content://com.android.contacts/contacts/lookup/nlookup/1/photo", "r"},
+ {"content://com.android.contacts/contacts/lookup/nlookup/display_photo", "-r"},
+ {"content://com.android.contacts/contacts/lookup/nlookup/1/display_photo", "-r"},
+ {"content://com.android.contacts/contacts/lookup/nlookup/entities"},
+ {"content://com.android.contacts/contacts/lookup/nlookup/1/entities"},
+
+ {"content://com.android.contacts/contacts/as_vcard/nlookup", "r"},
+ {"content://com.android.contacts/contacts/as_multi_vcard/XXX"},
+
+ {"content://com.android.contacts/contacts/strequent/", "s"},
+ {"content://com.android.contacts/contacts/strequent/filter/XXX", "s"},
+
+ {"content://com.android.contacts/contacts/group/XXX"},
+
+ {"content://com.android.contacts/contacts/frequent", "s"},
+ {"content://com.android.contacts/contacts/delete_usage", "-d"},
+ {"content://com.android.contacts/contacts/filter_enterprise?directory=0", "s"},
+ {"content://com.android.contacts/contacts/filter_enterprise/XXX?directory=0", "s"},
+
+ {"content://com.android.contacts/raw_contacts", "siud"},
+ {"content://com.android.contacts/raw_contacts/1", "sud"},
+ {"content://com.android.contacts/raw_contacts/1/data", "tu"},
+ {"content://com.android.contacts/raw_contacts/1/display_photo", "-rw"},
+ {"content://com.android.contacts/raw_contacts/1/entity"},
+
+ {"content://com.android.contacts/raw_contact_entities"},
+ {"content://com.android.contacts/raw_contact_entities_corp", "!"},
+
+ {"content://com.android.contacts/data", "tud"},
+ {"content://com.android.contacts/data/1", "tudr"},
+ {"content://com.android.contacts/data/phones", "t"},
+ {"content://com.android.contacts/data_enterprise/phones", "!"},
+ {"content://com.android.contacts/data/phones/1", "tud"},
+ {"content://com.android.contacts/data/phones/filter", "t"},
+ {"content://com.android.contacts/data/phones/filter/XXX", "t"},
+
+ {"content://com.android.contacts/data/phones/filter_enterprise?directory=0", "t"},
+ {"content://com.android.contacts/data/phones/filter_enterprise/XXX?directory=0", "t"},
+
+ {"content://com.android.contacts/data/emails", "t"},
+ {"content://com.android.contacts/data/emails/1", "tud"},
+ {"content://com.android.contacts/data/emails/lookup", "t"},
+ {"content://com.android.contacts/data/emails/lookup/XXX", "t"},
+ {"content://com.android.contacts/data/emails/filter", "t"},
+ {"content://com.android.contacts/data/emails/filter/XXX", "t"},
+ {"content://com.android.contacts/data/emails/filter_enterprise?directory=0", "t"},
+ {"content://com.android.contacts/data/emails/filter_enterprise/XXX?directory=0", "t"},
+ {"content://com.android.contacts/data/emails/lookup_enterprise", "t"},
+ {"content://com.android.contacts/data/emails/lookup_enterprise/XXX", "t"},
+ {"content://com.android.contacts/data/postals", "t"},
+ {"content://com.android.contacts/data/postals/1", "tud"},
+ {"content://com.android.contacts/data/usagefeedback/1,2,3", "-u"},
+ {"content://com.android.contacts/data/callables/", "t"},
+ {"content://com.android.contacts/data/callables/1", "tud"},
+ {"content://com.android.contacts/data/callables/filter", "t"},
+ {"content://com.android.contacts/data/callables/filter/XXX", "t"},
+ {"content://com.android.contacts/data/callables/filter_enterprise?directory=0", "t"},
+ {"content://com.android.contacts/data/callables/filter_enterprise/XXX?directory=0",
+ "t"},
+ {"content://com.android.contacts/data/contactables/", "t"},
+ {"content://com.android.contacts/data/contactables/filter", "t"},
+ {"content://com.android.contacts/data/contactables/filter/XXX", "t"},
+
+ {"content://com.android.contacts/groups", "iud"},
+ {"content://com.android.contacts/groups/1", "ud"},
+ {"content://com.android.contacts/groups_summary"},
+ {"content://com.android.contacts/syncstate", "iud"},
+ {"content://com.android.contacts/syncstate/1", "-ud"},
+ {"content://com.android.contacts/profile/syncstate", "iud"},
+ {"content://com.android.contacts/phone_lookup/XXX"},
+ {"content://com.android.contacts/phone_lookup_enterprise/XXX"},
+ {"content://com.android.contacts/aggregation_exceptions", "u"},
+ {"content://com.android.contacts/settings", "ud"},
+ {"content://com.android.contacts/status_updates", "ud"},
+ {"content://com.android.contacts/status_updates/1"},
+ {"content://com.android.contacts/search_suggest_query"},
+ {"content://com.android.contacts/search_suggest_query/XXX"},
+ {"content://com.android.contacts/search_suggest_shortcut/XXX"},
+ {"content://com.android.contacts/provider_status"},
+ {"content://com.android.contacts/directories", "u"},
+ {"content://com.android.contacts/directories/1"},
+ {"content://com.android.contacts/directories_enterprise"},
+ {"content://com.android.contacts/directories_enterprise/1"},
+ {"content://com.android.contacts/complete_name"},
+ {"content://com.android.contacts/profile", "su"},
+ {"content://com.android.contacts/profile/entities", "s"},
+ {"content://com.android.contacts/profile/data", "tud"},
+ {"content://com.android.contacts/profile/data/1", "td"},
+ {"content://com.android.contacts/profile/photo", "t"},
+ {"content://com.android.contacts/profile/display_photo", "-r"},
+ {"content://com.android.contacts/profile/as_vcard", "r"},
+ {"content://com.android.contacts/profile/raw_contacts", "siud"},
+
+ // Note this should have supported update... Too late to add.
+ {"content://com.android.contacts/profile/raw_contacts/1", "sd"},
+ {"content://com.android.contacts/profile/raw_contacts/1/data", "tu"},
+ {"content://com.android.contacts/profile/raw_contacts/1/entity"},
+ {"content://com.android.contacts/profile/status_updates", "ud"},
+ {"content://com.android.contacts/profile/raw_contact_entities"},
+ {"content://com.android.contacts/display_photo/1", "-r"},
+ {"content://com.android.contacts/photo_dimensions"},
+ {"content://com.android.contacts/deleted_contacts"},
+ {"content://com.android.contacts/deleted_contacts/1"},
+ {"content://com.android.contacts/directory_file_enterprise/XXX?directory=0", "-"},
+ };
+
+ private static final String[] ARG1 = {"-1"};
+
+ private ContentResolver mResolver;
+
+ private ArrayList<String> mFailures;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mFailures = new ArrayList<>();
+ mResolver = getContext().getContentResolver();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (mFailures != null) {
+ fail("mFailures is not null. Did you forget to call failIfFailed()?");
+ }
+
+ super.tearDown();
+ }
+
+ private void addFailure(String message, Throwable th) {
+ Log.e(TAG, "Failed: " + message, th);
+
+ final int MAX = 100;
+ if (mFailures.size() == MAX) {
+ mFailures.add("Too many failures.");
+ } else if (mFailures.size() > MAX) {
+ // Too many failures already...
+ } else {
+ mFailures.add(message);
+ }
+ }
+
+ private void failIfFailed() {
+ if (mFailures == null) {
+ fail("mFailures is null. Maybe called failIfFailed() twice?");
+ }
+ if (mFailures.size() > 0) {
+ StringBuilder message = new StringBuilder();
+
+ if (mFailures.size() > 0) {
+ Log.e(TAG, "Something went wrong:");
+ for (String s : mFailures) {
+ Log.e(TAG, s);
+ if (message.length() > 0) {
+ message.append("\n");
+ }
+ message.append(s);
+ }
+ }
+ mFailures = null;
+ fail("Following test(s) failed:\n" + message);
+ }
+ mFailures = null;
+ }
+
+ private static Uri getUri(String[] path) {
+ return Uri.parse(path[0]);
+ }
+
+ private static boolean supportsQuery(String[] path) {
+ if (path.length == 1) {
+ return true; // supports query by default.
+ }
+ return !(path[1].contains("-") || path[1].contains("!"));
+ }
+
+ private static boolean supportsInsert(String[] path) {
+ return (path.length) >= 2 && path[1].contains("i");
+ }
+
+ private static boolean supportsUpdate(String[] path) {
+ return (path.length) >= 2 && path[1].contains("u");
+ }
+
+ private static boolean supportsDelete(String[] path) {
+ return (path.length) >= 2 && path[1].contains("d");
+ }
+
+ private static boolean supportsRead(String[] path) {
+ return (path.length) >= 2 && path[1].contains("r");
+ }
+
+ private static boolean supportsWrite(String[] path) {
+ return (path.length) >= 2 && path[1].contains("w");
+ }
+
+ private String[] getColumns(Uri uri) {
+ try (Cursor c = mResolver.query(uri,
+ null, // projection
+ "1=2", // selection
+ null, // selection args
+ null // sort order
+ )) {
+ return c.getColumnNames();
+ }
+ }
+
+ private void checkQueryExecutable(Uri uri,
+ String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ try {
+ try (Cursor c = mResolver.query(uri, projection, selection,
+ selectionArgs, sortOrder)) {
+ c.moveToFirst();
+ }
+ } catch (Throwable th) {
+ addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th);
+ }
+ try {
+ // With CancellationSignal.
+ try (Cursor c = mResolver.query(uri, projection, selection,
+ selectionArgs, sortOrder, new CancellationSignal())) {
+ c.moveToFirst();
+ }
+ } catch (Throwable th) {
+ addFailure("Query with cancel failed: URI=" + uri + " Message=" + th.getMessage(), th);
+ }
+ try {
+ // With limit.
+ try (Cursor c = mResolver.query(
+ uri.buildUpon().appendQueryParameter(
+ ContactsContract.LIMIT_PARAM_KEY, "0").build(),
+ projection, selection, selectionArgs, sortOrder)) {
+ c.moveToFirst();
+ }
+ } catch (Throwable th) {
+ addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage(), th);
+ }
+
+ try {
+ // With account.
+ try (Cursor c = mResolver.query(
+ uri.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, "a")
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "b")
+ .appendQueryParameter(RawContacts.DATA_SET, "c")
+ .build(),
+ projection, selection, selectionArgs, sortOrder)) {
+ c.moveToFirst();
+ }
+ } catch (Throwable th) {
+ addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage(), th);
+ }
+ }
+
+ private void checkQueryNotExecutable(Uri uri,
+ String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ try {
+ try (Cursor c = mResolver.query(uri, projection, selection,
+ selectionArgs, sortOrder)) {
+ c.moveToFirst();
+ }
+ } catch (Throwable th) {
+ // pass.
+ return;
+ }
+ addFailure("Query on " + uri + " expected to fail, but succeeded.", null);
+ }
+
+ /**
+ * Make sure all URLs are accessible with all arguments = null.
+ */
+ public void testSelect() {
+ for (String[] path : URIs) {
+ if (!supportsQuery(path)) continue;
+ final Uri uri = getUri(path);
+
+ checkQueryExecutable(uri, // uri
+ null, // projection
+ null, // selection
+ null, // selection args
+ null // sort order
+ );
+ }
+ failIfFailed();
+ }
+
+ public void testNoHiddenColumns() {
+ for (String[] path : URIs) {
+ if (!supportsQuery(path)) continue;
+ final Uri uri = getUri(path);
+
+ for (String column : getColumns(uri)) {
+ if (column.toLowerCase().startsWith(ContactsContract.HIDDEN_COLUMN_PREFIX)) {
+ addFailure("Uri " + uri + " returned hidden column " + column, null);
+ }
+ }
+ }
+ failIfFailed();
+ }
+
+// Temporarily disabled due to taking too much time.
+// /**
+// * Make sure all URLs are accessible with a projection.
+// */
+// public void testSelectWithProjection() {
+// for (String[] path : URIs) {
+// if (!supportsQuery(path)) continue;
+// final Uri uri = getUri(path);
+//
+// for (String column : getColumns(uri)) {
+// // Some columns are not selectable alone due to bugs, and we don't want to fix them
+// // in order to avoid expanding the differences between versions, so here're some
+// // hacks to make it work...
+//
+// String[] projection = {column};
+//
+// final String u = path[0];
+// if ((u.startsWith("content://com.android.contacts/status_updates")
+// || u.startsWith("content://com.android.contacts/profile/status_updates"))
+// && ("im_handle".equals(column)
+// || "im_account".equals(column)
+// || "protocol".equals(column)
+// || "custom_protocol".equals(column)
+// || "presence_raw_contact_id".equals(column)
+// )) {
+// // These columns only show up when the projection contains certain columns.
+//
+// projection = new String[]{"mode", column};
+// } else if ((u.startsWith("content://com.android.contacts/search_suggest_query")
+// || u.startsWith("content://contacts/search_suggest_query"))
+// && "suggest_intent_action".equals(column)) {
+// // Can't be included in the projection due to a bug in GlobalSearchSupport.
+// continue;
+// } else if (RawContacts.BACKUP_ID.equals(column)) {
+// // Some URIs don't support a projection with BAKCUP_ID only.
+// projection = new String[]{RawContacts.BACKUP_ID, RawContacts.SOURCE_ID};
+// }
+//
+// checkQueryExecutable(uri,
+// projection, // projection
+// null, // selection
+// null, // selection args
+// null // sort order
+// );
+// }
+// }
+// failIfFailed();
+// }
+
+ /**
+ * Make sure all URLs are accessible with a selection.
+ */
+ public void testSelectWithSelection() {
+ for (String[] path : URIs) {
+ if (!supportsQuery(path)) continue;
+ final Uri uri = getUri(path);
+
+ checkQueryExecutable(uri,
+ null, // projection
+ "1=?", // selection
+ ARG1, // , // selection args
+ null // sort order
+ );
+ }
+ failIfFailed();
+ }
+
+// /**
+// * Make sure all URLs are accessible with a selection.
+// */
+// public void testSelectWithSelectionUsingColumns() {
+// for (String[] path : URIs) {
+// if (!supportsQuery(path)) continue;
+// final Uri uri = getUri(path);
+//
+// for (String column : getColumns(uri)) {
+// checkQueryExecutable(uri,
+// null, // projection
+// column + "=?", // selection
+// ARG1, // , // selection args
+// null // sort order
+// );
+// }
+// }
+// failIfFailed();
+// }
+
+// Temporarily disabled due to taking too much time.
+// /**
+// * Make sure all URLs are accessible with an order-by.
+// */
+// public void testSelectWithSortOrder() {
+// for (String[] path : URIs) {
+// if (!supportsQuery(path)) continue;
+// final Uri uri = getUri(path);
+//
+// for (String column : getColumns(uri)) {
+// checkQueryExecutable(uri,
+// null, // projection
+// "1=2", // selection
+// null, // , // selection args
+// column // sort order
+// );
+// }
+// }
+// failIfFailed();
+// }
+
+ /**
+ * Make sure all URLs are accessible with all arguments.
+ */
+ public void testSelectWithAllArgs() {
+ for (String[] path : URIs) {
+ if (!supportsQuery(path)) continue;
+ final Uri uri = getUri(path);
+
+ final String[] projection = {getColumns(uri)[0]};
+
+ checkQueryExecutable(uri,
+ projection, // projection
+ "1=?", // selection
+ ARG1, // , // selection args
+ getColumns(uri)[0] // sort order
+ );
+ }
+ failIfFailed();
+ }
+
+ public void testNonSelect() {
+ for (String[] path : URIs) {
+ if (supportsQuery(path)) continue;
+ final Uri uri = getUri(path);
+
+ checkQueryNotExecutable(uri, // uri
+ null, // projection
+ null, // selection
+ null, // selection args
+ null // sort order
+ );
+ }
+ failIfFailed();
+ }
+
+ private static boolean supportsTimesContacted(String[] path) {
+ return path.length > 1 && path[1].contains("s");
+ }
+
+ private static boolean supportsTimesUsed(String[] path) {
+ return path.length > 1 && path[1].contains("t");
+ }
+
+ private void checkColumnAccessible(Uri uri, String column) {
+ try {
+ try (Cursor c = mResolver.query(
+ uri, new String[]{column}, column + "=0", null, column
+ )) {
+ c.moveToFirst();
+ }
+ } catch (Throwable th) {
+ addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th);
+ }
+ }
+
+ /** Test for {@link #checkColumnAccessible} */
+ public void testCheckColumnAccessible() {
+ checkColumnAccessible(Contacts.CONTENT_URI, "x_times_contacted");
+ try {
+ failIfFailed();
+ } catch (AssertionFailedError expected) {
+ return; // expected.
+ }
+ fail("Failed to detect issue.");
+ }
+
+ private void checkColumnNotAccessibleInner(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ try {
+ try (Cursor c = mResolver.query(uri, projection, selection,
+ selectionArgs, sortOrder)) {
+ c.moveToFirst();
+ }
+ } catch (IllegalArgumentException th) {
+ // pass.
+ return;
+ }
+ addFailure("Query on " + uri +
+ " expected to throw IllegalArgumentException, but succeeded.", null);
+ }
+
+ private void checkColumnNotAccessible(Uri uri, String column) {
+ checkColumnNotAccessibleInner(uri, new String[] {column}, null, null, null);
+ checkColumnNotAccessibleInner(uri, null, column + "=1", null, null);
+ checkColumnNotAccessibleInner(uri, null, null, null, /* order by */ column);
+ }
+
+ /** Test for {@link #checkColumnNotAccessible} */
+ public void testCheckColumnNotAccessible() {
+ checkColumnNotAccessible(Contacts.CONTENT_URI, "times_contacted");
+ try {
+ failIfFailed();
+ } catch (AssertionFailedError expected) {
+ return; // expected.
+ }
+ fail("Failed to detect issue.");
+ }
+
+ /**
+ * Make sure the x_ columns are not accessible.
+ */
+ public void testProhibitedColumns() {
+ for (String[] path : URIs) {
+ final Uri uri = getUri(path);
+ if (supportsTimesContacted(path)) {
+ checkColumnAccessible(uri, "times_contacted");
+ checkColumnAccessible(uri, "last_time_contacted");
+
+ checkColumnNotAccessible(uri, "X_times_contacted");
+ checkColumnNotAccessible(uri, "X_slast_time_contacted");
+ }
+ if (supportsTimesUsed(path)) {
+ checkColumnAccessible(uri, "times_used");
+ checkColumnAccessible(uri, "last_time_used");
+
+ checkColumnNotAccessible(uri, "X_times_used");
+ checkColumnNotAccessible(uri, "X_last_time_used");
+ }
+ }
+ failIfFailed();
+ }
+
+ private void checkExecutable(String operation, Uri uri, boolean shouldWork, Runnable r) {
+ if (shouldWork) {
+ try {
+ r.run();
+ } catch (Exception e) {
+ addFailure(operation + " for '" + uri + "' failed: " + e.getMessage(), e);
+ }
+ } else {
+ try {
+ r.run();
+ addFailure(operation + " for '" + uri + "' NOT failed.", null);
+ } catch (Exception expected) {
+ }
+ }
+ }
+
+ public void testAllOperations() {
+ final ContentValues cv = new ContentValues();
+
+ for (String[] path : URIs) {
+ final Uri uri = getUri(path);
+
+ cv.clear();
+ if (supportsQuery(path)) {
+ cv.put(getColumns(uri)[0], 1);
+ } else {
+ cv.put("_id", 1);
+ }
+ if (uri.toString().contains("syncstate")) {
+ cv.put(SyncState.ACCOUNT_NAME, "abc");
+ cv.put(SyncState.ACCOUNT_TYPE, "def");
+ }
+
+ checkExecutable("insert", uri, supportsInsert(path), () -> {
+ final Uri newUri = mResolver.insert(uri, cv);
+ if (newUri == null) {
+ addFailure("Insert for '" + uri + "' returned null.", null);
+ } else {
+ // "profile/raw_contacts/#" is missing update support. too late to add, so
+ // just skip.
+ if (!newUri.toString().startsWith(
+ "content://com.android.contacts/profile/raw_contacts/")) {
+ checkExecutable("insert -> update", newUri, true, () -> {
+ mResolver.update(newUri, cv, null, null);
+ });
+ }
+ checkExecutable("insert -> delete", newUri, true, () -> {
+ mResolver.delete(newUri, null, null);
+ });
+ }
+ });
+ checkExecutable("update", uri, supportsUpdate(path), () -> {
+ mResolver.update(uri, cv, "1=2", null);
+ });
+ checkExecutable("delete", uri, supportsDelete(path), () -> {
+ mResolver.delete(uri, "1=2", null);
+ });
+ }
+ failIfFailed();
+ }
+
+ public void testAllFileOperations() {
+ for (String[] path : URIs) {
+ final Uri uri = getUri(path);
+
+ checkExecutable("openInputStream", uri, supportsRead(path), () -> {
+ try (InputStream st = mResolver.openInputStream(uri)) {
+ } catch (FileNotFoundException e) {
+ // TODO This happens because we try to read nonexistent photos. Ideally
+ // we should actually check it's readable.
+ if (e.getMessage().contains("Stream I/O not supported")) {
+ throw new RuntimeException("Caught Exception: " + e.toString(), e);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Caught Exception: " + e.toString(), e);
+ }
+ });
+ checkExecutable("openOutputStream", uri, supportsWrite(path), () -> {
+ try (OutputStream st = mResolver.openOutputStream(uri)) {
+ } catch (Exception e) {
+ throw new RuntimeException("Caught Exception: " + e.toString(), e);
+ }
+ });
+ }
+ failIfFailed();
+ }
+}
+
+
diff --git a/tools/contacts-db-schema.sh b/tools/contacts-db-schema.sh
index 3aa164bc..c5778441 100755
--- a/tools/contacts-db-schema.sh
+++ b/tools/contacts-db-schema.sh
@@ -23,7 +23,7 @@ db=/data/data/com.android.providers.contacts/databases/contacts2.db
# Otherwise sqlite3 would create an empty file owned by root.
# Sed inserts a newline after each ( and ,
-adb shell "(ls $db >/dev/null)&& sqlite3 $db \"select name, sql from sqlite_master where type in('table','index') order by name\"" |
+adb shell "(ls $db >/dev/null)&& sqlite3 $db \"select name, sql from sqlite_master where type in('table','index', 'view') order by name\"" |
sed -e 's/\([(,]\)/\1\n /g'
echo "> sqlite_stat1"
adb shell "(ls $db >/dev/null)&& sqlite3 $db \"select * from sqlite_stat1 order by tbl, idx, stat\""