diff options
author | Steve Kondik <shade@chemlab.org> | 2013-02-12 22:43:54 -0800 |
---|---|---|
committer | Steve Kondik <shade@chemlab.org> | 2013-02-12 22:43:54 -0800 |
commit | e5adf1f13c58f2d14dfbd1a179576ee5b269602f (patch) | |
tree | a5649d6ccce8838f6e951e88927689a819d153d4 | |
parent | f0fdf304d6a6b1616873004af8b6d6094fa47fed (diff) | |
parent | dacd5de146b413de86d38b6f56a3fe0b2af4b155 (diff) | |
download | android_packages_providers_ContactsProvider-mr1.1-staging.tar.gz android_packages_providers_ContactsProvider-mr1.1-staging.tar.bz2 android_packages_providers_ContactsProvider-mr1.1-staging.zip |
Merge tag 'android-4.2.2_r1' of https://android.googlesource.com/platform/packages/providers/ContactsProvider into 1.1cm-10.1.2cm-10.1.1cm-10.1.0-RC5cm-10.1.0-RC4cm-10.1.0-RC3cm-10.1.0-RC2cm-10.1.0-RC1cm-10.1.0cm-10.1-M3cm-10.1-M2mr1.1-staging
Android 4.2.2 release 1
6 files changed, 218 insertions, 27 deletions
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java index eb446709..95ffb5c4 100644 --- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java +++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java @@ -107,7 +107,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { * 700-799 Jelly Bean * </pre> */ - static final int DATABASE_VERSION = 705; + static final int DATABASE_VERSION = 706; private static final String DATABASE_NAME = "contacts2.db"; private static final String DATABASE_PRESENCE = "presence_db"; @@ -1276,7 +1276,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { ");"); createDirectoriesTable(db); - createSearchIndexTable(db); + createSearchIndexTable(db, false /* we build stats table later */); db.execSQL("CREATE TABLE " + Tables.DATA_USAGE_STAT + "(" + DataUsageStatColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + @@ -1297,7 +1297,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { createContactsViews(db); createGroupsView(db); createContactsTriggers(db); - createContactsIndexes(db); + createContactsIndexes(db, false /* we build stats table later */); loadNicknameLookupTable(db); @@ -1344,7 +1344,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { setProperty(db, DbProperties.DIRECTORY_SCAN_COMPLETE, "0"); } - public void createSearchIndexTable(SQLiteDatabase db) { + public void createSearchIndexTable(SQLiteDatabase db, boolean rebuildSqliteStats) { db.execSQL("DROP TABLE IF EXISTS " + Tables.SEARCH_INDEX); db.execSQL("CREATE VIRTUAL TABLE " + Tables.SEARCH_INDEX + " USING FTS4 (" @@ -1353,6 +1353,9 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + SearchIndexColumns.NAME + " TEXT, " + SearchIndexColumns.TOKENS + " TEXT" + ")"); + if (rebuildSqliteStats) { + updateSqliteStats(db); + } } private void createContactsTriggers(SQLiteDatabase db) { @@ -1492,7 +1495,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { + " END"); } - private void createContactsIndexes(SQLiteDatabase db) { + private void createContactsIndexes(SQLiteDatabase db, boolean rebuildSqliteStats) { db.execSQL("DROP INDEX IF EXISTS name_lookup_index"); db.execSQL("CREATE INDEX name_lookup_index ON " + Tables.NAME_LOOKUP + " (" + NameLookupColumns.NORMALIZED_NAME + "," + @@ -1510,6 +1513,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" + RawContacts.SORT_KEY_ALTERNATIVE + ");"); + + if (rebuildSqliteStats) { + updateSqliteStats(db); + } } private void createContactsViews(SQLiteDatabase db) { @@ -1940,6 +1947,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { boolean upgradeLegacyApiSupport = false; boolean upgradeSearchIndex = false; boolean rescanDirectories = false; + boolean rebuildSqliteStats = false; if (oldVersion == 99) { upgradeViewsAndTriggers = true; @@ -2383,13 +2391,20 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { oldVersion = 705; } + if (oldVersion < 706) { + // Prior to this version, we didn't rebuild the stats table after drop operations, + // which resulted in losing some of the rows from the stats table. + rebuildSqliteStats = true; + oldVersion = 706; + } + if (upgradeViewsAndTriggers) { createContactsViews(db); createGroupsView(db); createContactsTriggers(db); - createContactsIndexes(db); - updateSqliteStats(db); + createContactsIndexes(db, false /* we build stats table later */); upgradeLegacyApiSupport = true; + rebuildSqliteStats = true; } if (upgradeLegacyApiSupport) { @@ -2397,12 +2412,14 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { } if (upgradeNameLookup) { - rebuildNameLookup(db); + rebuildNameLookup(db, false /* we build stats table later */); + rebuildSqliteStats = true; } if (upgradeSearchIndex) { - createSearchIndexTable(db); + createSearchIndexTable(db, false /* we build stats table later */); setProperty(db, SearchIndexManager.PROPERTY_SEARCH_INDEX_VERSION, "0"); + rebuildSqliteStats = true; } if (rescanDirectories) { @@ -2411,6 +2428,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { setProperty(db, DbProperties.DIRECTORY_SCAN_COMPLETE, "0"); } + if (rebuildSqliteStats) { + updateSqliteStats(db); + } + if (oldVersion != newVersion) { throw new IllegalStateException( "error upgrading the database to version " + newVersion); @@ -2970,10 +2991,10 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { "WHERE NOT EXISTS (SELECT 1 FROM raw_contacts WHERE contact_id=contacts._id)"); } - private void rebuildNameLookup(SQLiteDatabase db) { + private void rebuildNameLookup(SQLiteDatabase db, boolean rebuildSqliteStats) { db.execSQL("DROP INDEX IF EXISTS name_lookup_index"); insertNameLookup(db); - createContactsIndexes(db); + createContactsIndexes(db, rebuildSqliteStats); } /** @@ -2994,7 +3015,7 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { loadNicknameLookupTable(db); insertNameLookup(db); rebuildSortKeys(db, provider); - createContactsIndexes(db); + createContactsIndexes(db, true); db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -3844,16 +3865,51 @@ public class ContactsDatabaseHelper extends SQLiteOpenHelper { /** * 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. + * Make sure to call this method after such operations. */ private void updateSqliteStats(SQLiteDatabase db) { + if (!mDatabaseOptimizationEnabled) { + return; // We don't use sqlite_stat1 during tests. + } // Specific stats strings are based on an actual large database after running ANALYZE // Important here are relative sizes. Raw-Contacts is slightly bigger than Contacts // Warning: Missing tables in here will make SQLite assume to contain 1000000 rows, // which can lead to catastrophic query plans for small tables - // See the latest of version of http://www.sqlite.org/cgi/src/finfo?name=src/analyze.c - // for what these numbers mean. + // What these numbers mean is described in this file. + // http://www.sqlite.org/cgi/src/finfo?name=src/analyze.c + + // Excerpt: + /* + ** Format of sqlite_stat1: + ** + ** There is normally one row per index, with the index identified by the + ** name in the idx column. The tbl column is the name of the table to + ** which the index belongs. In each such row, the stat column will be + ** a string consisting of a list of integers. The first integer in this + ** list is the number of rows in the index and in the table. The second + ** integer is the average number of rows in the index that have the same + ** value in the first column of the index. The third integer is the average + ** number of rows in the index that have the same value for the first two + ** columns. The N-th integer (for N>1) is the average number of rows in + ** the index which have the same value for the first N-1 columns. For + ** a K-column index, there will be K+1 integers in the stat column. If + ** the index is unique, then the last integer will be 1. + ** + ** The list of integers in the stat column can optionally be followed + ** by the keyword "unordered". The "unordered" keyword, if it is present, + ** must be separated from the last integer by a single space. If the + ** "unordered" keyword is present, then the query planner assumes that + ** the index is unordered and will not use the index for a range query. + ** + ** If the sqlite_stat1.idx column is NULL, then the sqlite_stat1.stat + ** column contains a single integer which is the (estimated) number of + ** rows in the table identified by sqlite_stat1.tbl. + */ + try { db.execSQL("DELETE FROM sqlite_stat1"); updateIndexStats(db, Tables.CONTACTS, diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index 082418c1..74b66c37 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -513,12 +513,12 @@ public class ContactsProvider2 extends AbstractContactsProvider */ private static final String EMAIL_FILTER_SORT_ORDER = Contacts.STARRED + " DESC, " + + Data.IS_SUPER_PRIMARY + " DESC, " + + Data.IS_PRIMARY + " DESC, " + SORT_BY_DATA_USAGE + ", " + Contacts.IN_VISIBLE_GROUP + " DESC, " + Contacts.DISPLAY_NAME + ", " - + Data.CONTACT_ID + ", " - + Data.IS_SUPER_PRIMARY + " DESC, " - + Data.IS_PRIMARY + " DESC"; + + Data.CONTACT_ID; /** Currently same as {@link #EMAIL_FILTER_SORT_ORDER} */ private static final String PHONE_FILTER_SORT_ORDER = EMAIL_FILTER_SORT_ORDER; @@ -5650,6 +5650,26 @@ public class ContactsProvider2 extends AbstractContactsProvider } else { sortOrder = EMAIL_FILTER_SORT_ORDER; } + + final String primaryAccountName = + uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME); + if (!TextUtils.isEmpty(primaryAccountName)) { + final int index = primaryAccountName.indexOf('@'); + if (index != -1) { + // Purposely include '@' in matching. + final String domain = primaryAccountName.substring(index); + final char escapeChar = '\\'; + + final StringBuilder likeValue = new StringBuilder(); + likeValue.append('%'); + DbQueryUtils.escapeLikeValue(likeValue, domain, escapeChar); + selectionArgs = appendSelectionArg(selectionArgs, likeValue.toString()); + + // similar email domains is the last sort preference. + sortOrder += ", (CASE WHEN " + Data.DATA1 + " like ? ESCAPE '" + + escapeChar + "' THEN 0 ELSE 1 END)"; + } + } } break; } @@ -7862,6 +7882,18 @@ public class ContactsProvider2 extends AbstractContactsProvider } } + private String[] appendSelectionArg(String[] selectionArgs, String arg) { + if (selectionArgs == null) { + return new String[]{arg}; + } else { + int newLength = selectionArgs.length + 1; + String[] newSelectionArgs = new String[newLength]; + newSelectionArgs[newLength] = arg; + System.arraycopy(selectionArgs, 0, newSelectionArgs, 0, selectionArgs.length - 1); + return newSelectionArgs; + } + } + protected Account getDefaultAccount() { AccountManager accountManager = AccountManager.get(getContext()); try { diff --git a/src/com/android/providers/contacts/SearchIndexManager.java b/src/com/android/providers/contacts/SearchIndexManager.java index 20fd16b4..d45009e1 100644 --- a/src/com/android/providers/contacts/SearchIndexManager.java +++ b/src/com/android/providers/contacts/SearchIndexManager.java @@ -271,7 +271,7 @@ public class SearchIndexManager { final long start = SystemClock.elapsedRealtime(); int count = 0; try { - mDbHelper.createSearchIndexTable(db); + mDbHelper.createSearchIndexTable(db, true); count = buildAndInsertIndex(db, null); } finally { mContactsProvider.setProviderStatus(ProviderStatus.STATUS_NORMAL); diff --git a/src/com/android/providers/contacts/util/DbQueryUtils.java b/src/com/android/providers/contacts/util/DbQueryUtils.java index c853a961..c184613f 100644 --- a/src/com/android/providers/contacts/util/DbQueryUtils.java +++ b/src/com/android/providers/contacts/util/DbQueryUtils.java @@ -94,4 +94,31 @@ public class DbQueryUtils { } } } + + /** + * Escape values to be used in LIKE sqlite clause. + * + * The LIKE clause has two special characters: '%' and '_'. If either of these + * characters need to be matched literally, then they must be escaped like so: + * + * WHERE value LIKE 'android\_%' ESCAPE '\' + * + * The ESCAPE clause is required and no default exists as the escape character in this context. + * Since the escape character needs to be defined as part of the sql string, it must be + * provided to this method so the escape characters match. + * + * @param sb The StringBuilder to append the escaped value to. + * @param value The value to be escaped. + * @param escapeChar The escape character to be defined in the sql ESCAPE clause. + */ + 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 == '_') { + sb.append(escapeChar); + } + sb.append(ch); + } + } + } diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java index 77789c3e..1011ae20 100644 --- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java +++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java @@ -1180,7 +1180,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { values3.putNull(Phone.LABEL); final Uri filterUri6 = Uri.withAppendedPath(baseFilterUri, "Chilled"); - assertStoredValues(filterUri6, new ContentValues[] {values1, values2, values3} ); + assertStoredValues(filterUri6, new ContentValues[]{values1, values2, values3}); // Insert a SIP address. From here, Phone URI and Callable URI may return different results // than each other. @@ -1247,10 +1247,10 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { ); assertStoredValues( Phone.CONTENT_FILTER_URI.buildUpon().appendPath("dad") - .appendQueryParameter(Phone.SEARCH_DISPLAY_NAME_KEY, "0") - .appendQueryParameter(Phone.SEARCH_PHONE_NUMBER_KEY, "0") - .build() - ); + .appendQueryParameter(Phone.SEARCH_DISPLAY_NAME_KEY, "0") + .appendQueryParameter(Phone.SEARCH_PHONE_NUMBER_KEY, "0") + .build() + ); } public void testPhoneLookup() { @@ -1688,7 +1688,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { v3.put(Email.ADDRESS, "address3@email.com"); Uri filterUri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "address"); - assertStoredValuesOrderly(filterUri, new ContentValues[] { v1, v2, v3 }); + assertStoredValuesOrderly(filterUri, new ContentValues[]{v1, v2, v3}); } /** @@ -1737,7 +1737,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { Uri filterUri3 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("acc") .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, ACCOUNT_1.name) .build(); - assertStoredValuesOrderly(filterUri3, new ContentValues[] { v1, v2 }); + assertStoredValuesOrderly(filterUri3, new ContentValues[]{v1, v2}); Uri filterUri4 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("acc") .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, ACCOUNT_2.name) @@ -1745,6 +1745,48 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { assertStoredValuesOrderly(filterUri4, new ContentValues[] { v2, v1 }); } + /** + * Test emails with the same domain as primary account are ordered first. + */ + public void testEmailFilterSameDomainAccountOrder() { + final Account account = new Account("tester@email.com", "not_used"); + final long rawContactId = createRawContact(account); + insertEmail(rawContactId, "account1@testemail.com"); + insertEmail(rawContactId, "account1@email.com"); + + final ContentValues v1 = cv(Email.ADDRESS, "account1@testemail.com"); + final ContentValues v2 = cv(Email.ADDRESS, "account1@email.com"); + + Uri filterUri1 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("acc") + .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, account.name) + .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE, account.type) + .build(); + assertStoredValuesOrderly(filterUri1, v2, v1); + } + + /** + * Test "default" emails are sorted above emails used last. + */ + public void testEmailFilterDefaultOverUsageSort() { + final long rawContactId = createRawContact(ACCOUNT_1); + final Uri emailUri1 = insertEmail(rawContactId, "account1@testemail.com"); + final Uri emailUri2 = insertEmail(rawContactId, "account2@testemail.com"); + insertEmail(rawContactId, "account3@testemail.com", true); + + // Update account1 and account 2 to have higher usage. + updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, emailUri1); + updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, emailUri1); + updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, emailUri2); + + final ContentValues v1 = cv(Email.ADDRESS, "account1@testemail.com"); + final ContentValues v2 = cv(Email.ADDRESS, "account2@testemail.com"); + final ContentValues v3 = cv(Email.ADDRESS, "account3@testemail.com"); + + // Test that account 3 is first even though account 1 and 2 have higher usage. + Uri filterUri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "acc"); + assertStoredValuesOrderly(filterUri, v3, v1, v2); + } + /** Tests {@link DataUsageFeedback} correctly promotes a data row instead of a raw contact. */ public void testEmailFilterSortOrderWithFeedback() { long rawContactId1 = createRawContact(); @@ -7207,6 +7249,12 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { } } + private void updateDataUsageFeedback(String usageType, Uri resultUri) { + final long id = ContentUris.parseId(resultUri); + final boolean successful = updateDataUsageFeedback(usageType, id) > 0; + assertTrue(successful); + } + private int updateDataUsageFeedback(String usageType, long... ids) { final StringBuilder idList = new StringBuilder(); for (long id : ids) { diff --git a/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java b/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java index 7769b492..e09e59ea 100644 --- a/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java +++ b/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java @@ -18,14 +18,16 @@ package com.android.providers.contacts.util; import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns; import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses; +import static com.android.providers.contacts.util.DbQueryUtils.escapeLikeValue; import android.content.ContentValues; -import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import com.android.common.content.ProjectionMap; import com.android.providers.contacts.EvenMoreAsserts; +import junit.framework.TestCase; + /** * Unit tests for the {@link DbQueryUtils} class. * Run the test like this: @@ -34,7 +36,7 @@ import com.android.providers.contacts.EvenMoreAsserts; * </code> */ @SmallTest -public class DBQueryUtilsTest extends AndroidTestCase { +public class DBQueryUtilsTest extends TestCase { public void testGetEqualityClause() { assertEquals("(foo = 'bar')", DbQueryUtils.getEqualityClause("foo", "bar")); assertEquals("(foo = 2)", DbQueryUtils.getEqualityClause("foo", 2)); @@ -71,4 +73,30 @@ public class DBQueryUtilsTest extends AndroidTestCase { } }); } + + public void testEscapeLikeValuesEscapesUnderscores() { + StringBuilder sb = new StringBuilder(); + DbQueryUtils.escapeLikeValue(sb, "my_test_string", '\\'); + assertEquals("my\\_test\\_string", sb.toString()); + + sb = new StringBuilder(); + DbQueryUtils.escapeLikeValue(sb, "_test_", '\\'); + assertEquals("\\_test\\_", sb.toString()); + } + + public void testEscapeLikeValuesEscapesPercents() { + 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", '\\'); + assertEquals("my test string", sb.toString()); + } } |