summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteve Kondik <shade@chemlab.org>2013-02-12 22:43:54 -0800
committerSteve Kondik <shade@chemlab.org>2013-02-12 22:43:54 -0800
commite5adf1f13c58f2d14dfbd1a179576ee5b269602f (patch)
treea5649d6ccce8838f6e951e88927689a819d153d4
parentf0fdf304d6a6b1616873004af8b6d6094fa47fed (diff)
parentdacd5de146b413de86d38b6f56a3fe0b2af4b155 (diff)
downloadandroid_packages_providers_ContactsProvider-e5adf1f13c58f2d14dfbd1a179576ee5b269602f.tar.gz
android_packages_providers_ContactsProvider-e5adf1f13c58f2d14dfbd1a179576ee5b269602f.tar.bz2
android_packages_providers_ContactsProvider-e5adf1f13c58f2d14dfbd1a179576ee5b269602f.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
-rw-r--r--src/com/android/providers/contacts/ContactsDatabaseHelper.java84
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java38
-rw-r--r--src/com/android/providers/contacts/SearchIndexManager.java2
-rw-r--r--src/com/android/providers/contacts/util/DbQueryUtils.java27
-rw-r--r--tests/src/com/android/providers/contacts/ContactsProvider2Test.java62
-rw-r--r--tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java32
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());
+ }
}