diff options
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\"" |